Move around files so the repo will be organized as a monorepo.

This commit is contained in:
be5invis 2023-12-03 02:49:33 -08:00
parent 65d1880a84
commit 08c69f0fd3
365 changed files with 1477 additions and 1262 deletions

View file

@ -0,0 +1,14 @@
export class Anchor {
constructor(x, y) {
this.x = x;
this.y = y;
}
transform(tfm) {
return Anchor.transform(tfm, this);
}
static transform(tfm, a) {
const x = a.x * tfm.xx + a.y * tfm.yx + tfm.x;
const y = a.x * tfm.xy + a.y * tfm.yy + tfm.y;
return new Anchor(x, y);
}
}

View file

@ -0,0 +1,42 @@
import { mix } from "@iosevka/util";
export class Box {
constructor(t, b, l, r) {
this.top = t;
this.bottom = this.bot = b;
this.left = l;
this.right = r;
this.xMid = this.xMiddle = mix(l, r, 0.5);
this.yMid = this.yMiddle = mix(b, t, 0.5);
}
withTop(t) {
return new Box(t, this.bottom, this.left, this.right);
}
withBottom(b) {
return new Box(this.top, b, this.left, this.right);
}
withLeft(l) {
return new Box(this.top, this.bottom, l, this.right);
}
withRight(r) {
return new Box(this.top, this.bottom, this.left, r);
}
withXPadding(d) {
return new Box(this.top, this.bottom, this.left + d, this.right - d);
}
withYPadding(d) {
return new Box(this.top - d, this.bottom + d, this.left, this.right);
}
mixX(t) {
return mix(this.left, this.right, t);
}
mixXMidLeft(t) {
return mix(this.xMid, this.left, t);
}
mixXMidRight(t) {
return mix(this.xMid, this.right, t);
}
mixY(t) {
return mix(this.bottom, this.top, t);
}
}

View file

