diff --git a/public/player/motion.js b/public/player/motion.js index d0456d0..acda571 100644 --- a/public/player/motion.js +++ b/public/player/motion.js @@ -195,12 +195,40 @@ export function cornerPosition(index, size, viewport, margin) { }; } +// How many stills each corner holds, and how far apart they sit as a fraction +// of the frame size. Deep enough that a corner reads as a pile, shallow enough +// that the stack stays in its own quarter of the screen. +const CORNER_STACK = 3; +const CORNER_STACK_STEP = 0.09; + +/** + * Top-left offset for the `depth`-th still of a corner: the same spot as the + * corner itself, nudged inward (toward the middle of the screen) once per + * depth. Successive stills therefore never land on top of each other — each + * one leaves the edges of the ones below it showing. + */ +export function cornerStackPosition(index, depth, size, viewport, margin) { + const at = cornerPosition(index, size, viewport, margin); + const right = index === 1 || index === 2; + const bottom = index === 2 || index === 3; + const step = depth * CORNER_STACK_STEP; + + return { + x: at.x + (right ? -1 : 1) * step * size.width, + y: at.y + (bottom ? -1 : 1) * step * size.height, + }; +} + /** * The video parks in one random corner and stays there; every few seconds a * screenshot of it lands in the next of the three *other* corners, cycling * clockwise. Because the video's own corner is never a slot, a still can never * cover the video — and since the crop only ever covers the video's rect, a * still never contains another still either. + * + * Each corner stacks CORNER_STACK stills at staggered offsets, and the newest + * shot always moves to the top of the layer, so a full corner still reads as + * something new arriving rather than as an unchanged picture. */ export function startCorners(context) { const { element } = context; @@ -209,13 +237,19 @@ export function startCorners(context) { // The three non-home corners, in clockwise order starting after the video. const slots = [1, 2, 3].map((step) => (home + step) % CORNERS); - const images = slots.map(() => { - const img = document.createElement('img'); - layer.appendChild(img); - return img; - }); - let slot = 0; + // Slot-major: consecutive shots move to the next corner, and the depth only + // advances once every corner at the current depth has been filled. + const cells = []; + for (let depth = 0; depth < CORNER_STACK; depth += 1) { + for (const corner of slots) { + const img = document.createElement('img'); + layer.appendChild(img); + cells.push({ img, corner, depth }); + } + } + + let next = 0; let size; function layout() { @@ -226,12 +260,12 @@ export function startCorners(context) { const spot = cornerPosition(home, size, viewport, margin); place(element, spot.x, spot.y); - images.forEach((img, i) => { - const at = cornerPosition(slots[i], size, viewport, margin); - img.style.width = `${size.width}px`; - img.style.height = `${size.height}px`; - place(img, at.x, at.y); - }); + for (const cell of cells) { + const at = cornerStackPosition(cell.corner, cell.depth, size, viewport, margin); + cell.img.style.width = `${size.width}px`; + cell.img.style.height = `${size.height}px`; + place(cell.img, at.x, at.y); + } } function shoot() { @@ -243,10 +277,12 @@ export function startCorners(context) { const shot = context.capture.grab(rect); if (!shot) return; - const img = images[slot]; - img.src = shot; - img.classList.add('visible'); - slot = (slot + 1) % images.length; + const cell = cells[next]; + cell.img.src = shot; + cell.img.classList.add('visible'); + // Re-append so the freshest still paints over the rest of its pile. + layer.appendChild(cell.img); + next = (next + 1) % cells.length; } layout(); @@ -256,7 +292,7 @@ export function startCorners(context) { return function stop() { clearInterval(timer); window.removeEventListener('resize', layout); - for (const img of images) img.remove(); + for (const cell of cells) cell.img.remove(); }; }