Fix srtail, Sswash and sswash. Add zero.reverse-slashed.

Closes #458.
This commit is contained in:
Belleve Invis 2020-03-23 22:59:20 -07:00
parent 866dc4daae
commit c0f030b513
19 changed files with 327 additions and 300 deletions

View file

@ -304,6 +304,7 @@ The current available styles for `design`/`upright`/`italic`/`oblique` options a
* `v-zero-slashed`, `cv13`: Slashed Zero `0` (default). * `v-zero-slashed`, `cv13`: Slashed Zero `0` (default).
* `v-zero-dotted`, `cv14`: Dotted Zero `0`. * `v-zero-dotted`, `cv14`: Dotted Zero `0`.
* `v-zero-unslashed`, `cv15`: O-like `0`. * `v-zero-unslashed`, `cv15`: O-like `0`.
* `v-zero-reverse-slashed`, `cv93`: Reverse-slashed `0`.
* Styles for `1`: * Styles for `1`:
* `v-one-nobase`, `cv50`: `1` with bottom serif (default for Sans). * `v-one-nobase`, `cv50`: `1` with bottom serif (default for Sans).
* `v-one-base`, `cv51`: `1` without bottom serif (default for Slab). * `v-one-base`, `cv51`: `1` without bottom serif (default for Slab).

View file

@ -4,7 +4,6 @@ import '../support/spirokit' as spirokit
import '../support/transform' as : Transform && [object [transformPoint tp] [unTransform utp] inverse] import '../support/transform' as : Transform && [object [transformPoint tp] [unTransform utp] inverse]
import '../support/anchor' as Anchor import '../support/anchor' as Anchor
import '../support/monotonic-interpolate' as smoothreg import '../support/monotonic-interpolate' as smoothreg
import '../support/fairify' as fairify
import [mix linreg clamp fallback TempFont includeGlyphPart compsiteMarkSet] from '../support/utils' import [mix linreg clamp fallback TempFont includeGlyphPart compsiteMarkSet] from '../support/utils'
import [calculateMetrics setFontMetrics MarksetDiv GenDivFrame] from '../meta/aesthetics' import [calculateMetrics setFontMetrics MarksetDiv GenDivFrame] from '../meta/aesthetics'

View file

@ -1,7 +1,6 @@
$$include '../meta/macros.ptl' $$include '../meta/macros.ptl'
import '../support/transform' as : Transform && [object [transformPoint tp] [unTransform utp] inverse] import '../support/transform' as : Transform && [object [transformPoint tp] [unTransform utp] inverse]
import '../support/fairify' as fairify
import [mix linreg clamp fallback] from '../support/utils' import [mix linreg clamp fallback] from '../support/utils'
import [designParameters] from '../meta/aesthetics' import [designParameters] from '../meta/aesthetics'

View file

