Move the shape cleanup functionality out.

This commit is contained in:
be5invis 2020-06-17 03:35:21 -07:00
parent 5a0546270c
commit 9543e23a95
5 changed files with 135 additions and 500 deletions

View file

@ -2,67 +2,43 @@
const autoRef = require("./autoref");
const TypoGeom = require("typo-geom");
const Point = require("../../support/point");
const CurveUtil = require("../../support/curve-util");
const { fairifyQuad } = require("../../support/fairify");
const { AnyCv } = require("../../support/gr");
const gcFont = require("./gc");
const { SpiroContourContext } = require("../../support/spiroexpand");
function regulateGlyph(g, skew) {
if (!g.contours) return;
module.exports = function finalizeFont(para, rawGlyphList, excludedCodePoints, font) {
const glyphList = filterWideGlyphs(para, rawGlyphList);
gcFont(glyphList, excludedCodePoints, font, {});
extractGlyfCmap(regulateGlyphList(para, glyphList), font);
};
// Regulate
for (let k = 0; k < g.contours.length; k++) {
const contour = g.contours[k];
for (let p = 0; p < contour.length; p++) {
contour[p].x -= contour[p].y * skew;
function filterWideGlyphs(para, glyphList) {
if (para.forceMonospace && para.spacing == 0) {
for (const g of glyphList) g.advanceWidth = Math.round(g.advanceWidth || 0);
return glyphList.filter(g => !(g.advanceWidth > Math.round(para.width)));
}
return glyphList;
}
function extractGlyfCmap(glyphList, font) {
const glyf = {};
const cmap = {};
for (let g of glyphList) {
glyf[g.name] = g;
if (!g.unicode) continue;
for (let u of g.unicode) {
if (isFinite(u - 0)) cmap[u] = g.name;
}
}
g.contours = simplifyContours(g.contours);
for (let k = 0; k < g.contours.length; k++) {
const contour = g.contours[k];
for (let p = 0; p < contour.length; p++) {
contour[p].x += contour[p].y * skew;
}
}
}
function simplifyContours(source) {
const simplifiedArcs = TypoGeom.Boolean.removeOverlap(
CurveUtil.convertShapeToArcs(source),
TypoGeom.Boolean.PolyFillType.pftNonZero,
1 << 17
);
const sc = new SpiroContourContext();
TypoGeom.transferBezArcShape(simplifiedArcs, sc);
const result = [];
for (const contour of sc.contours) {
if (contour.length <= 2) continue;
result.push(CurveUtil.cleanupQuadContour(fairifyQuad(contour)));
}
return result;
}
function byGlyphPriority(a, b) {
const pri1 = a.autoRefPriority || 0;
const pri2 = b.autoRefPriority || 0;
if (pri1 > pri2) return -1;
if (pri1 < pri2) return 1;
if (a.contours && b.contours && a.contours.length < b.contours.length) return 1;
if (a.contours && b.contours && a.contours.length > b.contours.length) return -1;
return 0;
}
function byRank(a, b) {
return (b.glyphRank || 0) - (a.glyphRank || 0) || (a.glyphOrder || 0) - (b.glyphOrder || 0);
font.glyf = glyf;
font.cmap = cmap;
}
function regulateGlyphList(para, gs) {
const skew = Math.tan(((para.italicAngle || 0) / 180) * Math.PI);
const skew = Math.tan(((para.slantAngle || 0) / 180) * Math.PI);
const excludeUnicode = new Set();
excludeUnicode.add(0x80);
@ -88,31 +64,103 @@ function regulateGlyphList(para, gs) {
return gs.sort(byRank);
}
function filterWideGlyphs(para, glyphList) {
if (para.forceMonospace && para.spacing == 0) {
for (const g of glyphList) g.advanceWidth = Math.round(g.advanceWidth || 0);
return glyphList.filter(g => !(g.advanceWidth > Math.round(para.width)));
}
return glyphList;
function byGlyphPriority(a, b) {
const pri1 = a.autoRefPriority || 0;
const pri2 = b.autoRefPriority || 0;
if (pri1 > pri2) return -1;
if (pri1 < pri2) return 1;
if (a.contours && b.contours && a.contours.length < b.contours.length) return 1;
if (a.contours && b.contours && a.contours.length > b.contours.length) return -1;
return 0;
}
function extractGlyfCmap(glyphList, font) {
const glyf = {};
const cmap = {};
for (let g of glyphList) {
glyf[g.name] = g;
if (!g.unicode) continue;
function byRank(a, b) {
return (b.glyphRank || 0) - (a.glyphRank || 0) || (a.glyphOrder || 0) - (b.glyphOrder || 0);
}
for (let u of g.unicode) {
if (isFinite(u - 0)) cmap[u] = g.name;
function regulateGlyph(g, skew) {
if (!g.contours || !g.contours.length) return;
for (const contour of g.contours) for (const z of contour) z.x -= z.y * skew;
g.contours = simplifyContours(g.contours);
for (const contour of g.contours) for (const z of contour) z.x += z.y * skew;
}
function simplifyContours(source) {
const sink = new FairizedShapeSink();
TypoGeom.transferGenericShape(
TypoGeom.Fairize.fairizeBezierShape(
TypoGeom.Boolean.removeOverlap(
CurveUtil.convertShapeToArcs(source),
TypoGeom.Boolean.PolyFillType.pftNonZero,
1 << 17
)
),
sink,
FINAL_SIMPLIFY_TOLERANCE
);
return sink.contours;
}
const FINAL_SIMPLIFY_RESOLUTION = 16;
const FINAL_SIMPLIFY_TOLERANCE = 2 / FINAL_SIMPLIFY_RESOLUTION;
class FairizedShapeSink {
constructor() {
this.contours = [];
this.lastContour = [];
}
beginShape() {}
endShape() {
if (this.lastContour.length > 2) {
const zFirst = this.lastContour[0],
zLast = this.lastContour[this.lastContour.length - 1];
if (zFirst.on && zLast.on && zFirst.x === zLast.x && zFirst.y === zLast.y) {
this.lastContour.pop();
}
this.contours.push(this.lastContour);
this.lastContour = [];
}
}
font.glyf = glyf;
font.cmap = cmap;
moveTo(x, y) {
this.endShape();
this.lineTo(x, y);
}
lineTo(x, y) {
const z = Point.cornerFromXY(x, y).round(FINAL_SIMPLIFY_RESOLUTION);
if (this.lastContour.length >= 2) {
const a = this.lastContour[this.lastContour.length - 2],
b = this.lastContour[this.lastContour.length - 1];
if (isLineExtend(a, b, z)) {
this.lastContour.pop();
this.lastContour.push(z);
return;
}
}
this.lastContour.push(z);
}
arcTo(arc, x, y) {
const offPoints = TypoGeom.Quadify.auto(arc, FINAL_SIMPLIFY_TOLERANCE);
if (offPoints) {
for (const z of offPoints)
this.lastContour.push(Point.offFrom(z).round(FINAL_SIMPLIFY_RESOLUTION));
}
this.lineTo(x, y);
}
}
function isLineExtend(a, b, c) {
return (
a.on &&
b.on &&
c.on &&
((aligned(a.x, b.x, c.x) && between(a.y, b.y, c.y)) ||
(aligned(a.y, b.y, c.y) && between(a.x, b.x, c.x)))
);
}
function aligned(a, b, c) {
return a === b && b === c;
}
function between(a, b, c) {
return (a <= b && b <= c) || (a >= b && b >= c);
}
module.exports = function finalizeFont(para, rawGlyphList, excludedCodePoints, font) {
const glyphList = filterWideGlyphs(para, rawGlyphList);
gcFont(glyphList, excludedCodePoints, font, {});
extractGlyfCmap(regulateGlyphList(para, glyphList), font);
};

View file

@ -3571,18 +3571,19 @@ glyph-block Letter-Latin-Upper-N : begin
local stroke : adviceBlackness [fallback crowd 1]
local halftopstroke : topstroke / 2
include : dispiro
flat left 0 [widths.heading 0 stroke Upward]
curl left (top * 0.4) [heading Upward]
straight.up.end left top [widths.heading 0 topstroke Upward]
include : dispiro
flat right top [widths.heading 0 stroke Downward]
curl right (top * 0.6) [heading Downward]
straight.down.end right 0 [widths.heading 0 topstroke Downward]
include : dispiro
flat (left + halftopstroke) top [widths.heading topstroke 0 Downward]
curl (right - halftopstroke) 0 [widths.heading 0 topstroke Downward]
include : AINSerifs top left right stroke xn
include : union
AINSerifs top left right stroke xn
dispiro
flat left 0 [widths.heading 0 stroke Upward]
curl left (top * 0.4) [heading Upward]
straight.up.end left top [widths.heading 0 topstroke Upward]
dispiro
flat right top [widths.heading 0 stroke Downward]
curl right (top * 0.6) [heading Downward]
straight.down.end right 0 [widths.heading 0 topstroke Downward]
dispiro
flat (left + halftopstroke) top [widths.heading topstroke 0 Downward]
curl (right - halftopstroke) 0 [widths.heading 0 topstroke Downward]
sketch # N
set-width Width

View file

@ -20,7 +20,7 @@
"topsort": "^0.0.2",
"ttf2woff": "^2.0.1",
"ttf2woff2": "^3.0.0",
"typo-geom": "^0.7.0",
"typo-geom": "^0.8.0",
"unicode-13.0.0": "^0.8.0",
"unorm": "^1.6.0",
"verda": "^1.0.1",

View file

@ -37,87 +37,6 @@ exports.curveToContour = function (curve, segments) {
return [Point.cornerFrom(z0), ...offPoints, Point.cornerFrom(z1)];
};
function removeMids(contour) {
for (let rounds = 0; rounds < 255; rounds++) {
const n0 = contour.length;
let last = contour.length - 1;
for (let j = 0; j < contour.length - 1; j++) {
if (
Math.abs(contour[j].x - contour[j + 1].x) < 1 &&
Math.abs(contour[j].y - contour[j + 1].y) < 1
) {
contour[j + 1].rem = true;
contour[j].on = true;
}
}
while (
last > 0 &&
Math.abs(contour[0].x - contour[last].x) < 1 &&
Math.abs(contour[0].y - contour[last].y) < 1
) {
contour[last].rem = true;
contour[0].on = true;
last -= 1;
}
contour = contour.filter(x => !x.rem);
last = contour.length - 1;
let zPre, z, zPost;
for (let j = 1; j < contour.length; j++) {
if (j < contour.length - 1) {
zPre = contour[j - 1];
z = contour[j];
zPost = contour[j + 1];
} else {
zPre = contour[last];
z = contour[0];
zPost = contour[1];
}
// if (!zPre || !zPost) continue;
const mx = zPre.x + zPost.x;
const my = zPre.y + zPost.y;
const dx = zPre.x - zPost.x;
const dy = zPre.y - zPost.y;
if (!zPre.on && z.on && !zPost.on) {
if (Math.abs(dy) >= 1 && Math.abs(z.x * 2 - mx) < 1 && Math.abs(z.y * 2 - my) < 1) {
z.rem = true;
}
} else if (!zPre.rem && zPre.on && z.on && !zPost.rem && zPost.on) {
if (Math.abs(dy) >= 1 && Math.abs(z.x * 2 - mx) < 1 && Math.abs(dx) < 1) {
z.rem = true;
} else if (Math.abs(dx) >= 1 && Math.abs(z.y * 2 - my) < 1 && Math.abs(dy) < 1) {
z.rem = true;
}
}
}
contour = contour.filter(x => !x.rem);
const n = contour.length;
if (n >= n0) break;
}
return contour;
}
function extPrior(a, b) {
return a.y < b.y || (a.y === b.y && ((a.on && !b.on) || (a.on === b.on && a.x < b.x)));
}
function canonicalStart(_points) {
const points = _points.reverse();
let jm = 0;
for (let j = 0; j < points.length * 2; j++) {
if (extPrior(points[j % points.length], points[jm])) {
jm = j % points.length;
}
}
return points.slice(jm).concat(points.slice(0, jm));
}
function cleanupQuadContour(c) {
return canonicalStart(removeMids(c));
}
function convertContourToCubic(contour) {
if (!contour || !contour.length) return [];
@ -271,7 +190,6 @@ function convertShapeToArcs(shape) {
return shape.map(convertContourToArcs);
}
exports.cleanupQuadContour = cleanupQuadContour;
exports.convertContourToCubic = convertContourToCubic;
exports.convertContourToCubicRev = convertContourToCubicRev;
exports.autoCubify = autoCubify;

View file

@ -1,332 +0,0 @@
"use strict";
const typoGeom = require("typo-geom");
const Point = require("./point");
const SMALL = 1e-6;
function solveTS(a, b, c, out, flag) {
const delta = b * b - 4 * a * c;
if (delta > 0) {
const t1 = (Math.sqrt(delta) - b) / (2 * a);
const t2 = (-Math.sqrt(delta) - b) / (2 * a);
if (flag) {
if (t1 >= 0 && t1 <= 1) out.push(t1);
if (t2 >= 0 && t2 <= 1) out.push(t2);
} else {
if (t1 > 0 && t1 < 1) out.push(t1);
if (t2 > 0 && t2 < 1) out.push(t2);
}
} else if (delta === 0) {
const t = -b / (2 * a);
if (flag) {
if (t >= 0 && t <= 1) out.push(t);
} else {
if (t > 0 && t < 1) out.push(t);
}
}
}
function findExtrema(z1, z2, z3, z4, out) {
const a = 3 * (-z1 + 3 * z2 - 3 * z3 + z4);
const b = 6 * (z1 - 2 * z2 + z3);
const c = 3 * (z2 - z1);
solveTS(a, b, c, out);
}
function ASCEND(a, b) {
return a - b;
}
function fineAllExtrema(z1, z2, z3, z4) {
let exs = [];
findExtrema(z1.x, z2.x, z3.x, z4.x, exs);
findExtrema(z1.y, z2.y, z3.y, z4.y, exs);
return exs.sort(ASCEND);
}
function bez1(z1, z2, t) {
if (t <= 0) return z1;
if (t >= 1) return z2;
let x = (1 - t) * z1.x + t * z2.x,
y = (1 - t) * z1.y + t * z2.y;
return { x: x, y: y };
}
function bez2(z1, z2, z3, t) {
if (t <= 0) return z1;
if (t >= 1) return z3;
let c1 = (1 - t) * (1 - t),
c2 = 2 * (1 - t) * t,
c3 = t * t;
return {
x: c1 * z1.x + c2 * z2.x + c3 * z3.x,
y: c1 * z1.y + c2 * z2.y + c3 * z3.y
};
}
function bez3(z1, z2, z3, z4, t) {
if (t <= 0) return z1;
if (t >= 1) return z4;
let c1 = (1 - t) * (1 - t) * (1 - t),
c2 = 3 * t * (1 - t) * (1 - t),
c3 = 3 * t * t * (1 - t),
c4 = t * t * t;
return {
x: c1 * z1.x + c2 * z2.x + c3 * z3.x + c4 * z4.x,
y: c1 * z1.y + c2 * z2.y + c3 * z3.y + c4 * z4.y
};
}
function splitBefore(z1, z2, z3, z4, t) {
return [z1, bez1(z1, z2, t), bez2(z1, z2, z3, t), bez3(z1, z2, z3, z4, t)];
}
function splitAfter(z1, z2, z3, z4, t) {
return [bez3(z1, z2, z3, z4, t), bez2(z2, z3, z4, t), bez1(z3, z4, t), z4];
}
function splitAtExtrema(z1, z2, z3, z4, curve) {
const ts = fineAllExtrema(z1, z2, z3, z4);
if (ts[0] < SMALL) {
ts[0] = 0;
} else {
ts.unshift(0);
}
if (ts[ts.length - 1] > 1 - SMALL) {
ts[ts.length - 1] = 1;
} else {
ts.push(1);
}
for (let k = 0; k < ts.length; k++) {
if (k > 0) {
const t1 = ts[k - 1];
const t2 = ts[k];
const bef = splitBefore(z1, z2, z3, z4, t2);
const seg = splitAfter(bef[0], bef[1], bef[2], bef[3], t1 / t2);
seg[1].on = seg[2].on = false;
seg[1].cubic = seg[2].cubic = true;
seg[3].on = true;
curve.push(seg[1], seg[2], seg[3]);
}
}
}
function veryClose(z1, z2) {
return (z1.x - z2.x) * (z1.x - z2.x) + (z1.y - z2.y) * (z1.y - z2.y) <= SMALL;
}
function toSpansForm(sourceCurve, fSplit) {
const curve = [sourceCurve[0]];
let last = sourceCurve[0];
for (let j = 1; j < sourceCurve.length; j++) {
if (sourceCurve[j].on) {
const z1 = last,
z4 = sourceCurve[j];
if (!veryClose(z1, z4)) {
curve.push(z4);
last = z4;
}
} else if (sourceCurve[j].cubic) {
const z1 = last,
z2 = sourceCurve[j],
z3 = sourceCurve[(j + 1) % sourceCurve.length],
z4 = sourceCurve[(j + 2) % sourceCurve.length];
if (!(veryClose(z1, z2) && veryClose(z2, z3) && veryClose(z3, z4))) {
if (fSplit) {
splitAtExtrema(z1, z2, z3, z4, curve);
} else {
curve.push(z2, z3, z4);
}
last = z4;
}
j += 2;
} else {
throw new Error("Unreachable.");
}
}
return curve;
}
function cross(z1, z2, z3) {
return (z2.x - z1.x) * (z3.y - z1.y) - (z3.x - z1.x) * (z2.y - z1.y);
}
function dot(z1, z2, z3) {
return (z2.x - z1.x) * (z3.x - z1.x) + (z3.y - z1.y) * (z2.y - z1.y);
}
function markCorners(curve) {
for (const z of curve) z.mark = 0;
for (let j = 0; j < curve.length; j++) {
if (!curve[j].on) continue;
const z1 = curve[j],
z0 = curve[(j - 1 + curve.length) % curve.length],
z2 = curve[(j + 1) % curve.length];
const almostLinear = Math.abs(cross(z1, z0, z2)) < SMALL;
const inBetween = dot(z1, z0, z2) < 0;
if (z0.on && z2.on && almostLinear && inBetween) {
z1.mark = 0;
} else if (z0.on || z2.on) {
z1.mark = 1;
} else if (almostLinear && inBetween) {
// Z0 -- Z1 -- Z2 are linear
const angle = Math.abs(Math.atan2(z2.y - z0.y, z2.x - z0.x)) % Math.PI;
if (Math.abs(angle) <= SMALL || Math.abs(angle - Math.PI) <= SMALL) {
z1.mark = 4;
} else if (Math.abs(angle - Math.PI / 2) <= SMALL) {
z1.mark = 2;
}
} else {
z1.mark = 1;
}
}
}
function canonicalStart(curve) {
let jm = 0,
rank = 0;
for (let j = 0; j < curve.length; j++) {
const zRank = (curve[j].on ? 1 : 0) + (2 * curve[j].mark || 0);
if (zRank > rank) {
jm = j;
rank = zRank;
}
}
return toSpansForm(curve.slice(jm).concat(curve.slice(0, jm)), false);
}
class BezierCurveCluster {
constructor(zs) {
let segments = [];
let lengths = [];
let last = zs[0];
for (let j = 1; j < zs.length; j++) {
if (zs[j].on) {
const z1 = last,
z4 = zs[j];
const seg = new typoGeom.Arcs.StraightSegment(z1, z4);
segments.push(seg);
lengths.push(this.measureLength(seg));
last = z4;
} else if (zs[j].cubic) {
const z1 = last,
z2 = zs[j],
z3 = zs[j + 1],
z4 = zs[j + 2];
const seg = new typoGeom.Arcs.Bez3(z1, z2, z3, z4);
segments.push(seg);
lengths.push(this.measureLength(seg));
last = z4;
j += 2;
} else {
throw new Error("Unreachable.");
}
}
let totalLength = 0;
for (let j = 0; j < lengths.length; j++) totalLength += lengths[j];
let lengthSofar = 0;
for (let j = 0; j < lengths.length; j++) {
let segLen = lengths[j];
lengths[j] = lengthSofar / totalLength;
lengthSofar += segLen;
}
this.segments = segments;
this.lengths = lengths;
}
measureLength(c) {
const N = 16;
let z0 = c.eval(0);
let d = 0;
for (let t = 1; t <= N; t++) {
const z = c.eval(t / N);
d += Math.hypot(z.x - z0.x, z.y - z0.y);
z0 = z;
}
return d;
}
getIndex(t) {
let j = this.lengths.length - 1;
while (j > 0 && this.lengths[j] > t) j--;
return j;
}
eval(t) {
const j = this.getIndex(t);
const tBefore = this.lengths[j];
const tNext = j < this.lengths.length - 1 ? this.lengths[j + 1] : 1;
const tRelative = (t - tBefore) / (tNext - tBefore);
return this.segments[j].eval(tRelative);
}
derivative(t) {
const j = this.getIndex(t);
const tBefore = this.lengths[j];
const tNext = j < this.lengths.length - 1 ? this.lengths[j + 1] : 1;
const tRelative = (t - tBefore) / (tNext - tBefore);
const d = this.segments[j].derivative(tRelative);
d.x /= tNext - tBefore;
d.y /= tNext - tBefore;
return d;
}
inRange(err, a, b, c) {
if (a <= c) return b >= a - err && b <= c + err;
else return b >= c - err && b <= a + err;
}
colinear(err, a, b, c) {
if (!this.inRange(err, a.x, b.x, c.x)) return false;
if (!this.inRange(err, a.y, b.y, c.y)) return false;
const det = (b.y - a.y) * (c.x - b.x) - (c.y - b.y) * (b.x - a.x);
return det < err * err && det > -err * err;
}
isAlmostLinear(err) {
const N = 64;
let z0 = this.eval(0);
let z1 = this.eval(1);
for (let k = 1; k < N; k++) {
const zt = this.eval(k / N);
if (!this.colinear(err, z0, zt, z1)) return false;
}
return true;
}
}
const QuadBuilder = {
corner(sink, z) {
sink.push(Point.cornerFrom(z).round(1024));
},
arc(sink, arc) {
if (arc.isAlmostLinear(1 / 4)) return;
const offPoints = typoGeom.Quadify.auto(arc, 1 / 4);
if (!offPoints) return;
for (const z of offPoints) {
sink.push(Point.offFrom(z).round(1024));
}
},
split: true,
canonicalStart: true,
duplicateStart: true
};
function buildCurve(curve, builder) {
let sink = [];
for (let j = 0; j < curve.length; j++) {
if (!curve[j].mark) continue;
builder.corner(sink, curve[j]);
let k = j;
for (; k < curve.length && (k === j || !curve[k].mark); k++);
const pts = curve.slice(j, k + 1);
if (pts.length > 1) builder.arc(sink, new BezierCurveCluster(pts));
j = k - 1;
}
return sink;
}
function fairifyImpl(sourceCubicContour, builder) {
let splitContour = toSpansForm(sourceCubicContour, builder.split);
markCorners(splitContour);
if (builder.canonicalStart) {
splitContour = canonicalStart(splitContour);
markCorners(splitContour);
}
return buildCurve(splitContour, builder);
}
exports.fairifyQuad = function (sourceCubicContour) {
return fairifyImpl(sourceCubicContour, QuadBuilder);
};