harfbuzz / boring-expansion-spec

Better-Engineered Font Formats; Part 1. Boring Expansion
79 stars 8 forks source link

[`avar`2] Enable non-orthogonal axis distortions #14

Closed behdad closed 1 year ago

behdad commented 2 years ago

Update: the aim of this proposal is just to enable non-orthogonal axes distortions. The higher-order interpolation will come by way of ItemVariationStore upgrades, via eg. https://github.com/be-fonts/boring-expansion-spec/issues/17

Update: Settled on the following format: If version is 0x00020000, the format1-like struct is followed by the following fields:

struct avar2 {
  Offset32To<DeltaSetIndexMap> axisIdxMap;
  Offset32To<ItemVariationStore> varStore;
};
behdad commented 2 years ago

cc @rsheeter @lorp @twardoch

behdad commented 2 years ago

There's no further requirements on the mapping. The mapping is NOT required to be reversible.

The pre-normalization/post-normalization to be determined / documented.

Lorp commented 2 years ago

I suggested using varation math on avar mappings in a call to @twardoch a few months ago. I should have written it up! I think we’ll need a new ItemVariationStore format, since we’re talking about deltas in n-space, not the 1-space of MVAR/HVAR/VVAR or the 2-space of gvar.

So if I understand your proposal correctly, these deltas are n-D vectors in normalized variation space, with linear response when 1 axis is non-default, quadratic response when 2 axes are non-default, cubic response when 3 axes are non-default, etc. As you say, normalization is to be determined. What about behaviour if the vector causes a transition across 0 on any axis? Maybe that’s fine.

behdad commented 2 years ago

On further thought:

Let's use a VarIdxMap, aka DeltaSetIndexMap for the (optional!) mapping. The mapping logic would be different from that of HVAR table in that, unlike in HVAR, the last entry is not repeated ad infinitum. Entries not in the mapping get a varidx of NO_VARIATION (aka 0xFFFFFFFF; or just 0 variation).

This also removes the 16bit limit on the number of axes, conceptually.

I also wanted to say let's borrow from how we extended COLRv1, and instead of going to major 2, go to major 1 minor 1, such that older implementations can use older representation still. While we can do that, it has some inconvenience because the old representation has to be parsed before new representation can be found. Still, I'm supportive of possibly doing it that way.

struct avar2 {
  uint16 major; // set to 2
  uint16 minor; // set to 0
  Offset32To<ItemVariationStore> varStore; // VarStore mapping axes from unwarped user-space to warped design-space
  Offset32To<DeltaSetIndexMap> map; // Optional VarIdxMap mapping axis index into VarIdx to be looked up in VarStore
};
behdad commented 2 years ago

The pre-normalization/post-normalization to be determined / documented.

Axis values are normalized to -1,+1 based on fvar axis min,default,max; then they are transformed via avar1 or avar2. Finally they are clamped to -1,+1 again. Some implementations then convert them to F2DOT14 at this point.

There is a functional requirement that if all input axes are at their fvar default position, then all output axes are at zero. But there's no mechanism to try to enforce that. OT1.8 made such an assumption and thought it enforced it, but failed at it.

behdad commented 2 years ago

There is a functional requirement that if all input axes are at their fvar default position, then all output axes are at zero.

Or rather, the functional requirement is that: if all input axes are at their fvar default position, the output should be identical to situation that no deltas are applied. That is, as if all output axes are zero...

behdad commented 2 years ago

I suggested using varation math on avar mappings in a call to @twardoch a few months ago. I should have written it up! I think we’ll need a new ItemVariationStore format, since we’re talking about deltas in n-space, not the 1-space of MVAR/HVAR/VVAR or the 2-space of gvar.

No no no. gvar is different in that it encodes spatial deltas.

ItemVariationStore always encodes 1-dimensional scalar deltas. That doesn't have to change. We encode each axes deltas separately.

So if I understand your proposal correctly, these deltas are n-D vectors in normalized variation space,

Except that each dimension's deltas is separately encoded.

with linear response when 1 axis is non-default, quadratic response when 2 axes are non-default, cubic response when 3 axes are non-default, etc.

Correct. Same non-linearity that we enable across the system.

As you say, normalization is to be determined. What about behaviour if the vector causes a transition across 0 on any axis? Maybe that’s fine.

