This commit is contained in:
be5invis 2023-03-09 05:12:06 -08:00
parent b548e9c1cb
commit 83df8bea3c
8 changed files with 329 additions and 243 deletions

View file

@ -3,7 +3,7 @@ import zlib from "zlib";
import { encode, decode } from "@msgpack/msgpack"; import { encode, decode } from "@msgpack/msgpack";
const Edition = 24; const Edition = 25;
const MAX_AGE = 16; const MAX_AGE = 16;
class GfEntry { class GfEntry {
constructor(age, value) { constructor(age, value) {

View file

@ -4,6 +4,7 @@ import [Transform] from"../../support/geometry/transform.mjs"
import [mix linreg clamp fallback] from"../../support/utils.mjs" import [mix linreg clamp fallback] from"../../support/utils.mjs"
import [Radical] from"../../support/gr.mjs" import [Radical] from"../../support/gr.mjs"
import [Interpolator] from"../../support/geometry/spiro-control.mjs"
glyph-module glyph-module
@ -513,25 +514,12 @@ glyph-block CommonShapes : begin
return : HookShape before after false args.y args.tight args.sw args.swItalicAdj args.noAdjTerminalY return : HookShape before after false args.y args.tight args.sw args.swItalicAdj args.noAdjTerminalY
glyph-block-export hookstart glyph-block-export hookstart
define [hookstart] : params [y tight sw swItalicAdj noAdjTerminalY] : return { define [hookstart] : params [y tight sw swItalicAdj noAdjTerminalY] : begin
.type 'interpolate' return : Interpolator hookStartBlender : object y tight sw swItalicAdj noAdjTerminalY
.blender hookStartBlender
.y y
.tight tight
.sw sw
.swItalicAdj swItalicAdj
.noAdjTerminalY noAdjTerminalY
}
glyph-block-export hookend glyph-block-export hookend
define [hookend] : params [y tight sw swItalicAdj noAdjTerminalY] : return { define [hookend] : params [y tight sw swItalicAdj noAdjTerminalY] : begin
.type 'interpolate' return : Interpolator hookEndBlender : object y tight sw swItalicAdj noAdjTerminalY
.blender hookEndBlender
.y y
.tight tight
.sw sw
.swItalicAdj swItalicAdj
.noAdjTerminalY noAdjTerminalY
}
glyph-block-export Ungizmo glyph-block-export Ungizmo
define [Ungizmo] : glyph-proc define [Ungizmo] : glyph-proc

View file

@ -357,7 +357,7 @@ glyph-block Letter-Latin-S : begin
CreateAccentedComposition 'sTildeOver' 0x1D74 's' 'tildeOverWide' CreateAccentedComposition 'sTildeOver' 0x1D74 's' 'tildeOverWide'
derive-composites 'SCedilla' 0x15E 'S' 'cedillaExtShapeBelowOArc' derive-composites 'SCedilla' 0x15E 'S' 'cedillaExtShapeBelowOArc'
derive-composites 'sCedilla' 0x15F 's' 'cedillaExtShapeBelowOArc' derive-composites 'sCedilla' 0x15F 's' 'cedillaExtShapeBelowSOArc'
create-glyph 'mathbb/S' 0x1D54A : glyph-proc create-glyph 'mathbb/S' 0x1D54A : glyph-proc
include : MarkSet.capital include : MarkSet.capital

View file

@ -54,6 +54,12 @@ glyph-block Mark-Below : begin
set-base-anchor 'belowBrace' markMiddle belowMarkMid set-base-anchor 'belowBrace' markMiddle belowMarkMid
include : CedillaShape (Stroke + O) include : CedillaShape (Stroke + O)
create-glyph 'cedillaExtShapeBelowSOArc' : glyph-proc
set-width 0
set-mark-anchor 'below' markMiddle 0 markMiddle belowMarkStack
set-base-anchor 'belowBrace' markMiddle belowMarkMid
include : CedillaShape ([AdviceStroke2 2 3 XH] + O)
create-glyph 'cedillaBelow' 0x327 : glyph-proc create-glyph 'cedillaBelow' 0x327 : glyph-proc
set-width 0 set-width 0
set-mark-anchor 'below' markMiddle 0 markMiddle belowMarkStack set-mark-anchor 'below' markMiddle 0 markMiddle belowMarkStack

View file

@ -1,48 +1,62 @@
import * as CurveUtil from "../support/geometry/curve-util.mjs"; import { DiSpiroGeometry, SpiroGeometry } from "../support/geometry/index.mjs";
import { SpiroGeometry, DiSpiroGeometry } from "../support/geometry/index.mjs"; import {
import { BiKnotCollector } from "../support/geometry/spiro-expand.mjs"; BiKnotCollector,
import { fallback, mix, bez3 } from "../support/utils.mjs"; ControlKnot,
Interpolator,
TerminateInstruction
} from "../support/geometry/spiro-control.mjs";
import { bez3, fallback, mix } from "../support/utils.mjs";
/////////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////////
class DispiroImpl {
class SpiroImplBase {
constructor(bindings, args) { constructor(bindings, args) {
this.bindings = bindings; this.bindings = bindings;
this.args = args; this.args = args;
} }
applyToGlyph(glyph) {
createCollector(glyph) {
const gizmo = glyph.gizmo || this.bindings.GlobalTransform; const gizmo = glyph.gizmo || this.bindings.GlobalTransform;
const collector = new BiKnotCollector(gizmo, this.bindings.Contrast);
const { knots, closed } = prepareSpiroKnots(this.args, collector); const collector = new BiKnotCollector(this.bindings.Contrast);
for (const knot of knots) { for (const control of this.args) collector.add(control);
collector.pushKnot(knot.type, knot.x, knot.y); collector.unwrap();
if (knot.af) knot.af.call(collector);
return { gizmo, collector };
} }
const dsp = new DiSpiroProxy(closed, collector, knots); }
class DispiroImpl extends SpiroImplBase {
constructor(bindings, args) {
super(bindings, args);
}
applyToGlyph(glyph) {
const { gizmo, collector } = this.createCollector(glyph);
const dsp = new DiSpiroProxy(gizmo, collector);
glyph.includeGeometry(dsp.geometry); glyph.includeGeometry(dsp.geometry);
return dsp; return dsp;
} }
} }
class SpiroOutlineImpl { class SpiroOutlineImpl extends SpiroImplBase {
constructor(bindings, args) { constructor(bindings, args) {
this.bindings = bindings; super(bindings, args);
this.args = args;
} }
applyToGlyph(glyph) { applyToGlyph(glyph) {
const gizmo = glyph.gizmo || this.bindings.GlobalTransform; const { gizmo, collector } = this.createCollector(glyph);
const g = new CurveUtil.BezToContoursSink(gizmo); return glyph.includeGeometry(
const { knots, closed } = prepareSpiroKnots(this.args, g); new SpiroGeometry(gizmo, collector.closed, collector.controls)
return glyph.includeGeometry(new SpiroGeometry(gizmo, closed, knots)); );
} }
} }
class DiSpiroProxy { class DiSpiroProxy {
constructor(closed, collector, origKnots) { constructor(gizmo, collector) {
this.geometry = new DiSpiroGeometry( this.geometry = new DiSpiroGeometry(
collector.gizmo, gizmo,
collector.contrast, collector.contrast,
closed, collector.closed,
collector.controlKnots collector.controls
); );
this.m_origKnots = origKnots; this.m_origKnots = collector.controls;
} }
get knots() { get knots() {
return this.m_origKnots; return this.m_origKnots;
@ -54,69 +68,23 @@ class DiSpiroProxy {
return this.geometry.expand().rhsUntransformed; return this.geometry.expand().rhsUntransformed;
} }
} }
function prepareSpiroKnots(_knots, s) {
let knots = [..._knots];
while (knots[0] && knots[0] instanceof Function) {
knots[0].call(s);
knots.splice(0, 1);
}
const closed = dropTailKnot(knots);
knots = flatten(s, knots);
return { knots, closed };
}
function dropTailKnot(knots) {
let last = knots[knots.length - 1];
if (last && (last.type === "close" || last.type === "end")) {
knots.length = knots.length - 1;
return last.type === "close";
} else {
return false;
}
}
function flatten(s, knots0) {
let knots = [];
flattenImpl(knots, knots0);
let unwrapped = false;
for (let j = 0; j < knots.length; j = j + 1)
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].blender(kBefore, kAfter, knots[j]);
unwrapped = true;
}
if (unwrapped) return flatten(s, knots);
return knots;
}
function flattenImpl(sink, knots) {
for (const p of knots) {
if (p instanceof Array) flattenImpl(sink, p);
else sink.push(p);
}
}
function nCyclic(p, n) {
return (p + n + n) % n;
}
export function SetupBuilders(bindings) { export function SetupBuilders(bindings) {
const { Contrast, GlobalTransform, Stroke, Superness } = bindings; const { Stroke, Superness } = bindings;
function validateCoord(x) {
if (!isFinite(x)) throw new TypeError("NaN detected");
return x;
}
function KnotType(type) { function KnotType(type) {
return (x, y, f) => ({ return (x, y, f) => {
type, if (!isFinite(x)) throw new TypeError("NaN detected for X");
x: validateCoord(x), if (!isFinite(y)) throw new TypeError("NaN detected for Y");
y: validateCoord(y), return new ControlKnot(type, x, y, f);
af: f };
});
} }
const g4 = KnotType("g4"); const g4 = KnotType("g4");
const g2 = KnotType("g2"); const g2 = KnotType("g2");
const corner = KnotType("corner"); const corner = KnotType("corner");
const flat = KnotType("left"); const flat = KnotType("left");
const curl = KnotType("right"); const curl = KnotType("right");
const close = f => ({ type: "close", af: f }); const close = f => new TerminateInstruction("close", f);
const end = f => ({ type: "end", af: f }); const end = f => new TerminateInstruction("end", f);
const straight = { l: flat, r: curl }; const straight = { l: flat, r: curl };
{ {
let directions = [ let directions = [
@ -148,9 +116,13 @@ export function SetupBuilders(bindings) {
} }
} }
} }
function widths(l, r) { function widths(l, r) {
return function () { return function () {
return this.setWidth ? this.setWidth(validateCoord(l), validateCoord(r)) : void 0; if (!isFinite(l)) throw new TypeError("NaN detected for left width");
if (!isFinite(r)) throw new TypeError("NaN detected for right width");
if (this.setWidth) this.setWidth(l, r);
}; };
} }
widths.lhs = function (w) { widths.lhs = function (w) {
@ -162,46 +134,49 @@ export function SetupBuilders(bindings) {
widths.center = function (w) { widths.center = function (w) {
return widths(fallback(w, Stroke) / 2, fallback(w, Stroke) / 2); return widths(fallback(w, Stroke) / 2, fallback(w, Stroke) / 2);
}; };
function heading(d) { function heading(d) {
return function () { return function () {
return this.headsTo ? this.headsTo(d) : void 0; if (this.headsTo) this.headsTo(d);
}; };
} }
widths.heading = function (l, r, d) { widths.heading = function (l, r, d) {
return function () { return function () {
if (this.setWidth) this.setWidth(l, r); if (this.setWidth) this.setWidth(l, r);
return this.headsTo ? this.headsTo(d) : void 0; if (this.headsTo) this.headsTo(d);
}; };
}; };
widths.lhs.heading = function (w, d) { widths.lhs.heading = function (w, d) {
return function () { return function () {
if (this.setWidth) this.setWidth(fallback(w, Stroke), 0); if (this.setWidth) this.setWidth(fallback(w, Stroke), 0);
return this.headsTo ? this.headsTo(d) : void 0; if (this.headsTo) this.headsTo(d);
}; };
}; };
widths.rhs.heading = function (w, d) { widths.rhs.heading = function (w, d) {
return function () { return function () {
if (this.setWidth) this.setWidth(0, fallback(w, Stroke)); if (this.setWidth) this.setWidth(0, fallback(w, Stroke));
return this.headsTo ? this.headsTo(d) : void 0; if (this.headsTo) this.headsTo(d);
}; };
}; };
widths.center.heading = function (w, d) { widths.center.heading = function (w, d) {
return function () { return function () {
if (this.setWidth) this.setWidth(fallback(w, Stroke) / 2, fallback(w, Stroke) / 2); if (this.setWidth) this.setWidth(fallback(w, Stroke) / 2, fallback(w, Stroke) / 2);
return this.headsTo ? this.headsTo(d) : void 0; if (this.headsTo) this.headsTo(d);
}; };
}; };
function disableContrast() { function disableContrast() {
return function () { return function () {
return (this.contrast = 1); if (this.setContrast) this.setContrast(1);
}; };
} }
function unimportant() { function unimportant() {
return this.setUnimportant ? this.setUnimportant() : void 0; if (this.setUnimportant) this.setUnimportant(1);
} }
function important() { function important() {
return void 0; return void 0;
} }
function afInterpolate(before, after, args) { function afInterpolate(before, after, args) {
return g4( return g4(
mix(before.x, after.x, args.rx), mix(before.x, after.x, args.rx),
@ -272,23 +247,24 @@ export function SetupBuilders(bindings) {
} }
function alsoThru(rx, ry, raf) { function alsoThru(rx, ry, raf) {
return { type: "interpolate", rx, ry, raf, blender: afInterpolate }; return Interpolator(afInterpolate, { rx, ry, raf });
} }
alsoThru.withOffset = function (rx, ry, deltaX, deltaY, raf) { alsoThru.withOffset = function (rx, ry, deltaX, deltaY, raf) {
return { type: "interpolate", rx, ry, deltaX, deltaY, raf, blender: afInterpolateDelta }; return Interpolator(afInterpolateDelta, { rx, ry, deltaX, deltaY, raf });
}; };
alsoThru.g2 = function (rx, ry, raf) { alsoThru.g2 = function (rx, ry, raf) {
return { type: "interpolate", rx, ry, raf, blender: afInterpolateG2 }; return Interpolator(afInterpolateG2, { rx, ry, raf });
}; };
function alsoThruThem(es, raf, ty) { function alsoThruThem(rs, raf, ty) {
return { type: "interpolate", rs: es, raf, ty, blender: afInterpolateThem }; return Interpolator(afInterpolateThem, { rs, raf, ty });
} }
alsoThruThem.withOffset = function (es, raf, ty) { alsoThruThem.withOffset = function (rs, raf, ty) {
return { type: "interpolate", rs: es, raf, ty, blender: afInterpolateThemWithDelta }; return Interpolator(afInterpolateThemWithDelta, { rs, raf, ty });
}; };
alsoThruThem.fromTWithOffset = function (es, raf, ty) { alsoThruThem.fromTWithOffset = function (rs, raf, ty) {
return { type: "interpolate", rs: es, raf, ty, blender: afInterpolateThemFromTWithDelta }; return Interpolator(afInterpolateThemFromTWithDelta, { rs, raf, ty });
}; };
function bezControlsImpl(x1, y1, x2, y2, samples, raf, ty) { function bezControlsImpl(x1, y1, x2, y2, samples, raf, ty) {
let rs = []; let rs = [];
for (let j = 1; j < samples; j = j + 1) for (let j = 1; j < samples; j = j + 1)
@ -312,6 +288,7 @@ export function SetupBuilders(bindings) {
raf raf
); );
} }
let DEFAULT_STEPS = 6; let DEFAULT_STEPS = 6;
let [buildHV, buildVH] = (function (cache) { let [buildHV, buildVH] = (function (cache) {
function build(samples, _superness) { function build(samples, _superness) {

View file

@ -114,8 +114,7 @@ export class SpiroGeometry extends GeometryBase {
export class DiSpiroGeometry extends GeometryBase { export class DiSpiroGeometry extends GeometryBase {
constructor(gizmo, contrast, closed, biKnots) { constructor(gizmo, contrast, closed, biKnots) {
super(); super();
this.m_biKnots = []; this.m_biKnots = biKnots; // untransformed
for (const k of biKnots) this.m_biKnots.push(k.clone());
this.m_closed = closed; this.m_closed = closed;
this.m_gizmo = gizmo; this.m_gizmo = gizmo;
this.m_contrast = contrast; this.m_contrast = contrast;
@ -149,7 +148,7 @@ export class DiSpiroGeometry extends GeometryBase {
this.m_gizmo, this.m_gizmo,
this.m_contrast, this.m_contrast,
this.m_closed, this.m_closed,
this.m_biKnots.map(k => k.clone()) this.m_biKnots
); );
expander.initializeNormals(); expander.initializeNormals();
expander.iterateNormals(); expander.iterateNormals();

View file

@ -0,0 +1,188 @@
import * as Format from "../util/formatter.mjs";
///////////////////////////////////////////////////////////////////////////////////////////////////
export class BiKnotCollector {
constructor(contrast) {
this.contrast = contrast; // stroke contrast
this.defaultD1 = 0; // default LHS
this.defaultD2 = 0; // default RHS sw
this.lastKnot = null; // last knot in the processed items
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 ControlKnot) {
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");
}
}
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) {
let k;
if (this.lastKnot) {
k = new BiKnot(c.type, c.x, c.y, this.lastKnot.d1, this.lastKnot.d2);
} else {
k = new BiKnot(c.type, c.x, c.y, this.defaultD1, this.defaultD2);
}
k.originalKnot = c;
this.controls.push(k);
this.lastKnot = k;
c.applyTo(this);
}
setWidth(l, r) {
if (this.lastKnot) {
this.lastKnot.d1 = l;
this.lastKnot.d2 = r;
} else {
this.defaultD1 = l;
this.defaultD2 = r;
}
}
headsTo(direction) {
if (this.lastKnot) {
this.lastKnot.proposedNormal = direction;
}
}
setUnimportant() {
if (this.lastKnot) {
this.lastKnot.unimportant = 1;
}
}
setContrast(c) {
this.contrast = c;
}
}
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;
this.unimportant = 0;
this.originalKnot = null;
}
clone() {
const k1 = new BiKnot(this.type, this.x, this.y, this.d1, this.d2);
k1.origTangent = this.origTangent;
k1.proposedNormal = this.proposedNormal;
k1.unimportant = this.unimportant;
return k1;
}
withGizmo(gizmo) {
const tfZ = gizmo.applyXY(this.x, this.y);
const k1 = new BiKnot(this.type, tfZ.x, tfZ.y, this.d1, this.d2);
k1.origTangent = this.origTangent ? gizmo.applyOffset(this.origTangent) : null;
k1.proposedNormal = this.proposedNormal ? gizmo.applyOffset(this.proposedNormal) : null;
k1.unimportant = this.unimportant;
return k1;
}
toShapeString() {
return Format.tuple(
this.type,
Format.n(this.x),
Format.n(this.y),
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))
: "",
this.proposedNormal
? Format.tuple(Format.n(this.proposedNormal.x), Format.n(this.proposedNormal.y))
: "",
this.unimportant
);
}
}
function nCyclic(p, n) {
return (p + n + n) % n;
}
///////////////////////////////////////////////////////////////////////////////////////////////////
export class ControlKnot {
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;
}

View file

@ -1,111 +1,39 @@
import * as SpiroJs from "spiro"; import * as SpiroJs from "spiro";
import * as Format from "../util/formatter.mjs";
import { linreg } from "../utils.mjs"; import { linreg } from "../utils.mjs";
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;
this.unimportant = 0;
}
clone() {
const k1 = new BiKnot(this.type, this.x, this.y, this.d1, this.d2);
k1.origTangent = this.origTangent;
k1.proposedNormal = this.proposedNormal;
k1.unimportant = this.unimportant;
return k1;
}
toShapeString() {
return Format.tuple(
this.type,
Format.n(this.x),
Format.n(this.y),
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))
: "",
this.proposedNormal
? Format.tuple(Format.n(this.proposedNormal.x), Format.n(this.proposedNormal.y))
: "",
this.unimportant
);
}
}
export class BiKnotCollector {
constructor(gizmo, contrast) {
this.gizmo = gizmo;
this.contrast = contrast;
this.controlKnots = [];
this.defaultD1 = 0;
this.defaultD2 = 0;
}
pushKnot(type, x, y) {
const tfZ = this.gizmo.applyXY(x, y);
const k0 = this.controlKnots[this.controlKnots.length - 1];
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) {
(k0.d1 = l), (k0.d2 = r);
} else {
(this.defaultD1 = l), (this.defaultD2 = r);
}
}
headsTo(direction) {
const transformedDirection = this.gizmo.applyOffset(direction);
const k0 = this.controlKnots[this.controlKnots.length - 1];
if (k0) k0.proposedNormal = transformedDirection;
}
setType(type) {
const k0 = this.controlKnots[this.controlKnots.length - 1];
if (k0) k0.type = type;
}
setUnimportant() {
const k0 = this.controlKnots[this.controlKnots.length - 1];
if (k0) k0.unimportant = 1;
}
}
export class SpiroExpander { export class SpiroExpander {
constructor(gizmo, contrast, closed, cks) { constructor(gizmo, contrast, closed, biKnots) {
this.gizmo = gizmo; this.m_gizmo = gizmo;
this.contrast = contrast; this.m_contrast = contrast;
this.closed = closed; this.m_closed = closed;
this.controlKnots = cks; this.m_biKnots = [];
for (const knot of biKnots) {
this.m_biKnots.push(knot.withGizmo(gizmo));
}
} }
initializeNormals() { initializeNormals() {
const normalRectifier = new NormalRectifier(this.controlKnots, this.gizmo); const normalRectifier = new NormalRectifier(this.m_biKnots, this.m_gizmo);
SpiroJs.spiroToArcsOnContext(this.controlKnots, this.closed, normalRectifier); SpiroJs.spiroToArcsOnContext(this.m_biKnots, this.m_closed, normalRectifier);
} }
iterateNormals() { iterateNormals() {
const centerBone = this.getPass2Knots(); const centerBone = this.getPass2Knots();
const normalRectifier = new NormalRectifier(this.controlKnots, this.gizmo); const normalRectifier = new NormalRectifier(this.m_biKnots, this.m_gizmo);
SpiroJs.spiroToArcsOnContext(centerBone, this.closed, normalRectifier); SpiroJs.spiroToArcsOnContext(centerBone, this.m_closed, normalRectifier);
} }
getPass2Knots() { getPass2Knots() {
const expanded = this.expand(this.contrast); const expanded = this.expand(this.m_contrast);
const middles = []; const middles = [];
for (let j = 0; j < this.controlKnots.length; j++) { for (let j = 0; j < this.m_biKnots.length; j++) {
const lhs = this.gizmo.unapply(expanded.lhs[j]); const lhs = this.m_gizmo.unapply(expanded.lhs[j]);
const rhs = this.gizmo.unapply(expanded.rhs[j]); const rhs = this.m_gizmo.unapply(expanded.rhs[j]);
middles[j] = { middles[j] = {
x: 0.5 * (lhs.x + rhs.x), x: 0.5 * (lhs.x + rhs.x),
y: 0.5 * (lhs.y + rhs.y), y: 0.5 * (lhs.y + rhs.y),
type: this.controlKnots[j].type, type: this.m_biKnots[j].type,
unimportant: this.controlKnots[j].unimportant unimportant: this.m_biKnots[j].unimportant
}; };
} }
return middles; return middles;
@ -114,8 +42,8 @@ export class SpiroExpander {
const lhs = [], const lhs = [],
rhs = []; rhs = [];
// Initialize knots // Initialize knots
for (let j = 0; j < this.controlKnots.length; j++) { for (let j = 0; j < this.m_biKnots.length; j++) {
const knot = this.controlKnots[j]; const knot = this.m_biKnots[j];
lhs[j] = { lhs[j] = {
type: knot.type, type: knot.type,
unimportant: knot.unimportant, unimportant: knot.unimportant,
@ -130,16 +58,16 @@ export class SpiroExpander {
}; };
} }
// Create important knots // Create important knots
for (let j = 0; j < this.controlKnots.length; j++) { for (let j = 0; j < this.m_biKnots.length; j++) {
const knot = this.controlKnots[j]; const knot = this.m_biKnots[j];
if (knot.unimportant) continue; if (knot.unimportant) continue;
let dx, dy; let dx, dy;
if (knot.proposedNormal) { if (knot.proposedNormal) {
dx = knot.proposedNormal.x; dx = knot.proposedNormal.x;
dy = knot.proposedNormal.y; dy = knot.proposedNormal.y;
} else { } else {
dx = normalX(knot.origTangent, this.contrast); dx = normalX(knot.origTangent, this.m_contrast);
dy = normalY(knot.origTangent, this.contrast); dy = normalY(knot.origTangent, this.m_contrast);
} }
lhs[j].x = knot.x + knot.d1 * dx; lhs[j].x = knot.x + knot.d1 * dx;
lhs[j].y = knot.y + knot.d1 * dy; lhs[j].y = knot.y + knot.d1 * dy;
@ -151,34 +79,34 @@ export class SpiroExpander {
const lhsUntransformed = [], const lhsUntransformed = [],
rhsUntransformed = []; rhsUntransformed = [];
for (const z of lhs) { for (const z of lhs) {
const u = this.gizmo.unapply(z); const u = this.m_gizmo.unapply(z);
lhsUntransformed.push({ type: z.type, x: u.x, y: u.y }); lhsUntransformed.push({ type: z.type, x: u.x, y: u.y });
} }
for (const z of rhs) { for (const z of rhs) {
const u = this.gizmo.unapply(z); const u = this.m_gizmo.unapply(z);
rhsUntransformed.push({ type: z.type, x: u.x, y: u.y }); rhsUntransformed.push({ type: z.type, x: u.x, y: u.y });
} }
return { lhs, rhs, lhsUntransformed, rhsUntransformed }; return { lhs, rhs, lhsUntransformed, rhsUntransformed };
} }
interpolateUnimportantKnots(lhs, rhs) { interpolateUnimportantKnots(lhs, rhs) {
for (let j = 0; j < this.controlKnots.length; j++) { for (let j = 0; j < this.m_biKnots.length; j++) {
const knot = this.controlKnots[j]; const knot = this.m_biKnots[j];
if (!knot.unimportant) continue; if (!knot.unimportant) continue;
let jBefore, jAfter; let jBefore, jAfter;
for (jBefore = j - 1; cyNth(this.controlKnots, jBefore).unimportant; jBefore--); for (jBefore = j - 1; cyNth(this.m_biKnots, jBefore).unimportant; jBefore--);
for (jAfter = j + 1; cyNth(this.controlKnots, jAfter).unimportant; jAfter++); for (jAfter = j + 1; cyNth(this.m_biKnots, jAfter).unimportant; jAfter++);
const knotBefore = this.gizmo.unapply(cyNth(this.controlKnots, jBefore)), const knotBefore = this.m_gizmo.unapply(cyNth(this.m_biKnots, jBefore)),
knotAfter = this.gizmo.unapply(cyNth(this.controlKnots, jAfter)), knotAfter = this.m_gizmo.unapply(cyNth(this.m_biKnots, jAfter)),
ref = this.gizmo.unapply(knot), ref = this.m_gizmo.unapply(knot),
lhsBefore = this.gizmo.unapply(cyNth(lhs, jBefore)), lhsBefore = this.m_gizmo.unapply(cyNth(lhs, jBefore)),
lhsAfter = this.gizmo.unapply(cyNth(lhs, jAfter)), lhsAfter = this.m_gizmo.unapply(cyNth(lhs, jAfter)),
rhsBefore = this.gizmo.unapply(cyNth(rhs, jBefore)), rhsBefore = this.m_gizmo.unapply(cyNth(rhs, jBefore)),
rhsAfter = this.gizmo.unapply(cyNth(rhs, jAfter)); rhsAfter = this.m_gizmo.unapply(cyNth(rhs, jAfter));
const lhsTf = this.gizmo.applyXY( const lhsTf = this.m_gizmo.applyXY(
linreg(knotBefore.x, lhsBefore.x, knotAfter.x, lhsAfter.x, ref.x), linreg(knotBefore.x, lhsBefore.x, knotAfter.x, lhsAfter.x, ref.x),
linreg(knotBefore.y, lhsBefore.y, knotAfter.y, lhsAfter.y, ref.y) linreg(knotBefore.y, lhsBefore.y, knotAfter.y, lhsAfter.y, ref.y)
); );
const rhsTf = this.gizmo.applyXY( const rhsTf = this.m_gizmo.applyXY(
linreg(knotBefore.x, rhsBefore.x, knotAfter.x, rhsAfter.x, ref.x), linreg(knotBefore.x, rhsBefore.x, knotAfter.x, rhsAfter.x, ref.x),
linreg(knotBefore.y, rhsBefore.y, knotAfter.y, rhsAfter.y, ref.y) linreg(knotBefore.y, rhsBefore.y, knotAfter.y, rhsAfter.y, ref.y)
); );
@ -189,33 +117,33 @@ export class SpiroExpander {
} }
class NormalRectifier { class NormalRectifier {
constructor(stage1ControlKnots, gizmo) { constructor(stage1ControlKnots, gizmo) {
this.gizmo = gizmo; this.m_gizmo = gizmo;
this.controlKnots = stage1ControlKnots; this.m_biKnots = stage1ControlKnots;
this.nKnotsProcessed = 0; this.m_nKnotsProcessed = 0;
} }
beginShape() {} beginShape() {}
endShape() {} endShape() {}
moveTo(x, y) { moveTo(x, y) {
this.nKnotsProcessed += 1; this.m_nKnotsProcessed += 1;
} }
arcTo(arc, x, y) { arcTo(arc, x, y) {
if (this.nKnotsProcessed === 1) { if (this.m_nKnotsProcessed === 1) {
const d = this.gizmo.applyOffsetXY(arc.deriveX0, arc.deriveY0); const d = this.m_gizmo.applyOffsetXY(arc.deriveX0, arc.deriveY0);
if (isTangentValid(d)) { if (isTangentValid(d)) {
this.controlKnots[0].origTangent = d; this.m_biKnots[0].origTangent = d;
} else { } else {
throw new Error("NaN angle detected."); throw new Error("NaN angle detected.");
} }
} }
if (this.controlKnots[this.nKnotsProcessed]) { if (this.m_biKnots[this.m_nKnotsProcessed]) {
const d = this.gizmo.applyOffsetXY(arc.deriveX1, arc.deriveY1); const d = this.m_gizmo.applyOffsetXY(arc.deriveX1, arc.deriveY1);
if (isTangentValid(d)) { if (isTangentValid(d)) {
this.controlKnots[this.nKnotsProcessed].origTangent = d; this.m_biKnots[this.m_nKnotsProcessed].origTangent = d;
} else { } else {
throw new Error("NaN angle detected."); throw new Error("NaN angle detected.");
} }
} }
this.nKnotsProcessed += 1; this.m_nKnotsProcessed += 1;
} }
} }