@ -0,0 +1,152 @@
import * as TypoGeom from "typo-geom";
import { Point } from "./point.mjs";
import { Transform } from "./transform.mjs";
function contourToRep(contour) {
let c = [];
for (const z of contour) c.push({ type: z.type, x: z.x, y: z.y });
return c;
}
function repToContour(contourRep) {
let c = [];
for (const z of contourRep) c.push(Point.fromXY(z.type, z.x, z.y));
return c;
}
function convertContourToArcs(contour) {
if (!contour || !contour.length) return [];
const newContour = [];
let z0 = Point.from(Point.Type.Corner, contour[0]);
for (let j = 1; j < contour.length; j++) {
const z = contour[j];
switch (z.type) {
case Point.Type.CubicStart: {
const z1 = z;
const z2 = contour[j + 1];
const z3 = contour[j + 2];
newContour.push(
new TypoGeom.Arcs.Bez3(
z0,
Point.from(Point.Type.CubicStart, z1),
Point.from(Point.Type.CubicEnd, z2),
Point.from(Point.Type.Corner, z3)
)
);
z0 = z3;
j += 2;
break;
}
case Point.Type.Quadratic: {
const zc = z;
let zf = contour[j + 1] || contour[0];
const zfIsCorner = zf.type === Point.Type.contour;
if (!zfIsCorner) zf = Point.from(Point.Type.Corner, zc).mix(0.5, zf);
newContour.push(
new TypoGeom.Arcs.Bez3(
z0,
Point.from(Point.Type.CubicStart, z0).mix(2 / 3, zc),
Point.from(Point.Type.CubicEnd, zf).mix(2 / 3, zc),
Point.from(Point.Type.Corner, zf)
)
);
z0 = zf;
if (zfIsCorner) j++;
break;
}
default: {
newContour.push(
TypoGeom.Arcs.Bez3.fromStraightSegment(
new TypoGeom.Arcs.StraightSegment(z0, Point.from(Point.Type.Corner, z))
)
);
z0 = z;
break;
}
}
}
return newContour;
}
export const SPIRO_PRECISION = 1 / 2;
export const OCCURRENT_PRECISION = 1 / 16;
export const GEOMETRY_PRECISION = 1 / 4;
export const BOOLE_RESOLUTION = 0x4000;
export class OffsetCurve {
constructor(bone, offset, contrast) {
this.bone = bone;
this.offset = offset;
this.contrast = contrast;
}
eval(t) {
const c = this.bone.eval(t);
const d = this.bone.derivative(t);
const absD = Math.hypot(d.x, d.y);
return {
x: c.x - (d.y / absD) * this.offset * this.contrast,
y: c.y + (d.x / absD) * this.offset
};
}
derivative(t) {
const DELTA = 1 / 0x10000;
const forward = this.eval(t + DELTA);
const backward = this.eval(t - DELTA);
return {
x: (forward.x - backward.x) / (2 * DELTA),
y: (forward.y - backward.y) / (2 * DELTA)
};
}
}
export class ReverseCurve {
constructor(original) {
this.m_original = original;
}
eval(t) {
return this.m_original.eval(1 - t);
}
derivative(t) {
return -this.m_original.derivative(1 - t);
}
}
export function convertShapeToArcs(shape) {
return shape.map(convertContourToArcs);
}
export function shapeToRep(shape) {
return shape.map(contourToRep);
}
export function repToShape(shapeRep) {
return shapeRep.map(repToContour);
}
export class BezToContoursSink {
constructor(gizmo) {
this.gizmo = gizmo || Transform.Id();
this.contours = [];
this.lastContour = [];
}
beginShape() {}
endShape() {
if (this.lastContour.length) {
this.contours.push(this.lastContour);
}
this.lastContour = [];
}
moveTo(x, y) {
this.endShape();
this.lastContour.push(Point.transformedXY(this.gizmo, Point.Type.Corner, x, y));
}
lineTo(x, y) {
this.lastContour.push(Point.transformedXY(this.gizmo, Point.Type.Corner, x, y));
}
curveTo(xc, yc, x, y) {
this.lastContour.push(Point.transformedXY(this.gizmo, Point.Type.Quadratic, xc, yc));
this.lastContour.push(Point.transformedXY(this.gizmo, Point.Type.Corner, x, y));
}
cubicTo(x1, y1, x2, y2, x, y) {
this.lastContour.push(Point.transformedXY(this.gizmo, Point.Type.CubicStart, x1, y1));
this.lastContour.push(Point.transformedXY(this.gizmo, Point.Type.CubicEnd, x2, y2));
this.lastContour.push(Point.transformedXY(this.gizmo, Point.Type.Corner, x, y));
}
}

View file