@ -4211,13 +4211,11 @@ glyph-block LetterUnified-Basic : begin
define tension : 0.46 - 0.005 * strokeCoeff + slantCoeff * TANSLANT define tension : 0.46 - 0.005 * strokeCoeff + slantCoeff * TANSLANT
return : alsothruthem {{0.4 tension} {0.6 (1 - tension)}} [widths.center stroke] return : alsothruthem {{0.4 tension} {0.6 (1 - tension)}} [widths.center stroke]
sketch # S define [SStroke] : begin
set-width WIDTH
include MarkSet.capital
local stroke : adviceBlackness2 2.875 2 CAP local stroke : adviceBlackness2 2.875 2 CAP
local ess : ESS * stroke / STROKE local ess : ESS * stroke / STROKE
define smooth : adviceSSmooth CAP (-1) stroke define smooth : adviceSSmooth CAP (-1) stroke
include : dispiro return : dispiro
widths.lhs stroke widths.lhs stroke
g4 RIGHTSB (CAP - HOOK) g4 RIGHTSB (CAP - HOOK)
hookstart CAPO (sw -- stroke) (swItalicAdj -- STROKE) hookstart CAPO (sw -- stroke) (swItalicAdj -- STROKE)
@ -4226,34 +4224,12 @@ glyph-block LetterUnified-Basic : begin
g4 RIGHTSB smooth [widths 0 stroke] g4 RIGHTSB smooth [widths 0 stroke]
hookend O (sw -- stroke) (swItalicAdj -- STROKE) hookend O (sw -- stroke) (swItalicAdj -- STROKE)
g4 SB HOOK g4 SB HOOK
save 'S' 'S'
save 'cyrDze' 0x405
sketch # s define [RevSStroke] : begin
set-width WIDTH
include MarkSet.e
define stroke : adviceBlackness2 2.875 2 XH
define ess : mix stroke (ESS * stroke / STROKE) 0.5
define strokeCoeff : linreg 18 0 126 1 stroke
define smooth : adviceSSmooth XH (-1) stroke
include : dispiro
widths.lhs stroke
g4 (RIGHTSB + OX / 4) (XH - SHOOK)
hookstart XO (sw -- stroke) (swItalicAdj -- STROKE)
g4 (SB + OX / 8) (XH - smooth)
SNeck stroke (slantCoeff -- -0.01)
g4 (RIGHTSB - OX / 8) (smooth) [widths 0 stroke]
hookend O (sw -- stroke) (swItalicAdj -- STROKE)
g4 (SB - OX / 4) SHOOK
save 's' 's'
save 'cyrdze' 0x455
sketch # revS
include MarkSet.capital
local stroke : adviceBlackness2 2.875 2 CAP local stroke : adviceBlackness2 2.875 2 CAP
local ess : ESS * stroke / STROKE local ess : ESS * stroke / STROKE
local smooth : adviceSSmooth CAP 0.5 stroke local smooth : adviceSSmooth CAP 0.5 stroke
include : dispiro return : dispiro
widths.rhs stroke widths.rhs stroke
g4 SB (CAP - HOOK) g4 SB (CAP - HOOK)
hookstart CAPO (sw -- stroke) (swItalicAdj -- STROKE) hookstart CAPO (sw -- stroke) (swItalicAdj -- STROKE)
@ -4263,15 +4239,27 @@ glyph-block LetterUnified-Basic : begin
hookend O (sw -- stroke) (swItalicAdj -- STROKE) hookend O (sw -- stroke) (swItalicAdj -- STROKE)
g4 RIGHTSB HOOK g4 RIGHTSB HOOK
save 'revS' 0x1A7 define [sStroke] : begin
define stroke : adviceBlackness2 2.875 2 XH
define ess : mix stroke (ESS * stroke / STROKE) 0.5
define strokeCoeff : linreg 18 0 126 1 stroke
define smooth : adviceSSmooth XH (-1) stroke
return : dispiro
widths.lhs stroke
g4 (RIGHTSB + OX / 4) (XH - SHOOK)
hookstart XO (sw -- stroke) (swItalicAdj -- STROKE)
g4 (SB + OX / 8) (XH - smooth)
SNeck stroke (slantCoeff -- -0.01)
g4 (RIGHTSB - OX / 8) (smooth) [widths 0 stroke]
hookend O (sw -- stroke) (swItalicAdj -- STROKE)
g4 (SB - OX / 4) SHOOK
sketch # revs define [RevsStroke] : begin
include MarkSet.e
define stroke : adviceBlackness2 2.875 2 XH define stroke : adviceBlackness2 2.875 2 XH
define ess : ESS * stroke / STROKE define ess : ESS * stroke / STROKE
define strokeCoeff : linreg 18 0 126 1 stroke define strokeCoeff : linreg 18 0 126 1 stroke
define smooth : adviceSSmooth XH 1 stroke define smooth : adviceSSmooth XH 1 stroke
include : dispiro return : dispiro
widths.rhs stroke widths.rhs stroke
g4 SB (XH - SHOOK) g4 SB (XH - SHOOK)
hookstart XO (sw -- stroke) (swItalicAdj -- STROKE) hookstart XO (sw -- stroke) (swItalicAdj -- STROKE)
@ -4281,43 +4269,73 @@ glyph-block LetterUnified-Basic : begin
hookend O (sw -- stroke) (swItalicAdj -- STROKE) hookend O (sw -- stroke) (swItalicAdj -- STROKE)
g4 RIGHTSB SHOOK g4 RIGHTSB SHOOK
sketch # S
include MarkSet.capital
include : SStroke
save 'S' 'S'
save 'cyrDze' 0x405
sketch # s
include MarkSet.e
include : sStroke
save 's' 's'
save 'cyrdze' 0x455
sketch # revS
include MarkSet.capital
include : RevSStroke
save 'revS' 0x1A7
sketch # revs
include MarkSet.e
include : RevsStroke
save 'revs' 0x1A8 save 'revs' 0x1A8
save 'cyrghe.italic' save 'cyrghe.italic'
sketch # srtail sketch # srtail
include glyphs.rtailBR
local stroke : adviceBlackness2 2.875 2 XH
apply-transform : Upright
apply-transform : Translate (SB + stroke * HVCONTRAST + OXHOOK) 0
apply-transform : Italify
include MarkSet.p include MarkSet.p
include glyphs.s local stroke : [sStroke].call currentGlyph
include : VBarLeft (SB + OXHOOK) 0 SHOOK local start : utp currentGlyph.gizmo stroke.lhsKnots.(stroke.lhsKnots.length - 1)
include : create-glyph : glyph-construction
local sw : adviceBlackness2 2.875 2 XH
include glyphs.rtailBR
apply-transform : Upright
apply-transform : Translate (start.x + sw * HVCONTRAST) 0
apply-transform : Italify
include : VBarLeft start.x 0 start.y
save 'srtail' 0x282 save 'srtail' 0x282
sketch # Sswash sketch # Sswash
include MarkSet.if include MarkSet.if
include glyphs.S
local stroke : adviceBlackness2 2.875 2 CAP local stroke : [SStroke].call currentGlyph
local start : utp currentGlyph.gizmo stroke.lhsKnots.(stroke.lhsKnots.length - 1)
local sw : adviceBlackness2 2.875 2 CAP
include : dispiro include : dispiro
widths.lhs [adviceBlackness 4.5] widths.lhs [adviceBlackness 4.5]
g4 (SB + OXHOOK) HOOK g4 start.x start.y
alsothru 0.15 0.6 important alsothru 0.15 0.6 important
flat (RIGHTSB - 1) DESCENDER [widths stroke 0] flat (RIGHTSB - 1) DESCENDER [widths sw 0]
curl RIGHTSB DESCENDER curl RIGHTSB DESCENDER
save 'Sswash' 0x2C7E save 'Sswash' 0x2C7E
sketch # sswash sketch # sswash
include MarkSet.p include MarkSet.p
include glyphs.s
local stroke : adviceBlackness2 2.875 2 XH local stroke : [sStroke].call currentGlyph
local start : utp currentGlyph.gizmo stroke.lhsKnots.(stroke.lhsKnots.length - 1)
local sw : adviceBlackness2 2.875 2 XH
include : dispiro include : dispiro
widths.lhs [adviceBlackness 4.5] widths.lhs [adviceBlackness 4.5]
g4 (SB + OXHOOK) SHOOK g4 start.x start.y
alsothru 0.2 0.6 important alsothru 0.15 0.6 important
flat (RIGHTSB - 1) DESCENDER [widths stroke 0] flat (RIGHTSB - 1) DESCENDER [widths sw 0]
curl RIGHTSB DESCENDER curl RIGHTSB DESCENDER
save 'sswash' 0x23F save 'sswash' 0x23F

