diff --git a/packages/font-glyphs/src/letter-like/fraktur.ptl b/packages/font-glyphs/src/letter-like/fraktur.ptl index 5925e2aeb..65522c680 100644 --- a/packages/font-glyphs/src/letter-like/fraktur.ptl +++ b/packages/font-glyphs/src/letter-like/fraktur.ptl @@ -137,7 +137,7 @@ glyph-block LetterLike-Fraktur : begin include : fraktur-stroke S g4.ld.start [S.xp SB RightSB 0.1] ([S.yt CAP] - 0.1 * ArchDepthA) flat [S.xl SB] ([S.yt CAP] - 0.6 * ArchDepthA) - curl [S.xl SB] ([S.yb 0] + ArchDepthB) + curl same-x ([S.yb 0] + ArchDepthB) ~~~ [hookend [S.yb 0] (sw -- S.thick)] g2 [S.xr RightSB] ([S.yb 0] + SHook) @@ -151,7 +151,7 @@ glyph-block LetterLike-Fraktur : begin include : fraktur-stroke F g2.ld.start [F.connL S xCenter] [F.connB S : [S.yt CAP] - DecoSizeX] ~~~ [Wave.vc (-Wave.DepthX)] - g2.ld.end [F.connL S xCenter] (CAP * 0.375) + g2.ld.end same-x (CAP * 0.375) create-glyph "frak/H" 0x210C : glyph-proc include : MarkSet.capDesc diff --git a/packages/font-glyphs/src/number/0.ptl b/packages/font-glyphs/src/number/0.ptl index 3a0e0b022..acda31c08 100644 --- a/packages/font-glyphs/src/number/0.ptl +++ b/packages/font-glyphs/src/number/0.ptl @@ -49,7 +49,7 @@ glyph-block Digits-Zero : begin local pcy 0.1 return : sink - g2.right.mid.post mxb (b + O) [widths.lhs fine] + g2.right.mid mxb (b + O) [widths.lhs fine] alsoThru.g2 pcx pcy flat [mix mxb r p1] [mix b myr p2] [widths.lhs sw1] curl [mix mxb r (1 - p4)] [mix b myr (1 - p3)] [widths.lhs sw2] @@ -65,7 +65,6 @@ glyph-block Digits-Zero : begin flat [mix l mxb p4] [mix myl b p3] [widths.lhs sw2] curl [mix l mxb (1 - p1)] [mix myl b (1 - p2)] [widths.lhs sw1] alsoThru.g2 (1 - pcx) (1 - pcy) - g2.right.mid.pre mxb (b + O) [widths.lhs fine] close define [ZeroShapeBase shapeT] : namespace diff --git a/packages/font-kits/src/spiro-kit.mjs b/packages/font-kits/src/spiro-kit.mjs index 79f25cd9a..04f307b9d 100644 --- a/packages/font-kits/src/spiro-kit.mjs +++ b/packages/font-kits/src/spiro-kit.mjs @@ -1,8 +1,15 @@ import { DiSpiroGeometry, SpiroGeometry } from "@iosevka/geometry"; import { BiKnotCollector, + DEP_POST_X, + DEP_POST_Y, + DEP_PRE_X, + DEP_PRE_Y, + DerivedCoordinateBase, Interpolator, + SpiroFlattener, TerminateInstruction, + UserCloseKnotPair, UserControlKnot, } from "@iosevka/geometry/spiro-control"; import { bez3, fallback, mix } from "@iosevka/util"; @@ -18,9 +25,12 @@ class SpiroImplBase { createCollector(glyph) { const gizmo = glyph.gizmo || this.bindings.GlobalTransform; + const flattener = new SpiroFlattener(); + for (const control of this.args) flattener.add(control); + flattener.flatten(); + const collector = new BiKnotCollector(this.bindings.Contrast); - for (const control of this.args) collector.add(control); - collector.unwrap(); + flattener.pipe(collector); return { gizmo, collector }; } @@ -73,31 +83,64 @@ class DiSpiroProxy { } } +/// The builder for directed knot pairs +function KnotType(type) { + return (x, y, f) => { + if (!UserControlKnot.isCoordinateValid(x)) throw new TypeError("NaN detected for X"); + if (!UserControlKnot.isCoordinateValid(y)) throw new TypeError("NaN detected for Y"); + return new UserControlKnot(type, x, y, f); + }; +} + /// The builder for directed knot pairs class DirectedKnotPairBuilder { - constructor(bindings, prevKnotType, nextKnotType, deltaX, deltaY) { + constructor(bindings, kPre, kCenter, kPost, deltaX, deltaY) { const { TINY } = bindings; - this.start = DirPairImpl(prevKnotType, nextKnotType, deltaX, deltaY, 0, TINY); - this.mid = DirPairImpl(prevKnotType, nextKnotType, deltaX, deltaY, -0.5 * TINY, 0.5 * TINY); - this.end = DirPairImpl(prevKnotType, nextKnotType, deltaX, deltaY, -TINY, 0); + this.start = DirPairImpl(kPre, kCenter, kPost, deltaX, deltaY, 0, TINY); + this.mid = DirPairImpl(kPre, kCenter, kPost, deltaX, deltaY, -0.5 * TINY, 0.5 * TINY); + this.end = DirPairImpl(kPre, kCenter, kPost, deltaX, deltaY, -TINY, 0); } } -function DirPairImpl(prevKnotType, nextKnotType, dirX, dirY, distPre, distPost) { - const fnPre = (x, y, af) => prevKnotType(x + dirX * distPre, y + dirY * distPre, af); - const fnPost = (x, y, af) => nextKnotType(x + dirX * distPost, y + dirY * distPost, af); - let buildFn = (x, y, af) => [fnPre(x, y, af), fnPost(x, y, af)]; - buildFn.pre = fnPre; - buildFn.post = fnPost; - return buildFn; +function DirPairImpl(kPre, kCenter, kPost, dirX, dirY, dPre, dPost) { + let tyPre = kPre(0, 0).type; + let tyPost = kPost(0, 0).type; + return (x, y, af) => + new UserCloseKnotPair(kCenter(x, y, af), tyPre, tyPost, dirX, dirY, dPre, dPost); } -function KnotType(type) { - return (x, y, f) => { - if (!isFinite(x)) throw new TypeError("NaN detected for X"); - if (!isFinite(y)) throw new TypeError("NaN detected for Y"); - return new UserControlKnot(type, x, y, f); - }; +/// Derivative coordinates +class CSameX extends DerivedCoordinateBase { + getDependency() { + return DEP_PRE_X; + } + resolve(pre) { + return pre.x; + } +} +class CSameY extends DerivedCoordinateBase { + getDependency() { + return DEP_PRE_Y; + } + resolve(pre) { + return pre.y; + } +} +class CSameXPost extends DerivedCoordinateBase { + getDependency() { + return DEP_POST_X; + } + resolve(pre, curr, post) { + return post.x; + } +} +class CSameYPost extends DerivedCoordinateBase { + getDependency() { + return DEP_POST_Y; + } + resolve(pre, curr, post) { + return post.y; + } } export function SetupBuilders(bindings) { @@ -121,15 +164,16 @@ export function SetupBuilders(bindings) { // Add the directed/heading knot builders { + // prettier-ignore let knotTypes = [ - [g4, g4, g4], - [g2, g2, g2], - [corner, corner, corner], - [straight, flat, curl], - [g2c, g2, corner], - [cg2, corner, g2], - [flatc, flat, corner], - [ccurl, corner, curl], + [ g4, g4, g4, g4 ], + [ g2, g2, g2, g2 ], + [ corner, corner, corner, corner ], + [ straight, flat, g2, curl ], + [ g2c, g2, corner, corner ], + [ cg2, corner, corner, g2 ], + [ flatc, flat, corner, corner ], + [ ccurl, corner, corner, curl ], ]; let directions = [ // Straights @@ -148,12 +192,12 @@ export function SetupBuilders(bindings) { { name: "lu", x: -1, y: 1 }, { name: "ld", x: -1, y: -1 }, ]; - for (const [sink, kl, kr] of knotTypes) { - sink.sl = s => new DirectedKnotPairBuilder(bindings, kl, kr, -1, s); - sink.sr = s => new DirectedKnotPairBuilder(bindings, kl, kr, 1, s); - sink.dir = (dx, dy) => new DirectedKnotPairBuilder(bindings, kl, kr, dx, dy); + for (const [sink, kl, kc, kr] of knotTypes) { + sink.sl = s => new DirectedKnotPairBuilder(bindings, kl, kc, kr, -1, s); + sink.sr = s => new DirectedKnotPairBuilder(bindings, kl, kc, kr, 1, s); + sink.dir = (dx, dy) => new DirectedKnotPairBuilder(bindings, kl, kc, kr, dx, dy); for (const d of directions) { - sink[d.name] = new DirectedKnotPairBuilder(bindings, kl, kr, d.x, d.y); + sink[d.name] = new DirectedKnotPairBuilder(bindings, kl, kc, kr, d.x, d.y); } } } @@ -435,5 +479,10 @@ export function SetupBuilders(bindings) { dispiro, "spiro-outline": spiroOutline, "spiro-collect": spiroCollect, + + "same-x": new CSameX(), + "same-y": new CSameY(), + "same-x-post": new CSameXPost(), + "same-y-post": new CSameYPost(), }; } diff --git a/packages/geometry/src/spiro-control.mjs b/packages/geometry/src/spiro-control.mjs index bdb6265ba..dd8105ef0 100644 --- a/packages/geometry/src/spiro-control.mjs +++ b/packages/geometry/src/spiro-control.mjs @@ -1,3 +1,393 @@ +// This class is used to "flatten" the spiro controls into a plain list of UserControlKnot +export class SpiroFlattener { + constructor() { + this.preControlFunctions = []; + this.controls = []; + this.postControls = []; + } + + add(c) { + if (Array.isArray(c)) { + for (const item of c) this.add(item); + } else if (c instanceof Function) { + if (!this.controls.length) this.preControlFunctions.push(c); + else throw new Error("Invalid spiro control sequence"); + } else if (c instanceof TerminateInstruction) { + this.postControls.push(c); + } else { + if (this.postControls.length) throw new Error("Invalid spiro control sequence"); + this.controls.push(c); + } + } + + flatten() { + for (let cycle = 0; cycle < 32; cycle++) { + const nd = this.flattenImpl(); + if (!nd) break; + } + + let final = []; + for (const c of this.controls) { + this.addToSink(final, c.resolveNonInterpolated()); + } + this.controls = final; + } + + pipe(collector) { + for (const fn of this.preControlFunctions) fn.call(collector); + for (const control of this.controls) collector.pushKnot(control); + for (const postControl of this.postControls) postControl.applyTo(collector); + } + + /// Add a control object (or list) to a sink + /// Return the total number of items that may have dependencies + addToSink(sink, c) { + if (Array.isArray(c)) { + let nd = 0; + for (const item of c) nd += this.addToSink(sink, item); + return nd; + } else { + if (!c.getDependency) { + console.error(c); + } + sink.push(c); + + const cHasDependency = + c.getDependency(RES_DEP_STAGE_COORDINATE_PROPOGATION_X) || + c.getDependency(RES_DEP_STAGE_COORDINATE_PROPOGATION_Y) || + c.getDependency(RES_DEP_STAGE_INTERPOLATION); + return cHasDependency ? 1 : 0; + } + } + + flattenImpl() { + this.propagateCoordinates(); + return this.doInterpolate(); + } + + propagateCoordinates() { + const propagator = new CoordinatePropagator(this.controls); + if (!propagator.nDependencies) return; + propagator.solveAll(); + } + + doInterpolate() { + let nd = 0; + let sink = []; + const dr = this.getDependenciesForInterpolation(); + for (let i = 0; i < this.controls.length; i++) { + if (dr.deps[i] <= DEP_SKIP) { + nd += this.addToSink(sink, this.controls[i]); + } else { + nd += this.addToSink( + sink, + this.controls[i].resolveInterpolation( + this.controls[dr.prevNonDependentIdx[i]].getKernelKnot(), + this.controls[dr.nextNonDependentIdx[i]].getKernelKnot(), + ), + ); + } + } + this.controls = sink; + return nd; + } + + getDependenciesForInterpolation(skipKind) { + let nNonDependent = 0; + let nDependent = 0; + let deps = []; + /// Index to the next non-dependent control + let nextNonDependentIdx = []; + let prevNonDependentIdx = []; + + for (let i = 0; i < this.controls.length; i++) { + let s = this.controls[i].getDependency(RES_DEP_STAGE_INTERPOLATION); + if (s) { + nDependent += 1; + } else { + nNonDependent += 1; + } + deps.push(s); + nextNonDependentIdx.push(-1); + prevNonDependentIdx.push(-1); + } + + let iFirstNonDependent = -1; + let iLastNonDependent = -1; + for (let i = 0; i < this.controls.length; i++) { + if (deps[i] === 0) { + if (iFirstNonDependent < 0) iFirstNonDependent = i; + if (iLastNonDependent >= 0) { + nextNonDependentIdx[iLastNonDependent] = i; + prevNonDependentIdx[i] = iLastNonDependent; + } + iLastNonDependent = i; + } else if (iLastNonDependent >= 0) { + prevNonDependentIdx[i] = iLastNonDependent; + } + } + if (iFirstNonDependent < 0 || iLastNonDependent < 0) { + console.log(this.controls, deps); + throw new Error("A control sequence must have at least one non-dependent control"); + } else { + nextNonDependentIdx[iLastNonDependent] = iFirstNonDependent; + prevNonDependentIdx[iFirstNonDependent] = iLastNonDependent; + } + + for (let i = 0; i < iFirstNonDependent; i++) { + prevNonDependentIdx[i] = iLastNonDependent; + } + for (let i = 0; i < this.controls.length; i++) { + if (deps[i] != 0) { + nextNonDependentIdx[i] = nextNonDependentIdx[prevNonDependentIdx[i]]; + } + } + + return { nDependent, deps, prevNonDependentIdx, nextNonDependentIdx }; + } +} + +/// Utility class to propagate coordinates +class CoordinatePropagator { + constructor(subjects) { + this.nDependencies = 0; + this.subjects = []; + this.depX = []; + this.stateX = []; + this.depY = []; + this.stateY = []; + + for (const subject of subjects) { + let dx = subject.getDependency(RES_DEP_STAGE_COORDINATE_PROPOGATION_X); + let dy = subject.getDependency(RES_DEP_STAGE_COORDINATE_PROPOGATION_Y); + if (dx === DEP_SKIP && dy === DEP_SKIP) continue; + + this.subjects.push(subject); + this.depX.push(dx), this.depY.push(dy); + this.stateX.push(dx > DEP_SKIP ? CR_UNRESOLVED : CR_RESOLVED); + this.stateY.push(dy > DEP_SKIP ? CR_UNRESOLVED : CR_RESOLVED); + if (dx > DEP_SKIP) this.nDependencies += 1; + if (dy > DEP_SKIP) this.nDependencies += 1; + } + } + + solveAll() { + for (let i = 0; i < this.subjects.length; i++) { + this.solve(i, 0); + this.solve(i, 1); + } + } + solve(i, ic) { + const depC = ic ? this.depY : this.depX; + const stateC = ic ? this.stateY : this.stateX; + + if (stateC[i] === CR_RESOLVED) return; + if (stateC[i] === CR_RESOLVING) throw new Error("Circular dependency detected"); + + stateC[i] = CR_RESOLVING; + + if (depC[i] & DEP_PRE_X) this.solve(this.cycI(i - 1), 0); + if (depC[i] & DEP_PRE_Y) this.solve(this.cycI(i - 1), 1); + if (depC[i] & DEP_POST_X) this.solve(this.cycI(i + 1), 0); + if (depC[i] & DEP_POST_Y) this.solve(this.cycI(i + 1), 1); + + // console.log(i, ic, this); + this.subjects[i].resolveCoordiantePropogation( + ic, + this.subjects[this.cycI(i - 1)].getKernelKnot(), + this.subjects[this.cycI(i + 1)].getKernelKnot(), + ); + + stateC[i] = CR_RESOLVED; + } + + cycI(i) { + return (i + this.subjects.length) % this.subjects.length; + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +const RES_DEP_STAGE_COORDINATE_PROPOGATION_X = 0; +const RES_DEP_STAGE_COORDINATE_PROPOGATION_Y = 1; +const RES_DEP_STAGE_INTERPOLATION = 2; + +export const DEP_SKIP = 0x1; +export const DEP_PRE_X = 0x2; +export const DEP_PRE_Y = 0x4; +export const DEP_POST_X = 0x8; +export const DEP_POST_Y = 0x10; + +const DEP_PRE = DEP_PRE_X | DEP_PRE_Y; +const DEP_POST = DEP_POST_X | DEP_POST_Y; + +const CR_UNRESOLVED = 0; +const CR_RESOLVING = 1; +const CR_RESOLVED = 2; + +export class UserControlKnot { + constructor(type, x, y, af) { + this.type = type; + this.x = x; + this.y = y; + this.af = af; + } + applyTo(ctx) { + if (this.af) this.af.call(ctx); + } + + getDependency(stage) { + switch (stage) { + case RES_DEP_STAGE_COORDINATE_PROPOGATION_X: + return typeof this.x === "number" ? 0 : this.x.getDependency(stage); + case RES_DEP_STAGE_COORDINATE_PROPOGATION_Y: + return typeof this.y === "number" ? 0 : this.y.getDependency(stage); + case RES_DEP_STAGE_INTERPOLATION: + return 0; + default: + return 0; + } + } + + getKernelKnot() { + return this; + } + resolveCoordiantePropogation(ic, pre, post) { + // console.log(this, ic, pre, post); + switch (ic) { + case 0: + this.x = this.x.resolve(pre, this, post); + break; + case 1: + this.y = this.y.resolve(pre, this, post); + break; + } + } + + resolveNonInterpolated() { + return this; + } + resolveInterpolation() { + throw new Error("Unreachable"); + } + + static isCoordinateValid(x) { + return (typeof x === "number" && isFinite(x)) || x instanceof DerivedCoordinateBase; + } +} + +export class UserCloseKnotPair { + constructor(center, tyPre, tyPost, dirX, dirY, dPre, dPost) { + this.center = center; + this.tyPre = tyPre; + this.tyPost = tyPost; + this.dirX = dirX; + this.dirY = dirY; + this.dPre = dPre; + this.dPost = dPost; + } + + getDependency(stage) { + return this.center.getDependency(stage); + } + + getKernelKnot() { + return this.center; + } + resolveCoordiantePropogation(ic, pre, post) { + this.center.resolveCoordiantePropogation(ic, pre, post); + } + + resolveNonInterpolated() { + return [ + new UserControlKnot( + this.tyPre, + this.center.x + this.dirX * this.dPre, + this.center.y + this.dirY * this.dPre, + this.center.af, + ), + new UserControlKnot( + this.tyPost, + this.center.x + this.dirX * this.dPost, + this.center.y + this.dirY * this.dPost, + this.center.af, + ), + ]; + } + resolveInterpolation() { + throw new Error("Unreachable"); + } +} + +export class InterpolatorBase { + constructor() {} + + getDependency(stage) { + switch (stage) { + case RES_DEP_STAGE_COORDINATE_PROPOGATION_X: + case RES_DEP_STAGE_COORDINATE_PROPOGATION_Y: + return DEP_SKIP; + case RES_DEP_STAGE_INTERPOLATION: + return DEP_PRE | DEP_POST; + default: + return 0; + } + } + + getKernelKnot() { + throw new Error("Unreachable"); + } + resolveCoordiantePropogation(pre, post) { + throw new Error("Unreachable"); + } + + resolveNonInterpolated() { + throw new Error("Unreachable: All interpolations shall be resolved now"); + } + resolveInterpolation(pre, post) { + throw new Error("Unimplemented"); + } +} + +class FunctionInterpolator extends InterpolatorBase { + constructor(blendFn, extraArgs) { + super(); + this.blendFn = blendFn; + this.extraArgs = extraArgs; + } + resolveInterpolation(pre, post) { + return this.blendFn(pre, post, this.extraArgs); + } +} +export function Interpolator(blender, restParameters) { + return new FunctionInterpolator(blender, restParameters); +} + +export class TerminateInstruction { + constructor(type, af) { + this.type = type; + this.af = af; + } + applyTo(ctx) { + if (this.type === "close") ctx.closed = true; + if (this.af) throw new Error("Unreachable"); + // if (this.af) this.af.call(ctx); + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +export class DerivedCoordinateBase { + getDependency() { + throw new Error("Unimplemented"); + } + resolve(pre, curr, post) { + throw new Error("Unimplemented"); + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// + export class BiKnotCollector { constructor(contrast) { this.contrast = contrast; // stroke contrast @@ -7,57 +397,6 @@ export class BiKnotCollector { this.controls = []; // all the control items this.closed = false; // whether the shape is closed - this.needsUnwrap = false; // whether there are interpolators - this.afterPreFunction = false; // whether we are really processing knots - } - add(c) { - if (c instanceof Function) { - if (this.afterPreFunction) throw new Error("Invalid spiro control sequence"); - c.call(this); - } else if (Array.isArray(c)) { - for (const item of c) this.add(item); - } else if (c instanceof UserControlKnot) { - this.afterPreFunction = true; - this.pushKnot(c); - } else if (c instanceof TerminateInstruction) { - this.afterPreFunction = true; - if (c.type === "close") this.closed = true; - c.applyTo(this); - } else if (c instanceof InterpolatorBase) { - this.afterPreFunction = true; - this.controls.push(c); - this.needsUnwrap = true; - } else { - throw new Error("Invalid spiro control type " + String(c)); - } - } - unwrap() { - while (this.needsUnwrap) { - const cs = [...this.controls]; - this.controls.length = 0; - this.needsUnwrap = false; - this.lastKnot = null; - this.unwrapImpl(cs); - } - for (const item of this.controls) { - if (!(item instanceof BiKnot)) throw new Error("Invalid control sequence"); - item.originalKnot = null; - } - } - unwrapImpl(cs) { - let tmp = []; - for (let j = 0; j < cs.length; j++) { - if (cs[j] instanceof InterpolatorBase) { - const kBefore = cs[nCyclic(j - 1, cs.length)]; - const kAfter = cs[nCyclic(j + 1, cs.length)]; - const blended = cs[j].blender(kBefore.originalKnot, kAfter.originalKnot, cs[j]); - tmp.push(blended); - } else { - tmp.push(cs[j].originalKnot); - } - } - - this.add(tmp); } pushKnot(c) { @@ -67,7 +406,6 @@ export class BiKnotCollector { } else { k = new BiKnot(c.type, c.x, c.y, this.defaultD1, this.defaultD2); } - k.originalKnot = c; this.controls.push(k); this.lastKnot = k; @@ -143,7 +481,6 @@ class BiKnot { // Derived properties this.origTangent = null; - this.originalKnot = null; } clone() { const k1 = new BiKnot(this.type, this.x, this.y, this.d1, this.d2); @@ -184,47 +521,3 @@ class BiKnot { return new MonoKnot(this.type, this.unimportant, this.x, this.y); } } - -function nCyclic(p, n) { - return (p + n + n) % n; -} - -/////////////////////////////////////////////////////////////////////////////////////////////////// - -export class UserControlKnot { - constructor(type, x, y, af) { - this.type = type; - this.x = x; - this.y = y; - this.af = af; - } - applyTo(ctx) { - if (this.af) this.af.call(ctx); - } -} - -export class TerminateInstruction { - constructor(type, af) { - this.type = type; - this.af = af; - } - applyTo(ctx) { - if (this.af) throw new Error("Unreachable"); - // if (this.af) this.af.call(ctx); - } -} - -/////////////////////////////////////////////////////////////////////////////////////////////////// - -export class InterpolatorBase { - constructor(blender) { - this.type = "interpolate"; - this.blender = blender; - } -} -export function Interpolator(blender, restParameters) { - const base = new InterpolatorBase(blender); - const interpolator = Object.create(base); - for (const prop in restParameters) interpolator[prop] = restParameters[prop]; - return interpolator; -}