From 9543e23a95dc076385c0227d81fb6ec7ef38ab87 Mon Sep 17 00:00:00 2001 From: be5invis Date: Wed, 17 Jun 2020 03:35:21 -0700 Subject: [PATCH] Move the shape cleanup functionality out. --- gen/finalize/index.js | 194 ++++++++++++++--------- glyphs/letter-latin.ptl | 25 +-- package.json | 2 +- support/curve-util.js | 82 ---------- support/fairify.js | 332 ---------------------------------------- 5 files changed, 135 insertions(+), 500 deletions(-) delete mode 100644 support/fairify.js diff --git a/gen/finalize/index.js b/gen/finalize/index.js index 880bd461f..54ce8811a 100644 --- a/gen/finalize/index.js +++ b/gen/finalize/index.js @@ -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); -}; diff --git a/glyphs/letter-latin.ptl b/glyphs/letter-latin.ptl index 00a7440ba..a2df3f101 100644 --- a/glyphs/letter-latin.ptl +++ b/glyphs/letter-latin.ptl @@ -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 diff --git a/package.json b/package.json index 40e822016..3d1a66ad4 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/support/curve-util.js b/support/curve-util.js index a8c55f9fc..38242b492 100644 --- a/support/curve-util.js +++ b/support/curve-util.js @@ -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; diff --git a/support/fairify.js b/support/fairify.js deleted file mode 100644 index 3c08084cb..000000000 --- a/support/fairify.js +++ /dev/null @@ -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); -};