fix: align the slash foot with the rendered tree spine
Reference site / build (push) Canceled after 0s

This commit is contained in:
2026-10-10 21:22:32 +09:00
parent 3b76025e10
commit 39e1565cb9
4 changed files with 54 additions and 12 deletions
+42 -4
View File
@@ -89,6 +89,42 @@ test('hover streams are masked at the actual slash edges throughout the loop', a
await expect(page.locator('.tap-stream-b')).toHaveCSS('opacity', '0');
});
test('completed tree aligns the slash foot with rendered box-drawing ink', 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);
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()!);
return { x: point.x, y: point.y };
});
// Sample the painted vertical stroke, below the slash and above the first
// horizontal branch. Element boxes alone missed the original 4.5px error.
const clip = { x: Math.floor(foot.x) - 6, y: Math.ceil(foot.y) + 4, width: 13, height: 5 };
const png = (await page.screenshot({ clip })).toString('base64');
const ink = await page.evaluate(async (png) => {
const image = new Image(); image.src = 'data:image/png;base64,' + png;
await image.decode();
const canvas = document.createElement('canvas');
canvas.width = image.width; canvas.height = image.height;
const ctx = canvas.getContext('2d')!;
ctx.drawImage(image, 0, 0);
const data = ctx.getImageData(0, 0, image.width, image.height).data;
let weight = 0, moment = 0;
for (let i = 0; i < data.length; i += 4) {
const ink = Math.max(0, 240 - (data[i] + data[i + 1] + data[i + 2]) / 3);
weight += ink;
moment += (i / 4 % image.width + .5) * ink;
}
return { weight, x: moment / weight };
}, png);
expect(ink.weight).toBeGreaterThan(100);
expect(Math.abs(clip.x + ink.x - foot.x)).toBeLessThan(.75);
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) => {
await page.clock.install({ time: new Date('2026-01-01T00:00:00Z') });
await page.clock.pauseAt(new Date('2026-01-01T00:00:01Z'));
@@ -113,8 +149,8 @@ test('trunk thickness and origin stay proportional to the slash throughout scali
expect(ink.opacity).toBeGreaterThan(0);
// At rest: 1px trunk, 21px of actual slash ink (56/64 of its 24px box).
expect(ink.stroke / ink.slashHeight).toBeCloseTo(1 / 21, 5);
// The same local anchor at every scale: centred, 1.5px below 21px of ink.
expect(ink.anchorX).toBeCloseTo(.5, 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);
expect(ink.stroke).toBeLessThan(previousWidth);
if ([1520, 2000, 2480].includes(time)) await page.screenshot({ path: testInfo.outputPath(`proportions-${time}.png`) });
@@ -197,7 +233,8 @@ test('continuous motion overlaps growth, travel and reveal without intermediate
const start = path.getPointAtLength(0).matrixTransform(path.getScreenCTM()!);
const tip = path.getPointAtLength(path.getTotalLength()).matrixTransform(path.getScreenCTM()!);
const root = document.querySelector('.root-mark')!.getBoundingClientRect();
return { dx: tip.x - start.x, dy: tip.y - start.y, axis: start.x - (root.left + root.width / 2), gap: start.y - root.bottom, bottom: tip.y };
const foot = new DOMPoint(12, 60).matrixTransform(document.querySelector<SVGSVGElement>('.root-vector')!.getScreenCTM()!);
return { dx: tip.x - start.x, dy: tip.y - start.y, axis: start.x - foot.x, gap: start.y - root.bottom, bottom: tip.y };
});
expect(Math.abs(extended.dx)).toBeLessThan(.1);
expect(Math.abs(extended.axis)).toBeLessThan(.1);
@@ -269,8 +306,9 @@ 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()!);
return {
rootColumnError: Math.abs(root.left + root.width / 2 - spine.left - spine.width / 2),
rootColumnError: Math.abs(foot.x - spine.left - spine.width / 2),
rootGap: Math.abs(root.bottom - spine.top),
frameCells: frame.width / cell,
rowHeights: rows.map((row) => row.height),
+2 -1
View File
@@ -11,7 +11,8 @@ test('home and book are usable without client JavaScript', async ({ page }, test
const spine = await page.locator('.branch-spine').first().boundingBox();
expect(root).not.toBeNull();
expect(spine).not.toBeNull();
expect(Math.abs(root!.x + root!.width / 2 - spine!.x - spine!.width / 2)).toBeLessThan(1);
const footX = await page.locator('.root-vector').evaluate((svg) => new DOMPoint(12, 60).matrixTransform((svg as SVGSVGElement).getScreenCTM()!).x);
expect(Math.abs(footX - spine!.x - spine!.width / 2)).toBeLessThan(1);
expect(Math.abs(root!.y + root!.height - spine!.y)).toBeLessThan(1);
await page.screenshot({ path: testInfo.outputPath('home.png'), fullPage: true });
await page.getByRole('link', { name: 'Book', exact: true }).click();