fix: carry landing motion through overlapping reveals
Reference site / build (push) Canceled after 0s

This commit is contained in:
2026-10-10 18:01:52 +09:00
parent 651cc573be
commit 5d418beeba
4 changed files with 146 additions and 103 deletions
+64 -35
View File
@@ -34,7 +34,7 @@ for (const trigger of ['click', 'wheel', 'key']) {
});
}
test('storyboard phases retain one root and ignore repeated triggers', async ({ page }, testInfo) => {
test('continuous motion overlaps growth, travel and reveal without intermediate stops', 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);
@@ -58,19 +58,26 @@ test('storyboard phases retain one root and ignore repeated triggers', async ({
await expect(page.locator(stage)).not.toHaveAttribute('data-hover');
await expect(page.locator('.tap-stream-a')).toHaveCSS('opacity', '0');
await root.click();
await expect(page.locator(stage)).toHaveAttribute('data-phase', 'orbit');
await expect(page.locator(stage)).toHaveAttribute('data-phase', 'flow');
await expect(page.locator(stage)).not.toHaveAttribute('data-hover');
await page.clock.runFor(1150);
await expect(page.locator(stage)).toHaveAttribute('data-phase', 'orbit');
await page.clock.runFor(420);
const arcLength = await page.locator('#welcome-orbit').evaluate((path) => (path as SVGPathElement).getTotalLength());
const circlePath = await page.locator('#welcome-orbit').getAttribute('d');
await page.screenshot({ path: testInfo.outputPath('03-orbit.png') });
await page.mouse.wheel(0, 180);
await page.keyboard.press('ArrowDown');
await page.mouse.click(20, 20);
await page.clock.runFor(1330); // Scene 4: early trunk growth, before docking.
await expect(page.locator(stage)).toHaveAttribute('data-phase', 'extend');
expect(await page.locator('#welcome-orbit').getAttribute('d')).toMatch(new RegExp(`^${circlePath} H`));
// No parked pose between orbit, release and growth: every sample keeps moving.
let previous = (await root.boundingBox())!;
for (let i = 0; i < 10; i++) {
await page.clock.runFor(100);
const current = (await root.boundingBox())!;
expect(Math.hypot(current.x - previous.x, current.y - previous.y)).toBeGreaterThan(.1);
expect(current.height).toBeLessThan(previous.height);
previous = current;
}
await expect(page.locator(stage)).toHaveAttribute('data-phase', 'flow');
expect(await page.locator('#welcome-orbit').getAttribute('d')).toMatch(new RegExp('^' + circlePath + ' H'));
const tangent = await page.locator('#welcome-orbit').evaluate((element, length) => {
const path = element as SVGPathElement;
const first = path.getPointAtLength(length + 40);
@@ -80,42 +87,34 @@ test('storyboard phases retain one root and ignore repeated triggers', async ({
expect(tangent.dx).toBeCloseTo(40, 1);
expect(tangent.dy).toBeCloseTo(0, 1);
expect(tangent.y).toBeCloseTo(-180, 1);
// The trunk must not cut through letters still on the bottom of the circle.
expect(Number(await page.locator('.orbit textPath').getAttribute('startOffset'))).toBeGreaterThan(arcLength / 2 + 100);
const earlyLength = await page.locator('.trunk-line').evaluate((path) => (path as SVGPathElement).getTotalLength());
const earlyRoot = (await root.boundingBox())!;
expect(earlyLength).toBeGreaterThan(10);
await page.screenshot({ path: testInfo.outputPath('04-unfold.png') });
await page.clock.runFor(700); // Let the downward trunk finish extending.
await expect(page.locator(stage)).toHaveAttribute('data-phase', 'extend');
await page.screenshot({ path: testInfo.outputPath('04-release-growth.png') });
await page.clock.runFor(260);
const extendedLength = await page.locator('.trunk-line').evaluate((path) => (path as SVGPathElement).getTotalLength());
const extendedRoot = (await root.boundingBox())!;
expect(extendedLength).toBeGreaterThan(earlyLength * 1.4);
expect(extendedLength).toBeGreaterThan(page.viewportSize()!.height * .18);
await expect(page.locator('.trunk-line')).toHaveCSS('stroke-width', '10px');
const extendedRoot = await root.boundingBox();
expect(extendedRoot!.height).toBeCloseTo(initial!.height, 0);
expect(extendedRoot!.y).toBeCloseTo(initial!.y, 0);
expect(extendedLength).toBeGreaterThan(page.viewportSize()!.height * .15);
// The trunk grows WHILE the root travels and shrinks, never after a hold.
expect(extendedRoot.height).toBeLessThan(earlyRoot.height - 5);
expect(extendedRoot.x + extendedRoot.width / 2).toBeLessThan(earlyRoot.x + earlyRoot.width / 2 - 2);
await expect(page.locator('.demo-node:not(.root-mark)').first()).toHaveCSS('opacity', '0');
const trunkGeometry = () => page.evaluate(() => {
const extended = await page.evaluate(() => {
const path = document.querySelector<SVGPathElement>('.trunk-line')!;
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 extended = await trunkGeometry();
expect(Math.abs(extended.dx)).toBeLessThan(.1);
expect(Math.abs(extended.axis)).toBeLessThan(.1);
expect(extended.dy).toBeGreaterThan(0);
expect(extended.gap).toBeCloseTo(8, 0);
expect(extended.bottom).toBeLessThan(page.viewportSize()!.height * .91);
await page.screenshot({ path: testInfo.outputPath('04-trunk.png') });
await page.clock.runFor(850);
const docking = await trunkGeometry();
expect(Math.abs(docking.dx)).toBeLessThan(.1);
expect(Math.abs(docking.axis)).toBeLessThan(.1);
expect(docking.gap).toBeGreaterThanOrEqual(0);
await page.screenshot({ path: testInfo.outputPath('05-docking.png') });
await page.clock.runFor(850); // SVG/box-drawing handoff, with neither removed.
expect(extended.bottom).toBeLessThan(page.viewportSize()!.height);
await page.screenshot({ path: testInfo.outputPath('04-travelling-trunk.png') });
await page.clock.runFor(920); // 2600ms: moving tree, SVG handoff AND panel onset.
const handoff = await page.evaluate(() => {
const path = document.querySelector<SVGPathElement>('.trunk-line')!;
const start = path.getPointAtLength(0).matrixTransform(path.getScreenCTM()!);
@@ -127,6 +126,8 @@ test('storyboard phases retain one root and ignore repeated triggers', async ({
end: Math.hypot(tip.x - (last.left + last.width / 2), tip.y - (last.top + last.height / 2)),
spineOpacity: Number(getComputedStyle(document.querySelector('.branch-spine')!).opacity),
trunkOpacity: Number(getComputedStyle(path).opacity),
labelOpacity: Number(getComputedStyle(document.querySelector('.demo-node:not(.root-mark)')!).opacity),
panelOpacity: Number(getComputedStyle(document.querySelector('.text-panel')!).opacity),
};
});
expect(handoff.start).toBeLessThan(1);
@@ -134,14 +135,22 @@ test('storyboard phases retain one root and ignore repeated triggers', async ({
expect(handoff.spineOpacity).toBeGreaterThan(0);
expect(handoff.trunkOpacity).toBeGreaterThan(0);
expect(handoff.spineOpacity + handoff.trunkOpacity).toBeCloseTo(1, 4);
await page.clock.runFor(550);
await expect(page.locator(stage)).toHaveAttribute('data-phase', 'tree');
await page.screenshot({ path: testInfo.outputPath('05-tree.png') });
await page.clock.runFor(600);
expect(handoff.labelOpacity).toBeGreaterThan(0);
expect(handoff.panelOpacity).toBeGreaterThan(0);
const growingTreeRoot = (await root.boundingBox())!;
expect(growingTreeRoot.height).toBeGreaterThan(24.1);
await page.screenshot({ path: testInfo.outputPath('05-moving-tree.png') });
await page.clock.runFor(200);
expect((await root.boundingBox())!.height).toBeLessThan(growingTreeRoot.height);
await expect(page.locator('.call-panel')).toHaveAttribute('data-glitching');
expect(await page.locator('.call-panel .box-fill').first().textContent()).not.toMatch(/^─+$/);
await page.screenshot({ path: testInfo.outputPath('06-glitch.png') });
await page.clock.runFor(1200);
await page.screenshot({ path: testInfo.outputPath('06-overlapping-glitch.png') });
await page.clock.runFor(400);
const chromeOpacity = await page.locator('.site-header').evaluate((el) => Number(getComputedStyle(el).opacity));
expect(chromeOpacity).toBeGreaterThan(0);
expect(chromeOpacity).toBeLessThan(1);
await expect(page.locator('.site-header')).toHaveAttribute('inert', '');
await page.clock.runFor(800);
await complete(page);
await expect(root).toHaveCount(1);
await expect(page.locator('.call-panel')).not.toHaveAttribute('inert');
@@ -191,14 +200,34 @@ test('storyboard phases retain one root and ignore repeated triggers', async ({
await expect(page.locator(stage)).toHaveAttribute('data-phase', 'complete');
});
for (const time of [1500, 2600]) {
test(`skipping at ${time}ms removes the moving tree transforms`, async ({ page }) => {
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('a');
await page.clock.runFor(time);
await expect(page.locator('.node-row').last()).not.toHaveCSS('transform', 'none');
await page.keyboard.press('Escape');
await complete(page);
for (const row of await page.locator('.node-row').all()) await expect(row).toHaveCSS('transform', 'none');
await expect(page.locator('.root-mark')).toHaveCSS('transform', 'none');
await expect(page.locator('.site-header')).toHaveCSS('opacity', '1');
await expect(page.locator('.branch-prefix').first()).toHaveText('├── ');
await expect(page.locator('.intro-art')).toBeHidden();
await page.clock.runFor(4000);
await expect(page.locator(stage)).toHaveAttribute('data-phase', 'complete');
});
}
test('skipping an active glitch restores exact text and frames', async ({ page }) => {
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('a');
await expect(page.locator(stage)).toHaveAttribute('data-phase', 'orbit');
await expect(page.locator(stage)).toHaveAttribute('data-phase', 'flow');
await expect(page.locator('.tap-stream-a')).toHaveCSS('opacity', '0');
await page.clock.runFor(6100);
await page.clock.runFor(3000);
await expect(page.locator('.call-panel')).toHaveAttribute('data-glitching');
await page.keyboard.press('Escape');
await complete(page);