Add a new spiro control flattener, and a new mechanism to propagate coordinates through the spiro construction (#2442)
This commit is contained in:
parent
6dff364caf
commit
6f7c864faa
4 changed files with 474 additions and 133 deletions
|
@ -137,7 +137,7 @@ glyph-block LetterLike-Fraktur : begin
|
||||||
include : fraktur-stroke S
|
include : fraktur-stroke S
|
||||||
g4.ld.start [S.xp SB RightSB 0.1] ([S.yt CAP] - 0.1 * ArchDepthA)
|
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)
|
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)]
|
~~~ [hookend [S.yb 0] (sw -- S.thick)]
|
||||||
g2 [S.xr RightSB] ([S.yb 0] + SHook)
|
g2 [S.xr RightSB] ([S.yb 0] + SHook)
|
||||||
|
|
||||||
|
@ -151,7 +151,7 @@ glyph-block LetterLike-Fraktur : begin
|
||||||
include : fraktur-stroke F
|
include : fraktur-stroke F
|
||||||
g2.ld.start [F.connL S xCenter] [F.connB S : [S.yt CAP] - DecoSizeX]
|
g2.ld.start [F.connL S xCenter] [F.connB S : [S.yt CAP] - DecoSizeX]
|
||||||
~~~ [Wave.vc (-Wave.DepthX)]
|
~~~ [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
|
create-glyph "frak/H" 0x210C : glyph-proc
|
||||||
include : MarkSet.capDesc
|
include : MarkSet.capDesc
|
||||||
|
|
|
@ -49,7 +49,7 @@ glyph-block Digits-Zero : begin
|
||||||
local pcy 0.1
|
local pcy 0.1
|
||||||
|
|
||||||
return : sink
|
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
|
alsoThru.g2 pcx pcy
|
||||||
flat [mix mxb r p1] [mix b myr p2] [widths.lhs sw1]
|
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]
|
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]
|
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]
|
curl [mix l mxb (1 - p1)] [mix myl b (1 - p2)] [widths.lhs sw1]
|
||||||
alsoThru.g2 (1 - pcx) (1 - pcy)
|
alsoThru.g2 (1 - pcx) (1 - pcy)
|
||||||
g2.right.mid.pre mxb (b + O) [widths.lhs fine]
|
|
||||||
close
|
close
|
||||||
|
|
||||||
define [ZeroShapeBase shapeT] : namespace
|
define [ZeroShapeBase shapeT] : namespace
|
||||||
|
|
|
@ -1,8 +1,15 @@
|
||||||
import { DiSpiroGeometry, SpiroGeometry } from "@iosevka/geometry";
|
import { DiSpiroGeometry, SpiroGeometry } from "@iosevka/geometry";
|
||||||
import {
|
import {
|
||||||
BiKnotCollector,
|
BiKnotCollector,
|
||||||
|
DEP_POST_X,
|
||||||
|
DEP_POST_Y,
|
||||||
|
DEP_PRE_X,
|
||||||
|
DEP_PRE_Y,
|
||||||
|
DerivedCoordinateBase,
|
||||||
Interpolator,
|
Interpolator,
|
||||||
|
SpiroFlattener,
|
||||||
TerminateInstruction,
|
TerminateInstruction,
|
||||||
|
UserCloseKnotPair,
|
||||||
UserControlKnot,
|
UserControlKnot,
|
||||||
} from "@iosevka/geometry/spiro-control";
|
} from "@iosevka/geometry/spiro-control";
|
||||||
import { bez3, fallback, mix } from "@iosevka/util";
|
import { bez3, fallback, mix } from "@iosevka/util";
|
||||||
|
@ -18,9 +25,12 @@ class SpiroImplBase {
|
||||||
createCollector(glyph) {
|
createCollector(glyph) {
|
||||||
const gizmo = glyph.gizmo || this.bindings.GlobalTransform;
|
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);
|
const collector = new BiKnotCollector(this.bindings.Contrast);
|
||||||
for (const control of this.args) collector.add(control);
|
flattener.pipe(collector);
|
||||||
collector.unwrap();
|
|
||||||
|
|
||||||
return { gizmo, 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
|
/// The builder for directed knot pairs
|
||||||
class DirectedKnotPairBuilder {
|
class DirectedKnotPairBuilder {
|
||||||
constructor(bindings, prevKnotType, nextKnotType, deltaX, deltaY) {
|
constructor(bindings, kPre, kCenter, kPost, deltaX, deltaY) {
|
||||||
const { TINY } = bindings;
|
const { TINY } = bindings;
|
||||||
this.start = DirPairImpl(prevKnotType, nextKnotType, deltaX, deltaY, 0, TINY);
|
this.start = DirPairImpl(kPre, kCenter, kPost, deltaX, deltaY, 0, TINY);
|
||||||
this.mid = DirPairImpl(prevKnotType, nextKnotType, deltaX, deltaY, -0.5 * TINY, 0.5 * TINY);
|
this.mid = DirPairImpl(kPre, kCenter, kPost, deltaX, deltaY, -0.5 * TINY, 0.5 * TINY);
|
||||||
this.end = DirPairImpl(prevKnotType, nextKnotType, deltaX, deltaY, -TINY, 0);
|
this.end = DirPairImpl(kPre, kCenter, kPost, deltaX, deltaY, -TINY, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function DirPairImpl(prevKnotType, nextKnotType, dirX, dirY, distPre, distPost) {
|
function DirPairImpl(kPre, kCenter, kPost, dirX, dirY, dPre, dPost) {
|
||||||
const fnPre = (x, y, af) => prevKnotType(x + dirX * distPre, y + dirY * distPre, af);
|
let tyPre = kPre(0, 0).type;
|
||||||
const fnPost = (x, y, af) => nextKnotType(x + dirX * distPost, y + dirY * distPost, af);
|
let tyPost = kPost(0, 0).type;
|
||||||
let buildFn = (x, y, af) => [fnPre(x, y, af), fnPost(x, y, af)];
|
return (x, y, af) =>
|
||||||
buildFn.pre = fnPre;
|
new UserCloseKnotPair(kCenter(x, y, af), tyPre, tyPost, dirX, dirY, dPre, dPost);
|
||||||
buildFn.post = fnPost;
|
|
||||||
return buildFn;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function KnotType(type) {
|
/// Derivative coordinates
|
||||||
return (x, y, f) => {
|
class CSameX extends DerivedCoordinateBase {
|
||||||
if (!isFinite(x)) throw new TypeError("NaN detected for X");
|
getDependency() {
|
||||||
if (!isFinite(y)) throw new TypeError("NaN detected for Y");
|
return DEP_PRE_X;
|
||||||
return new UserControlKnot(type, x, y, f);
|
}
|
||||||
};
|
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) {
|
export function SetupBuilders(bindings) {
|
||||||
|
@ -121,15 +164,16 @@ export function SetupBuilders(bindings) {
|
||||||
|
|
||||||
// Add the directed/heading knot builders
|
// Add the directed/heading knot builders
|
||||||
{
|
{
|
||||||
|
// prettier-ignore
|
||||||
let knotTypes = [
|
let knotTypes = [
|
||||||
[g4, g4, g4],
|
[ g4, g4, g4, g4 ],
|
||||||
[g2, g2, g2],
|
[ g2, g2, g2, g2 ],
|
||||||
[corner, corner, corner],
|
[ corner, corner, corner, corner ],
|
||||||
[straight, flat, curl],
|
[ straight, flat, g2, curl ],
|
||||||
[g2c, g2, corner],
|
[ g2c, g2, corner, corner ],
|
||||||
[cg2, corner, g2],
|
[ cg2, corner, corner, g2 ],
|
||||||
[flatc, flat, corner],
|
[ flatc, flat, corner, corner ],
|
||||||
[ccurl, corner, curl],
|
[ ccurl, corner, corner, curl ],
|
||||||
];
|
];
|
||||||
let directions = [
|
let directions = [
|
||||||
// Straights
|
// Straights
|
||||||
|
@ -148,12 +192,12 @@ export function SetupBuilders(bindings) {
|
||||||
{ name: "lu", x: -1, y: 1 },
|
{ name: "lu", x: -1, y: 1 },
|
||||||
{ name: "ld", x: -1, y: -1 },
|
{ name: "ld", x: -1, y: -1 },
|
||||||
];
|
];
|
||||||
for (const [sink, kl, kr] of knotTypes) {
|
for (const [sink, kl, kc, kr] of knotTypes) {
|
||||||
sink.sl = s => new DirectedKnotPairBuilder(bindings, kl, kr, -1, s);
|
sink.sl = s => new DirectedKnotPairBuilder(bindings, kl, kc, kr, -1, s);
|
||||||
sink.sr = s => new DirectedKnotPairBuilder(bindings, kl, kr, 1, s);
|
sink.sr = s => new DirectedKnotPairBuilder(bindings, kl, kc, kr, 1, s);
|
||||||
sink.dir = (dx, dy) => new DirectedKnotPairBuilder(bindings, kl, kr, dx, dy);
|
sink.dir = (dx, dy) => new DirectedKnotPairBuilder(bindings, kl, kc, kr, dx, dy);
|
||||||
for (const d of directions) {
|
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,
|
dispiro,
|
||||||
"spiro-outline": spiroOutline,
|
"spiro-outline": spiroOutline,
|
||||||
"spiro-collect": spiroCollect,
|
"spiro-collect": spiroCollect,
|
||||||
|
|
||||||
|
"same-x": new CSameX(),
|
||||||
|
"same-y": new CSameY(),
|
||||||
|
"same-x-post": new CSameXPost(),
|
||||||
|
"same-y-post": new CSameYPost(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
export class BiKnotCollector {
|
||||||
constructor(contrast) {
|
constructor(contrast) {
|
||||||
this.contrast = contrast; // stroke contrast
|
this.contrast = contrast; // stroke contrast
|
||||||
|
@ -7,57 +397,6 @@ export class BiKnotCollector {
|
||||||
|
|
||||||
this.controls = []; // all the control items
|
this.controls = []; // all the control items
|
||||||
this.closed = false; // whether the shape is closed
|
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) {
|
pushKnot(c) {
|
||||||
|
@ -67,7 +406,6 @@ export class BiKnotCollector {
|
||||||
} else {
|
} else {
|
||||||
k = new BiKnot(c.type, c.x, c.y, this.defaultD1, this.defaultD2);
|
k = new BiKnot(c.type, c.x, c.y, this.defaultD1, this.defaultD2);
|
||||||
}
|
}
|
||||||
k.originalKnot = c;
|
|
||||||
|
|
||||||
this.controls.push(k);
|
this.controls.push(k);
|
||||||
this.lastKnot = k;
|
this.lastKnot = k;
|
||||||
|
@ -143,7 +481,6 @@ class BiKnot {
|
||||||
|
|
||||||
// Derived properties
|
// Derived properties
|
||||||
this.origTangent = null;
|
this.origTangent = null;
|
||||||
this.originalKnot = null;
|
|
||||||
}
|
}
|
||||||
clone() {
|
clone() {
|
||||||
const k1 = new BiKnot(this.type, this.x, this.y, this.d1, this.d2);
|
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);
|
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;
|
|
||||||
}
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue