diff --git a/font-src/glyphs/letter/latin-ext/eszet.ptl b/font-src/glyphs/letter/latin-ext/eszet.ptl index 79860d732..ca01c94eb 100644 --- a/font-src/glyphs/letter/latin-ext/eszet.ptl +++ b/font-src/glyphs/letter/latin-ext/eszet.ptl @@ -10,7 +10,6 @@ glyph-block Letter-Latin-Eszet : begin glyph-block-import CommonShapes glyph-block-import Common-Derivatives glyph-block-import Letter-Shared-Shapes : FlatHookDepth - glyph-block-import Letter-Latin-S : SNeck local ymiddle : [mix 0 CAP 0.5] - HalfStroke local ymiddleCap : [mix 0 CAP 0.54] - HalfStroke diff --git a/font-src/glyphs/letter/latin/s.ptl b/font-src/glyphs/letter/latin/s.ptl index d15ecd20f..8a3f9efa4 100644 --- a/font-src/glyphs/letter/latin/s.ptl +++ b/font-src/glyphs/letter/latin/s.ptl @@ -23,13 +23,11 @@ glyph-block Letter-Latin-S : begin define SLAB-INWARD 2 define SOBot OX - define [SmallSTensionCoeff stroke] : mix [StrokeWidthBlend 1 0.375 stroke] 1 : clamp 0 1 : Math.pow (Width / (UPM * 0.55)) 0.5 - define [SmallSDistCoeff] : Math.min 0.25 (0.1 * [Math.max 1 (Width / HalfUPM)]) - glyph-block-export SNeck - define [SNeck] : params [[stroke Stroke] [slantCoeff 0] [tensionCoeff 1] [dist 0.1]] : begin - define strokeCoeff : StrokeWidthBlend 0 1 stroke - define tension : tensionCoeff * (0.5 - 0.4 * dist - 0.005 * strokeCoeff + slantCoeff * TanSlope) + define [SNeck] : params [[stroke Stroke]] : begin + define tensionCoeff : mix [StrokeWidthBlend 1 0.375 stroke] 1 : clamp 0 1 : Math.pow (Width / (UPM * 0.55)) 0.5 + define dist : Math.min 0.25 (0.1 * [Math.max 1 (Width / HalfUPM)]) + define tension : tensionCoeff * (0.5 - 0.4 * dist) return : alsoThruThem {{(0.5 - dist) tension} {(0.5 + dist) (1 - tension)}} [widths.center stroke] define [SStrokeDollarInterruptGap deltaX top bot gap stroke refSwEss] : begin @@ -112,7 +110,7 @@ glyph-block Letter-Latin-S : begin hookstart (XH - O) (sw -- stroke) (swItalicAdj -- Stroke) g4 (SB - OX) (XH - smooth) - SNeck (stroke -- ess) (slantCoeff -- -0.02) (dist -- [SmallSDistCoeff]) (tensionCoeff -- [SmallSTensionCoeff stroke]) + SNeck (stroke -- ess) g4 (RightSB + OX - SOBot) (smooth) [widths 0 stroke] match sb @@ -155,7 +153,7 @@ glyph-block Letter-Latin-S : begin g4 (RightSB + OX) (XH - SHook) hookstart (XH - O) (sw -- stroke) (swItalicAdj -- Stroke) g4 (SB - OX) (XH - smooth) - SNeck (stroke -- ess) (slantCoeff -- -0.02) (dist -- [SmallSDistCoeff]) (tensionCoeff -- [SmallSTensionCoeff stroke]) + SNeck (stroke -- ess) g4 (RightSB + OX - SOBot) (smooth) [widths 0 stroke] arcvh flat (Middle - CorrectionOMidS) 0 [widths 0 Stroke] @@ -175,7 +173,7 @@ glyph-block Letter-Latin-S : begin g4 (SB - OX) (XH - SHook) [widths.rhs stroke] hookstart (XH - O) (sw -- stroke) (swItalicAdj -- Stroke) g4 (RightSB + OX) (XH - smooth) - SNeck (stroke -- ess) (slantCoeff -- 0.02) (dist -- [SmallSDistCoeff]) (tensionCoeff -- [SmallSTensionCoeff stroke]) + SNeck (stroke -- ess) g4 (SB - OX + SOBot) (smooth) [widths stroke 0] match sb [Just SLAB-CLASSICAL] : SerifedArcEnd_LtrLhs RightSB Middle 0 stroke SHook diff --git a/font-src/kits/spiro-kit.ptl b/font-src/kits/spiro-kit.ptl index eb1d14a09..96dbd6051 100644 --- a/font-src/kits/spiro-kit.ptl +++ b/font-src/kits/spiro-kit.ptl @@ -185,6 +185,11 @@ export : define [SetupBuilders args] : begin define [convertSpiroToBezier knots closed ctx] : begin return : SpiroJs.spiroToBezierOnContext knots closed ctx CurveUtil.SPIRO_PRECISION + define [iterateNormals s closed] : begin + local knotsP2 : s.getPass2Knots closed [fallback s.contrast Contrast] + local s2 : new SpiroExpansionContext2 s.controlKnots s.gizmo + SpiroJs.spiroToArcsOnContext knotsP2 closed s2 + define [dispiro] : let [args : {}.slice.call arguments 0] : lambda [] : begin define CLOSED true local s : new SpiroExpansionContext1 (this.gizmo || GlobalTransform) @@ -195,10 +200,8 @@ export : define [SetupBuilders args] : begin if af : af.apply this args SpiroJs.spiroToArcsOnContext knots closed s - local knotsP2 : s.getPass2Knots closed [fallback s.contrast Contrast] - # console.log knotsP2 - local s2 : new SpiroExpansionContext2 s.controlKnots s.gizmo - SpiroJs.spiroToArcsOnContext knotsP2 closed s2 + iterateNormals s closed + iterateNormals s closed local {.lhs lhs .rhs rhs} : s.expand [fallback s.contrast Contrast] if closed : then diff --git a/font-src/meta/aesthetics.ptl b/font-src/meta/aesthetics.ptl index 5729b7f09..296f172a5 100644 --- a/font-src/meta/aesthetics.ptl +++ b/font-src/meta/aesthetics.ptl @@ -139,7 +139,7 @@ export : define [calculateMetrics para] : begin define PeriodRadius : PeriodSize / 2 define SideJut : Jut - HalfStroke * HVContrast - define [StrokeWidthBlend min max sw] : linreg 18 min 126 max [fallback sw Stroke] + define [StrokeWidthBlend min max sw] : linreg para.canonicalStrokeWidthMin min para.canonicalStrokeWidthMax max [fallback sw Stroke] define SmoothAdjust : StrokeWidthBlend 80 144 define [SmoothAOf smooth width] : smooth - TanSlope * SmoothAdjust / Width * width diff --git a/font-src/support/parameters.js b/font-src/support/parameters.js index 2864e01dc..65098227b 100644 --- a/font-src/support/parameters.js +++ b/font-src/support/parameters.js @@ -87,8 +87,11 @@ function getBlendArg(blendArgs, style) { function hiveBlend(hive, value) { if (!hive || !hive.blend || value == null) return hive; - const generatedHive = {}; const block = hive.blend; + delete hive.blend; + + const generatedHive = { ...hive }; + let keys = new Set(); for (const grade in block) { if (!isFinite(parseFloat(grade))) continue; diff --git a/font-src/support/spiro-expand.js b/font-src/support/spiro-expand.js index 5712f79be..2c0e197b8 100644 --- a/font-src/support/spiro-expand.js +++ b/font-src/support/spiro-expand.js @@ -1,6 +1,17 @@ -const Transform = require("./transform"); const { linreg } = require("./utils"); +class BiKnot { + constructor(type, x, y, d1, d2) { + this.type = type; + this.x = x; + this.y = y; + this.d1 = d1; + this.d2 = d2; + this.origTangent = null; + this.proposedNormal = null; + } +} + class SpiroExpansionContext1 { constructor(gizmo) { this.gizmo = gizmo; @@ -13,30 +24,22 @@ class SpiroExpansionContext1 { moveTo(x, y, unimportant) { if (unimportant) return; if (!isFinite(x) || !isFinite(y)) throw new Error("NaN detected."); - this.controlKnots.push({ - type: "g4", - d1: this.defaultD1, - d2: this.defaultD2, - ...this.gizmo.apply({ x, y }) - }); + const tfZ = this.gizmo.apply({ x, y }); + this.controlKnots.push(new BiKnot("g2", tfZ.x, tfZ.y, this.defaultD1, this.defaultD2)); } arcTo(arc, x, y) { if (!isFinite(x) || !isFinite(y)) throw new Error("NaN detected."); const k0 = this.controlKnots[this.controlKnots.length - 1]; if (!k0) throw new Error("Unreachable: lineTo called before moveTo"); - if (k0.normalAngle == null) { - const tfDerive0 = this.gizmo.applyOffset({ x: arc.deriveX0, y: arc.deriveY0 }); - k0.normalAngle = Math.PI / 2 + Math.atan2(tfDerive0.y, tfDerive0.x); + if (k0.origTangent == null) { + k0.origTangent = this.gizmo.applyOffset({ x: arc.deriveX0, y: arc.deriveY0 }); } { const tfDerive1 = this.gizmo.applyOffset({ x: arc.deriveX1, y: arc.deriveY1 }); - this.controlKnots.push({ - type: "g4", - d1: k0.d1, - d2: k0.d2, - ...this.gizmo.apply({ x, y }), - normalAngle: Math.PI / 2 + Math.atan2(tfDerive1.y, tfDerive1.x) - }); + const tfZ = this.gizmo.apply({ x, y }); + const bz = new BiKnot("g2", tfZ.x, tfZ.y, k0.d1, k0.d2); + bz.origTangent = tfDerive1; + this.controlKnots.push(bz); } } setWidth(l, r) { @@ -87,8 +90,8 @@ class SpiroExpansionContext1 { dx = knot.proposedNormal.x; dy = knot.proposedNormal.y; } else { - dx = normalX(knot.normalAngle, contrast); - dy = normalY(knot.normalAngle, contrast); + dx = normalX(knot.origTangent, contrast); + dy = normalY(knot.origTangent, contrast); } lhs[j] = { type: knot.type, @@ -155,24 +158,27 @@ class SpiroExpansionContext2 { } arcTo(arc, x, y) { if (this.nKnotsProcessed === 1) { - const angle = computeNormalAngle(this.gizmo, arc.deriveX0, arc.deriveY0); - if (isFinite(angle)) this.controlKnots[0].normalAngle = angle; + const d = this.gizmo.applyOffset({ x: arc.deriveX0, y: arc.deriveY0 }); + if (isTangentValid(d)) this.controlKnots[0].origTangent = d; else throw new Error("NaN angle detected."); } { - const angle = computeNormalAngle(this.gizmo, arc.deriveX1, arc.deriveY1); - if (isFinite(angle)) this.controlKnots[this.nKnotsProcessed].normalAngle = angle; + const d = this.gizmo.applyOffset({ x: arc.deriveX1, y: arc.deriveY1 }); + if (isTangentValid(d)) this.controlKnots[this.nKnotsProcessed].origTangent = d; else throw new Error("NaN angle detected."); } this.nKnotsProcessed += 1; } } -function normalX(angle, contrast) { - return Math.cos(angle) * contrast; +function isTangentValid(d) { + return isFinite(d.x) && isFinite(d.y); } -function normalY(angle) { - return Math.sin(angle); +function normalX(tangent, contrast) { + return contrast * (-tangent.y / Math.hypot(tangent.x, tangent.y)); +} +function normalY(tangent) { + return tangent.x / Math.hypot(tangent.x, tangent.y); } function reverseKnotType(ty) { return ty === "left" ? "right" : ty === "right" ? "left" : ty; diff --git a/params/shape-weight.toml b/params/shape-weight.toml index c36e0351a..17c216a85 100644 --- a/params/shape-weight.toml +++ b/params/shape-weight.toml @@ -1,3 +1,7 @@ +[shapeWeight] +canonicalStrokeWidthMin = 18 # sync with 100 entry below +canonicalStrokeWidthMax = 108 # sync with 900 entry below + [shapeWeight.blend.400] stroke = 70 # Primary stroke width contrast = 1.11 # Stroke width contrast @@ -89,3 +93,5 @@ vjut = 200 smooth = 215 rbalance = 50 rbalance2 = 25 +cthin = 0.75 +cthinb = 0.5