|
@ -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-dotted`, `cv14`: Dotted Zero `0`.
|
||||
* `v-zero-unslashed`, `cv15`: O-like `0`.
|
||||
* `v-zero-reverse-slashed`, `cv93`: Reverse-slashed `0`.
|
||||
* Styles for `1`:
|
||||
* `v-one-nobase`, `cv50`: `1` with bottom serif (default for Sans).
|
||||
* `v-one-base`, `cv51`: `1` without bottom serif (default for Slab).
|
||||
|
|
|
@ -4,7 +4,6 @@ import '../support/spirokit' as spirokit
|
|||
import '../support/transform' as : Transform && [object [transformPoint tp] [unTransform utp] inverse]
|
||||
import '../support/anchor' as Anchor
|
||||
import '../support/monotonic-interpolate' as smoothreg
|
||||
import '../support/fairify' as fairify
|
||||
|
||||
import [mix linreg clamp fallback TempFont includeGlyphPart compsiteMarkSet] from '../support/utils'
|
||||
import [calculateMetrics setFontMetrics MarksetDiv GenDivFrame] from '../meta/aesthetics'
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
$$include '../meta/macros.ptl'
|
||||
|
||||
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 [designParameters] from '../meta/aesthetics'
|
||||
|
|
|
@ -4211,13 +4211,11 @@ glyph-block LetterUnified-Basic : begin
|
|||
define tension : 0.46 - 0.005 * strokeCoeff + slantCoeff * TANSLANT
|
||||
return : alsothruthem {{0.4 tension} {0.6 (1 - tension)}} [widths.center stroke]
|
||||
|
||||
sketch # S
|
||||
set-width WIDTH
|
||||
include MarkSet.capital
|
||||
define [SStroke] : begin
|
||||
local stroke : adviceBlackness2 2.875 2 CAP
|
||||
local ess : ESS * stroke / STROKE
|
||||
define smooth : adviceSSmooth CAP (-1) stroke
|
||||
include : dispiro
|
||||
return : dispiro
|
||||
widths.lhs stroke
|
||||
g4 RIGHTSB (CAP - HOOK)
|
||||
hookstart CAPO (sw -- stroke) (swItalicAdj -- STROKE)
|
||||
|
@ -4226,34 +4224,12 @@ glyph-block LetterUnified-Basic : begin
|
|||
g4 RIGHTSB smooth [widths 0 stroke]
|
||||
hookend O (sw -- stroke) (swItalicAdj -- STROKE)
|
||||
g4 SB HOOK
|
||||
save 'S' 'S'
|
||||
save 'cyrDze' 0x405
|
||||
|
||||
sketch # s
|
||||
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
|
||||
define [RevSStroke] : begin
|
||||
local stroke : adviceBlackness2 2.875 2 CAP
|
||||
local ess : ESS * stroke / STROKE
|
||||
local smooth : adviceSSmooth CAP 0.5 stroke
|
||||
include : dispiro
|
||||
return : dispiro
|
||||
widths.rhs stroke
|
||||
g4 SB (CAP - HOOK)
|
||||
hookstart CAPO (sw -- stroke) (swItalicAdj -- STROKE)
|
||||
|
@ -4263,15 +4239,27 @@ glyph-block LetterUnified-Basic : begin
|
|||
hookend O (sw -- stroke) (swItalicAdj -- STROKE)
|
||||
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
|
||||
include MarkSet.e
|
||||
define [RevsStroke] : begin
|
||||
define stroke : adviceBlackness2 2.875 2 XH
|
||||
define ess : ESS * stroke / STROKE
|
||||
define strokeCoeff : linreg 18 0 126 1 stroke
|
||||
define smooth : adviceSSmooth XH 1 stroke
|
||||
include : dispiro
|
||||
return : dispiro
|
||||
widths.rhs stroke
|
||||
g4 SB (XH - SHOOK)
|
||||
hookstart XO (sw -- stroke) (swItalicAdj -- STROKE)
|
||||
|
@ -4281,43 +4269,73 @@ glyph-block LetterUnified-Basic : begin
|
|||
hookend O (sw -- stroke) (swItalicAdj -- STROKE)
|
||||
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 'cyrghe.italic'
|
||||
|
||||
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 glyphs.s
|
||||
include : VBarLeft (SB + OXHOOK) 0 SHOOK
|
||||
local stroke : [sStroke].call currentGlyph
|
||||
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
|
||||
|
||||
sketch # Sswash
|
||||
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
|
||||
widths.lhs [adviceBlackness 4.5]
|
||||
g4 (SB + OXHOOK) HOOK
|
||||
g4 start.x start.y
|
||||
alsothru 0.15 0.6 important
|
||||
flat (RIGHTSB - 1) DESCENDER [widths stroke 0]
|
||||
flat (RIGHTSB - 1) DESCENDER [widths sw 0]
|
||||
curl RIGHTSB DESCENDER
|
||||
|
||||
save 'Sswash' 0x2C7E
|
||||
|
||||
sketch # sswash
|
||||
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
|
||||
widths.lhs [adviceBlackness 4.5]
|
||||
g4 (SB + OXHOOK) SHOOK
|
||||
alsothru 0.2 0.6 important
|
||||
flat (RIGHTSB - 1) DESCENDER [widths stroke 0]
|
||||
g4 start.x start.y
|
||||
alsothru 0.15 0.6 important
|
||||
flat (RIGHTSB - 1) DESCENDER [widths sw 0]
|
||||
curl RIGHTSB DESCENDER
|
||||
save 'sswash' 0x23F
|
||||
|
||||
|
|
|
@ -24,6 +24,14 @@ glyph-block Numbers : begin
|
|||
flat 0 (top * (1 - 0.77)) [widths fine fine]
|
||||
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
|
||||
local radius : Math.min DOTRADIUS ((RIGHTSB - SB - STROKE * 2) / 4)
|
||||
return : Ring (top / 2 + radius) (top / 2 - radius) (MIDDLE + radius) (MIDDLE - radius)
|
||||
|
@ -39,6 +47,12 @@ glyph-block Numbers : begin
|
|||
include : ZeroSlashShape CAP
|
||||
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
|
||||
include MarkSet.capital
|
||||
include glyphs.'zero.lnum.unslashed'
|
||||
|
@ -56,6 +70,12 @@ glyph-block Numbers : begin
|
|||
include : ZeroSlashShape XH
|
||||
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
|
||||
include MarkSet.e
|
||||
include glyphs.'zero.onum.unslashed'
|
||||
|
|
Before Width: | Height: | Size: 171 KiB After Width: | Height: | Size: 173 KiB |
Before Width: | Height: | Size: 396 KiB After Width: | Height: | Size: 398 KiB |
Before Width: | Height: | Size: 338 KiB After Width: | Height: | Size: 337 KiB |
Before Width: | Height: | Size: 174 KiB After Width: | Height: | Size: 174 KiB |
Before Width: | Height: | Size: 336 KiB After Width: | Height: | Size: 337 KiB |
Before Width: | Height: | Size: 960 KiB After Width: | Height: | Size: 959 KiB |
Before Width: | Height: | Size: 319 KiB After Width: | Height: | Size: 319 KiB |
|
@ -394,6 +394,11 @@
|
|||
<span class="sample" style="font-feature-settings:'cv15'">0</span>
|
||||
<span class="sample italic" style="font-feature-settings:'cv15'">0</span>
|
||||
</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>
|
||||
<li><ol class="group">
|
||||
<li>
|
||||
|
|
|
@ -92,43 +92,49 @@ function removeMids(contour) {
|
|||
contour = contour.filter(x => !x.rem);
|
||||
|
||||
last = contour.length - 1;
|
||||
for (let j = 1; j < contour.length - 1; j++) {
|
||||
if (!contour[j - 1].on && contour[j].on && !contour[j + 1].on) {
|
||||
const mx = contour[j - 1].x + contour[j + 1].x;
|
||||
const my = contour[j - 1].y + contour[j + 1].y;
|
||||
const dy = contour[j - 1].y - contour[j + 1].y;
|
||||
if (
|
||||
Math.abs(dy) >= 1 &&
|
||||
Math.abs(contour[j].x * 2 - mx) < 1 &&
|
||||
Math.abs(contour[j].y * 2 - my) < 1
|
||||
) {
|
||||
contour[j].rem = true;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
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);
|
||||
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().map(z => {
|
||||
z.x = Math.round(z.x * 1024) / 1024;
|
||||
z.y = Math.round(z.y * 1024) / 1024;
|
||||
return z;
|
||||
});
|
||||
const points = _points.reverse();
|
||||
let jm = 0;
|
||||
for (var j = 0; j < points.length * 2; j++) {
|
||||
if (extPrior(points[j % points.length], points[jm])) {
|
||||
|
@ -138,93 +144,8 @@ function canonicalStart(_points) {
|
|||
return points.slice(jm).concat(points.slice(0, jm));
|
||||
}
|
||||
|
||||
function colinear(x1, y1, x2, y2, x3, y3, err) {
|
||||
const det = x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2);
|
||||
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 cleanupQuadContour(c) {
|
||||
return canonicalStart(removeMids(c));
|
||||
}
|
||||
|
||||
function convertContourToCubic(contour) {
|
||||
|
@ -243,34 +164,67 @@ function convertContourToCubic(contour) {
|
|||
const z1 = z;
|
||||
const z2 = contour[j + 1];
|
||||
const z3 = contour[j + 2];
|
||||
|
||||
newContour.push(Point.cubicOffFrom(z1));
|
||||
newContour.push(Point.cubicOffFrom(z2));
|
||||
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));
|
||||
}
|
||||
let zf = contour[j + 1] || contour[0];
|
||||
const zfIsCorner = zf.on;
|
||||
if (!zfIsCorner) zf = Point.cornerFrom(zc).mix(0.5, zf);
|
||||
|
||||
const x1 = mix(z0.x, zc.x, 2 / 3);
|
||||
const y1 = mix(z0.y, zc.y, 2 / 3);
|
||||
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.cubicOffFrom(z0).mix(2 / 3, zc));
|
||||
newContour.push(Point.cubicOffFrom(zf).mix(2 / 3, zc));
|
||||
newContour.push(Point.cornerFrom(zf));
|
||||
|
||||
z0 = zf;
|
||||
j++;
|
||||
if (zfIsCorner) j++;
|
||||
}
|
||||
}
|
||||
|
||||
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.convertContourListToTt = convertContourListToTt;
|
||||
exports.autoCubify = autoCubify;
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
"use strict";
|
||||
|
||||
const Transform = require("./transform.js");
|
||||
const Transform = require("./transform");
|
||||
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) {
|
||||
const delta = b * b - 4 * a * c;
|
||||
|
@ -43,7 +45,7 @@ function fineAllExtrema(z1, z2, z3, z4) {
|
|||
|
||||
return exs.sort(ASCEND);
|
||||
}
|
||||
function mix(z1, z2, t) {
|
||||
function bez1(z1, z2, t) {
|
||||
if (t <= 0) return z1;
|
||||
if (t >= 1) return z2;
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
|
||||
function splitCurve(sourceCurve) {
|
||||
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];
|
||||
// const z2 = mix(z1, z4, 1 / 3);
|
||||
//const z3 = mix(z1, z4, 2 / 3);
|
||||
if (!veryClose(z1, z4)) {
|
||||
curve.push(z4);
|
||||
// splitAtExtrema(z1, z2, z3, z4, ANGLES, curve);
|
||||
last = z4;
|
||||
}
|
||||
} else if (sourceCurve[j].cubic) {
|
||||
const z1 = last,
|
||||
z2 = sourceCurve[j],
|
||||
z3 = sourceCurve[j + 1],
|
||||
z4 = sourceCurve[j + 2];
|
||||
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 {
|
||||
const z1 = last,
|
||||
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;
|
||||
throw new Error("Unreachable.");
|
||||
}
|
||||
}
|
||||
return curve;
|
||||
|
@ -157,35 +151,47 @@ function dot(z1, z2, z3) {
|
|||
}
|
||||
|
||||
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];
|
||||
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
|
||||
if (!z0.on && !z2.on && dot(z1, z0, z2) < 0) {
|
||||
const angle0 = Math.atan2(z2.y - z0.y, z2.x - z0.x);
|
||||
const angle = Math.abs(((angle0 / Math.PI) * 2) % 1);
|
||||
if (
|
||||
Math.abs(Math.abs(angle0) - Math.PI / 2) <= SMALL ||
|
||||
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
|
||||
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 = 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 {
|
||||
constructor(zs) {
|
||||
let segments = [];
|
||||
|
@ -195,9 +201,7 @@ class BezierCurveCluster {
|
|||
if (zs[j].on) {
|
||||
const z1 = last,
|
||||
z4 = zs[j];
|
||||
const z2 = mix(z1, z4, 1 / 3);
|
||||
const z3 = mix(z1, z4, 2 / 3);
|
||||
const seg = new typoGeom.Curve.Bez3(z1, z2, z3, z4);
|
||||
const seg = new typoGeom.Curve.StraightSegment(z1, z4);
|
||||
segments.push(seg);
|
||||
lengths.push(this.measureLength(seg));
|
||||
last = z4;
|
||||
|
@ -212,16 +216,7 @@ class BezierCurveCluster {
|
|||
last = z4;
|
||||
j += 2;
|
||||
} else {
|
||||
const z1 = last,
|
||||
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;
|
||||
throw new Error("Unreachable.");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -281,7 +276,7 @@ class BezierCurveCluster {
|
|||
return det < err * err && det > -err * err;
|
||||
}
|
||||
isAlmostLinear(err) {
|
||||
const N = 16;
|
||||
const N = 64;
|
||||
let z0 = this.eval(0);
|
||||
let z1 = this.eval(1);
|
||||
for (let k = 1; k < N; k++) {
|
||||
|
@ -292,60 +287,72 @@ class BezierCurveCluster {
|
|||
}
|
||||
}
|
||||
|
||||
function buildCurve(curve) {
|
||||
let exitPoints = [];
|
||||
const QuadBuilder = {
|
||||
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++) {
|
||||
if (!curve[j].mark) continue;
|
||||
builder.corner(sink, gizmo, curve[j]);
|
||||
|
||||
let k = j;
|
||||
for (; k < curve.length && (k === j || !curve[k].mark); k++);
|
||||
exitPoints.push(curve[j]);
|
||||
const pts = curve.slice(j, k + 1);
|
||||
let nPtsOffPoints = 0;
|
||||
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
|
||||
});
|
||||
}
|
||||
}
|
||||
if (pts.length > 1) builder.arc(sink, gizmo, new BezierCurveCluster(pts));
|
||||
j = k - 1;
|
||||
}
|
||||
return exitPoints;
|
||||
return sink;
|
||||
}
|
||||
|
||||
module.exports = function(sourceCurve, gizmo) {
|
||||
for (let j = 0; j < sourceCurve.length; j++) {
|
||||
if (!isFinite(sourceCurve[j].x)) sourceCurve[j].x = 0;
|
||||
if (!isFinite(sourceCurve[j].y)) sourceCurve[j].y = 0;
|
||||
sourceCurve[j] = Transform.unTransform(gizmo, sourceCurve[j]);
|
||||
function fairifyImpl(sourceCubicContour, gizmo, builder) {
|
||||
for (let j = 0; j < sourceCubicContour.length; j++) {
|
||||
if (!isFinite(sourceCubicContour[j].x)) sourceCubicContour[j].x = 0;
|
||||
if (!isFinite(sourceCubicContour[j].y)) sourceCubicContour[j].y = 0;
|
||||
sourceCubicContour[j] = Transform.unTransform(gizmo, sourceCubicContour[j]);
|
||||
}
|
||||
const curve = splitCurve(sourceCurve);
|
||||
markCorners(curve);
|
||||
const builtCurve = buildCurve(curve);
|
||||
const ans = [];
|
||||
for (let j = 0; j < builtCurve.length; j++) {
|
||||
if (builtCurve[j] && !builtCurve[j].remove) {
|
||||
ans.push(Transform.transformPoint(gizmo, builtCurve[j]));
|
||||
let splitContour = toSpansForm(sourceCubicContour, builder.split);
|
||||
markCorners(splitContour);
|
||||
if (builder.canonicalStart) {
|
||||
splitContour = canonicalStart(splitContour);
|
||||
markCorners(splitContour);
|
||||
}
|
||||
return buildCurve(splitContour, gizmo, builder);
|
||||
}
|
||||
return ans;
|
||||
|
||||
exports.fairifyQuad = function(sourceCubicContour, gizmo) {
|
||||
return fairifyImpl(sourceCubicContour, gizmo, QuadBuilder);
|
||||
};
|
||||
exports.fairifySpiro = function(sourceCubicContour, gizmo) {
|
||||
return fairifyImpl(sourceCubicContour, gizmo, SpiroBuilder);
|
||||
};
|
||||
|
|
|
@ -6,6 +6,41 @@ export all : class Point
|
|||
this.cubic = cubic || 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
|
||||
* z.x
|
||||
* z.y
|
||||
|
|
|
@ -4,6 +4,8 @@ const Glyph = require("./glyph");
|
|||
const autoRef = require("./autoref");
|
||||
const caryllShapeOps = require("caryll-shapeops");
|
||||
const curveUtil = require("./curve-util");
|
||||
const { fairifyQuad } = require("./fairify");
|
||||
const Transform = require("./transform");
|
||||
|
||||
function regulateGlyph(g, skew) {
|
||||
if (!g.contours) return;
|
||||
|
@ -13,31 +15,11 @@ function regulateGlyph(g, skew) {
|
|||
const contour = g.contours[k];
|
||||
for (let p = 0; p < contour.length; p++) {
|
||||
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);
|
||||
|
||||
for (let k = 0; k < g.contours.length; k++) {
|
||||
const contour = g.contours[k];
|
||||
for (let p = 0; p < contour.length; p++) {
|
||||
|
@ -47,16 +29,17 @@ function regulateGlyph(g, skew) {
|
|||
}
|
||||
|
||||
function simplifyContours(contours) {
|
||||
const gizmo = Transform.Id();
|
||||
const source = [];
|
||||
for (const contour of contours) {
|
||||
if (contour.length > 2) source.push(curveUtil.convertContourToCubic(contour));
|
||||
}
|
||||
const simplified = curveUtil.convertContourListToTt(
|
||||
caryllShapeOps.removeOverlap(source, 1, 1 << 17, true)
|
||||
);
|
||||
const simplified = caryllShapeOps.removeOverlap(source, 1, 1 << 17, true);
|
||||
|
||||
const result = [];
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import 'spiro' as SpiroJs
|
||||
import './spiroexpand' as SpiroExpansionContext
|
||||
import './fairify' as fairify
|
||||
import [fairifySpiro] from './fairify'
|
||||
import 'caryll-shapeops' as ShapeOps
|
||||
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'
|
||||
SpiroJs.spiroToBezierOnContext [lhs.concat : rhs.reverse] true g QUAD PRECISION
|
||||
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.lhsKnots lhs
|
||||
set g.rhsKnots rhs
|
||||
|
@ -204,7 +204,7 @@ export : define [SetupBuilders args] : begin
|
|||
SpiroJs.spiroToBezierOnContext knots closed g QUAD PRECISION
|
||||
foreach af [items-of lastafs] : if af : af.call g
|
||||
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
|
||||
return g
|
||||
|
||||
|
|
|
@ -357,6 +357,12 @@ sampler = '0'
|
|||
description = 'O-like `0`'
|
||||
zero = 'unslashed'
|
||||
|
||||
[simple.v-zero-reverse-slashed]
|
||||
tag = 'cv93'
|
||||
sampler = '0'
|
||||
description = 'Reverse-slashed `0`'
|
||||
zero = 'reverse-slashed'
|
||||
|
||||
[simple.v-one-nobase]
|
||||
tag = "cv50"
|
||||
sampler = '1'
|
||||
|
|