Add hollow letters and digits (#2189)
This commit is contained in:
parent
8e7bd0a0bc
commit
cfac37ddf0
9 changed files with 318 additions and 65 deletions
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
114
packages/geometry/src/stroke.mjs
Normal file
114
packages/geometry/src/stroke.mjs
Normal 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
|
||||
);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue