Iosevka/font-src/support/spiro-expand.js
2021-07-11 18:30:29 -07:00

243 lines
6.6 KiB
JavaScript

"use strict";
const SpiroJs = require("spiro");
const { linreg } = require("./utils");
const Format = require("./formatter");
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),
Format.n(this.d1),
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
);
}
}
class BiKnotCollector {
constructor(gizmo, contrast) {
this.gizmo = gizmo;
this.contrast = contrast;
this.controlKnots = [];
this.defaultD1 = 0;
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.");
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);
}
}
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 k0 = this.controlKnots[this.controlKnots.length - 1];
if (k0) k0.proposedNormal = direction;
}
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;
}
}
class SpiroExpander {
constructor(gizmo, contrast, closed, cks) {
this.gizmo = gizmo;
this.contrast = contrast;
this.closed = closed;
this.controlKnots = cks;
}
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++) {
const lhs = this.gizmo.unapply(expanded.lhs[j]);
const rhs = this.gizmo.unapply(expanded.rhs[j]);
middles[j] = {
x: 0.5 * (lhs.x + rhs.x),
y: 0.5 * (lhs.y + rhs.y),
type: this.controlKnots[j].type,
unimportant: this.controlKnots[j].unimportant
};
}
return middles;
}
expand() {
const lhs = [],
rhs = [];
// Create important knots
for (let j = 0; j < this.controlKnots.length; j++) {
const knot = this.controlKnots[j];
if (knot.unimportant) continue;
let dx, dy;
if (knot.proposedNormal) {
dx = knot.proposedNormal.x;
dy = knot.proposedNormal.y;
} else {
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
};
}
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++);
const knotBefore = this.gizmo.unapply(this.controlKnots[jBefore]),
knotAfter = this.gizmo.unapply(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]);
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)
})
};
}
}
}
class NormalRectifier {
constructor(stage1ControlKnots, gizmo) {
this.gizmo = gizmo;
this.controlKnots = stage1ControlKnots;
this.nKnotsProcessed = 0;
}
beginShape() {}
endShape() {}
moveTo(x, y, unimportant) {
if (unimportant) return;
this.nKnotsProcessed += 1;
}
arcTo(arc, x, y) {
if (this.nKnotsProcessed === 1) {
const d = this.gizmo.applyOffset({ x: arc.deriveX0, y: arc.deriveY0 });
if (isTangentValid(d)) this.controlKnots[0].origTangent = d;
else throw new Error("NaN angle detected.");
}
{
const d = this.gizmo.applyOffset({ x: arc.deriveX1, y: arc.deriveY1 });
if (isTangentValid(d)) this.controlKnots[this.nKnotsProcessed].origTangent = d;
else throw new Error("NaN angle detected.");
}
this.nKnotsProcessed += 1;
}
}
function 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 computeNormalAngle(gizmo, x, y) {
const tfd = gizmo.applyOffset({ x, y });
return Math.PI / 2 + Math.atan2(tfd.y, tfd.x);
}
exports.BiKnotCollector = BiKnotCollector;
exports.SpiroExpander = SpiroExpander;