View file

@ -24,6 +24,14 @@ glyph-block Numbers : begin
flat 0 (top * (1 - 0.77)) [widths fine fine] flat 0 (top * (1 - 0.77)) [widths fine fine]
curl WIDTH (top * 0.77) curl WIDTH (top * 0.77)
define [ZeroReverseSlashShape top] : begin
local fine : 0.5 * OVERLAYSTROKE
return : intersection
OShapeOutline (top + O) (0 - O) (SB - O) (RIGHTSB + O)
dispiro
flat 0 (top * 0.77) [widths fine fine]
curl WIDTH (top * (1 - 0.77))
define [ZeroDotShape top] : begin define [ZeroDotShape top] : begin
local radius : Math.min DOTRADIUS ((RIGHTSB - SB - STROKE * 2) / 4) local radius : Math.min DOTRADIUS ((RIGHTSB - SB - STROKE * 2) / 4)
return : Ring (top / 2 + radius) (top / 2 - radius) (MIDDLE + radius) (MIDDLE - radius) return : Ring (top / 2 + radius) (top / 2 - radius) (MIDDLE + radius) (MIDDLE - radius)
@ -39,6 +47,12 @@ glyph-block Numbers : begin
include : ZeroSlashShape CAP include : ZeroSlashShape CAP
save 'zero.lnum.slashed' save 'zero.lnum.slashed'
sketch # zero.lnum.reverse-slashed
include MarkSet.capital
include glyphs.'zero.lnum.unslashed'
include : ZeroReverseSlashShape CAP
save 'zero.lnum.reverse-slashed'
sketch # zero.lnum.dotted sketch # zero.lnum.dotted
include MarkSet.capital include MarkSet.capital
include glyphs.'zero.lnum.unslashed' include glyphs.'zero.lnum.unslashed'
@ -56,6 +70,12 @@ glyph-block Numbers : begin
include : ZeroSlashShape XH include : ZeroSlashShape XH
save 'zero.onum.slashed' save 'zero.onum.slashed'
sketch # zero.onum.reverse-slashed
include MarkSet.e
include glyphs.'zero.onum.unslashed'
include : ZeroReverseSlashShape XH
save 'zero.onum.reverse-slashed'
sketch # zero.onum.dotted sketch # zero.onum.dotted
include MarkSet.e include MarkSet.e
include glyphs.'zero.onum.unslashed' include glyphs.'zero.onum.unslashed'

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

After

Width:  |  Height:  |  Size: 173 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 396 KiB

After

Width:  |  Height:  |  Size: 398 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 338 KiB

After

Width:  |  Height:  |  Size: 337 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 KiB

After

Width:  |  Height:  |  Size: 174 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 336 KiB

After

Width:  |  Height:  |  Size: 337 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 960 KiB

After

Width:  |  Height:  |  Size: 959 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 319 KiB

After

Width:  |  Height:  |  Size: 319 KiB

Before After
Before After

View file

@ -394,6 +394,11 @@
<span class="sample" style="font-feature-settings:'cv15'">0</span> <span class="sample" style="font-feature-settings:'cv15'">0</span>
<span class="sample italic" style="font-feature-settings:'cv15'">0</span> <span class="sample italic" style="font-feature-settings:'cv15'">0</span>
</li> </li>
<li>
<span class="tag">cv93</span>
<span class="sample" style="font-feature-settings:'cv93'">0</span>
<span class="sample italic" style="font-feature-settings:'cv93'">0</span>
</li>
</ol></li> </ol></li>
<li><ol class="group"> <li><ol class="group">
<li> <li>

View file