I tried to address that in my previous two comments.

Lorp commented 2 years ago

So if I understand your proposal correctly, these deltas are n-D vectors in normalized variation space,

Except that each dimension's deltas is separately encoded.

How can you keep axes separate if an arbitrary location in n-space maps to another arbitrary location in n-space?

behdad commented 2 years ago

How can you keep axes separate if an arbitrary location in n-space maps to another arbitrary location in n-space?

Each output axis is a piecewise-linear function of all input axes.

Imagine a point rotating in HOI. Both output X and Y are functions of both input X and Y, but you can represent/encode them separately.

Lorp commented 2 years ago

I don’t follow you. With 2 fvar axes both acting on avar, a ~point~ designspace location is acted on by the sum of axis0 × vector0 (linear), axis1 × vector1 (linear), and axis0 × axis1 × vector2 (quadratic), where the vectors are 2-D.

behdad commented 2 years ago

Now I don't follow you either. Let's get on VC.

behdad commented 2 years ago

There's no requirement that if all input axes are at default, then all output axes must be at zero. This would allow moving default freely. See: https://github.com/be-fonts/boring-expansion-spec/issues/17#issuecomment-924976649

We can declare fonts with avar2 requiring variations processing. I want to think more about it though.

davelab6 commented 2 years ago

Will https://github.com/google/fonts/issues/2981#issuecomment-927606320 be supported?

behdad commented 2 years ago

Will google/fonts#2981 (comment) be supported?

Yes. No such constraints.

davelab6 commented 2 years ago

Related issues/links:

davelab6 commented 2 years ago

Apple's VF version of SF Pro just dropped last week, with similar VF axes to Roboto Serif, https://v-fonts.com/fonts/sf-pro

The grade axis isn't "safe" with the weight axis; a good case study for avar2 :)

Screen Shot 2022-05-05 at 1 39 19 PM
davelab6 commented 2 years ago

Nice comments from @tiroj at https://twitter.com/TiroTypeworks/status/1524085630367506432

behdad commented 2 years ago

After some more thinking I decided it might be better to retain avar1 fields that do a linear per-axis mapping first, then do the full-matrix mapping. I also sketched a simple designspace-file xml that implements wght axis HOI: https://gist.github.com/behdad/7e74e0970280a0cf621671af6f0fa749#file-avar1-1-designspace-sketch

behdad commented 2 years ago

So this is the new avar table format I'm proposing, in fonttools otData format:

        ('avar', [
                ('Version', 'Version', None, None, 'Version of the avar table- 0x00010000 or 0x00020000'),
                ('uint16', 'Reserved', None, None, 'Permanently reserved; set to zero'),
                ('uint16', 'AxisCount', None, None, 'The number of variation axes for this font. This must be the same number as axisCount in the "fvar" table'),
                ('AxisSegmentMap', 'AxisSegmentMap', 'AxisCount', 0, 'The segment maps array — one segment map for each axis, in the order of axes specified in the "fvar" table'),
                ('LOffset', 'VarIdxMap', None, 'Version >= 0x00020000', ''),
                ('LOffset', 'VarStore', None, 'Version >= 0x00020000', ''),
        ]),
behdad commented 2 years ago

('LOffset', 'VarIdxMap', None, 'Version >= 0x00020000', ''),

Note this VarIdxMap is called DeltaSetIndexMap in the OT spec.

behdad commented 2 years ago

Here's sample script to generate avar2 table from RobotoFlex relationships between wght/wdth/opsz axes and its parametric axes: https://github.com/behdad/robotoflex-avar2/blob/main/robotoflex-avar2.py

behdad commented 2 years ago

The code for this is in HarfBuzz 5.0.0, and a FreeType patch is in:

https://gitlab.freedesktop.org/freetype/freetype/-/merge_requests/188

A sample font file (RobotoFlex) is in:

https://drive.google.com/file/d/1fcb9pyt6P7tDOnMf7AaTK4B7Apt4P1mK/view

The wght, wdth, and opsz axes are routed through avar2.

behdad commented 2 years ago

I'm drafting a spec for this here: https://github.com/harfbuzz/boring-expansion-spec/blob/avar2/avar2.md

behdad commented 1 year ago

This is complete.