diff --git a/video/README.md b/video/README.md
index cf3f723..d2f22fa 100644
--- a/video/README.md
+++ b/video/README.md
@@ -49,18 +49,29 @@ liegt auf einer eigenen Spur über dem Bild und der Rest ist durchsichtig.
| Komponente | Bilder ein / aus | bei 25 fps | bei 30 fps |
|---|---|---|---|
-| `bauchbinde-noah`, `bauchbinde-betrieb` | 20 / 12 | 0,80 s / 0,48 s | 0,67 s / 0,40 s |
-| `chip-generation` | 16 / 10 | 0,64 s / 0,40 s | 0,53 s / 0,33 s |
+| `bauchbinde-noah` | 20 / 12 | 0,80 s / 0,48 s | 0,67 s / 0,40 s |
| alle `satz-*` | 18 / 11 | 0,72 s / 0,44 s | 0,60 s / 0,37 s |
| beide `stapel-*` | 22 / 13 | 0,88 s / 0,52 s | 0,73 s / 0,43 s |
-| `ring-kern`, `wabenfeld` | 12 / 7 | 0,48 s / 0,28 s | 0,40 s / 0,23 s |
+| `ring-kern` | 12 / 7 | 0,48 s / 0,28 s | 0,40 s / 0,23 s |
| `logo-bug` | 14 / 8 | 0,56 s / 0,32 s | 0,47 s / 0,27 s |
-| `endkarte` | 20 / 12 | 0,80 s / 0,48 s | 0,67 s / 0,40 s |
+| `wabenfeld` | **50 / 30** | **2,00 s / 1,20 s** | **1,67 s / 1,00 s** |
Mehr Bilder heißt längere Bewegung, nicht flüssigere: die Staffelung steckt in
Anteilen des Verlaufs, nicht in Zeitwerten. Wer eine langsamere Einblendung
will, erhöht `bilder` bei der Komponente in `komponenten.html` und baut neu.
+**Das Wabenfeld tanzt aus der Reihe, und das ist Absicht.** Alle anderen
+Einblendungen setzen ein Stichwort, während Noah spricht — die dürfen nichts
+wollen und blenden deshalb schlicht auf. Das Wabenfeld ist das Bild zur
+Aussage und baut sich auf: die Nabe steht, dann wachsen die Waben in fünf
+Wellen von innen nach außen, und die Stränge ziehen jeweils kurz danach nach.
+Rückwärts zieht sich alles wieder in die Marke zurück.
+
+Die Bewegung ist die `introTimeline` aus `HoneycombSection.tsx` der Website,
+nicht neu erfunden: `scale` von 0,16 auf 1, `back.out(1.75)`, gestaffelt nach
+Abstand zur Mitte, danach die Stränge mit einer eigenen Deckungsrampe. Wer an
+der Zeitrechnung dreht, findet sie als `WABE_ZEIT` in `komponenten.html`.
+
> **Nicht skalieren, wo es nicht sein muss.** Die Dateien sind in doppelter
> Größe gerendert (`FAKTOR` in `bauen.mjs`). In einer Zeitleiste mit
> 1920 × 1080 setzt du sie also auf rund 50 % — dann sitzt jeder Pixel, und
@@ -71,8 +82,6 @@ will, erhöht `bilder` bei der Komponente in `komponenten.html` und baut neu.
| Schlüssel | Was es zeigt | Wofür im Skript |
|---|---|---|
| `bauchbinde-noah` | Noah Geerkens · Gründer | wenn er sich vorstellt |
-| `bauchbinde-betrieb` | Geerkens Haustechnik · SHK, vierte Generation | dazu oder statt dessen |
-| `chip-generation` | gedrehter Stempel „Vierte Generation · SHK" | zum Betrieb |
| `satz-erp-bleibt` | „Dein ERP **bleibt**." | der Einwand, direkt beantwortet |
| `satz-kein-ersatz` | „Kein **Ersatz**." / „Der Aufbau drumherum." | die Kernaussage |
| `satz-miteinander-reden` | „Die Tools reden **miteinander**." | was gefehlt hat |
@@ -82,7 +91,11 @@ will, erhöht `bilder` bei der Komponente in `komponenten.html` und baut neu.
| `ring-kern` | der Kern bleibt, HiveGrid liegt darum | „kein Ersatz, sondern der Aufbau drumherum" |
| `wabenfeld` | die Marke in der Mitte, die Programme drumherum | „dass die Tools endlich miteinander reden" |
| `logo-bug` | Wortmarke für die Ecke | durchgehend oder am Anfang |
-| `endkarte` | „Vom Handwerker für **Handwerker**" | Schlussbild |
+
+> Eine Bauchbinde für den Betrieb ließ sich über `?name=` und `?rolle=` ohnehin
+> aus der ersten erzeugen und ist deshalb weg, mit ihr der Stempel „Vierte
+> Generation". Eine Endkarte gab es auch einmal — ein Schlussbild gehört in den
+> Schnitt und nicht als Einblendung darauf.
**Wenig Text ist Absicht.** Noah spricht das Skript, die Einblendung setzt das
Stichwort. Zwei Sätze auf einer Karte lesen sich nicht, während jemand redet.
@@ -128,7 +141,26 @@ Ein paar Entscheidungen, die dahinterstehen:
- **Die Ausblendung ist die Einblendung rückwärts**, nur kürzer. Ein eigener
Ausgang sähe eine Spur besser aus und kostete doppelt so viele Bilder.
-## Zwei Fallen, in die wir hier schon getappt sind
+## Drei Fallen, in die wir hier schon getappt sind
+
+**`document.fonts.ready` war erfüllt, bevor es etwas zu laden gab.** Die Seite
+hat auf die Schrift gewartet, *bevor* die Karte im DOM stand — zu dem Zeitpunkt
+brauchte niemand Manrope, es lief also kein Ladevorgang, und das Versprechen
+löste sofort auf. Danach wurde die Karte gebaut, Chrome fing an zu laden, und ob
+es bis zur Messung fertig war, entschied der Zufall. Dieselbe Komponente
+hintereinander gemessen kam abwechselnd 984 und 1010 px breit heraus.
+
+Das ist die Falle aus `rules.md` §4, nur eine Ebene früher: dort steht „auf
+`document.fonts.ready` warten", und genau das tat die Seite — nur zu früh.
+`document.fonts.load()` holt eine Schriftlage unabhängig davon, ob gerade jemand
+sie benutzt; erst danach ist `ready` eine Aussage. Dieselbe Wette steckt in
+jedem `
` mit `width:auto`, deshalb wartet der Aufbau jetzt auch auf die
+Bilder.
+
+> **Woran man es merkt:** an den Maßen in `liste.json`. Eine Komponente, die
+> beim Neubau plötzlich 2 bis 4 Prozent schmaler wird, hat keine Textänderung
+> hinter sich, sondern die Ersatzschrift. Die reinen Zeichnungen (`ring-kern`,
+> `wabenfeld`) verraten es nicht — ihr SVG hat feste Maße.
**Der Schatten war nicht da.** Die Karte trug `clip-path` für die
abgeschnittenen Ecken und `box-shadow` für den Schatten — und `clip-path`
diff --git a/video/bauen.mjs b/video/bauen.mjs
index 08c1f4e..9b2dd5e 100644
--- a/video/bauen.mjs
+++ b/video/bauen.mjs
@@ -145,14 +145,10 @@ try {
const kasten = await werte(cdp, "window.KOMP_KASTEN()");
- // Standbild: der gehaltene Zustand.
- const still = await aufnehmen(cdp, kasten);
- await writeFile(path.join(pngDir, k.key + ".png"), still);
- dateien++; bytes += still.length;
-
// Sequenzen. Ein bis der Kasten steht, aus rueckwaerts und knapper.
const einN = k.bilder;
const ausN = Math.max(4, Math.round(k.bilder * AUS_ANTEIL));
+ let gehalten = null;
for (const [name, anzahl, richtung] of [["ein", einN, 1], ["aus", ausN, -1]]) {
const ordner = path.join(seqDir, k.key + "-" + name);
await rm(ordner, { recursive: true, force: true });
@@ -161,10 +157,25 @@ try {
const p = i / (anzahl - 1);
await werte(cdp, "window.setT(" + (richtung > 0 ? p : 1 - p) + ")");
const bild = await aufnehmen(cdp, kasten);
+ if (richtung > 0 && i === anzahl - 1) gehalten = bild;
await writeFile(path.join(ordner, `${k.key}-${name}_${String(i).padStart(4, "0")}.png`), bild);
dateien++; bytes += bild.length;
}
}
+
+ /* Das Standbild ist das letzte Bild der Einblendung, nicht eine eigene
+ Aufnahme. Der ganze Dreiteiler im Schnitt haengt daran, dass -ein, das
+ Standbild und -aus an den Stoessen dasselbe Bild zeigen; als zweite
+ Aufnahme war das bisher nur wahr, weil es zufaellig stimmte.
+
+ Beim Wabenfeld stimmte es dann nicht mehr. Dort tragen die Zellen
+ waehrend der Bewegung ein transform-Attribut, das am Ende wieder
+ weggenommen wird -- danach rastert Chrome eine Spur anders als bei einem
+ Element, das nie eines hatte. Der Unterschied lag bei einer Stufe auf
+ verstreuten Pixeln, also unsichtbar, aber die Zusage war weg. So wird
+ sie eingehalten, statt geprueft: es ist dieselbe Datei. */
+ await writeFile(path.join(pngDir, k.key + ".png"), gehalten);
+ dateien++; bytes += gehalten.length;
const zeile = `${k.key.padEnd(24)} ${String(kasten.w * FAKTOR).padStart(5)} x ` +
`${String(kasten.h * FAKTOR).padEnd(5)} still + ${einN} ein + ${ausN} aus`;
console.log(" " + zeile);
diff --git a/video/komponenten.html b/video/komponenten.html
index 1ce8f2f..b13af6a 100644
--- a/video/komponenten.html
+++ b/video/komponenten.html
@@ -112,22 +112,6 @@
background: var(--gelb);
}
- /* ─── Chip ───────────────────────────────────────────────────────────────
- Das gedrehte Schild der Landingpage. Monospace und weit gesperrt, damit
- es neben der Manrope als Stempel liest und nicht als zweite Ueberschrift. */
- .chip {
- display: inline-block; padding: 22px 34px;
- background: var(--gelb); color: var(--gelb-dunkel);
- font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
- font-size: 26px; font-weight: 850; letter-spacing: .12em;
- text-transform: uppercase; white-space: nowrap;
- transform: rotate(-8deg);
- clip-path: polygon(
- 17px 0, calc(100% - 17px) 0, 100% 17px, 100% calc(100% - 17px),
- calc(100% - 17px) 100%, 17px 100%, 0 calc(100% - 17px), 0 17px
- );
- }
-
/* ─── Satzkarte ──────────────────────────────────────────────────────────
Eine Aussage, sonst nichts. Noah spricht den Satz, die Karte setzt das
eine Wort, an dem er haengt -- deshalb hoechstens ein Akzent, wie in
@@ -181,19 +165,6 @@
text-transform: uppercase; color: var(--text2); white-space: nowrap;
}
- /* ─── Endkarte ───────────────────────────────────────────────────────────
- Der Abbinder. Eine Zeile Marke, eine Zeile Aufforderung, sonst nichts. */
- .ende { padding: 52px 60px; text-align: center; }
- .ende h1 {
- font-size: 62px; font-weight: 800; letter-spacing: -.032em;
- line-height: 1.06; color: var(--tinte); white-space: nowrap;
- }
- .ende h1 .a { color: var(--gelb); }
- .ende p {
- margin-top: 20px;
- font-size: 27px; font-weight: 600; color: var(--text2); white-space: nowrap;
- }
-
/* ─── Zeichenflaeche ─────────────────────────────────────────────────────
Die uebernommenen Zeichnungen fuellen ihre Buehne selbst aus. Kein
Layout-CSS hier -- in post2-alt.html hat genau das dreimal zugeschlagen
@@ -428,12 +399,9 @@ const SLIDE_GRUND = "#FFFFFF";
durch etwas anderes ersetzt, sondern sind wieder gewoehnliche leere
Waben.
- Alles Bewegte: Sog, Pakete, Hover. Ein Standbild hat davon nichts.
- - Das Feld nach aussen. Die Website fuellt einen ganzen Abschnitt bis
- Ring 18, hier reicht es bis knapp ueber den Rand der Slide.
-
- Der Ausschnitt ist so gewaehlt, dass die Marken frei stehen und nur die
- leeren Waben angeschnitten werden. Waagerecht laeuft das Feld bewusst
- ueber beide Slide-Kanten, senkrecht blendet es aus (WABE_MASKE). */
+ - Das Feld aus leeren Waben. Die Website fuellt damit einen ganzen
+ Abschnitt bis Ring 18; hier bleibt nur, was belegt ist. Der Grund steht
+ bei visWabenfeld(). */
const WABE_R = 62; // Umkreisradius, alles leitet sich davon ab
const WABE_B = 2 * WABE_R; // Breite Spitze zu Spitze
const WABE_ZELLE = 0.84; // laesst die Fuge zwischen zwei Waben offen
@@ -511,7 +479,12 @@ const SLIDE_GRUND = "#FFFFFF";
const STRANG_FUSS = 14, STRANG_KOPF = 16, STRANG_TAILLE = 4.5, STRANG_TIEF = 8;
const WABE_INNEN = WABE_R * WABE_ZELLE * 0.8660254; // Inkreis einer Wabenflaeche
- function strangForm(a, b) {
+ /* Nur der Pfad, nicht das fertige Element. Anders als in den Post-Seiten
+ wird die Form hier je Bild neu gerechnet: der Strang waechst, und dabei
+ wandert eines seiner Enden. Wer die Zeichnung in post2-alt.html oder
+ post3.html anfasst, aendert dort weiterhin strangForm() -- die Rechnung
+ darin ist Zeichen fuer Zeichen dieselbe wie hier. */
+ function strangPfad(a, b) {
const dx = b.x - a.x, dy = b.y - a.y;
const l = Math.hypot(dx, dy) || 1;
const ux = dx / l, uy = dy / l, nx = -uy, ny = ux;
@@ -521,13 +494,12 @@ const SLIDE_GRUND = "#FFFFFF";
const c1x = a.x + ux * l * 0.35, c1y = a.y + uy * l * 0.35;
const c2x = a.x + ux * l * 0.65, c2y = a.y + uy * l * 0.65;
const f = v => v.toFixed(2);
- return '';
+ f(c1y - ny * wc) + " " + f(a.x - nx * STRANG_FUSS) + " " + f(a.y - ny * STRANG_FUSS) + "Z";
}
/* Der Punkt, der `weit` von einer Wabe aus in Richtung einer anderen liegt.
@@ -547,28 +519,11 @@ const SLIDE_GRUND = "#FFFFFF";
"L" + f(cx - rr / 2) + " " + f(cy - hh) + "L" + f(cx + rr / 2) + " " + f(cy - hh) + "Z";
}
- /* Der Ausschnitt, und damit der Massstab. Er ist schmaler als die Slide
- (880 statt 1080), das SVG wird also auf 1080 gezogen und alles darin um
- den Faktor 1,227 groesser. Das ist die Zoomstufe: eng geschnitten, weil
- ein Feld aus leeren Waben rundherum nur Platz kostet.
-
- Senkrecht sitzt der Kasten nicht auf 0, sondern auf der Mitte des
- Marken-Haufens. Der liegt bei y 27 und nicht bei y 0, weil unten eine
- Reihe mehr belegt ist als oben; auf 0 zentriert stuende oben zu viel
- Leerraum und unten zu wenig.
-
- Die Grenzen sind gerechnet, nicht geschaetzt (Werte vor dem Faktor):
- Waben x +/-341, y -268 bis 322
- Marken x +/-308, y -243 bis 297
- Damit bleiben zu den Slide-Kanten 122 px, zur naechsten Marke 162 px --
- beides ueber der Sicherheitszone von 92 px aus rules.md 5. */
- const WABE_VB = { x: -440, y: -323, w: 880, h: 700 };
- /* Wie weit das Feld oben und unten ausblendet, in Anteilen der Hoehe. Die
- Zonen duerfen nur ueber leeren Waben liegen, sonst blasst eine Marke aus:
- mit 0,07 setzt die Blende bei y -274 und 328 ein, die aeusserste Wabe
- reicht bis -268 und 322. Sechs Einheiten Luft, mehr nicht. Wer den Wert
- erhoeht oder den Ausschnitt aendert, muss das nachrechnen. */
- const WABE_MASKE = 0.07;
+ /* Hier standen der feste Ausschnitt WABE_VB und die Ausblendung
+ WABE_MASKE. Beide gehoerten zum Feld aus leeren Waben, und das gibt es in
+ dieser Fassung nicht mehr: der Ausschnitt wird in visWabenfeld() aus den
+ belegten Waben gerechnet. Im Post stehen beide weiterhin, dort tragen
+ sie. */
/* Die Marken-Kaesten in Zeichenkoordinaten, je Slide. Der Export prueft sie
einzeln nach, siehe wabenZonen(). Je Slide und nicht als eine Liste: wer
@@ -576,106 +531,172 @@ const SLIDE_GRUND = "#FFFFFF";
die Kaesten der ersten ueberschrieben. */
const wabenMarkenBoxen = new Map();
+ /* ─── Die Wabe, freigestellt ───────────────────────────────────────────────
+ Anders als im Post: dort liegt hinter den belegten Waben ein blasses Feld
+ aus leeren, das nach aussen ausblendet. Auf dem weissen Blatt traegt das
+ die Zeichnung und gibt ihr Grund. Ueber Videomaterial ist es im Weg -- ein
+ Schleier aus halbdurchsichtigen Sechsecken auf einer Baustellenaufnahme
+ sieht nach Fehler aus, nicht nach Absicht.
+
+ Hier bleiben deshalb nur die belegten Waben, ihre Straenge und die Marken.
+ Zwei Dinge fallen damit mit weg:
+
+ - Die Ausblendung. Sie war dafuer da, das Feld nach oben und unten
+ auslaufen zu lassen; ohne Feld haette sie nichts mehr auszublenden
+ ausser den aeusseren Marken selbst.
+ - Der feste Ausschnitt WABE_VB. Der Kasten wird jetzt aus den belegten
+ Waben gerechnet, damit kein leerer Rand stehenbleibt. */
+ /* Die Staffelung, in Anteilen des Verlaufs. Zwischen der Nabe und der
+ aeussersten Wabe liegen fuenf verschiedene Abstaende; "welle" verteilt
+ sie darauf. Mit den Fenstern darunter faengt die letzte Zelle bei 0,46
+ an und der letzte Strang ist bei 0,94 fertig -- die letzten Bilder stehen
+ also still, und der Stoss zum Standbild ist ein Halten, kein Schnapp.
+
+ Wer hier dreht: welle + nach + strang darf 1 nicht ueberschreiten, sonst
+ ist die Bewegung am Ende der Sequenz noch unterwegs und das letzte Bild
+ ist nicht mehr das Standbild. Genau daran haengen die unsichtbaren
+ Stoesse im Schnitt. */
+ const WABE_ZEIT = {
+ welle: 0.46, // wie weit die Staffelung der Zellen nach aussen reicht
+ zelle: 0.26, // Fenster einer einzelnen Zelle
+ nach: 0.14, // Vorsprung einer Zelle vor ihrem Strang
+ strang: 0.34, // Fenster eines Strangs
+ };
+
function visWabenfeld(j) {
const boxen = [];
wabenMarkenBoxen.set(j, boxen);
const mitte = wabeZellen[0];
- let feld = "", flaechen = "", straenge = "", marken = "";
- wabeZellen.forEach(z => {
- // Was ganz ausserhalb des Ausschnitts liegt, wird nicht gezeichnet:
- // ".vis svg" traegt overflow:visible, ein Beschnitt am Rahmen findet
- // also nicht von selbst statt.
- if (z.x < WABE_VB.x - WABE_B || z.x > WABE_VB.x + WABE_VB.w + WABE_B) return;
- if (z.y < WABE_VB.y - WABE_B || z.y > WABE_VB.y + WABE_VB.h + WABE_B) return;
- if (z.nr === 1) return; // die Nabe kommt zuletzt
+ /* Nur die belegten Waben, kein blasses Feld dahinter. Die Nabe ist nicht
+ dabei: die kommt zuletzt und eine Spur groesser. */
+ const aktive = wabeZellen.filter(z => z.nr !== 1 && wabeAktiv(z.nr));
- if (wabeAktiv(z.nr)) {
- flaechen += '';
- } else {
- /* Die leeren Waben nach aussen hin blasser, eine Stufe je Ring. Auf
- der Website macht das ein Verlauf ueber den ganzen Abschnitt; der
- haette hier nichts zu tun, weil das Feld nur bis Ring 5 reicht.
- Nicht "deck" nennen -- so heisst weiter oben das Blatt im DOM. */
- const staerke = [0.10, 0.10, 0.10, 0.085, 0.06, 0.04][Math.min(z.ring, 5)];
- feld += '';
- }
- });
+ /* Der Abstand zur Nabe steuert die ganze Staffelung. Auf der Website wird
+ activeRevealNodes nach genau diesem Abstand sortiert, die Nabe steht mit
+ 0 vorn. Weil unsere Belegung nur fuenf verschiedene Abstaende kennt,
+ sind es fuenf saubere Wellen von innen nach aussen und nicht achtzehn
+ einzelne Ereignisse -- das liest sich als eine Bewegung. */
+ const abstand = z => Math.hypot(z.x - mitte.x, z.y - mitte.y);
+ const weiteste = Math.max(...aktive.map(abstand)) || 1;
+ const welle = z => WABE_ZEIT.welle * abstand(z) / weiteste;
+ const n3 = v => v.toFixed(3);
- /* Straenge von der Nabe zu ihren aktiven Nachbarn. */
+ /* Ein Strang als Element. Er behaelt die Endpunkte, mit denen er
+ gezeichnet wurde, und merkt sich ueber data-mv, welches der beiden
+ waehrend der Bewegung wandert: immer das aeussere. Damit laeuft die
+ Streckung in dieselbe Richtung wie die Welle, nach aussen -- und
+ rueckwaerts zieht sich das Feld in die Nabe zurueck, statt von aussen
+ her zu zerfallen. Der Verzug haengt an der Zelle, auf die der Strang
+ zulaeuft: er kommt an, kurz nachdem sie steht. */
+ const strangEl = (a, b, mv, ziel) =>
+ '';
+
+ let straenge = "";
+
+ /* Straenge von der Nabe zu ihren aktiven Nachbarn. Gezeichnet wird von der
+ Wabe zur Nabe, das aeussere Ende ist also a. */
wabeZellen.filter(z => z.ring === 1 && wabeAktiv(z.nr)).forEach(z => {
- straenge += strangForm(
+ straenge += strangEl(
wabeZu(z, mitte, WABE_INNEN - STRANG_TIEF),
- wabeZu(mitte, z, WABE_R * WABE_MITTE * 0.8660254 - STRANG_TIEF));
+ wabeZu(mitte, z, WABE_R * WABE_MITTE * 0.8660254 - STRANG_TIEF), "a", z);
});
/* Und zwischen je zwei aktiven Waben, die aneinandergrenzen. Erst das
macht aus den Einzelkacheln ein Netz: die Programme haengen nicht nur
- an der Mitte, sondern auch aneinander. */
- const aktive = wabeZellen.filter(z => z.ring > 0 && wabeAktiv(z.nr));
+ an der Mitte, sondern auch aneinander. Welches Ende wandert, entscheidet
+ hier der Abstand zur Nabe. */
aktive.forEach((a, i) => aktive.slice(i + 1).forEach(b => {
const dq = b.q - a.q, dr = b.r - a.r;
if (!WABE_RICHTUNG.some(([q, r]) => q === dq && r === dr)) return;
- straenge += strangForm(
+ const bAussen = abstand(b) >= abstand(a);
+ straenge += strangEl(
wabeZu(a, b, WABE_INNEN - STRANG_TIEF),
- wabeZu(b, a, WABE_INNEN - STRANG_TIEF));
+ wabeZu(b, a, WABE_INNEN - STRANG_TIEF),
+ bAussen ? "b" : "a", bAussen ? b : a);
}));
- /* Die Marken. Als mit Datenadresse und nicht inline: die Dateien
- von Aircall und Google Calendar bringen eigene