Add hollow letters and digits (#2189)

This commit is contained in:
be5invis 2024-02-02 19:11:16 -08:00
parent 8e7bd0a0bc
commit cfac37ddf0
9 changed files with 318 additions and 65 deletions

10
package-lock.json generated
View file

@ -3665,9 +3665,9 @@
}
},
"node_modules/typo-geom": {
"version": "0.13.2",
"resolved": "https://registry.npmjs.org/typo-geom/-/typo-geom-0.13.2.tgz",
"integrity": "sha512-0xEeNX/bQl/qx1+jgMy7ObtyUK9SmdhZALCNTs2dHyTTCCpHRNHL1nPw+Us0ZmxbLRi9gy5GpINJ3tynE8K6Pw==",
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/typo-geom/-/typo-geom-0.14.0.tgz",
"integrity": "sha512-h3KmTEdKHrD+VWrR/Oqfr/NAPyTNaEKqhcIMhpbotNiHyXTrv113NCY//o0sUswwDEpHcTxcagxdG3dm/J9hlA==",
"dependencies": {
"clipper-lib": "^6.4.2",
"tslib": "^2.6.2"
@ -3994,7 +3994,7 @@
"@iosevka/geometry-cache": "28.0.7",
"@iosevka/glyph": "28.0.7",
"@iosevka/util": "28.0.7",
"typo-geom": "^0.13.2"
"typo-geom": "^0.14.0"
}
},
"packages/font-kits": {
@ -4021,7 +4021,7 @@
"dependencies": {
"@iosevka/util": "28.0.7",
"spiro": "^3.0.0",
"typo-geom": "^0.13.2"
"typo-geom": "^0.14.0"
}
},
"packages/geometry-cache": {

View file

@ -13,6 +13,6 @@
"@iosevka/geometry-cache": "28.0.7",
"@iosevka/glyph": "28.0.7",
"@iosevka/util": "28.0.7",
"typo-geom": "^0.13.2"
"typo-geom": "^0.14.0"
}
}

View file

@ -4,6 +4,7 @@ $$include '../meta/macros.ptl'
import [linreg clamp mix fallback] from "@iosevka/util"
import [getGrTree IsSuperscript IsSubscript AnyCv DotlessOrNot] from "@iosevka/glyph/relation"
import [AnyLocalizedForm CvDecompose MathSansSerif Texture] from "@iosevka/glyph/relation"
import [BooleanGeometry StrokeGeometry] from "@iosevka/geometry"
import [NumeratorForm DenominatorForm] from "@iosevka/glyph/relation"
import [Transform] from "@iosevka/geometry/transform"
extern Map
@ -775,14 +776,13 @@ glyph-block Autobuild-Transformed-Texture : begin
createTextureDerivatives Texture.ShrR 0 SHRINK [jobs 0xF400]
createTextureDerivatives Texture.ShrLR SHRINK SHRINK [jobs 0xF500]
glyph-block Autobuild-Transformed-Mathematical : begin
glyph-block-import CommonShapes
glyph-block-import Common-Derivatives
glyph-block-import Recursive-Build : Fork
glyph-block-import Autobuild-Transformed-Shared : extendRelatedGlyphs link-relations wrapName
define [createMathDerivedSeriesImpl groupName tfm _records] : begin
define [createMathDerivedSeriesImpl groupName tfm _records postProcessing] : begin
local { records relSets targetNameMap } : extendRelatedGlyphs groupName _records
local pendingGlyphs : records.map : [record] => record.1
local forkedPara : para.createFork tfm
@ -793,6 +793,7 @@ glyph-block Autobuild-Transformed-Mathematical : begin
if [not glyphT] : console.log glyphid
include glyphT AS_BASE ALSO_METRICS
set currentGlyph.gizmo glyphT.gizmo
if postProcessing : include : postProcessing para forkedPara
link-relations relSets
@ -822,7 +823,7 @@ glyph-block Autobuild-Transformed-Mathematical : begin
define Greek2 : Array.from 'Ϝϝ'
define ObliqueBlackboardBolds : Array.from '𝔻𝕕𝕖𝕚𝕛'
define [CreateMathDerivatives groupName tfm gr base letters overrides] : begin
define [CreateMathDerivatives groupName tfm gr base letters overrides postProcessing] : begin
local jobs {}
local overrideMap : new Map (overrides || {})
foreach j [range 0 letters.length] : begin
@ -832,7 +833,7 @@ glyph-block Autobuild-Transformed-Mathematical : begin
local dst : base + j
if [overrideMap.has letter] : set dst [overrideMap.get letter]
if source : jobs.push { dst source }
createMathDerivedSeriesImpl groupName tfm jobs
createMathDerivedSeriesImpl groupName tfm jobs postProcessing
define [CreateMathAliasableImpl groupName altGroupName tfm gr base letters overrides] : begin
local overrideMap : new Map (overrides || {})
@ -902,6 +903,16 @@ glyph-block Autobuild-Transformed-Mathematical : begin
# Italic blackboard bold
CreateMathDerivatives 'mathit' tfItalic null 0x2145 ObliqueBlackboardBolds
# Outlined letters and digits -- for Symbols for Legacy Computing Supplement
define [TfOutline para forkedPara] : glyph-proc
local g currentGlyph.geometry
local sw : forkedPara.stroke / 4
local gizmo : currentGlyph.gizmo || GlobalTransform
set currentGlyph.geometry : new StrokeGeometry g gizmo sw HVContrast true
CreateMathDerivatives 'legacyComputingOutlined' tfBold null 0x1CCD6 UpperLatin null TfOutline
CreateMathDerivatives 'legacyComputingOutlined' tfBold null 0x1CCF0 Digits null TfOutline
glyph-block Autobuild-Rhotic : begin
glyph-block-import Mark-Shared-Metrics : markFine markstroke
glyph-block-import CommonShapes

