+ );
+};
diff --git a/apps/web/src/components/film/motion.ts b/apps/web/src/components/film/motion.ts
new file mode 100644
index 0000000..59cd774
--- /dev/null
+++ b/apps/web/src/components/film/motion.ts
@@ -0,0 +1,55 @@
+/**
+ * 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,
+ ),
+ };
+};
diff --git a/apps/web/src/content/de.ts b/apps/web/src/content/de.ts
index 3bb63d2..c919ec7 100644
--- a/apps/web/src/content/de.ts
+++ b/apps/web/src/content/de.ts
@@ -277,6 +277,17 @@ export const de: Content = {
},
},
automation: {
+ film: {
+ description:
+ "Ein Punkt springt zwischen zwei Quadraten hin und her. Die Kamera fährt zurück: Es sind viele Reihen, in denen jemand Dinge von Programm zu Programm trägt. Dann verbinden Linien die Programme, und die Arbeit läuft von selbst.",
+ statements: [
+ ["Sieht aus wie", "Pingpong."],
+ ["Ist aber", "Ihr Büro."],
+ ["Jede Bestellung.", "Von Hand."],
+ ["Oder Ihre Tools", "reden miteinander."],
+ ["Und Ihr Team", "hat Zeit."],
+ ],
+ },
label: "Prozessautomatisierung",
title: { lead: "Lassen Sie die Systeme", rest: "das Kopieren übernehmen." },
intro:
diff --git a/apps/web/src/content/en.ts b/apps/web/src/content/en.ts
index 824e95a..4f10a86 100644
--- a/apps/web/src/content/en.ts
+++ b/apps/web/src/content/en.ts
@@ -273,6 +273,17 @@ export const en: Content = {
},
},
automation: {
+ film: {
+ description:
+ "A dot hops back and forth between two squares. The camera pulls back: there are many rows in which someone carries things from tool to tool. Then lines connect the tools and the work runs by itself.",
+ statements: [
+ ["Looks like", "ping-pong."],
+ ["It's your", "office."],
+ ["Every order.", "By hand."],
+ ["Or your tools", "talk to each other."],
+ ["And your team", "has time."],
+ ],
+ },
label: "Process automation",
title: { lead: "Let the systems", rest: "do the copying." },
intro:
diff --git a/apps/web/src/content/types.ts b/apps/web/src/content/types.ts
index 3dddb37..54170ba 100644
--- a/apps/web/src/content/types.ts
+++ b/apps/web/src/content/types.ts
@@ -194,6 +194,11 @@ export type Content = {
readonly scenes: SceneLabels;
};
readonly automation: {
+ /** The film that opens the chapter: what it shows, and its five statements in order. */
+ readonly film: {
+ readonly description: string;
+ readonly statements: readonly (readonly string[])[];
+ };
readonly label: string;
readonly title: SplitTitle;
readonly intro: string;