POST 3 Slide 2 zeigt den Aufbau: HiveGrid in der Mitte, die eigenen Module auf dem gelben Ring, die fremden Programme aussen am gestrichelten Kreis. Nachbau des Schaubilds aus dem Prototyp, aber in den Markenfarben -- Magenta ist keine (rules.md 2) -- und ohne Produktnamen im Kern. Achtung: das ist eine andere Aussage als in post2-alt.html, wo in der Mitte das Branchenprogramm liegt und HiveGrid ausdruecklich der Ring ist. Steht als Warnung im Code. Die Ueberschrift "Der Kern bleibt." passt seit dem Wechsel der Mitte nicht mehr zum Bild und ist noch offen. Slide 1 traegt nur noch die Ueberschrift; ein leerer Fliesstext erzeugt jetzt keinen leeren Absatz mehr. Das Wabenfeld ist enger geschnitten (Faktor 1,301 statt 1,227) und steht am Anschlag: 96 px zur Slide-Kante, die Sicherheitszone liegt bei 92. Ueberschriften in 76 px und 3 px versetzt wie in post2-alt -- gemessen, nicht geschaetzt. Zwei Fehler, die dabei aufgefallen sind: - Die Andock-Kacheln lagen auf denselben Strahlen wie die Module und beruehrten sich. Dieselbe Ueberlappung hat der Exportpruefung eine Spanne von 37 vorgetaeuscht, wo eine fehlende Marke 0 ergeben muss -- der Gegentest kam durch, obwohl die Marke fehlte. 22,5 Grad Versatz behebt beides. - Die Pruefung kannte nur den Ausschnitt des Wabenfelds und haette auf Slide 2 an der falschen Stelle gemessen. Der Ausschnitt wandert jetzt mit in die Ablage, wabenZonen heisst darum markenZonen. VIDEO Neuer Ordner video/ mit 13 Einblendungen fuer den Schnitt, jede als Standbild und als Bildsequenz mit Ein- und Ausblendung, alle mit Alphakanal. Gerendert aus komponenten.html von bauen.mjs ueber CDP; kein ffmpeg noetig, weil eine Bildsequenz dieselbe Transparenz traegt wie ProRes 4444. Der Schatten fehlte zunaechst ganz: die Karte trug clip-path fuer die Ecken und box-shadow fuer den Schatten, und clip-path beschneidet ihn mit. Genau so steht es in rules.md 3. Sitzt jetzt als drop-shadow auf einer Huelle. NEBENBEI - Das ZIP hiess in allen Projektdateien "hivegrid-post1-loesung", weil sie voneinander kopiert wurden. Kommt jetzt aus PROJECT. - Das rechte Element-Feld liess sich nicht schliessen, wenn es nicht ganz ins Fenster passte: sein Knopf rutschte als erster hinaus. Knopf nach vorn, Escape hebt die Auswahl auf, max-width auf den Feldern. - uebersicht.html schrieb "1 Slides". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1006 lines
51 KiB
HTML
1006 lines
51 KiB
HTML
<meta charset="utf-8">
|
||
<title>HiveGrid · Video-Komponenten</title>
|
||
|
||
<!-- ═══════════════════════════════════════════════════════════════════════════
|
||
Einblendungen fuer den Videoschnitt.
|
||
|
||
Anders als die Post-Seiten ist das hier kein Editor, sondern eine Vorlage:
|
||
die Seite zeigt genau eine Komponente auf durchsichtigem Grund, in ihrer
|
||
eigenen Groesse. bauen.mjs faehrt sie der Reihe nach ab und legt daraus
|
||
PNGs mit Alphakanal und Bildsequenzen an.
|
||
|
||
Warum durchsichtig und nicht auf einer Leinwand: eine Einblendung wird im
|
||
Schnitt ueber das Bild gelegt und dort frei geschoben und skaliert. Eine
|
||
feste Leinwand von 1920 x 1080 waere zu 90 Prozent leer, und der Schnitt
|
||
muesste sie erst wieder wegrechnen.
|
||
|
||
Aufruf:
|
||
?k=bauchbinde-noah die Komponente im gehaltenen Zustand
|
||
?k=bauchbinde-noah&t=0.4 im Verlauf ihrer Einblendung
|
||
?k=bauchbinde-noah&text=... eigener Text, siehe felder je Komponente
|
||
?alle=1 Kontaktbogen mit allen, zum Durchsehen
|
||
|
||
Die Bildsprache ist die von post2-alt.html, Theme modern: weisse Karten
|
||
mit Haarlinie und weichem Schatten, ein gelber Akzent, Manrope. Die
|
||
Zeichen kommen aus demselben Feather-Satz wie die Oberflaeche. Wer hier
|
||
etwas aendert, sollte dort nachsehen -- rules.md 1a beschreibt, was das
|
||
Theme ausmacht.
|
||
══════════════════════════════════════════════════════════════════════ -->
|
||
|
||
<link rel="stylesheet" href="../assets/fonts/manrope.css">
|
||
<style>
|
||
:root {
|
||
--gelb: #F1B729;
|
||
--gelb-hell: #FFE082;
|
||
--gelb-dunkel: #3E2C0A;
|
||
--teal: #0D6E6E;
|
||
--teal-hell: #E8F4F4;
|
||
--tinte: #1A1A1A;
|
||
--text2: #555555;
|
||
--kante: #D5DBE1;
|
||
--karte: #FFFFFF;
|
||
--grund: #F6F8FA;
|
||
}
|
||
|
||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||
|
||
/* Durchsichtig bis in die Wurzel. Chrome bekommt beim Rendern zusaetzlich
|
||
--default-background-color=00000000 mit; ohne das hier waere trotzdem
|
||
Weiss darunter. */
|
||
html, body { background: transparent; }
|
||
|
||
body {
|
||
font-family: 'Manrope', system-ui, sans-serif;
|
||
-webkit-font-smoothing: antialiased;
|
||
}
|
||
|
||
/* Die Buehne hat exakt die Groesse der Komponente. Kein Rand, keine Mitte:
|
||
was hier steht, ist spaeter das PNG. */
|
||
#buehne { position: relative; overflow: visible; }
|
||
|
||
/* ─── Karte ──────────────────────────────────────────────────────────────
|
||
Die Grundform des Themes: weiss, Haarlinie, weicher Schatten. Ueber
|
||
Videomaterial traegt der Schatten mehr als in einem Post, weil darunter
|
||
kein ruhiges Blatt liegt, sondern eine Baustelle. Deshalb eine Spur
|
||
kraeftiger als in rules.md 5 notiert. */
|
||
.karte {
|
||
background: var(--karte);
|
||
border: 1px solid var(--kante);
|
||
clip-path: polygon(
|
||
14px 0, calc(100% - 14px) 0, 100% 14px, 100% calc(100% - 14px),
|
||
calc(100% - 14px) 100%, 14px 100%, 0 calc(100% - 14px), 0 14px
|
||
);
|
||
}
|
||
/* Der Schatten sitzt auf der Huelle, nicht auf der Karte. Der Grund steht
|
||
in rules.md 3: clip-path beschneidet den Schatten mit, und die Karte
|
||
traegt den Chamfer als clip-path. Ein box-shadow auf ihr war deshalb
|
||
schlicht nicht da -- am gerenderten PNG nachgemessen, die Deckung brach
|
||
an der Kartenkante von 255 auf 0 ab, ohne einen einzigen Zwischenwert.
|
||
|
||
drop-shadow statt box-shadow folgt der Alphamaske und legt sich damit um
|
||
die abgeschnittenen Ecken herum, statt um ein Rechteck. Etwas kraeftiger
|
||
als der Wert des Themes (rules.md 5: dy 7, 10 Unschaerfe, 14 Prozent):
|
||
unter einer Slide liegt ein ruhiges weisses Blatt, unter einer
|
||
Einblendung eine Baustelle. */
|
||
.schatten { filter: drop-shadow(0 9px 15px rgba(16, 24, 40, .22)); }
|
||
|
||
/* ─── Bauchbinde ─────────────────────────────────────────────────────────
|
||
Nachbau des Namensschilds von der Teamkarte aus post2-alt.html. Dort
|
||
sitzt es in der Ecke eines Fotos, hier steht es frei. Groesser gesetzt:
|
||
im Post wird die Slide auf gut 400 px im Feed geschrumpft, im Video
|
||
laeuft sie auf einem Fernseher. */
|
||
.bb {
|
||
display: flex; align-items: center; gap: 26px;
|
||
padding: 30px 44px 30px 32px;
|
||
background: var(--karte);
|
||
}
|
||
.bb .marke { width: 76px; height: 66px; flex: none; }
|
||
.bb .marke svg { display: block; width: 100%; height: 100%; }
|
||
.bb .zeilen { display: flex; flex-direction: column; gap: 6px; }
|
||
.bb b {
|
||
font-size: 46px; font-weight: 800; letter-spacing: -.028em;
|
||
line-height: 1.05; color: var(--tinte); white-space: nowrap;
|
||
}
|
||
.bb small {
|
||
font-size: 21px; font-weight: 700; letter-spacing: .16em;
|
||
text-transform: uppercase; color: var(--text2); white-space: nowrap;
|
||
}
|
||
/* Der gelbe Balken an der Kante. Er ist das, was die Bauchbinde auf einem
|
||
unruhigen Bild als Marke lesbar macht, nicht die Schrift. */
|
||
.bb-balken {
|
||
position: absolute; left: 0; top: 0; bottom: 0; width: 12px;
|
||
background: var(--gelb);
|
||
}
|
||
|
||
/* ─── Chip ───────────────────────────────────────────────────────────────
|
||
Das gedrehte Schild der Landingpage. Monospace und weit gesperrt, damit
|
||
es neben der Manrope als Stempel liest und nicht als zweite Ueberschrift. */
|
||
.chip {
|
||
display: inline-block; padding: 22px 34px;
|
||
background: var(--gelb); color: var(--gelb-dunkel);
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||
font-size: 26px; font-weight: 850; letter-spacing: .12em;
|
||
text-transform: uppercase; white-space: nowrap;
|
||
transform: rotate(-8deg);
|
||
clip-path: polygon(
|
||
17px 0, calc(100% - 17px) 0, 100% 17px, 100% calc(100% - 17px),
|
||
calc(100% - 17px) 100%, 17px 100%, 0 calc(100% - 17px), 0 17px
|
||
);
|
||
}
|
||
|
||
/* ─── Satzkarte ──────────────────────────────────────────────────────────
|
||
Eine Aussage, sonst nichts. Noah spricht den Satz, die Karte setzt das
|
||
eine Wort, an dem er haengt -- deshalb hoechstens ein Akzent, wie in
|
||
rules.md 4. */
|
||
.satz { padding: 40px 52px; }
|
||
.satz p {
|
||
font-size: 52px; font-weight: 800; letter-spacing: -.03em;
|
||
line-height: 1.12; color: var(--tinte); white-space: nowrap;
|
||
}
|
||
.satz p .a { color: var(--gelb); position: relative; }
|
||
/* Der Balken unter dem Akzentwort, wie im Theme modern. Masse in em, damit
|
||
er jeder Schriftgroesse folgt. */
|
||
.satz p .a::after {
|
||
content: ""; position: absolute; left: 0; right: 0; bottom: -.1em;
|
||
height: .055em; background: currentColor;
|
||
}
|
||
.satz .sub {
|
||
margin-top: 14px;
|
||
font-size: 26px; font-weight: 600; letter-spacing: -.005em;
|
||
line-height: 1.35; color: var(--text2); white-space: nowrap;
|
||
}
|
||
|
||
/* ─── Zeilen im Stapel ───────────────────────────────────────────────────
|
||
Drei Kaesten untereinander, jeder mit Zeichen. Fuer Aufzaehlungen, die
|
||
Noah spricht -- Stammdaten, Auftraege, Buchhaltung. */
|
||
.zeile {
|
||
display: flex; align-items: center; gap: 22px;
|
||
padding: 22px 34px; margin-bottom: 16px;
|
||
background: var(--karte);
|
||
}
|
||
.zeile:last-child { margin-bottom: 0; }
|
||
.zeile .ikon { width: 46px; height: 46px; flex: none; }
|
||
.zeile span {
|
||
font-size: 32px; font-weight: 700; letter-spacing: -.018em;
|
||
color: var(--tinte); white-space: nowrap;
|
||
}
|
||
/* Teal traegt die Zeichen, Gelb bleibt dem einen Akzent vorbehalten. */
|
||
.zeile .ikon svg { display: block; width: 100%; height: 100%; }
|
||
|
||
/* Die gleiche Zeile in gelb, fuer das, was HiveGrid dazustellt. */
|
||
.zeile.an .ikon svg { color: var(--gelb); }
|
||
|
||
/* ─── Logo-Zeile ─────────────────────────────────────────────────────────
|
||
Die Wortmarke fuer die Ecke. Auf einer weissen Kachel, weil sie sonst
|
||
auf hellem Videomaterial verschwindet. */
|
||
.bug { display: flex; align-items: center; gap: 20px; padding: 20px 30px; }
|
||
.bug img { display: block; height: 44px; width: auto; }
|
||
.bug .strich { width: 2px; align-self: stretch; background: var(--kante); }
|
||
.bug span {
|
||
font-size: 19px; font-weight: 700; letter-spacing: .15em;
|
||
text-transform: uppercase; color: var(--text2); white-space: nowrap;
|
||
}
|
||
|
||
/* ─── Endkarte ───────────────────────────────────────────────────────────
|
||
Der Abbinder. Eine Zeile Marke, eine Zeile Aufforderung, sonst nichts. */
|
||
.ende { padding: 52px 60px; text-align: center; }
|
||
.ende h1 {
|
||
font-size: 62px; font-weight: 800; letter-spacing: -.032em;
|
||
line-height: 1.06; color: var(--tinte); white-space: nowrap;
|
||
}
|
||
.ende h1 .a { color: var(--gelb); }
|
||
.ende p {
|
||
margin-top: 20px;
|
||
font-size: 27px; font-weight: 600; color: var(--text2); white-space: nowrap;
|
||
}
|
||
|
||
/* ─── Zeichenflaeche ─────────────────────────────────────────────────────
|
||
Die uebernommenen Zeichnungen fuellen ihre Buehne selbst aus. Kein
|
||
Layout-CSS hier -- in post2-alt.html hat genau das dreimal zugeschlagen
|
||
und ein fertiges SVG in eine Rasterzelle gequetscht. */
|
||
.zeichnung svg { display: block; width: 100%; height: auto; overflow: visible; }
|
||
|
||
/* ─── Kontaktbogen ───────────────────────────────────────────────────────
|
||
Nur zum Durchsehen, kommt nie in ein PNG. Karo statt Weiss, sonst sieht
|
||
man bei weissen Karten nicht, wo sie aufhoeren. */
|
||
body.bogen {
|
||
background:
|
||
repeating-conic-gradient(#e9edf2 0 25%, #f7f9fb 0 50%) 0 0 / 40px 40px;
|
||
padding: 40px;
|
||
}
|
||
body.bogen #buehne { display: flex; flex-wrap: wrap; gap: 40px; align-items: flex-start; }
|
||
.bogen-zelle { position: relative; }
|
||
.bogen-zelle .bez {
|
||
position: absolute; left: 0; top: -26px;
|
||
font-size: 15px; font-weight: 700; letter-spacing: .08em;
|
||
text-transform: uppercase; color: #7a8699; white-space: nowrap;
|
||
}
|
||
</style>
|
||
|
||
<div id="buehne"></div>
|
||
|
||
<script>
|
||
"use strict";
|
||
/* Ein Fehler auf oberster Ebene wuerde die Seite still leer lassen, und
|
||
bauen.mjs bekaeme lauter durchsichtige PNGs ohne Hinweis worauf. Also
|
||
sichtbar machen. */
|
||
function melden(was) {
|
||
document.title = "FEHLER: " + was;
|
||
const d = document.createElement("pre");
|
||
d.style.cssText = "position:fixed;left:0;top:0;background:#B3261E;color:#fff;" +
|
||
"padding:12px;font:14px monospace;z-index:99;white-space:pre-wrap";
|
||
d.textContent = "Fehler: " + was;
|
||
document.body.appendChild(d);
|
||
}
|
||
window.addEventListener("error", e => melden(e.message || e.error));
|
||
/* Der Aufbau laeuft in einer async-Funktion. Was dort fliegt, ist eine
|
||
abgelehnte Promise und kein error-Ereignis -- ohne diesen zweiten Horcher
|
||
bricht die Seite still nach der letzten gelungenen Komponente ab, und man
|
||
sucht den Grund im Rendern statt im Bauen. */
|
||
window.addEventListener("unhandledrejection", e =>
|
||
melden((e.reason && (e.reason.stack || e.reason.message)) || e.reason));
|
||
|
||
const n1 = v => Math.round(v * 10) / 10;
|
||
|
||
/* ─── Farben ───────────────────────────────────────────────────────────────
|
||
Dieselben Werte wie in rules.md 2. Als Konstanten und nicht als
|
||
CSS-Variablen, weil die Zeichnungen sie in SVG-Attribute schreiben und
|
||
dort keine var() steht. */
|
||
const GELB = "#F1B729", TINTE = "#1A1A1A", TEXT2 = "#555555",
|
||
TEAL = "#0D6E6E", LINIE = "#E5E5E5", KANTE = "#D5DBE1",
|
||
KARTE = "#FFFFFF", GRUND = "#F6F8FA";
|
||
/* Die Zeichnungen aus post2-alt.html fragen die Variante ab. Hier gibt es nur
|
||
die helle: eine Einblendung liegt auf fremdem Bildmaterial, und was darauf
|
||
traegt, ist eine weisse Karte, keine dunkle. */
|
||
const variant = "hell";
|
||
|
||
/* ─── Uebernommene Bausteine ───────────────────────────────────────────────
|
||
Woertlich aus post2-alt.html und post3.html. Nicht nachgebaut und nicht
|
||
verbessert: eine Einblendung im Video und eine Slide im Feed muessen
|
||
dieselbe Marke zeigen, und zwei Fassungen derselben Zeichnung laufen
|
||
frueher oder spaeter auseinander. Wer an einer Zeichnung etwas aendert,
|
||
muss an alle drei Dateien denken -- das ist der Preis dafuer, dass jede
|
||
Datei fuer sich lauffaehig bleibt (rules.md 1a). */
|
||
|
||
const CHAMFER_C = 16;
|
||
// Der Grund, den die Aussparung der G-Marke zeigt. Im Video liegt die
|
||
// Zeichnung auf einer weissen Karte, nicht auf fremdem Bildmaterial.
|
||
const SLIDE_GRUND = "#FFFFFF";
|
||
|
||
|
||
function chamfer(x, y, w, h, c) {
|
||
return "M" + n1(x + c) + "," + n1(y) +
|
||
"H" + n1(x + w - c) + "L" + n1(x + w) + "," + n1(y + c) +
|
||
"V" + n1(y + h - c) + "L" + n1(x + w - c) + "," + n1(y + h) +
|
||
"H" + n1(x + c) + "L" + n1(x) + "," + n1(y + h - c) +
|
||
"V" + n1(y + c) + "Z";
|
||
}
|
||
|
||
function glatt(pts, zu) {
|
||
const n = pts.length;
|
||
const at = i => pts[zu ? (i + n) % n : Math.max(0, Math.min(n - 1, i))];
|
||
let d = "M" + n1(pts[0][0]) + "," + n1(pts[0][1]);
|
||
const bis = zu ? n : n - 1;
|
||
for (let i = 0; i < bis; i++) {
|
||
const p0 = at(i - 1), p1 = at(i), p2 = at(i + 1), p3 = at(i + 2);
|
||
d += "C" + n1(p1[0] + (p2[0] - p0[0]) / 6) + "," + n1(p1[1] + (p2[1] - p0[1]) / 6) +
|
||
" " + n1(p2[0] - (p3[0] - p1[0]) / 6) + "," + n1(p2[1] - (p3[1] - p1[1]) / 6) +
|
||
" " + n1(p2[0]) + "," + n1(p2[1]);
|
||
}
|
||
return d;
|
||
}
|
||
|
||
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>';
|
||
|
||
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>';
|
||
|
||
const G_PUNKTE = "384.5,166.5 336.4,83.2 192.2,166.5 288.3,166.5 240.3,249.7 " +
|
||
"144.2,249.7 96.1,166.5 144.2,83.2 336.4,83.2 298,16.8 288.3,0 96.1,0 " +
|
||
"0,166.5 96.1,332.9 288.3,332.9";
|
||
function gMarke(cx, cy, hoehe, farbe) {
|
||
const s = hoehe / 332.9;
|
||
return '<g transform="translate(' + n1(cx - 384.5 * s / 2) + ',' + n1(cy - hoehe / 2) +
|
||
') scale(' + s.toFixed(5) + ')"><polygon points="' + G_PUNKTE + '" fill="' +
|
||
farbe + '"/></g>';
|
||
}
|
||
|
||
const FEATHER = {
|
||
"alert-triangle": '<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"/><path d="M12 9v4"/><path d="M12 17h.01"/>',
|
||
"check": '<polyline points="20 6 9 17 4 12"/>',
|
||
"clipboard-list": '<rect width="8" height="4" x="8" y="2" rx="1" ry="1"/><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/><path d="M12 11h4"/><path d="M12 16h4"/><path d="M8 11h.01"/><path d="M8 16h.01"/>',
|
||
"columns-3": '<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M9 3v18"/><path d="M15 3v18"/>',
|
||
"file-text": '<path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/><line x1="16" x2="8" y1="13" y2="13"/><line x1="16" x2="8" y1="17" y2="17"/><line x1="10" x2="8" y1="9" y2="9"/>',
|
||
"filter": '<polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/>',
|
||
"folder": '<path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/>',
|
||
"image": '<rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/>',
|
||
"mail": '<rect width="20" height="16" x="2" y="4" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/>',
|
||
"megaphone": '<path d="m3 11 18-5v12L3 14v-3z"/><path d="M11.6 16.8a3 3 0 1 1-5.8-1.6"/>',
|
||
"message-square": '<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>',
|
||
"smartphone": '<rect width="14" height="20" x="5" y="2" rx="2" ry="2"/><path d="M12 18h.01"/>',
|
||
"sparkles": '<path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z"/><path d="M5 3v4"/><path d="M19 17v4"/><path d="M3 5h4"/><path d="M17 19h4"/>',
|
||
"sticky-note": '<path d="M15.5 3H5a2 2 0 0 0-2 2v14c0 1.1.9 2 2 2h14a2 2 0 0 0 2-2V8.5L15.5 3Z"/><path d="M15 3v6h6"/>',
|
||
"table": '<path d="M12 3v18"/><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M3 9h18"/><path d="M3 15h18"/>',
|
||
"user": '<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>',
|
||
"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"/>',
|
||
"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) {
|
||
const s = groesse / 24;
|
||
return '<g transform="translate(' + n1(cx - groesse / 2) + ',' + n1(cy - groesse / 2) +
|
||
') scale(' + n1(s) + ')" fill="none" stroke="' + farbe + '" stroke-width="' +
|
||
n1((staerke == null ? 2 : staerke) / s) +
|
||
'" stroke-linecap="round" stroke-linejoin="round">' + FEATHER[name] + '</g>';
|
||
}
|
||
|
||
function visVerbinden() {
|
||
const W = 988, H = 730, CX = 494, CY = 368;
|
||
const KERN = 104, RING = 202, SATX = 344, SATY = 262, KACHEL = 88;
|
||
|
||
const module = [
|
||
{ a: 240, t: "KI", i: "sparkles" },
|
||
{ a: 300, t: "Chat", i: "message-square" },
|
||
{ a: 0, t: "Mitarbeiter", i: "users" },
|
||
{ a: 60, t: "Automation", i: "zap" },
|
||
{ a: 120, t: "Projekte", i: "columns-3" },
|
||
{ a: 180, t: "Formulare", i: "clipboard-list" },
|
||
];
|
||
// KI hat kein eigenes Zeichen im Satz, deshalb der Funke von Slide 5 alt.
|
||
module[0].ikon = (x, y) => funkeForm(x, y, 20);
|
||
|
||
let raus = "";
|
||
|
||
// Der Ring: HiveGrid liegt um den Kern, nicht an seiner Stelle.
|
||
raus += '<circle cx="' + CX + '" cy="' + CY + '" r="' + RING + '" fill="none" stroke="' +
|
||
GELB + '" stroke-width="3"/>';
|
||
|
||
// Speichen und Kacheln.
|
||
module.forEach(m => {
|
||
const r = m.a * Math.PI / 180;
|
||
const sx = CX + Math.cos(r) * SATX, sy = CY + Math.sin(r) * SATY;
|
||
const vx = sx - CX, vy = sy - CY, vl = Math.hypot(vx, vy) || 1;
|
||
const ux = vx / vl, uy = vy / vl;
|
||
raus += '<line x1="' + n1(CX + ux * RING) + '" y1="' + n1(CY + uy * RING) +
|
||
'" x2="' + n1(sx - ux * (KACHEL / 2 + 10)) + '" y2="' + n1(sy - uy * (KACHEL / 2 + 10)) +
|
||
'" stroke="' + LINIE + '" stroke-width="2"/>';
|
||
raus += '<circle cx="' + n1(CX + ux * RING) + '" cy="' + n1(CY + uy * RING) +
|
||
'" r="7" fill="' + GELB + '"/>';
|
||
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" ' +
|
||
'font-family="Manrope, sans-serif" font-size="24" font-weight="700" ' +
|
||
'letter-spacing="-0.01em" fill="' + TINTE + '">' + m.t + '</text>';
|
||
});
|
||
|
||
// Der Kern. Gefuellt und dunkel, damit er als das Bestehende liest.
|
||
raus += '<circle cx="' + CX + '" cy="' + CY + '" r="' + KERN + '" fill="' +
|
||
(variant === "dunkel" ? "#1A2740" : "#1A1A1A") + '"/>';
|
||
raus += '<text x="' + CX + '" y="' + (CY - 34) + '" text-anchor="middle" ' +
|
||
'font-family="Manrope, sans-serif" font-size="16" font-weight="700" ' +
|
||
'letter-spacing="0.18em" fill="rgba(255,255,255,.5)">KERN</text>';
|
||
["Dein", "Programm"].forEach((z, i) => {
|
||
raus += '<text x="' + CX + '" y="' + (CY + 6 + i * 34) + '" text-anchor="middle" ' +
|
||
'font-family="Manrope, sans-serif" font-size="30" font-weight="800" ' +
|
||
'letter-spacing="-0.02em" fill="#FFFFFF">' + z + '</text>';
|
||
});
|
||
|
||
/* Die Pille auf dem Ring, wie im Pitch-Diagramm. Sie sitzt auf der Bahn
|
||
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 += 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">' + SCHATTEN_DEFS + raus + '</svg>';
|
||
}
|
||
|
||
/* Vierzackiger Funke fuer KI, praezise gezogen. */
|
||
function funkeForm(cx, cy, r) {
|
||
const k = r * 0.3, p = [];
|
||
for (let i = 0; i < 8; i++) {
|
||
const a = -Math.PI / 2 + i * Math.PI / 4, rr = i % 2 ? k : r;
|
||
p.push([cx + Math.cos(a) * rr, cy + Math.sin(a) * rr]);
|
||
}
|
||
return glatt(p, true);
|
||
}
|
||
|
||
/* ─── Das Wabenfeld ───────────────────────────────────────────────────────
|
||
Nachbau von HoneycombSection.tsx aus der Website. Die Geometrie ist von
|
||
dort uebernommen und nicht nachgemessen: derselbe Umkreisradius, dieselbe
|
||
Achsrechnung, dieselben Zellnummern, dieselbe Belegung. Wer die Startseite
|
||
kennt, soll dasselbe Bild sehen und nicht eine Anmutung davon.
|
||
|
||
Drei Dinge fallen gegenueber der Website weg:
|
||
|
||
- Die Zellen "In Arbeit" (29, 41, 46, 59). Auf der Startseite sind sie ein
|
||
Versprechen, im Beitrag waeren sie ein Eingestaendnis. Sie werden nicht
|
||
durch etwas anderes ersetzt, sondern sind wieder gewoehnliche leere
|
||
Waben.
|
||
- Alles Bewegte: Sog, Pakete, Hover. Ein Standbild hat davon nichts.
|
||
- Das Feld nach aussen. Die Website fuellt einen ganzen Abschnitt bis
|
||
Ring 18, hier reicht es bis knapp ueber den Rand der Slide.
|
||
|
||
Der Ausschnitt ist so gewaehlt, dass die Marken frei stehen und nur die
|
||
leeren Waben angeschnitten werden. Waagerecht laeuft das Feld bewusst
|
||
ueber beide Slide-Kanten, senkrecht blendet es aus (WABE_MASKE). */
|
||
const WABE_R = 62; // Umkreisradius, alles leitet sich davon ab
|
||
const WABE_B = 2 * WABE_R; // Breite Spitze zu Spitze
|
||
const WABE_ZELLE = 0.84; // laesst die Fuge zwischen zwei Waben offen
|
||
const WABE_MITTE = 0.88; // die Nabe steht eine Spur groesser
|
||
|
||
/* Achsial (q,r) auf Pixel, flat-top. */
|
||
const wabeOrt = (q, r) => ({ x: WABE_R * 1.5 * q, y: WABE_R * Math.sqrt(3) * (r + q / 2) });
|
||
const WABE_RICHTUNG = [[1, 0], [1, -1], [0, -1], [-1, 0], [-1, 1], [0, 1]];
|
||
|
||
/* Alle Waben genau `k` Ringe von der Mitte entfernt, im Ringgang. Die
|
||
Reihenfolge bestimmt die Zellnummern und darf sich nicht aendern, sonst
|
||
sitzen die Marken woanders. */
|
||
function wabeRing(k) {
|
||
if (k === 0) return [[0, 0]];
|
||
const raus = [];
|
||
let q = WABE_RICHTUNG[4][0] * k, r = WABE_RICHTUNG[4][1] * k;
|
||
for (let seite = 0; seite < 6; seite++) {
|
||
for (let schritt = 0; schritt < k; schritt++) {
|
||
raus.push([q, r]);
|
||
q += WABE_RICHTUNG[seite][0];
|
||
r += WABE_RICHTUNG[seite][1];
|
||
}
|
||
}
|
||
return raus;
|
||
}
|
||
|
||
/* Die Zellnummern der Website: 1 die Nabe, 2 bis 7 der innere Ring, 8 bis 19
|
||
der Deko-Ring, danach das Feld fortlaufend. Nur so stimmen die Nummern in
|
||
WABE_MARKEN mit HONEYCOMB_DEFAULT_LOGO_PLACEMENTS ueberein. */
|
||
const WABE_RINGE = 5;
|
||
const wabeZellen = (() => {
|
||
const raus = [];
|
||
for (let k = 0; k <= WABE_RINGE; k++) {
|
||
wabeRing(k).forEach(([q, r]) => {
|
||
raus.push({ ...wabeOrt(q, r), q, r, ring: k, nr: raus.length + 1 });
|
||
});
|
||
}
|
||
return raus;
|
||
})();
|
||
|
||
/* Die Belegung der Website, Zellnummer auf Datei. `gr` ist deren Skalierung,
|
||
`art` der Renderpfad, aus dem sie stammt: eine Feld-Zelle rechnet mit
|
||
einer um 1,18 groesseren Grundflaeche als eine Deko-Zelle. Beides
|
||
zusammen ergibt bei jeder Marke dieselbe sichtbare Groesse -- Claude
|
||
(deko, 1,12) und OpenAI (feld, 0,95) sind die Eichmarken und kommen beide
|
||
auf 56,5 px. Wer eine Marke tauscht, rechnet ihren Wert genauso aus:
|
||
gr = Grundflaeche des Pfads geteilt durch den Anteil, den die Datei in
|
||
ihrer eigenen Flaeche tatsaechlich ausfuellt. */
|
||
const WABE_MARKE_FELD = WABE_B * 0.48;
|
||
const WABE_MARKE_DEKO = WABE_MARKE_FELD / 1.18;
|
||
const WABE_MARKEN = {
|
||
9: { datei: "datev.png", art: "deko", gr: 1.10 },
|
||
10: { datei: "pandadoc.png", art: "deko", gr: 1.12, radius: 0.18 },
|
||
12: { datei: "ms-outlook.svg", art: "deko", gr: 1.12 },
|
||
13: { datei: "google-drive.svg", art: "deko", gr: 1.12 },
|
||
15: { datei: "claude.svg", art: "deko", gr: 1.12 },
|
||
17: { datei: "kwp.png", art: "deko", gr: 1.12 },
|
||
18: { datei: "bauhow.png", art: "deko", gr: 1.49 },
|
||
19: { datei: "pds.svg", art: "deko", gr: 1.12 },
|
||
21: { datei: "aircall.svg", art: "feld", gr: 1.43 },
|
||
22: { datei: "onepage.png", art: "feld", gr: 0.96 },
|
||
26: { datei: "ms-teams.svg", art: "feld", gr: 0.95 },
|
||
28: { datei: "google-calendar.svg", art: "feld", gr: 0.95 },
|
||
30: { datei: "openai.svg", art: "feld", gr: 0.95 },
|
||
36: { datei: "hallopetra.png", art: "feld", gr: 0.99 },
|
||
};
|
||
const wabeMarkePfad = m => "../assets/partner/" + m.datei;
|
||
/* Drei gelbe Waben ohne Marke direkt an der Nabe. Sie sind auf der Website
|
||
genauso gesetzt und der Grund, warum die Mitte nicht allein steht. */
|
||
const WABE_LEER_AKTIV = [3, 5, 7];
|
||
const wabeAktiv = nr => WABE_MARKEN[nr] != null || WABE_LEER_AKTIV.includes(nr);
|
||
|
||
/* Die Straenge. Ein Strang ist keine Linie, sondern eine gefuellte Form:
|
||
breit dort, wo er wurzelt, duenn in der Mitte. Werte aus der Website. */
|
||
const STRANG_FUSS = 14, STRANG_KOPF = 16, STRANG_TAILLE = 4.5, STRANG_TIEF = 8;
|
||
const WABE_INNEN = WABE_R * WABE_ZELLE * 0.8660254; // Inkreis einer Wabenflaeche
|
||
|
||
function strangForm(a, b) {
|
||
const dx = b.x - a.x, dy = b.y - a.y;
|
||
const l = Math.hypot(dx, dy) || 1;
|
||
const ux = dx / l, uy = dy / l, nx = -uy, ny = ux;
|
||
const wm = STRANG_TAILLE;
|
||
// Kubischer Griff, der in der Mitte genau die Taille ergibt.
|
||
const wc = (8 * wm - STRANG_FUSS - STRANG_KOPF) / 6;
|
||
const c1x = a.x + ux * l * 0.35, c1y = a.y + uy * l * 0.35;
|
||
const c2x = a.x + ux * l * 0.65, c2y = a.y + uy * l * 0.65;
|
||
const f = v => v.toFixed(2);
|
||
return '<path d="M' + f(a.x + nx * STRANG_FUSS) + " " + f(a.y + ny * STRANG_FUSS) +
|
||
"C" + f(c1x + nx * wc) + " " + f(c1y + ny * wc) + " " + f(c2x + nx * wc) + " " +
|
||
f(c2y + ny * wc) + " " + f(b.x + nx * STRANG_KOPF) + " " + f(b.y + ny * STRANG_KOPF) +
|
||
"L" + f(b.x - nx * STRANG_KOPF) + " " + f(b.y - ny * STRANG_KOPF) +
|
||
"C" + f(c2x - nx * wc) + " " + f(c2y - ny * wc) + " " + f(c1x - nx * wc) + " " +
|
||
f(c1y - ny * wc) + " " + f(a.x - nx * STRANG_FUSS) + " " + f(a.y - ny * STRANG_FUSS) +
|
||
'Z" fill="' + GELB + '" fill-opacity="0.7"/>';
|
||
}
|
||
|
||
/* Der Punkt, der `weit` von einer Wabe aus in Richtung einer anderen liegt.
|
||
Die Enden sitzen bewusst unter der Wabenflaeche (STRANG_TIEF), damit die
|
||
Naht nicht als Kante zu sehen ist. */
|
||
const wabeZu = (von, nach, weit) => {
|
||
const dx = nach.x - von.x, dy = nach.y - von.y;
|
||
const l = Math.hypot(dx, dy) || 1;
|
||
return { x: von.x + (dx / l) * weit, y: von.y + (dy / l) * weit };
|
||
};
|
||
|
||
/* Flat-top-Wabe als Pfad. */
|
||
function wabePfad(cx, cy, rr) {
|
||
const hh = rr * 0.8660254, f = v => v.toFixed(1);
|
||
return "M" + f(cx + rr) + " " + f(cy) + "L" + f(cx + rr / 2) + " " + f(cy + hh) +
|
||
"L" + f(cx - rr / 2) + " " + f(cy + hh) + "L" + f(cx - rr) + " " + f(cy) +
|
||
"L" + f(cx - rr / 2) + " " + f(cy - hh) + "L" + f(cx + rr / 2) + " " + f(cy - hh) + "Z";
|
||
}
|
||
|
||
/* Der Ausschnitt, und damit der Massstab. Er ist schmaler als die Slide
|
||
(880 statt 1080), das SVG wird also auf 1080 gezogen und alles darin um
|
||
den Faktor 1,227 groesser. Das ist die Zoomstufe: eng geschnitten, weil
|
||
ein Feld aus leeren Waben rundherum nur Platz kostet.
|
||
|
||
Senkrecht sitzt der Kasten nicht auf 0, sondern auf der Mitte des
|
||
Marken-Haufens. Der liegt bei y 27 und nicht bei y 0, weil unten eine
|
||
Reihe mehr belegt ist als oben; auf 0 zentriert stuende oben zu viel
|
||
Leerraum und unten zu wenig.
|
||
|
||
Die Grenzen sind gerechnet, nicht geschaetzt (Werte vor dem Faktor):
|
||
Waben x +/-341, y -268 bis 322
|
||
Marken x +/-308, y -243 bis 297
|
||
Damit bleiben zu den Slide-Kanten 122 px, zur naechsten Marke 162 px --
|
||
beides ueber der Sicherheitszone von 92 px aus rules.md 5. */
|
||
const WABE_VB = { x: -440, y: -323, w: 880, h: 700 };
|
||
/* Wie weit das Feld oben und unten ausblendet, in Anteilen der Hoehe. Die
|
||
Zonen duerfen nur ueber leeren Waben liegen, sonst blasst eine Marke aus:
|
||
mit 0,07 setzt die Blende bei y -274 und 328 ein, die aeusserste Wabe
|
||
reicht bis -268 und 322. Sechs Einheiten Luft, mehr nicht. Wer den Wert
|
||
erhoeht oder den Ausschnitt aendert, muss das nachrechnen. */
|
||
const WABE_MASKE = 0.07;
|
||
|
||
/* Die Marken-Kaesten in Zeichenkoordinaten, je Slide. Der Export prueft sie
|
||
einzeln nach, siehe wabenZonen(). Je Slide und nicht als eine Liste: wer
|
||
die Slide kopiert, hat das Feld zweimal, und die zweite Zeichnung haette
|
||
die Kaesten der ersten ueberschrieben. */
|
||
const wabenMarkenBoxen = new Map();
|
||
|
||
function visWabenfeld(j) {
|
||
const boxen = [];
|
||
wabenMarkenBoxen.set(j, boxen);
|
||
const mitte = wabeZellen[0];
|
||
let feld = "", flaechen = "", straenge = "", marken = "";
|
||
|
||
wabeZellen.forEach(z => {
|
||
// Was ganz ausserhalb des Ausschnitts liegt, wird nicht gezeichnet:
|
||
// ".vis svg" traegt overflow:visible, ein Beschnitt am Rahmen findet
|
||
// also nicht von selbst statt.
|
||
if (z.x < WABE_VB.x - WABE_B || z.x > WABE_VB.x + WABE_VB.w + WABE_B) return;
|
||
if (z.y < WABE_VB.y - WABE_B || z.y > WABE_VB.y + WABE_VB.h + WABE_B) return;
|
||
if (z.nr === 1) return; // die Nabe kommt zuletzt
|
||
|
||
if (wabeAktiv(z.nr)) {
|
||
flaechen += '<path d="' + wabePfad(z.x, z.y, WABE_R * WABE_ZELLE) +
|
||
'" fill="' + GELB + '"/>';
|
||
} else {
|
||
/* Die leeren Waben nach aussen hin blasser, eine Stufe je Ring. Auf
|
||
der Website macht das ein Verlauf ueber den ganzen Abschnitt; der
|
||
haette hier nichts zu tun, weil das Feld nur bis Ring 5 reicht.
|
||
Nicht "deck" nennen -- so heisst weiter oben das Blatt im DOM. */
|
||
const staerke = [0.10, 0.10, 0.10, 0.085, 0.06, 0.04][Math.min(z.ring, 5)];
|
||
feld += '<path d="' + wabePfad(z.x, z.y, WABE_R * WABE_ZELLE) +
|
||
'" fill="' + GELB + '" fill-opacity="' + staerke + '"/>';
|
||
}
|
||
});
|
||
|
||
/* Straenge von der Nabe zu ihren aktiven Nachbarn. */
|
||
wabeZellen.filter(z => z.ring === 1 && wabeAktiv(z.nr)).forEach(z => {
|
||
straenge += strangForm(
|
||
wabeZu(z, mitte, WABE_INNEN - STRANG_TIEF),
|
||
wabeZu(mitte, z, WABE_R * WABE_MITTE * 0.8660254 - STRANG_TIEF));
|
||
});
|
||
|
||
/* Und zwischen je zwei aktiven Waben, die aneinandergrenzen. Erst das
|
||
macht aus den Einzelkacheln ein Netz: die Programme haengen nicht nur
|
||
an der Mitte, sondern auch aneinander. */
|
||
const aktive = wabeZellen.filter(z => z.ring > 0 && wabeAktiv(z.nr));
|
||
aktive.forEach((a, i) => aktive.slice(i + 1).forEach(b => {
|
||
const dq = b.q - a.q, dr = b.r - a.r;
|
||
if (!WABE_RICHTUNG.some(([q, r]) => q === dq && r === dr)) return;
|
||
straenge += strangForm(
|
||
wabeZu(a, b, WABE_INNEN - STRANG_TIEF),
|
||
wabeZu(b, a, WABE_INNEN - STRANG_TIEF));
|
||
}));
|
||
|
||
/* Die Marken. Als <image> mit Datenadresse und nicht inline: die Dateien
|
||
von Aircall und Google Calendar bringen eigene <style>-Bloecke mit
|
||
Klassennamen wie .st0 mit. In einem gemeinsamen SVG wuerden die sich
|
||
gegenseitig ueberschreiben, und zwei Marken saehen falsch aus. Als
|
||
eigenes Bild bleibt jede Datei ihr eigenes Dokument. */
|
||
wabeZellen.forEach(z => {
|
||
const m = WABE_MARKEN[z.nr];
|
||
if (!m) return;
|
||
const quelle = assets[wabeMarkePfad(m)];
|
||
if (!quelle) return;
|
||
const kante = (m.art === "feld" ? WABE_MARKE_FELD : WABE_MARKE_DEKO) * m.gr;
|
||
const x = z.x - kante / 2, y = z.y - kante / 2;
|
||
boxen.push({ name: m.datei, x, y, kante });
|
||
// Runde Ecken, wo die Vorlage sie hat: PandaDoc liefert eine harte
|
||
// Kachel, auf der Website liegt sie unter einem Radius.
|
||
if (m.radius) {
|
||
marken += '<clipPath id="wabe-clip-' + z.nr + '"><rect x="' + n1(x) +
|
||
'" y="' + n1(y) + '" width="' + n1(kante) + '" height="' + n1(kante) +
|
||
'" rx="' + n1(kante * m.radius) + '"/></clipPath>';
|
||
}
|
||
marken += '<image href="' + quelle + '" x="' + n1(x) + '" y="' + n1(y) +
|
||
'" width="' + n1(kante) + '" height="' + n1(kante) +
|
||
'" preserveAspectRatio="xMidYMid meet"' +
|
||
(m.radius ? ' clip-path="url(#wabe-clip-' + z.nr + ')"' : "") + '/>';
|
||
});
|
||
|
||
/* Die Nabe. Gelbe Wabe, die Marke als Aussparung darin. Auf der Website
|
||
ist die Aussparung durchsichtig und zeigt das Feld dahinter; auf dem
|
||
weissen Blatt kommt das auf dasselbe heraus, also steht hier Weiss. */
|
||
const nabe = '<path d="' + wabePfad(mitte.x, mitte.y, WABE_R * WABE_MITTE) +
|
||
'" fill="' + GELB + '"/>' +
|
||
gMarke(mitte.x, mitte.y, WABE_R * 0.98, SLIDE_GRUND);
|
||
|
||
/* Die ids sind feste Namen und kommen bei mehreren Blaettern doppelt vor.
|
||
Das stoert nicht: alle Zeichnungen sind gleich aufgebaut, und url(#..)
|
||
nimmt die erste. Dasselbe Argument wie beim Schattenfilter des Themes. */
|
||
const vb = WABE_VB;
|
||
return '<svg viewBox="' + vb.x + " " + vb.y + " " + vb.w + " " + vb.h +
|
||
'" width="' + vb.w + '" height="' + vb.h + '" xmlns="http://www.w3.org/2000/svg">' +
|
||
'<defs><linearGradient id="wabe-blende" x1="0" y1="0" x2="0" y2="1">' +
|
||
'<stop offset="0" stop-color="#000"/>' +
|
||
'<stop offset="' + WABE_MASKE + '" stop-color="#fff"/>' +
|
||
'<stop offset="' + (1 - WABE_MASKE) + '" stop-color="#fff"/>' +
|
||
'<stop offset="1" stop-color="#000"/></linearGradient>' +
|
||
'<mask id="wabe-maske" maskUnits="userSpaceOnUse" x="' + vb.x + '" y="' + vb.y +
|
||
'" width="' + vb.w + '" height="' + vb.h + '">' +
|
||
'<rect x="' + vb.x + '" y="' + vb.y + '" width="' + vb.w + '" height="' + vb.h +
|
||
'" fill="url(#wabe-blende)"/></mask>' +
|
||
'<clipPath id="wabe-rahmen"><rect x="' + vb.x + '" y="' + vb.y + '" width="' + vb.w +
|
||
'" height="' + vb.h + '"/></clipPath></defs>' +
|
||
'<g clip-path="url(#wabe-rahmen)"><g mask="url(#wabe-maske)">' +
|
||
feld + straenge + flaechen + marken + nabe +
|
||
"</g></g></svg>";
|
||
}
|
||
|
||
/* Die Marken des Wabenfelds als Datenadressen. In den Post-Seiten macht das
|
||
preloadAssets(); hier reicht dieser Lader, weil es nur diese eine Sorte
|
||
Bild gibt. Ohne sie zeichnet visWabenfeld() lauter leere gelbe Waben, und
|
||
zwar ohne Fehlermeldung -- deshalb wird ein Fehlschlag hier laut. */
|
||
const assets = {};
|
||
async function markenLaden() {
|
||
for (const m of Object.values(WABE_MARKEN)) {
|
||
const pfad = wabeMarkePfad(m);
|
||
const res = await fetch(pfad, { cache: "no-store" }).catch(() => null);
|
||
if (!res || !res.ok) throw new Error("Marke fehlt: " + pfad);
|
||
const blob = await res.blob();
|
||
assets[pfad] = await new Promise((ok, weg) => {
|
||
const fr = new FileReader();
|
||
fr.onload = () => ok(fr.result);
|
||
fr.onerror = weg;
|
||
fr.readAsDataURL(blob);
|
||
});
|
||
}
|
||
}
|
||
|
||
/* ─── Zeitverlauf ──────────────────────────────────────────────────────────
|
||
Jede Komponente kennt genau eine Bewegung: von 0 (nicht da) bis 1
|
||
(steht). Die Sequenz "aus" ist dieselbe rueckwaerts, deshalb gibt es hier
|
||
keine zweite Kurve. Ein eigener Ausgang saehe eine Spur besser aus und
|
||
kostete die doppelte Zahl an Bildern -- bei einer Einblendung, die eine
|
||
halbe Sekunde dauert, sieht das niemand.
|
||
|
||
Elemente melden ihre Bewegung ueber data-a an, ihren Verzug ueber data-v.
|
||
Der Verzug ist ein Anteil des Verlaufs, kein Zeitwert: so bleibt die
|
||
Staffelung gleich, egal mit wie vielen Bildern gerendert wird. */
|
||
const ruhig = p => 1 - Math.pow(1 - p, 3); // easeOutCubic
|
||
|
||
function bewegen(wurzel, t) {
|
||
wurzel.querySelectorAll("[data-a]").forEach(el => {
|
||
const v = parseFloat(el.dataset.v || "0");
|
||
// Der Verzug frisst vom Verlauf, der Rest bleibt fuer die Bewegung.
|
||
const p = ruhig(Math.max(0, Math.min(1, (t - v) / Math.max(0.001, 1 - v))));
|
||
const art = el.dataset.a;
|
||
if (art === "auf") {
|
||
el.style.opacity = p;
|
||
el.style.transform = "translateY(" + n1((1 - p) * 26) + "px)";
|
||
} else if (art === "wisch") {
|
||
/* Von links aufziehen. clip-path und nicht width: die Schrift darin
|
||
soll stehenbleiben und nicht mitgestaucht werden.
|
||
|
||
Am Ende faellt der Beschnitt ganz weg. "inset(0 0 0 0)" ist nicht
|
||
dasselbe wie kein Beschnitt: es schneidet exakt auf die Box, und der
|
||
weiche Schatten liegt ausserhalb davon. Das gehaltene Bild hatte
|
||
damit eine harte Kante statt eines Auslaufs -- am gerenderten PNG
|
||
nachgemessen, nicht vermutet. */
|
||
el.style.clipPath = p >= 1 ? "none" : "inset(0 " + n1((1 - p) * 100) + "% 0 0)";
|
||
el.style.opacity = p > 0 ? 1 : 0;
|
||
} else if (art === "pop") {
|
||
el.style.opacity = p;
|
||
el.style.transform = "scale(" + n1(0.9 + p * 0.1) + ")";
|
||
} else if (art === "dreh") {
|
||
// Der Chip behaelt seine Schraeglage und faellt nur hinein.
|
||
el.style.opacity = p;
|
||
el.style.transform = "rotate(-8deg) scale(" + n1(0.82 + p * 0.18) + ")";
|
||
}
|
||
});
|
||
}
|
||
|
||
/* ─── Bausteine ────────────────────────────────────────────────────────────
|
||
Kleine Helfer, damit die Komponenten darunter lesbar bleiben. */
|
||
const esc = s => String(s).replace(/[&<>]/g, c => ({ "&": "&", "<": "<", ">": ">" }[c]));
|
||
|
||
/* *Wort* setzt den Akzent, wie in den Post-Seiten. Genau eine Auszeichnung
|
||
pro Zeile -- zwei waren dort schon zweimal zu viel. */
|
||
const akzent = s => esc(s).replace(/\*([^*]+)\*/, '<span class="a">$1</span>');
|
||
|
||
const gMarkeHtml = farbe =>
|
||
'<svg viewBox="0 0 384.5 332.9" xmlns="http://www.w3.org/2000/svg">' +
|
||
'<polygon fill="' + farbe + '" points="' + G_PUNKTE + '"/></svg>';
|
||
|
||
const ikonHtml = (name, farbe) =>
|
||
'<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">' +
|
||
feather(name, 12, 12, 24, farbe || TEAL, 2) + "</svg>";
|
||
|
||
/* ─── Die Komponenten ──────────────────────────────────────────────────────
|
||
Jede kennt ihren Schluessel, eine Buehnengroesse und eine Bauanweisung.
|
||
Die Buehne ist absichtlich groesser als noetig: bauen.mjs misst nach dem
|
||
Rendern nach und schneidet auf das, was tatsaechlich dasteht. So muss hier
|
||
niemand Pixel zaehlen, und ein laengerer Name sprengt nichts.
|
||
|
||
"bilder" ist die Zahl der Einzelbilder fuer die Einblendung. Kleine
|
||
Komponenten bekommen mehr, die grossen Zeichnungen weniger -- bei 1800 px
|
||
Breite kostet jedes Bild ein Vielfaches, und eine Zeichnung, die als Ganzes
|
||
aufblendet, braucht keine feine Staffelung.
|
||
|
||
Wenig Text ist Absicht. Noah spricht das Skript, die Einblendung setzt das
|
||
Stichwort. Wer mehr braucht, haengt es an die Adresse: die Felder unten
|
||
sind ueber ?feld=Wert austauschbar. */
|
||
|
||
const KOMPONENTEN = [
|
||
|
||
/* ── Bauchbinden ───────────────────────────────────────────────────────── */
|
||
{
|
||
key: "bauchbinde-noah", name: "Bauchbinde · Noah", w: 900, h: 340, bilder: 20,
|
||
felder: { name: "Noah Geerkens", rolle: "Gründer" },
|
||
bau: f =>
|
||
'<div class="schatten" data-a="wisch" style="position:absolute;left:56px;top:56px">' +
|
||
'<div class="karte bb" style="position:relative">' +
|
||
'<span class="bb-balken"></span>' +
|
||
'<span class="marke" data-a="auf" data-v=".30">' + gMarkeHtml(GELB) + '</span>' +
|
||
'<span class="zeilen">' +
|
||
'<b data-a="auf" data-v=".40">' + esc(f.name) + '</b>' +
|
||
'<small data-a="auf" data-v=".52">' + esc(f.rolle) + '</small>' +
|
||
'</span>' +
|
||
'</div>' +
|
||
'</div>',
|
||
},
|
||
{
|
||
key: "bauchbinde-betrieb", name: "Bauchbinde · Betrieb", w: 900, h: 340, bilder: 20,
|
||
felder: { name: "Geerkens Haustechnik", rolle: "SHK · Vierte Generation" },
|
||
bau: f => KOMPONENTEN[0].bau(f),
|
||
},
|
||
|
||
/* ── Stempel ───────────────────────────────────────────────────────────── */
|
||
{
|
||
key: "chip-generation", name: "Chip · Vierte Generation", w: 800, h: 300, bilder: 16,
|
||
felder: { text: "Vierte Generation · SHK" },
|
||
bau: f =>
|
||
'<div style="position:absolute;left:70px;top:90px">' +
|
||
'<span class="chip schatten" data-a="dreh">' + esc(f.text) + '</span>' +
|
||
'</div>',
|
||
},
|
||
|
||
/* ── Satzkarten ────────────────────────────────────────────────────────── */
|
||
{
|
||
key: "satz-erp-bleibt", name: "Satz · Dein ERP bleibt", w: 1100, h: 340, bilder: 18,
|
||
felder: { zeile: "Dein ERP *bleibt*.", sub: "" },
|
||
bau: f => satzKarte(f),
|
||
},
|
||
{
|
||
key: "satz-kein-ersatz", name: "Satz · Kein Ersatz", w: 1100, h: 400, bilder: 18,
|
||
felder: { zeile: "Kein *Ersatz*.", sub: "Der Aufbau drumherum." },
|
||
bau: f => satzKarte(f),
|
||
},
|
||
{
|
||
key: "satz-miteinander-reden", name: "Satz · Miteinander reden", w: 1300, h: 400, bilder: 18,
|
||
felder: { zeile: "Die Tools reden *miteinander*.", sub: "" },
|
||
bau: f => satzKarte(f),
|
||
},
|
||
{
|
||
key: "satz-handwerksalltag", name: "Satz · Handwerksalltag", w: 1200, h: 400, bilder: 18,
|
||
felder: { zeile: "Kein *Startup*-Projekt.", sub: "Entstanden im Handwerksalltag." },
|
||
bau: f => satzKarte(f),
|
||
},
|
||
|
||
/* ── Aufzaehlungen ─────────────────────────────────────────────────────── */
|
||
{
|
||
key: "stapel-haengt-drin", name: "Stapel · Was drin haengt", w: 800, h: 500, bilder: 22,
|
||
felder: { a: "Stammdaten", b: "Aufträge", c: "Buchhaltung" },
|
||
bau: f => stapel([
|
||
{ ikon: "table", text: f.a },
|
||
{ ikon: "file-text", text: f.b },
|
||
{ ikon: "folder", text: f.c },
|
||
], false),
|
||
},
|
||
{
|
||
key: "stapel-was-fehlte", name: "Stapel · Was gefehlt hat", w: 800, h: 500, bilder: 22,
|
||
felder: { a: "KI-Lösungen", b: "CRM", c: "Automatisierungen" },
|
||
bau: f => stapel([
|
||
{ ikon: "sparkles", text: f.a },
|
||
{ ikon: "users", text: f.b },
|
||
{ ikon: "zap", text: f.c },
|
||
], true),
|
||
},
|
||
|
||
/* ── Zeichnungen ───────────────────────────────────────────────────────── */
|
||
{
|
||
/* Der Ring aus post2-alt.html, Slide 5. Er sagt in einem Bild, was der
|
||
Absatz sagt: die Mitte bleibt, HiveGrid liegt darum. */
|
||
key: "ring-kern", name: "Ring · Dein Programm bleibt", w: 1100, h: 840, bilder: 12,
|
||
felder: {},
|
||
bau: () =>
|
||
'<div class="zeichnung" data-a="pop" style="position:absolute;left:56px;top:56px;width:988px">' +
|
||
visVerbinden() + '</div>',
|
||
},
|
||
{
|
||
/* Das Wabenfeld von der Startseite, wie in post3.html. Fuer den Absatz
|
||
ueber die Tools, die endlich miteinander reden. */
|
||
key: "wabenfeld", name: "Wabenfeld · Alles verbunden", w: 1100, h: 940, bilder: 12,
|
||
felder: {},
|
||
bau: () =>
|
||
'<div class="zeichnung" data-a="pop" style="position:absolute;left:56px;top:56px;width:980px">' +
|
||
visWabenfeld("video") + '</div>',
|
||
},
|
||
|
||
/* ── Marke ─────────────────────────────────────────────────────────────── */
|
||
{
|
||
key: "logo-bug", name: "Logo · Ecke", w: 700, h: 220, bilder: 14,
|
||
felder: { text: "Vom Handwerker für Handwerker" },
|
||
bau: f =>
|
||
'<div class="schatten" data-a="auf" style="position:absolute;left:56px;top:56px">' +
|
||
'<div class="karte bug">' +
|
||
'<img src="../assets/brand/logo.svg" alt="HiveGrid">' +
|
||
'<span class="strich"></span>' +
|
||
'<span>' + esc(f.text) + '</span>' +
|
||
'</div>' +
|
||
'</div>',
|
||
},
|
||
{
|
||
key: "endkarte", name: "Endkarte · Warteliste", w: 1200, h: 500, bilder: 20,
|
||
felder: { zeile: "Vom Handwerker für *Handwerker*", sub: "Setz dich auf die Warteliste." },
|
||
bau: f =>
|
||
'<div class="schatten" data-a="pop" style="position:absolute;left:56px;top:56px">' +
|
||
'<div class="karte ende">' +
|
||
'<h1 data-a="auf" data-v=".25">' + akzent(f.zeile) + '</h1>' +
|
||
'<p data-a="auf" data-v=".45">' + esc(f.sub) + '</p>' +
|
||
'</div>' +
|
||
'</div>',
|
||
},
|
||
];
|
||
|
||
/* Zwei Bauanweisungen, die sich mehrere Komponenten teilen. Sie stehen
|
||
ausserhalb der Liste, damit dort nur der Inhalt steht und nicht dreimal
|
||
dasselbe Markup. */
|
||
function satzKarte(f) {
|
||
return '<div class="schatten" data-a="wisch" style="position:absolute;left:56px;top:56px">' +
|
||
'<div class="karte satz">' +
|
||
'<p data-a="auf" data-v=".30">' + akzent(f.zeile) + '</p>' +
|
||
(f.sub ? '<p class="sub" data-a="auf" data-v=".48">' + esc(f.sub) + '</p>' : "") +
|
||
'</div></div>';
|
||
}
|
||
|
||
/* Die Zeilen laufen versetzt ein, eine nach der anderen. Der Verzug ist ein
|
||
Anteil des Verlaufs und keine Zeit: mit mehr Bildern wird die Bewegung
|
||
feiner, die Staffelung bleibt dieselbe. */
|
||
function stapel(zeilen, an) {
|
||
return '<div class="schatten" style="position:absolute;left:56px;top:56px">' +
|
||
zeilen.map((z, i) =>
|
||
'<div class="karte zeile' + (an ? " an" : "") + '" data-a="auf" data-v="' +
|
||
(0.12 + i * 0.2).toFixed(2) + '">' +
|
||
'<span class="ikon">' + ikonHtml(z.ikon, an ? GELB : TEAL) + '</span>' +
|
||
'<span>' + esc(z.text) + '</span>' +
|
||
'</div>').join("") +
|
||
'</div>';
|
||
}
|
||
|
||
/* ─── Aufbau ───────────────────────────────────────────────────────────────
|
||
Ohne k steht der Kontaktbogen, mit k genau eine Komponente. bauen.mjs liest
|
||
KOMP_LISTE aus, wartet auf KOMP_BEREIT und faehrt sie ab. */
|
||
window.KOMP_LISTE = KOMPONENTEN.map(k => ({
|
||
key: k.key, name: k.name, w: k.w, h: k.h, bilder: k.bilder,
|
||
}));
|
||
|
||
const buehne = document.getElementById("buehne");
|
||
const q = new URLSearchParams(location.search);
|
||
|
||
function eine(k, felder) {
|
||
const el = document.createElement("div");
|
||
/* Der Rahmen, auf den sich die Komponenten mit position:absolute beziehen.
|
||
Er traegt die volle Buehnengroesse und ist selbst unsichtbar -- deshalb
|
||
ist er beim Messen ausgenommen, sonst waere jeder Kasten so gross wie die
|
||
Buehne und das PNG bestuende zur Haelfte aus Nichts. */
|
||
el.dataset.buehne = "1";
|
||
el.style.cssText = "position:relative;width:" + k.w + "px;height:" + k.h + "px";
|
||
el.innerHTML = k.bau(Object.assign({}, k.felder, felder));
|
||
return el;
|
||
}
|
||
|
||
/* Wie viel Luft um das Gemessene bleibt. Zwei Dinge stehen ausserhalb des
|
||
Rechtecks, das getBoundingClientRect() liefert: der weiche Schatten der
|
||
Karte (10 px Versatz, 34 px Unschaerfe) und die Bewegung selbst -- wer
|
||
aufblendet, startet 26 px tiefer. Ohne die Zugabe schneidet das PNG beides
|
||
ab, und im Schnitt sieht man eine harte Kante, wo der Schatten aufhoert. */
|
||
const ZUGABE = 52;
|
||
|
||
/* Der Ausschnitt, den bauen.mjs aufnimmt: alles, was tatsaechlich dasteht,
|
||
plus Zugabe. Gemessen wird im gehaltenen Zustand und einmal -- ein Kasten,
|
||
der je Bild wandert, ergaebe eine zappelnde Sequenz. */
|
||
window.KOMP_KASTEN = () => {
|
||
let x1 = Infinity, y1 = Infinity, x2 = -Infinity, y2 = -Infinity;
|
||
buehne.querySelectorAll("*").forEach(el => {
|
||
if (el.dataset.buehne) return;
|
||
const r = el.getBoundingClientRect();
|
||
if (r.width < 1 || r.height < 1) return;
|
||
x1 = Math.min(x1, r.left); y1 = Math.min(y1, r.top);
|
||
x2 = Math.max(x2, r.right); y2 = Math.max(y2, r.bottom);
|
||
});
|
||
if (!isFinite(x1)) throw new Error("nichts zu messen in " + window.KOMP_AKTUELL);
|
||
const x = Math.max(0, Math.floor(x1 - ZUGABE));
|
||
const y = Math.max(0, Math.floor(y1 - ZUGABE));
|
||
return { x, y, w: Math.ceil(x2 + ZUGABE) - x, h: Math.ceil(y2 + ZUGABE) - y };
|
||
};
|
||
|
||
/* Erst die Marken, dann die Schrift, dann bauen. Beides muss stehen, bevor
|
||
das erste Bild faellt: eine fehlende Marke bliebe eine leere gelbe Wabe,
|
||
eine fehlende Schrift rendert still in der Ersatzschrift und faellt erst
|
||
in der fertigen Datei auf (rules.md 4 und 7). */
|
||
window.KOMP_BEREIT = (async () => {
|
||
await markenLaden();
|
||
await document.fonts.ready;
|
||
|
||
if (q.has("alle")) {
|
||
document.body.className = "bogen";
|
||
KOMPONENTEN.forEach(k => {
|
||
const zelle = document.createElement("div");
|
||
zelle.className = "bogen-zelle";
|
||
zelle.innerHTML = '<span class="bez">' + esc(k.key) + " · " + k.w + "×" + k.h + "</span>";
|
||
zelle.appendChild(eine(k));
|
||
buehne.appendChild(zelle);
|
||
});
|
||
bewegen(buehne, 1);
|
||
return true;
|
||
}
|
||
|
||
const key = q.get("k") || KOMPONENTEN[0].key;
|
||
const k = KOMPONENTEN.find(x => x.key === key);
|
||
if (!k) throw new Error("Komponente unbekannt: " + key);
|
||
/* Freie Felder aus der Adresse, damit sich Texte ohne Codeaenderung
|
||
austauschen lassen: ?k=satz-erp-bleibt&zeile=Dein *CRM* bleibt. */
|
||
const felder = {};
|
||
for (const [n, w] of q) if (n !== "k" && n !== "t") felder[n] = w;
|
||
buehne.style.width = k.w + "px";
|
||
buehne.style.height = k.h + "px";
|
||
buehne.appendChild(eine(k, felder));
|
||
window.KOMP_AKTUELL = k.key;
|
||
window.setT = t => bewegen(buehne, t);
|
||
setT(q.has("t") ? parseFloat(q.get("t")) : 1);
|
||
return true;
|
||
})();
|
||
</script>
|