Add support for expression-style metric override. The old [metric-override.multiplies]
and [metric-override.adds]
are no longer supported (#1181).
This commit is contained in:
parent
51c0aadd77
commit
e49f4d2c8a
5 changed files with 282 additions and 58 deletions
42
README.md
42
README.md
|
@ -3014,23 +3014,47 @@ Subsection `metric-override` provides ability to override certain metric values,
|
||||||
| `powerlineShiftX`, `powerlineShiftY` | emu | 0 | X and Y shift of Powerline glyphs. |
|
| `powerlineShiftX`, `powerlineShiftY` | emu | 0 | X and Y shift of Powerline glyphs. |
|
||||||
| `onumZeroHeightRatio` | (*ratio*) | 1.145 | Ratio of height of `0` under `onum` feature, to the height of `x`. |
|
| `onumZeroHeightRatio` | (*ratio*) | 1.145 | Ratio of height of `0` under `onum` feature, to the height of `x`. |
|
||||||
|
|
||||||
Sub-subsection `metric-override.multiplies` and `metric-override.adds` could be used to override the value by multiplying a scale to the default value, then add a shift to it further. The following configuration
|
The values of each item could be either a number, or a string representing an expression so that it could be different for different instance fonts, or depending on default values. The syntax of valid expressions are:
|
||||||
|
|
||||||
|
```
|
||||||
|
Expression -> Term (('+' | '-') Term)*
|
||||||
|
Term -> Factor (('*' | '/') Factor)*
|
||||||
|
Factor -> ('+' | '-')* Primitive
|
||||||
|
Primitive -> Literal
|
||||||
|
| Call
|
||||||
|
| Binding
|
||||||
|
| Group
|
||||||
|
| List
|
||||||
|
Literal -> ['0'..'9']+ ('.' ['0'..'9']+)?
|
||||||
|
Identifier -> ['A'..'Z', 'a'..'z', '_']+
|
||||||
|
Call -> Identifier '(' Expression (',' Expression)* ')'
|
||||||
|
List -> Identifier '[' Expression (',' Expression)* ']'
|
||||||
|
Binding -> Identifier
|
||||||
|
```
|
||||||
|
|
||||||
|
Valid identifiers include:
|
||||||
|
* `weight`: being the weight grade;
|
||||||
|
* `width`: being the characters' unit width, measured in em-units;
|
||||||
|
* `slopeAngle`: being the slope angle in degrees;
|
||||||
|
* Default value of all overridable metrics, prefixed with `default_`, i.e., default `cap` value will be accessable thorugh `default_cap`.
|
||||||
|
|
||||||
|
Valid functions include:
|
||||||
|
* `blend`(_x_, \[_x1_, _y1_\], \[_x2_, _y2_\], ...): Perform a smooth interpolation through data pairs \[_x1_, _y1_\], \[_x2_, _y2_\], ..., against parameter _x_.
|
||||||
|
|
||||||
|
For example, the following configuration:
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[buildPlans.iosevka-custom.metric-override]
|
[buildPlans.iosevka-custom.metric-override]
|
||||||
leading = 1500
|
leading = 1500
|
||||||
|
sb = 'default_sb * 1.0625 + 15'
|
||||||
[buildPlans.iosevka-custom.metric-override.multiplies]
|
dotSize = 'blend(weight, [100, 50], [400, 125], [900, 180])'
|
||||||
sb = 1.0625
|
|
||||||
|
|
||||||
[buildPlans.iosevka-custom.metric-override.adds]
|
|
||||||
sb = 15
|
|
||||||
```
|
```
|
||||||
|
|
||||||
will:
|
will:
|
||||||
|
|
||||||
* Override line height to `1500` em-unit;
|
* Override line height to `1500` em-unit;
|
||||||
* Override the sidebearing value by its value multiplied by `1.0625` then added with `15`.
|
* Override the sidebearing value by its value multiplied by `1.0625` then added with `15`.
|
||||||
|
* Override the dot size by a interpolation against weight: at thin (`100`) being `50`, at regular (`400`) being `125`, and at heavy (`900`) being `180`.
|
||||||
|
|
||||||
#### Sample Configuration
|
#### Sample Configuration
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
* \[**Breaking**\]: Add support for expression-style metric override. The old `[metric-override.multiplies]` and `[metric-override.adds]` are no longer supported (#1181).
|
||||||
* Fix motion-serifed N's broken shape (#1170).
|
* Fix motion-serifed N's broken shape (#1170).
|
||||||
* Fix bar-serif overlapping in Latin Small H-bar (`U+0127`) and Cyrillic Small Dje (`U+0452`) in Sans subfamily's with-serif variants (#1171).
|
* Fix bar-serif overlapping in Latin Small H-bar (`U+0127`) and Cyrillic Small Dje (`U+0452`) in Sans subfamily's with-serif variants (#1171).
|
||||||
* Add flat-boundary brace shape (#1172).
|
* Add flat-boundary brace shape (#1172).
|
|
@ -10,6 +10,7 @@ const Toml = require("@iarna/toml");
|
||||||
|
|
||||||
const { buildFont } = require("./gen/build-font.js");
|
const { buildFont } = require("./gen/build-font.js");
|
||||||
const Parameters = require("./support/parameters");
|
const Parameters = require("./support/parameters");
|
||||||
|
const { applyMetricOverride } = require("./support/metric-override");
|
||||||
const VariantData = require("./support/variant-data");
|
const VariantData = require("./support/variant-data");
|
||||||
const { applyLigationData } = require("./support/ligation-data");
|
const { applyLigationData } = require("./support/ligation-data");
|
||||||
const { createGrDisplaySheet } = require("./support/gr");
|
const { createGrDisplaySheet } = require("./support/gr");
|
||||||
|
@ -49,7 +50,7 @@ async function getParameters() {
|
||||||
|
|
||||||
if (argv.excludedCharRanges) para.excludedCharRanges = argv.excludedCharRanges;
|
if (argv.excludedCharRanges) para.excludedCharRanges = argv.excludedCharRanges;
|
||||||
if (argv.compatibilityLigatures) para.compLig = argv.compatibilityLigatures;
|
if (argv.compatibilityLigatures) para.compLig = argv.compatibilityLigatures;
|
||||||
if (argv.metricOverride) Parameters.applyMetricOverride(para, argv.metricOverride);
|
if (argv.metricOverride) applyMetricOverride(para, argv.metricOverride, argv);
|
||||||
|
|
||||||
para.naming = {
|
para.naming = {
|
||||||
...para.naming,
|
...para.naming,
|
||||||
|
|
246
font-src/support/metric-override.js
Normal file
246
font-src/support/metric-override.js
Normal file
|
@ -0,0 +1,246 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const { monotonicInterpolate } = require("./util/monotonic-interpolate");
|
||||||
|
|
||||||
|
exports.applyMetricOverride = applyMetricOverride;
|
||||||
|
function applyMetricOverride(para, mo, argv) {
|
||||||
|
const bindings = initBindings(para, argv);
|
||||||
|
for (const [field, expr] of Object.entries(mo)) {
|
||||||
|
if (!validMetricOverrideFields.has(field)) {
|
||||||
|
console.error(`Field ${field} cannot be get overridden.`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (typeof expr === "number") {
|
||||||
|
para[field] = expr;
|
||||||
|
} else if (typeof expr === "string") {
|
||||||
|
const e = RootExpression(new State(expr), bindings);
|
||||||
|
if (typeof e !== "number")
|
||||||
|
throw new TypeError(`Expression ${e} do not evaluate to a number`);
|
||||||
|
para[field] = e;
|
||||||
|
} else {
|
||||||
|
throw new SyntaxError(`Invalid expression ${JSON.stringify(expr)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const validMetricOverrideFields = new Set([
|
||||||
|
"cap",
|
||||||
|
"xheight",
|
||||||
|
"sb",
|
||||||
|
"accentWidth",
|
||||||
|
"accentClearance",
|
||||||
|
"accentHeight",
|
||||||
|
"accentStackOffset",
|
||||||
|
"dotSize",
|
||||||
|
"periodSize",
|
||||||
|
"leading",
|
||||||
|
"winMetricAscenderPad",
|
||||||
|
"winMetricDescenderPad",
|
||||||
|
"symbolMid",
|
||||||
|
"parenSize",
|
||||||
|
"powerlineScaleY",
|
||||||
|
"powerlineScaleX",
|
||||||
|
"powerlineShiftY",
|
||||||
|
"powerlineShiftX",
|
||||||
|
"onumZeroHeightRatio"
|
||||||
|
]);
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Function bindings
|
||||||
|
|
||||||
|
function initBindings(para, argv) {
|
||||||
|
const valueBindings = new Map();
|
||||||
|
for (const k of validMetricOverrideFields) {
|
||||||
|
valueBindings.set("default_" + k, para[k]);
|
||||||
|
}
|
||||||
|
valueBindings.set("weight", argv.shape.weight);
|
||||||
|
valueBindings.set("width", argv.shape.width);
|
||||||
|
valueBindings.set("slopeAngle", argv.shape.slopeAngle);
|
||||||
|
|
||||||
|
const functionBindings = new Map();
|
||||||
|
functionBindings.set("blend", blend);
|
||||||
|
return { val: valueBindings, functions: functionBindings };
|
||||||
|
}
|
||||||
|
|
||||||
|
function blend(against, ...pairs) {
|
||||||
|
const xs = [],
|
||||||
|
ys = [];
|
||||||
|
for (const [x, y] of pairs) {
|
||||||
|
xs.push(x), ys.push(y);
|
||||||
|
}
|
||||||
|
return monotonicInterpolate(xs, ys)(against);
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Simple expression parser
|
||||||
|
class State {
|
||||||
|
constructor(input) {
|
||||||
|
this.input = input;
|
||||||
|
this.cp = 0;
|
||||||
|
}
|
||||||
|
fetch(ch) {
|
||||||
|
return this.input[this.cp];
|
||||||
|
}
|
||||||
|
test(ch) {
|
||||||
|
return this.input[this.cp] === ch;
|
||||||
|
}
|
||||||
|
testCk(f) {
|
||||||
|
return f(this.input[this.cp]);
|
||||||
|
}
|
||||||
|
advance() {
|
||||||
|
return this.input[this.cp++];
|
||||||
|
}
|
||||||
|
expectAndAdvance(ch) {
|
||||||
|
if (!this.test(ch)) this.fail();
|
||||||
|
this.advance();
|
||||||
|
}
|
||||||
|
expectEnd() {
|
||||||
|
if (this.cp < this.input.length) this.fail();
|
||||||
|
}
|
||||||
|
fail() {
|
||||||
|
throw new SyntaxError("Failed to parse expression: " + this.input + "@" + this.cp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function RootExpression(state, bindings) {
|
||||||
|
const e = Expression(state, bindings);
|
||||||
|
state.expectEnd();
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
function Expression(state, bindings) {
|
||||||
|
skipSpaces(state);
|
||||||
|
const e = Sum(state, bindings);
|
||||||
|
skipSpaces(state);
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Sum(state, bindings) {
|
||||||
|
let f = Term(state, bindings);
|
||||||
|
skipSpaces(state);
|
||||||
|
while (state.test("+") || state.test("-")) {
|
||||||
|
let op = state.advance();
|
||||||
|
skipSpaces(state);
|
||||||
|
const g = Term(state, bindings);
|
||||||
|
skipSpaces(state);
|
||||||
|
switch (op) {
|
||||||
|
case "+":
|
||||||
|
f = f + g;
|
||||||
|
break;
|
||||||
|
case "-":
|
||||||
|
f = f - g;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
function Term(state, bindings) {
|
||||||
|
let f = Factor(state, bindings);
|
||||||
|
skipSpaces(state);
|
||||||
|
while (state.test("*") || state.test("/")) {
|
||||||
|
let op = state.advance();
|
||||||
|
skipSpaces(state);
|
||||||
|
const g = Factor(state, bindings);
|
||||||
|
skipSpaces(state);
|
||||||
|
switch (op) {
|
||||||
|
case "*":
|
||||||
|
f = f * g;
|
||||||
|
break;
|
||||||
|
case "/":
|
||||||
|
f = f / g;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Factor(state, bindings) {
|
||||||
|
if (state.test("+")) {
|
||||||
|
state.advance();
|
||||||
|
skipSpaces(state);
|
||||||
|
return Factor(state, bindings);
|
||||||
|
} else if (state.test("-")) {
|
||||||
|
state.advance();
|
||||||
|
skipSpaces(state);
|
||||||
|
return -Factor(state, bindings);
|
||||||
|
} else {
|
||||||
|
return Primitive(state, bindings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Primitive(state, bindings) {
|
||||||
|
if (state.testCk(isDigit)) return Lit(state, bindings);
|
||||||
|
if (state.testCk(isAlpha)) return BindingOrCall(state, bindings);
|
||||||
|
if (state.test("(")) return Group(state, bindings);
|
||||||
|
if (state.test("[")) return List("[", "]", state, bindings);
|
||||||
|
state.fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
function Lit(state, bindings) {
|
||||||
|
let integerPart = 0;
|
||||||
|
let fractionPart = 0;
|
||||||
|
let fractionScale = 1;
|
||||||
|
while (state.testCk(isDigit)) {
|
||||||
|
const digit = state.advance().codePointAt(0) - "0".codePointAt(0);
|
||||||
|
integerPart = integerPart * 10 + digit;
|
||||||
|
}
|
||||||
|
if (state.test(".")) {
|
||||||
|
state.advance();
|
||||||
|
while (state.testCk(isDigit)) {
|
||||||
|
fractionScale /= 10;
|
||||||
|
const digit = state.advance().codePointAt(0) - "0".codePointAt(0);
|
||||||
|
fractionPart += digit * fractionScale;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return integerPart + fractionPart;
|
||||||
|
}
|
||||||
|
|
||||||
|
function BindingOrCall(state, bindings) {
|
||||||
|
let symbolName = "";
|
||||||
|
while (state.testCk(isAlpha)) symbolName += state.advance();
|
||||||
|
if (state.test("(")) {
|
||||||
|
const args = List("(", ")", state, bindings);
|
||||||
|
if (bindings.functions.has(symbolName)) return bindings.functions.get(symbolName)(...args);
|
||||||
|
else throw new TypeError(`Unknown function ${symbolName}.`);
|
||||||
|
} else {
|
||||||
|
if (bindings.val.has(symbolName)) return bindings.val.get(symbolName);
|
||||||
|
else throw new TypeError(`Unknown identifier ${symbolName}.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Group(state, bindings) {
|
||||||
|
state.expectAndAdvance("(");
|
||||||
|
skipSpaces(state);
|
||||||
|
const e = Expression(state, bindings);
|
||||||
|
skipSpaces(state);
|
||||||
|
state.expectAndAdvance(")");
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
function List(start, end, state, bindings) {
|
||||||
|
let results = [];
|
||||||
|
state.expectAndAdvance(start);
|
||||||
|
skipSpaces(state);
|
||||||
|
results.push(Expression(state, bindings));
|
||||||
|
skipSpaces(state);
|
||||||
|
while (state.test(",")) {
|
||||||
|
state.expectAndAdvance(",");
|
||||||
|
skipSpaces(state);
|
||||||
|
results.push(Expression(state, bindings));
|
||||||
|
skipSpaces(state);
|
||||||
|
}
|
||||||
|
state.expectAndAdvance(end);
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
function skipSpaces(state) {
|
||||||
|
while (state.testCk(isSpace)) state.advance();
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSpace(ch) {
|
||||||
|
return ch === " " || ch === "\t";
|
||||||
|
}
|
||||||
|
function isDigit(ch) {
|
||||||
|
return ch >= "0" && ch <= "9";
|
||||||
|
}
|
||||||
|
function isAlpha(ch) {
|
||||||
|
return (ch >= "A" && ch <= "Z") || (ch >= "a" && ch <= "z") || ch === "_";
|
||||||
|
}
|
|
@ -115,51 +115,3 @@ function hiveBlend(hive, value) {
|
||||||
}
|
}
|
||||||
return generatedHive;
|
return generatedHive;
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.applyMetricOverride = applyMetricOverride;
|
|
||||||
function applyMetricOverride(para, mo) {
|
|
||||||
const overrideObj = { metricOverride: {} };
|
|
||||||
createMetricDataSet(overrideObj.metricOverride, mo);
|
|
||||||
apply(para, overrideObj, ["metricOverride"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createMetricDataSet(sink, mo) {
|
|
||||||
for (const key in mo) {
|
|
||||||
if (metricOverrideHandlers[key]) {
|
|
||||||
metricOverrideHandlers[key](sink, key, mo[key]);
|
|
||||||
} else {
|
|
||||||
console.error(`Metric override key ${key} is not supported. Skipping it.`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function numericFieldHandler(sink, key, x) {
|
|
||||||
if (x != null && isFinite(x)) sink[key] = x;
|
|
||||||
}
|
|
||||||
function subObjectHandler(sink, key, obj) {
|
|
||||||
sink[key] = {};
|
|
||||||
createMetricDataSet(sink[key], obj);
|
|
||||||
}
|
|
||||||
const metricOverrideHandlers = {
|
|
||||||
cap: numericFieldHandler,
|
|
||||||
xheight: numericFieldHandler,
|
|
||||||
sb: numericFieldHandler,
|
|
||||||
accentWidth: numericFieldHandler,
|
|
||||||
accentClearance: numericFieldHandler,
|
|
||||||
accentHeight: numericFieldHandler,
|
|
||||||
accentStackOffset: numericFieldHandler,
|
|
||||||
dotSize: numericFieldHandler,
|
|
||||||
periodSize: numericFieldHandler,
|
|
||||||
leading: numericFieldHandler,
|
|
||||||
winMetricAscenderPad: numericFieldHandler,
|
|
||||||
winMetricDescenderPad: numericFieldHandler,
|
|
||||||
symbolMid: numericFieldHandler,
|
|
||||||
parenSize: numericFieldHandler,
|
|
||||||
powerlineScaleY: numericFieldHandler,
|
|
||||||
powerlineScaleX: numericFieldHandler,
|
|
||||||
powerlineShiftY: numericFieldHandler,
|
|
||||||
powerlineShiftX: numericFieldHandler,
|
|
||||||
onumZeroHeightRatio: numericFieldHandler,
|
|
||||||
multiplies: subObjectHandler,
|
|
||||||
adds: subObjectHandler
|
|
||||||
};
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue