Files
dejanandClaude Opus 5 b422b492e6 Slide 4: das Menue haengt sichtbar am Pluszeichen
Vier Sachen an derselben Slide.

BREITE AUS DEM INHALT. Sie stand als feste 470 in der Zeichenfunktion,
gemessen fuer Beschriftungen mit "senden", und liess ein Drittel
Leerraum rechts. Sie kommt jetzt aus dem laengsten Eintrag. Dabei
kippte die Zeichnung von quer auf hochkant, und ein hochkantes Bild
richtet sich nach der Hoehe: bei den alten 940 px wurde der Kasten
1093 hoch und lief in die Ueberschrift. Jetzt 680.

Gerechnet ist die Breite am sichtbaren Blatt und nicht am Kasten -- der
ist um den Stapelrand groesser als das, was man sieht, und mit seiner
Oberkante gerechnet stand die Zeichnung noch einmal 60 px zu hoch.

OBEN OHNE VERB. Wer das Menue ueber das Pluszeichen geoeffnet hat,
weiss, dass er etwas schickt; "Dokument senden", "Fotos und Videos
senden", "Audio senden", "Kontakt teilen" schreiben es viermal daneben.
Jetzt stehen dort die Dinge: Dokumente, Medien, Audio, Kontakt. Unten
bleibt das Verb, weil es etwas hinzufuegt -- "Termin" allein waere ein
Anhang, "Termin erstellen" ist eine Handlung. Das ist eine Abweichung
vom Composer und steht als solche in POST-6.md 4.

DIE EINGABEZEILE IST ZURUECK. Sie war weggenommen worden mit der
Begruendung, die Slide zeige ja, was man anhaengen kann. Das stimmte
nur, solange man wusste, worauf man schaut: ohne das Feld ist die Karte
eine Liste im Nichts. Das Pluszeichen traegt jetzt die gruene Marke wie
ein gedruecktes Element, der Zeiger steht darauf, und das Menue klappt
nach oben auf -- so wie im Werkzeug.

TEILEN STATT TELEFONHOERER. Ein Hoerer sagt "anrufen"; bei "Kontakt"
wird eine Karte weitergegeben. ShareNetwork aus demselben
Phosphor-bold-Satz, entnommen aus @phosphor-icons/react im
Website-Repo statt abgemalt. "phone" stand danach nirgends mehr und ist
aus der Tabelle heraus: 19 Zeichen, jedes in Gebrauch.

Das Bewegtbild ist nicht betroffen, es nutzt das Nachrichtenmenue.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U7RZ6ZpLD8Dp84Rtt4KHfv
2026-08-29 20:51:07 +02:00

