Theme modern: flaches Band im Slack-Look, Karten mit Schatten

Der metallisch schattierte Zylinder ist raus. Er sah fotorealistisch aus
und damit wie ein Fremdkoerper auf einem flachen, ruhigen Blatt. An seiner
Stelle steht eine deckende Flaeche mit einer blassen zweiten dahinter --
kein Verlauf, keine Woelbung, keine Glanzkante.

Zwei Toene, umschaltbar unter "Band" im Bedienfeld: Orange #F1B729 und
"HiveGrid Teal", was --dark-bg #30313D meint und nicht --accent-teal.
Der Unterschied steht jetzt in rules.md, damit die Verwechslung nicht
noch einmal passiert.

Das Band laeuft ueber Slide 3 bis 5 (BAND_VON / BAND_BIS). Slide 1 und 6
sollen ruhig sein, Slide 2 traegt ihre eigene orangene Leitung. Gezeichnet
als geschlossene Flaeche statt als dicke Linie: eine Linie bekommt am Ende
eine Kappe rechtwinklig zur Laufrichtung, und die stand bei schraeger Bahn
seitlich ueber die Naht.

Schatten sind fuer dieses Theme erlaubt, entgegen 5. Ohne sie stehen die
Karten gestochen auf dem Grund statt darueber. Als SVG-feDropShadow, weil
die Karten gezeichnet sind und ein CSS-Schatten vom clip-path beschnitten
wuerde. Noch nicht 3-fach exportgeprueft -- das steht als Auflage in der
Regel.

Ausserdem:

- Slide 1: die G-Marke in der Bauchbinde statt einer leeren Wabe, das
  Badge der Landingpage (Monospace, 10 Grad gedreht) statt des Chips, und
  das Foto fuellt die Inhaltsspalte statt 620 von 1080 px.
- Slide 6: das selbst gezeichnete Konfetti war ein Kleckshaufen und ist
  jetzt "rocket" aus dem Feather-Satz.
- Kacheln haben wieder einen Boden statt harter Kontur.
- migrateState 7 raeumt die gespeicherten Vis-Positionen einmalig weg:
  die Masse haben sich geaendert, und der offene Tab schreibt die Datei
  sonst zurueck (POST-2.md 6).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
dejanandClaude Opus 5 committed 2026-08-13 21:02:47 +02:00
1 parent 5fe1d2945f
commit d578d12991
3 files changed
+284 -206

No files matched your search

