Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions src/components/funnels/ScrollVideoBackground.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ const BOOTSTRAP_FRAME_COUNT = 24
const BOOTSTRAP_COLUMNS = 6
const BOOTSTRAP_ROWS = 4
const FIRST_FRAME_TIME_SECONDS = 0.04
const MAX_CACHED_FRAMES = 120
const CACHE_READY_FRAME_COUNT = 48
const MAX_CACHED_FRAME_WIDTH = 540
const TARGET_CACHED_FRAME_RATE = 24
const MAX_CACHED_FRAMES = 240
const MAX_CACHED_FRAME_WIDTH = 384

type ScrollVideoBackgroundProps = {
containerRef?: RefObject<HTMLElement | null>
Expand Down Expand Up @@ -176,7 +176,7 @@ export function ScrollVideoBackground({ containerRef, className = '', onProgress
})
const duration = source.duration
if (!Number.isFinite(duration) || duration <= 0) return
const count = Math.min(MAX_CACHED_FRAMES, Math.max(24, Math.round(duration * 12)))
const count = Math.min(MAX_CACHED_FRAMES, Math.max(24, Math.round(duration * TARGET_CACHED_FRAME_RATE)))
const width = Math.min(MAX_CACHED_FRAME_WIDTH, source.videoWidth || MAX_CACHED_FRAME_WIDTH)
const height = Math.max(1, Math.round(width * ((source.videoHeight || 540) / (source.videoWidth || 960))))
const temp = document.createElement('canvas')
Expand All @@ -203,8 +203,12 @@ export function ScrollVideoBackground({ containerRef, className = '', onProgress
framesRef.current[index]?.close()
framesRef.current[index] = frame
loadedCount += 1
if (loadedCount === CACHE_READY_FRAME_COUNT) setCacheReady(true)
}

// Do not hand off from the densely interpolated bootstrap sprite to a
// sparse subset of the main cache. Waiting for the complete cache keeps
// temporal spacing uniform and avoids a visible jump while scrolling.
if (!cancelled && loadedCount === count) setCacheReady(true)
}

const startExtraction = () => { extract().catch(() => undefined) }
Expand Down
62 changes: 54 additions & 8 deletions tests/evidence/cinematic-final-quality.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ async function renderedCanvasChecksum(page: Page) {

test.describe('ProChat final public-site quality', () => {
test('core routes keep every cinematic background painted through repeated scroll reversals', async ({ page }) => {
test.setTimeout(120_000)
test.setTimeout(180_000)
await page.setViewportSize({ width: 1440, height: 900 })
await page.emulateMedia({ reducedMotion: 'no-preference' })

Expand All @@ -121,12 +121,56 @@ test.describe('ProChat final public-site quality', () => {
await expect(page.locator('.cm-video__canvas')).toHaveAttribute('data-canvas-painted', 'true')
await expect.poll(() => page.locator('.cm-video__canvas').evaluate(canvas => getComputedStyle(canvas).opacity)).toBe('1')

const warming = await page.evaluate(async () => {
const canvas = document.querySelector<HTMLCanvasElement>('.cm-video__canvas')
const root = canvas?.closest<HTMLElement>('.cm-video')
if (!canvas || !root) throw new Error('Cinematic bootstrap surface is missing')
const sample = document.createElement('canvas')
sample.width = 32
sample.height = 18
const context = sample.getContext('2d')
if (!context) throw new Error('Canvas 2D context is unavailable')
const frames = new Set<number>()
let uncoveredSamples = 0

for (let index = 0; index < 18; index += 1) {
window.scrollBy(0, 8)
await new Promise<void>(resolve => requestAnimationFrame(() => requestAnimationFrame(() => resolve())))
context.clearRect(0, 0, sample.width, sample.height)
context.drawImage(canvas, 0, 0, sample.width, sample.height)
const pixels = context.getImageData(0, 0, sample.width, sample.height).data
let signature = 2166136261
let alpha = 0
for (let pixel = 0; pixel < pixels.length; pixel += 4) {
alpha += pixels[pixel + 3]
signature ^= pixels[pixel] + (pixels[pixel + 1] << 8) + (pixels[pixel + 2] << 16) + (pixels[pixel + 3] << 24)
signature = Math.imul(signature, 16777619)
}
frames.add(signature >>> 0)
const canvasOpacity = Number.parseFloat(getComputedStyle(canvas).opacity)
const poster = root.querySelector<HTMLElement>('.cm-video__poster')
const posterOpacity = Number.parseFloat(poster ? getComputedStyle(poster).opacity : '0')
if (canvasOpacity + posterOpacity < 0.98 || (canvasOpacity >= 0.99 && alpha === 0)) uncoveredSamples += 1
}

return {
state: root.dataset.renderState,
cacheReady: canvas.dataset.frameCacheReady,
uniqueFrames: frames.size,
uncoveredSamples,
}
})
expect(warming.state, `${route} stays on the stable bootstrap while the full cache builds`).toBe('bootstrap')
expect(warming.cacheReady, `${route} does not expose a sparse partial cache`).toBe('false')
expect(warming.uniqueFrames, `${route} bootstrap visibly scrubs during warm-up`).toBeGreaterThan(1)
expect(warming.uncoveredSamples, `${route} remains covered during warm-up`).toBe(0)

const canDecodeVideo = await page.evaluate(() => Boolean(
document.createElement('video').canPlayType('video/mp4; codecs="avc1.640028"'),
))
if (canDecodeVideo) {
await expect(page.locator('.cm-video__canvas')).toHaveAttribute('data-frame-cache-ready', 'true', { timeout: 45_000 })
await expect(page.locator('.cm-video__canvas')).toHaveAttribute('data-frame-count', '120')
await expect(page.locator('.cm-video__canvas')).toHaveAttribute('data-frame-cache-ready', 'true', { timeout: 90_000 })
await expect(page.locator('.cm-video__canvas')).toHaveAttribute('data-frame-count', '240')
}

const sampled = await sampleRenderedCanvas(page)
Expand Down Expand Up @@ -191,15 +235,17 @@ test.describe('ProChat final public-site quality', () => {
}
})

test('cached renderer presents tiny scroll changes above 30 frames per second', async ({ page }) => {
test('cached renderer presents tiny scroll changes at near-display cadence without frame quantization', async ({ page }) => {
test.setTimeout(120_000)
await page.setViewportSize({ width: 1440, height: 900 })
await page.emulateMedia({ reducedMotion: 'no-preference' })
await page.goto(routeUrl('/'), { waitUntil: 'domcontentloaded' })
const canDecodeVideo = await page.evaluate(() => Boolean(
document.createElement('video').canPlayType('video/mp4; codecs="avc1.640028"'),
))
test.skip(!canDecodeVideo, 'This browser has no supported H.264 decoder for the cached-video path.')
await expect(page.locator('.cm-video__canvas')).toHaveAttribute('data-frame-cache-ready', 'true', { timeout: 45_000 })
await expect(page.locator('.cm-video__canvas')).toHaveAttribute('data-frame-cache-ready', 'true', { timeout: 90_000 })
await expect(page.locator('.cm-video__canvas')).toHaveAttribute('data-frame-count', '240')

const cadence = await page.evaluate(async () => {
const canvas = document.querySelector<HTMLCanvasElement>('.cm-video__canvas')
Expand Down Expand Up @@ -254,9 +300,9 @@ test.describe('ProChat final public-site quality', () => {
})

expect(cadence.samples, 'tiny scroll movements repaint the visible canvas').toBeGreaterThan(45)
expect(cadence.framesPerSecond, `observed paint cadence: ${JSON.stringify(cadence)}`).toBeGreaterThanOrEqual(30)
expect(cadence.longestGap, `no long gaps during fine scroll: ${JSON.stringify(cadence)}`).toBeLessThan(100)
expect(cadence.distinctRenderedFrames, `tiny scroll movements visibly update the image: ${JSON.stringify(cadence)}`).toBeGreaterThanOrEqual(8)
expect(cadence.framesPerSecond, `observed paint cadence: ${JSON.stringify(cadence)}`).toBeGreaterThanOrEqual(50)
expect(cadence.longestGap, `no long gaps during fine scroll: ${JSON.stringify(cadence)}`).toBeLessThan(40)
expect(cadence.distinctRenderedFrames, `tiny scroll movements visibly update the image: ${JSON.stringify(cadence)}`).toBeGreaterThanOrEqual(Math.ceil(cadence.samples * 0.75))
})

test('four cinematic pages share one clean header and CTA labels stay on a single line at all target widths', async ({ page }) => {
Expand Down
Loading