googlefonts/amstelvar-avar2

implementing fences

gferreira opened this issue · 18 comments

TL;DR: we have fences working for the default, but not for blended extrema.

I've been trying to implement fences in avar2 for AmstelvarA2.

some background on what fences are: Designspace fences (Mom, You Sure Can Rehydrate a Pizza)

Fences keep users to “safe” or “approved” zones of the designspace.

Users who choose axis settings that are beyond the fence will be brought back inside the safe zone.

in other words, fences restrict the scope of parametric axes (ex: XOPQ XTRA YOPQ XTSP) depending on the current location in blended axes (ex: wght wdth).

the min/max values for each parametric axis, at each blended extreme for wght and wdth (duovars), were defined by @dberlow as a RTF document here:

https://github.com/googlefonts/amstelvar-avar2/blob/main/Source/Roman/Amstelvar%20avar2%20DSL.1.rtf

the relevant fences data was then extracted into this JSON file:

https://github.com/googlefonts/amstelvar-avar2/blob/main/Source/Roman/fences.json

finally, this data is used to add new mappings to the avar2 designspace file using the fontTools.designspaceLib API.

for example, at wght400 we would like to limit parametric axes to the following min/max values:

"wght400": {
  "XTRA": {
    "min": 300,
    "max": 450
  },
  "XOPQ": {
    "min": 72,
    "max": 110
  },
  "YOPQ": {
    "min": 50,
    "max": 75
  },
  "XTSP": {
    "min": -60,
    "max": 50
  }
}

in designspace XML this becomes the following: (only XTRA shown)

<mapping>
  <input>
    <dimension name="Weight" xvalue="400"/>
    <dimension name="XTRA" xvalue="208"/>
  </input>
  <output>
    <dimension name="XTRA" xvalue="300"/>
  </output>
</mapping>
<mapping>
  <input>
    <dimension name="Weight" xvalue="400"/>
    <dimension name="XTRA" xvalue="508"/>
  </input>
  <output>
    <dimension name="XTRA" xvalue="450"/>
  </output>
</mapping>

in plain language: when the weight value is 400, instead of going all the way down to 208, the user can only go until 300; and instead of going all the way up to 508, the user can only go until 450.

such fence mappings were added for the default and for all blended extrema (wght200 wght800 wdth85 wdth125), for parametric axes XTRA XOPQ YOPQ XTSP only. the resulting designspace and variable font are here:

https://github.com/googlefonts/amstelvar-avar2/blob/main/Source/Roman/AmstelvarA2-Roman_avar2_fences.designspace
http://github.com/googlefonts/amstelvar-avar2/blob/main/fonts/AsciiAlpha/AmstelvarA2-Roman_avar2_fences.ttf

the fences effect seems to be working at the default location (wght400 wdth100): the scope of XOPQ XTRA YOPQ XTSP is limited when these parametric axes are used alone or in combination with each other. fences for the blended extrema, however, are not working as intended; the fenced values seem to be morphing back to the default (?).

a second designspace and variable font were created containing fences for only one blended extreme (wght200):

https://github.com/googlefonts/amstelvar-avar2/blob/main/Source/Roman/AmstelvarA2-Roman_avar2_fences-wght200.designspace
http://github.com/googlefonts/amstelvar-avar2/blob/main/fonts/AsciiAlpha/AmstelvarA2-Roman_avar2_fences-wght200.ttf

also here fences for the blended extreme wght200 are not working as expected, showing the same behavior as in the previous attempt (above).


some questions at this point:

  • does avar2 allow adding fences for blended extrema?
  • is the above designspace syntax for fences correct?

cc @Lorp @behdad — many thanks in advance for any feedback you can give on this.

Does it make sense to have a none-user-acessible axis as input for an avar2 mapping?

As I understand it (and I very well be wrong about this): You need to check the entire designspace for places where XTRA would exceed the limit and add mappings to pull it back. So if Weight=-1 + Width=-1 will push XTRA too low, you add a mapping for that spot (and potentially some intermediate place to limit the influence of that extra mapping.)

