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