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

Obtain designspace mappings from avar2 data #25

Open
Lorp opened this issue Jul 16, 2024 · 12 comments
Open

Obtain designspace mappings from avar2 data #25

Lorp opened this issue Jul 16, 2024 · 12 comments

Comments

@Lorp
Copy link
Owner

Lorp commented Jul 16, 2024

Currently Fencer ignores any existing avar data, though it would be very useful if it could visualize existing avar1 and avar2 data as mappings.

Although a general solution may not be possible, it should be possible at least in all the cases where the avar2 table was compiled from mappings, which includes all fonts made via a .designspace document and all fonts exported from Fencer.

This code from Fontra is suggested by @justvanrossum, as noted in fonttools/fonttools#3086

https://github.com/googlefonts/fontra/blob/087fac4c01d5bbd918f3550d03fae5693f0dc292/src/fontra/backends/opentype.py#L255-L285

@Lorp
Copy link
Owner Author

Lorp commented Aug 1, 2024

The logic is actually pretty simple:

  • for each region in the ItemVariationStore, create an inputLocation array (length = axisCount)
  • the values for each inputLocation are all the peak values from the corresponding region
  • get a set of transformed deltas by processing each inputLocation by the ItemVariationStore itself
  • for each inputLocation, generate an outputLocation array, being the array-style sum of the inputLocation and the transformed deltas (taking care with the F2DOT14 to INT16 transformation and clamping to [-1, 1])
  • denormalize all the inputLocations and outputLocations

@behdad
Copy link

behdad commented Aug 1, 2024

See this comment for how that logic can fail:
fonttools/fonttools#3086 (comment)

@Lorp
Copy link
Owner Author

Lorp commented Aug 2, 2024

Thanks for the discussion. Indeed the corners definitely need to be considered for representation as mappings. Trying now with this logic:

Will it work to take 2^n+1 locations from every region encoded in the IVS? (where n=number of dimensions having value!=default, thus encoding all of the n-D region’s corners, and the +1 is for the n-D peak). Then we transform the unique locations with the IVS to obtain the deltas, and thereby the mappings.

@Lorp
Copy link
Owner Author

Lorp commented Aug 2, 2024

It seems to work well on arbitrary mappings on a 2-axis font, except for adding some redundant pin mappings. Amstelvar seems not to be working however.

Lorp added a commit that referenced this issue Aug 2, 2024
@Lorp
Copy link
Owner Author

Lorp commented Aug 2, 2024

@behdad @justvanrossum if you care to take a look, the code is here.

fencer/src/fencer.js

Lines 709 to 779 in 82d5727

if (importAvar2) {
console.log(avar);
// get locations from IVS
// from https://github.com/googlefonts/fontra/blob/087fac4c01d5bbd918f3550d03fae5693f0dc292/src/fontra/backends/opentype.py#L176C1-L186C23
const axisCount = GLOBAL.font.fvar.axes.length;
const ivs = avar.itemVariationStore;
console.log(ivs);
const ivd = ivs.ivds[0];
const locations = []; // for each location, location[0] is input, location[1] is output
const locationsTxt = new Set();
locationsTxt.add(Array(axisCount).fill(0).join()); // add the default location, prevents it ever being added
ivs.ivds.forEach(ivd => {
ivd.regionIds.forEach(regionId => {
const region = ivs.regions[regionId];
const activeAxisIds = [];
// for each region, we records its corners (2^n locations) and its peak (1 location) where n is the number of axes that are non-default
region.forEach((tent, axisId) => {
if (tent[1] !== 0) { // tent[0] is start, tent[1] is peak, tent[2] is end
activeAxisIds.push(axisId);
}
});
const numCorners = Math.pow(2, activeAxisIds.length); // 2^activeAxisIds.length is the number of corners
for (let c=-1; c<numCorners; c++) { // -1 we use for the peak, giving us 1+2^n locations (2^n indices remain intact for bit mangling)
const corner = Array(axisCount).fill(0); // initialize to default values
if (c === -1) { // handle the peak of the n-D region
activeAxisIds.forEach(axisId => corner[axisId] = region[axisId][1] ); // [1] obtains the peak
}
else { // handle the corners of the n-D region
// use bit masking to determine which corner we’re at (bit 0 controls whether we’re talking about start or end of the 0th active axis, bit 1 controls 1st active axis, etc.)
activeAxisIds.forEach((axisId, a) => {
const pos = 2 * ((c & (0x01 << a))>>a); // 2 * 0 or 2 * 1
const tent = region[axisId];
corner[axisId] = tent[pos]; // tent[0] (start) or tent[2] (end)
});
}
// only proceed if this is a new location
const locationTxt = corner.map(v => Math.round(v*16384)).join(); // a serialized int version of the array as a hash
if (locationsTxt.has(locationTxt)) {
// duplicate location
}
else {
locationsTxt.add(locationTxt); // prevent this location from being added again using a serialized version of the array as a hash
const location = [[...corner], [...corner]];
const deltasI16 = SamsaFont.prototype.itemVariationStoreInstantiate(ivs, location[0])[0]; // [0] means we only look at deltas in the first IVD (TODO: handle multiple IVDs)
deltasI16.forEach((delta, d) => {
location[1][activeAxisIds[d]] = clamp(location[1][activeAxisIds[d]] + delta/16384, -1, 1);
});
locations.push(location);
}
}
});
});
// now denormalize these locations
locations.forEach(location => {
const mapping = [
denormalizeTuple(location[0]),
denormalizeTuple(location[1]),
];
GLOBAL.mappings.push(mapping);
});
console.log(GLOBAL.mappings);
}

@behdad
Copy link

behdad commented Aug 2, 2024

@behdad @justvanrossum if you care to take look, the code is here.

The Fontra link is outdated now?

@Lorp
Copy link
Owner Author

Lorp commented Aug 2, 2024

Yes, since we’re agreed that a tent’s start and end need to be accounted for, as well as peak.

@behdad
Copy link

behdad commented Aug 2, 2024

@behdad @justvanrossum if you care to take look, the code is here.

Looks about right, except that part about getting delta from first ivsd?

@behdad
Copy link

behdad commented Aug 2, 2024

@behdad @justvanrossum if you care to take look, the code is here.

Looks about right, except that part about getting delta from first ivsd?

I actually don't fully understand that comment.

@Lorp
Copy link
Owner Author

Lorp commented Aug 2, 2024

Ah, that’s an old comment. It does handle multiple IVDs. I’ll delete.

@Lorp
Copy link
Owner Author

Lorp commented Aug 2, 2024

Hang on, there is a bug there (the [0] index at the end), which will affect IVDs after the 0th one. On it…

@Lorp
Copy link
Owner Author

Lorp commented Aug 2, 2024

Now uses [ivdIndex] instead of [0].

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

2 participants