@ -92,43 +92,49 @@ function removeMids(contour) {
contour = contour.filter(x => !x.rem); contour = contour.filter(x => !x.rem);
last = contour.length - 1; last = contour.length - 1;
for (let j = 1; j < contour.length - 1; j++) { let zPre, z, zPost;
if (!contour[j - 1].on && contour[j].on && !contour[j + 1].on) { for (let j = 1; j < contour.length; j++) {
const mx = contour[j - 1].x + contour[j + 1].x; if (j < contour.length - 1) {
const my = contour[j - 1].y + contour[j + 1].y; zPre = contour[j - 1];
const dy = contour[j - 1].y - contour[j + 1].y; z = contour[j];
if ( zPost = contour[j + 1];
Math.abs(dy) >= 1 && } else {
Math.abs(contour[j].x * 2 - mx) < 1 && zPre = contour[last];
Math.abs(contour[j].y * 2 - my) < 1 z = contour[0];
) { zPost = contour[1];
contour[j].rem = true; }
// 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;
} }
} }
} }
if (!contour[last].rem && !contour[last].on && contour[0].on && !contour[1].on) {
const mx = contour[last].x + contour[1].x;
const my = contour[last].y + contour[1].y;
if (Math.abs(contour[0].x * 2 - mx) < 1 && Math.abs(contour[0].y * 2 - my) < 1) {
contour[0].rem = true;
}
}
contour = contour.filter(x => !x.rem); contour = contour.filter(x => !x.rem);
const n = contour.length; const n = contour.length;
if (n >= n0) break; if (n >= n0) break;
} }
return contour; return contour;
} }
function extPrior(a, b) { 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))); return a.y < b.y || (a.y === b.y && ((a.on && !b.on) || (a.on === b.on && a.x < b.x)));
} }
function canonicalStart(_points) { function canonicalStart(_points) {
const points = _points.reverse().map(z => { const points = _points.reverse();
z.x = Math.round(z.x * 1024) / 1024;
z.y = Math.round(z.y * 1024) / 1024;
return z;
});
let jm = 0; let jm = 0;
for (var j = 0; j < points.length * 2; j++) { for (var j = 0; j < points.length * 2; j++) {
if (extPrior(points[j % points.length], points[jm])) { if (extPrior(points[j % points.length], points[jm])) {
@ -138,93 +144,8 @@ function canonicalStart(_points) {
return points.slice(jm).concat(points.slice(0, jm)); return points.slice(jm).concat(points.slice(0, jm));
} }
function colinear(x1, y1, x2, y2, x3, y3, err) { function cleanupQuadContour(c) {
const det = x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2); return canonicalStart(removeMids(c));
return det <= err && det >= -err;
}
function inspan(a, b, c) {
if (a > c) return inspan(c, b, a);
return a <= b && b <= c;
}
function handle(z1, z2, z3, z4, err) {
if (
colinear(z1.x, z1.y, z2.x, z2.y, z4.x, z4.y, err) &&
colinear(z1.x, z1.y, z3.x, z3.y, z4.x, z4.y, err) &&
inspan(z1.x, z2.x, z4.x) &&
inspan(z1.y, z2.y, z4.y) &&
inspan(z1.x, z3.x, z4.x) &&
inspan(z1.y, z3.y, z4.y)
) {
return [];
}
const curve = new typoGeom.Curve.Bez3(z1, z2, z3, z4);
const offPoints = typoGeom.Quadify.auto(curve, err);
const ans = [];
for (const z of offPoints) {
ans.push(Point.offFrom(z));
}
return ans;
}
function convertContourToTt(contour, err) {
if (contour.length === 0) return [];
if (contour.length === 1) return [contour[0]];
err = err || 1 / 4;
const newContour = [];
let z0 = contour[0];
newContour.push(Point.cornerFrom(z0));
for (let j = 1; j < contour.length; j++) {
const z = contour[j];
if (z.on) {
newContour.push(Point.cornerFrom(z));
z0 = z;
} else if (z.cubic) {
const z1 = z;
const z2 = contour[j + 1];
const z3 = contour[j + 2];
const quadZs = handle(z0, z1, z2, z3, err);
for (const z of quadZs) newContour.push(z);
newContour.push(Point.cornerFrom(z3));
z0 = z3;
j += 2;
} else {
const zc = z;
let zf = contour[j + 1] ? contour[j + 1] : contour[0];
if (!zf.on) {
zf = Point.cornerFromXY(mix(zc.x, zf.x, 0.5), mix(zc.y, zf.y, 0.5));
}
newContour.push(Point.offFrom(zc));
newContour.push(Point.cornerFrom(zf));
z0 = zf;
j++;
}
}
return newContour;
}
function byFirstPointCoord(a, b) {
if (!a.length) return -1;
if (!b.length) return 1;
let z1 = a[0];
let z2 = b[0];
return z1.y !== z2.y
? z1.y - z2.y
: z1.x !== z2.x
? z1.x - z2.x
: byFirstPointCoord(a.slice(1), b.slice(1));
}
function convertContourListToTt(contours, err) {
err = err || 1 / 4;
let ans = [];
for (let c of contours) {
ans.push(canonicalStart(removeMids(convertContourToTt(c, err))));
}
return ans.sort(byFirstPointCoord);
} }
function convertContourToCubic(contour) { function convertContourToCubic(contour) {
@ -243,34 +164,67 @@ function convertContourToCubic(contour) {
const z1 = z; const z1 = z;
const z2 = contour[j + 1]; const z2 = contour[j + 1];
const z3 = contour[j + 2]; const z3 = contour[j + 2];
newContour.push(Point.cubicOffFrom(z1)); newContour.push(Point.cubicOffFrom(z1));
newContour.push(Point.cubicOffFrom(z2)); newContour.push(Point.cubicOffFrom(z2));
newContour.push(Point.cornerFrom(z3)); newContour.push(Point.cornerFrom(z3));
z0 = z3; z0 = z3;
j += 2; j += 2;
} else { } else {
const zc = z; const zc = z;
let zf = contour[j + 1] ? contour[j + 1] : contour[0]; let zf = contour[j + 1] || contour[0];
if (!zf.on) { const zfIsCorner = zf.on;
zf = Point.cornerFromXY(mix(zc.x, zf.x, 0.5), mix(zc.y, zf.y, 0.5)); if (!zfIsCorner) zf = Point.cornerFrom(zc).mix(0.5, zf);
}
const x1 = mix(z0.x, zc.x, 2 / 3); newContour.push(Point.cubicOffFrom(z0).mix(2 / 3, zc));
const y1 = mix(z0.y, zc.y, 2 / 3); newContour.push(Point.cubicOffFrom(zf).mix(2 / 3, zc));
const x2 = mix(zf.x, zc.x, 2 / 3);
const y2 = mix(zf.y, zc.y, 2 / 3);
newContour.push(Point.cubicOffFromXY(x1, y1));
newContour.push(Point.cubicOffFromXY(x2, y2));
newContour.push(Point.cornerFrom(zf)); newContour.push(Point.cornerFrom(zf));
z0 = zf; z0 = zf;
j++; if (zfIsCorner) j++;
} }
} }
return newContour; return newContour;
} }
exports.convertContourToTt = convertContourToTt; function autoCubify(arc, err) {
const MaxSegments = 16;
const Hits = 64;
let offPoints = [];
for (let nSeg = 1; nSeg <= MaxSegments; nSeg++) {
const perSegHits = Math.ceil(Hits / nSeg);
offPoints.length = 0;
let good = true;
out: for (let s = 0; s < nSeg; s++) {
const tBefore = s / nSeg;
const tAfter = (s + 1) / nSeg;
const z0 = Point.cornerFrom(arc.eval(tBefore));
const z3 = Point.cornerFrom(arc.eval(tAfter));
const z1 = Point.cubicOffFrom(z0).addScale(1 / (3 * nSeg), arc.derivative(tBefore));
const z2 = Point.cubicOffFrom(z3).addScale(-1 / (3 * nSeg), arc.derivative(tAfter));
if (s > 0) offPoints.push(z0);
offPoints.push(z1, z2);
const bezArc = new typoGeom.Curve.Bez3(z0, z1, z2, z3);
for (let k = 1; k < perSegHits; k++) {
const tk = k / perSegHits;
const zTest = arc.eval(mix(tBefore, tAfter, tk));
const zBez = bezArc.eval(tk);
if (Math.hypot(zTest.x - zBez.x, zTest.y - zBez.y) > err) {
good = false;
break out;
}
}
}
if (good) break;
}
return offPoints;
}
exports.cleanupQuadContour = cleanupQuadContour;
exports.convertContourToCubic = convertContourToCubic; exports.convertContourToCubic = convertContourToCubic;
exports.convertContourListToTt = convertContourListToTt; exports.autoCubify = autoCubify;

View file

@ -1,9 +1,11 @@
"use strict"; "use strict";
const Transform = require("./transform.js"); const Transform = require("./transform");
const typoGeom = require("typo-geom"); const typoGeom = require("typo-geom");
const Point = require("./point");
const curveUtil = require("./curve-util");
const SMALL = 1e-4; const SMALL = 1e-6;
function solveTS(a, b, c, out, flag) { function solveTS(a, b, c, out, flag) {
const delta = b * b - 4 * a * c; const delta = b * b - 4 * a * c;
@ -43,7 +45,7 @@ function fineAllExtrema(z1, z2, z3, z4) {
return exs.sort(ASCEND); return exs.sort(ASCEND);
} }
function mix(z1, z2, t) { function bez1(z1, z2, t) {
if (t <= 0) return z1; if (t <= 0) return z1;
if (t >= 1) return z2; if (t >= 1) return z2;
let x = (1 - t) * z1.x + t * z2.x, let x = (1 - t) * z1.x + t * z2.x,
@ -74,10 +76,10 @@ function bez3(z1, z2, z3, z4, t) {
}; };
} }
function splitBefore(z1, z2, z3, z4, t) { function splitBefore(z1, z2, z3, z4, t) {
return [z1, mix(z1, z2, t), bez2(z1, z2, z3, t), bez3(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) { function splitAfter(z1, z2, z3, z4, t) {
return [bez3(z1, z2, z3, z4, t), bez2(z2, z3, z4, t), mix(z3, z4, t), z4]; return [bez3(z1, z2, z3, z4, t), bez2(z2, z3, z4, t), bez1(z3, z4, t), z4];
} }
function splitAtExtrema(z1, z2, z3, z4, curve) { function splitAtExtrema(z1, z2, z3, z4, curve) {
const ts = fineAllExtrema(z1, z2, z3, z4); const ts = fineAllExtrema(z1, z2, z3, z4);
@ -109,41 +111,33 @@ function veryClose(z1, z2) {
return (z1.x - z2.x) * (z1.x - z2.x) + (z1.y - z2.y) * (z1.y - z2.y) <= SMALL; return (z1.x - z2.x) * (z1.x - z2.x) + (z1.y - z2.y) * (z1.y - z2.y) <= SMALL;
} }
function splitCurve(sourceCurve) { function toSpansForm(sourceCurve, fSplit) {
const curve = [sourceCurve[0]]; const curve = [sourceCurve[0]];
let last = sourceCurve[0]; let last = sourceCurve[0];
for (let j = 1; j < sourceCurve.length; j++) { for (let j = 1; j < sourceCurve.length; j++) {
if (sourceCurve[j].on) { if (sourceCurve[j].on) {
const z1 = last, const z1 = last,
z4 = sourceCurve[j]; z4 = sourceCurve[j];
// const z2 = mix(z1, z4, 1 / 3);
//const z3 = mix(z1, z4, 2 / 3);
if (!veryClose(z1, z4)) { if (!veryClose(z1, z4)) {
curve.push(z4); curve.push(z4);
// splitAtExtrema(z1, z2, z3, z4, ANGLES, curve);
last = z4; last = z4;
} }
} else if (sourceCurve[j].cubic) { } else if (sourceCurve[j].cubic) {
const z1 = last, const z1 = last,
z2 = sourceCurve[j], z2 = sourceCurve[j],
z3 = sourceCurve[j + 1], z3 = sourceCurve[(j + 1) % sourceCurve.length],
z4 = sourceCurve[j + 2]; z4 = sourceCurve[(j + 2) % sourceCurve.length];
if (!(veryClose(z1, z2) && veryClose(z2, z3) && veryClose(z3, z4))) { if (!(veryClose(z1, z2) && veryClose(z2, z3) && veryClose(z3, z4))) {
splitAtExtrema(z1, z2, z3, z4, curve); if (fSplit) {
splitAtExtrema(z1, z2, z3, z4, curve);
} else {
curve.push(z2, z3, z4);
}
last = z4; last = z4;
} }
j += 2; j += 2;
} else { } else {
const z1 = last, throw new Error("Unreachable.");
zm = sourceCurve[j],
z4 = sourceCurve[j + 1];
if (!(veryClose(z1, zm) && veryClose(zm, z4))) {
const z2 = mix(zm, z1, 1 / 3);
const z3 = mix(zm, z4, 1 / 3);
splitAtExtrema(z1, z2, z3, z4, curve);
last = z4;
}
j += 1;
} }
} }
return curve; return curve;
@ -157,35 +151,47 @@ function dot(z1, z2, z3) {
} }
function markCorners(curve) { function markCorners(curve) {
for (const z of curve) z.mark = 0;
for (let j = 0; j < curve.length; j++) { for (let j = 0; j < curve.length; j++) {
if (!curve[j].on) continue; if (!curve[j].on) continue;
const z1 = curve[j], const z1 = curve[j],
z0 = curve[(j - 1 + curve.length) % curve.length], z0 = curve[(j - 1 + curve.length) % curve.length],
z2 = curve[(j + 1) % curve.length]; z2 = curve[(j + 1) % curve.length];
if (Math.abs(cross(z1, z0, z2)) < 1e-6) {
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 // Z0 -- Z1 -- Z2 are linear
if (!z0.on && !z2.on && dot(z1, z0, z2) < 0) { const angle = Math.abs(Math.atan2(z2.y - z0.y, z2.x - z0.x)) % Math.PI;
const angle0 = Math.atan2(z2.y - z0.y, z2.x - z0.x); if (Math.abs(angle) <= SMALL || Math.abs(angle - Math.PI) <= SMALL) {
const angle = Math.abs(((angle0 / Math.PI) * 2) % 1); z1.mark = 4;
if ( } else if (Math.abs(angle - Math.PI / 2) <= SMALL) {
Math.abs(Math.abs(angle0) - Math.PI / 2) <= SMALL || z1.mark = 2;
angle <= SMALL ||
angle >= 1 - SMALL
) {
z1.mark = true; // curve extremum
}
} else if (z0.on && z2.on && dot(z1, z0, z2) < 0) {
// Colinear on-knots
// Remove
} else {
z1.mark = true; // also corner
} }
} else { } else {
z1.mark = true; // corner 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 { class BezierCurveCluster {
constructor(zs) { constructor(zs) {
let segments = []; let segments = [];
@ -195,9 +201,7 @@ class BezierCurveCluster {
if (zs[j].on) { if (zs[j].on) {
const z1 = last, const z1 = last,
z4 = zs[j]; z4 = zs[j];
const z2 = mix(z1, z4, 1 / 3); const seg = new typoGeom.Curve.StraightSegment(z1, z4);
const z3 = mix(z1, z4, 2 / 3);
const seg = new typoGeom.Curve.Bez3(z1, z2, z3, z4);
segments.push(seg); segments.push(seg);
lengths.push(this.measureLength(seg)); lengths.push(this.measureLength(seg));
last = z4; last = z4;
@ -212,16 +216,7 @@ class BezierCurveCluster {
last = z4; last = z4;
j += 2; j += 2;
} else { } else {
const z1 = last, throw new Error("Unreachable.");
zm = zs[j],
z4 = zs[j + 1];
const z2 = mix(zm, z1, 1 / 3);
const z3 = mix(zm, z4, 1 / 3);
const seg = new typoGeom.Curve.Bez3(z1, z2, z3, z4);
segments.push(seg);
lengths.push(this.measureLength(seg));
last = z4;
j += 1;
} }
} }
@ -281,7 +276,7 @@ class BezierCurveCluster {
return det < err * err && det > -err * err; return det < err * err && det > -err * err;
} }
isAlmostLinear(err) { isAlmostLinear(err) {
const N = 16; const N = 64;
let z0 = this.eval(0); let z0 = this.eval(0);
let z1 = this.eval(1); let z1 = this.eval(1);
for (let k = 1; k < N; k++) { for (let k = 1; k < N; k++) {
@ -292,60 +287,72 @@ class BezierCurveCluster {
} }
} }
function buildCurve(curve) { const QuadBuilder = {
let exitPoints = []; corner(sink, gizmo, z) {
sink.push(Transform.transformPoint(gizmo, Point.cornerFrom(z)).round(1024));
},
arc(sink, gizmo, 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(Transform.transformPoint(gizmo, Point.offFrom(z)).round(1024));
}
},
split: false,
canonicalStart: true,
duplicateStart: true
};
const SpiroBuilder = {
corner(sink, gizmo, z) {
sink.push(Transform.transformPoint(gizmo, Point.cornerFrom(z)));
},
arc(sink, gizmo, arc) {
if (arc.isAlmostLinear(1 / 4)) return;
const offPoints = curveUtil.autoCubify(arc, 1 / 4);
for (const z of offPoints) {
sink.push(Transform.transformPoint(gizmo, z));
}
},
split: true,
canonicalStart: false,
duplicateStart: false
};
function buildCurve(curve, gizmo, builder) {
let sink = [];
for (let j = 0; j < curve.length; j++) { for (let j = 0; j < curve.length; j++) {
if (!curve[j].mark) continue; if (!curve[j].mark) continue;
builder.corner(sink, gizmo, curve[j]);
let k = j; let k = j;
for (; k < curve.length && (k === j || !curve[k].mark); k++); for (; k < curve.length && (k === j || !curve[k].mark); k++);
exitPoints.push(curve[j]);
const pts = curve.slice(j, k + 1); const pts = curve.slice(j, k + 1);
let nPtsOffPoints = 0; if (pts.length > 1) builder.arc(sink, gizmo, new BezierCurveCluster(pts));
for (const z of pts) {
if (!z.on) nPtsOffPoints += 1;
}
if (nPtsOffPoints > 0) {
const curve = new BezierCurveCluster(pts);
if (curve.isAlmostLinear(1)) continue;
const offPoints = typoGeom.Quadify.auto(curve, 1 / 4);
if (!offPoints) continue;
for (let k = 0; k < offPoints.length; k++) {
const z = offPoints[k];
if (k > 0) {
const z0 = offPoints[k - 1];
exitPoints.push({
x: (z.x + z0.x) / 2,
y: (z.y + z0.y) / 2,
on: true
});
}
exitPoints.push({
x: z.x,
y: z.y,
cubic: false,
on: false
});
}
}
j = k - 1; j = k - 1;
} }
return exitPoints; return sink;
} }
module.exports = function(sourceCurve, gizmo) { function fairifyImpl(sourceCubicContour, gizmo, builder) {
for (let j = 0; j < sourceCurve.length; j++) { for (let j = 0; j < sourceCubicContour.length; j++) {
if (!isFinite(sourceCurve[j].x)) sourceCurve[j].x = 0; if (!isFinite(sourceCubicContour[j].x)) sourceCubicContour[j].x = 0;
if (!isFinite(sourceCurve[j].y)) sourceCurve[j].y = 0; if (!isFinite(sourceCubicContour[j].y)) sourceCubicContour[j].y = 0;
sourceCurve[j] = Transform.unTransform(gizmo, sourceCurve[j]); sourceCubicContour[j] = Transform.unTransform(gizmo, sourceCubicContour[j]);
} }
const curve = splitCurve(sourceCurve); let splitContour = toSpansForm(sourceCubicContour, builder.split);
markCorners(curve); markCorners(splitContour);
const builtCurve = buildCurve(curve); if (builder.canonicalStart) {
const ans = []; splitContour = canonicalStart(splitContour);
for (let j = 0; j < builtCurve.length; j++) { markCorners(splitContour);
if (builtCurve[j] && !builtCurve[j].remove) {
ans.push(Transform.transformPoint(gizmo, builtCurve[j]));
}
} }
return ans; return buildCurve(splitContour, gizmo, builder);
}
exports.fairifyQuad = function(sourceCubicContour, gizmo) {
return fairifyImpl(sourceCubicContour, gizmo, QuadBuilder);
};
exports.fairifySpiro = function(sourceCubicContour, gizmo) {
return fairifyImpl(sourceCubicContour, gizmo, SpiroBuilder);
}; };

View file

@ -6,6 +6,41 @@ export all : class Point
this.cubic = cubic || false this.cubic = cubic || false
this.subdivided = subdivided || false this.subdivided = subdivided || false
public [add z2] : new Point
this.x + z2.x
this.y + z2.y
* this.on
* this.cubic
* this.subdivided
public [addScale scale z2] : new Point
this.x + scale * z2.x
this.y + scale * z2.y
* this.on
* this.cubic
* this.subdivided
public [mix scale z2] : new Point
this.x + scale * (z2.x - this.x)
this.y + scale * (z2.y - this.y)
* this.on
* this.cubic
* this.subdivided
public [scale t] : new Point
t * this.x
t * this.y
* this.on
* this.cubic
* this.subdivided
public [round d] : new Point
[Math.round (this.x * d)] / d
[Math.round (this.y * d)] / d
* this.on
* this.cubic
* this.subdivided
static [from z on cubic subdivided] : new Point static [from z on cubic subdivided] : new Point
* z.x * z.x
* z.y * z.y

View file

@ -4,6 +4,8 @@ const Glyph = require("./glyph");
const autoRef = require("./autoref"); const autoRef = require("./autoref");
const caryllShapeOps = require("caryll-shapeops"); const caryllShapeOps = require("caryll-shapeops");
const curveUtil = require("./curve-util"); const curveUtil = require("./curve-util");
const { fairifyQuad } = require("./fairify");
const Transform = require("./transform");
function regulateGlyph(g, skew) { function regulateGlyph(g, skew) {
if (!g.contours) return; if (!g.contours) return;
@ -13,31 +15,11 @@ function regulateGlyph(g, skew) {
const contour = g.contours[k]; const contour = g.contours[k];
for (let p = 0; p < contour.length; p++) { for (let p = 0; p < contour.length; p++) {
contour[p].x += contour[p].y * skew; contour[p].x += contour[p].y * skew;
if (!contour[p].on) continue;
contour[p].x = Math.round(contour[p].x);
}
let offJ = null,
mx = null;
for (let p = 0; p < contour.length; p++) {
if (!contour[p].on) continue;
if (offJ) {
const origX = contour[p].x;
const rx = Math.round(contour[p].x * 4) / 4;
const origX0 = mx;
const rx0 = contour[offJ - 1].x;
if (origX === origX0) continue;
for (let pOff = offJ; pOff < p; pOff++) {
contour[pOff].x =
((contour[pOff].x - origX0) / (origX - origX0)) * (rx - rx0) + rx0;
}
}
mx = contour[p].x;
contour[p].x = Math.round(contour[p].x * 4) / 4;
offJ = p + 1;
} }
} }
g.contours = simplifyContours(g.contours); g.contours = simplifyContours(g.contours);
for (let k = 0; k < g.contours.length; k++) { for (let k = 0; k < g.contours.length; k++) {
const contour = g.contours[k]; const contour = g.contours[k];
for (let p = 0; p < contour.length; p++) { for (let p = 0; p < contour.length; p++) {
@ -47,16 +29,17 @@ function regulateGlyph(g, skew) {
} }
function simplifyContours(contours) { function simplifyContours(contours) {
const gizmo = Transform.Id();
const source = []; const source = [];
for (const contour of contours) { for (const contour of contours) {
if (contour.length > 2) source.push(curveUtil.convertContourToCubic(contour)); if (contour.length > 2) source.push(curveUtil.convertContourToCubic(contour));
} }
const simplified = curveUtil.convertContourListToTt( const simplified = caryllShapeOps.removeOverlap(source, 1, 1 << 17, true);
caryllShapeOps.removeOverlap(source, 1, 1 << 17, true)
);
const result = []; const result = [];
for (const contour of simplified) { for (const contour of simplified) {
if (contour.length > 2) result.push(contour); if (contour.length <= 2) continue;
result.push(curveUtil.cleanupQuadContour(fairifyQuad(contour, gizmo)));
} }
return result; return result;
} }

View file

@ -1,6 +1,6 @@
import 'spiro' as SpiroJs import 'spiro' as SpiroJs
import './spiroexpand' as SpiroExpansionContext import './spiroexpand' as SpiroExpansionContext
import './fairify' as fairify import [fairifySpiro] from './fairify'
import 'caryll-shapeops' as ShapeOps import 'caryll-shapeops' as ShapeOps
import './curve-util' as CurveUtil import './curve-util' as CurveUtil
@ -190,7 +190,7 @@ export : define [SetupBuilders args] : begin
lhs.0.type = rhs.0.type = lhs.(lhs.length - 1).type = rhs.(rhs.length - 1).type = 'corner' 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 SpiroJs.spiroToBezierOnContext [lhs.concat : rhs.reverse] true g QUAD PRECISION
foreach [j : range 0 g.contours.length] : begin foreach [j : range 0 g.contours.length] : begin
set g.contours.(j) : fairify g.contours.(j) globalTransform set g.contours.(j) : fairifySpiro g.contours.(j) globalTransform
set g.knots knots set g.knots knots
set g.lhsKnots lhs set g.lhsKnots lhs
set g.rhsKnots rhs set g.rhsKnots rhs
@ -204,7 +204,7 @@ export : define [SetupBuilders args] : begin
SpiroJs.spiroToBezierOnContext knots closed g QUAD PRECISION SpiroJs.spiroToBezierOnContext knots closed g QUAD PRECISION
foreach af [items-of lastafs] : if af : af.call g foreach af [items-of lastafs] : if af : af.call g
foreach [j : range 0 g.contours.length] : begin foreach [j : range 0 g.contours.length] : begin
set g.contours.(j) : fairify g.contours.(j) (g.fairGizmo || g.gizmo) set g.contours.(j) : fairifySpiro g.contours.(j) (g.fairGizmo || g.gizmo)
if [not dontinc] : this.include g if [not dontinc] : this.include g
return g return g

View file

@ -357,6 +357,12 @@ sampler = '0'
description = 'O-like `0`' description = 'O-like `0`'
zero = 'unslashed' zero = 'unslashed'
[simple.v-zero-reverse-slashed]
tag = 'cv93'
sampler = '0'
description = 'Reverse-slashed `0`'
zero = 'reverse-slashed'
[simple.v-one-nobase] [simple.v-one-nobase]
tag = "cv50" tag = "cv50"
sampler = '1' sampler = '1'