diff --git a/POST-6.md b/POST-6.md index 13860c5..4eceda1 100644 --- a/POST-6.md +++ b/POST-6.md @@ -8,8 +8,8 @@ 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`. -**Vier Slides.** Fünf waren erlaubt; der fünfte Punkt wäre DSGVO und deutsche -Server gewesen, und dafür gibt es keinen Bildschirm. Er steht in der +**Fünf Slides.** DSGVO und deutsche Server hätten einen sechsten Punkt +ergeben, dafür gibt es aber keinen Bildschirm. Der steht in der Bildunterschrift, wo eine Aussage ohne Bild hingehört. --- @@ -22,6 +22,7 @@ Bildunterschrift, wo eine Aussage ohne Bild hingehört. | 2 | Die Kanäle | Ein Kanal **pro Projekt.** | Die Chatliste groß: Projektkanäle, Direktnachrichten, Gruppe | | 3 | Die Aufgabe | Foto rein, **Aufgabe raus.** | Baustellenfotos, die Aufgabe daraus, die Bestätigung im Projekt | | 4 | Das Pluszeichen | Nicht nur **Text.** | Das Menü hinter dem Plus im Eingabefeld | +| 5 | Bald verfügbar | Der Chat für unterwegs **wird gerade gebaut.** | Das Handy-Mockup der Website, verwischt, mit Absperrband | **Slide 3 ist der Kern.** Der Redaktionsplan verlangt „Foto rein, Aufgabe raus", und genau das liegt im Werkzeug als Abfolge vor: Tobias schickt die @@ -110,14 +111,14 @@ Beitrag zu setzen, der von der eigenen Baustelle spricht, wäre eine Behauptung | | | |---|---| -| Slides | 4 | +| Slides | 5 | | Format | 1080 × 1350, Ausgabe zweifach: 2160 × 2700 | -| Luft zwischen Text und Zeichnung | 102 / 61 / 254 / 141 px (Grenze 40) | +| Luft zwischen Text und Zeichnung | 116 / 79 / 190 / 141 / 77 px (Grenze 40) | | Zeichnung ragt über die Seite | nein, alle innerhalb der 92-px-Schutzzone | | Breiteste Überschriftzeile | 818 px von 896 | | Stylesheet vollständig | 214 Regeln, `[data-edit]` vorhanden | | Konsole | keine Fehler | -| Ausgabeweg | geprüft, `01-chat-hell.png` bis `04-menue-hell.png` | +| Ausgabeweg | geprüft, `01-chat-hell.png` bis `05-handy-hell.png` | 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 @@ -127,7 +128,94 @@ Ausgabeweg und fängt `/save` ab. --- -## 6. Drei Fallen aus diesem Bau +## 5a. Slide 5 kommt von der Website, nicht von mir + +**Das Gerät ist eine bewusste Abweichung.** Die Website zeichnet nur einen +hellen Umriss (`border: 4px solid #D1D5DB`). Auf einer Slide, die selbst hell +ist, fällt der in sich zusammen und sieht nach Platzhalter aus. Hier steht +deshalb ein richtiges Gerät: dunkler Korpus, heller Grat, Schirm eingelassen, +Tasten an der Seite. Die Abweichung betrifft den Rahmen, nicht den Inhalt. Der +Schirm wird auch schwächer verwischt als dort: auf der Website liegt das Handy +groß neben Fließtext und darf unlesbar sein, hier ist es klein, und bei voller +Stärke blieb nur Brei. + +**Die App gibt es noch nicht, und die Website sagt das bereits.** In +`components/sections/ChatDemoSection.tsx` steht unter der Fahne `comingSoon` +genau dieser Zustand: ein Handy-Mockup, das mit `blur(3px) saturate(0.92)` +verwischt wird, und darüber ein **Absperrband** aus dem Bau, gedreht um +**-13°**, in `--accent-yellow` `#F1B729` mit Kanten in `#171106`, darauf immer +wieder Schutzhelm und **BALD VERFÜGBAR**. + +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. + +Auch der Inhalt des Schirms kommt von dort und **nicht** aus dem Pitch-Paket: +Heizungsprojekt Schulz, „4 Mitglieder · 2 online", die vier Nachrichten aus +`seedMessages`, die Blasenfarben `#0D6E6E` für eigene und `#FFFFFF` mit Rand +`#E9E7E5` für fremde, der warme Grund `hsl(30 17% 95.5%)`. Auf der Website +steht dieses Handy für den mobilen Chat, also zeigt es hier dasselbe. + +Ü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. + +--- + +## 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 und 2 liegt er, weil dort ein Bildschirm zu sehen ist. Auf Slide 3 +nicht: ein Ausschnitt aus einer Unterhaltung ist kein Blatt, dort tragen die +Blasen ihre eigene Erhebung. Slide 4 ist eine Überlagerung und trägt +`--shadow-overlay`. + +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. Fünf 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 @@ -150,6 +238,21 @@ 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. +**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. + +**`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. diff --git a/README.md b/README.md index 6481cf5..eb694ab 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Downloads landen direkt in `~/Downloads`, ohne Browser-Dialog. | Post 3 · Verbinden statt ersetzen | `post3.html` | Einzelbild, Theme `modern`, erscheint 17.08. | | | Post 4 · Workflow-Engine | `post4.html` | Carousel, 4 Slides, Theme `modern`, erscheint 19.08. | [`POST-4.md`](./POST-4.md) | | Post 5 · Projektmanagement | `post5.html` | Carousel, 5 Slides, Theme `modern`, erscheint 21.08. | [`POST-5.md`](./POST-5.md) | -| Post 6 · Team-Chat | `post6.html` | Carousel, 4 Slides, Theme `modern`, erscheint 26.08. | [`POST-6.md`](./POST-6.md) | +| Post 6 · Team-Chat | `post6.html` | Carousel, 5 Slides, Theme `modern`, erscheint 26.08. | [`POST-6.md`](./POST-6.md) | **Alle Streifen nebeneinander** zeigt `uebersicht.html`, erreichbar über *Projekt → Alle Projekte im Streifen*. Die Seite baut die Slides nicht selbst @@ -224,6 +224,7 @@ Baustellenfotos. | 2 | die Chatliste groß: Projektkanäle, Direktnachrichten, Gruppe | | 3 | Fotos, die Aufgabe daraus, die Bestätigung im Projekt | | 4 | das Menü hinter dem Plus im Eingabefeld | +| 5 | das Handy-Mockup der Website, verwischt, mit Absperrband | **Slide 3 ist der Kern**, und sie ist keine Konstruktion: im Werkzeug folgen die drei Nachrichten genau so aufeinander. Tobias schickt die Baustellenfotos, @@ -231,9 +232,26 @@ darauf eine Nachricht mit angehängter Aufgabe, darauf die Systemzeile „Aufgabe „Materialliste prüfen" im Projekt erstellt". Die letzte Zeile macht den Unterschied: die Aufgabe liegt danach im Projekt, nicht nur im Verlauf. -**Vier Slides, nicht fünf.** Der fünfte Punkt wäre DSGVO und deutsche Server -gewesen. Dafür gibt es keinen Bildschirm, und eine Behauptung ohne Bild gehört -in die Bildunterschrift. +**Slide 5 kommt von der Website, nicht aus dem Pitch-Paket.** Die App gibt es +noch nicht, und `ChatDemoSection.tsx` hat den Zustand bereits: ein Handy-Mockup +mit `blur(3px)` und darüber ein **Absperrband** aus dem Bau, -13° gedreht, in +`#F1B729` mit Kanten in `#171106`, darauf Schutzhelm und „BALD VERFÜGBAR". +Auch Inhalt, Blasenfarben, Überschrift und Fließtext sind die der Website. Ein +Zwischenstand hatte hier einen selbst erfundenen Aufkleber; das war eine eigene +Bildsprache neben einer vorhandenen. + +**Die Tiefe kommt aus dem Stapel und gestaffelten Schatten, nicht aus +Flächen.** Ein Zwischenstand legte graue Flächen hinter die Elemente; das nimmt +den Zeichnungen ihre Durchsichtigkeit und setzt einen Kasten auf den +Punktgrund. Stattdessen zweierlei: die echten Schatten aus `ui/theme.css`, die +dort **gestaffelt** sind (enge dunkle Kante plus weite helle, in SVG zwei +`feDropShadow` im selben Filter), und der **Stapel** aus `post1.html`, in SVG +nachgebaut mit denselben Winkeln, die dort im CSS stehen. Die hinteren Blätter +sind leer: Andeutung, keine Kopie. Auf Slide 1 und 2, wo ein Bildschirm zu +sehen ist; nicht auf Slide 3, wo ein Ausschnitt einer Unterhaltung steht. + +Dazu die Datumsmarke und die Zeile „Maike Berger schreibt gerade …" aus der +Quelle, die der Unterhaltung Struktur geben, ohne eine Fläche zu behaupten. **Die Bildergalerie ist eine Anhangkarte, kein Bild.** Im Werkzeug lädt `GalleryPreview` zwei Fotos von Unsplash. Fremde Fotos in einen Beitrag zu diff --git a/post6.html b/post6.html index 804f100..696c7f7 100644 --- a/post6.html +++ b/post6.html @@ -1305,7 +1305,7 @@ if (location.search.indexOf("nur=streifen") >= 0) eyebrow: "Team-Chat", head: 'Büro und Baustelle,
endlich im selben Chat.', body: "", - vis: "chat", visW: 896, visLeft: 92, visBottom: 120, + vis: "chat", visW: 948, visLeft: 66, visBottom: 120, }, { /* Der Fliesstext ist das Merkmal "Themen-Kanäle" von der Website, @@ -1316,7 +1316,7 @@ if (location.search.indexOf("nur=streifen") >= 0) eyebrow: "Die Kanäle", head: 'Ein Kanal
pro Projekt.', body: "Projektkanäle, Direktnachrichten, Gruppen. Jeder Kanal hängt an seiner Projektnummer.", - vis: "kanaele", visW: 620, visLeft: 230, visBottom: 120, + vis: "kanaele", visW: 672, visLeft: 204, visBottom: 120, }, { /* Der Satz aus dem Redaktionsplan: "Foto rein, Aufgabe raus." Die @@ -1337,6 +1337,22 @@ if (location.search.indexOf("nur=streifen") >= 0) body: "Dokument, Fotos, Sprachnachricht, Kontakt. Und wenn daraus etwas werden soll: Termin oder Aufgabe.", vis: "menue", visW: 800, visLeft: 140, visBottom: 120, }, + { + /* Ein Ausblick, kein Beleg. Die App gibt es noch nicht -- deshalb steht + die Marke "In Arbeit" ueber dem Geraet, und der Fliesstext sagt es + geradeheraus. Auf dem Schirm ist nichts Neues: dieselben zwei + Nachrichten wie auf Slide 1, nur schmaler gesetzt. + + "Im Buero am Rechner, auf der Baustelle am Handy" steht als Merkmal + "Desktop & Mobile" auf der Website. Der Satz beschreibt das Ziel, + nicht den heutigen Stand -- deshalb ist er hier nicht zitiert. */ + key: "handy", type: "feature", + layouts: [{ id: "standard", label: "Standard" }], + eyebrow: "Bald verfügbar", + head: 'Der Chat für unterwegs
wird gerade gebaut.', + body: "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.", + vis: "handy", visW: 358, visLeft: 361, visBottom: 120, + }, ]; /* Vorlage für eine selbst angelegte Slide. Sie startet bewusst mit Text @@ -1482,10 +1498,45 @@ if (location.search.indexOf("nur=streifen") >= 0) mit beschnitten (lib/chamfer.ts der Website). Die doppelte id in mehreren Blaettern stoert nicht: alle Filter sind gleich, und url(#..) nimmt den ersten. */ + /* Die Schatten des Produkts, aufgeloest aus ui/theme.css. Sie sind + gestaffelt und nicht einlagig -- genau daran hing es, dass die + Zeichnungen flach aussahen: + + --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 Flaeche, die weite helle hebt es davon ab. In + SVG heisst das zwei feDropShadow hintereinander im selben Filter. + + Der negative Spread der CSS-Fassung (-2px, -4px) laesst sich in SVG nicht + nachbilden; er wird ueber eine etwas kleinere Streuung angenaehert. */ + /* Wie wfSvg, aber mit Rand: der Inhalt wird um `rand` verschoben und der + Kasten entsprechend groesser. Damit liegt eine Flaeche, die bei -rand + beginnt, genau auf der Kante des Kastens -- und die Randmessung sieht + wieder, was wirklich dasteht. */ + const svgMitRand = (b, h, rand, inhalt) => + wfSvg(b + 2 * rand, h + 2 * rand, + '' + inhalt + ''); + const SCHATTEN_DEFS = - '' + - ''; + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + ''; // Eine Karte: Flaeche, Haarlinie, Schatten. Das ist die Grundform des Themes. const karteFlaeche = (x, y, w, h, c, fuell) => @@ -1822,6 +1873,8 @@ if (location.search.indexOf("nur=streifen") >= 0) und der -Rahmen muss weg, sonst rendert ein einkindriges Zeichen nicht. */ const FEATHER = { + "hard-hat": " ", + "building": " ", "briefcase": " ", "folder-open": "", "mic": " ", @@ -2381,6 +2434,20 @@ if (location.search.indexOf("nur=streifen") >= 0) Alle Namen, Zeiten, Vorschautexte und Anhaenge stehen woertlich in CHATS und INITIAL_MESSAGES. Erfunden ist nichts. */ const CHAT_TEXT = "#24242B", CHAT_TEXT2 = "#505057", CHAT_TEXT3 = "#858589"; + /* Die drei Flaechen des Werkzeugs, aufgeloest aus ui/theme.css: + surface-neutral-primary grey-50 #F5F4F4 der Grund der Anwendung + surface-neutral-tertiary grey-100 #F0F0F0 die Unterhaltungsspalte + surface-neutral-secondary grey-0 #FAFAFA Karten und Blasen + + Auf den Slides werden sie nicht als Hintergrund gemalt -- die + Zeichnungen bleiben durchsichtig und stehen auf dem Punktgrund der + Slide. Gebraucht werden die Werte dort, wo eine Flaeche wirklich eine + ist: im Schirm des Geraets auf Slide 5. + + Karten und Blasen stehen auf reinem Weiss statt auf #FAFAFA. Auf dem + hellen Blatt der Slide waere der Unterschied sonst nicht mehr zu sehen + und die Blase verloere ihre Kante. */ + const FL_GRUND = "#F5F4F4", FL_SPALTE = "#F0F0F0", FL_KARTE = "#FFFFFF"; const CHAT_RAND = "#E5E5E5"; /* Die Chatliste links. type steuert das Sinnbild davor: ein Projekt bekommt @@ -2420,8 +2487,12 @@ if (location.search.indexOf("nur=streifen") >= 0) function chatZeile(x, y, c) { let g = ""; if (c.aktiv) { - g += ''; + /* Der offene Kanal ist hell hinterlegt, mit Rand und feinem Schatten -- + nicht farbig. Der erste Nachbau hatte ihn violett; im Original steht + dort bg-surface-neutral-primary mit border und shadow-sm. */ + g += ''; } const mx = x + 16, my = y + ZEILE_H / 2; if (c.art === "direkt") { @@ -2471,9 +2542,16 @@ if (location.search.indexOf("nur=streifen") >= 0) const BLASE_MAX = 470; const blaseBreite = spalte => Math.min(BLASE_MAX, spalte - 60); + /* Wie breit eine Anhangkarte sein muss, damit ihr Titel ungekuerzt + hineinpasst. Ohne diese Rechnung war die Blase nur so breit wie ihr + Fliesstext, und "Materialliste pruefen · faellig 31.07.2026" wurde + abgeschnitten -- an einer Stelle, an der der Titel die Aussage traegt. */ + const anhangNoetig = a => + 52 + messen(a[1], 15, 600) + (a[3] ? messen(a[3], 13, 800) + 56 : 0) + 12; + function anhangKarte(x, y, b, ikon, titel, meta, marke) { let g = ''; + 'fill="' + FL_KARTE + '" stroke="' + CHAT_RAND + '" stroke-width="1.2"/>'; g += ''; g += feather(ikon, x + 26, y + 27, 17, CHAT_TEXT2, 1.9); @@ -2496,7 +2574,7 @@ if (location.search.indexOf("nur=streifen") >= 0) let b = 32 + Math.max( ...zeilen.map(z => messen(z, 17, 400)), m.mein ? 0 : messen(m.autor, 14, 800), - m.anhang ? 300 : 0, 90); + m.anhang ? anhangNoetig(m.anhang) : 0, 90); return { zeilen, hoehe: Math.round(h), breite: Math.round(Math.min(b, max || BLASE_MAX)) }; } @@ -2520,7 +2598,7 @@ if (location.search.indexOf("nur=streifen") >= 0) } const kontur = rundRechteck(bx, y, ma.breite, ma.hoehe, 14); g += ''; + (m.mein ? "#C8FFF7" : FL_KARTE) + '"/>'; if (!m.mein) g += ''; let cy = y + 14; @@ -2564,13 +2642,89 @@ if (location.search.indexOf("nur=streifen") >= 0) ]; + /* Der Stapel-Look aus post1.html, in SVG nachgebaut. Dort macht ihn CSS + ueber zwei Blaetter hinter dem Bild: + + .sheet-mid rotate(-2.4deg) translate(-2.2%, 3%) scale(.975) + .sheet-back rotate( 3.2deg) translate( 3%, 5.5%) scale(.95) + + Dieselben Winkel und Verschiebungen, damit die Reihe zusammenpasst. In + CSS liegt der Drehpunkt in der Mitte und die Prozente beziehen sich auf + die eigene Groesse -- in SVG heisst das + translate(mitte) rotate() translate(prozent*groesse) scale() translate(-mitte). + + Der Stapel traegt zwei Aufgaben: er gibt Tiefe, die ein Schatten allein + nicht schafft, und er sagt, dass hinter der einen Ansicht weitere + liegen. Deshalb sind die hinteren Blaetter leer -- sie sind Andeutung, + keine Kopie. */ + /* Die beiden hinteren Blaetter je: Winkel, Verschiebung in Anteilen der + eigenen Groesse, Massstab, Fuellung. Reihenfolge wie in CSS -- erst + skalieren, dann verschieben, dann drehen. */ + const STAPEL_BLAETTER = [ + { winkel: 3.2, px: 0.03, py: 0.055, skala: 0.95, fuell: "#F7F7F8" }, + { winkel: -2.4, px: -0.022, py: 0.03, skala: 0.975, fuell: "#FCFCFD" }, + ]; + + /* Wie weit ragen die gedrehten Blaetter ueber das vordere hinaus? Gerechnet + statt geschaetzt: die vier Ecken durch dieselbe Kette schicken und den + groessten Ueberstand nehmen. Der erste Anlauf hatte den Ueberstand + ignoriert -- die Blaetter standen dann bis zu 24 px ausserhalb der + 92-px-Schutzzone. */ + function stapelRand(b, h) { + let raus = 0; + for (const bl of STAPEL_BLAETTER) { + const co = Math.cos(bl.winkel * Math.PI / 180), si = Math.sin(bl.winkel * Math.PI / 180); + for (const [ex, ey] of [[-1, -1], [1, -1], [1, 1], [-1, 1]]) { + // Ecke relativ zur Mitte: skalieren, verschieben, drehen. + const x = ex * b / 2 * bl.skala + bl.px * b; + const y = ey * h / 2 * bl.skala + bl.py * h; + raus = Math.max(raus, Math.abs(x * co - y * si) - b / 2, + Math.abs(x * si + y * co) - h / 2); + } + } + return Math.ceil(raus) + 2; + } + + /* Der Stapel-Look aus post1.html, in SVG nachgebaut. Dort macht ihn CSS + ueber zwei Blaetter hinter dem Bild, mit genau diesen Winkeln und + Verschiebungen -- damit die Reihe zusammenpasst. + + Der Stapel traegt zwei Aufgaben: er gibt Tiefe, die ein Schatten allein + nicht schafft, und er sagt, dass hinter der einen Ansicht weitere + liegen. Deshalb sind die hinteren Blaetter leer -- sie sind Andeutung, + keine Kopie. + + b und h sind die Groesse des vorderen Blattes. Der Kasten drumherum wird + um stapelRand() groesser; darum kuemmert sich stapelKasten(). */ + function stapel(b, h, radius, inhalt) { + const m = stapelRand(b, h); + const blatt = bl => { + const cx = b / 2, cy = h / 2; + return '' + + ''; + }; + return blatt(STAPEL_BLAETTER[0]) + blatt(STAPEL_BLAETTER[1]) + + '' + + '' + inhalt + ''; + } + + // Der Kasten, in den ein Stapel dieser Groesse hineinpasst. + const stapelKasten = (b, h, radius, inhalt) => { + const m = stapelRand(b, h); + return wfSvg(b + 2 * m, h + 2 * m, stapel(b, h, radius, inhalt)); + }; + /* ─── Slide 1 · Der Chat ───────────────────────────────────────────────── Kanalliste links, Verlauf rechts. Im Profilraster ist die Slide eine Kachel von rund 150 px -- dort ist kein Wort lesbar, und was ankommt, sind zwei Spalten mit Blasen. Daran erkennt jemand einen Chat, bevor er etwas gelesen hat. Dieselbe Erwaegung wie bei Post 3, 4 und 5. */ function visChat() { - const SP = 26, RECHTS = 440, W = LISTE_B + SP + RECHTS; + const SP = 26, RECHTS = 460, W = LISTE_B + SP + RECHTS, PAD = 26; let g = ""; /* Die Filterzeile ueber der Liste. Die vier Woerter sind der Typ @@ -2579,18 +2733,35 @@ if (location.search.indexOf("nur=streifen") >= 0) ["Alle", "Projekte", "Direkt", "Ungelesen"].forEach((f, i) => { const b = Math.round(messen(f, 15, 700)) + 26; g += ''; g += uiText(fx + b / 2, 21, f, 15, 700, i === 0 ? "#FFFFFF" : CHAT_TEXT2, { anker: "middle" }); fx += b + 8; }); + /* Die Unterhaltungsspalte liegt auf eigener Flaeche, eine Stufe dunkler + als die Blasen darauf. Das ist die Tiefe, die vorher fehlte: ohne sie + schwebten weisse Blasen auf weissem Grund. */ const listeO = 50; CHATS.forEach((c, i) => { g += chatZeile(0, listeO + i * ZEILE_H, c); }); const listeH = listeO + CHATS.length * ZEILE_H; - // Die Kopfzeile des offenen Kanals, wie in ChatHeader. const kx = LISTE_B + SP; + /* Keine Flaeche hinter der Unterhaltung. Ein Zwischenstand hatte hier + drei graue Stufen liegen, um Tiefe zu erzeugen -- das nimmt den + Zeichnungen aber ihre Durchsichtigkeit und setzt einen Kasten auf den + Punktgrund, der da nicht hingehoert. Die Tiefe kommt jetzt allein aus + den gestaffelten Schatten und daraus, dass die Blasen einen Rand + tragen und der offene Kanal als Karte liegt. + + Die duenne Linie zwischen Liste und Unterhaltung bleibt: sie steht so + im Werkzeug (border-r an der Chatliste) und trennt zwei Spalten, ohne + eine Flaeche zu behaupten. */ + g += ''; + g = '' + g + ''; + + // Die Kopfzeile des offenen Kanals, wie in ChatHeader. g += ''; g += feather("briefcase", kx + 20, 20, 20, "#691CD4", 1.9); @@ -2602,24 +2773,46 @@ if (location.search.indexOf("nur=streifen") >= 0) /* Der Verlauf laeuft bis kurz vor das Eingabefeld. Was nicht mehr hineinpasst, faellt weg -- ein Chat ist oben abgeschnitten, nicht unten, aber fuer ein Standbild ist die Leserichtung wichtiger. */ - const feldH = 64, platz = listeH - feldH - 20; - let y = 76; + const feldH = 64, tippH = 40, platz = listeH - feldH - tippH - 16; + + /* Die Datumsmarke steht so ueber dem ersten Beitrag des Tages, als + Badge mit dem Ton brand-primary-subtle. Sie kostet wenig und sagt, + dass man einen Verlauf sieht und keine Liste. */ + const dt = "Heute · 30.07.2026"; + const dtB = Math.round(messen(dt, 14, 700)) + 28; + g += '' + + uiText(kx + RECHTS / 2, 92, dt, 14, 700, "#9B5E0D", { anker: "middle" }); + + let y = 118; for (const m of VERLAUF) { if (y + blaseHoehe(m, RECHTS) > platz) break; g += blase(kx, y, RECHTS, m); y += blaseHoehe(m, RECHTS) + 14; } + /* "Maike Berger schreibt gerade …" steht im Werkzeug als eigene Zeile + ueber dem Eingabefeld. Sie sagt in vier Woertern, dass da jemand + mitliest -- das traegt eine Standaufnahme besser als jede Bewegung. */ + const tt = "Maike Berger schreibt gerade …"; + const ttB = Math.round(messen(tt, 14, 500)) + 32; + const ty = listeH - feldH - tippH + 2; + g += '' + + uiText(kx + 16, ty + 21, tt, 14, 500, CHAT_TEXT3); + // Das Eingabefeld unten, wie in Composer. const fy = listeH - feldH; g += ''; g += feather("plus", kx + 20, fy + 34, 21, CHAT_TEXT2, 2); - g += ''; + g += ''; g += uiText(kx + 62, fy + 40, "Nachricht schreiben …", 16, 400, "#C9C9CE"); g += feather("mic", kx + RECHTS - 20, fy + 34, 21, CHAT_TEXT2, 2); - return wfSvg(W, listeH, g); + return stapelKasten(W + 2 * PAD, listeH + 2 * PAD, 22, g); } /* ─── Slide 2 · Die Kanäle ─────────────────────────────────────────────── @@ -2632,9 +2825,10 @@ if (location.search.indexOf("nur=streifen") >= 0) Direktnachricht, Gruppe -- und lassen die Zeilen gross genug, dass die Projektnummer lesbar bleibt. Eine Liste scrollt ohnehin. */ const zeigen = CHATS.slice(0, 5); + const hoehe = zeigen.length * ZEILE_H, PAD = 22; let g = ""; - zeigen.forEach((c, i) => { g += chatZeile(0, i * ZEILE_H, c); }); - return wfSvg(LISTE_B, zeigen.length * ZEILE_H, g); + zeigen.forEach((c, i) => { g += chatZeile(PAD, PAD + i * ZEILE_H, c); }); + return stapelKasten(LISTE_B + 2 * PAD, hoehe + 2 * PAD, 20, g); } /* ─── Slide 3 · Aus der Nachricht wird die Aufgabe ─────────────────────── @@ -2643,13 +2837,16 @@ if (location.search.indexOf("nur=streifen") >= 0) sie im Projekt liegt. Das ist der Satz aus dem Redaktionsplan -- "Foto rein, Aufgabe raus" -- und er steht so im Werkzeug. */ function visAufgabe() { - const W = 520; - let g = "", y = 0; + /* Schmaler gesetzt als die Spalte auf Slide 1: dadurch skaliert die + Zeichnung groesser und fuellt die Flaeche, statt mit einem Handbreit + Leere darueber zu stehen. */ + const W = 470; + let inhalt = "", y = 0; for (const m of VERLAUF.slice(2, 5)) { - g += blase(0, y, W, m); + inhalt += blase(0, y, W, m); y += blaseHoehe(m, W) + 18; } - return wfSvg(W, y - 18, g); + return wfSvg(W, y - 18, inhalt); } /* ─── Slide 4 · Was noch in den Chat passt ─────────────────────────────── @@ -2670,7 +2867,9 @@ if (location.search.indexOf("nur=streifen") >= 0) const W = 470, ZH = 52, PAD = 14; const hoehe = 2 * PAD + MENUE.reduce((a, e) => a + (e ? ZH : 13), 0); const kontur = rundRechteck(0, 0, W, hoehe, 14); - let g = '' + + /* Ein aufgeklapptes Menue liegt ueber allem anderen. Im Werkzeug traegt + es --shadow-overlay, also dieselbe Staffelung wie --shadow-lg. */ + let g = '' + ''; let y = PAD; for (const e of MENUE) { @@ -2687,18 +2886,158 @@ if (location.search.indexOf("nur=streifen") >= 0) es schwebt die Liste im Nichts und man sieht nicht, woher sie kommt. */ const fy = hoehe + 18; g += feather("plus", 20, fy + 26, 22, CHAT_TEXT2, 2); - g += ''; + g += ''; g += uiText(64, fy + 33, "Nachricht schreiben …", 17, 400, "#C9C9CE"); g += feather("mic", W - 20, fy + 26, 22, CHAT_TEXT2, 2); return wfSvg(W, fy + 52, g); } + + /* ─── Slide 5 · Der Chat für unterwegs ────────────────────────────────── + Diese Slide erfindet nichts: die Website hat den Zustand bereits, in + components/sections/ChatDemoSection.tsx unter der Fahne `comingSoon`. + Dort steht ein Handy-Mockup, das verwischt wird (blur(3px)), und + darueber liegt ein Absperrband -- gedreht, mit Schutzhelm und der Zeile + "Bald verfuegbar", wieder und wieder. + + Ich hatte das zuerst selbst gebaut, mit einem gelben Aufkleber "In + Arbeit". Das war eine eigene Erfindung neben einer vorhandenen + Bildsprache. Uebernommen ist jetzt die der Website, samt Winkel (-13°), + Farben (#F1B729 auf #171106) und Wortlaut. + + Auch der Inhalt des Schirms kommt von dort und nicht aus dem Pitch-Paket: + Heizungsprojekt Schulz, 4 Mitglieder · 2 online, die Nachrichten aus + seedMessages. Auf der Website steht dieses Handy fuer den mobilen Chat -- + dann soll es hier dasselbe zeigen. */ + const WEB = { + grund: "#F5F2EF", // CHAT_BACKGROUND, hsl(30 17% 95.5%) + eigen: "#0D6E6E", // OWN_BUBBLE_BACKGROUND + eigenR: "#0A5A5A", // OWN_BUBBLE_BORDER + fremd: "#FFFFFF", // OTHER_BUBBLE_BACKGROUND + fremdR: "#E9E7E5", // OTHER_BUBBLE_BORDER + zeit: "#7A726C", // OTHER_BUBBLE_TIME + sender: "#F59F0A", // OTHER_BUBBLE_SENDER + rahmen: "#D1D5DB", // border des Mockups + band: "#F1B729", // accent-yellow + bandDunkel: "#171106", + }; + + // seedMessages aus ChatDemoSection.tsx, woertlich und in dieser Reihenfolge. + const WEB_NACHRICHTEN = [ + ["Murat Kaya", "Foto vom Heizungskeller hochgeladen.", "09:12", false], + ["Lea Brandt", "Aufmaß ist fertig, schicke gleich die Unterlagen.", "09:15", false], + ["Du", "Super, dann können wir morgen mit der Installation anfangen.", "09:17", true], + ["Murat Kaya", "Material ist bestellt, Lieferung Donnerstag.", "09:20", false], + ]; + + function visHandy() { + /* Das Geraet. Die Website zeichnet nur einen hellen Umriss (border 4px + #D1D5DB) -- auf einer Slide, die selbst hell ist, faellt der in sich + zusammen und sieht nach Platzhalter aus. Hier steht deshalb ein + richtiges Geraet: dunkler Korpus, heller Grat darauf, Schirm + eingelassen, Tasten an der Seite. Das ist eine Abweichung, aber eine + am Rahmen und nicht am Inhalt -- was auf dem Schirm steht, bleibt das + der Website. + + Der Schirm wird weniger stark verwischt als dort. Auf der Website + liegt das Handy gross neben Fliesstext und darf unlesbar sein; hier + ist es klein, und bei blur(6) blieb nur Brei. Erkennbar, aber nicht + lesbar ist die Absicht. */ + const B = 600, H = 1240, BEZEL = 20, R = 92; + const sx = BEZEL, sy = BEZEL, sb = B - 2 * BEZEL, sh = H - 2 * BEZEL, sr = R - BEZEL + 4; + let schirm = ""; + + // Statuszeile und Kopf. + schirm += uiText(sx + 40, sy + 56, "9:41", 26, 700, CHAT_TEXT); + const ko = sy + 84; + schirm += ''; + schirm += uiText(sx + 40, sy + 56, "9:41", 26, 700, CHAT_TEXT); + schirm += ''; + schirm += feather("building", sx + 64, ko + 56, 30, "#8A6116", 2.2); + schirm += uiText(sx + 110, ko + 50, "Heizungsprojekt Schulz", 27, 700, CHAT_TEXT); + schirm += uiText(sx + 110, ko + 80, "4 Mitglieder · 2 online", 21, 500, CHAT_TEXT2); + schirm += ''; + + let y = ko + 142; + for (const [wer, text, zeit, eigen] of WEB_NACHRICHTEN) { + const maxB = sb - 88; + const zeilen = umbrechen(text, maxB - 40, 24, 400); + const h = 20 + (eigen ? 0 : 26) + zeilen.length * 32 + 26; + const breite = 40 + Math.max(...zeilen.map(z => messen(z, 24, 400)), + eigen ? 0 : messen(wer, 19, 700)); + const x0 = eigen ? sx + sb - 28 - breite : sx + 28; + const k = rundRechteck(x0, y, breite, h, 20); + schirm += ''; + let cy = y + 20; + if (!eigen) { schirm += uiText(x0 + 20, cy + 19, wer, 19, 700, WEB.sender); cy += 26; } + zeilen.forEach((z, i) => { + schirm += uiText(x0 + 20, cy + 25 + i * 32, z, 24, 400, eigen ? "#FFFFFF" : CHAT_TEXT); + }); + schirm += uiText(x0 + 20, cy + 25 + zeilen.length * 32, zeit, 18, 400, + eigen ? "rgba(255,255,255,0.82)" : WEB.zeit); + y += h + 18; + } + + const fy = sy + sh - 92; + schirm += ''; + schirm += ''; + schirm += uiText(sx + 54, fy + 43, "Nachricht schreiben...", 23, 400, "#A8A29E"); + // Der Balken unten, den jedes heutige Geraet hat. + schirm += ''; + + /* Der Korpus. Tasten zuerst, damit sie hinter dem Rand verschwinden. */ + let g = ''; + [[-6, 300, 90], [-6, 420, 130], [B, 360, 150]].forEach(([tx, ty2, th]) => { + g += ''; + }); + g += ''; + g += ''; + g += ''; + g += ''; + g += '' + schirm + ''; + // Die Insel oben. + g += ''; + + /* Das Absperrband, Winkel und Wortlaut der Website. */ + const bh = 64, ty = H / 2 - bh / 2; + const bL = B / Math.cos(13 * Math.PI / 180) + 24, bA = (B - bL) / 2; + let band = '' + + '' + + ''; + const wort = "BALD VERFÜGBAR"; + const wb = messen(wort, 21, 800) + 21 * 0.16 * wort.length; + let bx = bA + 14; + while (bx < bA + bL - 40) { + band += feather("hard-hat", bx + 13, ty + bh / 2, 24, WEB.bandDunkel, 2.6); + band += uiText(bx + 32, ty + bh / 2 + 8, wort, 21, 800, WEB.bandDunkel, { sperr: "0.16em" }); + bx += wb + 78; + } + g += '' + band + ''; + return wfSvg(B, H, g); + } + const VIS = { chat: () => visChat(), kanaele: () => visKanaele(), aufgabe: () => visAufgabe(), menue: () => visMenue(), + handy: () => visHandy(), }; /* ─── Das Deck ──────────────────────────────────────────────────────────── diff --git a/state/post6.json b/state/post6.json index 3644b35..ea5db11 100644 --- a/state/post6.json +++ b/state/post6.json @@ -13,6 +13,10 @@ "bg": "raster", "int": "dezent" }, + { + "bg": "raster", + "int": "dezent" + }, { "bg": "raster", "int": "dezent" @@ -35,6 +39,10 @@ { "ref": "menue", "werte": {} + }, + { + "ref": "handy", + "werte": {} } ], "edits": [ @@ -53,6 +61,11 @@ "dy": 3 } }, + { + "headline": { + "dy": 3 + } + }, { "headline": { "dy": 3 @@ -64,6 +77,7 @@ "standard", "standard", "standard", + "standard", "standard" ], "rasterAn": "punkte",