the parametric axes are the ones we're trying to limit, so they are not non-user-acessible – there is an input, and we would like to modify the output.

the current fences logic is working for the default style, as far as I can tell. we are trying to find out how to make it work for blended extrema (duovars), if that's possible at all.

Lorp commented

First, although the spec doesn’t require it, I recommend being explicit with your mappings when any of the axes involved in the mapping are at default. This makes it clearer to visualize the "distortion vector" that is represented. So:

<mapping>
  <input>
    <dimension name="Weight" xvalue="400"/>
    <dimension name="XTRA" xvalue="208"/>
  </input>
  <output>
    <dimension name="Weight" xvalue="400"/>
    <dimension name="XTRA" xvalue="300"/>
  </output>
</mapping>
<mapping>
  <input>
    <dimension name="Weight" xvalue="400"/>
    <dimension name="XTRA" xvalue="508"/>
  </input>
  <output>
    <dimension name="Weight" xvalue="400"/>
    <dimension name="XTRA" xvalue="450"/>
  </output>
</mapping>

Second, to implement the requirement "at wght400 we would like to limit parametric axes to the following min/max values", I will assume that a) XTRA is not to be distorted at wght min and wght max, and b) distortion should be interpolated between regular wght and the extremes. So the next thing I would do is make sure Weight max and Weight min are unaffected by what is happening at default. The following 4 "null" mappings will keep those locations "pinned" to their original locations:

<!-- wght min, XTRA min -->
<mapping>
  <input>
    <dimension name="Weight" xvalue="200"/>
    <dimension name="XTRA" xvalue="208"/>
  </input>
  <output>
    <dimension name="Weight" xvalue="200"/>
    <dimension name="XTRA" xvalue="208"/>
  </output>
</mapping>

<!-- wght min, XTRA max -->
<mapping>
  <input>
    <dimension name="Weight" xvalue="200"/>
    <dimension name="XTRA" xvalue="508"/>
  </input>
  <output>
    <dimension name="Weight" xvalue="200"/>
    <dimension name="XTRA" xvalue="508"/>
  </output>
</mapping>

<!-- wght max, XTRA min -->
<mapping>
  <input>
    <dimension name="Weight" xvalue="800"/>
    <dimension name="XTRA" xvalue="208"/>
  </input>
  <output>
    <dimension name="Weight" xvalue="800"/>
    <dimension name="XTRA" xvalue="208"/>
  </output>
</mapping>

<!-- wght max, XTRA max -->
<mapping>
  <input>
    <dimension name="Weight" xvalue="800"/>
    <dimension name="XTRA" xvalue="508"/>
  </input>
  <output>
    <dimension name="Weight" xvalue="800"/>
    <dimension name="XTRA" xvalue="508"/>
  </output>
</mapping>

To answer David’s question "does that fencing modify the parametric range of these four parametric axes in the "corners", the answer is yes, and therefore explicit mappings (whether "null" or not) are required everywhere that you want to override the effect of those initial two mappings.

For the case where "two parametric axes, both effecting y values, compete for less space than is available", avar2 cannot directly represent a condition based on where one or more parametric axes end up; all such distortion must be represented via the input axes. In other words, there is only one mappings layer.

Does this clarify things?

In other words, there is only one mappings layer.

Correct. We thought about adding multiple rounds of mapping, but the ship has sailed for now. Let's see if we can make do with the current design.

I think some automation in producing the avar2 mapping XML is needed.

In other words, there is only one mappings layer.

Correct. We thought about adding multiple rounds of mapping

That seems to preclude shipping the 3 level "Atomic Element, Deep Component, Character Glyph" source model in Fontra, in OT2 files, if the DCs are mapped to AEs and then CGs are mapped to DCs?

In other words, there is only one mappings layer.

Correct. We thought about adding multiple rounds of mapping

