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

Support "Axis Mapping" custom parameter for GlyphsApp fonts #568

Closed
arrowtype opened this issue Dec 9, 2019 · 31 comments
Closed

Support "Axis Mapping" custom parameter for GlyphsApp fonts #568

arrowtype opened this issue Dec 9, 2019 · 31 comments

Comments

@arrowtype
Copy link

GlyphsApp has a Custom Property for "Axis Mappings" (it appears to have been added in v1261).

However, adding this has no effect on the Designspace output from glyphsLib, as in a FontMake build.

image

The only way to get around this as a user, as far as I know, is to have Axis Location custom props on Instances, but not on Masters. I get this impression from the builder/axes code, and a test file only got the proper designspace mapping once I did removed axis location props from Masters.

def to_designspace_axes(self):
if not self.font.masters:
return
regular_master = get_regular_master(self.font)
assert isinstance(regular_master, classes.GSFontMaster)
for axis_def in get_axis_definitions(self.font):
axis = self.designspace.newAxisDescriptor()
axis.tag = axis_def.tag
axis.name = axis_def.name
# TODO add support for localised axis.labelNames when Glyphs.app does
# See https://github.com/googlefonts/glyphsLib/issues/280
if font_uses_new_axes(self.font):
# Build the mapping from the "Axis Location" of the masters
# TODO: (jany) use Virtual Masters as well?
mapping = {}
for master in self.font.masters:
designLoc = axis_def.get_design_loc(master)
userLoc = axis_def.get_user_loc(master)
if userLoc in mapping and mapping[userLoc] != designLoc:
logger.warning(
"Axis location (%s) was redefined by '%s'", userLoc, master.name
)
mapping[userLoc] = designLoc
regularDesignLoc = axis_def.get_design_loc(regular_master)
regularUserLoc = axis_def.get_user_loc(regular_master)
else:
# Build the mapping from the isntances because they have both
# a user location and a design location.
instance_mapping = {}
for instance in self.font.instances:
if is_instance_active(instance) or self.minimize_glyphs_diffs:
designLoc = axis_def.get_design_loc(instance)
userLoc = axis_def.get_user_loc(instance)
if (
userLoc in instance_mapping
and instance_mapping[userLoc] != designLoc
):
logger.warning(
"Instance user-space location (%s) redefined by " "'%s'",
userLoc,
instance.name,
)
instance_mapping[userLoc] = designLoc
master_mapping = {}
for master in self.font.masters:
# Glyphs masters don't have a user location
userLoc = designLoc = axis_def.get_design_loc(master)
master_mapping[userLoc] = designLoc
# Prefer the instance-based mapping
mapping = instance_mapping or master_mapping
regularDesignLoc = axis_def.get_design_loc(regular_master)
# Glyphs masters don't have a user location, so we compute it by
# looking at the axis mapping in reverse.
reverse_mapping = [(dl, ul) for ul, dl in sorted(mapping.items())]
regularUserLoc = interp(reverse_mapping, regularDesignLoc)
# TODO make sure that the default is in mapping?
minimum = min(mapping)
maximum = max(mapping)
default = min(maximum, max(minimum, regularUserLoc)) # clamp
is_identity_map = all(uloc == dloc for uloc, dloc in mapping.items())
if (
minimum < maximum
or minimum != axis_def.default_user_loc
or not is_identity_map
):
if not is_identity_map:
axis.map = sorted(mapping.items())
axis.minimum = minimum
axis.maximum = maximum
axis.default = default
self.designspace.addAxis(axis)

However, it's problematic to not be able to control the axis maps at a single, top level. In the Nunito project, it means that I also had to add a random extra Instance, so that the whole Slant range would be covered by Instances, and the Slant axis would go from the min to max values present in sources (otherwise, the build fails).

Here are the files I was using:

nunito-build-test.zip

NunitoSans_Pitstop-26.glyphs is the full GlyphsApp file, with the problem of only-partial Weight axis mapping. With this, the generated wght map is partial (resulting in instances with weight values that are a bit off, with floating-point values):

    <axis tag="wght" name="Weight" minimum="200" maximum="1000" default="200">
      <map input="200" output="42"/>
      <map input="700" output="125"/>
      <map input="1000" output="208"/>
    </axis>

NunitoSans_Pitstop-26--testfile_ABC.glyphs is a GlyphsApp file with the problem fixed as a hack, but with the superfluous Instance to make the Slant axis work. With this, the generated map is:

    <axis tag="wght" name="Weight" minimum="200" maximum="1000" default="200">
      <map input="200" output="42"/>
      <map input="300" output="61"/>
      <map input="400" output="81"/>
      <map input="600" output="101"/>
      <map input="700" output="125"/>
      <map input="800" output="151"/>
      <map input="900" output="178"/>
      <map input="1000" output="208"/>
    </axis>