+177 -120
View File
@@ -433,7 +433,7 @@
ueber diesen Rand hinausragen und wird erst vom Aussenradius
beschnitten -- daher inset 0 und z-index 4. */
.karte {
position: relative; aspect-ratio: 4/5; overflow: hidden;
position: relative; aspect-ratio: 1/1; overflow: hidden;
background: #E8F4F4; isolation: isolate; border-radius: 38px;
}
.karte-grund {
@@ -441,14 +441,15 @@
border-radius: 27px; background: #2b2d2a;
filter: grayscale(.04) saturate(.74) brightness(.73);
}
.karte-grund img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.karte-grund img { position: absolute; inset: 0; width: 100%; height: 100%;
object-fit: cover; object-position: 50% 34%; }
.karte-wash {
position: absolute; inset: 4.2%; border-radius: 27px;
background: linear-gradient(180deg, rgba(15,26,46,.01), rgba(15,26,46,.12));
}
.karte-person {
position: absolute; inset: 0; z-index: 4;
width: 100%; height: 100%; object-fit: cover;
width: 100%; height: 100%; object-fit: cover; object-position: 50% 34%;
filter: saturate(1) contrast(1.01);
}
/* Die Infokarte schwebt ueber dem Foto, statt als flaches Schild darin zu
@@ -463,25 +464,38 @@
calc(100% - 14px) 100%, 14px 100%, 0 calc(100% - 14px), 0 14px
);
}
/* Die G-Marke statt einer nackten Wabe. Sie hat fast dieselben
Proportionen (384,5 : 332,9), passt also in denselben Platz. Als
Inline-SVG und nicht als Datei: ein Bild im foreignObject kann beim
Export verschwinden (siehe 7). */
.karte-name .marke {
width: 52px; height: 46px; flex: none; background: var(--accent-yellow);
clip-path: polygon(25% 0%, 75% 0%, 100% 50%, 75% 100%, 25% 100%, 0% 50%);
width: 52px; height: 45px; flex: none; display: block;
}
.karte-name .marke svg { display: block; width: 100%; height: 100%; }
.karte-name .zeilen { display: flex; flex-direction: column; gap: 2px; }
.karte-name b { font-size: 30px; font-weight: 800; letter-spacing: -.025em;
line-height: 1.1; color: #1A1A1A; }
.karte-name small { font-size: 20px; font-weight: 600; color: #555555; }
/* Der Chip oben rechts traegt die Einordnung, die sonst im Fliesstext
stuende. Inhalt, keine Verzierung. */
/* Das Badge der Landingpage, Werte aus LandingPageClient.tsx: Monospace,
um 10 Grad gedreht, rechts ueber die Kante haengend. Masse mit 1,9
skaliert -- die Website zeigt es auf einem 680er Foto, hier steht es auf
einem 1080er Blatt, das im Feed auf gut 400 px schrumpft. Der
drop-shadow der Vorlage faellt weg (§5).
Es liegt neben .karte und nicht darin: .karte hat overflow:hidden fuer
den Radius, und ein Badge, das ueber die Kante haengt, waere dort
abgeschnitten. */
.karte-chip {
position: absolute; right: 5.4%; top: 5.4%; z-index: 5;
padding: 10px 18px; background: var(--accent-yellow);
color: var(--accent-yellow-dark);
font-size: 21px; font-weight: 700; letter-spacing: .12em; text-transform: uppercase;
position: absolute; right: -50px; top: 104px; z-index: 7;
padding: 24px 32px; background: var(--accent-yellow); color: #3F2F0A;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 25px; font-weight: 850; letter-spacing: .1em;
text-transform: uppercase; white-space: nowrap;
transform: rotate(10deg); transform-origin: right center;
clip-path: polygon(
10px 0, calc(100% - 10px) 0, 100% 10px, 100% calc(100% - 10px),
calc(100% - 10px) 100%, 10px 100%, 0 calc(100% - 10px), 0 10px
19px 0, calc(100% - 19px) 0, 100% 19px, 100% calc(100% - 19px),
calc(100% - 19px) 100%, 19px 100%, 0 calc(100% - 19px), 0 19px
);
}
@@ -1052,6 +1066,14 @@
</div>
</details>
<details class="group" data-grp="band">
<summary>Band</summary>
<div class="opts" id="bands">
<button data-b="orange" aria-pressed="true">Orange</button>
<button data-b="teal" aria-pressed="false">HiveGrid&nbsp;Teal</button>
</div>
</details>
<details class="group" data-grp="bildrahmen">
<summary>Bildrahmen</summary>
<div class="opts three" id="frames">
@@ -1206,7 +1228,7 @@
eyebrow: "Die Geschichte",
head: 'Die Geschichte fängt<br><span class="a">auf der Baustelle an.</span>',
body: "Noah Geerkens, SHK-Betrieb in vierter Generation.",
vis: "karte", visW: 620, visLeft: 230, visBottom: 76,
vis: "karte", visW: 896, visLeft: 92, visBottom: 96,
},
{
key: "prozesse", type: "feature",
@@ -1238,7 +1260,7 @@
eyebrow: "Die Lösung",
head: 'Dein Branchenprogramm<br><span class="a">bleibt, wo es ist.</span>',
body: "Kein Ersatz, sondern der Aufbau drumherum. HiveGrid holt raus, was bisher gefehlt hat.",
vis: "verbinden", visW: 896, visLeft: 92, visBottom: 120,
vis: "verbinden", visW: 988, visLeft: 0, visBottom: 120,
},
{
key: "warteliste", type: "feature",
@@ -1250,7 +1272,7 @@
eine Abweichung, die niemandem auffaellt und trotzdem stoert. */
head: 'Vom Handwerker<br>für <span class="a">Handwerker</span>',
body: "Kein Startup-Projekt. Setz dich auf die Warteliste, dann melden wir uns.",
vis: "warteliste", visW: 896, visLeft: 92, visBottom: 170,
vis: "warteliste", visW: 896, visLeft: 92, visBottom: 240,
},
];
@@ -1373,7 +1395,8 @@
GELB = "#F1B729", GELB_DUNKEL = "#3E2C0A", GELB_HELL = "#FFE082",
TEAL = "#0D6E6E", TEAL_HELL = "#E8F4F4",
ROT = "#E07B54", ROT_HELL = "#FBEAE4", // accent-orange der Website
LINIE = "#E5E5E5", KANTE = "#D5DBE1", KARTE = "#FFFFFF";
LINIE = "#E5E5E5", KANTE = "#D5DBE1", KARTE = "#FFFFFF",
GRUND = "#F6F8FA"; // --container-bg, der Boden unter einem Zeichen
function farbenNachziehen() {
const d = variant === "dunkel";
TINTE = d ? "#FFFFFF" : "#1A1A1A";
@@ -1386,6 +1409,7 @@
LINIE = d ? "rgba(255,255,255,.14)" : "#E5E5E5";
KANTE = d ? "rgba(255,255,255,.22)" : "#D5DBE1";
KARTE = d ? "#1A2740" : "#FFFFFF";
GRUND = d ? "#222F49" : "#F6F8FA";
}
/* Der Chamfer der Marke: 16 px Schnitt, wie in lib/chamfer.ts der Website.
@@ -1393,6 +1417,26 @@
clip-path beschneidet ihn mit. Also fuellen statt umranden. */
const CHAMFER_C = 16;
/* Weicher Schatten. Bis hierhin waren die Posts schattenfrei (rules.md 5),
fuer das Theme modern ist das aufgehoben: ohne ihn stehen die Kaesten
gestochen auf dem Grund, statt darueber zu schweben.
Als SVG-Filter und nicht als box-shadow -- die Kaesten sind gezeichnet
und nicht im DOM, und ein CSS-Schatten wuerde vom clip-path ohnehin
mit beschnitten (lib/chamfer.ts der Website). Die doppelte id in
mehreren Blaettern stoert nicht: alle Filter sind gleich, und url(#..)
nimmt den ersten. */
const SCHATTEN_DEFS =
'<defs><filter id="hgschatten" x="-35%" y="-35%" width="170%" height="170%">' +
'<feDropShadow dx="0" dy="7" stdDeviation="10" flood-color="#101828" ' +
'flood-opacity="0.14"/></filter></defs>';
// Eine Karte: Flaeche, Haarlinie, Schatten. Das ist die Grundform des Themes.
const karteFlaeche = (x, y, w, h, c, fuell) =>
'<g filter="url(#hgschatten)"><path d="' +
chamfer(x, y, w, h, c == null ? CHAMFER_C : c) + '" fill="' + (fuell || KARTE) +
'" stroke="' + KANTE + '" stroke-width="1.2"/></g>';
/* Die G-Marke, Punkte woertlich aus assets/brand/g-mark.svg. Als Pfad und
nicht als Bild: ein PNG im foreignObject kann beim Export verschwinden
(rules.md §7), ein Pfad nie. Der Massstab braucht mehr als eine
@@ -1738,8 +1782,7 @@
c.r = c.x + c.w;
});
// Deckender Grund: sonst laeuft das Band mitten durch die Leitung.
let raus = chamferFlaeche(0, 0, W, H, KARTE, 22, true);
let raus = "";
// Rechte Kette: von rechter Kante zu rechter Kante, mit weiten Bogen.
const rechts = [
@@ -1764,18 +1807,18 @@
gewachsen, durchgezogen und gelb auf Slide 5 heisst verbunden: dieselbe
Farbe, aber die Strichart trennt die beiden Aussagen. Etwas kraeftiger
als in Grau, weil eine unterbrochene Linie weniger Flaeche hat. */
raus += zug(leitung, { f: GELB, w: 2.9, op: .78, strichel: "11 13" });
raus += zug(leitung, { f: GELB, w: 2.9, op: .85, strichel: "11 13" });
[[700, 96, "file-text", 52], [770, 236, "image", 46], [760, 430, "user", 50],
[250, 690, "file-text", 44]]
.forEach(k => {
raus += chamferFlaeche(k[0] - k[3] / 2, k[1] - k[3] / 2, k[3], k[3], KARTE, 9, true);
raus += feather(k[2], k[0], k[1], k[3] * 0.46, TEXT3, 2);
raus += karteFlaeche(k[0] - k[3] / 2, k[1] - k[3] / 2, k[3], k[3], 9, GRUND);
raus += feather(k[2], k[0], k[1], k[3] * 0.46, TEXT3, 1.8);
});
chips.forEach(c => {
const y = c.y - HOCH / 2;
raus += chamferFlaeche(c.x, y, c.w, HOCH, KARTE, 13, true);
raus += karteFlaeche(c.x, y, c.w, HOCH, 13);
raus += feather(c.i, c.x + 34, c.y, 30, TINTE, 2);
raus += '<text x="' + (c.x + 64) + '" y="' + (c.y + 10) + '" ' +
'font-family="Manrope, sans-serif" font-size="27" font-weight="600" ' +
@@ -1783,7 +1826,7 @@
});
return '<svg viewBox="0 0 ' + W + ' ' + H + '" width="' + W + '" height="' + H +
'" xmlns="http://www.w3.org/2000/svg">' + raus + '</svg>';
'" xmlns="http://www.w3.org/2000/svg">' + SCHATTEN_DEFS + raus + '</svg>';
}
/* ─── Slide 1: Foto im gezeichneten Rahmen ───────────────────────────────
@@ -1993,7 +2036,7 @@
];
const ton = { ok: [TEAL_HELL, TEAL], warn: [GELB_HELL, GELB_DUNKEL], fehler: [ROT_HELL, ROT] };
let raus = chamferFlaeche(0, 0, W, H, KARTE, null, true);
let raus = karteFlaeche(0, 0, W, H);
raus += '<text x="' + P + '" y="86" font-family="Manrope, sans-serif" font-size="20" ' +
'font-weight="700" letter-spacing="0.16em" fill="' + TEXT3 + '">MIGRATION</text>';
@@ -2039,7 +2082,7 @@
'">ein halbes Jahr</text>';
return '<svg viewBox="0 0 ' + W + ' ' + H + '" width="' + W + '" height="' + H +
'" xmlns="http://www.w3.org/2000/svg">' + raus + '</svg>';
'" xmlns="http://www.w3.org/2000/svg">' + SCHATTEN_DEFS + raus + '</svg>';
}
/* ─── Feather-Icons ──────────────────────────────────────────────────────
@@ -2070,7 +2113,8 @@
"users": '<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>',
"workflow": '<rect width="8" height="8" x="3" y="3" rx="2"/><path d="M7 11v4a2 2 0 0 0 2 2h4"/><rect width="8" height="8" x="13" y="13" rx="2"/>',
"x": '<path d="M18 6 6 18"/><path d="m6 6 12 12"/>',
"zap": '<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>'
"zap": '<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>',
"rocket": '<path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z"/><path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z"/><path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0"/><path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5"/>'
};
function feather(name, cx, cy, groesse, farbe, staerke) {
@@ -2197,7 +2241,7 @@
{ f: TEAL, w: 2.4 });
}
raus += chamferFlaeche(KX, y, KB, KH, KARTE, null, true);
raus += karteFlaeche(KX, y, KB, KH);
raus += chamferFlaeche(KX + 28, y + 26, 64, 64, kachelGrund, 10);
raus += feather(p.i, KX + 60, y + 58, 34, TEAL, 2.2);
raus += '<text x="' + (KX + 118) + '" y="' + n1(my + 12) + '" ' +
@@ -2212,7 +2256,7 @@
});
return '<svg viewBox="0 0 ' + W + ' ' + H + '" width="' + W + '" height="' + H +
'" xmlns="http://www.w3.org/2000/svg">' + raus + '</svg>';
'" xmlns="http://www.w3.org/2000/svg">' + SCHATTEN_DEFS + raus + '</svg>';
}
/* ─── Slide 6: was nach der Anmeldung passiert ───────────────────────────
@@ -2237,14 +2281,14 @@
aktiv. */
function visWarteliste() {
const W = 896, H = 600;
const HB = 86, HH = 77, HX = 52, TX = 172, ABSTAND = 84;
const HB = 86, HH = 77, HX = 0, TX = 120, ABSTAND = 84;
const schritte = [
{ t: "Du trägst dich ein", ikon: "brief",
z: ["Eine E-Mail genügt. Kein Vertrag,", "keine Kreditkarte, unverbindlich."] },
{ t: "Wir melden uns", ikon: "chat",
z: ["Sobald HiveGrid für deinen Betrieb", "bereit ist, persönlich und ohne Spam."] },
{ t: "Du bist von Anfang an dabei", ikon: "konfetti",
{ t: "Du bist von Anfang an dabei", ikon: "rakete",
z: ["Unter den Ersten, die HiveGrid", "nutzen und mitgestalten."] },
];
@@ -2275,24 +2319,12 @@
[-8, 0, 8].map(o => '<circle cx="' + (cx + o) + '" cy="' + (y + h / 2) +
'" r="2.4" fill="#171106"/>').join("");
},
konfetti: (cx, cy) => {
const g = '<path d="M' + (cx - 16) + ',' + (cy + 15) + 'L' + (cx - 1) + ',' + (cy - 2) +
'L' + (cx + 9) + ',' + (cy + 11) + 'Z" fill="none" stroke="#171106" ' +
'stroke-width="2.6" stroke-linejoin="round"/>';
let f = "";
[[6, -14], [14, -6], [13, -17]].forEach(v => {
f += '<circle cx="' + (cx + v[0]) + '" cy="' + (cy + v[1]) + '" r="2.3" fill="#171106"/>';
});
[[2, -11, 9, -18], [11, -1, 19, -4]].forEach(v => {
f += '<path d="M' + (cx + v[0]) + ',' + (cy + v[1]) + 'L' + (cx + v[2]) + ',' +
(cy + v[3]) + '" stroke="#171106" stroke-width="2.4" stroke-linecap="round"/>';
});
return g + f;
},
/* Das selbst gezeichnete Konfetti war ein Kleckshaufen. Hier steht
dieselbe Aussage als Zeichen aus dem Satz der Software. */
rakete: (cx, cy) => feather("rocket", cx, cy, 34, "#171106", 2.4),
};
// Deckender Grund: sonst laeuft das Band mitten durch die Liste.
let raus = chamferFlaeche(0, 0, W, H, KARTE, 22, true);
let raus = "";
const mitte = i => oben + i * (blockHoehe + ABSTAND) + HH / 2;
// Die Schiene liegt hinter den Waben und verbindet ihre Mitten.
@@ -2314,7 +2346,7 @@
});
return '<svg viewBox="0 0 ' + W + ' ' + H + '" width="' + W + '" height="' + H +
'" xmlns="http://www.w3.org/2000/svg">' + raus + '</svg>';
'" xmlns="http://www.w3.org/2000/svg">' + SCHATTEN_DEFS + raus + '</svg>';
}
/* ─── Slide 5 · Die Loesung ──────────────────────────────────────────────
@@ -2341,8 +2373,7 @@
// KI hat kein eigenes Zeichen im Satz, deshalb der Funke von Slide 5 alt.
module[0].ikon = (x, y) => funkeForm(x, y, 20);
// Deckender Grund: sonst laeuft das Band mitten durch das Diagramm.
let raus = chamferFlaeche(0, 0, W, H, KARTE, 22, true);
let raus = "";
// Der Ring: HiveGrid liegt um den Kern, nicht an seiner Stelle.
raus += '<circle cx="' + CX + '" cy="' + CY + '" r="' + RING + '" fill="none" stroke="' +
@@ -2359,7 +2390,7 @@
'" stroke="' + LINIE + '" stroke-width="2"/>';
raus += '<circle cx="' + n1(CX + ux * RING) + '" cy="' + n1(CY + uy * RING) +
'" r="7" fill="' + GELB + '"/>';
raus += chamferFlaeche(sx - KACHEL / 2, sy - KACHEL / 2, KACHEL, KACHEL, KARTE, 12, true);
raus += karteFlaeche(sx - KACHEL / 2, sy - KACHEL / 2, KACHEL, KACHEL, 12, GRUND);
raus += feather(m.i, sx, sy, 42, GELB, 2.3);
const ty = m.a > 180 ? sy - KACHEL / 2 - 16 : sy + KACHEL / 2 + 34;
raus += '<text x="' + n1(sx) + '" y="' + n1(ty) + '" text-anchor="middle" ' +
@@ -2383,11 +2414,11 @@
und macht damit sichtbar, dass HiveGrid der Ring ist und nicht die
Mitte. */
const PB = 94, PH = 72, PX = CX - PB / 2, PY = CY - RING - PH / 2;
raus += chamferFlaeche(PX, PY, PB, PH, KARTE, 14, true);
raus += karteFlaeche(PX, PY, PB, PH, 14);
raus += gMarke(CX, CY - RING, 42, GELB);
return '<svg viewBox="0 0 ' + W + ' ' + H + '" width="' + W + '" height="' + H +
'" xmlns="http://www.w3.org/2000/svg">' + raus + '</svg>';
'" xmlns="http://www.w3.org/2000/svg">' + SCHATTEN_DEFS + raus + '</svg>';
}
/* Vierzackiger Funke fuer KI, praezise gezogen. */
@@ -2403,13 +2434,16 @@
const VIS = {
karte: j =>
'<div class="karte-halter">' + visKarte(layoutState[j]) +
'<div class="karte-chip">Vierte Generation · SHK</div>' +
'<div class="karte">' +
'<div class="karte-grund"><img alt="" src="' + assets["./assets/team/noah-bg.webp"] + '">' +
'<span class="karte-wash"></span></div>' +
'<img class="karte-person" alt="Noah Geerkens" src="' +
assets["./assets/team/noah-person.webp"] + '">' +
'<div class="karte-chip">Vierte Generation</div>' +
'<div class="karte-name"><span class="marke"></span><span class="zeilen">' +
'<div class="karte-name"><span class="marke">' +
'<svg viewBox="0 0 384.5 332.9" xmlns="http://www.w3.org/2000/svg">' +
'<polygon fill="#F1B729" points="' + G_PUNKTE + '"/></svg></span><span class="zeilen">' +
'<b>Noah Geerkens</b><small>Gründer</small></span></div>' +
'</div></div>',
@@ -2523,6 +2557,7 @@
let current = 0;
let variant = "dunkel", tilt = "an", superSample = 2, view = "streifen", tagOut = "an", frameStyle = "chamfer";
let bandTon = "orange";
/* ─── Projektstand ────────────────────────────────────────────────────────
Alles, was am Projekt hängt, wird gespeichert: Einstellungen, Layouts,
@@ -2544,11 +2579,11 @@
const STORE = "hivegrid.projekt." + PROJECT;
const STATE_URL = "./state/" + PROJECT + ".json";
const STATE_V = 6;
const STATE_V = 7;
function currentState() {
return {
v: STATE_V, variant, tilt, superSample, view, tagOut, frameStyle,
v: STATE_V, variant, tilt, superSample, view, tagOut, frameStyle, bandTon,
current, layoutState, bgState, edits,
// Welche Slides der Post ueberhaupt hat. Ohne das waere eine selbst
// angelegte Slide nach dem Neuladen weg.
@@ -2599,6 +2634,16 @@
if (alt < 5 && Array.isArray(d.edits)) {
for (const e of d.edits) { if (e) delete e.badge; }
}
/* Stand 7: Die Visualisierungen haben andere Masse bekommen -- das Foto
auf Slide 1 ist quadratisch und fuellt die Inhaltsspalte, Slide 3 ist
120 px hoeher, Slide 5 hat wieder ihre volle Breite. Die gespeicherten
Verschiebungen und Groessen stammen aus der Zeit davor und wuerden die
Elemente aus dem Blatt schieben. Das gehoert in den Ladepfad und nicht
in die Datei: der offene Tab schreibt die Datei sonst zurueck
(POST-2.md 6). */
if (alt < 7 && Array.isArray(d.edits)) {
for (const e of d.edits) { if (e) delete e.vis; }
}
d.v = STATE_V;
return true;
}
@@ -2677,6 +2722,7 @@
press("quality", "q", String(superSample));
press("tagouts", "o", tagOut);
press("frames", "f", frameStyle);
press("bands", "b", bandTon);
press("views", "w", view);
// Nach einem Schritt, der Slides hinzugefuegt, geloescht oder umsortiert
// hat, stimmt der Aufbau im Canvas nicht mehr mit dem Stand ueberein.
@@ -2808,6 +2854,7 @@
view = d.view || view;
tagOut = d.tagOut || tagOut;
frameStyle = d.frameStyle || frameStyle;
bandTon = d.bandTon || bandTon;
current = Number.isInteger(d.current) ? Math.min(d.current, SLIDES.length - 1) : 0;
}
@@ -2927,75 +2974,76 @@
das Band an den Flanken um bis zu 14 % schmaler -- das liest sich als
Verkuerzung und ist erwuenscht, waehrend echte Parallelkurven an engen
Radien Selbstschnitte bekommen. */
const BAND_RAMPE = [
[0.00, "#D9A65E"], // Oberkante, streifendes Licht
[0.07, "#C2841C"],
[0.14, "#F0C144"],
[0.20, "#FFF6E2"], // Glanzstelle
[0.27, "#F6C94E"],
[0.40, "#EBA81E"], // Koerperton, unser Orange
[0.54, "#C68310"],
[0.68, "#985E08"],
[0.78, "#6E4205"], // Kernschatten
[0.88, "#B67B12"], // Licht vom Grund zurueck
[0.96, "#96620F"],
[1.00, "#C08B34"], // Unterkante, streifendes Licht
];
const BAND_STRIEME = [
[0.11, 1.4, .16], [0.31, 1.2, -.14], [0.47, 1.8, .12],
[0.62, 1.3, -.16], [0.85, 1.5, .14],
];
/* ─── Das Band ──────────────────────────────────────────────────────────
Flach und deckend, wie die Formen bei Slack: eine kraeftige Flaeche,
dahinter eine blasse zweite als Tiefe. Kein Verlauf, keine Woelbung,
keine Glanzkante.
function mischen(x, y, t) {
const z = h => [1, 3, 5].map(i => parseInt(h.slice(i, i + 2), 16));
const v = z(x), w = z(y);
const k = n => Math.max(0, Math.min(255, Math.round(n))).toString(16).padStart(2, "0");
return "#" + k(v[0] + (w[0] - v[0]) * t) + k(v[1] + (w[1] - v[1]) * t) +
k(v[2] + (w[2] - v[2]) * t);
}
function rampe(stufen, p) {
for (let i = 1; i < stufen.length; i++)
if (p <= stufen[i][0]) {
const v = stufen[i - 1], w = stufen[i];
return mischen(v[1], w[1], w[0] === v[0] ? 0 : (p - v[0]) / (w[0] - v[0]));
}
return stufen[stufen.length - 1][1];
}
Davor stand hier ein metallisch schattierter Zylinder aus vierzig
Streifen. Der sah nach Foto aus und nach nichts, was zu einem flachen,
ruhigen Blatt passt. Wer ihn wiederhaben will, findet ihn im Commit
"Theme modern: Metallband, Icon-Satz und echte G-Marke".
Zwei Toene, umschaltbar. "teal" ist nicht --accent-teal, sondern
--dark-bg #30313D -- die Farbe des dunklen Abschnitts auf der Website. */
const BAND_TOENE = {
orange: { voll: "#F1B729", blass: "#FCE7B4" },
teal: { voll: "#30313D", blass: "#D3D5DE" },
};
/* Die Bahn steht als Stuetzpunkte da, nicht als Formel -- einer je halbe
Slide, in Streifen-Koordinaten. Eine Sinuskurve war kuerzer, liess sich
aber nicht steuern: auf Slide 5 lief sie quer durch die Kacheln. Jeder
Wert ist am gerenderten Bild geprueft, nicht gerechnet.
Slide 2 quer durch, die Kaesten liegen als Karten darueber
Slide 3 hinter der Karte, sichtbar in den Seitenraendern
Slide 4 zwischen den Kartenreihen hindurch
Slide 5 unter dem Diagramm, dessen Beschriftungen bei 1030 enden */
/* Von welcher Slide bis zu welcher das Band laeuft, jeweils als Anzahl
Slides ohne Band am Anfang und am Ende. Slide 1 und die letzte sollen
ruhig sein, Slide 2 traegt ihre eigene Leitung und wuerde sich mit dem
Band beissen. Bleibt Slide 3 bis 5. */
const BAND_VON = 2, BAND_BIS = 1;
const BAND_BAHN = [990, 900, 740, 690, 1130, 1275, 1215];
function bandSvg() {
const W = SLIDES.length * 1080, H = 1350;
const MITTE = 880, HUB = 215, WELLE = 2650, PHASE = 700, HALB = 95, N = 40;
const y = x => MITTE + HUB * Math.sin(2 * Math.PI * (x + PHASE) / WELLE);
const xs = [];
for (let x = -180; x <= W + 180; x += 80) xs.push(x);
const punkte = v => xs.map(x => x + "," + (y(x) + v).toFixed(1));
const bahn = v => "M" + punkte(v).join("L");
const flaeche = "M" + punkte(-HALB).join("L") + "L" +
punkte(HALB).reverse().join("L") + "Z";
const streifen = (p, w, f) => '<path d="' + bahn((p * 2 - 1) * HALB) +
'" fill="none" stroke="' + f + '" stroke-width="' + w.toFixed(2) +
'" stroke-linecap="butt" stroke-linejoin="round"/>';
/* Slide 1 und die letzte tragen kein Band: Anfang und Schluss sollen
ruhig sein. Es laeuft von der linken Kante der zweiten bis zur
rechten Kante der vorletzten Slide -- zwei Pixel innerhalb der Naht,
weil genau auf der Kante ein Antialiasing-Splitter auf der
Nachbarslide stehen bleibt. */
const H = 1350;
const X0 = BAND_VON * 1080 + 2;
const X1 = Math.max(X0, (SLIDES.length - BAND_BIS) * 1080 - 2);
const W = SLIDES.length * 1080;
const P = BAND_BAHN, SCHRITT = (X1 - X0) / (P.length - 1);
const ton = BAND_TOENE[bandTon] || BAND_TOENE.orange;
let raus = "";
// Die Woelbung: schmale Streifen, leicht ueberlappend, damit zwischen
// ihnen keine Naht stehen bleibt.
const dicke = (2 * HALB / N) * 1.3;
for (let i = 0; i < N; i++) {
const p = (i + 0.5) / N;
raus += streifen(p, dicke, rampe(BAND_RAMPE, p));
}
// Die harte Glanzkante -- der einzige Strich mit scharfem Rand.
raus += streifen(0.20, 2.6, "#FFFDF4");
// Schliffspuren laengs.
BAND_STRIEME.forEach(v => {
raus += streifen(v[0], v[1],
mischen(rampe(BAND_RAMPE, v[0]), v[2] > 0 ? "#FFFFFF" : "#000000", Math.abs(v[2])));
});
// Die Silhouette. Ohne sie franst das Band auf hellem Grund aus.
raus += '<path d="' + flaeche + '" fill="none" stroke="#8A5A10" stroke-width="1.4"/>';
// Catmull-Rom: laeuft durch die Stuetzpunkte, nicht daran vorbei.
const y = x => {
const u = Math.max(0, Math.min(P.length - 1.0001, (x - X0) / SCHRITT));
const i = Math.floor(u), t = u - i;
const g = k => P[Math.max(0, Math.min(P.length - 1, k))];
const a0 = g(i - 1), a1 = g(i), a2 = g(i + 1), a3 = g(i + 2);
return 0.5 * (2 * a1 + (a2 - a0) * t +
(2 * a0 - 5 * a1 + 4 * a2 - a3) * t * t +
(-a0 + 3 * a1 - 3 * a2 + a3) * t * t * t);
};
const xs = [];
for (let x = X0; x < X1; x += 45) xs.push(x);
xs.push(X1);
/* Gefuellte Flaeche statt dicker Linie: eine Linie bekommt an ihrem Ende
eine Kappe rechtwinklig zur Laufrichtung, und die steht bei schraeger
Bahn seitlich ueber die Naht hinaus -- auf der Nachbarslide blieb ein
Splitter stehen. Eine geschlossene Kontur endet exakt senkrecht. */
const kante = v => xs.map(x => x + "," + (y(x) + v).toFixed(1));
const flaeche = (v, h, f) => '<path d="M' + kante(v - h).join("L") + "L" +
kante(v + h).reverse().join("L") + '" fill="' + f + '"/>';
return '<svg class="layer-band" width="' + W + '" height="' + H + '" viewBox="0 0 ' +
W + ' ' + H + '" xmlns="http://www.w3.org/2000/svg">' + raus + '</svg>';
W + ' ' + H + '" xmlns="http://www.w3.org/2000/svg">' +
flaeche(58, 107, ton.blass) + flaeche(0, 88, ton.voll) + '</svg>';
}
// Positionen im 5400er Raum. Die Nähte liegen auf 1080 / 2160 / 3240 / 4320.
@@ -4931,6 +4979,15 @@
const b = e.target.closest("button");
if (b) { frameStyle = b.dataset.f; press("frames", "f", frameStyle); applyAll(); saveState(); }
});
/* Das Band haengt in .bg und wird von buildSlide gesetzt, nicht von
applyAll. Beim Umschalten wird es deshalb hier direkt getauscht. */
document.getElementById("bands").addEventListener("click", e => {
const b = e.target.closest("button");
if (!b) return;
bandTon = b.dataset.b; press("bands", "b", bandTon);
document.querySelectorAll(".bg .layer-band").forEach(el => el.outerHTML = bandSvg());
saveState();
});
document.getElementById("tilts").addEventListener("click", e => {
const b = e.target.closest("button");
if (b) { tilt = b.dataset.t; press("tilts", "t", tilt); applyAll(); saveState(); }
+58 -39
View File
@@ -59,10 +59,11 @@ nur — die Unterschiede stecken in den CSS-Werten und in den Zeichen-Primitiven
| Zweitfarbe | keine | Teal `#0D6E6E` |
| Zeichen | selbst gezeichnet | Feather aus `@subframe/core` |
**Karten stehen weiß auf weiß und tragen über ihre Kante.** `#D5DBE1`, nicht
`#E5E5E5` — die hellere Linie verschwindet auf Weiß. Den Grund zu tönen wäre
der einfachere Weg und ist verboten, siehe oben. Auf der Website reicht
`#E5E5E5`, weil dort ein Schatten mitträgt; hier nicht (§7).
**Karten stehen weiß auf weiß und tragen über Kante und Schatten.** Kante
`#D5DBE1`, nicht `#E5E5E5` — die hellere Linie verschwindet auf Weiß. Den
Grund zu tönen wäre der einfachere Weg und ist verboten, siehe oben. Der
Schatten ist die Ausnahme von §5, die dieses Theme sich nimmt; dort steht,
warum und mit welchen Werten.
**Das Band ist ein einziges Bild über den ganzen Streifen.** Jede Slide zeigt
ihren Ausschnitt, verschoben über `--slide-i`. An der Naht läuft es deshalb
@@ -72,43 +73,52 @@ nicht hat, weil jede Slide für sich steht. Drei Punkte, die daran hängen:
- **Die Wellenlänge darf kein Vielfaches von 1080 sein.** Sonst liegt an jeder
Naht derselbe Punkt der Welle, meist ein Scheitel, und ein waagerechtes Band
an der Kante sagt dem Auge nichts.
- **Der Querschnitt ist ein Rohr, kein Farbband.** Vierzig schmale Streifen,
versetzt entlang derselben Bahn, eingefärbt aus der Rampe `BAND_RAMPE`. Was
Metall von Bonbon unterscheidet, steht in diesen vier Punkten:
- Die **Glanzstelle liegt nicht an der Oberkante**, sondern eingerückt bei
20 %, und sie ist schmal und fast weiß. Ein breites weiches Hellgelb
obenauf ist genau der Bonbon-Eindruck.
- Der **dunkelste Punkt liegt nicht an der Unterkante**, sondern bei 78 %.
Darunter kommt Licht vom weißen Grund zurück.
- Der **Tonumfang ist groß**: `#6E4205` bis fast Weiß. Metall hat Kontrast,
lackierter Kunststoff nicht.
- Beide **Silhouetten sind aufgehellt** (Fresnel), dazu ein paar
Längsstreifen als Schliff.
- **Kein SVG-Verlauf.** Nicht weil Verläufe verboten wären — verboten sind sie
nur beim Marker (§5) —, sondern weil ein `linearGradient` immer geradlinig
durch die Bounding-Box läuft. Das Band wellt sich, der Verlauf würde also
quer statt längs schattieren. Versetzte Streifen folgen der Kurve von selbst.
- **Der Versatz ist senkrecht, nicht rechtwinklig zur Bahn.** Dadurch wird das
Band an den Flanken um bis zu 14 % schmaler. Das liest sich als Verkürzung
und ist erwünscht; echte Parallelkurven bekommen an engen Radien
Selbstschnitte.
- **Volle Deckkraft.** Jede Abschwächung nimmt dem Band genau das wieder weg,
was es zum Körper macht.
- **Flach und deckend, wie die Formen bei Slack.** Eine kräftige Fläche,
dahinter eine blasse zweite als Tiefe. Kein Verlauf, keine Wölbung, keine
Glanzkante. Ein Zwischenstand hatte hier einen metallisch schattierten
Zylinder aus vierzig Streifen — fotorealistisch und damit ein Fremdkörper
auf einem flachen, ruhigen Blatt. Er steht im Commit *„Theme modern:
Metallband, Icon-Satz und echte G-Marke"*, falls ihn jemand wiederhaben will.
- **Zwei Töne, umschaltbar** unter *Band* im Bedienfeld (`BAND_TOENE`):
**Das Band läuft quer durch die Komposition, nicht am Rand entlang.** Es liegt
in `.bg` und damit unter allem. Damit das trägt, gilt für das Theme `modern`:
| Ton | Fläche | Blasse zweite |
|---|---|---|
| `orange` | `--accent-yellow` `#F1B729` | `#FCE7B4` |
| `teal` | `--dark-bg` `#30313D` | `#D3D5DE` |
> **Jede Visualisierung liegt auf einer deckenden Karte.** Linienwerk auf
> Transparenz verschwindet im Band. Alle sechs Slides von Post 2 sind deshalb
> auf `chamferFlaeche(0, 0, W, H, KARTE, 22, true)` gesetzt — das Band taucht
> dahinter ab und kommt daneben wieder hoch, wie der Schlauch in der Vorlage
> von awork. Eine weiße Ummantelung um einzelne Linien war der erste Versuch
> und sah nach Panne aus.
> **„HiveGrid Teal" ist `--dark-bg` `#30313D`, nicht `--accent-teal`.** Im
> Haus heißt der dunkle Ton des Story-Abschnitts auf der Website so. Er ist
> ein blaustichiges Anthrazit; `--accent-teal` `#0D6E6E` ist ein anderes
> Grün und trägt im Theme `modern` die Zeichenkacheln. Beim Reden über
> Farben lohnt die Nachfrage, welches gemeint ist.
- **Der Umschalter greift an `.bg`, nicht über `applyAll`.** Das Band setzt
`buildSlide`, nicht der Neuaufbau der Seiten; der Handler tauscht deshalb
`.bg .layer-band` direkt aus.
- **Die Bahn steht als Stützpunkte da, nicht als Formel** (`BAND_BAHN`, einer
je halbe Slide). Eine Sinuskurve war kürzer, ließ sich aber nicht steuern:
auf Slide 5 lief sie quer durch die Kacheln, und dort verschwinden die
orangenen Zeichen. Jeder Wert ist am gerenderten Bild geprüft, nicht
gerechnet. Wer `BAND_VON` oder `BAND_BIS` ändert, muss die Liste mitziehen:
die Punkte verteilen sich gleichmäßig über die neue Strecke, und alle
Werte verschieben sich.
- **Wo das Band läuft, steht in `BAND_VON` / `BAND_BIS`** — Anzahl Slides ohne
Band am Anfang und am Ende. Bei Post 2 sind das 2 und 1, das Band trägt also
Slide 3 bis 5. Slide 1 und die letzte sollen ruhig sein, Slide 2 trägt ihre
eigene orangene Leitung und würde sich damit beißen.
- **Als gefüllte Fläche gezeichnet, nicht als dicke Linie.** Eine Linie bekommt
am Ende eine Kappe rechtwinklig zur Laufrichtung; bei schräger Bahn steht die
seitlich über die Naht hinaus, und auf der Nachbarslide bleibt ein Splitter
stehen. Eine geschlossene Kontur endet exakt senkrecht — dann genügen zwei
Pixel Abstand zur Naht.
Daraus folgt: **`visW` darf den Satzspiegel nicht überschreiten.** Solange eine
Visualisierung Linienwerk war, durfte sie breiter sein als 896; als Karte muss
sie auf die 92er Kante. `.vis svg` skaliert über `--vis-w` mit, die Innenmaße
bleiben also unangetastet — bei Slide 5 genügte `visW: 988 → 896`.
**Was über dem Band liegt, ist eine Karte mit Schatten.** Nicht randlos: der
Versuch mit reinen Konturen sah gestochen aus. Kleine Karten über einem Band
sind die Vorlage; eine randlose Karte über die ganze Fläche war der Fehler
davor, weil das Band dahinter komplett verschwand.
> **Eine Ausnahme bleibt: Slide 3.** Deren Karte *ist* die Visualisierung, ein
> Oberflächen-Panel mit Fortschrittsbalken und Statuszeilen. Die kann nicht
> randlos sein, also zeigt sich das Band dort nur in den Seitenrändern.
**Die Zeichen kommen aus dem Icon-Satz der Software, nicht aus der Hand.**
`@subframe/core`, dieselben Feather-Icons, mit denen die Oberfläche gebaut ist
@@ -140,7 +150,7 @@ derselben Datei.
| `--accent-yellow` | `#F1B729` | Primärer Akzent, Marker, G-Marke |
| `--accent-yellow-light` | `#FFE082` | Badge-Hintergrund |
| `--accent-yellow-dark` | `#3E2C0A` | Badge-Text |
| `--accent-teal` | `#0D6E6E` | Sekundär, aktuell ungenutzt |
| `--accent-teal` | `#0D6E6E` | Sekundär; im Theme `modern` die Zeichenkacheln |
| `--dark-bg` | `#30313D` | Dunkle Variante + Formen-Ebene |
### Flächen & Text
@@ -418,6 +428,15 @@ aussehen lässt. Flächig gesetzt wirkt es ruhiger und passt zur Website, die
denselben Weg geht. Nebenbei ist es beim Export billiger — jeder unscharfe
Filter muss beim 3-fachen Rendern dreifach berechnet werden.
> **Für das Theme `modern` ist diese Regel aufgehoben.** Dort schweben Karten
> über einem durchlaufenden Band, und ohne Schatten stehen sie gestochen auf
> dem Grund statt darüber. Der Schatten ist ein SVG-`feDropShadow`
> (`dy 7`, `stdDeviation 10`, `#101828` bei 14 %), kein `box-shadow`: die
> Karten sind gezeichnet und nicht im DOM, und ein CSS-Schatten würde vom
> `clip-path` ohnehin mit beschnitten. Der Kostenpunkt aus dem Absatz oben
> bleibt — **nach jeder Änderung daran einmal 3-fach exportieren und
> hinsehen.** Im Theme `notebook` gilt die Regel unverändert.
### Was global gilt und was pro Slide
| Einstellung | Geltung | Warum |
+49 -47
View File
@@ -1,4 +1,5 @@
{
"bandTon": "teal",
"bgState": [
{
"bg": "raster",
@@ -25,7 +26,7 @@
"int": "dezent"
}
],
"current": 1,
"current": 2,
"deck": [
{
"ref": "titel",
@@ -68,9 +69,8 @@
"size": 88
},
"vis": {
"dx": -38,
"dy": -79,
"size": 692
"dx": 0,
"dy": -64
}
},
{
@@ -82,8 +82,8 @@
"dy": 3
},
"vis": {
"dx": 3,
"dy": 13
"dx": 31,
"dy": -107
}
},
{
@@ -95,47 +95,49 @@
"dy": 3
},
"vis": {
"dx": 0,
"dy": 0
}
},
{
"body": {
"dx": 0,
"dy": 20
},
"headline": {
"dx": 0,
"dy": 3
},
"vis": {
"dx": -46,
"dy": 0
}
},
{
"body": {
"dy": 20
},
"headline": {
"dy": 3
},
"vis": {
"dx": 0,
"dy": 0
}
},
{
"body": {
"dy": 20
},
"headline": {
"dy": 3
},
"vis": {
"dx": 3,
"dx": -3,
"dy": -88
}
},
{
"body": {
"dx": 0,
"dy": 20
},
"headline": {
"dx": 0,
"dy": 3
},
"vis": {
"dx": -95,
"dy": -12,
"size": 1088
}
},
{
"body": {
"dy": 20
},
"headline": {
"dy": 3
},
"vis": {
"dx": 33,
"dy": -118
}
},
{
"body": {
"dy": 20
},
"headline": {
"dy": 3
},
"vis": {
"dx": 12,
"dy": 74,
"size": 1184
}
}
],
"frameStyle": "aus",
@@ -147,10 +149,10 @@
"standard",
"standard"
],
"superSample": 2,
"superSample": 3,
"tagOut": "an",
"tilt": "an",
"v": 6,
"v": 7,
"variant": "hell",
"view": "einzeln"
}