View file

@ -4,7 +4,7 @@ import zlib from "zlib";
import * as CurveUtil from "@iosevka/geometry/curve-util";
import { encode, decode } from "@msgpack/msgpack";
const Edition = 32;
const Edition = 33;
const MAX_AGE = 16;
class GfEntry {
constructor(age, value) {

View file

@ -15,6 +15,6 @@
"dependencies": {
"@iosevka/util": "28.0.7",
"spiro": "^3.0.0",
"typo-geom": "^0.13.2"
"typo-geom": "^0.14.0"
}
}

View file

@ -1,7 +1,8 @@
import * as TypoGeom from "typo-geom";
import { Point } from "./point.mjs";
import { Point, Vec2 } from "./point.mjs";
import { Transform } from "./transform.mjs";
import { mix } from "@iosevka/util";
function contourToRep(contour) {
let c = [];
@ -71,6 +72,26 @@ export const OCCURRENT_PRECISION = 1 / 16;
export const GEOMETRY_PRECISION = 1 / 4;
export const BOOLE_RESOLUTION = 0x4000;
export function derivativeFromFiniteDifference(c, t) {
const DELTA = 1 / 0x10000;
const forward2 = c.eval(t + 2 * DELTA);
const forward1 = c.eval(t + DELTA);
const backward1 = c.eval(t - DELTA);
const backward2 = c.eval(t - 2 * DELTA);
return new Vec2(
((1 / 12) * backward2.x -
(2 / 3) * backward1.x +
(2 / 3) * forward1.x -
(1 / 12) * forward2.x) /
DELTA,
((1 / 12) * backward2.y -
(2 / 3) * backward1.y +
(2 / 3) * forward1.y -
(1 / 12) * forward2.y) /
DELTA
);
}
export class OffsetCurve {
constructor(bone, offset, contrast) {
this.bone = bone;
@ -87,25 +108,7 @@ export class OffsetCurve {
};
}
derivative(t) {
const DELTA = 1 / 0x10000;
const forward = this.eval(t + DELTA);
const backward = this.eval(t - DELTA);
return {
x: (forward.x - backward.x) / (2 * DELTA),
y: (forward.y - backward.y) / (2 * DELTA)
};
}
}
export class ReverseCurve {
constructor(original) {
this.m_original = original;
}
eval(t) {
return this.m_original.eval(1 - t);
}
derivative(t) {
return -this.m_original.derivative(1 - t);
return derivativeFromFiniteDifference(this, t);
}
}
@ -149,3 +152,49 @@ export class BezToContoursSink {
this.lastContour.push(Point.transformedXY(this.gizmo, Point.Type.Corner, x, y));
}
}
export function Bez3FromHermite(zStart, dStart, zEnd, dEnd) {
const a = zStart,
d = zEnd;
const b = new Vec2(a.x + dStart.x / 3, a.y + dStart.y / 3);
const c = new Vec2(d.x - dEnd.x / 3, d.y - dEnd.y / 3);
return new TypoGeom.Arcs.Bez3(a, b, c, d);
}
export class RoundCapCurve {
constructor(side, contrast, center0, point0, center1, point1) {
this.contrast = contrast;
this.center0 = center0;
this.center1 = center1;
const theta0 = Math.atan2(point0.y - center0.y, (point0.x - center0.x) / contrast);
let theta1 = Math.atan2(point1.y - center1.y, (point1.x - center1.x) / contrast);
if (side) {
while (theta1 < theta0) theta1 += 2 * Math.PI;
} else {
while (theta1 > theta0) theta1 -= 2 * Math.PI;
}
this.theta0 = theta0;
this.theta1 = theta1;
this.r0 = Math.hypot(center0.y - point0.y, (center0.x - point0.x) / contrast);
this.r1 = Math.hypot(center1.y - point1.y, (center1.x - point1.x) / contrast);
}
eval(t) {
const centerX = mix(this.center0.x, this.center1.x, t);
const centerY = mix(this.center0.y, this.center1.y, t);
const r = mix(this.r0, this.r1, t);
const theta = mix(this.theta0, this.theta1, t);
return {
x: centerX + r * Math.cos(theta) * this.contrast,
y: centerY + r * Math.sin(theta)
};
}
derivative(t) {
// TODO: calculate an exact form instead of using finite difference
return derivativeFromFiniteDifference(this, t);
}
}

View file

@ -9,6 +9,7 @@ import { Point } from "./point.mjs";
import { QuadifySink } from "./quadify.mjs";
import { SpiroExpander } from "./spiro-expand.mjs";
import { Transform } from "./transform.mjs";
import { strokeArcs } from "./stroke.mjs";
export const CPLX_NON_EMPTY = 0x01; // A geometry tree that is not empty
export const CPLX_NON_SIMPLE = 0x02; // A geometry tree that contains non-simple contours
@ -533,6 +534,83 @@ export class BooleanGeometry extends GeometryBase {
}
}
export class StrokeGeometry extends GeometryBase {
constructor(geom, gizmo, radius, contrast, fInside) {
super();
this.m_geom = geom;
this.m_gizmo = gizmo;
this.m_radius = radius;
this.m_contrast = contrast;
this.m_fInside = fInside;
}
asContours() {
// Produce simplified arcs
const nonTransformedGeometry = new TransformedGeometry(this.m_geom, this.m_gizmo.inverse());
let arcs = TypoGeom.Boolean.removeOverlap(
CurveUtil.convertShapeToArcs(nonTransformedGeometry.asContours()),
TypoGeom.Boolean.PolyFillType.pftNonZero,
CurveUtil.BOOLE_RESOLUTION
);
// Fairize to get get some arcs that are simple enough
const fairizedArcs = TypoGeom.Fairize.fairizeBezierShape(arcs);
// Stroke the arcs
const strokedArcs = strokeArcs(
fairizedArcs,
this.m_radius,
this.m_contrast,
this.m_fInside
);
// Convert to Iosevka format
let sink = new CurveUtil.BezToContoursSink(this.m_gizmo);
TypoGeom.ShapeConv.transferBezArcShape(strokedArcs, sink, CurveUtil.GEOMETRY_PRECISION);
return sink.contours;
}
asReferences() {
return null;
}
getDependencies() {
return this.m_geom.getDependencies();
}
unlinkReferences() {
return new StrokeGeometry(
this.m_geom.unlinkReferences(),
this.m_gizmo,
this.m_radius,
this.m_contrast,
this.m_fInside
);
}
filterTag(fn) {
return new StrokeGeometry(
this.m_geom.filterTag(fn),
this.m_gizmo,
this.m_radius,
this.m_contrast,
this.m_fInside
);
}
measureComplexity() {
return this.m_geom.measureComplexity() | CPLX_NON_SIMPLE;
}
toShapeStringOrNull() {
const sTarget = this.m_geom.unlinkReferences().toShapeStringOrNull();
if (!sTarget) return null;
return Format.struct(
`StrokeGeometry`,
sTarget,
Format.gizmo(this.m_gizmo),
Format.n(this.m_radius),
Format.n(this.m_contrast),
this.m_fInside
);
}
}
// This special geometry type is used in the finalization phase to create TTF contours.
export class SimplifyGeometry extends GeometryBase {
constructor(g) {

View file

@ -0,0 +1,114 @@
import * as TypoGeom from "typo-geom";
import {
BOOLE_RESOLUTION,
Bez3FromHermite,
GEOMETRY_PRECISION,
OCCURRENT_PRECISION,
OffsetCurve,
RoundCapCurve
} from "./curve-util.mjs";
export function strokeArcs(arcs, radius, contrast, fInside) {
let currentArcs = null;
for (const contour of arcs) {
let leftSide = offsetContour(contour, -radius, contrast);
let rightSide = offsetContour(contour, radius, contrast);
let bezs = TypoGeom.ShapeConv.convertShapeToBez3([leftSide, rightSide], GEOMETRY_PRECISION);
if (!currentArcs) {
currentArcs = bezs;
} else {
currentArcs = TypoGeom.Boolean.combine(
TypoGeom.Boolean.ClipType.ctUnion,
currentArcs,
bezs,
TypoGeom.Boolean.PolyFillType.pftNonZero,
TypoGeom.Boolean.PolyFillType.pftNonZero,
BOOLE_RESOLUTION
);
}
}
if (currentArcs) {
if (fInside) {
return TypoGeom.Boolean.combine(
TypoGeom.Boolean.ClipType.ctIntersection,
TypoGeom.ShapeConv.convertShapeToBez3(arcs, GEOMETRY_PRECISION),
currentArcs,
TypoGeom.Boolean.PolyFillType.pftNonZero,
TypoGeom.Boolean.PolyFillType.pftNonZero,
BOOLE_RESOLUTION
);
} else {
return currentArcs;
}
} else {
return [];
}
}
function offsetContour(arcs, distance, contrast) {
// The arcs here are guaranteed to be simple, i.e. no self-intersections.
const fReverse = distance < 0;
let offsetArcs = [];
let prevOffsetedArc = new OffsetCurve(arcs[arcs.length - 1], distance, contrast);
for (let i = 0; i < arcs.length; i++) {
const current = arcs[i];
const currentOffsetedArc = new OffsetCurve(current, distance, contrast);
// Evaluate the previous' end and the current's start, determine whether they are close enough
const prevEnd = prevOffsetedArc.eval(1);
const currentStart = currentOffsetedArc.eval(0);
if (
Math.abs(prevEnd.x - currentStart.x) > OCCURRENT_PRECISION ||
Math.abs(prevEnd.y - currentStart.y) > OCCURRENT_PRECISION
) {
offsetArcs.push(
createCap(
distance < 0,
contrast,
prevOffsetedArc.bone.eval(1),
prevEnd,
prevOffsetedArc.derivative(1),
currentOffsetedArc.bone.eval(0),
currentStart,
currentOffsetedArc.derivative(0)
)
);
// offsetArcs.push(Bez3FromHermite(prevEnd, dPrevEnd, currentStart, dCurrentStart));
}
// Push the current arc
offsetArcs.push(currentOffsetedArc);
prevOffsetedArc = currentOffsetedArc;
}
if (fReverse) {
offsetArcs.reverse();
for (let i = 0; i < offsetArcs.length; i++) {
offsetArcs[i] = new TypoGeom.Arcs.Reverted(offsetArcs[i]);
}
}
return offsetArcs;
}
function createCap(
side,
contrast,
prevEndNoOffset, // Previous non-offseted curve's end point
prevEnd, // Previous offseted curve's end point
dPrevEnd, // Previous offseted curve's end point's derivative
currentStartNoOffset, // Current non-offseted curve's start point
currentStart, // Current offseted curve's start point
dCurrentStart // Current offseted curve's start point's derivative
) {
return new RoundCapCurve(
side,
contrast,
prevEndNoOffset,
prevEnd,
currentStartNoOffset,
currentStart
);
}

View file

@ -4,7 +4,8 @@ export async function collectBlockData() {
const BlockData = [
[[0xe0a0, 0xe0df], "Private Use Area — Powerline"],
[[0xee00, 0xee0f], "Private Use Area — Progress Bar"],
[[0xef10, 0xef1f], "Private Use Area — Iosevka Private Dingbats"]
[[0xef10, 0xef1f], "Private Use Area — Iosevka Private Dingbats"],
[[0x1cc00, 0x1ceaf], "Symbols for Legacy Computing Supplement"]
];
for (const id of UnicodeDataIndex.Block) {