Theme modern: Slide 5 und 6 aus den echten Quellen gebaut

Bisher habe ich die Skizzen entschraffiert statt neu gedacht. Das ist bei
Slide 6 offensichtlich geworden: eine Wabe ohne Zittern ist immer noch
meine Wabe und nicht die Sektion, die es auf der Website schon gibt.

Slide 6 ist jetzt ein Nachbau von app/waitlist/WaitlistNext.tsx. Alle Werte
von dort, mit 1,6 hochskaliert, weil die Liste dort 560 breit ist und hier
896: Wabe 54x48 wird 86x77, Schiene links 26 wird 43 also die Wabenmitte,
Abstand 56 wird 90, Nummer 13 wird 21, Titel 20 wird 32, Text 14,5 wird 23.
Auf der Website leuchten die Waben beim Scrollen nacheinander auf und der
dritte ist blass; ein Standbild kennt keinen Scrollstand, hier sind alle
drei aktiv.

Slide 5 ist ein Nachbau des Architektur-Diagramms aus dem Pitch-Paket:
Kern als gefuellter Kreis in der Mitte, farbiger Ring darum, Pille mit dem
Produktnamen auf dem Ring, Module als Kacheln aussen. Das Diagramm sagt
genau, was die Ueberschrift sagt -- der Kern bleibt, der Ring kommt
drumherum. Meine Wabe hatte dieselbe Aussage, aber in einer Form, die es in
unserer Software nirgends gibt.

Zwei eigene Fehler dabei behoben:

