fix: shrink the resting slash independently of the tree
Reference site / build (push) Canceled after 0s

This commit is contained in:
2026-10-11 01:39:56 +09:00
parent 6897b2368a
commit ae2c236df7
4 changed files with 72 additions and 18 deletions
+49 -7
View File
@@ -16,13 +16,14 @@ async function trunkGeometry(page: import('@playwright/test').Page) {
const start = new DOMPoint(0, 0).matrixTransform(anchorMatrix);
const tip = new DOMPoint(0, reveal.height.baseVal.value).matrixTransform(anchorMatrix);
const slash = document.querySelector('.root-shape')!.getBoundingClientRect();
const foot = new DOMPoint(12, 60).matrixTransform(document.querySelector<SVGSVGElement>('.root-vector')!.getScreenCTM()!);
const foot = new DOMPoint(12, 60).matrixTransform(document.querySelector<SVGPathElement>('.root-shape')!.getScreenCTM()!);
return {
width: bounds.width * Math.hypot(matrix.a, matrix.b),
length: reveal.height.baseVal.value,
opacity: Number(getComputedStyle(path).opacity),
stemWidth: Number(anchor.dataset.stemWidth),
slashHeight: slash.height,
rowHeight: document.querySelector('.node-row:not(.root-slot)')!.getBoundingClientRect().height,
anchorX: (start.x - slash.left) / slash.width,
gapRatio: (start.y - slash.bottom) / slash.height,
dx: tip.x - start.x, dy: tip.y - start.y,
@@ -39,10 +40,50 @@ async function armed(page: import('@playwright/test').Page) {
async function complete(page: import('@playwright/test').Page) {
await expect(page.locator(stage)).toHaveAttribute('data-phase', 'complete', { timeout: 10000 });
await expect(page.locator('html')).not.toHaveAttribute('data-landing');
expect((await page.locator('.root-shape').boundingBox())!.height).toBeCloseTo(12.25, 4);
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true);
await expect(page.getByRole('link', { name: 'Book', exact: true })).toBeVisible();
}
test('only the resting slash shrinks; intro ink and tree dimensions stay unchanged', async ({ page }, testInfo) => {
await page.clock.install({ time: new Date('2026-01-01T00:00:00Z') });
await page.clock.pauseAt(new Date('2026-01-01T00:00:01Z'));
await armed(page);
const slash = page.locator('.root-shape');
const initial = (await slash.boundingBox())!;
expect(initial.height).toBeCloseTo(151.2, 3); // Original 21px ink × 7.2 intro scale.
expect(initial.width).toBeCloseTo(91.8, 3);
await page.screenshot({ path: testInfo.outputPath('slash-initial-unchanged.png') });
await page.keyboard.press('Enter');
let previousTime = 0, previousHeight = initial.height;
for (const time of [500, 1400, 2200, 2800, 4000]) {
await page.clock.runFor(time - previousTime);
const ink = await trunkGeometry(page);
expect(ink.slashHeight).toBeLessThan(previousHeight);
// Shared scale is still applied to the original 24px rows and 1.33px stem.
expect(ink.width / ink.rowHeight).toBeCloseTo(1.33 / 24, 5);
expect(Math.abs(ink.axis)).toBeLessThan(.01);
previousHeight = ink.slashHeight;
previousTime = time;
}
await complete(page);
const final = (await slash.boundingBox())!;
expect(final.width).toBeCloseTo(7.4375, 4);
const dimensions = () => page.evaluate(() => ({
rows: [...document.querySelectorAll('.node-row, .demo-node, .branch-prefix')].map((el) => el.getBoundingClientRect().toJSON()),
trunk: document.querySelector('.trunk-line')!.getBoundingClientRect().toJSON(),
textSize: getComputedStyle(document.querySelector('.demo-node:not(.root-mark)')!).fontSize,
}));
const tree = await dimensions();
expect(tree.textSize).toBe('14px');
expect(tree.trunk.width).toBeCloseTo(1.33, 4);
await page.locator('.slash-art').evaluate((el) => (el as SVGGElement).style.setProperty('--slash-scale', '1'));
expect((await slash.boundingBox())!.height).toBeCloseTo(21, 4);
expect(await dimensions()).toEqual(tree); // Changing only slash ink cannot resize/reflow the tree.
await page.locator('.slash-art').evaluate((el) => (el as SVGGElement).style.removeProperty('--slash-scale'));
await page.locator('.demo-tree').screenshot({ path: testInfo.outputPath('slash-final-smaller.png') });
});
for (const trigger of ['click', 'wheel', 'key']) {
test(`one ${trigger} plays through to the interactive world without more input`, async ({ page }) => {
const errors: string[] = [];
@@ -142,7 +183,7 @@ test.describe('persistent vertical trunk', () => {
const points = await page.locator('.root-vector').evaluate((svg) => {
const matrix = (svg as SVGSVGElement).getScreenCTM()!;
const origin = new DOMPoint(12, 64).matrixTransform(matrix);
const foot = new DOMPoint(12, 60).matrixTransform(matrix);
const foot = new DOMPoint(12, 60).matrixTransform(svg.querySelector<SVGPathElement>('.root-shape')!.getScreenCTM()!);
return { x: origin.x, y: origin.y, footY: foot.y };
});
const clip = { x: Math.floor(points.x) - 2, y: Math.floor(points.y) - 8, width: 5, height: 20 };
@@ -179,8 +220,8 @@ test('completed tree aligns the slash foot with rendered box-drawing ink', async
await page.keyboard.press('Enter');
await page.clock.runFor(4000);
await complete(page);
const foot = await page.locator('.root-vector').evaluate((svg) => {
const point = new DOMPoint(12, 60).matrixTransform((svg as SVGSVGElement).getScreenCTM()!);
const foot = await page.locator('.root-shape').evaluate((svg) => {
const point = new DOMPoint(12, 60).matrixTransform((svg as SVGGraphicsElement).getScreenCTM()!);
return { x: point.x, y: point.y };
});
// Sample the painted vertical stroke, below the slash and above the first
@@ -208,7 +249,7 @@ test('completed tree aligns the slash foot with rendered box-drawing ink', async
await page.locator('.demo-tree').screenshot({ path: testInfo.outputPath('completed-tree.png') });
});
test('trunk thickness and origin stay proportional to the slash throughout scaling', async ({ page }, testInfo) => {
test('trunk thickness follows the unchanged tree scale while the slash shrinks independently', async ({ page }, testInfo) => {
await page.clock.install({ time: new Date('2026-01-01T00:00:00Z') });
await page.clock.pauseAt(new Date('2026-01-01T00:00:01Z'));
await armed(page);
@@ -221,7 +262,7 @@ test('trunk thickness and origin stay proportional to the slash throughout scali
expect(ink.opacity).toBeGreaterThan(0);
// At rest: tree ink is weighted 25% above the font's native 1.064px stem.
expect(ink.stemWidth).toBeCloseTo(1.33, 5);
expect(ink.width / ink.slashHeight).toBeCloseTo(ink.stemWidth / 21, 5);
expect(ink.width / ink.rowHeight).toBeCloseTo(ink.stemWidth / 24, 5);
// The foot is at x=12 within ink bounds x=7…41, not their centre.
expect(ink.anchorX).toBeCloseTo(5 / 34, 5);
expect(ink.gapRatio).toBeCloseTo(1 / 14, 5);
@@ -381,7 +422,7 @@ test('continuous motion overlaps growth, travel and reveal without intermediate
const frame = document.querySelector('.landing-content')!.getBoundingClientRect();
const rows = [...document.querySelectorAll<HTMLElement>('.node-row:not(.root-slot)')].map((row) => row.getBoundingClientRect());
const nodes = [...document.querySelectorAll<HTMLElement>('.demo-node:not(.root-mark)')].map((node) => node.getBoundingClientRect());
const foot = new DOMPoint(12, 60).matrixTransform(document.querySelector<SVGSVGElement>('.root-vector')!.getScreenCTM()!);
const foot = new DOMPoint(12, 60).matrixTransform(document.querySelector<SVGPathElement>('.root-shape')!.getScreenCTM()!);
return {
rootColumnError: Math.abs(foot.x - prefix.getBoundingClientRect().left - cell / 2),
rootGap: Math.abs(root.bottom - prefix.getBoundingClientRect().top),
@@ -595,6 +636,7 @@ test.describe('JavaScript-free glyph tree', () => {
await page.goto('./');
await expect(page.locator('html')).not.toHaveAttribute('data-landing');
await expect(page.locator('.no-script-note')).toBeVisible();
expect((await page.locator('.root-shape').boundingBox())!.height).toBeCloseTo(12.25, 4);
await expect(page.locator('.trunk-line')).toHaveCount(1);
await expect(page.locator('.trunk-line')).toHaveCSS('opacity', '1');
await expect(page.locator('.trunk-line')).toHaveCSS('stroke', 'none');