Stack corner stills at staggered offsets

A full corner used to swap its single still in place, so a fresh
screenshot was indistinguishable from the one it replaced. Each corner
now holds three stills nudged inward by 9% of the frame per depth, and
the newest one moves to the top of the layer, leaving the edges of the
ones below visible. Fill order is slot-major, so a new shot never lands
where the previous shot at that corner was.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tg15tLAuT6NrCE1ZxKVgDB
This commit is contained in:
simon
2026-08-09 22:19:30 +02:00
co-authored by Claude Opus 5
parent e031ebda1a
commit 1afe6580ae
+53 -17
View File
@@ -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();
};
}