Further cleanup and optimize spiro expansion.

This commit is contained in:
be5invis 2020-06-19 19:46:32 -07:00
parent e86890e16a
commit e1e8144f1c
12 changed files with 234 additions and 224 deletions

2
.gitignore vendored
View file

@ -44,8 +44,8 @@ snapshot/iosevka*
snapshot/index.css
# Generated scripts
support/*.js
gen/build-glyphs.js
gen/kits/*.js
meta/*.js
otl/*.js
glyphs/*.js

View file

@ -1,7 +1,7 @@
import '../support/glyph' as Glyph
import '../support/point' as Point
import '../support/spiro-kit' as spirokit
import '../support/boole-kit' as BooleKit
import './kits/spiro-kit' as spirokit
import './kits/boole-kit' as BooleKit
import '../support/anchor' as Anchor
import '../support/monotonic-interpolate' as smoothreg

View file

@ -125,8 +125,8 @@ class FairizedShapeSink {
this.lastContour.pop();
}
this.contours.push(this.lastContour);
this.lastContour = [];
}
this.lastContour = [];
}
moveTo(x, y) {
this.endShape();

View file

@ -1,6 +1,5 @@
import 'typo-geom' as TypoGeom
import './curve-util' as CurveUtil
import './spiroexpand' as [object SpiroContourContext]
import '../../support/curve-util' as CurveUtil
export : define [SetupBuilders args] : begin
define [object Glyph globalTransform] args
@ -17,7 +16,7 @@ export : define [SetupBuilders args] : begin
g1.include item
set g1.contours : g1.contours.map CurveUtil.convertContourToCubic
local c1 : TypoGeom.Boolean.combine operator [CurveUtil.convertShapeToArcs g.contours] [CurveUtil.convertShapeToArcs g1.contours] TypoGeom.Boolean.PolyFillType.pftNonZero TypoGeom.Boolean.PolyFillType.pftNonZero 16384
local ctx : new SpiroContourContext
local ctx : new CurveUtil.ArcFlattener
TypoGeom.transferBezArcShape c1 ctx
set g.contours ctx.contours
this.includeGlyph g

View file

@ -1,13 +1,8 @@
import 'spiro' as SpiroJs
import './spiroexpand' as [object SpiroExpansionContext SpiroContourContext]
import './curve-util' as CurveUtil
import './transform' as Transform
define [fallback] : for [local j 0] (j < arguments.length) [inc j] : if (arguments.(j) !== nothing) : return arguments.(j)
define [mix a b p] : a + (b - a) * p
define [bez2 a b c t] : (1 - t) * (1 - t) * a + 2 * (1 - t) * t * b + t * t * c
define [bez3 a b c d t] : (1 - t) * (1 - t) * (1 - t) * a + 3 * (1 - t) * (1 - t) * t * b + 3 * t * t * (1 - t) * c + t * t * t * d
import '../../support/spiro-expand' as SpiroExpansionContext
import '../../support/curve-util' as CurveUtil
import '../../support/transform' as Transform
import '../../support/utils' as [object fallback mix bez2 bez3]
export : define [SetupBuilders args] : begin
define [object para Glyph Contrast globalTransform Stroke Superness] args
@ -166,12 +161,12 @@ export : define [SetupBuilders args] : begin
set knot.af : lambda [] : begin
this.setType ty
if af : af.apply this args
SpiroJs.spiroToBezierOnContext knots closed s
SpiroJs.spiroToArcsOnContext knots closed s
foreach af [items-of lastafs] : if af : af.call s
local {.lhs lhs .rhs rhs} : s.expand [fallback s.contrast Contrast]
if closed : then
local g : new SpiroContourContext
local g : new CurveUtil.ArcFlattener
SpiroJs.spiroToBezierOnContext [lhs.slice 0 (-1)] true g QUAD PRECISION
local lhsContour g.contours.0
set g.contours {}
@ -179,7 +174,7 @@ export : define [SetupBuilders args] : begin
local rhsContour g.contours.0
set g.contours {[lhsContour.concat rhsContour]}
: else : begin
local g : new SpiroContourContext
local g : new CurveUtil.ArcFlattener
lhs.0.type = rhs.0.type = lhs.(lhs.length - 1).type = rhs.(rhs.length - 1).type = 'corner'
SpiroJs.spiroToBezierOnContext [lhs.concat : rhs.reverse] true g QUAD PRECISION
@ -190,7 +185,7 @@ export : define [SetupBuilders args] : begin
return g
define [spiro-outline] : let [k : {}.slice.call arguments 0] : lambda [dontinc] : begin
local g : new SpiroContourContext (this.gizmo || globalTransform)
local g : new CurveUtil.ArcFlattener (this.gizmo || globalTransform)
local {.knots knots .closed closed .lastafs lastafs} : prepareSpiroKnots k g
SpiroJs.spiroToBezierOnContext knots closed g QUAD PRECISION
foreach af [items-of lastafs] : if af : af.call g

View file

@ -14,7 +14,7 @@
"otfcc-ttcize": "^0.10.1",
"patel": "^0.33.1",
"semver": "^7.1.3",
"spiro": "^1.1.0",
"spiro": "^2.0.0",
"stylus": "^0.54.7",
"toml": "^3.0.0",
"topsort": "^0.0.2",

View file

@ -3,6 +3,7 @@
const TypoGeom = require("typo-geom");
const Point = require("./point");
const { mix } = require("./utils");
const Transform = require("./transform");
exports.OffsetCurve = class OffsetCurve {
constructor(bone, offset, contrast) {
@ -195,3 +196,34 @@ exports.convertContourToCubicRev = convertContourToCubicRev;
exports.autoCubify = autoCubify;
exports.fixedCubify = fixedCubify;
exports.convertShapeToArcs = convertShapeToArcs;
exports.ArcFlattener = class ArcFlattener {
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, x, y, true));
}
lineTo(x, y) {
this.lastContour.push(Point.transformedXY(this.gizmo, x, y, true));
}
curveTo(xc, yc, x, y) {
this.lastContour.push(Point.transformedXY(this.gizmo, xc, yc, false, false));
this.lastContour.push(Point.transformedXY(this.gizmo, x, y, true));
}
cubicTo(x1, y1, x2, y2, x, y) {
this.lastContour.push(Point.transformedXY(this.gizmo, x1, y1, false, true));
this.lastContour.push(Point.transformedXY(this.gizmo, x2, y2, false, true));
this.lastContour.push(Point.transformedXY(this.gizmo, x, y, true));
}
};

132
support/spiro-expand.js Normal file
View file

@ -0,0 +1,132 @@
const Transform = require("./transform");
const { linreg } = require("./utils");
module.exports = class SpiroExpansionContext {
constructor() {
this.gizmo = Transform.Id();
this.controlKnots = [];
this.defaultD1 = 0;
this.defaultD2 = 0;
}
beginShape() {}
endShape() {}
moveTo(x, y, unimportant) {
if (unimportant) return;
this.controlKnots.push({
type: "g4",
d1: this.defaultD1,
d2: this.defaultD2,
...this.gizmo.apply({ x, y })
});
}
arcTo(arc, x, y) {
const k0 = this.controlKnots[this.controlKnots.length - 1];
if (!k0) throw new Error("Unreachable: lineTo called before moveTo");
if (k0.normalAngle == null) {
const tfDerive0 = this.gizmo.applyOffset({ x: arc.deriveX0, y: arc.deriveY0 });
k0.normalAngle = Math.PI / 2 + Math.atan2(tfDerive0.y, tfDerive0.x);
}
{
const tfDerive1 = this.gizmo.applyOffset({ x: arc.deriveX1, y: arc.deriveY1 });
this.controlKnots.push({
type: "g4",
d1: k0.d1,
d2: k0.d2,
...this.gizmo.apply({ x, y }),
normalAngle: Math.PI / 2 + Math.atan2(tfDerive1.y, tfDerive1.x)
});
}
}
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;
}
expand(contrast) {
if (contrast == null) contrast = 1 / 0.9;
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 = 0,
dy = 0;
if (knot.proposedNormal) {
dx = knot.proposedNormal.x - normalX(knot.normalAngle, contrast);
dy = knot.proposedNormal.y - normalY(knot.normalAngle, contrast);
}
lhs[j] = {
type: knot.type,
x: knot.x + knot.d1 * (dx + normalX(knot.normalAngle, contrast)),
y: knot.y + knot.d1 * (dy + normalY(knot.normalAngle, contrast))
};
rhs[j] = {
type: reverseKnotType(knot.type),
x: knot.x - knot.d2 * (dx + normalX(knot.normalAngle, contrast)),
y: knot.y - knot.d2 * (dy + normalY(knot.normalAngle, contrast))
};
}
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] = {
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] = {
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)
})
};
}
}
};
function zeroes(n) {
let a = new Array(n);
for (let i = 0; i < n; ++i) a[i] = 0;
return a;
}
function normalX(angle, contrast) {
return Math.cos(angle) * contrast;
}
function normalY(angle) {
return Math.sin(angle);
}
function reverseKnotType(ty) {
return ty === "left" ? "right" : ty === "right" ? "left" : ty;
}

View file

@ -1,191 +0,0 @@
import './monotonic-interpolate' as smooth
import './transform' as Transform
import './point' as Point
define [fallback] : for [local j 0] (j < arguments.length) [inc j] : if (arguments.(j) !== nothing) : return arguments.(j)
define [linreg x0 y0 x1 y1 x] : y0 + (x - x0) * (y1 - y0) / (x1 - x0)
define-macro xytransform : syntax-rules
`[xytransform @tfm @x @y] : begin
set [env.declarations.get [formOf x]].isParameter 0
set [env.declarations.get [formOf y]].isParameter 0
let [t : env.newt] `[begin \\
set @t @x
set @x : @x * @tfm.xx + @y * @tfm.yx + @tfm.x
set @y : @t * @tfm.xy + @y * @tfm.yy + @tfm.y
]
define [normalY angle] : Math.sin angle
define [normalX angle vex] : [Math.cos angle] * vex
class SpiroExpansionContext
public [new] : begin
set this.gizmo [Transform.Id]
set this.controlKnots {}
set this.defaultd1 0
set this.defaultd2 0
public [beginShape] : begin
public [endShape] : begin
public [moveTo x y unimportant] : begin
if unimportant : return nothing
# Transform incoming knots using gizmo
xytransform this.gizmo x y
this.controlKnots.push {.x x .y y .type 'g4' .d1 this.defaultd1 .d2 this.defaultd2}
public [lineTo x y unimportant] : begin
local lastKnot this.controlKnots.(this.controlKnots.length - 1)
xytransform this.gizmo x y
local thisKnot {.x x .y y .type 'g4' .d1 lastKnot.d1 .d2 lastKnot.d2}
if lastKnot : begin
local normalAngle : Math.PI / 2 + [Math.atan2 (y - lastKnot.y) (x - lastKnot.x)]
set thisKnot.normalAngle normalAngle
if (lastKnot.normalAngle === nothing) : set lastKnot.normalAngle normalAngle
if [not unimportant] : this.controlKnots.push thisKnot
public [cubicTo x1 y1 x2 y2 x y unimportant] : begin
local lastKnot this.controlKnots.(this.controlKnots.length - 1)
xytransform this.gizmo x1 y1
xytransform this.gizmo x2 y2
xytransform this.gizmo x y
local thisKnot {.x x .y y .type 'g4' .d1 lastKnot.d1 .d2 lastKnot.d2}
if (lastKnot && lastKnot.normalAngle === nothing) : begin
local normalAngle : Math.PI / 2 + [Math.atan2 (y1 - lastKnot.y) (x1 - lastKnot.x)]
if (lastKnot.normalAngle === nothing) : set lastKnot.normalAngle normalAngle
if [not unimportant] : begin
local normalAngle : Math.PI / 2 + [Math.atan2 (y - y2) (x - x2)]
set thisKnot.normalAngle normalAngle
this.controlKnots.push thisKnot
public [setWidth l r] : begin
local lastKnot this.controlKnots.(this.controlKnots.length - 1)
if lastKnot : then
lastKnot.d1 = l; lastKnot.d2 = r
: else
this.defaultd1 = l; this.defaultd2 = r
public [headsTo direction] : begin
local lastKnot this.controlKnots.(this.controlKnots.length - 1)
if lastKnot : begin
lastKnot.proposedNormal = direction
public [setType type] : begin
local lastKnot this.controlKnots.(this.controlKnots.length - 1)
if lastKnot : begin
lastKnot.type = type
public [expand contrast] : begin
local lhs {}
local rhs {}
local contrast : fallback contrast (1 / 0.9)
local d1s {}
local d2s {}
local dxs {}
local dys {}
local js {}
foreach j [range 0 this.controlKnots.length] : if [not this.controlKnots.(j).unimportant] : begin
local knot this.controlKnots.(j)
js.push j
d1s.push knot.d1
d2s.push knot.d2
if knot.proposedNormal : then
dxs.push : knot.proposedNormal.x - [normalX knot.normalAngle contrast]
dys.push : knot.proposedNormal.y - [normalY knot.normalAngle contrast]
: else
dxs.push 0; dys.push 0
local fd1 : smooth js d1s
local fd2 : smooth js d2s
local fdx : smooth js dxs
local fdy : smooth js dys
# interpolate important knots
foreach j [range 0 this.controlKnots.length] : begin
local knot this.controlKnots.(j)
if [not knot.unimportant] : begin
set lhs.(j) : object
x : knot.x + ([fdx j] + [normalX knot.normalAngle contrast]) * [fd1 j]
y : knot.y + ([fdy j] + [normalY knot.normalAngle contrast]) * [fd1 j]
type knot.type
set rhs.(j) : object
x : knot.x - ([fdx j] + [normalX knot.normalAngle contrast]) * [fd2 j]
y : knot.y - ([fdy j] + [normalY knot.normalAngle contrast]) * [fd2 j]
type : match knot.type
"left" "right"
"right" "left"
type type
# interpolate unimportant knots referencing their original position relationship
foreach j [range 0 this.controlKnots.length] : begin
local knot this.controlKnots.(j)
if knot.unimportant : begin
local jBefore (j - 1)
while this.controlKnots.(jBefore).unimportant : dec jBefore
local jAfter (j + 1)
while this.controlKnots.(jAfter).unimportant : inc jAfter
local knotBefore : this.gizmo.unapply this.controlKnots.(jBefore)
local knotAfter : this.gizmo.unapply this.controlKnots.(jAfter)
local ref : this.gizmo.unapply knot
local lhsBefore : this.gizmo.unapply lhs.(jBefore)
local lhsAfter : this.gizmo.unapply lhs.(jAfter)
local rhsBefore : this.gizmo.unapply rhs.(jBefore)
local rhsAfter : this.gizmo.unapply rhs.(jAfter)
local kLHS : this.gizmo.apply : object
x : linreg knotBefore.x lhsBefore.x knotAfter.x lhsAfter.x ref.x
y : linreg knotBefore.y lhsBefore.y knotAfter.y lhsAfter.y ref.y
local kRHS : this.gizmo.apply : object
x : linreg knotBefore.x rhsBefore.x knotAfter.x rhsAfter.x ref.x
y : linreg knotBefore.y rhsBefore.y knotAfter.y rhsAfter.y ref.y
set lhs.(j) : object
x kLHS.x
y kLHS.y
type knot.type
set rhs.(j) : object
x kRHS.x
y kRHS.y
type : match knot.type
"left" "right"
"right" "left"
type type
return {.lhs lhs .rhs rhs}
class SpiroContourContext
public [new gizmo] : begin
set this.gizmo (gizmo || [Transform.Id])
set this.contours { }
set this.defaultTag null
public [beginShape] : begin
public [endShape] : begin
public [moveTo x y] : begin
local contour {[Point.transformedXY this.gizmo x y true]}
set contour.tag this.defaultTag
this.contours.push contour
return this
public [lineTo x y] : begin
this.contours.((this.contours.length - 1)).push
Point.transformedXY this.gizmo x y true
return this
public [curveTo xc yc x y] : begin
this.contours.((this.contours.length - 1)).push
Point.transformedXY this.gizmo xc yc false
Point.transformedXY this.gizmo x y true
return this
public [cubicTo x1 y1 x2 y2 x y] : begin
this.contours.((this.contours.length - 1)).push
Point.transformedXY this.gizmo x1 y1 false true
Point.transformedXY this.gizmo x2 y2 false true
Point.transformedXY this.gizmo x y true
return this
export SpiroExpansionContext
export SpiroContourContext

View file

@ -20,6 +20,12 @@ module.exports = class Transform {
y: pt.x * this.xy + pt.y * this.yy + this.y
};
}
applyOffset(delta) {
return {
x: delta.x * this.xx + delta.y * this.yx,
y: delta.x * this.xy + delta.y * this.yy
};
}
unapply(pt) {
const xx = pt.x - this.x;
const yy = pt.y - this.y;

49
support/utils.js Normal file
View file

@ -0,0 +1,49 @@
"use strict";
function mix(a, b, p) {
return a + (b - a) * p;
}
function ratio(l, r, m) {
return l === r ? 0 : (m - l) / (r - l);
}
function barmixL(l, r, b, p) {
return l > r ? barmixL(r, l, b, p) : l + b + p * (r - l - b * 3);
}
function barmixM(l, r, b, p) {
return barmixL(l, r, b, p) + b / 2;
}
function barMixR(l, r, b, p) {
return barMixR(l, r, b, p) + b;
}
function linreg(x0, y0, x1, y1, x) {
return y0 + ((x - x0) * (y1 - y0)) / (x1 - x0);
}
function clamp(l, h, x) {
return x < l ? l : x > h ? h : x;
}
function fallback(...args) {
for (const item of args) if (item !== void 0) return item;
return void 0;
}
function bez2(a, b, c, t) {
return (1 - t) * (1 - t) * a + 2 * (1 - t) * t * b + t * t * c;
}
function bez3(a, b, c, d, t) {
return (
(1 - t) * (1 - t) * (1 - t) * a +
3 * (1 - t) * (1 - t) * t * b +
3 * t * t * (1 - t) * c +
t * t * t * d
);
}
exports.mix = mix;
exports.ratio = ratio;
exports.barmixL = barmixL;
exports.barmixM = barmixM;
exports.barmixR = barMixR;
exports.linreg = linreg;
exports.clamp = clamp;
exports.fallback = fallback;
exports.bez2 = bez2;
exports.bez3 = bez3;

View file

@ -1,12 +0,0 @@
import './anchor' as Anchor
export : define [mix a b p] : a + (b - a) * p
export : define [ratio l r m] : if [l === r] 0 ((m - l) / (r - l))
export : define [barmixL l r b p] : if (l > r) [barmixL r l b p] (l + b + p * (r - l - b * 3))
export : define [barmixM l r b p] : [barmixL l r b p] + b / 2
export : define [barmixR l r b p] : [barmixR l r b p] + b
export : define [linreg x0 y0 x1 y1 x] : y0 + (x - x0) * (y1 - y0) / (x1 - x0)
export : define [clamp l h x] : if (x < l) l : if (x > h) h x
export : define [fallback] : begin
for [local j 0] (j < arguments.length) [inc j] : if (arguments.(j) !== nothing) : return arguments.(j)
return nothing