Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/thatstupidcomputer / apps/web/src/animation/stage/engine.mjs |
| Date | Not specified in source |
| Repository | Janady13/thatstupidcomputer |
| Revision | 57555cde46910102dd25d6c1c1fb2c359e9655bd |
| Branch | main |
| Path or archive member | apps/web/src/animation/stage/engine.mjs |
| Original SHA-256 | b56e24c83631fd8b45df4600194e90e17e097575c3b31629907ccc3ab4ed7ec4 |
| Git blob SHA | b26c53c44d6c9e00233f2892bf7b32930e1ab3f7 |
| Reading-copy SHA-256 | b56e24c83631fd8b45df4600194e90e17e097575c3b31629907ccc3ab4ed7ec4 |
Recovered source transcript
Download complete source text ↓162 lines · 10.8 KB
/** Deterministic stage math. Pixels internally; public command targets are 0..1.
* All travel is finite. No random numbers, clocks, DOM, timers or mirrored artwork.
*/
import { PERFORMANCES } from '../performances/catalog.mjs';
export const COMMAND_KINDS = Object.freeze(['enterLeft', 'enterRight', 'exitLeft', 'exitRight', 'walkTo', 'runTo', 'roam', 'peekLeft', 'peekRight', 'jump', 'home', 'return', 'cancel', 'replay']);
export const MAX_DELTA = 0.05;
export const MAX_SEQUENCE = 12;
const finite = (value, fallback = 0) => Number.isFinite(value) ? value : fallback;
const clamp = (value, low, high) => Math.min(high, Math.max(low, finite(value, low)));
const lerp = (a, b, t) => a + (b - a) * t;
const smooth = t => t * t * (3 - 2 * t);
function dimensions(value = {}) {
return {
width: clamp(value.width ?? 720, 0, 20_000), height: clamp(value.height ?? 400, 0, 20_000),
actorWidth: clamp(value.actorWidth ?? 180, 1, 10_000), actorHeight: clamp(value.actorHeight ?? 230, 1, 10_000),
};
}
export function boundsFor(size) {
const { width, height, actorWidth, actorHeight } = dimensions(size);
const minX = actorWidth > width ? (width - actorWidth) / 2 : 0;
const minY = actorHeight > height ? (height - actorHeight) / 2 : 0;
return { minX, maxX: actorWidth > width ? minX : width - actorWidth, minY, maxY: actorHeight > height ? minY : height - actorHeight };
}
export function normalizedPosition(state) {
const bounds = boundsFor(state.size);
return {
x: bounds.maxX > bounds.minX ? clamp((state.x - bounds.minX) / (bounds.maxX - bounds.minX), 0, 1) : 0.5,
y: bounds.maxY > bounds.minY ? clamp((state.y - bounds.minY) / (bounds.maxY - bounds.minY), 0, 1) : 1,
};
}
function targetPoint(state, target, defaultX = 0.5) {
const bounds = boundsFor(state.size);
const point = typeof target === 'number' ? { x: target } : target || {};
return { x: lerp(bounds.minX, bounds.maxX, clamp(point.x ?? defaultX, 0, 1)), y: lerp(bounds.minY, bounds.maxY, clamp(point.y ?? 1, 0, 1)) };
}
export function createStage(size = {}) {
const actual = dimensions(size);
const bounds = boundsFor(actual);
return { size: actual, x: lerp(bounds.minX, bounds.maxX, 0.5), y: bounds.maxY, visible: true, phase: 'idle', active: null, queue: [], elapsed: 0, commandId: null, commandKind: null, lastCommand: null, revision: 0, velocity: { x: 0, y: 0 } };
}
function segment(state, to, style = 'walk', extra = {}) {
const distance = Math.hypot(to.x - state.x, to.y - state.y);
const running = style === 'run';
return { from: { x: state.x, y: state.y }, to, duration: clamp(distance / (running ? 360 : 145), 0.16, running ? 3 : 5), style, pose: `${running ? 'Run' : 'Walk'}${to.x < state.x ? 'Left' : 'Right'}`, ...extra };
}
function startSegments(state, segments) {
const bounded = segments.slice(0, MAX_SEQUENCE);
return { ...state, active: bounded[0] || null, queue: bounded.slice(1), elapsed: 0, phase: bounded.length ? (bounded[0].style === 'settle' ? 'settling' : 'moving') : (state.visible ? 'idle' : 'hidden'), revision: state.revision + 1 };
}
function cloneCommand(command) {
return { id: command.id, kind: command.kind, ...(command.target === undefined ? {} : { target: typeof command.target === 'number' ? finite(command.target, 0.5) : { x: finite(command.target?.x, 0.5), y: finite(command.target?.y, 1) } }) };
}
export function issueCommand(previous, command) {
if (!command || !COMMAND_KINDS.includes(command.kind)) return previous;
let intent = command.kind === 'replay' && previous.lastCommand ? { ...previous.lastCommand, id: command.id } : cloneCommand(command);
if (intent.kind === 'replay') return previous;
let state = { ...previous, active: null, queue: [], elapsed: 0, velocity: { x: 0, y: 0 }, commandId: command.id, commandKind: intent.kind, revision: previous.revision + 1 };
if (intent.kind === 'cancel') return { ...state, phase: state.visible ? 'idle' : 'hidden' };
state.lastCommand = cloneCommand(intent);
const bounds = boundsFor(state.size);
const home = targetPoint(state, intent.target);
const { width, actorWidth } = state.size;
if (intent.kind === 'enterLeft' || intent.kind === 'enterRight') {
state = { ...state, x: intent.kind === 'enterLeft' ? -actorWidth - 4 : width + 4, y: home.y, visible: true };
return startSegments(state, [segment(state, home, 'walk', { style: 'enter' })]);
}
if (intent.kind === 'exitLeft' || intent.kind === 'exitRight') {
return startSegments({ ...state, visible: true }, [segment(state, { x: intent.kind === 'exitLeft' ? -actorWidth - 4 : width + 4, y: state.y }, 'walk', { style: 'exit', hideAfter: true })]);
}
if (intent.kind === 'peekLeft' || intent.kind === 'peekRight') {
const left = intent.kind === 'peekLeft';
state = { ...state, x: left ? -actorWidth - 4 : width + 4, y: bounds.maxY, visible: true };
const point = { x: left ? -actorWidth * 0.58 : width - actorWidth * 0.42, y: bounds.maxY };
const entry = segment(state, point, 'walk', { duration: 0.5 });
const hold = { from: point, to: point, duration: PERFORMANCES.Suspicious.seconds, style: 'hold', pose: 'Suspicious' };
return startSegments(state, [entry, hold]);
}
if (intent.kind === 'jump') {
const from = { x: clamp(state.x, bounds.minX, bounds.maxX), y: clamp(state.y, bounds.minY, bounds.maxY) };
state = { ...state, ...from, visible: true };
return startSegments(state, [
// Authored Jump owns the vertical body trajectory. Do not add a second
// world-space lift to its body-bob motion; preserve the whole acting clip.
{ from, to: from, duration: PERFORMANCES.Jump.seconds, style: 'jump', pose: 'Jump', height: 0 },
{ from, to: from, duration: PERFORMANCES.Land.seconds, style: 'land', pose: 'Land' },
]);
}
if (intent.kind === 'roam') {
const segments = [];
let cursor = { ...state, visible: true };
for (const [index, x] of [0.12, 0.83, 0.31, 0.68, 0.5].entries()) {
const to = targetPoint(cursor, x);
const next = segment(cursor, to, index === 2 ? 'run' : 'walk');
segments.push(next);
cursor = { ...cursor, ...to };
}
return startSegments({ ...state, visible: true }, segments);
}
return startSegments({ ...state, visible: true }, [segment(state, home, intent.kind === 'runTo' ? 'run' : 'walk')]);
}
export function hasStageWork(state) { return !!state.active && state.phase !== 'dragging'; }
export function tickStage(previous, delta, enabled = true) {
if (!enabled || !hasStageWork(previous)) return previous;
const dt = clamp(delta, 0, MAX_DELTA);
if (!dt) return previous;
const active = previous.active;
const elapsed = Math.min(active.duration, previous.elapsed + dt);
const t = clamp(elapsed / active.duration, 0, 1);
const progress = ['enter', 'exit'].includes(active.style) ? smooth(t) : active.style === 'settle' ? 1 - Math.pow(1 - t, 3) : t;
let x = lerp(active.from.x, active.to.x, progress);
let y = lerp(active.from.y, active.to.y, progress);
if (active.style === 'jump') y -= Math.sin(Math.PI * t) * finite(active.height);
if (active.style === 'settle') {
// Analytic decay with a finite deadline: no accumulated physics energy.
y = active.to.y + (active.from.y - active.to.y) * Math.exp(-7 * t) * (0.5 + 0.5 * Math.cos(3 * Math.PI * t));
const bounds = boundsFor(previous.size);
x = clamp(x, bounds.minX, bounds.maxX); y = clamp(y, bounds.minY, bounds.maxY);
}
let state = { ...previous, x, y, elapsed };
if (t < 1) return state;
state = { ...state, x: active.to.x, y: active.to.y, visible: active.hideAfter ? false : state.visible, velocity: { x: 0, y: 0 } };
if (state.queue.length) return startSegments(state, state.queue);
return { ...state, active: null, queue: [], elapsed: 0, phase: state.visible ? 'idle' : 'hidden', revision: state.revision + 1 };
}
function remap(value, oldMin, oldMax, nextMin, nextMax, actorRatio, collapsedRatio = 0.5) {
if (value < oldMin) return nextMin + (value - oldMin) * actorRatio;
if (value > oldMax) return nextMax + (value - oldMax) * actorRatio;
return oldMax === oldMin ? lerp(nextMin, nextMax, collapsedRatio) : lerp(nextMin, nextMax, clamp((value - oldMin) / (oldMax - oldMin), 0, 1));
}
export function resizeStage(previous, size) {
const actual = dimensions({ ...previous.size, ...size });
const old = boundsFor(previous.size), next = boundsFor(actual);
const map = point => ({ x: remap(point.x, old.minX, old.maxX, next.minX, next.maxX, actual.actorWidth / previous.size.actorWidth), y: clamp(remap(point.y, old.minY, old.maxY, next.minY, next.maxY, actual.actorHeight / previous.size.actorHeight, 1), next.minY, next.maxY) });
const mapSegment = value => ({ ...value, from: map(value.from), to: map(value.to), ...(value.height === undefined ? {} : { height: Math.max(0, Math.min(value.height, map(value.from).y - next.minY)) }) });
const position = map(previous);
// Overscan remapping is piecewise: interpolating remapped original endpoints
// could jump after a resize. Continue from the actual remapped position for
// the remaining duration, while keeping the same acting pose and deadline.
const active = previous.active ? { ...mapSegment(previous.active), from: position, duration: Math.max(0.001, previous.active.duration - previous.elapsed) } : null;
return { ...previous, ...position, size: actual, active, elapsed: 0, queue: previous.queue.map(mapSegment), velocity: { x: 0, y: 0 }, revision: previous.revision + 1 };
}
export function beginDrag(previous) {
const bounds = boundsFor(previous.size);
return { ...previous, x: clamp(previous.x, bounds.minX, bounds.maxX), y: clamp(previous.y, bounds.minY, bounds.maxY), visible: true, active: null, queue: [], elapsed: 0, commandId: null, commandKind: null, phase: 'dragging', velocity: { x: 0, y: 0 }, revision: previous.revision + 1 };
}
export function dragTo(previous, point, delta = 1 / 60) {
if (previous.phase !== 'dragging') return previous;
const bounds = boundsFor(previous.size), dt = clamp(delta, 0.004, MAX_DELTA);
const x = clamp(point?.x, bounds.minX, bounds.maxX), y = clamp(point?.y, bounds.minY, bounds.maxY);
return { ...previous, x, y, velocity: { x: clamp((x - previous.x) / dt, -900, 900), y: clamp((y - previous.y) / dt, -900, 900) } };
}
export function endDrag(previous) {
if (previous.phase !== 'dragging') return previous;
const bounds = boundsFor(previous.size);
const to = { x: clamp(previous.x + previous.velocity.x * 0.08, bounds.minX, bounds.maxX), y: bounds.maxY };
return startSegments(previous, [{ from: { x: previous.x, y: previous.y }, to, duration: PERFORMANCES.Land.seconds, style: 'settle', pose: 'Land' }]);
}
export function cancelDrag(previous) { return endDrag({ ...previous, velocity: { x: 0, y: 0 } }); }
export function stageStatus(state) {
return { phase: state.phase, commandId: state.commandId, commandKind: state.commandKind, complete: !state.active && state.phase !== 'dragging', position: normalizedPosition(state), pose: state.active?.pose ?? null, visible: state.visible };
}