That seems to preclude shipping the 3 level "Atomic Element, Deep Component, Character Glyph" source model in Fontra, in OT2 files, if the DCs are mapped to AEs and then CGs are mapped to DCs?

No, thats VARC, which is a different thing, and it's recursive so unlimited number of layers are supported. In avar2, there's just a one-time VarStore mapping.

In other words, there is only one mappings layer.

Correct. We thought about adding multiple rounds of mapping

That seems to preclude shipping the 3 level "Atomic Element, Deep Component, Character Glyph" source model in Fontra, in OT2 files, if the DCs are mapped to AEs and then CGs are mapped to DCs?

No, thats VARC, which is a different thing, and it's recursive so unlimited number of layers are supported. In avar2, there's just a one-time VarStore mapping.

I'm thinking about combining varc and avar2, though. Can varc be used to replicate avar2 functionality without an actual avar2 table?

Can varc be used to replicate avar2 functionality without an actual avar2 table?

That's an interesting thought. Yes, I think it would be possible. Just that the mapping should be replicated for all glyphs then...

@behdad good thing we can have 16 million of them then ;)

That still won't deliver the reflexivity of absolute values that I understand @dberlow desires, but if we can fence the poles this way, that's good news.

@dberlow given the opportunity for reuse you proposed in votf/vuid in 2016, I wonder if it would be good to make a complete development cycle on just numerals in both avar2 projects, to fully explore how both varc and avar2 can be brought to life in these projects.

On votf/vuid and axar2 projects, we can certainly take the Roboto project in that direction with little effort. googlefonts/roboto-flex-avar2#16.

Lorp commented

Regarding my XML code example, I’ll note that I did not mean to imply there would be no further adjustments to XTRA in a real font. The suggested code was purely to implement the requirement previously stated in the GitHub issue, with my (tentative) working method that I would try to localise the effect of that particular warp, then test it.

In the unfenced parametric use case (i.e. no restrictions on users tweaking parametric values), I have been assuming that the axes involved in <input> are disjoint from the set involved in <output>.

When one or more axes is involved in both <input> and <output> of a mapping, my mental model is a rubber sheet on top of a cork board: I stick a pin into the rubber sheet, stretch that position on the sheet to a new position on the board, pin it there, then see how that affects the font. In the example case, after testing that these first mappings worked as expected, my next step (depending on my particular mental model of the distortion) would likely be to work out what should happen at wght and XTRA extremes, then test those mappings work as expected.

When we get more practised (or there’s a GUI by @schriftgestalt or someone else to help us), there will perhaps be no need for such a steo-by-step approach.

I chatted with @behdad today and looked at the fences.json files; he suggested you'll need to add all 8 27 corners of the wght-wdth-opsz cuboid for each parametric axis, as these currently seem missing.

Lorp commented

The documentation and the reorg is appreciated.

I’m not sure exactly how the fences.json file is interpreted, but I wanted to make sure it was well understood that to implement any fence you need to do at least the following:

  1. bring the excessive values back to the fence;
  2. define where the fence starts, in order to stop values within the fence being undesirably rescaled.

So even in the 1D case, we need two mappings for each fence.

For example, if you have Weight fences of 200 (min) and 800 (max), but the axis runs from 100 to 900, then you need:

  • 100 → 200
  • 200 → 200
  • 900 → 800
  • 800 → 800

It’s easy to forget the 200 → 200 and 800 → 800 mappings, thinking they are not necessary; but they are what stops the other mappings from affecting entire ranges.

For example, if you have Weight fences of 200 (min) and 800 (max), but the axis runs from 100 to 900, then you need:

  • 100 → 200
  • 200 → 200
  • 900 → 800
  • 800 → 800

It’s easy to forget the 200 → 200 and 800 → 800 mappings, thinking they are not necessary; but they are what stops the other mappings from affecting entire ranges.

That would indeed be the way to implement a fence. I think what's desired here is a warp of the designspace, not fencing it... I might be wrong.

Can this be closed?