Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fvar: make explicit support for duplicate axis tags #680

Open
tiroj opened this issue Nov 9, 2020 · 22 comments
Open

fvar: make explicit support for duplicate axis tags #680

tiroj opened this issue Nov 9, 2020 · 22 comments

Comments

@tiroj
Copy link

tiroj commented Nov 9, 2020

cf. Twitter thread: https://twitter.com/ArrowType/status/1325648820101853184?s=20

Existing (most? all?) fvar software implementations allow for multiple axs to have a common tag, which is used in fonts to apply non-linear/higher-order interpolation. To ensure compatibility in this regard, the spec should make explicit that this is intended and correct, and provide any relevant recommendations for fonts (e.g. setting flags to hide all but one of the duplicate tags).


Document Details

Do not edit this section. It is required for docs.microsoft.com ➟ GitHub issue linking.

@PeterCon PeterCon self-assigned this Nov 9, 2020
@PeterCon
Copy link
Collaborator

PeterCon commented Nov 9, 2020

Presumably there should be at least a strong recommendation (or requirement?) that, when an axis tag is repeated, the axis min/default/max values be the same. (If not a requirement, then there should also be guidance that implementations may use the values for the first axis record?)

@davelab6
Copy link

A possible argument against doing this, is in the following metaphor.

Assume that rather than interpolating - mixing - glyphs, we are playing with our childhood painting kit. If we want to blend on our palette, green, orange, and purple, what we want is to pull out paint tubes labelled as what they are:

Blue + Yellow = green
Yellow + Red = orange
Red + Blue = purple

What we don't want is to pull out 6 colors, with duplicate names:

Blue (renamed to green) + Yellow (renamed to green) = green
Yellow (renamed to orange) + Red (renamed to orange) = orange
Red (renamed to purple) + Blue (renamed to purple) = purple

@PeterCon
Copy link
Collaborator

Not sure if I fully understand your metaphor.

The idea of HOI is that, for some axis 'abcd', instead of describing the variation of a target item—e.g., a contour point of some outline—using a single linearly-interpolated delta from (normalized coordinates) 0 to 1; instead you break it into two or more deltas that apply over the same range 0 to 1 such that the combination of deltas result in a non-linear trajectory for the variation of that contour point. These deltas are linked to the same axis both (i) so that the input coordinate is the same for each, but also (ii) because those deltas aren't independently interesting.