@ -0,0 +1,546 @@
import crypto from "crypto";
import * as Format from "@iosevka/util/formatter";
import * as SpiroJs from "spiro";
import * as TypoGeom from "typo-geom";
import * as CurveUtil from "./curve-util.mjs";
import { Point } from "./point.mjs";
import { SpiroExpander } from "./spiro-expand.mjs";
import { Transform } from "./transform.mjs";
export class GeometryBase {
asContours() {
throw new Error("Unimplemented");
}
asReferences() {
throw new Error("Unimplemented");
}
getDependencies() {
throw new Error("Unimplemented");
}
unlinkReferences() {
return this;
}
filterTag(fn) {
return this;
}
isEmpty() {
return true;
}
measureComplexity() {
return 0;
}
toShapeStringOrNull() {
return null;
}
}
export class ContourGeometry extends GeometryBase {
constructor(points) {
super();
this.m_points = [];
for (const z of points) {
this.m_points.push(Point.from(z.type, z));
}
}
asContours() {
if (this.isEmpty()) return [];
let c1 = [];
for (const z of this.m_points) c1.push(Point.from(z.type, z));
return [c1];
}
asReferences() {
return null;
}
getDependencies() {
return null;
}
filterTag(fn) {
return this;
}
isEmpty() {
return !this.m_points.length;
}
measureComplexity() {
for (const z of this.m_points) {
if (!isFinite(z.x) || !isFinite(z.y)) return 0xffff;
}
return this.m_points.length;
}
toShapeStringOrNull() {
return Format.struct(`ContourGeometry`, Format.list(this.m_points.map(Format.typedPoint)));
}
}
export class SpiroGeometry extends GeometryBase {
constructor(gizmo, closed, knots) {
super();
this.m_knots = [];
for (const k of knots) {
this.m_knots.push({ type: k.type, x: k.x, y: k.y });
}
this.m_closed = closed;
this.m_gizmo = gizmo;
this.m_cachedContours = null;
}
asContours() {
if (this.m_cachedContours) return this.m_cachedContours;
const s = new CurveUtil.BezToContoursSink(this.m_gizmo);
SpiroJs.spiroToBezierOnContext(this.m_knots, this.m_closed, s, CurveUtil.SPIRO_PRECISION);
this.m_cachedContours = s.contours;
return this.m_cachedContours;
}
asReferences() {
return null;
}
getDependencies() {
return null;
}
filterTag(fn) {
return this;
}
isEmpty() {
return !this.m_knots.length;
}
measureComplexity() {
for (const z of this.m_knots) {
if (!isFinite(z.x) || !isFinite(z.y)) return 0xffff;
}
return this.m_knots.length;
}
toShapeStringOrNull() {
return Format.struct(
"SpiroGeometry",
Format.gizmo(this.m_gizmo),
this.m_closed,
Format.list(this.m_knots.map(Format.typedPoint))
);
}
}
export class DiSpiroGeometry extends GeometryBase {
constructor(gizmo, contrast, closed, biKnots) {
super();
this.m_biKnots = biKnots; // untransformed
this.m_closed = closed;
this.m_gizmo = gizmo;
this.m_contrast = contrast;
this.m_cachedExpansionResults = null;
this.m_cachedContours = null;
}
asContours() {
if (this.m_cachedContours) return this.m_cachedContours;
const expandResult = this.expand();
const lhs = [...expandResult.lhsUntransformed];
const rhs = [...expandResult.rhsUntransformed];
let rawGeometry;
if (this.m_closed) {
rawGeometry = new CombineGeometry([
new SpiroGeometry(this.m_gizmo, true, lhs),
new SpiroGeometry(this.m_gizmo, true, rhs.reverse())
]);
} else {
lhs[0].type = lhs[lhs.length - 1].type = "corner";
rhs[0].type = rhs[rhs.length - 1].type = "corner";
const allKnots = lhs.concat(rhs.reverse());
rawGeometry = new SpiroGeometry(this.m_gizmo, true, allKnots);
}
this.m_cachedContours = rawGeometry.asContours();
return this.m_cachedContours;
}
expand() {
if (this.m_cachedExpansionResults) return this.m_cachedExpansionResults;
const expander = new SpiroExpander(
this.m_gizmo,
this.m_contrast,
this.m_closed,
this.m_biKnots
);
expander.initializeNormals();
expander.iterateNormals();
expander.iterateNormals();
expander.iterateNormals();
expander.iterateNormals();
this.m_cachedExpansionResults = expander.expand();
return this.m_cachedExpansionResults;
}
asReferences() {
return null;
}
getDependencies() {
return null;
}
filterTag(fn) {
return this;
}
isEmpty() {
return !this.m_biKnots.length;
}
measureComplexity() {
for (const z of this.m_biKnots) {
if (!isFinite(z.x) || !isFinite(z.y)) return 0xffff;
}
return this.m_biKnots.length;
}
toShapeStringOrNull() {
return Format.struct(
"DiSpiroGeometry",
Format.gizmo(this.m_gizmo),
Format.n(this.m_contrast),
this.m_closed,
Format.list(this.m_biKnots.map(z => z.toShapeString()))
);
}
}
export class ReferenceGeometry extends GeometryBase {
constructor(glyph, x, y) {
super();
if (!glyph || !glyph.geometry) throw new TypeError("Invalid glyph");
this.m_glyph = glyph;
this.m_x = x || 0;
this.m_y = y || 0;
}
unwrap() {
return new TransformedGeometry(
this.m_glyph.geometry,
Transform.Translate(this.m_x, this.m_y)
);
}
asContours() {
if (this.isEmpty()) return [];
return this.unwrap().asContours();
}
asReferences() {
if (this.isEmpty()) return [];
return [{ glyph: this.m_glyph, x: this.m_x, y: this.m_y }];
}
getDependencies() {
return [this.m_glyph];
}
filterTag(fn) {
if (this.isEmpty()) return null;
return this.unwrap().filterTag(fn);
}
isEmpty() {
if (!this.m_glyph || !this.m_glyph.geometry) return true;
return this.m_glyph.geometry.isEmpty();
}
measureComplexity() {
return this.m_glyph.geometry.measureComplexity();
}
unlinkReferences() {
return this.unwrap().unlinkReferences();
}
toShapeStringOrNull() {
let sTarget = this.m_glyph.geometry.toShapeStringOrNull();
if (!sTarget) return null;
return Format.struct("ReferenceGeometry", sTarget, Format.n(this.m_x), Format.n(this.m_y));
}
}
export class TaggedGeometry extends GeometryBase {
constructor(g, tag) {
super();
this.m_geom = g;
this.m_tag = tag;
}
asContours() {
return this.m_geom.asContours();
}
asReferences() {
return this.m_geom.asReferences();
}
getDependencies() {
return this.m_geom.getDependencies();
}
filterTag(fn) {
if (!fn(this.m_tag)) return null;
else return new TaggedGeometry(this.m_geom.filterTag(fn), this.m_tag);
}
isEmpty() {
return this.m_geom.isEmpty();
}
measureComplexity() {
return this.m_geom.measureComplexity();
}
unlinkReferences() {
return this.m_geom.unlinkReferences();
}
toShapeStringOrNull() {
return this.m_geom.toShapeStringOrNull();
}
}
export class TransformedGeometry extends GeometryBase {
constructor(g, tfm) {
super();
this.m_geom = g;
this.m_transform = tfm;
}
asContours() {
let result = [];
for (const c of this.m_geom.asContours()) {
let c1 = [];
for (const z of c) c1.push(Point.transformed(this.m_transform, z));
result.push(c1);
}
return result;
}
asReferences() {
if (!Transform.isTranslate(this.m_transform)) return null;
const rs = this.m_geom.asReferences();
if (!rs) return null;
let result = [];
for (const { glyph, x, y } of rs)
result.push({ glyph, x: x + this.m_transform.x, y: y + this.m_transform.y });
return result;
}
getDependencies() {
return this.m_geom.getDependencies();
}
filterTag(fn) {
const e = this.m_geom.filterTag(fn);
if (!e) return null;
return new TransformedGeometry(e, this.m_transform);
}
isEmpty() {
return this.m_geom.isEmpty();
}
measureComplexity() {
return this.m_geom.measureComplexity();
}
unlinkReferences() {
const unwrapped = this.m_geom.unlinkReferences();
if (Transform.isIdentity(this.m_transform)) {
return unwrapped;
} else if (
unwrapped instanceof TransformedGeometry &&
Transform.isTranslate(this.m_transform) &&
Transform.isTranslate(unwrapped.m_transform)
) {
return new TransformedGeometry(
unwrapped.m_geom,
Transform.Translate(
this.m_transform.x + unwrapped.m_transform.x,
this.m_transform.y + unwrapped.m_transform.y
)
);
} else {
return new TransformedGeometry(unwrapped, this.m_transform);
}
}
toShapeStringOrNull() {
const sTarget = this.m_geom.toShapeStringOrNull();
if (!sTarget) return null;
return Format.struct("TransformedGeometry", sTarget, Format.gizmo(this.m_transform));
}
}
export class RadicalGeometry extends GeometryBase {
constructor(g) {
super();
this.m_geom = g;
}
asContours() {
return this.m_geom.asContours();
}
asReferences() {
return null;
}
getDependencies() {
return this.m_geom.getDependencies();
}
filterTag(fn) {
const e = this.m_geom.filterTag(fn);
if (!e) return null;
return new RadicalGeometry(e);
}
isEmpty() {
return this.m_geom.isEmpty();
}
measureComplexity() {
return this.m_geom.measureComplexity();
}
unlinkReferences() {
return this.m_geom.unlinkReferences();
}
toShapeStringOrNull() {
const sTarget = this.m_geom.toShapeStringOrNull();
if (!sTarget) return null;
return Format.struct("RadicalGeometry", sTarget);
}
}
export class CombineGeometry extends GeometryBase {
constructor(parts) {
super();
this.m_parts = parts || [];
}
with(g) {
if (g instanceof CombineGeometry) {
return new CombineGeometry([...this.m_parts, ...g.m_parts]);
} else {
return new CombineGeometry([...this.m_parts, g]);
}
}
asContours() {
let results = [];
for (const part of this.m_parts) {
for (const c of part.asContours()) {
results.push(c);
}
}
return results;
}
asReferences() {
let results = [];
for (const part of this.m_parts) {
const rs = part.asReferences();
if (!rs) return null;
for (const c of rs) {
results.push(c);
}
}
return results;
}
getDependencies() {
let results = [];
for (const part of this.m_parts) {
const rs = part.getDependencies();
if (!rs) continue;
for (const c of rs) results.push(c);
}
return results;
}
filterTag(fn) {
let filtered = [];
for (const part of this.m_parts) {
const fp = part.filterTag(fn);
if (fp) filtered.push(fp);
}
return new CombineGeometry(filtered);
}
isEmpty() {
for (const part of this.m_parts) if (!part.isEmpty()) return false;
return true;
}
measureComplexity() {
let s = 0;
for (const part of this.m_parts) s += part.measureComplexity();
}
unlinkReferences() {
let parts = [];
for (const part of this.m_parts) {
const unwrapped = part.unlinkReferences();
if (unwrapped instanceof CombineGeometry) {
for (const p of unwrapped.m_parts) parts.push(p);
} else {
parts.push(unwrapped);
}
}
return new CombineGeometry(parts);
}
toShapeStringOrNull() {
let sParts = [];
for (const item of this.m_parts) {
const sPart = item.toShapeStringOrNull();
if (!sPart) return null;
sParts.push(sPart);
}
return Format.struct("CombineGeometry", Format.list(sParts));
}
}
export class BooleanGeometry extends GeometryBase {
constructor(operator, operands) {
super();
this.m_operator = operator;
this.m_operands = operands;
this.m_resolved = null;
}
asContours() {
if (this.m_resolved) return this.m_resolved;
this.m_resolved = this.asContoursImpl();
return this.m_resolved;
}
asContoursImpl() {
if (this.m_operands.length === 0) return [];
let arcs = CurveUtil.convertShapeToArcs(this.m_operands[0].asContours());
for (let j = 1; j < this.m_operands.length; j++) {
arcs = TypoGeom.Boolean.combine(
this.m_operator,
arcs,
CurveUtil.convertShapeToArcs(this.m_operands[j].asContours()),
TypoGeom.Boolean.PolyFillType.pftNonZero,
TypoGeom.Boolean.PolyFillType.pftNonZero,
CurveUtil.BOOLE_RESOLUTION
);
}
const ctx = new CurveUtil.BezToContoursSink();
TypoGeom.ShapeConv.transferBezArcShape(arcs, ctx);
return ctx.contours;
}
asReferences() {
return null;
}
getDependencies() {
let results = [];
for (const part of this.m_operands) {
const rs = part.getDependencies();
if (!rs) continue;
for (const c of rs) results.push(c);
}
return results;
}
filterTag(fn) {
let filtered = [];
for (const operand of this.m_operands) {
const fp = operand.filterTag(fn);
if (fp) filtered.push(fp);
}
return new BooleanGeometry(this.m_operator, filtered);
}
isEmpty() {
for (const operand of this.m_operands) if (!operand.isEmpty()) return false;
return true;
}
measureComplexity() {
let s = 0;
for (const operand of this.m_operands) s += operand.measureComplexity();
}
unlinkReferences() {
if (this.m_operands.length === 0) return new CombineGeometry([]);
if (this.m_operands.length === 1) return this.m_operands[0].unlinkReferences();
let operands = [];
for (const operand of this.m_operands) {
operands.push(operand.unlinkReferences());
}
return new BooleanGeometry(this.m_operator, operands);
}
toShapeStringOrNull() {
let sParts = [];
for (const item of this.m_operands) {
const sPart = item.toShapeStringOrNull();
if (!sPart) return null;
sParts.push(sPart);
}
return Format.struct("BooleanGeometry", this.m_operator, Format.list(sParts));
}
}
export function combineWith(a, b) {
if (a instanceof CombineGeometry) {
return a.with(b);
} else {
return new CombineGeometry([a, b]);
}
}
export function hashGeometry(geom) {
const s = geom.toShapeStringOrNull();
if (!s) return null;
return crypto.createHash("sha256").update(s).digest("hex");
}

