Iosevka/packages/font-kits/src/spiro-kit.mjs

488 lines
13 KiB
JavaScript

import { DiSpiroGeometry, SpiroGeometry } from "@iosevka/geometry";
import {
BiKnotCollector,
DEP_POST_X,
DEP_POST_Y,
DEP_PRE_X,
DEP_PRE_Y,
DerivedCoordinateBase,
Interpolator,
SpiroFlattener,
TerminateInstruction,
UserCloseKnotPair,
UserControlKnot,
} from "@iosevka/geometry/spiro-control";
import { bez3, fallback, mix } from "@iosevka/util";
///////////////////////////////////////////////////////////////////////////////////////////////////
class SpiroImplBase {
constructor(bindings, args) {
this.bindings = bindings;
this.args = args;
}
createCollector(glyph) {
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);
flattener.pipe(collector);
return { gizmo, collector };
}
}
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);
return dsp;
}
}
class SpiroOutlineImpl extends SpiroImplBase {
constructor(bindings, args) {
super(bindings, args);
}
applyToGlyph(glyph) {
const { gizmo, collector } = this.createCollector(glyph);
return glyph.includeGeometry(
new SpiroGeometry(
gizmo,
collector.closed,
collector.controls.map(k => k.toMono()),
),
);
}
}
class DiSpiroProxy {
constructor(gizmo, collector) {
this.geometry = new DiSpiroGeometry(
gizmo,
collector.contrast,
collector.closed,
collector.controls,
);
this.m_origKnots = collector.controls;
}
get knots() {
return this.m_origKnots;
}
get lhsKnots() {
return this.geometry.expand().lhsUntransformed;
}
get rhsKnots() {
return this.geometry.expand().rhsUntransformed;
}
}
/// 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
class DirectedKnotPairBuilder {
constructor(bindings, kPre, kCenter, kPost, deltaX, deltaY) {
const { TINY } = bindings;
this.start = DirPairImpl(kPre, kCenter, kPost, deltaX, deltaY, 0, TINY);
this.mid = DirPairImpl(kPre, kCenter, kPost, deltaX, deltaY, -0.5 * TINY, 0.5 * TINY);
this.end = DirPairImpl(kPre, kCenter, kPost, deltaX, deltaY, -TINY, 0);
}
}
function DirPairImpl(kPre, kCenter, kPost, dirX, dirY, dPre, dPost) {
let tyPre = kPre(0, 0).type;
let tyPost = kPost(0, 0).type;
return (x, y, af) =>
new UserCloseKnotPair(kCenter(x, y, af), tyPre, tyPost, dirX, dirY, dPre, dPost);
}
/// Derivative coordinates
class CSameX extends DerivedCoordinateBase {
getDependency() {
return DEP_PRE_X;
}
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) {
const { Stroke, Superness } = bindings;
// Simple knot types
const g4 = KnotType("g4");
const g2 = KnotType("g2");
const corner = KnotType("corner");
const flat = KnotType("left");
const curl = KnotType("right");
const close = f => new TerminateInstruction("close", f);
const end = f => new TerminateInstruction("end", f);
// Pair knots
const straight = { l: flat, r: curl };
const g2c = { l: g2, r: corner };
const cg2 = { l: corner, r: g2 };
const flatc = { l: flat, r: corner };
const ccurl = { l: corner, r: curl };
// Add the directed/heading knot builders
{
// prettier-ignore
let knotTypes = [
[ g4, g4, g4, g4 ],
[ g2, g2, g2, g2 ],
[ corner, corner, corner, corner ],
[ straight, flat, g2, curl ],
[ g2c, g2, corner, corner ],
[ cg2, corner, corner, g2 ],
[ flatc, flat, corner, corner ],
[ ccurl, corner, corner, curl ],
];
let directions = [
// Straights
{ name: "up", x: 0, y: 1 },
{ name: "down", x: 0, y: -1 },
{ name: "left", x: -1, y: 0 },
{ name: "right", x: 1, y: 0 },
{ name: "u", x: 0, y: 1 },
{ name: "d", x: 0, y: -1 },
{ name: "l", x: -1, y: 0 },
{ name: "r", x: 1, y: 0 },
// Diagonals
{ name: "ru", x: 1, y: 1 },
{ name: "rd", x: 1, y: -1 },
{ name: "lu", x: -1, y: 1 },
{ name: "ld", x: -1, y: -1 },
];
for (const [sink, kl, kc, kr] of knotTypes) {
sink.sl = s => new DirectedKnotPairBuilder(bindings, kl, kc, kr, -1, s);
sink.sr = s => new DirectedKnotPairBuilder(bindings, kl, kc, kr, 1, s);
sink.dir = (dx, dy) => new DirectedKnotPairBuilder(bindings, kl, kc, kr, dx, dy);
for (const d of directions) {
sink[d.name] = new DirectedKnotPairBuilder(bindings, kl, kc, kr, d.x, d.y);
}
}
}
function widths(l, r) {
if (!isFinite(l)) throw new TypeError("NaN detected for left width");
if (!isFinite(r)) throw new TypeError("NaN detected for right width");
return function () {
if (this.setWidth) this.setWidth(l, r);
};
}
widths.lhs = function (w) {
w = fallback(w, Stroke);
if (!isFinite(w)) throw new TypeError("NaN detected for left width");
return widths(w, 0);
};
widths.rhs = function (w) {
w = fallback(w, Stroke);
if (!isFinite(w)) throw new TypeError("NaN detected for left width");
return widths(0, w);
};
widths.center = function (w) {
w = fallback(w, Stroke);
if (!isFinite(w)) throw new TypeError("NaN detected for left width");
return widths(w / 2, w / 2);
};
function heading(d) {
if (!isFinite(d.x) || !isFinite(d.y))
throw new TypeError("NaN detected for heading directions");
return function () {
if (this.headsTo) this.headsTo(d);
};
}
widths.heading = function (l, r, d) {
if (!isFinite(l)) throw new TypeError("NaN detected for left width");
if (!isFinite(r)) throw new TypeError("NaN detected for left width");
if (!isFinite(d.x) || !isFinite(d.y))
throw new TypeError("NaN detected for heading directions");
return function () {
if (this.setWidth) this.setWidth(l, r);
if (this.headsTo) this.headsTo(d);
};
};
widths.lhs.heading = function (w, d) {
w = fallback(w, Stroke);
if (!isFinite(w)) throw new TypeError("NaN detected for left width");
if (!isFinite(d.x) || !isFinite(d.y))
throw new TypeError("NaN detected for heading directions");
return function () {
if (this.setWidth) this.setWidth(w, 0);
if (this.headsTo) this.headsTo(d);
};
};
widths.rhs.heading = function (w, d) {
w = fallback(w, Stroke);
if (!isFinite(w)) throw new TypeError("NaN detected for left width");
if (!isFinite(d.x) || !isFinite(d.y))
throw new TypeError("NaN detected for heading directions");
return function () {
if (this.setWidth) this.setWidth(0, w);
if (this.headsTo) this.headsTo(d);
};
};
widths.center.heading = function (w, d) {
w = fallback(w, Stroke);
if (!isFinite(w)) throw new TypeError("NaN detected for left width");
if (!isFinite(d.x) || !isFinite(d.y))
throw new TypeError("NaN detected for heading directions");
return function () {
if (this.setWidth) this.setWidth(w / 2, w / 2);
if (this.headsTo) this.headsTo(d);
};
};
function disableContrast() {
return function () {
if (this.setContrast) this.setContrast(1);
};
}
function unimportant() {
if (this.setUnimportant) this.setUnimportant(1);
}
function important() {
return void 0;
}
function afInterpolate(before, after, args) {
return g4(
mix(before.x, after.x, args.rx),
mix(before.y, after.y, args.ry),
fallback(args.raf, unimportant),
);
}
function afInterpolateDelta(before, after, args) {
return g4(
mix(before.x, after.x, args.rx) + args.deltaX,
mix(before.y, after.y, args.ry) + args.deltaY,
fallback(args.raf, unimportant),
);
}
function afInterpolateG2(before, after, args) {
return g2(
mix(before.x, after.x, args.rx),
mix(before.y, after.y, args.ry),
fallback(args.raf, unimportant),
);
}
function afInterpolateThem(before, after, args) {
let innerKnots = [];
for (const [rx, ry, rt] of args.rs) {
innerKnots.push(
fallback(args.ty, g2)(
mix(before.x, after.x, rx),
mix(before.y, after.y, ry),
args.raf && args.raf.blend && rt !== void 0
? args.raf.blend(rt)
: args.raf
? args.raf
: unimportant,
),
);
}
return innerKnots;
}
function afInterpolateThemWithDelta(before, after, args) {
let innerKnots = [];
for (const [rx, ry, deltaX, deltaY, rt] of args.rs) {
innerKnots.push(
fallback(args.ty, g2)(
mix(before.x, after.x, rx) + deltaX,
mix(before.y, after.y, ry) + deltaY,
args.raf && args.raf.blend && rt !== void 0
? args.raf.blend(rt)
: args.raf
? args.raf
: unimportant,
),
);
}
return innerKnots;
}
function afInterpolateThemFromTWithDelta(before, after, args) {
let innerKnots = [];
for (const rt of args.rs) {
innerKnots.push(
fallback(args.ty, g2)(
mix(before.x, after.x, args.raf.rx(rt)) + args.raf.deltaX(rt),
mix(before.y, after.y, args.raf.ry(rt)) + args.raf.deltaY(rt),
args.raf.modifier(rt),
),
);
}
return innerKnots;
}
function alsoThru(rx, ry, raf) {
return Interpolator(afInterpolate, { rx, ry, raf });
}
alsoThru.withOffset = function (rx, ry, deltaX, deltaY, raf) {
return Interpolator(afInterpolateDelta, { rx, ry, deltaX, deltaY, raf });
};
alsoThru.g2 = function (rx, ry, raf) {
return Interpolator(afInterpolateG2, { rx, ry, raf });
};
function alsoThruThem(rs, raf, ty) {
return Interpolator(afInterpolateThem, { rs, raf, ty });
}
alsoThruThem.withOffset = function (rs, raf, ty) {
return Interpolator(afInterpolateThemWithDelta, { rs, raf, ty });
};
alsoThruThem.fromTWithOffset = function (rs, raf, ty) {
return Interpolator(afInterpolateThemFromTWithDelta, { rs, raf, ty });
};
function bezControlsImpl(x1, y1, x2, y2, samples, raf, ty) {
let rs = [];
for (let j = 1; j < samples; j = j + 1)
rs.push([
bez3(0, x1, x2, 1, j / samples),
bez3(0, y1, y2, 1, j / samples),
j / samples,
]);
return alsoThruThem(rs, raf);
}
function bezControls(x1, y1, x2, y2, _samples, raf) {
return bezControlsImpl(x1, y1, x2, y2, fallback(_samples, 3), raf);
}
function quadControls(x1, y1, _samples, raf) {
return bezControlsImpl(
(x1 * 2) / 3,
(y1 * 2) / 3,
mix(1, x1, 2 / 3),
mix(1, y1, 2 / 3),
fallback(_samples, 3),
raf,
);
}
let DEFAULT_STEPS = 6;
let [buildHV, buildVH] = (function (cache) {
function build(samples, _superness) {
const superness = fallback(_superness, Superness);
let hv = [];
let vh = [];
for (let j = 1; j < samples; j = j + 1) {
const theta = (((j + 1) / (samples + 2)) * Math.PI) / 2;
const c = Math.pow(Math.cos(theta), 2 / superness);
const s = Math.pow(Math.sin(theta), 2 / superness);
hv.push([s, 1 - c]);
vh.push([1 - c, s]);
}
return { hv, vh: vh };
}
function buildHVImpl(samples, _superness) {
if (_superness) return build(samples, _superness).hv;
if (!cache[samples]) cache[samples] = build(samples, _superness);
return cache[samples].hv;
}
function buildVHImpl(samples, _superness) {
if (_superness) return build(samples, _superness).vh;
if (!cache[samples]) cache[samples] = build(samples, _superness);
return cache[samples].vh;
}
return [buildHVImpl, buildVHImpl];
})([]);
function archv(samples, superness) {
return alsoThruThem(buildHV(fallback(samples, DEFAULT_STEPS), superness));
}
archv.superness = function (s) {
return archv(DEFAULT_STEPS, s);
};
function arcvh(samples, superness) {
return alsoThruThem(buildVH(fallback(samples, DEFAULT_STEPS), superness));
}
arcvh.superness = function (s) {
return arcvh(DEFAULT_STEPS, s);
};
archv.yFromX = function (px, _s) {
const s = fallback(_s, Superness);
return 1 - Math.pow(1 - Math.pow(px, s), 1 / s);
};
function dispiro(...args) {
return new DispiroImpl(bindings, args);
}
function spiroOutline(...args) {
return new SpiroOutlineImpl(bindings, args);
}
function spiroCollect(glyph, ...args) {
const spb = new SpiroImplBase(bindings, args);
return spb.createCollector(glyph);
}
return {
g4,
g2,
corner,
flat,
curl,
close,
end,
straight,
g2c,
cg2,
flatc,
ccurl,
widths,
heading,
"disable-contrast": disableContrast,
unimportant,
important,
alsoThru,
alsoThruThem,
bezControls,
quadControls,
archv,
arcvh,
dispiro,
"spiro-outline": spiroOutline,
"spiro-collect": spiroCollect,
"same-x": new CSameX(),
"same-y": new CSameY(),
"same-x-post": new CSameXPost(),
"same-y-post": new CSameYPost(),
};
}