IOW, (if I'm understanding your metaphor correctly), it's not like there are tubes of blue and yellow paint that are independently useful. It's more like there's a tube of colourless compound X and another tube of colourless compound Y and they always have to be mixed in equal proportions (like epoxy resin and catalyst), but by varying the combined amounts of the two compounds you can produce a continuum of colours between green and blue.

@tiroj
Copy link
Author

tiroj commented Nov 11, 2020

If I am understanding correctly, Peter, you are suggesting that normalised min/default/max values should be the same in axes with shared tags. I think that is probably the case, although would really like some input from someone actually making this kind of font, preferably from the Underware guys if they feel like sharing more information about their implementations.

Another thing I have been pondering is whether we should specify a limit on the number of axes that can share the same tag. I think, if I am understanding HOI correctly, that it uses two axes with a common tag. Are there cases in which more than two axes with a common tag might be used?

@PeterCon
Copy link
Collaborator

PeterCon commented Nov 11, 2020

I would also like to see a clear explanation. The bits that I've gleaned from stuff Underware guys have share is that you have multiple delta sets for the same axis and over the same region, with the number of extra axis records / delta sets determining the order of non-linearity. Info that was in an arrowtype repo recently (but now seems to have gone!) was the best explanation I had seen so far, but still left crucial things to be filled in by the reader. This tweet from José Miguel Solé Bruning illustrates is helpful: you can see there are three deltas for each contour point that combine together so that the rectangle rotates pretty smoothly without the odd, undesirable side effects you might otherwise get. But watch the video and try to picture in your mind what one of the delta sets alone would produce: I suspect it would be junk.


Correction: the axis records have the same min/default/max, but the "regions" over which delta sets operate are in an n-dimensional space determined by the axis records, not the axis tags. And the region for a delta set can be defined using any combination of the axes.

So, suppose a font has two axis records for one axis tag 'abcd', with the same min/default/max. Since there are two axis records, the variation region is two dimensional. Considering just one quadrant of the two-axis min/default/max space, (and ignoring any "intermediates"), there can be delta sets for regions defined by (0, 1), (1, 0) and (1, 1).

Also—and I think this is probably key—because each axis of this region is driven by the same axis tag—hence the same input "user" coordinate—, the scalar coefficient for the (1, 1) region will be the square of the scalar coefficient for the other delta sets.

There's one axis tag, so one input user coordinate; let's call the corresponding normalized coordinate t. Because there are two axis records, the variation region is 2-dimensional; let's call those two axes X and Y. There can be delta sets for three regions defined by (X,Y) = (0,1), (1,0), and (1,1). Let's call these delta sets D1, D2, and D2, and the delta values for a particular contour point d1, d2, and d3. So, for a given user coordinate (instance) value between default and max, with normalized value t, the combined delta will be

d1 × t + d2 × t + d3 ×

So, we've introduced a quadratic element in the scalar calculations. If the axis had been repeated in three axis records, then we could introduce a delta set for a region defined by (X,Y,Z) = (1, 1, 1), and the scalars would include a cubic element , which is applied to the deltas for that region.

@PeterCon
Copy link
Collaborator

It's also worth pointing out that, while you repeat axis record for the same tag N times to get Nth-order non-linear interpolation, that doesn't imply that you necessarily need a geometric explosion of delta sets, 2N - 1. Polynomial equations with limited order may be able to interpolate a function with small precision. To approximate the sine function, a fifth order polynomial with only three terms (i.e., only three delta sets) approximates to several decimal places.

sin x ≈ x - x³/3! + x⁵/5!

with an error ≤ x⁶/720.

In other words, if I've understood it all correctly, it should be possible to implement very smooth rotations with an axis tag repeated in five axis records and just three delta sets for (1,0,0,0,0), (1,1,1,0,0), (1,1,1,1,1).

Of course, the exercise for the reader is figuring out what the deltas should look like!

@PeterCon
Copy link
Collaborator

PeterCon commented Nov 11, 2020

Improving my revision of Dave's paint metaphor.

You've probably seen epoxy glue kits that have two tubes, resin and catalyst, combined in a syringe such that, as you press in the plunger, equal amounts of resin and catalyst are mixed to form the glue. The "HOI" technique is similar to one of those, except there can be multiple tubes, and the proportions aren't necessarily equal, or even linear, but can also be exponential.

So, instead of a blue tube and a yellow tube, you have tubes of colourless (or ugly brownish-gray—something not meant to be used on its own) compound loaded into a special syringe. As you squeeze, the compounds are combined in exponential proportions to one another. So, 1 unit of compound A is combined with 1 unit of compound B; 2 units of A with 4 units (or 8, 16...) units of B, etc. Depending on how much you squeeze, you'll get paint anywhere from blue to green.

The number of tubes and the highest exponent you can have is determined by the number of times the axis tag is repeated in axis records. For N records, the highest exponent would be N. You can have 2N-1 tubes, but tubes that are combined using the same exponent are always combined in a linear proportion to one another, and so those tubes could be pre-mixed. In other words, there's really no need to have more than N tubes. And you don't have to have tubes using all possible exponents if not needed.

At least, that's the understanding I've arrived at.

@PeterCon
Copy link
Collaborator

PeterCon commented Nov 11, 2020

Having made sense of NLI, or "HOI", which is the use case for having duplicate axis tags, I think there's a key open issue to be resolved, and so think it's premature to add this into OT 1.8.4 at the last minute (three days left in the beta review). So, I'm marking this for the future: it would be good to include in OT 1.9.

The open questions have to do with whether the min/default/max values in the duplicated records must be the same or can be different, and if the latter, what is expected.

I think it might be useful to allow records to have different min or max values, as it introduces one more way to customize how scalars will get computed (see below). Potentially also the default value, though that would have an issue that, for glyf/gvar fonts, the default instance would no longer display the same in legacy and VF-aware implementations.

But there would need to be clarity around what input values are accepted: since we assume only one axis record isn't hidden, we can assume those min/max values would be used in a UI. But if a hidden duplicate record has a lower min or greater max, would that mean that a larger range for that axis tag could be utilized programmatically? Or should the range be clamped to the first record? Or should the usable range, including in UI, be from the lowest min to the highest max?

Then there's the question of whether having different values would work in existing implementations. We know that NLI using duplicated axis records works in existing software implementations; but has that only been tested with fonts in which the duplicate axis records have exactly the same min/default/max values? I think this should be tested in some existing implementations before determining whether it should be permitted.

What would the affect be of having different min or max values? Let me explain.

Let's say an axis tag 'abcd' is duplicated in 2 records, and that the min/default/max values are the same. Ignoring the min side of the space, and ignoring "intermediates", deltas can be defined for three regions defined by tuples (0,1), (1,0) and (1,1). Suppose there are delta sets for each, with particular deltas for one particular item d₁, d₂, and d₃. For a given input user coordinate T for axis 'abcd', a normalized coordinate t is calculated as t = (T - default) / (max - default). So, given the way that the delta scalars are computed, the combined delta dtotal will be

dtotal = d₁t + d₂t + d₃

In this case, the deltas d₁ and d₂ could actually be combined into one delta set since they are always used in equal proportions.

But, now, suppose the second axis record has a lower max value than the first axis record. Let's refer to these as max₁ and max₂. Now for a given user coordinate T for axis 'abcd', different normalized coordinates will be calculated for the two axis records:

t₁ = (T - default) / (max₁ - default)
t₂ = (T - default) / (max₂ - default)

Now the combined delta would be computed as follows:

dtotal = d₁t₁ + d₂t₂ + d₃t₁t₂

There is still a non-linear effect because of the term with t₁t₂. But now d₁ and d₂ are combined not in equal proportions but in geometric proportions. (t₁ / t₂ = (max₁ - default) / (max₂ - default).) So, in this case, d₁ and d₂ could not be pre-combined.

Also note a second effect: t₂ will reach 1 sooner than t₁ — specifically, for any T in the range [max₂, max₁], t₂ will equal 1.

@PeterCon
Copy link
Collaborator

PeterCon commented Nov 12, 2020

Btw, some math relevant to all this is Taylor's theorem, which has to do with use of polynomial equations (kth order polynomial has terms involving x, x2... xk) to approximate smooth functions (k-times differentiable), and being able to determine the maximal error. The example I gave above of approximating a sine function (through sin(x) = 0) used a 5th order Taylor polynomial.

@underware
Copy link

I totally agree that HOI related issues are too premature to be added to OT 1.8.4.
It's much better to keep this for OT 1.9.

@bungeman
Copy link

I wrote an explainer at https://bungeman.github.io/hoi.html so that I would have a more practical understanding of basic HOI (or at least one way to do it, where all the axes used have the same min, max, and default). While I put in a footnote about using axes with different min, max, and default, maybe I should go write down some of that too. In any event, hopefully it can provide a more concrete understanding of what can be done so we all understand some of the benefits and limitations.

@PeterCon
Copy link
Collaborator

It's much better to keep this for OT 1.9.

Understand that OT 1.9 likely won't be too far out—I'd guess probably within the next six months. So, makes sense to start engaging on spec'ing.

@PeterCon
Copy link
Collaborator

PeterCon commented Nov 18, 2020

The key to NLI is in the scalar function, how "axes" are counted, and the fact that there's a distinction between an external API perspective and the perspective of the 'fvar'/'gvar' interface that's relevant for scalar calculation.

I was looking at whether the spec is clear on how axes are counted for scalar calculation, and it basically is clear. Quoting from algorithm for calculating scalars:

/* for each axis, calculate a per-axis scalar AS */
(for i = 0; i < axisCount; i++)

Now, it's possible that some implementation might check for distinct axis tags and ignore duplication of axis records in the scalar calculation: just not calculate any axis scalar for those, or just count the axis scalar as 1. It's not likely implementations would do something like this, but it is possible since the spec isn't clear on that point.

So, if NLI is something to be supported, it wouldn't hurt for the spec to make clear in this section of the otvaroverview chapter that it iterates over all axis records, using the normalized coordinates derived from the data in each given axis record.

@PeterCon
Copy link
Collaborator

@bungeman wrote:

I wrote an explainer...

I've also written up an explainer, Understanding Non-Linear Interpolation in OpenType Variable Fonts. I've approached things a bit differently, though I suspect we're basically on the same page.

@underware
Copy link

I would like to propose the clarification of terminology. The specific implementation of higher order interpolation (or curved interpolation or non-linear interpolation) in OpenType Variable Fonts as discussed here was introduced to the type design world at TypoLabs 2018 under the term HOI. Therefore I would like to propose that whenever we talk about this specific concept and design, we should use the term HOI. Everything else is only confusing. Just consider the following: we all use the term Bézier-curve when we talk about the implementation of drawing static outlines, based on non linear interpolation. Wouldn't it be strange to (re)name this NLC (Non-Linear Contours). Both terms, Bézier-curve and HOI, are names for a specific implementation of higher order interpolations within type design.

@anthrotype
Copy link

FWIW I also second the term "higher-order interpolation" (HOI), I find it clearer than "non-linear interpolation".

@underware
Copy link

underware commented Nov 18, 2020

Great. We should also distinguish between math & application of the math.
Higher-order interpolation is a term for a certain math formula.
HOI is a term for a concept/design to make this math 'available and/or apply' for OpenType Variable Fonts.

@Lorp
Copy link

Lorp commented Jan 20, 2021

Regarding the matching of min/default/max between same-named HOI axes, the main issue seems to be that instance selection in existing APIs is designed for a key-value syntax. To my knowledge, no current API offers direct access to axis settings via axis indices (i.e. axes[0]=100, axes[1]=300, etc., whether with user values or normalized values). Without new APIs, therefore, one can only set all the axes tagged 'wght' to a single value. If min/default/max are different in same-named HOI axes, then clamping and remapping will likely occur in some of the axes. This seems unlikely to be useful and implementations are unlikely to have been tested. Nevertheless it seems prudent to allow some time for experimentation and discussion before deciding if these different min/default/max values should be explicitly allowed or disallowed.

@PeterCon
Copy link
Collaborator

@Lorp: In DWrite, the IDWriteFontResource::CreateFontFace method takes an array of axis values. However, each axis value is expressed using a DWRITE_FONT_AXIS_VALUE structure, which takes a tag and a value. At a minimum, that means that the order of elements in the array doesn't necessarily need to match the fvar tag order. But the documentation also states, "The array should be the size ... indicated by the fontAxisValueCount argument." So, it's really unclear (without some experimentation or code inspection) what it does with duplicated tags.

@Lorp
Copy link

Lorp commented Jan 20, 2021

Thanks Peter. If anyone wants to experiment using samsa-core.js, then you can create instances either by using fvs structures (akin to CSS font-variation-settings or indeed a DWRITE_FONT_AXIS_VALUE array), or by using tuple arrays which set normalized axis values by axis index. Ping me for more details if interested — the GUI app does not allow this.

@PeterCon
Copy link
Collaborator

Regarding the name "HOI": This has become familiar since the technique was introduced a few years ago, and so I don't mind using it. But let's understand: it doesn't clearly reflect how the mechanism works, and isn't quite accurate.

Individual deltas in the gvar table still represent linear vectors.

The deltas get scaled using a scalar coefficient. The scalar is derived from "user" instance values.

On a per-axis basis, scalars are derived from the "user" value using linear interpolation.

However, the overall scalar applied to a delta combines the per-axis scalars, and it is here that duplication of axis tags in the fvar axis records leads to non-linearity—higher-order terms—in the overall scalar calculation.

Variation of contour control points along non-linear paths is achieved by combining two or more non-parallel delta vectors associated with different regions of the variation space that will have distinct scalar functions, at least one of which must have higher-order terms.

A more accurate description for the mechanism would be "non-linear variation of control points by means of higher-order terms in scalar calculation". But that's a mouthful.

For font items that are scalar values rather than design-grid positions (e.g., an advance width value), things are a tad simpler since two separate deltas aren't required to get non-linearity. If a delta is associated with a region that involves duplicated axis tags, the associated scalar function will have higher-order terms. However, this is less interesting: a similar effect on such values is obtained from the 'avar' table.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants