246 lines
6.3 KiB
JavaScript
246 lines
6.3 KiB
JavaScript
import { linreg } from "@iosevka/util";
|
|
import * as SpiroJs from "spiro";
|
|
|
|
import * as CurveUtil from "./curve-util.mjs";
|
|
import { Point } from "./point.mjs";
|
|
import { MonoKnot } from "./spiro-to-outline.mjs";
|
|
|
|
export class PenKnotCollector {
|
|
constructor(gizmo, defaultProfile, fProfileFixed) {
|
|
this.gizmo = gizmo;
|
|
this.m_profile = defaultProfile;
|
|
this.m_lastKnot = null;
|
|
this.m_finished = false;
|
|
this.m_profileFixed = fProfileFixed;
|
|
|
|
this.knots = [];
|
|
this.closed = false;
|
|
}
|
|
finish() {
|
|
this.m_finished = true;
|
|
}
|
|
pushKnot(c) {
|
|
if (this.m_finished) throw new Error("Cannot push knot after finish");
|
|
let k = new PenKnot(c.type, c.x, c.y, this.m_profile);
|
|
this.knots.push(k);
|
|
this.m_lastKnot = k;
|
|
|
|
c.applyTo(this);
|
|
}
|
|
|
|
setWidth() {}
|
|
headsTo() {}
|
|
setUnimportant() {
|
|
if (this.m_lastKnot) this.m_lastKnot.profile = null;
|
|
}
|
|
setImportant() {} // Not used
|
|
setContrast() {}
|
|
|
|
setProfile(profile) {
|
|
if (this.m_profileFixed) return;
|
|
if (profile.length !== this.m_profile.length)
|
|
throw new Error("Pen profile length mismatch");
|
|
if (this.m_lastKnot) this.m_lastKnot.profile = profile;
|
|
this.m_profile = profile;
|
|
}
|
|
}
|
|
|
|
export class PenKnot {
|
|
constructor(type, x, y, profile) {
|
|
this.type = type;
|
|
this.x = x;
|
|
this.y = y;
|
|
this.profile = profile;
|
|
}
|
|
clone() {
|
|
const k1 = new PenKnot(this.type, this.x, this.y, this.profile);
|
|
return k1;
|
|
}
|
|
withGizmo(gizmo) {
|
|
const tfZ = gizmo.applyXY(this.x, this.y);
|
|
const k1 = new PenKnot(this.type, tfZ.x, tfZ.y, this.profile);
|
|
return k1;
|
|
}
|
|
hash(h) {
|
|
h.beginStruct("PenKnot");
|
|
h.str(this.type);
|
|
h.f64(this.x);
|
|
h.f64(this.y);
|
|
h.bool(this.profile != null);
|
|
if (this.profile) {
|
|
h.beginArray(this.profile.length);
|
|
for (let i = 0; i < this.profile.length; i++) {
|
|
h.f64(this.profile[i].x);
|
|
h.f64(this.profile[i].y);
|
|
}
|
|
h.endArray();
|
|
}
|
|
h.endStruct();
|
|
}
|
|
}
|
|
|
|
export class PenSpiroExpander {
|
|
constructor(gizmo, profile, closed, knots) {
|
|
this.m_gizmo = gizmo;
|
|
this.m_closed = closed;
|
|
this.m_knotsU = Array.from(knots);
|
|
this.m_knotsT = knots.map(k => k.withGizmo(gizmo));
|
|
this.m_profileEdges = profile.length;
|
|
|
|
this.m_traces = [];
|
|
}
|
|
|
|
getGeometry() {
|
|
this.traceAll();
|
|
|
|
const contours = [];
|
|
for (let i = 0; i < this.m_traces.length; i++) {
|
|
const iNext = (i + 1) % this.m_traces.length;
|
|
if (this.m_traces[i].length !== this.m_traces[iNext].length) {
|
|
throw new Error("Different number of arcs in traces");
|
|
}
|
|
for (let j = 0; j < this.m_traces[i].length; j++) {
|
|
const arcForward = this.m_traces[i][j];
|
|
const arcBackward = this.m_traces[iNext][j];
|
|
makeProfiledStroke(contours, arcForward, arcBackward);
|
|
}
|
|
}
|
|
|
|
return contours;
|
|
}
|
|
|
|
traceAll() {
|
|
for (let i = 0; i < this.m_profileEdges; i++) {
|
|
this.calculateShiftedProfile(i);
|
|
}
|
|
}
|
|
calculateShiftedProfile(iEdge) {
|
|
let traceT = [],
|
|
traceU = [];
|
|
for (let i = 0; i < this.m_knotsT.length; i++) {
|
|
const trT = this.getTrace(this.m_knotsT[i], iEdge);
|
|
const trU = trT.clone();
|
|
this.m_gizmo.unapplyToSink(trT, trU);
|
|
traceT.push(trT), traceU.push(trU);
|
|
}
|
|
this.interpolateUnimportantTraceKnots(traceU, traceT);
|
|
|
|
let arcc = new SimplyCollectArcs();
|
|
SpiroJs.spiroToArcsOnContext(traceT, this.m_closed, arcc);
|
|
this.m_traces.push(arcc.arcs);
|
|
}
|
|
getTrace(k, iEdge) {
|
|
if (k.profile) {
|
|
return new MonoKnot(k.type, false, k.x + k.profile[iEdge].x, k.y + k.profile[iEdge].y);
|
|
} else {
|
|
return new MonoKnot(k.type, true, k.x, k.y);
|
|
}
|
|
}
|
|
interpolateUnimportantTraceKnots(traceU, traceT) {
|
|
let firstImportantIdx = -1;
|
|
let lastImportantIdx = -1;
|
|
|
|
for (let i = 0; i < traceU.length; i++) {
|
|
if (traceU[i].unimportant) continue;
|
|
|
|
if (lastImportantIdx !== -1) {
|
|
this.interpolateUnimportantTraceKnotsRg(traceU, traceT, lastImportantIdx, i);
|
|
}
|
|
|
|
if (firstImportantIdx === -1) firstImportantIdx = i;
|
|
lastImportantIdx = i;
|
|
}
|
|
|
|
if (firstImportantIdx !== -1 && lastImportantIdx !== -1) {
|
|
this.interpolateUnimportantTraceKnotsRg(
|
|
traceU,
|
|
traceT,
|
|
lastImportantIdx,
|
|
firstImportantIdx,
|
|
);
|
|
}
|
|
}
|
|
interpolateUnimportantTraceKnotsRg(traceU, traceT, last, next) {
|
|
let count = next > last ? next - last : traceU.length - last + next;
|
|
for (let offset = 1; offset < count; offset++) {
|
|
let i = (last + offset) % traceU.length;
|
|
this.interpolateKnot(
|
|
this.m_knotsU[last],
|
|
traceU[last],
|
|
this.m_knotsU[i],
|
|
traceU[i],
|
|
traceT[i],
|
|
this.m_knotsU[next],
|
|
traceU[next],
|
|
);
|
|
}
|
|
}
|
|
interpolateKnot(rBefore, uBefore, rCurr, uCurr, tCurr, rAfter, uAfter) {
|
|
uCurr.x = linreg(rBefore.x, uBefore.x, rAfter.x, uAfter.x, rCurr.x);
|
|
uCurr.y = linreg(rBefore.y, uBefore.y, rAfter.y, uAfter.y, rCurr.y);
|
|
this.m_gizmo.applyToSink(uCurr, tCurr);
|
|
}
|
|
}
|
|
|
|
class SimplyCollectArcs {
|
|
constructor() {
|
|
this.arcs = [];
|
|
}
|
|
beginShape() {}
|
|
endShape() {}
|
|
moveTo() {}
|
|
arcTo(arc) {
|
|
this.arcs.push(arc);
|
|
}
|
|
}
|
|
|
|
// arcForward and arcBackward must be spiro arcs.
|
|
function makeProfiledStroke(contours, arcForward, arcBackward) {
|
|
const [subdividesForward, subdividesBackward] = subdivideKnotPair(
|
|
arcForward,
|
|
arcBackward,
|
|
CurveUtil.GEOMETRY_PRECISION,
|
|
);
|
|
for (let i = 0; i < subdividesForward.length; i++) {
|
|
const [a1, b1, c1, d1] = subdividesForward[i].toCubicBezier();
|
|
const [a2, b2, c2, d2] = subdividesBackward[i].toCubicBezier();
|
|
contours.push([
|
|
Point.from(Point.Type.Corner, a1),
|
|
Point.from(Point.Type.CubicStart, b1),
|
|
Point.from(Point.Type.CubicEnd, c1),
|
|
Point.from(Point.Type.Corner, d1),
|
|
Point.from(Point.Type.Corner, d2),
|
|
Point.from(Point.Type.CubicStart, c2),
|
|
Point.from(Point.Type.CubicEnd, b2),
|
|
Point.from(Point.Type.Corner, a2),
|
|
]);
|
|
}
|
|
}
|
|
|
|
function subdivideKnotPair(arcForward, arcBackward, delta) {
|
|
const MAX_STOPS = 16;
|
|
|
|
let sinkForward = [],
|
|
sinkBackward = [];
|
|
for (let stops = 1; stops < MAX_STOPS; stops++) {
|
|
sinkForward.length = 0;
|
|
sinkBackward.length = 0;
|
|
|
|
uniformSubdivide(arcForward, stops, sinkForward);
|
|
uniformSubdivide(arcBackward, stops, sinkBackward);
|
|
|
|
let flatEnough = true;
|
|
for (const fwd of sinkForward) if (fwd.bend > delta) flatEnough = false;
|
|
for (const bwd of sinkBackward) if (bwd.bend > delta) flatEnough = false;
|
|
|
|
if (flatEnough) break;
|
|
}
|
|
return [sinkForward, sinkBackward];
|
|
}
|
|
function uniformSubdivide(arc, stops, sink) {
|
|
for (; stops > 1; stops--) {
|
|
const f = arc.subdivide(1 / stops);
|
|
sink.push(f[0]), (arc = f[1]);
|
|
}
|
|
sink.push(arc);
|
|
}
|