Post 3: zweite Slide, und eine Komponenten-Library fuers Video

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>
This commit is contained in:
dejanandClaude Opus 5 committed 2026-08-17 16:55:18 +02:00
1 parent a5ec0c565e
commit ceb63fec38
391 files changed
+1950 -52

No files matched your search

+365 -37
View File
@@ -146,8 +146,14 @@ if (location.search.indexOf("nur=streifen") >= 0)
.forget:hover { color: #EB7C6B; }
/* ─── Control panel ──────────────────────────────────────────────────── */
/* max-width statt nur width: bei einem schmalen Fenster kaempfen linkes
Feld, Buehne und rechtes Feld um denselben Platz, und ohne Obergrenze
schiebt sich das rechte aus dem Fenster. Dann ist es nicht mehr
zuzumachen, weil sein Knopf mit hinausrutscht.
overflow statt overflow-y aus demselben Grund: wird das Feld schmal,
muss sein Inhalt waagerecht erreichbar bleiben. */
.panel {
width: 306px; flex: none; height: 100%; overflow-y: auto;
width: 306px; max-width: 42vw; flex: none; height: 100%; overflow: auto;
padding: 28px 22px 40px 28px;
border-right: 1px solid #24262F; background: #17181F;
}
@@ -515,11 +521,26 @@ if (location.search.indexOf("nur=streifen") >= 0)
Ueberschriften von Slide 2, 4 und 5 wuerden dreizeilig. */
/* Groesser und enger als im Theme notebook: die Vorbilder setzen die
Ueberschrift als Blickfang ueber fast die ganze Breite. */
/* white-space: nowrap gehoert zu einer Ueberschrift mit gesetzten <br>:
ohne sie bricht eine zu lange Zeile ein zweites Mal um, und aus zwei
Zeilen werden drei (rules.md 8). Der Preis ist, dass eine zu lange Zeile
jetzt still ueber den Rand laeuft statt umzubrechen -- wer den Text
aendert, muss die Breite nachsehen. */
.headline {
font-size: 76px; font-weight: 800;
letter-spacing: -0.032em; line-height: 1.04;
color: var(--head-color);
color: var(--head-color); white-space: nowrap;
}
/* Hier stand eine eigene Groesse fuer Slide 1: 73 statt 76 px, weil "Du
musst nichts ersetzen." bei 76 px genau 914 px misst und nur 896 zur
Verfuegung stehen. Sie ist wieder raus -- alle Ueberschriften sollen so
gross sein wie in post2-alt.html, im Profilraster stehen die Beitraege
nebeneinander.
Der Preis steht damit fest und ist bewusst in Kauf genommen: die Zeile
ragt 9 px je Seite in die Sicherheitszone aus rules.md 5, sie beginnt
also bei x 83 statt bei 92. Wer den Text laenger macht, vergroessert
diesen Ueberstand -- nachmessen. */
/* Kein Marker. Der Akzent ist im Theme modern ein farbiges Wort mit einem
Balken darunter -- der Marker ist das Erkennungszeichen des Themes
notebook und hat hier nichts verloren.
@@ -1118,9 +1139,13 @@ if (location.search.indexOf("nur=streifen") >= 0)
</main>
<aside class="panel panel-rechts" id="element-panel">
<!-- Der Schliessknopf steht vor der Ueberschrift und nicht dahinter. Am
rechten Rand war er der erste, der verschwand, sobald das Feld nicht
ganz ins Fenster passte -- und damit war das Feld nicht mehr
zuzumachen. Vorn ist er immer da. -->
<div class="el-kopf">
<button class="el-zu" id="el-zu" title="Auswahl aufheben (Esc)">✕</button>
<h1 id="el-titel">Element</h1>
<button class="el-zu" id="el-zu" title="Auswahl aufheben">✕</button>
</div>
<div id="el-inhalt"></div>
</aside>
@@ -1169,13 +1194,42 @@ if (location.search.indexOf("nur=streifen") >= 0)
key: "verbinden", type: "feature",
layouts: [{ id: "standard", label: "Standard" }],
eyebrow: "Haltung",
head: 'Wir ersetzen nichts.<br><span class="a">Wir verbinden.</span>',
body: "Dein Branchenprogramm bleibt, wo es ist. HiveGrid legt sich darum und holt raus, was bisher gefehlt hat.",
/* Direkte Anrede statt Selbstaussage. "Wir ersetzen nichts" stand hier
vorher und fing mit einer Verneinung ueber uns selbst an; jetzt
spricht die erste Zeile den Betrieb an und nimmt ihm die Sorge, und
die zweite sagt, was statt dessen passiert.
25 Zeichen in der ersten Zeile, dicht an der Faustregel von 26 bei
76 px und -0,032em. Am gerenderten Blatt geprueft: die Zeile misst
846 von 896 verfuegbaren px. Wer sie verlaengert, muss nachsehen. */
head: 'Du musst nichts ersetzen.<br><span class="a">HiveGrid verbindet.</span>',
/* Ohne Fliesstext: die Slide traegt die Haltung in einer Zeile, und das
Wabenfeld darunter sagt den Rest. Was hier stand, wiederholte in
Prosa, was das Bild ohnehin zeigt. Slide 2 erklaert dann, wie es
zusammenhaengt -- dort steht der Fliesstext weiterhin. */
body: "",
/* Volle Slide-Breite: das Feld laeuft links und rechts ueber die Kante,
wie auf der Startseite. Kein Logo geraet dabei in den Beschnitt, die
aeusserste Wabe mit Marke sitzt bei x 341 von 540. */
vis: "wabenfeld", visW: 1080, visLeft: 0, visBottom: 92,
},
{
/* Slide 2 beantwortet die Frage, die Slide 1 aufwirft: wenn nichts
ersetzt wird, wie haengt es dann zusammen? Die Ueberschrift greift
das "bleibt" von vorhin auf und fuehrt es weiter, statt es zu
wiederholen. */
key: "aufbau", type: "feature",
layouts: [{ id: "standard", label: "Standard" }],
eyebrow: "Aufbau",
/* "Andocken" ist raus, in beiden Zeilen. "Binden wir an" in der
Ueberschrift knuepft ausserdem an Slide 1 an, wo HiveGrid verbindet
-- derselbe Wortstamm, und die Zeile ist aktiv gefuehrt statt
beschreibend. Im Fliesstext steht deshalb nicht dasselbe Wort
nochmal: zweimal dieselbe Wendung auf einer Slide ist einmal zu viel. */
head: 'Der Kern bleibt.<br><span class="a">Den Rest binden wir an.</span>',
body: "Angebote, Aufträge und Abrechnung laufen weiter wie bisher. Neue Werkzeuge kommen von außen dazu.",
vis: "aufbau", visW: 1080, visLeft: 0, visBottom: 92,
},
];
/* Vorlage für eine selbst angelegte Slide. Sie startet bewusst mit Text
@@ -1219,15 +1273,86 @@ if (location.search.indexOf("nur=streifen") >= 0)
const n1 = v => Math.round(v * 10) / 10;
/* ─── Bausteine ────────────────────────────────────────────────────────── */
/* Die Zeichenfarben. Ein Standbild aus Waben braucht davon zwei: den
Markenton, der in beiden Varianten derselbe ist, und den Grund der Slide,
denn die Aussparung in der Nabe zeigt ihn. Die uebrige Palette des Themes
steht in rules.md Paragraf 2 und ausgeschrieben in post2-alt.html; hier
stuende sie nur herum. Werte aus app/globals.css der Website. */
/* Die Zeichenfarben des Themes modern, Werte aus app/globals.css der
Website und aufgelistet in rules.md Paragraf 2. SLIDE_GRUND ist keiner
davon, sondern der Grund der Slide selbst: die Aussparung in der Nabe des
Wabenfelds zeigt ihn, und auf Dunkel ist er ein anderer. */
const GELB = "#F1B729"; // --accent-yellow
let TINTE = "#1A1A1A", TEXT2 = "#555555", TEAL = "#0D6E6E",
KANTE = "#D5DBE1", KARTE = "#FFFFFF", GRUND = "#F6F8FA";
let SLIDE_GRUND = "#FFFFFF"; // --surface, auf Dunkel --dark-bg
function farbenNachziehen() {
SLIDE_GRUND = variant === "dunkel" ? "#30313D" : "#FFFFFF";
const d = variant === "dunkel";
TINTE = d ? "#FFFFFF" : "#1A1A1A";
TEXT2 = d ? "#C8D0DC" : "#555555";
TEAL = d ? "#4FBEBE" : "#0D6E6E";
KANTE = d ? "rgba(255,255,255,.22)" : "#D5DBE1";
KARTE = d ? "#1A2740" : "#FFFFFF";
GRUND = d ? "#222F49" : "#F6F8FA";
SLIDE_GRUND = d ? "#30313D" : "#FFFFFF";
}
/* 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;
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";
}
/* Weicher Schatten. Fuer das Theme modern ist die Schattenfreiheit aus
rules.md 5 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. */
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 Zeichen kommen aus dem Icon-Satz der Software, nicht aus der Hand:
dieselben Feather-Icons, mit denen die Oberflaeche gebaut ist. Woertlich
aus post2-alt.html uebernommen. */
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>';
}
/* Die G-Marke, Punkte woertlich aus assets/brand/g-mark.svg. Als Pfad und
@@ -1243,6 +1368,163 @@ if (location.search.indexOf("nur=streifen") >= 0)
') scale(' + s.toFixed(5) + ')"><polygon points="' + G_PUNKTE + '" fill="' +
farbe + '"/></g>';
}
/* ─── Der Aufbau ──────────────────────────────────────────────────────────
Nachbau des Schaubilds aus dem Prototyp: drei Schalen von innen nach
aussen. In der Mitte das Branchenprogramm, das bleibt. Darum der Ring
mit dem, was HiveGrid selbst mitbringt. Aussen der gestrichelte Kreis,
an dem fremde Werkzeuge andocken.
Drei Dinge weichen bewusst von der Vorlage ab:
- Die Farbe. Der Prototyp ist magenta, und Magenta ist keine
Markenfarbe (rules.md 2). Der HiveGrid-Ring traegt hier Gelb, wie
schon der Ring auf Slide 5 von Post 2; der Marktplatz aussen liegt in
Teal, der Zweitfarbe des Themes.
- Der Kern bleibt namenlos. Die Vorlage nennt ein bestimmtes Programm,
die Slide davor sagt "Dein Branchenprogramm". Ein Produktname waere
an dieser Stelle ein Bruch und wuerde den Beitrag ausserdem auf
Betriebe verengen, die genau dieses Programm haben.
- Weniger Beschriftung. Die Vorlage hat unter jeder Kachel eine Zeile,
im Feed schrumpft die Slide auf gut 400 px und davon bleibt nichts
Lesbares. Aussen tragen deshalb nur die Marken selbst, ohne Text.
Die Winkel sind so gewaehlt, dass oben in beiden Ringen eine Luecke
bleibt: dort sitzen die Pille mit der G-Marke und der Marktplatz-Chip. */
function visAufbau(j) {
const W = 1080, H = 800, CX = 540, CY = 400;
/* Die Kaesten der Marken melden, damit der Export sie einzeln nachprueft
-- sechs PNG in einem SVG sind hier dieselbe Wette wie im Wabenfeld
(rules.md 7). Der Ausschnitt ist ein anderer als dort, deshalb geht er
mit in die Ablage. */
const boxen = [];
markenBoxen.set(j, { vb: { x: 0, y: 0, w: W, h: H }, boxen });
/* Der Kreis darf wieder weiter nach aussen, seit der Chip oben weg ist:
er war der Grund, warum die Andockstellen enger sitzen mussten. 335
plus die halbe Kachel ergibt 381, der Kasten ist 400 hoch -- 19 Pixel
Luft nach oben und unten. */
const KERN = 100, RING = 208, MARKT = 335;
const KACHEL = 132, MKACHEL = 92;
/* Was HiveGrid selbst mitbringt. Vier und nicht mehr: eine Kachel muss
ihre Beschriftung tragen koennen, und bei sechs auf diesem Radius
stossen sie aneinander. */
/* Der Reihe nach im Uhrzeigersinn, oben links beginnend -- so steht die
Liste auch da, damit Reihenfolge im Code und im Bild dieselbe ist. */
const module = [
{ a: 225, t: "Projekte", i: "columns-3" }, // oben links
{ a: 315, t: "Formulare", i: "clipboard-list" }, // oben rechts
{ a: 45, t: "Workflows", i: "workflow" }, // unten rechts
{ a: 135, t: "Chat", i: "message-square" }, // unten links
];
/* Was aussen andockt. Dieselben Dateien wie im Wabenfeld auf Slide 1,
also auch dieselbe Groessen-Eichung: WABE_MARKE_FELD und
WABE_MARKE_DEKO sind die Grundflaechen der beiden Renderpfade, und
jede Marke bringt ihren Faktor mit. Beides zusammen ergibt bei jeder
dieselbe sichtbare Groesse. Hier wird das Ergebnis nur noch auf die
kleinere Kachel heruntergerechnet. */
const MARKE_ZOOM = MKACHEL * 0.62 / 56.5;
/* Acht Stellen, und zwar um 22,5 Grad gegen die Module versetzt. Auf
denselben Strahlen wie sie ging es nicht: Modul und Andockstelle
liegen dann radial hintereinander, und bei 208 zu 310 Radius stossen
ihre Ecken aneinander. Sichtbar war das als Beruehrung zwischen der
Kachel "Seiten" und der von Teams -- und dieselbe Ueberlappung hat der
Exportpruefung eine Spanne von 37 vorgetaeuscht, wo eine fehlende
Marke 0 ergeben muss. Ein Layoutfehler, der sich als Pruefungsfehler
getarnt hat. Versetzt liegen die Mittelpunkte 142 Pixel auseinander,
noetig sind 126. */
const andocker = [
{ a: 292.5, nr: 18 }, // Bauhow
{ a: 337.5, nr: 9 }, // DATEV
{ a: 22.5, nr: 10 }, // PandaDoc
{ a: 67.5, nr: 13 }, // Google Drive
{ a: 112.5, nr: 26 }, // Teams
{ a: 157.5, nr: 36 }, // Hallo Petra
{ a: 202.5, nr: 22 }, // Onepage
{ a: 247.5, nr: 17 }, // kwp
];
const ort = (a, r) => ({
x: CX + Math.cos(a * Math.PI / 180) * r,
y: CY + Math.sin(a * Math.PI / 180) * r,
});
let raus = "";
/* Der Marktplatz. Gestrichelt, weil er nicht fertig ist und nicht fertig
sein soll: was hier haengt, wechselt. */
raus += '<circle cx="' + CX + '" cy="' + CY + '" r="' + MARKT + '" fill="none" stroke="' +
TEAL + '" stroke-width="2" stroke-opacity="0.45" stroke-dasharray="10 12"/>';
// Der HiveGrid-Ring. Durchgezogen und gelb: das ist das Feste daran.
raus += '<circle cx="' + CX + '" cy="' + CY + '" r="' + RING + '" fill="none" stroke="' +
GELB + '" stroke-width="3"/>';
/* Die Andockstellen. Erst die Leitung nach innen, dann die Kachel --
sonst laeuft die Linie ueber die Flaeche. */
andocker.forEach(d => {
const p = ort(d.a, MARKT);
const ux = (p.x - CX) / MARKT, uy = (p.y - CY) / MARKT;
raus += '<line x1="' + n1(CX + ux * RING) + '" y1="' + n1(CY + uy * RING) +
'" x2="' + n1(p.x - ux * (MKACHEL / 2 + 8)) + '" y2="' + n1(p.y - uy * (MKACHEL / 2 + 8)) +
'" stroke="' + TEAL + '" stroke-width="2" stroke-opacity="0.45"/>';
raus += '<circle cx="' + n1(CX + ux * RING) + '" cy="' + n1(CY + uy * RING) +
'" r="6" fill="' + GELB + '"/>';
});
andocker.forEach(d => {
const p = ort(d.a, MARKT);
const x = p.x - MKACHEL / 2, y = p.y - MKACHEL / 2;
raus += karteFlaeche(x, y, MKACHEL, MKACHEL, 14);
const m = WABE_MARKEN[d.nr];
const quelle = assets[wabeMarkePfad(m)];
if (!quelle) return;
const kante = (m.art === "feld" ? WABE_MARKE_FELD : WABE_MARKE_DEKO) * m.gr * MARKE_ZOOM;
boxen.push({ name: m.datei, x: p.x - kante / 2, y: p.y - kante / 2, kante });
raus += '<image href="' + quelle + '" x="' + n1(p.x - kante / 2) + '" y="' +
n1(p.y - kante / 2) + '" width="' + n1(kante) + '" height="' + n1(kante) +
'" preserveAspectRatio="xMidYMid meet"/>';
});
// Die Module auf dem Ring.
module.forEach(m => {
const p = ort(m.a, RING);
const x = p.x - KACHEL / 2, y = p.y - KACHEL / 2;
raus += karteFlaeche(x, y, KACHEL, KACHEL, 16);
raus += feather(m.i, p.x, p.y - 16, 44, GELB, 2.3);
raus += '<text x="' + n1(p.x) + '" y="' + n1(p.y + 40) + '" text-anchor="middle" ' +
'font-family="Manrope, sans-serif" font-size="24" font-weight="700" ' +
'letter-spacing="-0.01em" fill="' + TINTE + '">' + m.t + '</text>';
});
/* Die Mitte traegt die Marke. Gefuellt und dunkel, damit sie als der eine
feste Punkt liest und nicht als eine Kachel unter vielen; die G-Marke
darin in Gelb.
Achtung, das ist eine andere Aussage als in post2-alt.html: dort liegt
in der Mitte das Branchenprogramm und HiveGrid ist der Ring drumherum,
ausdruecklich. Hier steht HiveGrid selbst in der Mitte, mit den eigenen
Modulen auf dem Ring und den fremden Programmen aussen. Wer die beiden
Bilder nebeneinander legt, sollte das wissen. */
raus += '<circle cx="' + CX + '" cy="' + CY + '" r="' + KERN + '" fill="' +
(variant === "dunkel" ? "#1A2740" : "#1A1A1A") + '"/>';
raus += gMarke(CX, CY - 16, 82, GELB);
/* Die Schnittstelle steht in der Marke selbst und nicht als eigenes
Schild daneben. Zwei Anlaeufe davor: auf der Kante der Mitte wurde es
von ihr angeschnitten, frei darunter klemmte es zwischen den unteren
beiden Kacheln -- links und rechts blieben nur sechs Pixel. Innen ist
Platz, und es sagt an dieser Stelle auch das Richtige: verbunden wird
ueber die Schnittstelle. Gesetzt wie das Wort KERN im Diagramm von
Post 2, damit beide Bilder dieselbe Sprache sprechen. */
raus += '<text x="' + CX + '" y="' + (CY + 62) + '" text-anchor="middle" ' +
'font-family="Manrope, sans-serif" font-size="17" font-weight="700" ' +
'letter-spacing="0.18em" fill="rgba(255,255,255,.55)">API-SYNC</text>';
return '<svg viewBox="0 0 ' + W + ' ' + H + '" width="' + W + '" height="' + H +
'" xmlns="http://www.w3.org/2000/svg">' + SCHATTEN_DEFS + raus + '</svg>';
}
/* ─── Das Wabenfeld ───────────────────────────────────────────────────────
Nachbau von HoneycombSection.tsx aus der Website. Die Geometrie ist von
dort uebernommen und nicht nachgemessen: derselbe Umkreisradius, dieselbe
@@ -1376,8 +1658,8 @@ if (location.search.indexOf("nur=streifen") >= 0)
}
/* 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
(830 statt 1080), das SVG wird also auf 1080 gezogen und alles darin um
den Faktor 1,301 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
@@ -1388,25 +1670,40 @@ if (location.search.indexOf("nur=streifen") >= 0)
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 };
Nach dem Faktor bleiben zur Slide-Kante 96 px und zur naechsten Marke
140 px. Die 96 sind der Anschlag: die Sicherheitszone aus rules.md 5
liegt bei 92, weiter aufziehen geht also nicht mehr, ohne die aeusserste
Wabe hineinzuschieben.
Die Hoehe folgt daraus. 674 mal 1,301 sind 877 px, und die Flaeche sitzt
92 px ueber der Unterkante -- sie beginnt damit bei y 380. Der Textblock
endet bei 363 (gemessen, nicht geschaetzt). Die 17 px dazwischen sind
kein Gedraenge: die obersten 44 px des Kastens sind die Ausblendung und
zeigen nichts. */
const WABE_VB = { x: -415, y: -310, w: 830, h: 674 };
/* 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;
mit 0,05 setzt die Blende bei y -276 und 330 ein, die aeusserste Wabe
reicht bis -268 und 322. Acht Einheiten Luft, mehr nicht. Der Wert haengt
an der Hoehe des Ausschnitts -- wer eines von beiden aendert, muss das
nachrechnen. */
const WABE_MASKE = 0.05;
/* 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();
einzeln nach, siehe markenZonen(). Je Slide und nicht als eine Liste: wer
die Slide kopiert, hat die Zeichnung zweimal, und die zweite haette die
Kaesten der ersten ueberschrieben.
Mitgespeichert wird der Ausschnitt, in dem die Kaesten stehen. Zwei
Zeichnungen tragen Marken und haben verschiedene viewBoxen -- das
Wabenfeld auf Slide 1 und der Aufbau auf Slide 2. Waere der Ausschnitt
fest verdrahtet, misst die Pruefung auf der zweiten Slide an der
falschen Stelle und meldet Marken als fehlend, die da sind. */
const markenBoxen = new Map();
function visWabenfeld(j) {
const boxen = [];
wabenMarkenBoxen.set(j, boxen);
markenBoxen.set(j, { vb: WABE_VB, boxen });
const mitte = wabeZellen[0];
let feld = "", flaechen = "", straenge = "", marken = "";
@@ -1508,6 +1805,7 @@ if (location.search.indexOf("nur=streifen") >= 0)
const VIS = {
wabenfeld: j => visWabenfeld(j),
aufbau: j => visAufbau(j),
};
/* ─── Das Deck ────────────────────────────────────────────────────────────
@@ -2167,9 +2465,14 @@ if (location.search.indexOf("nur=streifen") >= 0)
function contentHtml(s, i) {
if (s.type === "feature") {
/* Ein leerer Fliesstext heisst kein Fliesstext, nicht ein leerer Absatz.
Sonst stuende dort ein <p> mit Zeilenhoehe und Abstand, das nichts
zeigt und trotzdem Platz nimmt -- und im Bedienfeld haenge daran ein
Element, das sich anwaehlen und verschieben laesst, obwohl es keines
gibt. */
return '<p class="badge">' + s.eyebrow + '</p>' +
'<h2 class="headline">' + s.head + '</h2>' +
'<p class="body">' + s.body + '</p>';
(s.body ? '<p class="body">' + s.body + '</p>' : "");
}
const logo = variant === "dunkel" ? assets["./assets/brand/logo-white.svg"] : assets["./assets/brand/logo.svg"];
if (layoutState[i] === "szene") {
@@ -2295,6 +2598,11 @@ if (location.search.indexOf("nur=streifen") >= 0)
const page = document.createElement("div");
page.className = "page";
page.dataset.j = String(j);
/* Der Schluessel der Slide am Knoten. Gebraucht wird er, damit CSS eine
einzelne Slide treffen kann, ohne dass ihre Nummer im Stylesheet
steht -- eine Nummer waere falsch, sobald jemand die Reihenfolge
aendert. */
if (sl.key) page.dataset.key = sl.key;
page.style.left = (j * 1080) + "px";
const content = document.createElement("div");
@@ -2714,20 +3022,21 @@ if (location.search.indexOf("nur=streifen") >= 0)
Fehlt eine Marke, bleibt die reine gelbe Wabe stehen. Das ist eine
vollkommen gleichmaessige Flaeche und damit genau der Fall, auf den
spanneAn() prueft. */
function wabenZonen(el, i) {
function markenZonen(el, i) {
/* Die eigene Seite, nicht die erste im Blatt: eine Slide traegt bei
durchgehendem Carousel alle Seiten, und ein blosses querySelector(".vis")
haette die Zeichnung der ersten gemessen. Bei einem Einzelbild faellt das
nicht auf, bei zwei Slides sofort. */
const v = el.querySelector('.page[data-j="' + i + '"] .vis');
const boxen = wabenMarkenBoxen.get(i) || [];
if (!v || !boxen.length) return [];
const eintrag = markenBoxen.get(i);
if (!v || !eintrag || !eintrag.boxen.length) return [];
const { vb, boxen } = eintrag;
const sr = el.getBoundingClientRect(), vr = v.getBoundingClientRect();
if (!sr.width || !vr.width) return [];
const k = sr.width / 1080; // Bildschirm auf Slide
const s = (vr.width / k) / WABE_VB.w; // Zeichnung auf Slide
const x0 = (vr.left - sr.left) / k - WABE_VB.x * s;
const y0 = (vr.top - sr.top) / k - WABE_VB.y * s;
const k = sr.width / 1080; // Bildschirm auf Slide
const s = (vr.width / k) / vb.w; // Zeichnung auf Slide
const x0 = (vr.left - sr.left) / k - vb.x * s;
const y0 = (vr.top - sr.top) / k - vb.y * s;
/* Ein Punktraster wie in ueberlappPunkte(), keine vier Ecken: spanneAn()
laeuft ueber die Punkte selbst und winkt alles unter zwanzig als "nichts
zu beurteilen" durch. Mit vier Ecken waere die Pruefung stumm gewesen.
@@ -2744,7 +3053,7 @@ if (location.search.indexOf("nur=streifen") >= 0)
for (let a = 0; a <= 16; a++)
for (let c = 0; c <= 16; c++)
pts.push({ x: Math.round(x + g * a / 16), y: Math.round(y + g * c / 16) });
return { name: "wabe " + b.name, pts, rahmen: false, erwartetDunkel: 0 };
return { name: "marke " + b.name, pts, rahmen: false, erwartetDunkel: 0 };
});
}
@@ -2890,7 +3199,7 @@ if (location.search.indexOf("nur=streifen") >= 0)
erwartetDunkel: rahmen ? dunkelAnteilVorlage(bild) : 0,
};
}).filter(Boolean);
ziele.push(...wabenZonen(el, i));
ziele.push(...markenZonen(el, i));
for (let attempt = 1; attempt <= MAX_TRIES; attempt++) {
/* Die Slide entsteht bei Renderqualität k in 1080k × 1350k, und genau
@@ -2969,8 +3278,13 @@ if (location.search.indexOf("nur=streifen") >= 0)
}
show(current);
setStatus("Packe ZIP …");
/* Der Name kommt aus PROJECT und nicht aus einer festen Zeichenkette.
Vorher stand hier "hivegrid-post1-loesung" -- in allen Projektdateien,
weil sie voneinander kopiert wurden. Post 2 und Post 3 haben ihre ZIPs
damit unter dem Namen von Post 1 abgelegt, und im Downloads-Ordner lagen
drei verschiedene Beitraege unter einem Namen. */
const path = await downloadBlob(await zip.generateAsync({ type: "blob" }),
"hivegrid-post1-loesung-" + variant + ".zip");
"hivegrid-" + PROJECT + "-" + variant + ".zip");
setStatus("ZIP gespeichert: " + path.split("/").pop() + ", " + ausgabeGroesse(), "ok");
}
@@ -3649,10 +3963,24 @@ if (location.search.indexOf("nur=streifen") >= 0)
if (c) { put("#f-dx", c.x); put("#f-dy", c.y); }
}
document.getElementById("el-zu").addEventListener("click", () => {
/* Auswahl aufheben, und damit das rechte Feld zu. Zwei Wege, weil einer
zu wenig war: der Knopf setzt voraus, dass man ihn sieht, und genau das
war nicht immer der Fall. Escape geht immer.
Nicht wenn gerade in einem Feld getippt wird -- dort soll Escape die
Eingabe verlassen und nicht die Auswahl wegnehmen. */
function auswahlAufheben() {
if (!picked) return;
picked = null;
deck.querySelectorAll("[data-edit]").forEach(n => n.classList.remove("picked"));
syncEditPanel();
}
document.getElementById("el-zu").addEventListener("click", auswahlAufheben);
document.addEventListener("keydown", e => {
if (e.key !== "Escape") return;
const z = document.activeElement;
if (z && /^(INPUT|TEXTAREA|SELECT)$/.test(z.tagName)) return;
auswahlAufheben();
});
document.getElementById("el-inhalt").addEventListener("click", ev => {