diff --git a/README.md b/README.md index fa47947c7..1f3855ccd 100644 --- a/README.md +++ b/README.md @@ -53,156 +53,137 @@ Monospace Iosevka contains various stylistic sets to change the shape of certain - - + - - + + - - + - - + + - - + - - + + - - + - - + + - - + - - + + - - + - - + + - - + - - + + - - + - - + + - - + - - + + - - + - - + + - - + - - + + - - + - - + + - - + - - + + - - + - - + + - - + - - + + - - + - - + + - - + - - + + - - + - - + + - - + - - + +
ss01Andale Mono Styless01 — Andale Mono Style
ss02Anonymous Pro Styless02 — Anonymous Pro Style
ss03Consolas Styless03 — Consolas Style
ss04Menlo Styless04 — Menlo Style
ss05Fira Mono Styless05 — Fira Mono Style
ss06Liberation Mono Styless06 — Liberation Mono Style
ss07Monaco Styless07 — Monaco Style
ss08Pragmata Pro Styless08 — Pragmata Pro Style
ss09Source Code Pro Styless09 — Source Code Pro Style
ss10Envy Code R Styless10 — Envy Code R Style
ss11X Window Styless11 — X Window Style
ss12Ubuntu Mono Styless12 — Ubuntu Mono Style
ss13Lucida Styless13 — Lucida Style
ss14JetBrains Mono Styless14 — JetBrains Mono Style
ss15IBM Plex Mono Styless15 — IBM Plex Mono Style
ss16PT Mono Styless16 — PT Mono Style
ss17Recursive Mono Styless17 — Recursive Mono Style
ss18Input Mono Styless18 — Input Mono Style
ss20Curly Styless20 — Curly Style
@@ -2971,16 +2952,23 @@ Subsection `variants` is used to configure character variants in the font. Prope Subsection `weights` is used to change the weight grades that the custom family needs. It is a dictionary of sub-objects with properties: -* `shape`: Integer, configures the weight grade of the glyphs' shapes. +* `shape`: Number, configures the weight grade of the glyphs' shapes. * `menu`: Integer, configures the weight grade used when naming fonts. * `css`: Integer, configures the weight grade used in web font CSS. -Subsection `widths` is used to change the weight grades that the custom family needs. It is a dictionary of sub-objects with properties: +Subsection `widths` is used to change the width grades that the custom family needs. It is a dictionary of sub-objects with properties: -* `shape`: Integer, configures the width of the glyphs' shapes, measured in 1/1000 em. +* `shape`: Number, configures the width of the glyphs' shapes, measured in 1/1000 em. * `menu`: Integer, configures the width grade used when naming fonts. The valid values are `1` to `9`, inclusive. * `css`: String, configures the [font-stretch](https://developer.mozilla.org/en-US/docs/Web/CSS/font-stretch) value used in web font CSS. +Subsection `slopes` is used to change the slope angles and grades that the custom family needs. It is a dictionary of sub-objects with properties: + +* `angle`: Number, configures the slope angle in degrees. The valid vales are `0` to `15`, inclusive. +* `shape`: String from `upright`, `italic` or `oblique`. Configures the slope used for variant selection. +* `menu`: String from `upright`, `italic` or `oblique`. Configures the slope grade used when naming fonts. +* `css`: String from `normal`, `italic` or `oblique`. Configures the [CSS font-style](https://developer.mozilla.org/zh-CN/docs/Web/CSS/font-style) value. + Subsection `slopes` is a simple string-to-string dictionary maps slopes (`upright`, `italic` or `oblique`) to [CSS font-style](https://developer.mozilla.org/zh-CN/docs/Web/CSS/font-style) values, represented in string. #### Compatibility Ligatures diff --git a/build-plans.toml b/build-plans.toml index 42b0cb1fe..c6fdb63e2 100644 --- a/build-plans.toml +++ b/build-plans.toml @@ -990,11 +990,6 @@ from = ["iosevka-aile"] release = true from = ["iosevka-etoile"] -[collectConfig] -distinguishWeights = true -distinguishWidths = false -distinguishSlope = false - ################################################################################################### # Weight mappings (style => shape weight, menu weight, CSS weight) # Shape weight : affects the shape of the glyphs @@ -1048,13 +1043,24 @@ shape = 900 menu = 900 css = 900 -# slope mappings (style => CSS) -# NOTE: This mapping does NOT affect the font's metadata, only affects -# the webfont CSS. Change `params/parameters.toml` instead. -[slopes] -upright = "normal" -oblique = "oblique" -italic = "italic" +# slope mappings (style => slope angle, shape slope grade, menu slope, CSS slope) +[slopes.upright] +angle = 0 +shape = "upright" +menu = "upright" +css = "normal" + +[slopes.oblique] +angle = 9.4 +shape = "oblique" +menu = "oblique" +css = "oblique" + +[slopes.italic] +angle = 9.4 +shape = "italic" +menu = "italic" +css = "italic" # Width mappings (style => shape width, menu width, CSS stretch) # Shape width : affects the shape of the glyphs diff --git a/changes/8.0.0.md b/changes/8.0.0.md new file mode 100644 index 000000000..e38f22dc6 --- /dev/null +++ b/changes/8.0.0.md @@ -0,0 +1,22 @@ + * \[**Breaking**\] Add support for slope customization (#599, #1165). + - Slope customization format has a major change, giving ability to customize slope grade used for variant selection, as well as slope angle. + - The format will look like this: + ``` toml + [buildPlans.iosevka-custom.slopes.upright] + angle = 0 # Angle in degrees. Valid range [0, 15] + shape = "upright" # Slope grade used for shape selection. `upright` | `oblique` | `italic` + menu = "upright" # Slope grade used for naming. `upright` | `oblique` | `italic` + css = "normal" # Slope grade used for webfont CSS. `normal` | `oblique` | `italic` + + [buildPlans.iosevka-custom.slopes.oblique] + angle = 9.4 + shape = "oblique" + menu = "oblique" + css = "oblique" + + [buildPlans.iosevka-custom.slopes.italic] + angle = 9.4 + shape = "italic" + menu = "italic" + css = "italic" + ``` \ No newline at end of file diff --git a/font-src/index.js b/font-src/index.js index 628515591..dacc60bf6 100644 --- a/font-src/index.js +++ b/font-src/index.js @@ -26,6 +26,7 @@ async function getParameters() { const PARAMETERS_TOML = path.resolve(__dirname, "../params/parameters.toml"); const WEIGHTS_TOML = path.resolve(__dirname, "../params/shape-weight.toml"); const WIDTHS_TOML = path.resolve(__dirname, "../params/shape-width.toml"); + const SLOPES_TOML = path.resolve(__dirname, "../params/shape-slope.toml"); const PRIVATE_TOML = path.resolve(__dirname, "../params/private-parameters.toml"); const VARIANTS_TOML = path.resolve(__dirname, "../params/variants.toml"); const LIGATIONS_TOML = path.resolve(__dirname, "../params/ligation-set.toml"); @@ -35,6 +36,7 @@ async function getParameters() { await tryParseToml(PARAMETERS_TOML), await tryParseToml(WEIGHTS_TOML), await tryParseToml(WIDTHS_TOML), + await tryParseToml(SLOPES_TOML), fs.existsSync(PRIVATE_TOML) ? await tryParseToml(PRIVATE_TOML) : {} ); const rawVariantsData = await tryParseToml(VARIANTS_TOML); diff --git a/font-src/support/parameters.js b/font-src/support/parameters.js index dcaa64000..56231d55e 100644 --- a/font-src/support/parameters.js +++ b/font-src/support/parameters.js @@ -11,6 +11,7 @@ function initPara(data, argv) { applyBlendingParam(argv, para, data, "shapeWeight", "weight"); applyBlendingParam(argv, para, data, "shapeWidth", "width"); + applyBlendingParam(argv, para, data, "shapeSlopeAngle", "slopeAngle"); applyAlternatesParam(argv, para, data, "slope", "slope"); if (argv.featureControl.noCvSs) para.enableCvSs = false; diff --git a/package.json b/package.json index c960e33fa..25c8eca23 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "iosevka", - "version": "7.3.3", + "version": "8.0.0", "main": "./font-src/index.js", "scripts": { "build": "node utility/ensure-verda-exists && verda -f verdafile.js", diff --git a/params/parameters.toml b/params/parameters.toml index 477d8ffb9..e7e6385de 100644 --- a/params/parameters.toml +++ b/params/parameters.toml @@ -127,8 +127,6 @@ diversityII = 0.50 ###### Slopes [slope-italic] isItalic = true -slopeAngle = 9.4 [slope-oblique] isOblique = true -slopeAngle = 9.4 diff --git a/params/shape-slope.toml b/params/shape-slope.toml new file mode 100644 index 000000000..38bad1ab8 --- /dev/null +++ b/params/shape-slope.toml @@ -0,0 +1,8 @@ +[shapeSlopeAngle.blend.0] +slopeAngle = 0 + +[shapeSlopeAngle.blend.-15] +slopeAngle = -15 + +[shapeSlopeAngle.blend.15] +slopeAngle = 15 diff --git a/private-build-plans.sample.toml b/private-build-plans.sample.toml index 1a4fcae66..2dbb8c905 100644 --- a/private-build-plans.sample.toml +++ b/private-build-plans.sample.toml @@ -67,14 +67,25 @@ css = 700 ################################################################################################### # Override default building slope sets -# Format: = <"normal"|"italic"|"oblique"> # When this section is absent, all slopes would be built. -[buildPlans.iosevka-custom.slopes] -upright = "normal" -italic = "italic" -oblique = "oblique" +[buildPlans.iosevka-custom.slopes.upright] +angle = 0 # Angle in degrees. Valid range [0, 15] +shape = "upright" # Slope grade used for shape selection. `upright` | `oblique` | `italic` +menu = "upright" # Slope grade used for naming. `upright` | `oblique` | `italic` +css = "normal" # Slope grade used for webfont CSS. `normal` | `oblique` | `italic` +[buildPlans.iosevka-custom.slopes.oblique] +angle = 9.4 +shape = "oblique" +menu = "oblique" +css = "oblique" + +[buildPlans.iosevka-custom.slopes.italic] +angle = 9.4 +shape = "italic" +menu = "italic" +css = "italic" # End slope section ################################################################################################### diff --git a/utility/amend-readme/index.js b/utility/amend-readme/index.js index 137ed1ddb..7802d9bcb 100644 --- a/utility/amend-readme/index.js +++ b/utility/amend-readme/index.js @@ -39,18 +39,13 @@ async function processSsOt() { if (!ss.rank) continue; { md.log(``); - md.log(`${ss.tag}`); - md.log(`${ss.description}`); + md.log(`${ss.tag} — ${ss.description}`); md.log(``); } { md.log(``); - md.log( - `` - ); - md.log( - `` - ); + md.log(``); + md.log(``); md.log(``); } } diff --git a/verdafile.js b/verdafile.js index a1a519412..4d7504a1a 100644 --- a/verdafile.js +++ b/verdafile.js @@ -36,7 +36,6 @@ const webfontFormatsPages = [["woff2", "woff2"]]; const WIDTH_NORMAL = "normal"; const WEIGHT_NORMAL = "regular"; const SLOPE_NORMAL = "upright"; -const SLOPE_OBLIQUE = "oblique"; const DEFAULT_SUBFAMILY = "regular"; const BUILD_PLANS = "build-plans.toml"; @@ -189,8 +188,9 @@ const FontInfoOf = computed.group("metadata:font-info-of", async (target, fileNa serifs: bp.serifs || null, spacing: bp.spacing || null, weight: sfi.shapeWeight, - slope: sfi.slope, - width: sfi.shapeWidth + width: sfi.shapeWidth, + slope: sfi.shapeSlope, + slopeAngle: sfi.shapeSlopeAngle }, // Menu menu: { @@ -228,23 +228,28 @@ function getSuffixMapping(weights, slopes, widths) { return mapping; } function getSuffixMappingItem(weights, w, slopes, s, widths, wd) { + const weightDef = wwsDefValidate("Weight definition of " + s, weights[w]); + const widthDef = wwsDefValidate("Width definition of " + s, widths[wd]); + const slopeDef = wwsDefValidate("Slope definition of " + s, slopes[s]); return { // Weights 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), + shapeWeight: nValidate("Shape weight of " + w, weightDef.shape, VlShapeWeight), + cssWeight: nValidate("CSS weight of " + w, weightDef.css, VlCssWeight), + menuWeight: nValidate("Menu weight of " + w, weightDef.menu, VlMenuWeight), // Widths width: wd, - shapeWidth: nValidate("Shape width of " + wd, widths[wd].shape, VlShapeWidth), - cssStretch: widths[wd].css || wd, - menuWidth: nValidate("Menu width of " + wd, widths[wd].menu, VlMenuWidth), + shapeWidth: nValidate("Shape width of " + wd, widthDef.shape, VlShapeWidth), + cssStretch: sValidate("CSS stretch of " + wd, widthDef.css, VlCssFontStretch), + menuWidth: nValidate("Menu width of " + wd, widthDef.menu, VlMenuWidth), // Slopes slope: s, - cssStyle: slopes[s] || s, - menuSlope: slopes[s] || s + shapeSlope: sValidate("Shape slope of " + s, slopeDef.shape, VlShapeSlope), + shapeSlopeAngle: nValidate("Angle of " + s, slopeDef.angle, VlSlopeAngle), + cssStyle: sValidate("CSS style of " + s, slopeDef.css, VlCssStyle), + menuSlope: sValidate("Menu slope of " + s, slopeDef.menu, VlShapeSlope) }; } @@ -366,18 +371,11 @@ const DistWoff2 = file.make( const CollectPlans = computed(`metadata:collect-plans`, async target => { const [rawPlans] = await target.need(RawPlans); - return await getCollectPlans( - target, - rawPlans.collectPlans, - rawPlans.collectConfig, - fnStandardTtc - ); + return await getCollectPlans(target, rawPlans.collectPlans); }); -async function getCollectPlans(target, rawCollectPlans, config, fnFileName) { - const glyfTtcComposition = {}, - ttcComposition = {}, - plans = {}; +async function getCollectPlans(target, rawCollectPlans) { + const plans = {}; let allCollectableGroups = new Set(); for (const collectPrefix in rawCollectPlans) { @@ -392,65 +390,60 @@ async function getCollectPlans(target, rawCollectPlans, config, fnFileName) { } for (const collectPrefix in amendedRawCollectPlans) { - const groupFileList = new Set(); + const glyfTtcComposition = {}; + const ttcComposition = {}; const collect = amendedRawCollectPlans[collectPrefix]; if (!collect || !collect.from || !collect.from.length) continue; for (const prefix of collect.from) { const [gri] = await target.need(BuildPlanOf(prefix)); const ttfFileNameSet = new Set(gri.targets); - const suffixMapping = getSuffixMapping(gri.weights, gri.slopes, gri.widths); - for (const suffix in suffixMapping) { - const sfi = suffixMapping[suffix]; - const ttcFileName = fnFileName( - config, - collectPrefix, - sfi.weight, - sfi.width, - sfi.slope - ); - const glyfTtcFileName = fnFileName( - { ...config, distinguishWidths: true, distinguishWhetherUpright: true }, - collectPrefix, - sfi.weight, - sfi.width, - sfi.slope - ); + const suffixMap = getSuffixMapping(gri.weights, gri.slopes, gri.widths); + for (const suffix in suffixMap) { + const sfi = suffixMap[suffix]; const ttfTargetName = makeFileName(prefix, suffix); if (!ttfFileNameSet.has(ttfTargetName)) continue; + const glyfTtcFileName = fnStandardTtc(true, collectPrefix, suffixMap, sfi); if (!glyfTtcComposition[glyfTtcFileName]) glyfTtcComposition[glyfTtcFileName] = []; glyfTtcComposition[glyfTtcFileName].push({ dir: prefix, file: ttfTargetName }); + + const ttcFileName = fnStandardTtc(false, collectPrefix, suffixMap, sfi); if (!ttcComposition[ttcFileName]) ttcComposition[ttcFileName] = []; ttcComposition[ttcFileName].push(glyfTtcFileName); - - groupFileList.add(ttcFileName); } } plans[collectPrefix] = { - ttcContents: [...groupFileList], + glyfTtcComposition, + ttcComposition, groupDecomposition: [...collect.from], inRelease: !!collect.release, isAmended: !!collect.isAmended }; } - return { glyfTtcComposition, ttcComposition, plans }; + return plans; } -function fnStandardTtc(collectConfig, prefix, w, wd, s) { - const ttcSuffix = makeSuffix( - collectConfig.distinguishWeights ? w : WEIGHT_NORMAL, - collectConfig.distinguishWidths ? wd : WIDTH_NORMAL, - collectConfig.distinguishSlope - ? s - : collectConfig.distinguishWhetherUpright - ? s === SLOPE_NORMAL - ? SLOPE_NORMAL - : SLOPE_OBLIQUE - : SLOPE_NORMAL, - DEFAULT_SUBFAMILY - ); - return `${prefix}-${ttcSuffix}`; + +function fnStandardTtc(fIsGlyfTtc, prefix, suffixMapping, sfi) { + let optimalSfi = null, + maxScore = 0; + for (const ttcSuffix in suffixMapping) { + const sfiT = suffixMapping[ttcSuffix]; + if (sfi.shapeWeight !== sfiT.shapeWeight) continue; + if (sfi.shapeWidth !== sfiT.shapeWidth) continue; + if (fIsGlyfTtc && sfi.shapeSlopeAngle !== sfiT.shapeSlopeAngle) continue; + const score = + (sfiT.weight === WEIGHT_NORMAL ? 1 : 0) + + (sfiT.width === WIDTH_NORMAL ? 1 : 0) + + (sfiT.slope === SLOPE_NORMAL ? 1 : 0); + if (!optimalSfi || score > maxScore) { + maxScore = score; + optimalSfi = sfiT; + } + } + if (!optimalSfi) throw new Error("Unreachable: TTC name decision"); + return `${prefix}-${makeSuffix(optimalSfi.weight, optimalSfi.width, optimalSfi.slope)}`; } /////////////////////////////////////////////////////////// @@ -464,25 +457,25 @@ const CollectedSuperTtcFile = file.make( cgr => `${DIST_SUPER_TTC}/${cgr}.ttc`, async (target, out, cgr) => { const [cp] = await target.need(CollectPlans, de(out.dir)); - const parts = Array.from(new Set(cp.plans[cgr].ttcContents)); - const [inputs] = await target.need(parts.map(pt => CollectedTtcFile(cgr, pt))); + const parts = Array.from(Object.keys(cp[cgr].glyfTtcComposition)); + const [inputs] = await target.need(parts.map(pt => GlyfTtc(cgr, pt))); await buildCompositeTtc(out, inputs); } ); const CollectedTtcFile = file.make( - (cgr, f) => `${BUILD}/ttc-collect/${cgr}/ttc/${f}.ttc`, - async (target, out, gr, f) => { + (cgr, f) => `${BUILD}/ttc-collect/${cgr}/${f}.ttc`, + async (target, out, cgr, f) => { const [cp] = await target.need(CollectPlans, de`${out.dir}`); - const parts = Array.from(new Set(cp.ttcComposition[f])); - const [inputs] = await target.need(parts.map(pt => GlyfTtc(gr, pt))); + const parts = Array.from(new Set(cp[cgr].ttcComposition[f])); + const [inputs] = await target.need(parts.map(pt => GlyfTtc(cgr, pt))); await buildCompositeTtc(out, inputs); } ); const GlyfTtc = file.make( (cgr, f) => `${BUILD}/glyf-ttc/${cgr}/${f}.ttc`, - async (target, out, gr, f) => { + async (target, out, cgr, f) => { const [cp] = await target.need(CollectPlans); - const parts = cp.glyfTtcComposition[f]; + const parts = cp[cgr].glyfTtcComposition[f]; await buildGlyphSharingTtc(target, parts, out); } ); @@ -508,13 +501,13 @@ async function buildGlyphSharingTtc(target, parts, out) { const TtcArchiveFile = file.make( (cgr, version) => `${ARCHIVE_DIR}/ttc-${cgr}-${version}.zip`, async (target, out, cgr) => { - const [collectPlans] = await target.need(CollectPlans, de`${out.dir}`); - const ttcFiles = Array.from(new Set(collectPlans.plans[cgr].ttcContents)); + const [cp] = await target.need(CollectPlans, de`${out.dir}`); + const ttcFiles = Array.from(Object.keys(cp[cgr].ttcComposition)); await target.need(ttcFiles.map(pt => CollectedTtcFile(cgr, pt))); // Packaging await rm(out.full); - await cd(`${BUILD}/ttc-collect/${cgr}/ttc`).run( + await cd(`${BUILD}/ttc-collect/${cgr}`).run( ["7z", "a"], ["-tzip", "-r", "-mx=9"], `../../../../${out.full}`, @@ -795,7 +788,7 @@ phony(`clean`, async () => { phony(`release`, async target => { const [collectPlans] = await target.need(CollectPlans); let goals = []; - for (const [cgr, plan] of Object.entries(collectPlans.plans)) { + for (const [cgr, plan] of Object.entries(collectPlans)) { if (!plan.inRelease) continue; goals.push(ReleaseGroup(cgr)); } @@ -804,7 +797,7 @@ phony(`release`, async target => { }); const ReleaseGroup = phony.group("release-group", async (target, cgr) => { const [version, collectPlans] = await target.need(Version, CollectPlans); - const subGroups = collectPlans.plans[cgr].groupDecomposition; + const subGroups = collectPlans[cgr].groupDecomposition; let goals = [TtcArchiveFile(cgr, version), SuperTtcArchiveFile(cgr, version)]; for (const gr of subGroups) { @@ -887,6 +880,7 @@ const Parameters = task(`meta:parameters`, async target => { sfu`params/parameters.toml`, sfu`params/shape-weight.toml`, sfu`params/shape-width.toml`, + sfu`params/shape-slope.toml`, ofu`params/private-parameters.toml`, sfu`params/variants.toml`, sfu`params/ligation-set.toml` @@ -937,6 +931,13 @@ function validateRecommendedWeight(w, value, label) { } // Value validation +function wwsDefValidate(key, obj) { + if (!obj || typeof obj === "string") { + throw new TypeError(`${key} is invalid.`); + } + return obj; +} + function nValidate(key, v, validator) { if (validator.fixup) v = validator.fix(v); if (typeof v !== "number" || !isFinite(v) || !validator.validate(v)) { @@ -968,3 +969,26 @@ const VlShapeWidth = { } }; const VlMenuWidth = { validate: x => x >= 1 && x <= 9 && x % 1 === 0 }; +const VlSlopeAngle = { validate: x => x >= 0 && x <= 15 }; + +function sValidate(key, v, validator) { + if (validator.fixup) v = validator.fix(v); + if (typeof v !== "string" || !validator.validate(v)) { + throw new TypeError(`${key} = ${v} is not a valid string.`); + } + return v; +} +const VlShapeSlope = { validate: x => x === "upright" || x === "oblique" || x === "italic" }; +const VlCssStyle = { validate: x => x === "normal" || x === "oblique" || x === "italic" }; +const VlCssFontStretch = { + validate: x => + x == "ultra-condensed" || + x == "extra-condensed" || + x == "condensed" || + x == "semi-condensed" || + x == "normal" || + x == "semi-expanded" || + x == "expanded" || + x == "extra-expanded" || + x == "ultra-expanded" +};