diff --git a/build-plans.toml b/build-plans.toml index f32c5581c1..69c3cfa153 100644 --- a/build-plans.toml +++ b/build-plans.toml @@ -360,7 +360,7 @@ snapshotFamily = 'iosevka-aile' design = ["diversity-2"] [buildPlans.iosevka-aile.widths.normal] -shape = 7 +shape = 576 menu = 5 css = "normal" @@ -379,7 +379,7 @@ snapshotFamily = 'iosevka-etoile' design = ["diversity-1"] [buildPlans.iosevka-etoile.widths.normal] -shape = 7 +shape = 576 menu = 5 css = "normal" @@ -398,7 +398,7 @@ snapshotFamily = 'iosevka-sparkle' design = ["diversity-1"] [buildPlans.iosevka-sparkle.widths.normal] -shape = 7 +shape = 576 menu = 5 css = "normal" @@ -614,12 +614,12 @@ italic = "italic" # and "menu" only support 1 ... 9 [widths.normal] -shape = 5 +shape = 500 menu = 5 css = "normal" [widths.extended] -shape = 7 +shape = 576 menu = 7 css = "expanded" diff --git a/changes/3.2.0.md b/changes/3.2.0.md index 25cad8136c..ecd714b56f 100644 --- a/changes/3.2.0.md +++ b/changes/3.2.0.md @@ -9,3 +9,4 @@ * Add parenthesis variant with larger contour (#570). * Fix placement of U+0315 COMBINING COMMA ABOVE RIGHT (#583). * Fix shape of U+1D24 LATIN LETTER VOICED LARYNGEAL SPIRANT (#584). + * Allow the user to customize the characters' width freely (#554). \ No newline at end of file diff --git a/gen/index.js b/gen/index.js index 362a0ce7d8..275f35c218 100644 --- a/gen/index.js +++ b/gen/index.js @@ -32,7 +32,10 @@ async function getParameters(argv) { const rawVariantsData = await tryParseToml(VARIANTS_TOML); const rawLigationData = await tryParseToml(LIGATIONS_TOML); - const para = Parameters.build(parametersData, argv.hives, { shapeWeight: argv.shape.weight }); + const para = Parameters.build(parametersData, argv.hives, { + shapeWeight: argv.shape.weight, + shapeWidth: argv.shape.width + }); const variantsData = FormVariantData(rawVariantsData, para); para.variants = variantsData; diff --git a/params/parameters.toml b/params/parameters.toml index 565f8fb6df..a6069e64c8 100644 --- a/params/parameters.toml +++ b/params/parameters.toml @@ -271,7 +271,18 @@ spacing = 0 # NOTE: this section is highly experimental # HANDLE WITH EXTREME CARE # Expanded : I heard someone want it being wider... -[wd-9.multiplies] +[shapeWidth.multiplies.blend.500] +width = 1 +stroke = 1 +sb = 1 +jut = 1 +longjut = 1 +rhook = 1 +rbalance = 1 +tbalance = 1 +smallsmooth = 1 + +[shapeWidth.multiplies.blend.664] width = 1.328 # 664 for normal char stroke = 1.103 # Make strokes a little thicker sb = 1.777 @@ -282,10 +293,7 @@ rbalance = 1.236 tbalance = 1.210 smallsmooth = 1.103 -[ultra-extended] -inherits = ['wd-9'] - -[wd-8.multiplies] +[shapeWidth.multiplies.blend.618] width = 1.236 # 618 for normal char stroke = 1.075 # Make strokes a little thicker sb = 1.539 @@ -296,10 +304,7 @@ rbalance = 1.236 tbalance = 1.154 smallsmooth = 1.075 -[extra-extended] -inherits = ['wd-8'] - -[wd-7.multiplies] +[shapeWidth.multiplies.blend.576] width = 1.152 # 576mem for normal char stroke = 1.050 # Make strokes a little thicker sb = 1.333 @@ -310,10 +315,7 @@ rbalance = 1.152 tbalance = 1.100 smallsmooth = 1.050 -[extended] -inherits = ['wd-7'] - -[wd-6.multiplies] +[shapeWidth.multiplies.blend.537] width = 1.074 # 537mem for normal char stroke = 1.023 # Make strokes a little thicker sb = 1.154 @@ -324,10 +326,7 @@ rbalance = 1.074 tbalance = 1.049 smallsmooth = 1.023 -[semi-extended] -inherits = ['wd-6'] - -[wd-4.multiplies] +[shapeWidth.multiplies.blend.466] width = 0.932 # 466mem for normal char stroke = 0.975 sb = 0.866 @@ -338,10 +337,7 @@ rbalance = 0.931 tbalance = 0.953 smallsmooth = 0.975 -[semi-condensed] -inherits = ['wd-4'] - -[wd-3.multiplies] +[shapeWidth.multiplies.blend.434] width = 0.868 # 434mem for normal char stroke = 0.952 sb = 0.750 @@ -352,8 +348,7 @@ rbalance = 0.868 tbalance = 0.909 smallsmooth = 0.952 -[condensed] -inherits = ['wd-3'] +###### Diversity [diversity-1] diversityM = 1.25 diff --git a/private-build-plans.sample.toml b/private-build-plans.sample.toml index a7b1ae681d..4b49ab8163 100644 --- a/private-build-plans.sample.toml +++ b/private-build-plans.sample.toml @@ -49,18 +49,20 @@ oblique = "oblique" # Override default building widths # When buildPlans..widths is absent, all widths would built and mapped to # default values. -# IMPORTANT : Currently "shape" property only supports integers between 3 and 9 (inclusive), while -# "menu" only supports integers between 1 and 9 (inclusive). -# If you decide to use custom widths you have to define all the widths you -# plan to use otherwise they will not be built. +# IMPORTANT : Currently "shape" property only supports numbers between 434 and 664 (inclusive), +# while "menu" only supports integers between 1 and 9 (inclusive). +# The "shape" parameter specifies the unit width, measured in 1/1000 em. The glyphs' +# width are equal to, or a simple multiple of the unit width. +# If you decide to use custom widths you have to define all the widths you plan to use, +# otherwise they will not be built. [buildPlans.iosevka-custom.widths.normal] -shape = 5 # Width grade of glyph shapes. NOT actual character width. -menu = 5 # Width grade for the font's names. NOT actual character width. +shape = 500 # Unit Width, measured in 1/1000 em. +menu = 5 # Width grade for the font's names. css = "normal" # "font-stretch' property of webfont CSS. [buildPlans.iosevka-custom.widths.extended] -shape = 7 +shape = 576 menu = 7 css = "expanded" diff --git a/support/param-blend.js b/support/param-blend.js deleted file mode 100644 index 0919e43bd1..0000000000 --- a/support/param-blend.js +++ /dev/null @@ -1,31 +0,0 @@ -"use strict"; - -const blend = require("./monotonic-interpolate"); - -module.exports = function (aspect, hive, params, sink) { - if (!hive || !hive.blend || !params) return; - - const block = hive.blend; - let keys = new Set(); - for (const grade in block) { - sink[grade] = block[grade]; - if (!isFinite(parseFloat(grade))) continue; - for (const key in block[grade]) { - if (block[grade][key] == null) continue; - keys.add(key); - } - } - - for (const key of keys) { - let xs = [], - ys = []; - for (const grade in block) { - if (!isFinite(parseFloat(grade))) continue; - if (block[grade][key] == null) continue; - xs.push(grade); - ys.push(block[grade][key]); - } - const fn = blend(xs, ys); - sink[key] = fn(params[aspect]); - } -}; diff --git a/support/parameters.js b/support/parameters.js new file mode 100644 index 0000000000..a95e71e600 --- /dev/null +++ b/support/parameters.js @@ -0,0 +1,100 @@ +"use strict"; + +const monotonicInterpolate = require("./monotonic-interpolate"); + +function build(parametersData, styles, blendArgs) { + const sink = {}; + for (const item of styles) intro(parametersData, item, blendArgs, sink); + return sink; +} +exports.build = build; + +function intro(source, style, blendArgs, sink) { + let hive = source[style]; + if (!hive) return; + hive = { ...hive }; + + if (hive.inherits) { + for (const hn of hive.inherits) intro(source, hn, blendArgs, sink); + delete hive.inherits; + } + if (hive.multiplies) { + const mu = hiveBlend(hive.multiplies, getBlendArg(blendArgs, style), sink); + for (const k in mu) sink[k] = (sink[k] || 0) * mu[k]; + delete hive.multiplies; + } + if (hive.adds) { + const mu = hiveBlend(hive.adds, getBlendArg(blendArgs, style), sink); + for (const k in mu) sink[k] = (sink[k] || 0) + mu[k]; + delete hive.adds; + } + if (hive.appends) { + const mu = hive.appends; + for (const k in mu) sink[k] = [...(sink[k] || []), ...mu[k]]; + delete hive.appends; + } + hive = hiveBlend(hive, getBlendArg(blendArgs, style), sink); + for (const k in hive) sink[k] = hive[k]; +} + +function getBlendArg(blendArgs, style) { + if (!blendArgs) return undefined; + return blendArgs[style]; +} + +function hiveBlend(hive, value, sink) { + if (!hive || !hive.blend || value == null) return hive; + + const generatedHive = {}; + const block = hive.blend; + let keys = new Set(); + for (const grade in block) { + sink[grade] = block[grade]; + if (!isFinite(parseFloat(grade))) continue; + for (const key in block[grade]) { + if (block[grade][key] == null) continue; + keys.add(key); + } + } + + for (const key of keys) { + let xs = [], + ys = []; + for (const grade in block) { + if (!isFinite(parseFloat(grade))) continue; + if (block[grade][key] == null) continue; + xs.push(grade); + ys.push(block[grade][key]); + } + generatedHive[key] = monotonicInterpolate(xs, ys)(value); + } + return generatedHive; +} + +function numericConfigExists(x) { + return x != null && isFinite(x); +} +function applyMetricOverride(para, mo) { + if (numericConfigExists(mo.leading)) { + para.leading = mo.leading; + } + if (numericConfigExists(mo.winMetricAscenderPad)) { + para.winMetricAscenderPad = mo.winMetricAscenderPad; + } + if (numericConfigExists(mo.winMetricDescenderPad)) { + para.winMetricDescenderPad = mo.winMetricDescenderPad; + } + if (numericConfigExists(mo.powerlineScaleY)) { + para.powerlineScaleY = mo.powerlineScaleY; + } + if (numericConfigExists(mo.powerlineScaleX)) { + para.powerlineScaleX = mo.powerlineScaleX; + } + if (numericConfigExists(mo.powerlineShiftY)) { + para.powerlineShiftY = mo.powerlineShiftY; + } + if (numericConfigExists(mo.powerlineShiftX)) { + para.powerlineShiftX = mo.powerlineShiftX; + } +} +exports.applyMetricOverride = applyMetricOverride; diff --git a/support/parameters.ptl b/support/parameters.ptl deleted file mode 100644 index 383afa9ecb..0000000000 --- a/support/parameters.ptl +++ /dev/null @@ -1,43 +0,0 @@ -import "./param-blend" as paramBlend - -export : define [build parametersData styles blendParams] : begin - local param {.} - - define [introStyle style] : begin - local hive parametersData.(style) - if (!hive) : return nothing - - if hive.inherits : foreach [h : items-of hive.inherits] : introStyle h - - foreach [k : items-of : Object.keys hive] : piecewise - (k === "multiplies") : foreach [k : items-of : Object.keys hive.multiplies] : begin - set param.(k) : param.(k) * hive.multiplies.(k) - (k === "adds") : foreach [k : items-of : Object.keys hive.adds] : begin - set param.(k) : param.(k) + hive.adds.(k) - (k === "appends") : foreach [k : items-of : Object.keys hive.appends] : begin - set param.(k) : (param.(k) || {}).concat(hive.appends.(k)) - true : set param.(k) hive.(k) - - paramBlend style hive blendParams param - - foreach [style : items-of styles] : introStyle style - return param - -extern isFinite -define [numericConfigExists x] : [isFinite x] && (x != null) - -export : define [applyMetricOverride para mo] : begin - if [numericConfigExists mo.leading] - set para.leading mo.leading - if [numericConfigExists mo.winMetricAscenderPad] - set para.winMetricAscenderPad mo.winMetricAscenderPad - if [numericConfigExists mo.winMetricDescenderPad] - set para.winMetricDescenderPad mo.winMetricDescenderPad - if [numericConfigExists mo.powerlineScaleY] - set para.powerlineScaleY mo.powerlineScaleY - if [numericConfigExists mo.powerlineScaleX] - set para.powerlineScaleX mo.powerlineScaleX - if [numericConfigExists mo.powerlineShiftY] - set para.powerlineShiftY mo.powerlineShiftY - if [numericConfigExists mo.powerlineShiftX] - set para.powerlineShiftX mo.powerlineShiftX diff --git a/verdafile.js b/verdafile.js index 4997c64543..a97dca2796 100644 --- a/verdafile.js +++ b/verdafile.js @@ -213,13 +213,18 @@ function getSuffixMapping(weights, slants, widths) { for (const wd in widths) { const suffix = makeSuffix(w, wd, s, "regular"); mapping[suffix] = { - hives: [`shapeWeight`, `s-${s}`, `wd-${widths[wd].shape}`], + hives: [`shapeWeight`, `s-${s}`, `shapeWidth`], weight: w, shapeWeight: nValidate("Shape weight of " + w, weights[w].shape, vlShapeWeight), cssWeight: nValidate("CSS weight of " + w, weights[w].css, vlCssWeight), menuWeight: nValidate("Menu weight of " + w, weights[w].menu, vlMenuWeight), width: wd, - shapeWidth: nValidate("Shape width of " + wd, widths[wd].shape, vlShapeWidth), + shapeWidth: nValidate( + "Shape width of " + wd, + widths[wd].shape, + vlShapeWidth, + fixShapeWidth + ), cssStretch: widths[wd].css || wd, menuWidth: nValidate("Menu width of " + wd, widths[wd].menu, vlMenuWidth), slant: s, @@ -248,9 +253,10 @@ function validateRecommendedWeight(w, value, label) { } } -function nValidate(key, v, f) { +function nValidate(key, v, f, ft) { + if (ft) v = ft(v); if (typeof v !== "number" || !isFinite(v) || (f && !f(v))) { - throw new TypeError(`${key} = "${v}" is not a valid number.`); + throw new TypeError(`${key} = ${v} is not a valid number.`); } return v; } @@ -263,8 +269,23 @@ function vlCssWeight(x) { function vlMenuWeight(x) { return vlCssWeight(x); } +const g_widthFixupMemory = new Map(); +function fixShapeWidth(x) { + if (x >= 3 && x <= 9) { + if (g_widthFixupMemory.has(x)) return g_widthFixupMemory.get(x); + const xCorrected = Math.round(500 * Math.pow(Math.sqrt(576 / 500), x - 5)); + echo.warn( + `The build plan is using legacy width grade ${x}. ` + + `Converting to unit width ${xCorrected}.` + ); + g_widthFixupMemory.set(x, xCorrected); + return xCorrected; + } else { + return x; + } +} function vlShapeWidth(x) { - return x >= 3 && x <= 9 && x % 1 === 0; + return x >= 433 && x <= 665; } function vlMenuWidth(x) { return x >= 1 && x <= 9 && x % 1 === 0;