I'm building with FontMake:

fontmake -o variable -g NunitoSans_Pitstop-26--testfile.glyphs

This issue was filed on behalf of @Fonthausen, but I can try to answer questions if I left anything unclear.

@Fonthausen
Copy link

@arrowtype We have rearranged our file as you advised us to do, but it doesnt pars through fontmake. Barbara took the file and deleted everything except ABC like you did and it worked...

We are now trying to find out what could be the culprit. Maybe you have an idea ?
We tried:
– Deleting the features.
– Take out the Cyrillic.
– Decompose the Cyrillic.
– Putting the axis Location in the masters and deleting the ones in the instances.
– Etc....

And then in the end we deleted the kerning exceptions. And the font was produced...
So now we have to find the culprits...

@anthrotype
Copy link
Member

Hey Stephen, yes we actually asked Georg to add that feature, as we want to use it when exporting designspace from glyphsLib. I just wasn't aware this had landed already in Glyphs.app. We definitely want to support this in the near future.
Stay tuned

@anthrotype
Copy link
Member

For reference, this is the discussion where the Axis Mapping idea comes from
#483

@madig
Copy link
Collaborator

madig commented Jan 30, 2020

@schriftgestalt how do the Axes and Axis Mapping parameters interact?

@schriftgestalt
Copy link
Collaborator

The Axes parameter defines the axes (you could say it defines the fvar table), The Axis Mappings are basically the avar table.

@anthrotype
Copy link
Member

is this new Axis Mappings parameter documented somewhere? What does it look like?

@schriftgestalt
Copy link
Collaborator

It is not documented jet. I need to polish the implementation a bit first. But the data should stay like this:

{
	SMNA = {
		0 = 2;
		1 = 4;
		2 = 6;
	};
	wght = {
		3 = 100;
		70 = 400;
		180 = 800;
	};
};

@anthrotype
Copy link
Member

@schriftgestalt thanks!

does Axis Mappings then supersede the masters' Axis Location? I think the two methods are mutually exclusive, with Axis Mappings being more general, right?

@schriftgestalt
Copy link
Collaborator