View file

@ -0,0 +1,82 @@
import { mix } from "@iosevka/util";
export class Vec2 {
constructor(x, y) {
this.x = x;
this.y = y;
}
static from(z) {
return new Vec2(z.x, z.y);
}
}
export class Point {
constructor(type, x, y) {
this.type = type;
this.x = x;
this.y = y;
}
get on() {
throw new Error("Unreachable");
}
get cubic() {
throw new Error("Unreachable");
}
add(z2) {
return this.addScale(1, z2);
}
addScale(scale, z2) {
return new Point(this.type, this.x + scale * z2.x, this.y + scale * z2.y);
}
mix(scale, z2) {
return new Point(
this.type,
this.x + scale * (z2.x - this.x),
this.y + scale * (z2.y - this.y)
);
}
scale(t) {
return new Point(this.type, t * this.x, t * this.y);
}
round(d) {
return new Point(this.type, Math.round(d * this.x) / d, Math.round(d * this.y) / d);
}
static from(type, z) {
return new Point(type, z.x || 0, z.y || 0);
}
static fromXY(type, x, y) {
return new Point(type, x || 0, y || 0);
}
static corner(x, y) {
return new Point(Point.Type.Corner, x || 0, y || 0);
}
static withX(z, x) {
return new Point(z.type, x || 0, z.y);
}
static withY(z, y) {
return new Point(z.type, z.x, y || 0);
}
static transformed(tfm, z) {
return Point.transformedXY(tfm, z.type, z.x, z.y);
}
static transformedXY(tfm, type, x, y) {
return new Point(
type,
x * tfm.xx + y * tfm.yx + tfm.x || 0,
x * tfm.xy + y * tfm.yy + tfm.y || 0
);
}
static translated(z, dx, dy) {
return new Point(z.type, z.x + dx || 0, z.y + dy || 0);
}
static mix(type, a, b, p) {
return new Point(type, mix(a.x, b.x, p), mix(a.y, b.y, p));
}
}
Point.Type = {
Corner: 0,
CubicStart: 1,
CubicEnd: 2,
Quadratic: 3
};

