fix: increase tree glyph stroke weight
Reference site / build (push) Canceled after 0s

This commit is contained in:
2026-10-11 01:15:32 +09:00
parent ef5ea5e528
commit 6897b2368a
4 changed files with 45 additions and 14 deletions
+4 -1
View File
@@ -90,7 +90,10 @@ It reads `public/fonts/LythMono-Regular.ttf` from the site working directory,
extracts box glyphs directly through cmap (no programming ligatures or GSUB extracts box glyphs directly through cmap (no programming ligatures or GSUB
shaping), and normalizes the vertical cell to the 24px tree row. Direct glyph shaping), and normalizes the vertical cell to the 24px tree row. Direct glyph
lookup also avoids Lyth Mono's contextual GSUB lookups that OpenType.js 2.0.0 lookup also avoids Lyth Mono's contextual GSUB lookups that OpenType.js 2.0.0
does not support. The 0.604em advance and glyph stroke width are retained. does not support. The 0.604em advance and connection centers are retained;
tree stroke edges are expanded symmetrically by 25% for legibility (the resting
vertical stem is 1.33px). Cell boundaries stay fixed so neighboring glyphs join.
The distributed font binaries and ordinary text weights are unchanged.
The resulting SVG paths are emitted into static HTML; neither the parser nor The resulting SVG paths are emitted into static HTML; neither the parser nor
the TTF is fetched by the browser. Anime.js controls the shared tree transform the TTF is fetched by the browser. Anime.js controls the shared tree transform
+23 -7
View File
@@ -12,10 +12,22 @@ export const cellWidth = font.charToGlyph('0').advanceWidth * fontSize / font.un
const vertical = font.charToGlyph('│').getPath(0, 0, fontSize); const vertical = font.charToGlyph('│').getPath(0, 0, fontSize);
const bounds = vertical.getBoundingBox(); const bounds = vertical.getBoundingBox();
const yScale = rowHeight / (bounds.y2 - bounds.y1); const yScale = rowHeight / (bounds.y2 - bounds.y1);
export const stemWidth = bounds.x2 - bounds.x1; // Increase only tree ink, not text size, cell advance or connection positions.
const treeWeight = 1.25;
const nativeStemWidth = bounds.x2 - bounds.x1;
export const stemWidth = nativeStemWidth * treeWeight;
const horizontal = font.charToGlyph('─').getPath(0, 0, fontSize).getBoundingBox(); const horizontal = font.charToGlyph('─').getPath(0, 0, fontSize).getBoundingBox();
export const branchY = ((horizontal.y1 + horizontal.y2) / 2 - bounds.y1) * yScale; export const branchY = ((horizontal.y1 + horizontal.y2) / 2 - bounds.y1) * yScale;
export const branchThickness = (horizontal.y2 - horizontal.y1) * yScale; const nativeBranchThickness = (horizontal.y2 - horizontal.y1) * yScale;
export const branchThickness = nativeBranchThickness * treeWeight;
// These box glyphs have straight edges. Move the two stroke edges symmetrically
// while keeping cell boundaries fixed, so neighboring cells still meet exactly.
function thickenEdge(value, center, width) {
return Math.abs(Math.abs(value - center) - width / 2) < 1e-6
? center + (value - center) * treeWeight
: value;
}
/** Retain the font's advance and normalize its full vertical cell to one row. */ /** Retain the font's advance and normalize its full vertical cell to one row. */
export function glyphPath(text) { export function glyphPath(text) {
@@ -26,13 +38,17 @@ export function glyphPath(text) {
// programming ligatures (Lyth Mono's GSUB includes lookups unsupported by OT2). // programming ligatures (Lyth Mono's GSUB includes lookups unsupported by OT2).
const path = new opentype.Path(); const path = new opentype.Path();
[...text].forEach((char, index) => { [...text].forEach((char, index) => {
path.extend(font.charToGlyph(char).getPath(index * cellWidth, 0, fontSize).commands); const glyph = font.charToGlyph(char).getPath(0, 0, fontSize);
}); for (const command of glyph.commands) {
for (const command of path.commands) { for (const key of ['x', 'x1', 'x2']) {
if (key in command) command[key] = thickenEdge(command[key], cellWidth / 2, nativeStemWidth) + index * cellWidth;
}
for (const key of ['y', 'y1', 'y2']) { for (const key of ['y', 'y1', 'y2']) {
if (key in command) command[key] = (command[key] - bounds.y1) * yScale; if (key in command) command[key] = thickenEdge((command[key] - bounds.y1) * yScale, branchY, nativeBranchThickness);
} }
} }
path.extend(glyph.commands);
});
return path.toPathData(5); return path.toPathData(5);
} }
@@ -40,7 +56,7 @@ export function glyphPath(text) {
// Normalize length independently of thickness; a mask exposes the growing tip. // Normalize length independently of thickness; a mask exposes the growing tip.
for (const command of vertical.commands) { for (const command of vertical.commands) {
for (const key of ['x', 'x1', 'x2']) { for (const key of ['x', 'x1', 'x2']) {
if (key in command) command[key] -= (bounds.x1 + bounds.x2) / 2; if (key in command) command[key] = (command[key] - (bounds.x1 + bounds.x2) / 2) * treeWeight;
} }
for (const key of ['y', 'y1', 'y2']) { for (const key of ['y', 'y1', 'y2']) {
if (key in command) command[key] = (command[key] - bounds.y1) / (bounds.y2 - bounds.y1); if (key in command) command[key] = (command[key] - bounds.y1) / (bounds.y2 - bounds.y1);
+3 -3
View File
@@ -219,8 +219,8 @@ test('trunk thickness and origin stay proportional to the slash throughout scali
await page.clock.runFor(time - previousTime); await page.clock.runFor(time - previousTime);
const ink = await trunkGeometry(page); const ink = await trunkGeometry(page);
expect(ink.opacity).toBeGreaterThan(0); expect(ink.opacity).toBeGreaterThan(0);
// At rest: the Lyth Mono filled stem is 1.064px, slash ink is 21px. // At rest: tree ink is weighted 25% above the font's native 1.064px stem.
expect(ink.stemWidth).toBeCloseTo(1.064, 5); expect(ink.stemWidth).toBeCloseTo(1.33, 5);
expect(ink.width / ink.slashHeight).toBeCloseTo(ink.stemWidth / 21, 5); expect(ink.width / ink.slashHeight).toBeCloseTo(ink.stemWidth / 21, 5);
// The foot is at x=12 within ink bounds x=7…41, not their centre. // The foot is at x=12 within ink bounds x=7…41, not their centre.
expect(ink.anchorX).toBeCloseTo(5 / 34, 5); expect(ink.anchorX).toBeCloseTo(5 / 34, 5);
@@ -601,7 +601,7 @@ test.describe('JavaScript-free glyph tree', () => {
await expect(page.locator('.branch-prefix svg')).toHaveCount(8); await expect(page.locator('.branch-prefix svg')).toHaveCount(8);
expect(await page.locator('.branch-prefix').evaluateAll((prefixes) => prefixes.every((prefix) => !prefix.textContent!.trim()))).toBe(true); expect(await page.locator('.branch-prefix').evaluateAll((prefixes) => prefixes.every((prefix) => !prefix.textContent!.trim()))).toBe(true);
const trunk = await trunkGeometry(page); const trunk = await trunkGeometry(page);
expect(trunk.width).toBeCloseTo(1.064, 5); expect(trunk.width).toBeCloseTo(1.33, 5);
expect(trunk.length).toBeGreaterThan(0); expect(trunk.length).toBeGreaterThan(0);
expect(Math.abs(trunk.axis)).toBeLessThan(.1); expect(Math.abs(trunk.axis)).toBeLessThan(.1);
expect(trunk.gapRatio).toBeCloseTo(1 / 14, 5); expect(trunk.gapRatio).toBeCloseTo(1 / 14, 5);
+13 -1
View File
@@ -20,7 +20,8 @@ test('pinned font binaries and upstream licenses match their integrity manifest'
test('Lyth Mono paths retain exact advance, row bounds and stroke dimensions', () => { test('Lyth Mono paths retain exact advance, row bounds and stroke dimensions', () => {
close(cellWidth, 8.456); close(cellWidth, 8.456);
close(stemWidth, 1.064); close(stemWidth, 1.064 * 1.25);
close(branchThickness, (68 / 1170) * 24 * 1.25);
close(branchY, rowHeight / 2); close(branchY, rowHeight / 2);
const vertical = box(glyphPath('│')); const vertical = box(glyphPath('│'));
close(vertical.y1, 0); close(vertical.y1, 0);
@@ -31,6 +32,17 @@ test('Lyth Mono paths retain exact advance, row bounds and stroke dimensions', (
close(horizontal.x1, 0); close(horizontal.x1, 0);
close(horizontal.x2, cellWidth); close(horizontal.x2, cellWidth);
close(horizontal.y2 - horizontal.y1, branchThickness); close(horizontal.y2 - horizontal.y1, branchThickness);
close((horizontal.y1 + horizontal.y2) / 2, branchY);
const tee = box(glyphPath('├'));
close(tee.x1, cellWidth / 2 - stemWidth / 2);
close(tee.x2, cellWidth);
close(tee.y1, 0);
close(tee.y2, rowHeight);
const elbow = box(glyphPath('└'));
close(elbow.x1, tee.x1);
close(elbow.x2, cellWidth);
close(elbow.y1, 0);
close(elbow.y2, branchY + branchThickness / 2);
const trunk = box(trunkPath); const trunk = box(trunkPath);
close(trunk.x1 + trunk.x2, 0); close(trunk.x1 + trunk.x2, 0);
close(trunk.x2 - trunk.x1, stemWidth); close(trunk.x2 - trunk.x1, stemWidth);