not really. The Axis Location is going into fvar. The Axis Mappings is only going into avar (for now, I need to find some time to see how to put that together properly.

@anthrotype
Copy link
Member

but the two parameters can't contradict one another. They must match.

Axis Location assigns a user-space location (i.e. in the same coordinate space as fvar) to a given master. This has the effect of implicitly mapping a master's internal design location to the given user-space location.

Axis Mappings provides a mapping from user-space locations to internal design location (at least, that's the interpretation of DesignSpace.axes <map> elements, with 'input' and 'output' attributes respectively, cf: https://github.com/fonttools/fonttools/tree/master/Doc/source/designspaceLib#axisdescriptor-object).

@madig
Copy link
Collaborator

madig commented Feb 13, 2020

I had a look at how Axes and Axis Mappings behave in Glyphs 2.6.4. Axes supplies the list of axes which can then be selected in Axis Mappings. This would map nicely onto DS semantics. I can imagine a future where glyphsLib hard-requires an Axis parameter and only sets mappings through Axis Mappings, any Axis Location parameter anywhere would then result in an error.

@schriftgestalt
Copy link
Collaborator

As I said, the current idea is that the Axis Mapping is only effecting the avar table and the axis location is effecting the external axis metrics in the fvar table.

@anthrotype
Copy link
Member

axis location is effecting the external axis metrics in the fvar table

by which you mean the axes bounds (minumum, default and maximum values) in user-scale, right?
"Axis Location" is a master-level custom parameter. Do you mean it's only useful/used for masters that sit on the axes bounds. What about the rest of the masters?
Assigning an "Axis Location" to a master also means associating a user-scale axis coordinate (the value of the Axis Location) to the internal design location of that master (the value used internally for interpolation).

the Axis Mapping is only effecting the avar table

avar mappings are used to map from the default normalized coordinates (i.e. resulting from mapping fvar axis min to -1, default to 0 and max to +1, and interpolating the in-betweens) to the modified normalized coordinates (-1, 0 and +1 stay the same, in-between segments can be stretched or compresed).

Your Axis Mappings font-wide custom parameter, from what I can see, maps from user-scale coordinates to internal design coordinates (just like the DesignSpace axes map elements do).

The Axis Mappings param can be translated into avar mappings by simply piecewise-linearly mapping both the keys and the values to the normalized scale -1,0,+1. This also requires that there exist at least three required key-value pairs in a valid Axis Mappings param: one for min, one for default and one for max along a given axis.

So far so good. But you can immediately see that Axis Location master params (defined as the user-scale coordinate of the masters) on the one hand, and the Axis Mappings keys on the other hand, must be in agreement with each other. So one can argue that Axis Mappings supersed Axis Location in that it is more general, since it allows to assign mappings not only for the masters but also any value in between them. And that storing the same values twice is redundant and may result in contradictory pieces of data.

@schriftgestalt
Copy link
Collaborator

by which you mean the axes bounds (minumum, default and maximum values) in user-scale, right?
"Axis Location" is a master-level custom parameter. Do you mean it's only useful/used for masters that sit on the axes bounds. What about the rest of the masters?

It is only used to define the min, default, max values. For masters that are in different position, it is ignored.

The Axis Mapping is supposed to map from design coordinates to design coordinates. I really need to have a look at that. I think it should map from design to user coordinates.

I’ll have a look at that as soon ASAP and will update here.

@anthrotype
Copy link
Member

I think it should map from design to user coordinates

no, please, have it map from user to design coordinates, like DesignSpace axis maps already do, as that more clearly reflects the data in avar (where input is [normalized] user coords [the 'user' is the font user, not the designer] and output is [normalized] internal coords)

@schriftgestalt
Copy link
Collaborator

I’ll think about that.

@madig
Copy link
Collaborator

madig commented Feb 14, 2020

I hope we find a good solution to this before Glyphs 3 is released. I'd like to see axes management more front and center in the font info dialog instead of being relegated to hard to understand custom parameters. FontLab 7's axes dialog is a good starting point.

@madig
Copy link
Collaborator

madig commented Feb 17, 2020

@schriftgestalt how do you determine the min/default/max masters for an arbitrary axis?

@schriftgestalt
Copy link
Collaborator

Min/max are the lowest/highest position of all masters. Default comes from the default master (the first or where the Variable Font Origin parameter points at.

@madig
Copy link
Collaborator

madig commented Feb 19, 2020

So, I'm back on trying to knock some sense into glyphsLib's axis handling. I don't know how to proceed.

Data that influences the mapping:

  • Axis Mappings parameter
  • Axis Location parameter on masters
  • Instance OS/2 Weight and Width class

Cosimo suggests:

Use Axis Mappings for anything which is not already an instance (masters, or in-between locations which are not named instances), or for all other axes which are not weight and width. When instance user coordinates and Axis Mappings disagree, take the instance user coordinates as the good ones (which are going to be used anyway when making statics) and update the Axis Mappings (which can be empty if no param is set) with the output value of the instance.

In this scheme, Axis Location would be completely ignored.

@schriftgestalt This is just ugh. Is there still time to find a better solution for Glyphs 3? Something like a dedicated "Axes" font info pane that centralizes axis management? Instances then default to the OS/2 weight and width values derived from their axis location with overrides available through custom parameters?

@schriftgestalt
Copy link
Collaborator

I should strongly suggest to not look at the Weight/Width class of the instances. That is what the Axis Mapping parameter is for.

You don’t seem to like the Axis Location. It is as a external design space mapping. So each master has two coordinates. One in the internal design space and one in the external (variable font) space.

I’m myself working on how to bring all of that together. I’ll report when I’m through with it.

@madig
Copy link
Collaborator

madig commented Feb 19, 2020

But is the Axis Mapping parameter then used to set the OS/2 classes of static fonts? What happens if the instance values disagree?

You don’t seem to like the Axis Location. It is as a external design space mapping

Rather, I don't understand its purpose when one has Axis Mappings? It just seems to be a limited version of it?

So each master has two coordinates

But it's only honored on min/max/default masters?

@schriftgestalt
Copy link
Collaborator

No, the static font don’t use any of this.

So each master has two coordinates

But it's only honored on min/max/default masters?

That is what I’m working on right now. And mapping between the two spaces is tricky. But that has to be done regardless where the mapping comes from. And relying on avar to fix things later doesn't work.

And for me the AxisMapping is something extra, to fine tune or to do funny stuff. Not something I like to get my data for the fvar table.

@anthrotype
Copy link
Member

anthrotype commented Feb 20, 2020

the static font don’t use any of this

what me and Nikolaus are trying to say is that the OS2 weight/width class of the static fonts, on the one hand, and the fvar user-space (or external if you like) locations of the VF named instances, on the other hand, must not contradict one another. This of course is to enable interoperability between the static fonts and the VF generated from the same set of sources, so one can swap one with the other without visible changes.

See "wght" registered axis spec

Values must be in the range 1 to 1000 ... can be interpreted in direct comparison to values for usWeightClass in the OS/2 table, or the CSS font-weight property.

Or the "wdth" registered axis spec:

Values can be interpreted as a percentage of ... “normal width”... The description of usWidthClass in the OS/2 table documentation provides a table of mappings from usWidthClass values to “% of normal” values (etc.)

How do we make sure that:

  1. user can specify a custom mapping between user-space coordinates (as in fvar, what is called "input" in the DesignSpace axis.map) to internal design-space coordinates (i.e. the values used for interpolation, the "output" attribute of DS axis.map elements).
  2. these explicit {external: internal} mappings are not limited to the masters' locations, or the instances' locations; one should be able to assign arbitrary mappings at any point in the variation space.
  3. these explicit mappings must not contradict the implicit mappings that, for weight and width, arise from the fact that instances (that are used to specify both static instances and VF named instances) need to have an OS/2 usWeightClass and usWidthClass, and these values have a predefined relationship with registered wght and wdth axis.

@anthrotype
Copy link
Member

mapping between the two spaces is tricky

it is not once the user has the ability to specify these explicitly, as it can be done with the .designspace axis and map elements.

Let me recap how these work and how they are translated into avar mappings.

There is a global list of axes. Each axis has a minimum, default and maximum values, specified in user-scale coordinates. These value go straight into fvar axes definitions.

An axis can optionally contain a set of mappings from user-scale to internal design-scale coordinates (the <map> elements). These are only required iff the interpolation values assigned to the masters and instances use a different scale (or unit of measurement) than the values that are displayed to the user through the fvar axes and named instances definitions.

Some (registered) axes are required by the OT spec to have a predefined scale: wght must go from 1..1000, wdth 50..[100]..150 (%) etc. Other axes may not have a required user scale, but the designer, for whatever reason, may chose to define one set of values for the UI (fvar) and another set of values for the internal interpolation. This ability to warp the design-space (compressing or enlarging specific segments along a given axis without the need to insert additional intermediate masters) may also be used as a design tool. However for some axes (like wght or wdth) these mappings are required (unless in very simple setups where a designer is happy with using the same predefined user-scale also for the internal interpolation values, in which case there is a 1:1 relationship between external and internal values and there is no need for a custom mapping).

Each axis can be assigned a list of key:value pairs. The key (or input) is understood as the value that the user selects when requesting an instance (be it a named instance or a custom one). The value (or output) is what the user input key maps to internally in the variation space, and belongs to the same (arbitrary) scale defined by the designer to set up the interpolation.
The purpose of these axis mappings is to generate an avar table. This allows to distort some segments of the variation space, increasing or decreasing the "speed" at which interpolation occurs between locations along a given axis (it can even create discontinuous effects), without needing to insert an additional set of deltas (a master) to control this.

Not all axes need to have custom avar mappings. If external, user values and internal design values are the same for that axis, axis mappings won't actually do anything (all map to themselves), then no avar segments are needed for that axis. Only when an axis does have some interesting mappings, we do create avar segments for it.

The avar uses normalized coordinates for both keys and values: i.e. all values must be in the -1..+1 range, where -1 is always defined as the axis minimum, 0 as the axis default, +1 as the axis maximum. The {-1:-1, 0:0, 1:1} mappings are always required by the avar spec, so one cannot say map -1 to 0.5, or 0 to 0.3, etc. This means that only the in-between portions of the variation space (between the predefined mappings) can be influenced by a custom avar.

So, we can say by default there are at least 3 predefined axis mappings that are always present and are determined by the that axis min/default/max values, the locations of the masters and the identification of the default or base master (the Variable Font Origin). When an axis default is equal to either the min or max, we only need 2 predefined axis mappings.

For example, we have a single axis with the following (min, default, max) values {"wght": (100, 400, 700)}, and 3 masters that are assigned the following (external, internal) mappings for this axis: [(100, 80), (400, 120), (700, 180)].
If that's all, the resulting avar will not be interesting, as it would only contain the predefined mappings {-1:1, 0:0, 1:1}. Same if the axis was defined as {"wght": (400, 400, 700)} (with min==default), and we have 2 masters with mappings [(400, 120), (700, 180)]. The hypotetical avar will still be {-1:1, 0:0, 1:1} (the -1..0 range is ineffective as it will never be selected).

To convert from the (external, internal) values to the normalized ones in the range -1..0..+1, we need to apply the normalization based on the (min, default, max) triplet which is appropriate for each space, whether external or internal: if we are normalizing an external coordinate, we use as the (min, default, max) triplet the user-space coordinates of the masters at the minimum, default and maximum points along an axisl; whereas if we are normalizing an internal design coordinate, we use for triplet the (min, default, max) interpolation values of the masters (see normalizeValue function in varLib.models).

# external, user-space coordinates
normalizeValue(100, (100, 400, 700)) == -1
normalizeValue(400, (100, 400, 700)) == 0
normalizeValue(700, (100, 400, 700)) == 1

# internal, design-space coordinates
normalizeValue(80, (80, 120, 180)) == -1
normalizeValue(120, (80, 120, 180)) == 0
normalizeValue(180, (80, 120, 180)) == 1

Values that are in-between these predefined mappings will be linearly interpolated between them. E.g. say our axis mappings contain an extra map at (150, 90), the resulting avar normalized key:value pair will be -0.83333: -0.75

# external
normalizeValue(150, (100, 400, 700)) == -0.83333
# internal
normalizeValue(90, (80, 120, 180)) == -0.75

So you see that if we have access to the following data structures:

axes = {
    "wght": (100, 400, 700),  # (min, max, default)
    "wdth": (100, 100, 150),
}

axis_mappings = {
    "wght": [
        (100, 80),  # (external, internal)
        (150, 90),
        (400, 120),
        (700, 180),
    ],
    # wdth has no custom mappings in this example 
}

# locations use internal design coordinates
masters = [
    Master(location={"wght": 80, "wdth": 100}),
    Master(location={"wght": 90, "wdth": 100}),
    Master(location={"wght": 120, "wdth": 100}),  # this one is, by definition, the default base master,
    ...                                           # given the definitions of the axes defaults above
]

... we can easily build our avar (and that's exactly what varLib._add_avar method is doing -- the code is perhaps clearer than thousands words).

@anthrotype
Copy link
Member

@schriftgestalt any updates on this?

@arrowtype
Copy link
Author

As far as I'm able to understand, here, the real difficulty is how to make a Glyphs UI such that users can:

  1. Easily set up masters & instances within their typeface's overall "designspace" (not the file, but the concept), which will provide the overall axis mapping / speed curve along each axis
  2. Add "Axis Mappings" to adjust the speed curve of axes in ways that masters & instances may not cover or specify
  3. Not be able to make an axis mapping that conflicts with master & instance locations. For example, when working directly in a .designspace file with UFOs, it is easy to set up a <map> element that contradicts values in masters/instances, and causes build failures that are unexpected for beginners. (For example, for me it wasn't immediately intuitive that input was the end-user/CSS values, while output was the designer/internal values, so I have gotten those flipped many times.)

My best guess is, Georg is trying to make part 1 work for everyone, part 2 available for advanced use cases, and part 3 such that people don't put themselves into a confusing corner. I'm betting this is what he meant by saying "mapping between the two spaces is tricky"?

If this is the case, I agree that it is a tricky app UX/UI challenge. My two cents is that:

  • The UI for Axis Mapping could label each column clearly but probably without terms "input" and "output" (which I think are confusing for a font designer who may think of their built, shipped fonts as the "output"). E.g. these could get labels such as "Font User Values" → "Internal Axis Values".
  • The UI for Axis Mapping could also perhaps pre-populate with values derived from the Masters & Instances as they are set up in their own tabs. Potentially, these derived values shouldn't even editable, but maybe clicking them should give the suggestion to edit values in their own tabs.

Just some thoughts. I could be wrong about any of this.

@anthrotype
Copy link
Member

No, you make all valid points, thanks Stephen for chiming in.

for me it wasn't immediately intuitive that input was the end-user/CSS values, while output was the designer/internal values, so I have gotten those flipped many times.

you're not the first, nor last one. I got tripped a few times as well 😅

I like both your UI suggestions, especially the idea of pre-populating Axis Mappings with non-editable values from those implicit in the Masters/Instances tabs.

@m4rc1e
Copy link
Contributor

m4rc1e commented Jul 29, 2020

I've been fighting this issue for a while so I thought I'd do some research.

My research is too long to post as an issue (it also goes off topic a bit) so I'm producing a doc (wip) which should help Georg fix variable font generation in Glyphsapp and help the developers of glyphsLib better understand what this param does. I'm still thinking through how I'd like to see it implemented in glyphsLib though.

If anyone would like me to conduct any further tests, I'm more than happy to do so.

@schriftgestalt More than happy to elaborate on any points I've brought up.

@schriftgestalt
Copy link
Collaborator

Thanks. I’m planing to work on this, soon.

@anthrotype
Copy link
Member

should be supported now after #618, thanks Marc!

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

No branches or pull requests

6 participants