View file

@ -0,0 +1,194 @@
import * as Format from "@iosevka/util/formatter";
///////////////////////////////////////////////////////////////////////////////////////////////////
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 " + 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) {
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.proposedNormal = null;
this.unimportant = 0;
// Derived properties
this.origTangent = null;
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,
this.unimportant,
Format.n(this.x),
Format.n(this.y),
this.d1 == null ? "" : Format.n(this.d1),
this.d2 == null ? "" : Format.n(this.d2),
this.proposedNormal
? Format.tuple(Format.n(this.proposedNormal.x), Format.n(this.proposedNormal.y))
: ""
);
}
}
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;
}
export class ImportanceControlKnot extends ControlKnot {
constructor(type, x, y, unimportant) {
super(type, x, y, null);
this.unimportant = unimportant;
}
}

View file

@ -0,0 +1,151 @@
import { linreg, mix } from "@iosevka/util";
import * as SpiroJs from "spiro";
import { Vec2 } from "./point.mjs";
import { ControlKnot } from "./spiro-control.mjs";
///////////////////////////////////////////////////////////////////////////////////////////////////
export class SpiroExpander {
constructor(gizmo, contrast, closed, biKnots) {
this.m_gizmo = gizmo;
this.m_contrast = contrast;
this.m_closed = closed;
this.m_biKnotsU = Array.from(biKnots);
this.m_biKnotsT = biKnots.map(k => k.withGizmo(gizmo));
}
initializeNormals() {
const normalRectifier = new NormalRectifier(this.m_biKnotsT, this.m_gizmo);
SpiroJs.spiroToArcsOnContext(this.m_biKnotsT, this.m_closed, normalRectifier);
}
iterateNormals() {
const centerBone = this.getPass2Knots();
const normalRectifier = new NormalRectifier(this.m_biKnotsT, this.m_gizmo);
SpiroJs.spiroToArcsOnContext(centerBone, this.m_closed, normalRectifier);
}
getPass2Knots() {
const expanded = this.expand(this.m_contrast);
const middles = [];
for (let j = 0; j < this.m_biKnotsT.length; j++) {
const lhs = expanded.lhs[j];
const rhs = expanded.rhs[j];
middles[j] = new ControlKnot(
this.m_biKnotsT[j].type,
mix(lhs.x, rhs.x, 0.5),
mix(lhs.y, rhs.y, 0.5)
);
}
return middles;
}
expand() {
const lhs = [],
rhs = [],
lhsUntransformed = [],
rhsUntransformed = [];
for (let j = 0; j < this.m_biKnotsT.length; j++) {
const knot = this.m_biKnotsT[j];
lhs[j] = new ControlKnot(knot.type, 0, 0);
rhs[j] = new ControlKnot(reverseKnotType(knot.type), 0, 0);
lhsUntransformed[j] = new ControlKnot(knot.type, 0, 0);
rhsUntransformed[j] = new ControlKnot(reverseKnotType(knot.type), 0, 0);
}
for (let j = 0; j < this.m_biKnotsT.length; j++) {
const knotT = this.m_biKnotsT[j];
if (knotT.unimportant) continue;
let dx, dy;
if (knotT.proposedNormal) {
dx = knotT.proposedNormal.x;
dy = knotT.proposedNormal.y;
} else {
dx = normalX(knotT.origTangent, this.m_contrast);
dy = normalY(knotT.origTangent, this.m_contrast);
}
lhs[j].x = knotT.x + knotT.d1 * dx;
lhs[j].y = knotT.y + knotT.d1 * dy;
rhs[j].x = knotT.x - knotT.d2 * dx;
rhs[j].y = knotT.y - knotT.d2 * dy;
this.m_gizmo.unapplyToSink(lhs[j], lhsUntransformed[j]);
this.m_gizmo.unapplyToSink(rhs[j], rhsUntransformed[j]);
}
this.interpolateUnimportantKnots(lhs, rhs, lhsUntransformed, rhsUntransformed);
return { lhs, rhs, lhsUntransformed, rhsUntransformed };
}
interpolateUnimportantKnots(lhsT, rhsT, lhsU, rhsU) {
for (let j = 0; j < this.m_biKnotsU.length; j++) {
const knotU = this.m_biKnotsU[j];
if (!knotU.unimportant) continue;
let jBefore, jAfter;
for (jBefore = j - 1; cyNth(this.m_biKnotsU, jBefore).unimportant; jBefore--);
for (jAfter = j + 1; cyNth(this.m_biKnotsU, jAfter).unimportant; jAfter++);
const knotUBefore = cyNth(this.m_biKnotsU, jBefore),
knotUAfter = cyNth(this.m_biKnotsU, jAfter),
lhsUBefore = cyNth(lhsU, jBefore),
lhsUAfter = cyNth(lhsU, jAfter),
rhsUBefore = cyNth(rhsU, jBefore),
rhsUAfter = cyNth(rhsU, jAfter);
lhsU[j].x = linreg(knotUBefore.x, lhsUBefore.x, knotUAfter.x, lhsUAfter.x, knotU.x);
lhsU[j].y = linreg(knotUBefore.y, lhsUBefore.y, knotUAfter.y, lhsUAfter.y, knotU.y);
rhsU[j].x = linreg(knotUBefore.x, rhsUBefore.x, knotUAfter.x, rhsUAfter.x, knotU.x);
rhsU[j].y = linreg(knotUBefore.y, rhsUBefore.y, knotUAfter.y, rhsUAfter.y, knotU.y);
this.m_gizmo.applyToSink(lhsU[j], lhsT[j]);
this.m_gizmo.applyToSink(rhsU[j], rhsT[j]);
}
}
}
class NormalRectifier {
constructor(stage1ControlKnots, gizmo) {
this.m_gizmo = gizmo;
this.m_biKnots = stage1ControlKnots;
this.m_nKnotsProcessed = 0;
}
beginShape() {}
endShape() {}
moveTo(x, y) {
this.m_nKnotsProcessed += 1;
}
arcTo(arc, x, y) {
if (this.m_nKnotsProcessed === 1) {
const d = new Vec2(arc.deriveX0, arc.deriveY0);
if (isTangentValid(d)) {
this.m_biKnots[0].origTangent = d;
} else {
throw new Error("NaN angle detected.");
}
}
if (this.m_biKnots[this.m_nKnotsProcessed]) {
const d = new Vec2(arc.deriveX1, arc.deriveY1);
if (isTangentValid(d)) {
this.m_biKnots[this.m_nKnotsProcessed].origTangent = d;
} else {
throw new Error("NaN angle detected.");
}
}
this.m_nKnotsProcessed += 1;
}
}
function isTangentValid(d) {
return isFinite(d.x) && isFinite(d.y);
}
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;
}
function cyNth(a, j) {
return a[j % a.length];
}