Ich hatte fuer das Theme Farben erfunden (#0E1F1E, #F7FAF9). Die Website
hat ein vollstaendiges Token-Set in app/globals.css. Jetzt stehen dort
dessen Werte: surface, text-primary, text-secondary, container-bg,
border-neutral, accent-yellow-light, accent-yellow-dark, accent-teal-light.

Meine Karten hatten Chamfer und Haarlinie zugleich. In lib/chamfer.ts der
Website steht ausdruecklich, dass clip-path den Rand mitbeschneidet und man
stattdessen fuellen soll. Die Karten sind jetzt gefuellt, ohne Rand.

Das Chamfer-Badge der Website ist zurueck. Es steht dort ueber jeder
Sektion, und im Theme modern traegt es die Einordnung: Die Geschichte, Der
Alltag, Der Wechsel, Was gefehlt hat, Die Loesung, So geht's weiter.

Neu im Vorgehen: nach jeder groesseren Aenderung wird das Skript aus der
Datei geschnitten und mit node --check geprueft. Ein doppelt vergebener
Name hat die Seite still lahmgelegt, und die Pruefung findet so etwas in
einer Sekunde statt in einem Renderlauf.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
dejanandClaude Opus 5 committed 2026-08-13 18:57:44 +02:00
1 parent 993dc3e5ef
commit 9b405ce931
2 files changed
+213 -120

No files matched your search

+212 -119
View File
@@ -486,9 +486,9 @@
}
.page { position: absolute; top: 0; width: 1080px; height: 1350px; }
/* 59 px oben: das ist die gemessene Oberkante der Ueberschrift auf der
Titel-Slide von Post 1. Seitlich bleiben es 92. */
.content { position: relative; z-index: 2; padding: 59px 92px 0; }
/* Im Theme modern traegt das Badge wieder Platz, deshalb beginnt der Block
bei 92 -- dem Rand der Safe Zone -- statt bei den 59 des Themes notebook. */
.content { position: relative; z-index: 2; padding: 92px 92px 0; }
/* Section badge, straight from the site: .chamfer-badge in globals.css with
accent-yellow-light on accent-yellow-dark. Scaled from the 12px web size to
@@ -499,13 +499,16 @@
dadurch 125 px tiefer als die Titel-Slide von Post 1. Jetzt gibt das Tag
den Platz frei, und der obere Abstand steht direkt im padding von
.content. Wer das Tag je sichtbar macht, muss das padding mitziehen. */
/* Das Chamfer-Badge der Website: gelb aufgehellte Flaeche, dunkler Text,
Versalien und weite Sperrung. Es steht dort ueber jeder Sektion. Im Theme
notebook ist es ausgeblendet, hier traegt es die Einordnung. */
.badge {
display: inline-block; margin-bottom: 38px;
font-size: 26px; font-weight: 600; letter-spacing: 0.1em;
padding: 9px 20px;
display: inline-block; margin-bottom: 28px;
font-size: 24px; font-weight: 600; letter-spacing: 0.16em;
text-transform: uppercase;
padding: 11px 22px;
background: var(--accent-yellow-light);
color: var(--accent-yellow-dark);
display: none;
clip-path: polygon(
11px 0, calc(100% - 11px) 0, 100% 11px, 100% calc(100% - 11px),
calc(100% - 11px) 100%, 11px 100%, 0 calc(100% - 11px), 0 11px
@@ -521,8 +524,8 @@
/* Groesser und enger als im Theme notebook: die Vorbilder setzen die
Ueberschrift als Blickfang ueber fast die ganze Breite. */
.headline {
font-size: 84px; font-weight: 800;
letter-spacing: -0.035em; line-height: 1.02;
font-size: 76px; font-weight: 800;
letter-spacing: -0.032em; line-height: 1.04;
color: var(--head-color);
}
/* Kein Marker. Der Akzent ist im Theme modern ein farbiges Wort -- so
@@ -807,9 +810,9 @@
/* ─── Variant: dunkel ────────────────────────────────────────────────── */
/* Theme modern, dunkle Variante. Derselbe Aufbau, invertiert. */
/* Theme modern, dunkle Variante. bg-dark aus der Website. */
.slide[data-variant="dunkel"] {
background: #0E1F1E;
background: #0F1A2E;
--head-color: var(--text-white);
--body-color: #B9BDC9;
--accent-on: var(--accent-yellow);
@@ -826,19 +829,19 @@
}
/* ─── Variant: hell ──────────────────────────────────────────────────── */
/* Theme modern, helle Variante. Drei Marken-Farben und sonst nichts:
Weiss als Grund, Orange als Akzent, Teal als zweiter Akzent. Die
Schriftfarbe ist ein sehr dunkles Teal statt reinem Schwarz, damit sie
zur Palette gehoert und nicht danebensteht. */
/* Theme modern, helle Variante. Die Werte stammen aus app/globals.css der
Website, nicht aus eigener Erfindung: surface, text-primary,
text-secondary, container-bg, border-neutral. Eine erfundene Palette war
der erste Fehler dieser Fassung. */
.slide[data-variant="hell"] {
background: #F7FAF9;
--head-color: #0E1F1E;
--body-color: #5B6D6B;
background: #FFFFFF;
--head-color: #1A1A1A;
--body-color: #555555;
--accent-on: #C8901A;
--marker: var(--accent-yellow);
--frame-bg: #EAF0EF;
--shape-fill: #0E1F1E;
--shape-line: rgba(14,31,30,.13);
--frame-bg: #F6F8FA;
--shape-fill: #1A1A1A;
--shape-line: #E5E5E5;
--pat-color: #30313D; --pat-strength: 0;
--gmark-color: rgba(48,49,61,.16);
--guide-line: rgba(48,49,61,.20);
@@ -1147,7 +1150,7 @@
key: "titel", type: "feature",
layouts: [{ id: "ecke", label: "Helm auf der Ecke" },
{ id: "kopf", label: "Helm auf dem Kopf" }],
eyebrow: "Titel",
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,
@@ -1155,7 +1158,7 @@
{
key: "prozesse", type: "feature",
layouts: [{ id: "standard", label: "Standard" }],
eyebrow: "Zettelwirtschaft",
eyebrow: "Der Alltag",
head: 'Wer lange dabei ist,<br><span class="a">hat nicht nur Erfahrung.</span>',
body: "Auch Abläufe, die laufen, weil die Software es vorgibt. Nicht, weil ein Konzept dahintersteckt.",
vis: "schiene", visW: 896, visLeft: 92, visBottom: 120,
@@ -1163,7 +1166,7 @@
{
key: "wechseln", type: "feature",
layouts: [{ id: "standard", label: "Standard" }],
eyebrow: "Wechseln",
eyebrow: "Der Wechsel",
head: 'Und wechseln?<br><span class="a">Kannst du vergessen.</span>',
body: "Da hängen Jahre an Stammdaten, Aufträgen und Buchhaltung drin. Ein Wechsel führt ins Chaos.",
vis: "ersetzen", visW: 896, visLeft: 92, visBottom: 190,
@@ -1171,7 +1174,7 @@
{
key: "mehr", type: "feature",
layouts: [{ id: "standard", label: "Standard" }],
eyebrow: "Mehr",
eyebrow: "Was gefehlt hat",
head: 'Wir wollten nicht weg.<br><span class="a">Wir wollten mehr.</span>',
body: "Ein CRM, Vertrieb, Marketing, Automatisierungen. Unsere Tools sollen endlich miteinander reden.",
vis: "lose", visW: 988, visLeft: 92, visBottom: 120,
@@ -1179,7 +1182,7 @@
{
key: "verbinden", type: "feature",
layouts: [{ id: "standard", label: "Standard" }],
eyebrow: "Verbinden",
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: 988, visLeft: 0, visBottom: 120,
@@ -1187,14 +1190,14 @@
{
key: "warteliste", type: "feature",
layouts: [{ id: "standard", label: "Standard" }],
eyebrow: "Warteliste",
eyebrow: "So geht's weiter",
/* Die Marken-Tagline. Sie wird genauso ausgezeichnet wie in Post 1 und
im Titel-Layout: der Marker sitzt auf dem zweiten "Handwerker", nicht
auf der ganzen Zeile. Eine zweite Schreibweise derselben Zeile waere
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: 120,
vis: "warteliste", visW: 896, visLeft: 92, visBottom: 170,
},
];
@@ -1311,17 +1314,29 @@
Gelb darf dort der volle Markenton sein statt des abgedunkelten. */
/* Drei Marken-Farben, dazu Tinte und eine Haarlinie. Teal war in rules.md
bisher als "ungenutzt" vermerkt und traegt hier den zweiten Akzent. */
let TINTE = "#0E1F1E", GELB = "#F1B729", TEAL = "#0D6E6E",
TEXT2 = "#5B6D6B", LINIE = "#DCE4E3", KARTE = "#FFFFFF";
/* Alle Werte aus app/globals.css der Website. GELB_DUNKEL ist die Textfarbe
auf gelbem Grund, GELB_HELL die aufgehellte Flaeche fuer Badges. */
let TINTE = "#1A1A1A", TEXT2 = "#555555", TEXT3 = "#888888",
GELB = "#F1B729", GELB_DUNKEL = "#3E2C0A", GELB_HELL = "#FFE082",
TEAL = "#0D6E6E", TEAL_HELL = "#E8F4F4",
LINIE = "#E5E5E5", KARTE = "#F6F8FA";
function farbenNachziehen() {
const d = variant === "dunkel";
TINTE = d ? "#F2F6F5" : "#0E1F1E";
GELB = "#F1B729";
TEAL = d ? "#3FA9A9" : "#0D6E6E";
TEXT2 = d ? "#9FB2B0" : "#5B6D6B";
LINIE = d ? "rgba(242,246,245,.18)" : "#DCE4E3";
KARTE = d ? "rgba(242,246,245,.05)" : "#FFFFFF";
TINTE = d ? "#FFFFFF" : "#1A1A1A";
TEXT2 = d ? "#C8D0DC" : "#555555";
TEXT3 = d ? "#8C97A8" : "#888888";
TEAL = d ? "#4FBEBE" : "#0D6E6E";
TEAL_HELL = d ? "rgba(79,190,190,.16)" : "#E8F4F4";
LINIE = d ? "rgba(255,255,255,.14)" : "#E5E5E5";
KARTE = d ? "#1A2740" : "#F6F8FA";
}
/* Der Chamfer der Marke: 16 px Schnitt, wie in lib/chamfer.ts der Website.
Dort steht auch, warum eine chamferte Flaeche keinen Rand bekommt --
clip-path beschneidet ihn mit. Also fuellen statt umranden. */
const CHAMFER_C = 16;
const chamferFlaeche = (x, y, w, h, fuell, c) =>
'<path d="' + chamfer(x, y, w, h, c == null ? CHAMFER_C : c) + '" fill="' + fuell + '"/>';
const zug = (d, o) => {
o = o || {};
return '<path d="' + d + '" fill="none" stroke="' + (o.f || TINTE) +
@@ -2070,7 +2085,7 @@
{ t: "Marketing", g: (x, y) => formMegafon(x, y, 0.86) },
];
const oben = (H - (punkte.length * KH + (punkte.length - 1) * LU)) / 2;
const kachelGrund = variant === "dunkel" ? "rgba(63,169,169,.16)" : "#E7F1F0";
const kachelGrund = TEAL_HELL;
let raus = "";
punkte.forEach((p, i) => {
@@ -2080,15 +2095,13 @@
if (i < punkte.length - 1) {
const vy = y + KH, vm = vy + LU / 2;
raus += zug(strich(mx, vy, mx, vy + LU), { f: LINIE, w: 2 });
raus += '<circle cx="' + mx + '" cy="' + n1(vm) + '" r="10" fill="' + KARTE +
'" stroke="' + LINIE + '" stroke-width="2"/>';
raus += '<circle cx="' + mx + '" cy="' + n1(vm) + '" r="10" fill="' + KARTE + '"/>';
raus += zug(strich(mx - 5, vm, mx + 5, vm) + strich(mx, vm - 5, mx, vm + 5),
{ f: TEAL, w: 2 });
}
raus += '<path d="' + chamfer(KX, y, KB, KH, 14) + '" fill="' + KARTE +
'" stroke="' + LINIE + '" stroke-width="2"/>';
raus += '<path d="' + chamfer(KX + 28, y + 26, 64, 64, 10) + '" fill="' + kachelGrund + '"/>';
raus += chamferFlaeche(KX, y, KB, KH, KARTE);
raus += chamferFlaeche(KX + 28, y + 26, 64, 64, kachelGrund, 10);
raus += zug(p.g(KX + 60, y + 58), { f: TEAL, w: 2.6 });
raus += '<text x="' + (KX + 118) + '" y="' + n1(my + 12) + '" ' +
'font-family="Manrope, sans-serif" font-size="34" font-weight="700" ' +
@@ -2109,41 +2122,99 @@
Uebernahme der Sektion "So geht\'s weiter" von der Website, in derselben
Handschrift wie Slide 5. Drei Schritte an einer Leitung, Wabe statt Kreis,
der dritte Schritt blass: er hat noch nicht stattgefunden. */
/* ─── Slide 6 · So geht es weiter ────────────────────────────────────────
Nachbau der Sektion WaitlistNext.tsx von der Website, nicht eine
entschraffierte Zeichnung. Alle Werte kommen von dort und sind mit 1,6
hochskaliert (die Liste ist dort 560 breit, hier 896):
Wabe 54 x 48 -> 86 x 77, flat-top wie im clip-path
Schiene links 26 -> 43, also die Wabenmitte, 2 px -> 3
Abstand 56 -> 90
Nummer 13 px -> 21, Sperrung 0,14em
Titel 20 px -> 32
Text 14,5 px -> 23
Auf der Website leuchten die Waben nacheinander auf, waehrend man
scrollt; der dritte Schritt ist dort blass, weil er noch nicht erreicht
ist. Ein Standbild kennt keinen Scrollstand, deshalb sind hier alle drei
aktiv. */
function visWarteliste() {
keim(20260814);
const W = 896, H = 730, HX = 66, R = 54, TX = 176, HOEH = R * 0.866;
const W = 896, H = 600;
const HB = 86, HH = 77, HX = 0, TX = 120, ABSTAND = 84;
const schritte = [
{ y: 112, titel: "Du trägst dich ein", ikon: ikonBrief,
zeilen: ["Eine E-Mail genügt. Kein Vertrag,", "keine Kreditkarte, unverbindlich."] },
{ y: 352, titel: "Wir melden uns", ikon: ikonBlaseKlein,
zeilen: ["Sobald HiveGrid für deinen Betrieb", "bereit ist, persönlich und ohne Spam."] },
{ y: 592, titel: "Du bist von Anfang an dabei", ikon: ikonKonfetti,
zeilen: ["Unter den Ersten, die HiveGrid", "nutzen und mitgestalten."] },
{ n: "01", t: "Du trägst dich ein", ikon: "brief",
z: ["Eine E-Mail genügt. Kein Vertrag,", "keine Kreditkarte, unverbindlich."] },
{ n: "02", t: "Wir melden uns", ikon: "chat",
z: ["Sobald HiveGrid für deinen Betrieb", "bereit ist, persönlich und ohne Spam."] },
{ n: "03", t: "Du bist von Anfang an dabei", ikon: "konfetti",
z: ["Unter den Ersten, die HiveGrid", "nutzen und mitgestalten."] },
];
const txt = (x, y, groesse, gewicht, farbe, sperr, inhalt) =>
'<text x="' + x + '" y="' + y + '" font-family="Manrope, sans-serif" font-size="' +
groesse + '" font-weight="' + gewicht + '" letter-spacing="' + sperr +
'" fill="' + farbe + '">' + inhalt + '</text>';
const zeilenHoehe = 37, blockHoehe = 142;
const oben = (H - (schritte.length * blockHoehe + (schritte.length - 1) * ABSTAND)) / 2;
// Flat-top-Wabe in den Proportionen des clip-path der Website.
const wabeForm = (x, y) =>
"M" + n1(x + HB * .25) + "," + y + "H" + n1(x + HB * .75) +
"L" + n1(x + HB) + "," + n1(y + HH / 2) +
"L" + n1(x + HB * .75) + "," + n1(y + HH) +
"H" + n1(x + HB * .25) + "L" + x + "," + n1(y + HH / 2) + "Z";
const zeichen = {
brief: (cx, cy) => {
const w = 36, h = 26, x = cx - w / 2, y = cy - h / 2;
return '<rect x="' + x + '" y="' + y + '" width="' + w + '" height="' + h +
'" rx="4" fill="none" stroke="#171106" stroke-width="2.6"/>' +
'<path d="M' + (x + 2) + ',' + (y + 4) + 'L' + cx + ',' + (y + h * .6) +
'L' + (x + w - 2) + ',' + (y + 4) + '" fill="none" stroke="#171106" ' +
'stroke-width="2.6" stroke-linejoin="round" stroke-linecap="round"/>';
},
chat: (cx, cy) => {
const w = 36, h = 28, x = cx - w / 2, y = cy - h / 2 - 3;
return '<rect x="' + x + '" y="' + y + '" width="' + w + '" height="' + h +
'" rx="9" fill="none" stroke="#171106" stroke-width="2.6"/>' +
'<path d="M' + (x + 9) + ',' + (y + h) + 'l-2,9l11,-9z" fill="#171106"/>' +
[-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;
},
};
let raus = "";
const mitte = i => oben + i * (blockHoehe + ABSTAND) + HH / 2;
// Von Wabe zu Wabe ein Pfeil. Er endet kurz vor der naechsten Wabe, sonst
// laeuft die Spitze in die Kante und ist nicht mehr als Spitze zu lesen.
let pfeile = "";
for (let i = 0; i < schritte.length - 1; i++) {
pfeile += pfeil(HX, schritte[i].y + HOEH + 8, HX, schritte[i + 1].y - HOEH - 9);
}
raus += zug(pfeile, { f: GELB, w: 3.8 });
// Die Schiene liegt hinter den Waben und verbindet ihre Mitten.
raus += '<rect x="' + n1(HB / 2 - 1.5) + '" y="' + n1(mitte(0)) + '" width="3" height="' +
n1(mitte(schritte.length - 1) - mitte(0)) + '" rx="1.5" fill="' + GELB + '"/>';
schritte.forEach(sc => {
raus += flaeche(wabeEcken(HX, sc.y, R), GELB, .16);
raus += zug(wabe(HX, sc.y, R), { f: GELB, w: 4 });
raus += sc.ikon(HX, sc.y, TINTE);
raus += txt(TX, sc.y - 26, 40, 800, TINTE, "-0.02em", sc.titel);
sc.zeilen.forEach((z, k) => {
raus += txt(TX, sc.y + 24 + k * 34, 26, 500, TEXT2, "0", z);
schritte.forEach((sc, i) => {
const y = oben + i * (blockHoehe + ABSTAND);
raus += '<path d="' + wabeForm(HX, y) + '" fill="' + GELB + '"/>';
raus += zeichen[sc.ikon](HX + HB / 2, y + HH / 2);
raus += '<text x="' + TX + '" y="' + n1(y + 42) + '" font-family="Manrope, sans-serif" ' +
'font-size="21" font-weight="800" letter-spacing="0.14em" fill="' + GELB_DUNKEL + '">' +
sc.n + '</text>';
raus += '<text x="' + (TX + 52) + '" y="' + n1(y + 42) + '" font-family="Manrope, sans-serif" ' +
'font-size="32" font-weight="700" letter-spacing="-0.015em" fill="' + TINTE + '">' +
sc.t + '</text>';
sc.z.forEach((z, k) => {
raus += '<text x="' + TX + '" y="' + n1(y + 94 + k * zeilenHoehe) + '" ' +
'font-family="Manrope, sans-serif" font-size="23" font-weight="500" fill="' +
TEXT2 + '">' + z + '</text>';
});
});
@@ -2151,66 +2222,88 @@
'" xmlns="http://www.w3.org/2000/svg">' + raus + '</svg>';
}
/* ─── Slide 5 · Die Loesung ──────────────────────────────────────────────
Nachbau des Architektur-Diagramms aus dem Pitch-Paket (Screen 0), nicht
eine entschraffierte Wabe. Dort sitzt der Kern als gefuellter Kreis in
der Mitte, ein farbiger Ring liegt darum und traegt oben eine Pille mit
dem Produktnamen, und die andockenden Module stehen als Kacheln aussen.
Das Diagramm sagt genau das, was die Ueberschrift sagt: der Kern bleibt,
der Ring kommt drumherum. Meine Wabe hat dieselbe Aussage transportiert,
aber in einer Form, die es in unserer Software nirgends gibt. */
function visVerbinden() {
keim(20260812);
/* Die Flaeche beginnt an der linken Slide-Kante statt bei 92, damit der
Pfeil von Slide 4 dort ankommt. Die Zeichnung rueckt um dieselben 92 px
nach rechts, steht also unveraendert auf der Slide. */
const W = 988, H = 730, cx = 540, cy = 350, R = 160;
const RX = 352, RY = 300;
const W = 988, H = 730, CX = 494, CY = 368;
const KERN = 104, RING = 202, SATX = 344, SATY = 262, KACHEL = 88;
// Die Winkel sind nicht frei gewaehlt: sie sind die sechs Eckwinkel der
// Wabe. Dadurch sitzt jeder Knoten auf einer Ecke.
const sats = [
{ a: 240, name: "KI & MCP", ikon: ikonKi, vx: -46 },
{ a: 300, name: "Marketing", ikon: ikonMegafon, vx: 46 },
{ a: 0, name: "CRM", ikon: ikonCrm },
{ a: 60, name: "Vertrieb", ikon: ikonTrichter },
{ a: 120, name: "Automatisierung", ikon: ikonAuto },
{ a: 180, name: "Workflow-Engine", ikon: ikonWorkflow },
const module = [
{ a: 240, t: "KI & MCP", ikon: (x, y) => formWorkflow(x, y, 0) },
{ a: 300, t: "Marketing", ikon: (x, y) => formMegafon(x, y, 1.0) },
{ a: 0, t: "CRM", ikon: (x, y) => glyLeute(x, y) },
{ a: 60, t: "Vertrieb", ikon: (x, y) => formTrichter(x, y, 1.0) },
{ a: 120, t: "Automatisierung", ikon: (x, y) => glyZahnrad(x, y) },
{ a: 180, t: "Workflow-Engine", ikon: (x, y) => formWorkflow(x, y, 1.0) },
];
// KI hat kein eigenes Zeichen im Satz, deshalb der Funke von Slide 5 alt.
module[0].ikon = (x, y) => funkeForm(x, y, 20);
let speichen = "", knoten = "", bilder = "", schrift = "";
let raus = "";
sats.forEach(s => {
const r = s.a * Math.PI / 180;
const sx = cx + Math.cos(r) * RX, sy = cy + Math.sin(r) * RY;
const ix = cx + Math.cos(r) * R, iy = cy + Math.sin(r) * R; // Ecke der Wabe
const vx = sx - ix, vy = sy - iy, vl = Math.hypot(vx, vy) || 1;
// 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;
const ax = sx - ux * (s.ikon.rad + 20), ay = sy - uy * (s.ikon.rad + 20);
speichen += strich(ix + ux * 12, iy + uy * 12, ax - ux * 11, ay - uy * 11, { j: 1.2 });
knoten += kringel(ix, iy, 10.5, 10.5, { ueber: .1 }) + kringel(ax, ay, 10.5, 10.5, { ueber: .1 });
bilder += s.ikon(sx, sy);
schrift += '<text x="' + n1(sx + (s.vx || 0)) + '" y="' + n1(sy + s.ikon.unten) + '" text-anchor="middle" ' +
'font-family="Manrope, sans-serif" font-size="28" font-weight="700" ' +
'letter-spacing="-0.01em" fill="' + TINTE + '">' + s.name + '</text>';
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 += chamferFlaeche(sx - KACHEL / 2, sy - KACHEL / 2, KACHEL, KACHEL, TEAL_HELL, 12);
raus += zug(m.ikon(sx, sy), { f: TEAL, w: 2.6 });
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>';
});
// Die Wabe wird zweimal gezogen. Ein zweiter Umriss auf groesserem Radius
// sah nach Versehen aus, derselbe Radius nach zweitem Zug.
// Breite und Hoehe stehen als Attribute mit dran, nicht nur im viewBox.
// Ohne sie hat Chrome die Hoehe nicht aus dem Seitenverhaeltnis abgeleitet,
// das SVG wurde zu hoch und die Zeichnung lief in den Fliesstext.
/* Die Fortsetzung des Pfeils von Slide 4, auf derselben Hoehe: der vordere
Teil des Schafts und die Spitze. */
/* Die Fuellung folgt der Form: im Schaft konstant, in der Spitze laeuft
der Ausschlag auf null zu. */
const zHalb = x => x <= 104 ? 14 : Math.max(2, 40 * (1 - (x - 104) / 68));
const zulauf =
flaeche([[0, 157], [104, 157], [104, 130], [172, 175], [104, 220],
[104, 193], [0, 193]], TINTE);
// 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 = 210, PH = 54, PX = CX - PB / 2, PY = CY - RING - PH / 2;
raus += chamferFlaeche(PX, PY, PB, PH, GELB, 12);
raus += '<text x="' + CX + '" y="' + n1(PY + 36) + '" text-anchor="middle" ' +
'font-family="Manrope, sans-serif" font-size="25" font-weight="800" ' +
'letter-spacing="0.1em" fill="' + GELB_DUNKEL + '">HIVEGRID</text>';
return '<svg viewBox="0 0 ' + W + ' ' + H + '" width="' + W + '" height="' + H +
'" xmlns="http://www.w3.org/2000/svg">' +
zulauf +
bilder + schrift +
zug(speichen, { f: GELB, w: 4.4 }) +
zug(wabe(cx, cy, R), { f: GELB, w: 4.8 }) +
zug(wabe(cx, cy, R - 2), { f: GELB, w: 3.2, op: .72 }) +
zug(knoten, { f: GELB, w: 3.8 }) +
nabe(cx, cy) +
'</svg>';
'" xmlns="http://www.w3.org/2000/svg">' + 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);
}
const VIS = {
@@ -2285,7 +2378,7 @@
Schriftgröße steuerbar (Bilder, Geräte). */
const STANDARD = {
"badge": { label: "Tag", size: 26, prop: "font-size" },
"headline": { label: "Überschrift", size: 84, prop: "font-size" },
"headline": { label: "Überschrift", size: 76, prop: "font-size" },
"body": { label: "Beschreibung", size: 30, prop: "font-size" },
"wabe-tagline": { label: "Tagline", size: 74, prop: "font-size" },
"wabe-claim": { label: "Claim", size: 31, prop: "font-size" },
+1 -1
View File
@@ -25,7 +25,7 @@
"int": "dezent"
}
],
"current": 3,
"current": 2,
"deck": [
{
"ref": "titel",