diff --git a/site/src/scripts/landing.ts b/site/src/scripts/landing.ts index 5761c18..d89ed09 100644 --- a/site/src/scripts/landing.ts +++ b/site/src/scripts/landing.ts @@ -268,7 +268,9 @@ export function initLanding() { const tipY = lineStartY + mix(longLength * extension, finalLength * scale, converge); const handoff = progress(time, flow.handoff, 280); trunk.setAttribute('d', `M${centreX} ${lineStartY} V${tipY}`); - trunk.style.strokeWidth = String(mix(10, scale, converge)); + // Keep the 1px resting stroke proportional to the slash's shared scale. + // Convergence changes the trunk's length, never its relative thickness. + trunk.style.strokeWidth = String(scale); const ink = [[32, 142], [34, 152], [31, 134]].map(([from, to]) => Math.round(mix(from, to, converge))); trunk.style.stroke = `rgb(${ink.join(',')})`; trunk.style.opacity = String(progress(time, flow.grow, 100) * (1 - handoff)); diff --git a/site/tests/browser/landing.spec.ts b/site/tests/browser/landing.spec.ts index a83283b..8e4964c 100644 --- a/site/tests/browser/landing.spec.ts +++ b/site/tests/browser/landing.spec.ts @@ -89,6 +89,34 @@ 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('trunk thickness keeps the same proportion 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')); + await armed(page); + await page.keyboard.press('Enter'); + let previousTime = 0; + let previousWidth = Infinity; + for (const time of [1360, 1520, 1760, 2000, 2240, 2480, 2700]) { + await page.clock.runFor(time - previousTime); + const ink = await page.evaluate(() => { + const path = document.querySelector('.trunk-line')!; + const matrix = path.getScreenCTM()!; + const stroke = parseFloat(getComputedStyle(path).strokeWidth) * Math.hypot(matrix.a, matrix.b); + const slashHeight = document.querySelector('.root-shape')!.getBoundingClientRect().height; + return { stroke, slashHeight, opacity: Number(getComputedStyle(path).opacity) }; + }); + 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); + expect(ink.stroke).toBeLessThan(previousWidth); + if ([1520, 2000, 2480].includes(time)) await page.screenshot({ path: testInfo.outputPath(`proportions-${time}.png`) }); + previousTime = time; + previousWidth = ink.stroke; + } + await page.clock.runFor(1400); + await complete(page); +}); + 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'));