View file

@ -0,0 +1,97 @@
import { Vec2 } from "./point.mjs";
export class Transform {
constructor(xx, yx, xy, yy, x, y) {
this.xx = xx;
this.yx = yx;
this.xy = xy;
this.yy = yy;
this.x = x;
this.y = y;
}
static Id() {
return new Transform(1, 0, 0, 1, 0, 0);
}
static Translate(x, y) {
return new Transform(1, 0, 0, 1, x, y);
}
applyX(x, y) {
return x * this.xx + y * this.yx + this.x;
}
applyY(x, y) {
return x * this.xy + y * this.yy + this.y;
}
applyXY(x, y) {
return new Vec2(this.applyX(x, y), this.applyY(x, y));
}
applyToSink(pt, sink) {
sink.x = this.applyX(pt.x, pt.y);
sink.y = this.applyY(pt.x, pt.y);
}
apply(pt) {
return this.applyXY(pt.x, pt.y);
}
applyOffset(delta) {
return this.applyOffsetXY(delta.x, delta.y);
}
applyOffsetXY(deltaX, deltaY) {
return {
x: deltaX * this.xx + deltaY * this.yx,
y: deltaX * this.xy + deltaY * this.yy
};
}
unapplyToSink(pt, sink) {
const xx = pt.x - this.x;
const yy = pt.y - this.y;
const denom = this.xx * this.yy - this.xy * this.yx;
sink.x = (xx * this.yy - yy * this.yx) / denom;
sink.y = (yy * this.xx - xx * this.xy) / denom;
}
unapply(pt) {
let sink = new Vec2(0, 0);
this.unapplyToSink(pt, sink);
return sink;
}
inverse() {
const denom = this.xx * this.yy - this.xy * this.yx;
return new Transform(
this.yy / denom,
-this.yx / denom,
-this.xy / denom,
this.xx / denom,
-(this.x * this.yy - this.y * this.yx) / denom,
-(-this.x * this.xy + this.y * this.xx) / denom
);
}
toString() {
return `[[${this.xx} ${this.xy}] [${this.yx} ${this.yy}]] + [[${this.x}] [${this.y}]]`;
}
static isTranslate(tfm) {
return tfm.xx === 1 && tfm.yy === 1 && tfm.xy === 0 && tfm.yx === 0;
}
static isIdentity(tfm) {
return this.isTranslate(tfm) && tfm.x === 0 && tfm.y === 0;
}
static Combine(...tfms) {
let z00 = new Vec2(0, 0);
let z10 = new Vec2(1, 0);
let z01 = new Vec2(0, 1);
for (const tfm of tfms) {
z00 = tfm.apply(z00);
z10 = tfm.apply(z10);
z01 = tfm.apply(z01);
}
return new Transform(
z10.x - z00.x,
z01.x - z00.x,
z10.y - z00.y,
z01.y - z00.y,
z00.x,
z00.y
);
}
}