939 lines
51 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Post 6 · Team-Chat
Fünfter Beitrag der Produktreihe, Termin **Mi 26.08.** laut Redaktionsplan
(von Mo 24.08. verschoben). Stichwort dort: „Ein Kanal pro Projekt, Foto rein,
Aufgabe raus."
Hell, Theme `modern`, Punktgrund, Text zentriert — dieselbe Anlage wie Post 4
und 5. Datei `post6.html`, Stand in `state/post6.json`, erreichbar über das
Projektmenü jeder Seite und über `uebersicht.html`.
**Fünf Slides, dazu ein Bewegtbild.** DSGVO und deutsche Server hätten einen
weiteren Punkt ergeben, dafür gibt es aber keinen Bildschirm. Der steht in der
Bildunterschrift, wo eine Aussage ohne Bild hingehört.
Dazu kommt die erste Animation im Repo. Sie ist **keine Slide**: sie steht in
`ANIMS` statt in `SLIDES`, liegt nicht im Streifen und wird vom Bildexport
nicht angefasst. Im Carousel ist sie ein eigenes Element, das mitkommen kann
oder eben nicht. Bedient wird sie im Bereich unter dem Canvas, gebaut mit
einem eigenen Knopf. Wie das funktioniert, steht im
[README unter „Animation"](./README.md#animation); was diese eine zeigt, in
§7 weiter unten.
---
## 1. Die fünf Slides und das Bewegtbild
| # | Tag | Überschrift | Was zu sehen ist |
|---|---|---|---|
| 1 | Team-Chat | Büro und Baustelle, **endlich im selben Chat.** | Kanalliste und Verlauf nebeneinander, mit Filterzeile und Eingabefeld |
| 2 | Die Kanäle | Ein Kanal **pro Projekt.** | Links das Durcheinander als Zeichnung, rechts die Kanalliste |
| 3 | Die Aufgabe | Foto rein, **Aufgabe raus.** | Baustellenfotos, die Aufgabe daraus, die Bestätigung im Projekt |
| 4 | Das Pluszeichen | Nicht nur **Text.** | Das Menü über der Eingabezeile, aus der es aufklappt |
| 5 | Bald verfügbar | Der Chat für unterwegs **wird gerade gebaut.** | Zwei Ausschnitte der Oberfläche, versetzt übereinander, mit dem Absperrband darüber |
Dazu, außerhalb des Streifens:
| Schlüssel | Tag | Überschrift | Was zu sehen ist |
|---|---|---|---|
| `animChat` | Bewegtbild | Chatnachrichten werden **Aufgaben im Projekt.** | 6 s Animation: eine Nachricht wird angeklickt, im Menü „Aufgabe erstellen" gewählt, und die Aufgabe klappt in der Nachricht auf |
**Slide 3 ist der Kern.** Der Redaktionsplan verlangt „Foto rein, Aufgabe
raus", und genau das liegt im Werkzeug als Abfolge vor: Tobias schickt die
Baustellenfotos, darauf folgt eine Nachricht mit angehängter Aufgabe, darauf
eine Systemzeile „Aufgabe „Materialliste prüfen" im Projekt erstellt". Die
letzte Zeile macht den Unterschied: die Aufgabe liegt danach im Projekt und
nicht nur im Verlauf.
Slide 1 trägt den ganzen Chat, weil sie im Profilraster eine Kachel von rund
150 px ist. Dort ist kein Wort lesbar; was ankommt, sind zwei Spalten mit
Blasen.
---
## 2. Die Bildunterschrift
```
Im Handwerk läuft die halbe Baustelle über WhatsApp. Das Foto von letzter Woche findet trotzdem keiner wieder.
In HiveGrid hat jedes Projekt seinen eigenen Kanal, mit Projektnummer. Daneben stehen Direktnachrichten und Gruppen, aber eben nicht alles in einem Verlauf.
Ein Foto von der Baustelle geht rein, alle sehen es sofort. Aus „mach das mal“ wird mit einem Klick eine echte Aufgabe: mit Frist, mit Namen, im Projekt.
Dokument, Sprachnachricht, Kontakt, Termin. Was sonst im Chat-Verlauf verschwindet, hängt hier am Projekt.
Kundendaten gehören nicht in eine WhatsApp-Gruppe. Bei HiveGrid liegen sie verschlüsselt auf deutschen Servern.
#handwerk #shk #handwerksbetrieb #digitalisierungimhandwerk #teamchat #baustelle #kommunikation #hivegrid
```
| Vorgabe | Ist |
|---|---|
| Haken in den ersten 125 Zeichen | erster Absatz, 110 |
| 300 bis 800 Zeichen | 644 |
| 5 bis 10 Hashtags, unten abgesetzt | 8 |
| Suchwörter zusätzlich im Fließtext | Handwerk, Baustelle, Projekt, Kanal, Aufgabe, Chat, Kontakt, Termin |
Der letzte Absatz steht **wörtlich** so als Merkmal „DSGVO-konform" in
`lib/produkt-daten.ts` der Website. „Aus „mach das mal" wird mit einem Klick
eine echte Aufgabe" ebenfalls, als Merkmal „Aufgaben-Integration".
---
## 3. Woher die Angaben kommen
Quelle ist `components/general/GeneralTeamChatPage.tsx` aus dem Pitch-Paket
(*fertig bombo · Kundentermin 30.07.2026*).
| Was | Woher |
|---|---|
| Alle acht Kanäle: Titel, Vorschau, Zeit, Zuordnung, Nadel, Ungelesen | `CHATS`, wörtlich |
| Der Verlauf: Autoren, Texte, Zeiten, Anhänge | `INITIAL_MESSAGES.waermepumpe`, wörtlich |
| Aufbau einer Listenzeile (drei Zeilen) | die Zeilen 170 bis 172 der Quelle |
| Aufbau einer Nachricht, Anhangkarte, Systemzeile | `MessageBubble` und `AttachmentCard` |
| Sinnbild je Kanalsorte (Koffer, Personen, Kürzel) | `ChatAvatar` |
| Kopfzeile „P-2026-0042 · 8 Mitglieder · aktiv" | `ChatHeader` |
| Filterzeile Alle / Projekte / Direkt / Ungelesen | der Typ `ChatFilter` |
| Das Menü hinter dem Plus, samt Trenner und Reihenfolge | `Composer` (Beschriftungen oben geändert, §4) |
| Eingabefeld „Nachricht schreiben …" | derselbe Ort |
| Farben, Ränder, Plättchentöne | `ui/theme.css` |
| Alle Symbole | `@subframe/core`, Pfade aus dem Paket kopiert |
| „Büro und Baustelle, endlich im selben Chat." | Überschrift in `lib/produkt-daten.ts` |
**Vom Chat gibt es nur eine Fassung.** Anders als bei der Pipeline, die mit
Board, Liste, Detail und Schablone vier eigene SHK-Bildschirme hat, liegt der
Chat allein unter `/allgemein/team-chat`. Die Inhalte darin sind trotzdem aus
dem Gewerk: Wärmepumpe Müller, Montagetermin, Baustellenfotos.
---
## 4. Wo bewusst abgewichen wird
**Alles, was einen Chat kennzeichnet, ist grün.** Projektkanal, Gruppe und
Direktnachricht tragen dieselbe Marke (`MARKE`, Fläche `#DCFCE4`, Rand
`#B7EDC6`, Zeichen `#148134`) — in der Liste wie im Verlauf. Vorher trug der
Projektkanal Lila, die Gruppe Grau und jede Person ihre eigene Farbe: vier
Familien in einer Liste, und keine davon sagte etwas, das die Zeile nicht schon
sagt. Die Kürzel unterscheiden die Personen, dafür braucht es keine vier
Farbtöne.
**Die vier Profilbilder zeigen niemanden.** `avatarKreis()` nimmt ein Foto,
sobald eines unter `assets/eigene/portrait-<kürzel>.<endung>` liegt (also
`portrait-sk`, `portrait-mb`, `portrait-tw`, `portrait-ms`), rund beschnitten
mit demselben Rand wie der Kürzelkreis; sonst steht dort weiter das Kürzel.
`preloadAssets()` holt alles aus `assets/eigene` ohnehin mit, und der Export
bekommt sie damit als Daten-URL statt als Verweis. Nur Menschen bekommen eins
— Projektkanal und Gruppe behalten ihr Sinnbild.
**Es sind Aufnahmen von Unsplash, keine erzeugten Gesichter.** Vier Portraits,
über die Suche nach `professional headshot` gefunden:
| Datei | Rolle | Quelle |
|---|---|---|
| `portrait-sk` | Sandra Koch, Projektleitung | [SJvDxw0azqw](https://unsplash.com/photos/SJvDxw0azqw) |
| `portrait-mb` | Maike Berger | [ZAo0cKz_IKM](https://unsplash.com/photos/ZAo0cKz_IKM) |
| `portrait-tw` | Tobias Wagner, Montage | [TTdJV-K1IUg](https://unsplash.com/photos/TTdJV-K1IUg) |
| `portrait-ms` | Michael Schuster, Kundendienst | [7YVZYZeITc8](https://unsplash.com/photos/7YVZYZeITc8) |
Ausgewählt wurde nicht nach Gesicht, sondern nach **Unterscheidbarkeit im
Kreis von 42 px**: dort zählt der Gesamtwert und die Haarkontur, nicht die
Züge. Die vier decken hell, mittelhell, dunkel und mittel ab, dazu vier
verschiedene Silhouetten — kurze Locken, schulterlang, Schiebermütze mit
Bart, kurz und glatt. Bei Tobias trägt die Mütze die Erkennbarkeit fast
allein; sie ist auf dieser Größe deutlicher als jedes Gesichtsmerkmal.
> **Was dabei offen bleibt, und zwar wissentlich.** Die Unsplash-Lizenz deckt
> das Urheberrecht des Fotografen — **kein Model Release**. Nachgelesen an der
> Quelle: Unsplash sagt zu erkennbaren Personen nichts, Pexels schreibt
> „Identifiable people may not appear in a bad light or in a way that is
> offensive", was eine Einschränkung ist und keine Erlaubnis. Ein fremdes
> Gesicht als „Sandra Koch, Projektleitung" auf dem Firmenkanal berührt §22
> KUG und die DSGVO, nicht das Urheberrecht. Ein Zwischenstand wich deshalb
> auf erzeugte Gesichter aus (StyleGAN2 über `thispersondoesnotexist.com`);
> das ist zugunsten echter Aufnahmen zurückgenommen worden. Wer es
> rechtssicher will, braucht **model-released Stock** (Adobe Stock, iStock,
> kostenpflichtig) — das ist der einzige Weg, der die Frage beseitigt statt
> sie zu tragen.
**Der Zuschnitt braucht keinen Füllton mehr.** Ein ordentliches
Business-Portrait hat Luft über dem Kopf; ein quadratischer Ausschnitt, auf
das Gesicht gesetzt, füllt den Kreis von selbst. Die erzeugten Gesichter
davor waren bis an den Rand beschnitten und brauchten deshalb erst ein
Herauszoomen und dann etwas, womit der freiwerdende Rand zu füllen war — zwei
Anläufe, die beide nur Folgen der Quelle waren:
> **Erst hineingezoomt, dann weichgezeichnet.** Der erste Anlauf schnitt
> 800 × 800 aus dem 1024er, also **enger** — die Köpfe waren an Haaransatz
> und Schläfen abgeschnitten, was bei 42 px nicht als Zuschnitt liest,
> sondern als Fehler. Der zweite zoomte richtig heraus und füllte den Rand
> mit einer weichgezeichneten Kopie des Bildes. Das sah aus, als sei das
> ganze Bild unscharf: ein Schleier, dessen Machart sich deutlich vom eigenen
> Bokeh der Aufnahme unterschied. Mit richtigen Vorlagen fällt beides weg.
**Geschärft wird beim Verkleinern, nicht danach am Ergebnis.** Skaliert mit
`lanczos` statt bikubisch, dazu ein leichtes `unsharp=5:5:0.7`. Jedes
Verkleinern kostet Kanten, und der Avatar misst am Ende 42 px — was dort an
Schärfe fehlt, holt keine Renderqualität zurück.
Abgelegt als **320 × 320 JPEG** bei Qualität 2. Der Avatar misst 42 px auf der
Slide, bei Renderqualität 3 also 126 — 320 lässt Luft, und JPEG ist für Fotos
das richtige Format; als PNG wäre es das Fünffache. Zusammen 100 KB.
**Im Menü hinter dem Plus fällt oben das Verb weg.** Im Werkzeug heißen die
vier oberen Einträge „Dokument senden", „Fotos und Videos senden", „Audio
senden" und „Kontakt teilen" — viermal ein Wort, das der Ort schon sagt: wer
das Menü über das Pluszeichen geöffnet hat, weiß, dass er etwas schickt. Hier
stehen nur die Dinge: **Dokumente, Medien, Audio, Kontakt.** Unten bleibt das
Verb, weil es dort etwas hinzufügt — „Termin" allein wäre ein Anhang, „Termin
erstellen" ist eine Handlung.
„Dokument" steht dabei in der Mehrzahl, weil man mehrere anhängen kann; Audio
und Kontakt bleiben einzeln, man schickt eine Aufnahme und einen Kontakt. Aus
„Fotos und Videos" wird **„Medien"** — dasselbe in einem Wort.
**Bei „Kontakt" steht jetzt das Teilen-Zeichen statt des Telefonhörers.** Ein
Hörer sagt „anrufen"; hier wird eine Karte weitergegeben. `ShareNetwork` aus
demselben Phosphor-bold-Satz wie alle anderen Zeichen, entnommen aus
`@phosphor-icons/react` im Website-Repo statt abgemalt. `phone` stand danach
nirgends mehr und ist aus der Tabelle heraus — sie zählt jetzt 19 Zeichen, und
jedes davon wird benutzt.
**Und die Eingabezeile ist zurück.** Sie stand einmal unter dem Menü und war
weggenommen worden mit der Begründung, die Slide zeige, was man anhängen kann,
und dafür brauche es das Feld nicht. Das stimmte nur so lange, wie man wusste,
worauf man schaut: ohne das Feld ist die Karte eine Liste im Nichts, und dass
sie aufklappt, wenn man im Chat auf das Pluszeichen tippt, stand nirgends.
Das Pluszeichen trägt jetzt die grüne Marke wie ein gedrücktes Element, und
der Zeiger steht darauf — derselbe, der im Bewegtbild die Nachricht anklickt.
Beides zusammen sagt den Zusammenhang, ohne dass ein Wort danebenstehen muss.
Das Menü öffnet nach oben, weil es das im Werkzeug auch tut.
Mit den kürzeren Beschriftungen kommt die Breite der Karte jetzt aus dem
längsten Eintrag statt aus einer festen Zahl. Vorher standen dort 470 Einheiten
— gemessen für Beschriftungen mit „senden" —, und die Karte lief mit einem
Drittel Leerraum quer über die Slide.
> **Ein hochkantes Bild richtet sich nach der Höhe, nicht nach der Breite.**
> Der Kasten misst 460 × 535. Bei der alten Breite von 940 px wurde er 1093 px
> hoch und lief in die Überschrift. Gerechnet wird die neue Breite außerdem am
> **sichtbaren Blatt** und nicht am Kasten: der ist um den Stapelrand größer
> als das, was man sieht, und mit seiner Oberkante gerechnet stand die
> Zeichnung noch einmal 60 px zu hoch und berührte den Fließtext. Bei 680 px
> bleiben rund 115 px Luft.
**Die Plakette für Ungelesenes trägt eine Zahl und ist grün.** In der Quelle
ist `unread` ein Wahrheitswert und der Punkt violett. Beides ist geändert:
violett ist im Produkt die Farbe der Projekte, das Kanalsymbol in derselben
Zeile trägt sie schon, und ein zweites violettes Zeichen daneben konkurriert
damit, statt etwas anderes zu sagen. Ein farbloser Punkt sagt außerdem nur
„da war was", eine Zahl sagt, wie viel — so macht es jeder Messenger, den ein
Betrieb kennt.
Die Zahlen (3, 2, 1) stehen **nicht** in der Quelle; sie ist an dieser Stelle
binär. Sie sind die einzigen erfundenen Werte im ganzen Post.
**Die Symbole kommen aus Phosphor, nicht aus Feather.** Der Feather-Satz
steckt in so ziemlich jeder Oberfläche der letzten Jahre; man erkennt ihn
sofort wieder, und damit sieht eine Zeichnung aus wie jede andere. Am
deutlichsten war das am Gruppensymbol — zwei Köpfe nebeneinander, das
Piktogramm schlechthin.
Phosphor ist dieselbe Familie von Strichsymbolen, nur mit eigener Handschrift:
runder, gleichmäßiger, andere Metaphern (`UsersThree` statt `Users`,
`PaperPlaneRight` statt `Send`, `CaretDown` statt `ChevronDown`). Es ist
dasselbe Paket, das die Website benutzt — `@phosphor-icons/react` steht neben
`lucide-react` in ihrer `package.json` — also keine dritte Handschrift im Haus.
Die Pfade sind **direkt aus dem installierten Paket gezogen**, nicht
abgeschrieben: Gewichtung `bold`, Zeichenfläche 256 × 256. Anders als bei
Feather sind es gefüllte Formen und keine Striche, `ikon()` färbt deshalb über
`fill` statt über `stroke`.
**Die Gewichtung ist gemessen, nicht geschätzt.** Kein Farbwert hat sich beim
Tausch geändert — trotzdem sah es anders aus, und das zu Recht: ein Symbol aus
dünneren Formen hat weniger voll gedeckte Pixel und mehr angeschnittene, es
liest sich blasser, obwohl derselbe Hexwert darinsteht. Im Kanalsymbol,
Ausschnitt 44 × 44:
| | volle Pixel | Deckung |
|---|---|---|
| Feather, wie es vorher war | 59 | 180 |
| Phosphor `regular` | 23 | 151 (16 % zu blass) |
| Phosphor `bold` | 62 | 192 (6 % darüber) |
Ein Zwischenstand hatte `regular`, weil es „im Menü leichter aussah". Geschätzt
statt gemessen, und daneben. Wer eine optische Stärke beurteilt, zählt Pixel.
Ein Zwischenstand ersetzte nur das eine Gruppensymbol durch zwei überlappende
Scheiben. Das war keine Antwort auf die Frage: die Frage war der Satz, nicht
das einzelne Zeichen.
**Slide 4 nennt drei der sechs Einträge, nicht alle.** „Versende mehr als nur
Textnachrichten: Dokumente, Fotos, Sprachnachrichten." Drei Beispiele sagen,
worum es geht; die vollständige Liste steht im Bild daneben.
Vorher stand dort die ganze Liste **mit Erklärung** („Dokument, Fotos,
Sprachnachricht, Kontakt. Soll daraus etwas werden: Termin oder Aufgabe."). Zu
spezifisch war nicht das Aufzählen, sondern das Durchdeklinieren. Auf dem Weg
zur jetzigen Fassung habe ich zweimal in die andere Richtung übersteuert und
gar nichts mehr genannt — ein Fließtext auf einer Feature-Slide soll das
Feature erklären, nicht umschreiben und auch nicht in vier Wörtern abhandeln.
**Slide 4 zeigt das Menü ohne das Eingabefeld, an dem es hängt.** Das Feld
stand einmal darunter, damit man sieht, woher das Menü kommt. Es ist weg: die
Slide zeigt, was man anhängen kann, und dafür braucht es das Feld nicht. Der
Stapel sagt ohnehin, dass hier ein Bildschirm liegt und kein Kasten im Nichts.
**Slide 1 zeigt drei von sechs Nachrichten, Slide 2 fünf von acht Kanälen.**
Was nicht ins Bild passt, fällt weg; ein Chat und eine Liste scrollen beide.
Slide 2 behält alle drei Sorten: Projektkanal, Direktnachricht, Gruppe.
**Die Bildergalerie ist eine Anhangkarte, kein Bild.** Im Werkzeug zeigt
`GalleryPreview` zwei Fotos, geladen von Unsplash. Fremde Fotos in einen
Beitrag zu setzen, der von der eigenen Baustelle spricht, wäre eine Behauptung
über Material, das wir nicht haben. Stattdessen steht dort die Karte
„2 Baustellenfotos · Galerie · heute" — der Text ist der der Quelle.
---
## 5. Stand der Dinge
| | |
|---|---|
| Slides | 5, dazu 1 Animation außerhalb des Streifens |
| Format | 1080 × 1350, Ausgabe zweifach: 2160 × 2700 |
| Luft zwischen Text und Zeichnung | 118 / 170 / 153 / 155 / 55 px, Animation 84 (Grenze 40) |
| Zeichnung ragt über die Seite | nein, alle innerhalb der 92-px-Schutzzone |
| Breiteste Überschriftzeile | 818 px von 896 |
| Konsole | keine Fehler |
| Überschneidungen | keine, `kollision.mjs` über alle Slides |
| Ausgabeweg | geprüft, `01-chat-hell.png` bis `05-mobil-hell.png` |
| Bewegtbild | `animation/post6-animChat.mp4`, Ränder 177/176/123, Luft 118, ffprobe siehe §7 |
Geprüft wird mit den Hilfsskripten aus dem Sitzungsordner: `pruef.mjs` misst
über `getBBox()` die tatsächlich gezeichnete Fläche je Slide und die Breite
jeder Überschriftzeile, zählt die Regeln im Stylesheet und liest die Konsole
mit; `schuss.mjs` rendert einzelne Slides; `export.mjs` fährt den echten
Ausgabeweg und fängt `/save` ab; `leiste.mjs` prüft in jedem Projekt, ob die
Animationsleiste an der richtigen Stelle steht und ob die Seite fehlerfrei
anläuft.
---
## 5a. Slide 5 kommt von der Website, nicht von mir
**Kein Gerät.** Vorbild ist die Machart, die Signal für genau solche Slides
benutzt: zwei Ausschnitte der Oberfläche, versetzt übereinander, jeder
mit eigenem Schatten. Man sieht zwei Zustände nebeneinander, ohne dass ein
Telefonrahmen die halbe Fläche frisst.
Ein Zwischenstand hatte hier ein nachgebautes Handy samt Insel und Tasten,
ganz verwischt. Das kostet viel Platz für etwas, das nichts über das Produkt
sagt, und der Rahmen war ohnehin frei erfunden. Die Unschärfe ist damit auch
weg: das Band sagt deutlich genug, dass es die App noch nicht gibt, und die
Ausschnitte sollen lesbar sein.
Links die Kanalliste von Slide 2, rechts der Verlauf von Slide 1, beide schmal
gesetzt und am Rand angeschnitten. Gezeigt wird also nichts, was nicht vorher
schon zu sehen war. Beide sind **leicht verwischt** (`stdDeviation 2.2`):
erkennbar, aber nicht zu lesen. Scharf bleibt allein das Band, und genau das
soll es auch sein.
**Das Absperrband kommt von der Website.** In
`components/sections/ChatDemoSection.tsx` steht unter der Fahne `comingSoon`
genau dieses Element: ein **Absperrband** aus dem Bau, gedreht um **-13°**, in
`--accent-yellow` `#F1B729` mit Kanten in `#171106`, darauf immer wieder
Schutzhelm und **BALD VERFÜGBAR**. Auf der Website ist es an den Rand des
Telefons beschnitten — „*clipped to the phone's rounded bounds so the ends cut
off like real tape*" steht als Kommentar daneben.
Ein Zwischenstand hatte hier einen selbst erfundenen gelben Aufkleber
„In Arbeit". Das war eine eigene Bildsprache neben einer vorhandenen.
Übernommen ist jetzt die der Website, samt Winkel, Farben und Wortlaut.
**Das Band läuft hinter die Ausschnitte.** Es liegt über beiden und
verschwindet an beiden Enden hinter ihnen: links hinter der linken Kante der
hinteren Platte, rechts hinter der rechten der vorderen. Beschnitten wird also
an der Silhouette **beider** Ausschnitte, und die ist die ganze Sache — der
Gegensatz „absperren oder beschneiden", den ein früherer Stand hier
behauptete, gibt es nicht.
Vorher lief das Band über die ganze Zeichnung hinaus und endete an der
Bildkante, quer zur eigenen Laufrichtung abgeschnitten. Genau das ist das
Zeichen für *ein Ende*: so liest sich ein Aufkleber, der zufällig dort aufhört,
wo das Bild aufhört. Ein Ende, das an einer **senkrechten Plattenkante**
abbricht, während das Band mit 13° ankommt, ist keines.
**Drei Dinge tragen das, und keines verbiegt die Form.**
*Die Schnittkante ist die Plattenkante.* Getroffen wird die Silhouette an genau
zwei Stellen, beide dort gerade — die Rundungen liegen weit weg — und beide
senkrecht.
*Sie ist weicher als eine gezogene Linie und härter als die Platten.* Mit deren
`stdDeviation 2.2` lief das Band über vier Einheiten ins Weiße aus und sah nach
Ausblenden aus statt nach Verdecken; ganz scharf stand eine gestochene Kante an
einer unscharfen Platte. `1.1` liegt dazwischen.
*Die letzten Einheiten verlieren Licht,* weil die Fläche sich wegdreht: von
`#F1B82A` auf `#815F11` über 56 Einheiten, mit dem Höhepunkt als Plateau dicht
vor der Kante. Der Ton ist ein warmes Dunkelbraun und kein Schwarz — ein Band,
das sich wegdreht, wird nicht farblos.
**Die Wölbung.** Quer über die Höhe der vorderen Fläche liegt ein Verlauf von
`#F8C64A` über `#F1B729` nach `#D89F1B`, knapp unter der Oberkante ein
Glanzlicht. Die untere Längskante ist einen Pixel dicker als die obere, weil
sie im Schatten der Wölbung liegt.
**Und es ist kein Rechteck mehr, sondern ein Spruchband, das sich um den
Stapel legt.** Das ist die größte Abweichung von der Website in diesem
Beitrag. Dort ist die Form ein durchgehendes Rechteck; hier knickt sie
**genau an der Plattenkante** um, läuft dahinter weiter und kommt an der
Kante wieder hervor — tiefer, weil sie dabei umgeschlagen ist, niedriger,
weil sie vom Betrachter wegführt, und flacher getönt, weil man von dort ihre
Unterseite sieht.
| | |
|---|---|
| `HERVOR` 78 | wie weit das Stück hinter der Platte hervorsteht |
| `VERSATZ` 40 | wie tief es dabei hängt |
| `SCHMAL` 0,72 | seine Höhe: verkürzt, weil es wegführt |
| `NEIG` 10 | zusätzlicher Fall nach außen |
| `#DFA81E` | sein Ton — eine Fläche, die wegführt, fängt weniger Licht |
**Die Stelle des Knicks wird nicht gerechnet.** Beide Stücke reichen
absichtlich weit in die Platte hinein, und **zwei Masken** schneiden sie
exakt an deren Kante ab: `bandSilhouette` lässt die vordere Fläche innerhalb
der Silhouette stehen, `bandAussen` die Stücke außerhalb. Der Knick sitzt
damit immer auf der Kante, auch wenn sich die Platten einmal verschieben.
Beide kommen aus **derselben Kontur, ohne Strich darauf** — so ergänzen sich
ihre Deckungen an jeder Stelle zu genau eins. Darüber liegt die **Falzkante**
als eigener Strich (`bandGesamt`, 4,5 breit, `#171106`): senkrecht auf der
Plattenkante, während das Band mit 13° ankommt, und über Fläche *und* Stück
hinweg, weil beide dort zusammenstoßen. Ohne sie brechen die dunklen
Längskanten an der Naht einfach ab, und der Falz sieht zerschnitten aus statt
geknickt.
Weggelassen sind die zwei Festlichkeiten des klassischen Spruchbands: **keine
Zacken** an den Enden und **kein Bogen** über die Länge. Ein gewölbtes Band
über einer flachen Oberfläche sieht nach Urkunde aus.
> **Drei Anläufe, und woran die ersten beiden lagen.** Erst waren die Enden
> zur Spitze verjüngt, dann war das Band um eine Knickstelle gekippt — das
> eine sah aus wie eine Schleife, das andere wie eine Welle. Beide verbogen
> das Band **als Ganzes** und ließen es nirgendwo hin.
>
> Der dritte Anlauf hatte den Knick schon richtig, saß aber **70 Einheiten
> vor** der Kante. Damit lag er mitten auf dem Bildschirm, und das Ganze sah
> aus wie ein Spruchband, das jemand daraufgeklebt und in der Mitte geknickt
> hat. Ein Band knickt nicht um nichts: der Knick gehört an die Kante, weil
> dort etwas ist, worum es sich legen kann.
Der Lichtabfall vor der Kante ist dabei auf die Hälfte zurückgenommen worden
(`0,80` → `0,42` an der Kante). Zusammen mit dem ohnehin dunkleren
umgeschlagenen Stück wurde daraus sonst ein brauner Schatten statt eines
Bandes. Er gilt **beiden**: nachgemessen liegt das Stück an der Naht bei
Helligkeit 125, das Band daneben bei 132 — das Stück ist also eine Spur
dunkler, und das ist die richtige Richtung, denn es liegt weiter hinten.
*Helm und Schrift stehen einmal, mittig auf der geraden Fläche.* Wiederholt und
am linken Rand beginnend passte der Satz zu einem endlosen Band. Er sitzt in
der Mitte des sichtbaren Laufs, weil dessen Mitte und die Drehmitte des Bandes
dieselbe Stelle sind: beide Kanten liegen gleich weit von ihr weg.
**Vom Absperrband abgesehen kommt der Schirm aus dem Pitch-Paket.** Eine
Zwischenfassung zeigte dort den Chat der Website (Heizungsprojekt Schulz,
`seedMessages`, Blasen in `#0D6E6E`). Der ist echt, gehört aber zu einer
anderen Oberfläche, und der Beitrag zeigt sonst durchgehend das Werkzeug.
Jetzt steht auf dem Schirm derselbe Verlauf wie auf Slide 1 und 3, gezeichnet
mit derselben `blase()`, nur schmaler. Man sieht es kaum, weil das Gerät
verwischt ist — das ist kein Grund, etwas anderes hinzulegen: was unter der
Unschärfe liegt, soll stimmen.
Überschrift und Fließtext sind ebenfalls die der Website: „Der Chat für
unterwegs wird gerade gebaut." und „Die mobile App legen wir gerade auf die
Baustelle. Am Desktop bist du schon voll dabei, die Version fürs Handy folgt
in Kürze." Der Em-Dash der Vorlage ist durch ein Komma ersetzt, `rules.md` §4.
---
## 5d. Slide 2 stellt gegenüber statt zu zeigen
Links das Durcheinander als Zeichnung, rechts die Oberfläche als Ausschnitt.
Die Aussage steckt in der Gegenüberstellung und nicht in einem der beiden
Teile — der Satz der Website ist genau diese Zweiteilung:
„Schluss mit dem einen Verlauf, in dem alles durcheinandergeht. Ein Kanal pro
Projekt."
**Die linke Hälfte gibt es in diesem Repo schon**, in `post2.html` auf Slide 2
(`visProzesse`): beschriftete Plättchen, die eine geschlängelte gestrichelte
Linie verbindet. Dort ist sie im Zeichenblock-Stil mit schiefen Kanten, hier
glatt — das Theme ist ein anderes, und dieselbe Form zweimal in zwei
Anmutungen wäre schlechter als zwei Formen mit derselben Idee.
Die fünf Plättchen (Angebot, Baustellenfoto, Termin, Rückfrage, Materialliste)
sind **Redaktionstext und keine Produktstrings**. Sie beschreiben die
Wirklichkeit im Betrieb, nicht die Oberfläche; dieselbe Lage wie bei
„E-Mails" und „Zettel im Auto" in Post 2. Gewählt sind Wörter, die im Beitrag
ohnehin vorkommen.
**Die Zeichnung steht ganz auf ihrer eigenen Seite.** Ein Zwischenstand zog
sie über die Kante nach links, sodass zwei Plättchen auf Slide 1 lagen und die
Leitung hindurchlief. Das ist zurückgenommen, und mit ihm der negative
`visLeft` und die Sonderregel für die Slide-Kante. `visLeft` ist wieder 92 wie
überall.
**Die Leitung fängt am ersten Plättchen an.** Sie hatte einen Anlauf, der links
vor dem Nullpunkt begann — ein Rest aus der Fassung über zwei Slides. Ohne die
Nachbarslide kam er aus dem Nichts und versprach einen Zusammenhang, den es
nicht gibt. Jetzt beginnt sie bei „Angebot" und endet bei „Materialliste", also
an beiden Enden an etwas.
**Kein Pfeil zwischen den Hälften.** Er stand einmal dort und machte die Sache
zu ordentlich — eine Zeichnung, die Durcheinander zeigt, braucht keinen
Wegweiser, und die Leitung führt ohnehin von oben nach unten. Die Plättchen
sind aus demselben Grund gestreut und leicht gekippt statt in einer geraden Spalte
gesetzt. Die Werte dafür stehen fest im Code und sind nicht gewürfelt: ein
Zufallsgenerator gäbe bei jedem Neubau ein anderes Bild, und die Slide soll
zweimal gleich aussehen.
**Der Ausschnitt steht größer da als die Zeichnung daneben** (`LISTE_SKALA`,
1,28). Er trägt das Produkt, die Plättchen tragen nur die Ausgangslage; gleich
groß gesetzt lasen sich beide als gleichwertig, und das sind sie nicht. Platz
dafür war da: gemessen endet die linke Hälfte samt Leitung bei x = 344, der
Kasten begann erst bei 464.
**Die Projektnummer misst dadurch wieder 15,8 px** statt 12,4 in der halben
Breite. Sie ist der Beleg für den Fließtext („Jeder Kanal hängt an seiner
Projektnummer") und muss lesbar bleiben — wer sie verkleinert, nimmt der Slide
ihre eigene Aussage.
---
## 5c. Slide 3 kommt ohne Hervorhebung aus
Um die Anhangkarte der Aufgabe lag einmal ein gelber Rahmen, geborgt aus den
Slack-Beiträgen: rund, über die Blase hinausragend, auf genau eine Sache
zeigend. Er ist weg.
Bei sechs Pixeln war er plump — er trug mehr Farbe als die Karte, auf die er
zeigen sollte. Der Versuch, ihn zu retten, ging über vier Varianten (ganz weg,
weicher Schein, gelbe Kante links, dünner Ring), jeweils gerendert und auf
Feed-Breite heruntergerechnet; gewählt war am Ende derselbe Ring mit drei
statt sechs Pixeln. Im Beitrag sah das aus wie vorher.
**Die Slide braucht ihn nicht.** Sie besteht aus drei Nachrichten, die
aufeinander folgen, und die Aufgabenkarte ist das einzige Element mit einer
gelben Marke („Offen") — der Blick geht ohnehin dorthin. Ein Rahmen sagt
nochmal, was die Anordnung schon sagt, und kostet dafür die Ruhe der Fläche.
Mit ihm ist `letzterAnhang` weggefallen: `blase()` merkte sich die Lage der
zuletzt gezeichneten Anhangkarte, damit der Rahmen nicht aus einer zweiten
Rechnung kommt. Ohne Rahmen liest das niemand mehr.
---
## 5b. Woher die Tiefe kommt
Zwei Anläufe waren falsch, bevor es saß.
**Erster Versuch: graue Flächen hinter die Elemente.** Das nimmt den
Zeichnungen ihre Durchsichtigkeit und setzt einen Kasten auf den Punktgrund,
der dort nicht hingehört. Wieder raus.
**Zweiter Versuch: nur bessere Schatten.** Richtig, aber zu wenig. Die
Schatten des Produkts sind in `ui/theme.css` **gestaffelt** und nicht
einlagig, und das war tatsächlich ein Fehler im ersten Nachbau:
```
--shadow-sm 0 1px 2px rgb(0 0 0 / .08)
--shadow-md 0 4px 16px -2px + 0 2px 4px -1px
--shadow-lg 0 12px 32px -4px + 0 4px 8px -2px
```
Zwei Lagen, weil eine allein nicht beides kann: die enge dunkle Kante setzt
das Element auf die Fläche, die weite helle hebt es davon ab. In SVG zwei
`feDropShadow` im selben Filter. Vorher stand dort einer mit `dy 7` und
`stdDeviation 10`.
**Was es gebracht hat: der Stapel.** `post1.html` hat ihn als Bildrahmen
`stapel` im CSS, für Screenshots. Hier ist er in SVG nachgebaut, mit genau
denselben Werten, damit die Reihe zusammenpasst:
```
.sheet-mid rotate(-2.4deg) translate(-2.2%, 3%) scale(.975)
.sheet-back rotate( 3.2deg) translate( 3%, 5.5%) scale(.95)
```
Er trägt zwei Aufgaben: er gibt Tiefe, die ein Schatten allein nicht schafft,
und er sagt, dass hinter der einen Ansicht weitere liegen. Die hinteren
Blätter sind deshalb **leer** — Andeutung, keine Kopie.
Auf Slide 1, 2 und 4 liegt er, weil dort ein Bildschirm oder ein Menü zu sehen
ist. Auf Slide 3 nicht: ein Ausschnitt aus einer Unterhaltung ist kein Blatt,
dort tragen die Blasen ihre eigene Erhebung.
Dazu zwei Dinge aus der Quelle, die der Unterhaltung Struktur geben, ohne eine
Fläche zu behaupten: die Datumsmarke **Heute · 30.07.2026** über dem ersten
Beitrag und die Zeile **„Maike Berger schreibt gerade …"** über dem
Eingabefeld. Beides steht so in `GeneralTeamChatPage.tsx`.
Nebenbei richtiggestellt: **der offene Kanal ist grau hinterlegt, nicht
violett.** Im Original steht dort `bg-surface-neutral-primary` mit Rand und
`shadow-sm`.
---
## 6. Vierundzwanzig Fallen aus diesem Bau
**Die Listenzeile hat drei Zeilen, nicht zwei.** Der erste Anlauf setzte Titel
und Vorschau untereinander und schob Nadel und Ungelesen-Punkt rechts neben
die Vorschau. Dort stießen sie an den Text — und die Zuordnung fehlte ganz.
Im Original steht sie als dritte Zeile: `P-2026-0042` links, die Nadel rechts.
Das war nicht nur ein Layoutfehler: der Fließtext behauptete „Jeder Kanal hängt
an seiner Projektnummer", und die Nummer war nirgends zu sehen.
**Eine Anhangkarte auf eine Zeile zu zwingen kürzt Zitate.** In der schmalen
Spalte auf Slide 5 stand „Materialliste prüfen · fälli…". Der Titel darf jetzt
auf zwei Zeilen, und die Karte wächst mit; die Blasenhöhe rechnet das mit.
Ebenso bricht die Systemzeile um, statt aus der Fläche zu laufen.
**Eine feste Obergrenze für die Blasenbreite hat ein Zitat gekürzt.** Für die
Titelslide musste sie klein sein, damit die Unterhaltung neben die Liste passt.
Auf Slide 3 stand deshalb „Materialliste prüfen · fälli…" statt
„… · fällig 31.07.2026". Die Grenze hängt jetzt an der Spalte, in der die Blase
steht.
**Beinahe hätte ich ein Zitat ohne Grund gekürzt.** Die Website-Überschrift
lautet „Büro und Baustelle, endlich im selben Chat." Ich hatte das „endlich"
gestrichen mit der Begründung, die Zeile laufe sonst über die Schutzzone, und
diese Begründung sogar mit einer Zahl versehen, die ich nicht gemessen hatte.
Nachgemessen sind es 818 px von 896 erlaubten: sie passt. Das Wort ist wieder
drin. Dafür gibt es jetzt `breite.mjs`, das eine Formulierung misst, bevor sie
in die Datei kommt.
**Eine Verschiebung mitten in der Zeichenfunktion erfasst nur die Hälfte.**
Die Zeile, die den Inhalt um `PAD` einrückt, stand mitten in `visChat()`.
Alles, was danach gezeichnet wurde — Datumsmarke, Blasen, Tippzeile,
Eingabefeld — lag um 26 Einheiten daneben, und die Kürzel schnitten die
Trennlinie zwischen den Spalten. Auf der ganzen Slide sieht man das nicht, im
Ausschnitt sofort. Die Verschiebung steht jetzt unmittelbar vor der Rückgabe.
Dafür gibt es `kollision.mjs`: es nimmt jedes gezeichnete Element, rechnet
seinen Kasten in Nutzereinheiten und meldet, was über den viewBox hinausragt
oder die senkrechte Trennlinie schneidet. Die Stapelblätter sind ausgenommen,
sie dürfen alles schneiden.
**Ein Prüfskript, das in den falschen Einheiten rechnet, ist schlimmer als
keines.** `kollision.mjs` benutzte zuerst `getCTM()`. Das enthält die
Skalierung der viewBox auf die Anzeigebreite — die Maße kamen also in
Bildschirmpixeln heraus und wurden gegen die viewBox in Nutzereinheiten
geprüft. Auf Slide 1 fiel es nicht auf, weil der Faktor dort zufällig bei 0,93
lag; auf Slide 2 meldete es 26 Fehler, die keine waren. Gerechnet wird jetzt
über `svg.getScreenCTM().inverse().multiply(el.getScreenCTM())`.
**Gedrehte Blätter ragen aus dem Kasten.** Der Stapel dreht das hinterste
Blatt um 3,2° und verschiebt es um 3 % und 5,5 % der eigenen Größe. Die Ecken
stehen damit über das vordere Blatt hinaus, und die Zeichnung lag bis zu 24 px
außerhalb der 92-px-Schutzzone. Der Überstand wird jetzt gerechnet statt
geschätzt: `stapelRand()` schickt die vier Ecken durch dieselbe Kette wie das
Blatt und nimmt den größten Wert. `stapelKasten()` macht den Kasten
entsprechend größer.
**Ein Maß, das aus einer Konstante kommt statt aus dem Gezeichneten, veraltet
still.** Der Pfeil auf Slide 2 rechnete seine Lage aus einer Blockbreite. Als
die Plättchen weiter nach rechts rückten, stimmte sie nicht mehr, und der Pfeil
zerfiel in zwei Striche. Er leitet seine Lage jetzt aus den Plättchen selbst
ab, so wie der Rahmen auf Slide 3 seine aus `blase()` bekommt.
**Eine Sicherung, die zu spät greift, sichert den Fehler mit.** `export.mjs`
schaltete die Slides mit der Pfeiltaste weiter und schrieb dabei im
Bearbeiten-Modus `vis`-Versätze in `state/post6.json`. Die Gegenmaßnahme war,
den Stand vor dem Lauf zu lesen und danach zurückzuschreiben — nur stand die
Drift beim nächsten Lauf schon in der Datei und wurde mitgesichert. Jetzt
werden die Slides über `#slides-nav` angeklickt statt durchgetastet; ohne
Tasten gibt es das Problem gar nicht erst.
**Ein gedrehtes Element wandert seitlich aus, wenn der Drehpunkt woanders
liegt.** Das Band sitzt 24 Einheiten unter der Mitte der Zeichnung, gedreht
wurde aber um die Mitte. Dadurch verschiebt sich sein Kasten um
24·sin(13°) ≈ 5 Einheiten zur Seite und stand über dem Rand. Gedreht wird
jetzt um die eigene Mitte des Bandes.
**`getBBox()` kennt keine Beschneidung.** Das Absperrband war zuerst auf die
175 % der Website gezogen und per `clip-path` am Gerät abgeschnitten. Im Bild
stimmte das, die Randmessung meldete aber einen Überstand von 146 px, den es
gar nicht gab: `getBBox()` liefert die Geometrie, nicht das Sichtbare. Das
Band ist jetzt nur so lang, wie es bei -13° quer über das Gerät sein muss,
also `B / cos(13°)` plus Zugabe. Messung und Bild sagen wieder dasselbe.
**Das Prüfskript nahm eine leere Slide auf.** `schuss.mjs` stellte für eine
einzelne Slide ein Fenster von 1400 px. Im durchgehenden Blatt liegt Seite 3
aber bei x = 2160, und `captureBeyondViewport` fing das nicht zuverlässig ab.
Die Bühne steht jetzt immer auf 7000 px.
**Eine ungültige `var()` fällt lautlos durch.** Die Animationsleiste war in
Farben gesetzt, die es in diesen Dateien nie gab — `var(--line)`,
`var(--panel)`, `var(--ink)`. Eine Variable ohne Wert und ohne Ersatzwert macht
nicht etwa die Farbe kaputt, sondern lässt **die ganze Angabe** fallen: kein
Rahmen, kein Grund, keine Meldung in der Konsole, kein Fehler in irgendeinem
Prüfskript. Aufgefallen ist es erst auf dem Bildschirmfoto. Die Leiste trägt
jetzt dieselben festen Werte wie der Rest der Oberfläche (`#17181F`, `#24262F`,
`#1D1F26`, `#2E313B`).
**Ein Geschwister hinter einem absolut liegenden Element landet nicht
darunter.** Die Leiste wurde als nächstes Element nach `#scroller` eingehängt
und lag prompt oben über dem Streifen: `.scroller` ist `position: absolute;
inset: 0` und nimmt keinen Platz im Fluss ein. Die Bühne ist deshalb jetzt eine
Spalte aus zwei Zeilen — oben eine neu eingezogene `#stageflaeche` mit Scroller
und Fußleiste, darunter die Leiste. Eingezogen wird sie beim Start per Skript,
damit der HTML-Rumpf jeder Datei unverändert bleibt.
**Eine Animation deckt auf, was ein Standbild verdeckt.** Auf Slide 3 steht
die Nachricht „Danke. Ich habe dazu direkt eine Aufgabe erstellt." mit
angehängter Aufgabenkarte, und das liest sich einwandfrei: ein Zustand, eine
Momentaufnahme. Sobald derselbe Inhalt einen Ablauf bekommt, fällt auf, dass
Text und Anhang einander bedingen — die Nachricht meldete eine Aufgabe, die im
selben Zug erst entstand. Der Fehler lag nicht in der Quelle, sondern darin,
eine Momentaufnahme für eine Reihenfolge zu halten. Mehr dazu in §7.
**Das Pitch-Paket ist ein Stand, nicht das Produkt.** Der zweite Anlauf für die
Animation nahm das Menü hinter dem Pluszeichen, weil `GeneralTeamChatPage.tsx`
den einzigen dort ausgezeichneten Weg zu einer Aufgabe so zeigt. Belegt war das
sauber — Datei, Zeile, Grep über beide Repos und über die Staging-API, die eine
Aufgabe nur mit `contact | company | lead | project` verknüpft und eine
Nachricht als Bezug gar nicht kennt. Trotzdem falsch: im laufenden Team-Chat
hängt an jeder Nachricht ein Menü mit „Aufgabe erstellen".
Die Lehre ist nicht „schlechter prüfen", sondern **wissen, was eine Quelle
belegt.** Ein Pitch-Paket vom Juli belegt, was im Juli gebaut war. Es belegt
nicht, was das Produkt heute kann. Wo eine Aussage der Website (hier:
„Aus „mach das mal" wird mit einem Klick eine echte Aufgabe") keine Entsprechung
im vorliegenden Code hat, ist das ein Grund nachzufragen und nicht, die Aussage
auf den vorhandenen Code zu biegen.
**Ich habe die falsche Slide geprüft.** Nach dem Vergrößern des Ausschnitts auf
Slide 2 lief `kollision.mjs` mit dem Argument `1` — das ist Slide 1. Auf Slide 2
ragte das hinterste, gedrehte Blatt des Stapels fünf Einheiten unter den Kasten:
`stapelRand()` rechnet den Überstand für die unskalierte Größe, und die
Skalierung vergrößert ihn mit. Der Kasten wird jetzt aus dem skalierten Stapel
gerechnet. Gefunden wurde es erst zwei Commits später, bei einer Prüfung, die
alle fünf Slides durchging.
**Ein `|| 0` an der falschen Stelle verdeckt ein NaN.** Der Abspielen-Knopf tat
nichts, sobald man ihn nach dem Laden als Erstes anfasste. `animAbspielen`
rechnete `performance.now() - ANIM_T[i] * dauer`, damit die Vorschau dort
weiterläuft, wo der Regler steht — nur ist `ANIM_T[i]` undefined, solange der
Regler nie bewegt wurde. Daraus wird NaN, der Fortschritt wird NaN, und
`animT()` liefert wegen seines eigenen `|| 0` brav wieder 0: die Vorschau stand
still, ohne dass in der Konsole etwas auftauchte.
Der Fehler war vier Commits alt und wurde nicht bemerkt, weil jede Prüfung
vorher `animSetzen()` aufgerufen hatte, um irgendetwas zu messen — und damit
genau den Zustand herstellte, in dem es funktioniert. **Ein Prüfskript, das den
Anfangszustand anfasst, prüft ihn nicht mehr.** `animSetzen` weist NaN jetzt
zusätzlich mit `Number.isFinite` ab, damit ein solcher Wert gar nicht erst in
den Stand kommt.
**Ein weicher Schnitt und ein Schatten an derselben Stelle heben sich auf.**
Die Falte des Absperrbands hatte ihren dunkelsten Punkt genau auf der
Plattenkante. Dort blendet die Maske das Band aber schon aus — das Dunkelste
lag also im Durchsichtigen und mischte sich mit dem Weiß der Platte zu einem
blassen Streifen, der die Falte gerade wieder aufhob. Der Höhepunkt sitzt jetzt
als Plateau ein Stück davor, wo das Band noch voll deckt.
**Ein Verlauf gehört in die Koordinaten dessen, woran er endet.** Das Abdunkeln
der Bandenden stand zuerst im gedrehten Bandkasten und lief damit 13° schräg
zur senkrechten Plattenkante, an der es aufhören soll. Über die Bandhöhe
verschob sich der Höhepunkt dadurch um 21 Einheiten. Es steht jetzt in den
Koordinaten der Zeichnung, waagerecht, parallel zur Kante.
**Mehr als die Hälfte der „Fotos" von einer Bildersuche waren Profilbilder
der Fotografen.** Beim Einsammeln der Unsplash-Treffer greift ein
`figure img[src*='images.unsplash.com']` auch das Avatarbild des Urhebers ab,
das mit im `<figure>` steht. Von 56 Kandidaten waren **32** solche
Profilbilder — und sie sehen aus wie Portraits, weil sie welche sind. Zwei
davon waren schon eingesetzt und gerendert, bevor es beim Aufschreiben der
Herkunft auffiel: ihre URL beginnt mit `profile-` statt mit `photo-`. Ein
Profilbild ist nicht von der Unsplash-Lizenz gedeckt. Gefiltert wird jetzt
auf `/photo-` in der Bild-URL.
**Zwei gegenläufige Teilstücke in einem Pfad löschen ihren Überlapp weg.**
Der Beschnitt für die Falzkante war ein einziger `<path>` aus Bandrechteck und
umgeschlagenem Stück. Das Rechteck läuft im Uhrzeigersinn, das Stück dagegen —
und die Füllregel `nonzero` macht daraus ein **Loch**, genau dort, wo beide
sich überlappen. Und genau dort sollte die Falzkante hin. Sie fehlte, ohne dass
irgendetwas einen Fehler meldete. Die Kinder einer `clipPath` werden dagegen
vereinigt: jetzt stehen dort zwei `<path>`-Elemente statt eines.
**Ein Rechteck in Zeichnungsgröße trifft nicht, was über die Zeichnung
hinaussteht.** Die Abdunklung der umgeschlagenen Stücke lag auf einem Rechteck
`0 0 W H`. Die Stücke stehen aber außerhalb von `0..W` — sie ragen ja über die
Plattenkante hinaus. Das Rechteck traf sie nie, und das Stück blieb heller als
das Band daneben: verkehrt herum für etwas, das weiter hinten liegt. Aufgefallen
ist es erst beim Auszählen der Pixel quer über die Naht, nicht am Bild.
**Ein Kommentar, der einen zurückgenommenen Stand beschreibt, ist schlimmer als
keiner.** Über dem Band standen zwei Kommentarblöcke übereinander: der eine
sagte, es rage beidseitig hinaus, der andere, es knicke an den Enden weg. Das
erste stimmte noch, das zweite war einen Commit vorher zurückgenommen worden.
Wer danach etwas ändert, glaubt dem Kommentar. Mit ihnen weg sind auch
`WEB.bandKnick` und `WEB.bandSpitze`, die Farben des Knicks, die niemand mehr
las.
---
## 7. Das Bewegtbild
Sechs Sekunden, 180 Bilder, `animation/post6-animChat.mp4`. Gezeichnet wird es
von `animChat(i, t)` in `post6.html`, mit denselben Bausteinen wie jede Slide —
nur bekommt die Funktion neben der Nummer den Fortschritt `t` und leitet daraus
alles ab. Der Eintrag steht in `ANIMS`, nicht in `SLIDES`.
**Gezeigt wird der Weg, den das laufende Werkzeug hat:** Nachricht anfassen,
Menü öffnet sich, „Aufgabe erstellen", die Aufgabe klappt in der Nachricht auf,
das Projekt bestätigt sie. Das ist die Funktion, die die Website meint mit
„Aus „mach das mal" wird mit einem Klick eine echte Aufgabe."
| Zeit | Was passiert |
|---|---|
| 0,0 – 0,2 s | Ruhe. Der Verlauf steht: Sandra, Maike, Tobias |
| 0,2 – 1,1 s | Der Zeiger fährt auf Tobias' Nachricht, der Pfeil erscheint an ihr |
| 1,3 – 1,6 s | Klick |
| 1,5 – 2,0 s | Das Menü schnappt nach oben auf: Reagieren, Antworten, Im Thread antworten, Anpinnen, Aufgabe erstellen, Bearbeiten, Löschen |
| 2,4 – 3,4 s | Der Zeiger geht auf „Aufgabe erstellen", die Zeile hebt sich, Klick |
| 3,6 – 4,8 s | Das Menü schließt, die Aufgabenkarte klappt in Tobias' Nachricht auf, der Verlauf rutscht nach |
| 4,8 – 5,4 s | Die Systemzeile „Aufgabe „Materialliste prüfen" im Projekt erstellt" erscheint |
| 5,4 – 6,0 s | Halt auf dem Ergebnis |
### Die Kurven sind die von GSAP, die Bibliothek ist es nicht
Die Website macht ihre Bewegungen mit **GSAP** (`gsap@3.14.2`, Standard-Lizenz;
`HeroSection`, `Navbar`, `HoneycombSection`, `FAQSection` und weitere). Die
Frage lag also nahe, es hier auch zu tun. Die Antwort ist: **die Kurven ja, die
Bibliothek nein.**
Was GSAP über eine Kurve hinaus kann — Zeitleisten, ScrollTrigger,
MotionPathPlugin —, setzt voraus, dass etwas *läuft*. Hier läuft nichts:
`animChat(i, t)` bekommt seinen Fortschritt von außen und zeichnet daraus den
Stand. Die Funktion **ist** schon die Zeitleiste, und `animation.mjs` sampelt
sie Bild für Bild. Ein `gsap.timeline({paused: true})` mit `.progress(t)` wäre
genau dieselbe Konstruktion, nur mit 70 KB darunter — und das Repo ist autark,
jede Datei in `vendor/` liegt dort, weil sie etwas kann, das man nicht in fünf
Zeilen schreibt (DOM nach Canvas, ZIP). Ein Easing ist eine Zeile.
Dazu kommt: **zwei der Kurven waren längst GSAPs.** `daempfen` ist Wort für
Wort `power2.inOut`, `rueck` ist `back.out(1.70158)`. Die Datei sprach die
Sprache schon, sie sagte es nur nicht. Jetzt stehen die Namen daneben, und der
Satz ist vollständig:
| in der Datei | GSAP | wofür |
|---|---|---|
| `daempfen` | `power2.inOut` | Wege mit Anfang und Ende: das Wandern zur Menüzeile, das Aufklappen der Karte |
| `rueck` | `back.out(1.7)` | das Menü, das aufschnappt und einmal leicht überschwingt |
| `raus` | `power2.out` | alles, was ankommt und liegen bleibt: der Zeiger, die Systemzeile |
| `knall` | `expo.out` | der Klickring |
**Der Zeiger fährt jetzt wie eine Hand und nicht wie ein Schlitten.** Er lag
auf `power2.inOut` — langsam an, in der Mitte schnell, wieder abbremsend. So
bewegt sich ein Schlitten auf einer Schiene. Eine Hand ist sofort schnell und
wird zum Ziel hin langsam, also `power2.out`.
**Und er fährt einen Bogen.** Auf der Geraden zwischen Start und Ziel liegt kein
Zeiger, den je ein Mensch geführt hat. Der Stützpunkt einer quadratischen
Bézierkurve sitzt quer zur Strecke, 13 % ihrer Länge nach außen — vom Verlauf
weg, damit der Zeiger nicht über die Blasen streicht. Das sind rund 27 px
Ausbuchtung auf 410 px Weg; es fällt nicht auf, und genau das ist der Punkt.
Bei `p = 1` liegt die Bahn exakt auf dem Ziel, das anschließende Wandern zur
Menüzeile setzt also sauber daran an.
**Der Klickring geht auf zwei Kurven.** Der Radius auf `expo.out`, also fast
sofort draußen, die Deckung linear hinterher. Beides auf derselben Kurve sah
aus wie eine Blase, die sich aufbläst — ein Klick ist ein Stoß und kein
Aufpumpen.
**Der Anlauf ist von 0,6 s auf 0,18 s geschrumpft.** Am Ende steht das Bild
eine halbe Sekunde still, damit man das Ergebnis liest. Lief vorne noch einmal
eine halbe Sekunde nichts, waren das über den Schleifenstoß hinweg 1,2 von
6 Sekunden Stillstand am Stück, und die Schleife sah aus, als hänge sie. Der
Halt am Ende bleibt, der am Anfang ist auf ein Achtel geschrumpft.
> **Nachgeprüft ist das an Einzelbildern, nicht am Lauf.** Kontaktabzüge über
> die 180 Bilder und eine Nahaufnahme des Klickrings zeigen, was die Kurven
> rechnen. Ob die Bewegung *läuft*, sieht man erst im Abspielen — dieser eine
> Schritt steht noch aus.
### Warum Tobias' Nachricht
Nicht irgendeine: die Aufgabenkarte aus der Quelle trägt **„Tobias Wagner ·
offen"** als Zuständigkeit. Wer sie an einer fremden Nachricht entstehen lässt,
muss den Namen erfinden. So bleiben alle vier Felder der Karte
(`square-check`, „Materialliste prüfen · fällig 31.07.2026", „Tobias Wagner ·
offen", „Offen") genau die aus `GeneralTeamChatPage.tsx`.
### Vier Entscheidungen
- **Der Verlauf hängt am unteren Rand, nicht am oberen.** Damit erzeugt jedes
Wachstum das Hochschieben von selbst, ohne eine zweite Rechnung für das
Scrollen. Wenn die Aufgabenkarte aufklappt, rutscht der Verlauf nach.
- **Die Karte wird aufgedeckt, nicht gestaucht.** Sie liegt in voller Größe da
und wird über eine `clipPath` von oben freigegeben. Eine gestauchte Karte
sieht kaputt aus, eine aufgedeckte sieht nach Aufklappen aus.
- **Der Mauszeiger ist erzählerisches Mittel, kein Bestandteil des Werkzeugs.**
Ohne ihn öffnet sich ein Menü und man weiß nicht, warum. Weiß mit dunkler
Kante, damit er auf Verlauf wie Menü liest, und nach dem zweiten Klick zügig
weg — sonst zieht er den Blick von der aufklappenden Aufgabe ab.
- **Am Ende wird nichts zurückgenommen.** Ein früherer Stand blendete die neue
Nachricht wieder aus, damit der Schleifenstoß unsichtbar wird. Das sah aus,
als lösche sich eine abgeschickte Nachricht von selbst. Eine Schleife darf
springen.
### Ohne Fließtext
Die Animation trägt als einzige keine Beschreibung. Die Überschrift sagt es,
die Bewegung zeigt es, und die Systemzeile im Bild bestätigt es — ein Satz
darunter wäre die dritte Stelle für dieselbe Aussage. Ein leerer `body` lässt
`contentHtml` den Absatz ganz weg, sonst schöbe sein Zeilenabstand die
Zeichnung nach unten, ohne dass etwas dasteht.
Die gut hundert Pixel, die dadurch frei werden, gehen an die Zeichnung:
`visW` von 700 auf 776, mittig gesetzt bei `visLeft: 152`. Luft zum Text 118,
also derselbe Wert wie auf Slide 1.
Die Überschrift „Chatnachrichten werden / **Aufgaben im Projekt.**" misst
879 px in der ersten und 733 in der zweiten Zeile, bei 896 erlaubten. Gemessen
mit `breite.mjs`, bevor sie in die Datei kam, und nach dem Einsetzen noch
einmal am gerenderten Blatt nachgezählt: zwei Zeilen, kein Umbruch.
**„Chatnachrichten" zusammengeschrieben, nicht mit Bindestrich.** Beides ist
korrekt, also entscheidet die Breite: „Chat-Nachrichten werden" misst 916 px
und läuft über die Schutzzone, zusammengeschrieben sind es 879. Verworfen
wurden außerdem „Aufgaben im richtigen Projekt." (1067 px) und „Aus
Chat-Nachrichten werden" (1063 px).
**Das Menü schnappt auf, es kriecht nicht.** Ein Zwischenstand nahm dafür
dieselbe gedämpfte Kurve wie alles andere, über 0,8 Sekunden. Die Deckung war
darin nach fünf Bildern fertig, während die Skalierung noch eine halbe Sekunde
von 0,96 auf 1,00 weiterlief. Zu sehen war davon fast nichts, zu spüren war es
als Hänger genau in dem Moment, in dem geklickt wird. Jetzt öffnet es in einer
Drittelsekunde mit `rueck`, schwingt einmal ganz leicht über und steht: drei
Bilder Aufblenden, dann Ruhe.
### Zur Länge
Instagram nimmt je Carousel-Element **3 bis 60 Sekunden** an. Sechs liegt weit
von beiden Grenzen weg; die Länge richtet sich also allein nach dem Ablauf.
Vier Schritte — anfassen, wählen, aufklappen, bestätigen — brauchen etwa so
lange. Eine kurze Schleife wiederholt sich außerdem öfter, während jemand liest.
Eine Zwischenfassung lief acht Sekunden, weil dort zusätzlich eine Nachricht
getippt wurde; das fällt mit dem richtigen Weg weg.
Die Symbole im Menü sind unsere Strichsymbole. Im Werkzeug steht bei
„Reagieren" ein Emoji; ein Emoji zwischen sechs Strichsymbolen bricht die
Bildsprache, und die Slides tragen durchgehend Feather-Symbole.
Nachgeprüft mit `ffprobe`: H.264 High, `yuv420p`, 1080 × 1350, 30 fps, 180
Bilder, Dauer exakt 6,000000 s, stumme AAC-Spur, `moov` am Dateianfang, 1,1 MB.