Files
mintel.me/apps/web/src/components/film/motion.ts
T
mmintelandClaude Opus 5.5 f50bb553e8 feat: the automation chapter opens with a scroll film
The homepage read as sections to scroll past. The automation chapter now
opens the way the social media films do: one continuous shot on a dark
stage that stays in place while the visitor scrolls. Scrolling moves the
camera (close on a dot hopping between two squares, pull back to a whole
office carrying things from tool to tool, freeze, lines take over) and
changes the statement; a second clock keeps the dots moving when the
visitor stops. The statements are the approved ones from the film.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 10:09:50 +02:00

56 lines
1.9 KiB
TypeScript

/**
* Motion helpers for the scroll films. All pure functions of a frame or a time,
* the same ones the social media films use, so both move alike.
*/
export type Point = { readonly x: number; readonly y: number };
const clamp01 = (value: number): number => Math.min(1, Math.max(0, value));
/** 0 before `from`, 1 after `to`, linear in between. */
export const progress = (frame: number, from: number, to: number): number =>
clamp01((frame - from) / (to - from));
export const mix = (from: number, to: number, t: number): number =>
from + (to - from) * t;
export const inOut = (t: number): number =>
t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2;
export const out = (t: number): number => 1 - (1 - t) ** 3;
const easeInOut = (t: number): number =>
t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2;
const round = (value: number): number => Math.round(value * 100) / 100;
const wrap = (index: number, length: number): number =>
((index % length) + length) % length;
/**
* Where something is that hops from stop to stop in an arc, round and round.
* It rests on `stops[0]` at time 0 and reaches the next stop every
* `framesPerHop`. The arc rises by `arcRatio` times the distance covered.
*/
export const hopAt = (
time: number,
stops: readonly Point[],
framesPerHop: number,
arcRatio: number,
): Point => {
const hop = Math.floor(time / framesPerHop);
const from = stops[wrap(hop, stops.length)];
const to = stops[wrap(hop + 1, stops.length)];
if (!from || !to) return { x: 0, y: 0 };
const t = easeInOut(time / framesPerHop - hop);
const distance = Math.hypot(to.x - from.x, to.y - from.y);
// Rounded: sine and square root differ in the last digit between the server
// and the browser, and the first frame is rendered by both.
return {
x: round(mix(from.x, to.x, t)),
y: round(
mix(from.y, to.y, t) - Math.sin(Math.PI * t) * distance * arcRatio,
),
};
};