From 70f41352c14d974288c75e6abce09f390c8d307b Mon Sep 17 00:00:00 2001 From: be5invis Date: Tue, 13 Jul 2021 20:55:39 -0700 Subject: [PATCH] Make first stage of dispiro expansion cachable --- font-src/gen/caching/index.js | 2 +- font-src/glyphs/common/shapes.ptl | 20 ++++- font-src/glyphs/letter/latin/s.ptl | 1 - font-src/glyphs/number/8.ptl | 3 +- font-src/kits/spiro-kit.js | 46 ++--------- font-src/meta/macros.ptl | 2 +- font-src/support/geometry.js | 1 + font-src/support/spiro-expand.js | 128 ++++++++++++++--------------- font-src/support/transform.js | 14 +++- 9 files changed, 102 insertions(+), 115 deletions(-) diff --git a/font-src/gen/caching/index.js b/font-src/gen/caching/index.js index 077566c30..f18c945f6 100644 --- a/font-src/gen/caching/index.js +++ b/font-src/gen/caching/index.js @@ -4,7 +4,7 @@ const fs = require("fs-extra"); const zlib = require("zlib"); const { encode, decode } = require("@msgpack/msgpack"); -const Edition = 11; +const Edition = 12; const MAX_AGE = 5; class GfEntry { diff --git a/font-src/glyphs/common/shapes.ptl b/font-src/glyphs/common/shapes.ptl index fe759f66e..1dafa5f7a 100644 --- a/font-src/glyphs/common/shapes.ptl +++ b/font-src/glyphs/common/shapes.ptl @@ -565,13 +565,29 @@ glyph-block CommonShapes : begin * keyKnot * segAfter + define [hookStartBlender before after args] : begin + return : HookShape after before true args.y args.tight args.sw args.swItalicAdj args.noAdjTerminalY + + define [hookEndBlender before after args] : begin + return : HookShape before after false args.y args.tight args.sw args.swItalicAdj args.noAdjTerminalY + define [hookstart] : params [y tight sw swItalicAdj noAdjTerminalY] : return { .type 'interpolate' - .af [lambda [before after] [HookShape after before true y tight sw swItalicAdj noAdjTerminalY]] + .blender hookStartBlender + .y y + .tight tight + .sw sw + .swItalicAdj swItalicAdj + .noAdjTerminalY noAdjTerminalY } define [hookend] : params [y tight sw swItalicAdj noAdjTerminalY] : return { .type 'interpolate' - .af [lambda [before after] [HookShape before after false y tight sw swItalicAdj noAdjTerminalY]] + .blender hookEndBlender + .y y + .tight tight + .sw sw + .swItalicAdj swItalicAdj + .noAdjTerminalY noAdjTerminalY } # Composite transformations diff --git a/font-src/glyphs/letter/latin/s.ptl b/font-src/glyphs/letter/latin/s.ptl index 1b22daee8..61c76b2f8 100644 --- a/font-src/glyphs/letter/latin/s.ptl +++ b/font-src/glyphs/letter/latin/s.ptl @@ -123,7 +123,6 @@ glyph-block Letter-Latin-S : begin hookend O (sw -- stroke) (swItalicAdj -- Stroke) g4 (SB - OX + SOBot) SHook - glyph-block-export SStrokeAlt define [SStrokeAlt] : params [top hook swStart swEnd oXLeftTop offsetLT offsetRB offsetC] : begin define stroke : Math.max swStart swEnd define fine : Math.min swStart swEnd diff --git a/font-src/glyphs/number/8.ptl b/font-src/glyphs/number/8.ptl index a53f2d0b7..f1fa9405d 100644 --- a/font-src/glyphs/number/8.ptl +++ b/font-src/glyphs/number/8.ptl @@ -103,7 +103,8 @@ glyph-block Digits-Eight : begin kty (Middle - CorrectionOMidS) (top - O) [widths.lhs stroke] archv g4 [mix r l p] (top - [SmoothAOf (arch * p) Width]) - alsoThru.sNeck 0.96 0.3 stroke (2 / 3) + alsoThruThem { {0.34 0.45 (2 / 3)} {0.66 0.55 (1 / 3)} } : object + blend : function [rt] : widths (stroke * rt) (stroke * (1 - rt)) g4 r [SmoothAOf arch Width] [widths.rhs stroke] arcvh kty (Middle + CorrectionOMidS) (O) diff --git a/font-src/kits/spiro-kit.js b/font-src/kits/spiro-kit.js index 03fd63588..9563bd6ae 100644 --- a/font-src/kits/spiro-kit.js +++ b/font-src/kits/spiro-kit.js @@ -136,31 +136,14 @@ exports.SetupBuilders = function (bindings) { } return innerKnots; } - function afInterpolateSNeck(before, after, args) { - return [ - g2( - mix(before.x, after.x, 1 / 2 - args.px / 6), - mix(before.y, after.y, 1 / 2 - args.py / 6), - widths(args.sw * args.ps, args.sw * (1 - args.ps)) - ), - g2( - mix(before.x, after.x, 1 / 2 + args.px / 6), - mix(before.y, after.y, 1 / 2 + args.py / 6), - widths(args.sw * (1 - args.ps), args.sw * args.ps) - ) - ]; - } function alsoThru(rx, ry, raf) { - return { type: "interpolate", rx, ry, raf, af: afInterpolate }; + return { type: "interpolate", rx, ry, raf, blender: afInterpolate }; } alsoThru.g2 = function (rx, ry, raf) { - return { type: "interpolate", rx, ry, raf, af: afInterpolateG2 }; - }; - alsoThru.sNeck = function (px, py, sw, ps) { - return { type: "interpolate", px, py, sw, ps, af: afInterpolateSNeck }; + return { type: "interpolate", rx, ry, raf, blender: afInterpolateG2 }; }; function alsoThruThem(es, raf, ty) { - return { type: "interpolate", rs: es, raf, ty, af: afInterpolateThem }; + return { type: "interpolate", rs: es, raf, ty, blender: afInterpolateThem }; } function bezControlsImpl(x1, y1, x2, y2, samples, raf, ty) { let rs = []; @@ -227,16 +210,6 @@ exports.SetupBuilders = function (bindings) { arcvh.superness = function (s) { return arcvh(DEFAULT_STEPS, s); }; - function complexThru(...a) { - return { - type: "interpolate", - af: function (before, after, args) { - let ks = []; - for (const knot of a) ks.push(knot.af.call(this, before, after, knot)); - return ks; - } - }; - } function flattenImpl(sink, knots) { for (const p of knots) { if (p instanceof Array) flattenImpl(sink, p); @@ -254,7 +227,7 @@ exports.SetupBuilders = function (bindings) { if (knots[j] && knots[j].type === "interpolate") { const kBefore = knots[nCyclic(j - 1, knots.length)]; const kAfter = knots[nCyclic(j + 1, knots.length)]; - knots[j] = knots[j].af.call(s, kBefore, kAfter, knots[j]); + knots[j] = knots[j].blender(kBefore, kAfter, knots[j]); unwrapped = true; } if (unwrapped) return flatten(s, knots); @@ -308,14 +281,10 @@ exports.SetupBuilders = function (bindings) { const collector = new BiKnotCollector(gizmo, Contrast); const { knots, closed } = prepareSpiroKnots([].slice.call(args, 0), collector); for (const knot of knots) { - const ty = knot.type; - const af = knot.af; - knot.af = function () { - this.setType(ty); - return af ? af.apply(this, args) : void 0; - }; + collector.pushKnot(knot.type, knot.x, knot.y); + if (knot.af) knot.af.call(collector); } - SpiroJs.spiroToArcsOnContext(knots, closed, collector); + const dsp = new DiSpiroProxy(closed, collector, knots); this.includeGeometry(dsp.geometry); return dsp; @@ -350,7 +319,6 @@ exports.SetupBuilders = function (bindings) { quadControls, archv, arcvh, - complexThru, dispiro, "spiro-outline": spiroOutline }; diff --git a/font-src/meta/macros.ptl b/font-src/meta/macros.ptl index 18fba1b13..f86f31c79 100644 --- a/font-src/meta/macros.ptl +++ b/font-src/meta/macros.ptl @@ -222,7 +222,7 @@ define-macro glyph-block : syntax-rules SmoothAdjust MidJutSide MidJutCenter compositeBaseAnchors YSmoothMidR YSmoothMidL] define spiroFnImports `[g4 g2 corner flat curl close end straight widths disable-contrast heading unimportant important alsoThru alsoThruThem bezControls - quadControls archv arcvh complexThru dispiro spiro-outline] + quadControls archv arcvh dispiro spiro-outline] define booleFnImports `[union intersection difference] dirty `[$GlyphBlocks$.push : lambda [$Capture_Ext$] : begin \\ diff --git a/font-src/support/geometry.js b/font-src/support/geometry.js index 31dda9aa6..39e1ceb25 100644 --- a/font-src/support/geometry.js +++ b/font-src/support/geometry.js @@ -153,6 +153,7 @@ class DiSpiroGeometry extends GeometryBase { this.m_closed, this.m_biKnots.map(k => k.clone()) ); + expander.initializeNormals(); expander.iterateNormals(); expander.iterateNormals(); this.m_cachedExpansionResults = expander.expand(); diff --git a/font-src/support/spiro-expand.js b/font-src/support/spiro-expand.js index 87d86700a..839ccaa40 100644 --- a/font-src/support/spiro-expand.js +++ b/font-src/support/spiro-expand.js @@ -29,8 +29,8 @@ class BiKnot { this.type, Format.n(this.x), Format.n(this.y), - Format.n(this.d1), - Format.n(this.d2), + this.d1 == null ? "" : Format.n(this.d1), + this.d2 == null ? "" : Format.n(this.d2), this.origTangent ? Format.tuple(Format.n(this.origTangent.x), Format.n(this.origTangent.y)) : "", @@ -52,29 +52,16 @@ class BiKnotCollector { this.defaultD2 = 0; } - beginShape() {} - endShape() {} - moveTo(x, y, unimportant) { - if (unimportant) return; - if (!isFinite(x) || !isFinite(y)) throw new Error("NaN detected."); - 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."); + pushKnot(type, x, y) { + const tfZ = this.gizmo.applyXY(x, y); const k0 = this.controlKnots[this.controlKnots.length - 1]; - if (!k0) throw new Error("Unreachable: lineTo called before moveTo"); - 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 }); - 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); + if (k0) { + this.controlKnots.push(new BiKnot(type, tfZ.x, tfZ.y, k0.d1, k0.d2)); + } else { + this.controlKnots.push(new BiKnot(type, tfZ.x, tfZ.y, this.defaultD1, this.defaultD2)); } } + setWidth(l, r) { const k0 = this.controlKnots[this.controlKnots.length - 1]; if (k0) { @@ -105,16 +92,20 @@ class SpiroExpander { this.controlKnots = cks; } + initializeNormals() { + const normalRectifier = new NormalRectifier(this.controlKnots, this.gizmo); + SpiroJs.spiroToArcsOnContext(this.controlKnots, this.closed, normalRectifier); + } + iterateNormals() { const centerBone = this.getPass2Knots(); const normalRectifier = new NormalRectifier(this.controlKnots, this.gizmo); SpiroJs.spiroToArcsOnContext(centerBone, this.closed, normalRectifier); } - getPass2Knots() { const expanded = this.expand(this.contrast); const middles = []; - for (let j = 0; j + (this.closed ? 1 : 0) < this.controlKnots.length; j++) { + for (let j = 0; j < this.controlKnots.length; j++) { const lhs = this.gizmo.unapply(expanded.lhs[j]); const rhs = this.gizmo.unapply(expanded.rhs[j]); middles[j] = { @@ -130,6 +121,22 @@ class SpiroExpander { expand() { const lhs = [], rhs = []; + // Initialize knots + for (let j = 0; j < this.controlKnots.length; j++) { + const knot = this.controlKnots[j]; + lhs[j] = { + type: knot.type, + unimportant: knot.unimportant, + x: 0, + y: 0 + }; + rhs[j] = { + type: reverseKnotType(knot.type), + unimportant: knot.unimportant, + x: 0, + y: 0 + }; + } // Create important knots for (let j = 0; j < this.controlKnots.length; j++) { @@ -144,52 +151,43 @@ class SpiroExpander { dx = normalX(knot.origTangent, this.contrast); dy = normalY(knot.origTangent, this.contrast); } - lhs[j] = { - type: knot.type, - x: knot.x + knot.d1 * dx, - y: knot.y + knot.d1 * dy - }; - rhs[j] = { - type: reverseKnotType(knot.type), - x: knot.x - knot.d2 * dx, - y: knot.y - knot.d2 * dy - }; + lhs[j].x = knot.x + knot.d1 * dx; + lhs[j].y = knot.y + knot.d1 * dy; + + rhs[j].x = knot.x - knot.d2 * dx; + rhs[j].y = knot.y - knot.d2 * dy; } this.interpolateUnimportantKnots(lhs, rhs); return { lhs, rhs }; } + interpolateUnimportantKnots(lhs, rhs) { for (let j = 0; j < this.controlKnots.length; j++) { const knot = this.controlKnots[j]; if (!knot.unimportant) continue; let jBefore, jAfter; - for (jBefore = j - 1; this.controlKnots[jBefore].unimportant; jBefore--); - for (jAfter = j + 1; this.controlKnots[jAfter].unimportant; jAfter++); + for (jBefore = j - 1; cyNth(this.controlKnots, jBefore).unimportant; jBefore--); + for (jAfter = j + 1; cyNth(this.controlKnots, jAfter).unimportant; jAfter++); - const knotBefore = this.gizmo.unapply(this.controlKnots[jBefore]), - knotAfter = this.gizmo.unapply(this.controlKnots[jAfter]), + const knotBefore = this.gizmo.unapply(cyNth(this.controlKnots, jBefore)), + knotAfter = this.gizmo.unapply(cyNth(this.controlKnots, jAfter)), ref = this.gizmo.unapply(knot), - lhsBefore = this.gizmo.unapply(lhs[jBefore]), - lhsAfter = this.gizmo.unapply(lhs[jAfter]), - rhsBefore = this.gizmo.unapply(rhs[jBefore]), - rhsAfter = this.gizmo.unapply(rhs[jAfter]); + lhsBefore = this.gizmo.unapply(cyNth(lhs, jBefore)), + lhsAfter = this.gizmo.unapply(cyNth(lhs, jAfter)), + rhsBefore = this.gizmo.unapply(cyNth(rhs, jBefore)), + rhsAfter = this.gizmo.unapply(cyNth(rhs, jAfter)); - lhs[j] = { - unimportant: knot.unimportant, - type: knot.type, - ...this.gizmo.apply({ - x: linreg(knotBefore.x, lhsBefore.x, knotAfter.x, lhsAfter.x, ref.x), - y: linreg(knotBefore.y, lhsBefore.y, knotAfter.y, lhsAfter.y, ref.y) - }) - }; - rhs[j] = { - unimportant: knot.unimportant, - type: reverseKnotType(knot.type), - ...this.gizmo.apply({ - x: linreg(knotBefore.x, rhsBefore.x, knotAfter.x, rhsAfter.x, ref.x), - y: linreg(knotBefore.y, rhsBefore.y, knotAfter.y, rhsAfter.y, ref.y) - }) - }; + const lhsTf = this.gizmo.applyXY( + linreg(knotBefore.x, lhsBefore.x, knotAfter.x, lhsAfter.x, ref.x), + linreg(knotBefore.y, lhsBefore.y, knotAfter.y, lhsAfter.y, ref.y) + ); + const rhsTf = this.gizmo.applyXY( + linreg(knotBefore.x, rhsBefore.x, knotAfter.x, rhsAfter.x, ref.x), + linreg(knotBefore.y, rhsBefore.y, knotAfter.y, rhsAfter.y, ref.y) + ); + + (lhs[j].x = lhsTf.x), (lhs[j].y = lhsTf.y); + (rhs[j].x = rhsTf.x), (rhs[j].y = rhsTf.y); } } } @@ -203,18 +201,17 @@ class NormalRectifier { beginShape() {} endShape() {} - moveTo(x, y, unimportant) { - if (unimportant) return; + moveTo(x, y) { this.nKnotsProcessed += 1; } arcTo(arc, x, y) { if (this.nKnotsProcessed === 1) { - const d = this.gizmo.applyOffset({ x: arc.deriveX0, y: arc.deriveY0 }); + const d = this.gizmo.applyOffsetXY(arc.deriveX0, arc.deriveY0); if (isTangentValid(d)) this.controlKnots[0].origTangent = d; else throw new Error("NaN angle detected."); } - { - const d = this.gizmo.applyOffset({ x: arc.deriveX1, y: arc.deriveY1 }); + if (this.controlKnots[this.nKnotsProcessed]) { + const d = this.gizmo.applyOffsetXY(arc.deriveX1, arc.deriveY1); if (isTangentValid(d)) this.controlKnots[this.nKnotsProcessed].origTangent = d; else throw new Error("NaN angle detected."); } @@ -234,9 +231,8 @@ function normalY(tangent) { function reverseKnotType(ty) { return ty === "left" ? "right" : ty === "right" ? "left" : ty; } -function computeNormalAngle(gizmo, x, y) { - const tfd = gizmo.applyOffset({ x, y }); - return Math.PI / 2 + Math.atan2(tfd.y, tfd.x); +function cyNth(a, j) { + return a[j % a.length]; } exports.BiKnotCollector = BiKnotCollector; diff --git a/font-src/support/transform.js b/font-src/support/transform.js index 178aee6db..070d6fe66 100644 --- a/font-src/support/transform.js +++ b/font-src/support/transform.js @@ -19,15 +19,21 @@ module.exports = class Transform { } apply(pt) { + return this.applyXY(pt.x, pt.y); + } + applyXY(x, y) { return { - x: pt.x * this.xx + pt.y * this.yx + this.x, - y: pt.x * this.xy + pt.y * this.yy + this.y + x: x * this.xx + y * this.yx + this.x, + y: x * this.xy + y * this.yy + this.y }; } applyOffset(delta) { + return this.applyOffsetXY(delta.x, delta.y); + } + applyOffsetXY(deltaX, deltaY) { return { - x: delta.x * this.xx + delta.y * this.yx, - y: delta.x * this.xy + delta.y * this.yy + x: deltaX * this.xx + deltaY * this.yx, + y: deltaX * this.xy + deltaY * this.yy }; } unapply(pt) {