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

+3
View File
@@ -2,3 +2,6 @@
# Zwischenschritt beim PDF-Bau, entsteht neu aus bauen.py
redaktionsplan/*.html
# Wegwerf-Profil des Browsers beim Bau der Video-Komponenten
video/.chrome/
+27 -3
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;
}
@@ -1002,9 +1008,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>
@@ -3042,10 +3052,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 => {
+33 -4
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;
}
@@ -1220,9 +1226,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>
@@ -3921,8 +3931,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");
}
@@ -4601,10 +4616,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 => {
+33 -4
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;
}
@@ -1093,9 +1099,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>
@@ -3542,8 +3552,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");
}
@@ -4222,10 +4237,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 => {
+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 => {
+24 -4
View File
@@ -1,6 +1,10 @@
{
"bandTon": "orange",
"bgState": [
{
"bg": "raster",
"int": "dezent"
},
{
"bg": "raster",
"int": "dezent"
@@ -11,21 +15,37 @@
{
"ref": "verbinden",
"werte": {}
},
{
"ref": "aufbau",
"werte": {}
}
],
"edits": [
{
"body": {
"dx": 33
"headline": {
"dy": 3
},
"vis": {
"dx": -2,
"dy": -67
}
},
{
"body": {},
"headline": {
"dx": 0,
"dy": 0
"dy": 3
},
"vis": {
"dx": 2,
"dy": -93
}
}
],
"frameStyle": "chamfer",
"layoutState": [
"standard",
"standard"
],
"rasterAn": "aus",
@@ -35,5 +55,5 @@
"tilt": "an",
"v": 7,
"variant": "hell",
"view": "streifen"
"view": "einzeln"
}
+152
View File
@@ -0,0 +1,152 @@
# Video-Komponenten
Einblendungen für den Videoschnitt, in derselben Bildsprache wie die Posts:
weiße Karten mit Haarlinie und weichem Schatten, ein gelber Akzent, Manrope.
Gedacht für Noahs Beitrag über den Betrieb, taugt aber für jeden Schnitt.
Jede Komponente liegt zweimal vor — als **Standbild** und als **Bildsequenz**
mit fertiger Ein- und Ausblendung. Alles mit Alphakanal, alles freigestellt:
kein Rand, keine Leinwand, keine Farbe hinter der Karte.
```
video/
├── komponenten.html die Vorlage, hier steht die Gestaltung
├── bauen.mjs rendert daraus die Dateien
├── liste.json was zuletzt gebaut wurde, mit Maßen
├── png/ Standbilder
└── sequenz/ je Komponente ein Ordner "-ein" und einer "-aus"
```
## In Premiere holen
**Standbild:** `png/<name>.png` in die Timeline ziehen. Ein- und Ausblendung
machst du selbst über Position und Deckkraft. Das ist der flexiblere Weg.
**Sequenz:** *Datei → Importieren*, im Ordner `sequenz/<name>-ein/` das **erste**
Bild anklicken und unten **„Bildsequenz"** ankreuzen. Premiere legt daraus
einen einzigen Clip an, nicht 20 Einzelbilder.
> **Eine Bildsequenz hat keine eigene Bildrate.** Die Dateien wissen nichts
> davon, wie schnell sie laufen sollen — Premiere vergibt sie beim Import aus
> *Einstellungen → Medien → Zeitbasis für unbestimmte Medien*. Ab Werk stehen
> dort meist 30 fps. Was das für die Dauer heißt, steht in der Tabelle unten.
> Ändern lässt es sich jederzeit: Rechtsklick auf den Clip im Projektfenster →
> *Ändern → Videomaterial interpretieren → Bildrate annehmen*. Die Bewegung
> wird davon nur schneller oder langsamer, nie ruckelig — es liegt für jedes
> Bild eine eigene Datei vor.
Der übliche Aufbau im Schnitt sind drei Teile hintereinander:
`<name>-ein` → `png/<name>.png` beliebig lang gezogen → `<name>-aus`.
Das letzte Bild von `-ein` und das erste von `-aus` sind das Standbild, die
Stöße sind also unsichtbar. Alle drei Teile haben dieselben Maße und denselben
Bildausschnitt — was du am ersten an Position und Größe einstellst, kannst du
auf die beiden anderen einfügen (*Bearbeiten → Einfügen → Attribute*).
Der Alphakanal kommt mit. Kein Keying, keine Maske, kein Mischmodus: die Karte
liegt auf einer eigenen Spur über dem Bild und der Rest ist durchsichtig.
### Dauer je Komponente
| Komponente | Bilder ein / aus | bei 25 fps | bei 30 fps |
|---|---|---|---|
| `bauchbinde-noah`, `bauchbinde-betrieb` | 20 / 12 | 0,80 s / 0,48 s | 0,67 s / 0,40 s |
| `chip-generation` | 16 / 10 | 0,64 s / 0,40 s | 0,53 s / 0,33 s |
| alle `satz-*` | 18 / 11 | 0,72 s / 0,44 s | 0,60 s / 0,37 s |
| beide `stapel-*` | 22 / 13 | 0,88 s / 0,52 s | 0,73 s / 0,43 s |
| `ring-kern`, `wabenfeld` | 12 / 7 | 0,48 s / 0,28 s | 0,40 s / 0,23 s |
| `logo-bug` | 14 / 8 | 0,56 s / 0,32 s | 0,47 s / 0,27 s |
| `endkarte` | 20 / 12 | 0,80 s / 0,48 s | 0,67 s / 0,40 s |
Mehr Bilder heißt längere Bewegung, nicht flüssigere: die Staffelung steckt in
Anteilen des Verlaufs, nicht in Zeitwerten. Wer eine langsamere Einblendung
will, erhöht `bilder` bei der Komponente in `komponenten.html` und baut neu.
> **Nicht skalieren, wo es nicht sein muss.** Die Dateien sind in doppelter
> Größe gerendert (`FAKTOR` in `bauen.mjs`). In einer Zeitleiste mit
> 1920 × 1080 setzt du sie also auf rund 50 % — dann sitzt jeder Pixel, und
> nach oben ist bis 4K Luft.
## Was es gibt
| Schlüssel | Was es zeigt | Wofür im Skript |
|---|---|---|
| `bauchbinde-noah` | Noah Geerkens · Gründer | wenn er sich vorstellt |
| `bauchbinde-betrieb` | Geerkens Haustechnik · SHK, vierte Generation | dazu oder statt dessen |
| `chip-generation` | gedrehter Stempel „Vierte Generation · SHK" | zum Betrieb |
| `satz-erp-bleibt` | „Dein ERP **bleibt**." | der Einwand, direkt beantwortet |
| `satz-kein-ersatz` | „Kein **Ersatz**." / „Der Aufbau drumherum." | die Kernaussage |
| `satz-miteinander-reden` | „Die Tools reden **miteinander**." | was gefehlt hat |
| `satz-handwerksalltag` | „Kein **Startup**-Projekt." | der Abbinder |
| `stapel-haengt-drin` | Stammdaten · Aufträge · Buchhaltung | was einen Wechsel unmöglich macht |
| `stapel-was-fehlte` | KI-Lösungen · CRM · Automatisierungen | was sie wollten |
| `ring-kern` | der Kern bleibt, HiveGrid liegt darum | „kein Ersatz, sondern der Aufbau drumherum" |
| `wabenfeld` | die Marke in der Mitte, die Programme drumherum | „dass die Tools endlich miteinander reden" |
| `logo-bug` | Wortmarke für die Ecke | durchgehend oder am Anfang |
| `endkarte` | „Vom Handwerker für **Handwerker**" | Schlussbild |
**Wenig Text ist Absicht.** Noah spricht das Skript, die Einblendung setzt das
Stichwort. Zwei Sätze auf einer Karte lesen sich nicht, während jemand redet.
## Texte ändern
Die Texte hängen an der Adresse, nicht am Code. Server starten (`./serve.sh`),
dann zum Beispiel:
```
http://127.0.0.1:4321/video/komponenten.html?k=satz-erp-bleibt&zeile=Dein *CRM* bleibt.
http://127.0.0.1:4321/video/komponenten.html?k=bauchbinde-noah&name=Lena Geerkens&rolle=Geschäftsführung
```
`*Wort*` setzt den gelben Akzent, **höchstens einer je Zeile** (rules.md §4).
Welche Felder eine Komponente kennt, steht bei ihr unter `felder` in
`komponenten.html`. `?alle=1` zeigt alle nebeneinander auf einem Karo-Grund.
Wer einen geänderten Text als Datei braucht, trägt ihn in `komponenten.html`
ein und baut neu — die Adresse wirkt nur im Browser.
## Neu bauen
```sh
./serve.sh # muss laufen, in einem zweiten Terminal
node video/bauen.mjs # alles
node video/bauen.mjs bauchbinde satz # nur passende Schlüssel
```
Ein paar Entscheidungen, die dahinterstehen:
- **Bildsequenz statt Video mit Alphakanal.** Ein Video mit echter
Transparenz wäre ProRes 4444, und dafür braucht es ffmpeg. Das ist hier
nicht installiert, und eine Bildsequenz kann Premiere genauso importieren.
- **Node statt Python**, anders als sonst im Repo. Gebraucht wird eine
Steuerverbindung zu Chrome, und die hat Node ohne Zusatzpaket an Bord.
Chrome pro Einzelbild neu zu starten wäre die Alternative gewesen — bei rund
380 Bildern sind das zehn Minuten Warten für nichts.
- **Der Ausschnitt wird gemessen, nicht angegeben.** `KOMP_KASTEN()` misst
nach dem Rendern, was tatsächlich dasteht, und legt 52 px Zugabe drumherum:
der Schatten liegt außerhalb der Box, und was aufblendet, startet 26 px
tiefer. Ohne die Zugabe schneidet das PNG beides ab.
- **Die Ausblendung ist die Einblendung rückwärts**, nur kürzer. Ein eigener
Ausgang sähe eine Spur besser aus und kostete doppelt so viele Bilder.
## Zwei Fallen, in die wir hier schon getappt sind
**Der Schatten war nicht da.** Die Karte trug `clip-path` für die
abgeschnittenen Ecken und `box-shadow` für den Schatten — und `clip-path`
beschneidet den Schatten mit. Genau so steht es in `rules.md` §3, und genau so
ist es passiert. Am gerenderten PNG fiel die Deckung an der Kartenkante von
255 auf 0, ohne einen einzigen Zwischenwert. Der Schatten sitzt jetzt als
`filter: drop-shadow()` auf einer Hülle. Nebenbei folgt der der Alphamaske und
legt sich um die abgeschnittenen Ecken herum statt um ein Rechteck.
**`inset(0 0 0 0)` ist nicht dasselbe wie kein Beschnitt.** Die Einblendung
zieht die Karte über `clip-path: inset()` von links auf. Am Ende steht dort
rechnerisch null — das schneidet aber weiterhin exakt auf die Box und kappt
damit wieder den Schatten. Bei vollem Verlauf fällt der Beschnitt deshalb ganz
weg.
## Was fehlt
Der Grund, warum die Marken im Wabenfeld sitzen, steht in `rules.md` §3
unter *Wabenfeld*. Wer dort etwas ändert, muss an drei Dateien denken:
`post2-alt.html`, `post3.html` und diese hier. Das ist der Preis dafür, dass
jede Datei für sich lauffähig bleibt.
+199
View File
@@ -0,0 +1,199 @@
#!/usr/bin/env node
/* Rendert komponenten.html zu PNGs mit Alphakanal und zu Bildsequenzen.
*
* node video/bauen.mjs alles neu
* node video/bauen.mjs bauchbinde nur passende Schluessel
*
* Warum Node und nicht Python wie sonst im Repo: gebraucht wird eine
* Steuerverbindung zu Chrome (CDP, ueber WebSocket), und die hat Node ohne
* Zusatzpaket an Bord, Python nicht. Der Umweg ueber "Chrome pro Bild neu
* starten" waere die Alternative gewesen -- bei rund 400 Einzelbildern und
* anderthalb Sekunden Startzeit sind das zehn Minuten Warten fuer nichts.
*
* Es laeuft ueber den Server aus dem Repo und nicht ueber file://, weil die
* Seite Schrift und Marken per fetch holt und das lokale Dateiprotokoll das
* verbietet.
*
* Kein ffmpeg noetig. Ein Video mit echtem Alphakanal waere ProRes 4444 und
* damit ein Encoder, den dieser Rechner nicht hat; eine Bildsequenz kann
* Premiere genauso importieren und behaelt die Transparenz.
*/
import { spawn } from "node:child_process";
import { mkdir, writeFile, readFile, rm } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
const HIER = path.dirname(fileURLToPath(import.meta.url));
const BASIS = "http://127.0.0.1:4321/video/komponenten.html";
const CHROME = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
const PORT = 9333;
/* Aufloesung. Die Komponenten sind fuer eine Zeitleiste in 1920 x 1080
entworfen -- eine Bauchbinde misst dort rund 500 px. Zweifach gerendert
traegt sie damit auch in 4K und haelt eine Vergroesserung im Schnitt aus,
ohne dass die Sequenzen den Ordner sprengen. Standbild und Sequenz haben
denselben Faktor, sonst passen sie im Schnitt nicht uebereinander. */
const FAKTOR = 2;
/* Die Ausblendung ist die Einblendung rueckwaerts, nur kuerzer. Ein eigener
Ausgang saehe eine Spur besser aus und kostete die doppelte Zahl an
Bildern; bei einer halben Sekunde sieht das niemand. */
const AUS_ANTEIL = 0.6;
const filter = process.argv.slice(2);
const passt = key => !filter.length || filter.some(f => key.includes(f));
/* ── CDP, das Noetigste ──────────────────────────────────────────────────── */
async function verbinden(port) {
let ziele = null;
for (let i = 0; i < 50; i++) {
ziele = await fetch(`http://127.0.0.1:${port}/json/list`).then(r => r.json()).catch(() => null);
if (ziele && ziele.some(t => t.type === "page")) break;
await new Promise(r => setTimeout(r, 200));
}
const seite = ziele && ziele.find(t => t.type === "page");
if (!seite) throw new Error("Chrome antwortet nicht auf " + port);
const ws = new WebSocket(seite.webSocketDebuggerUrl);
const offen = new Map();
const horcher = [];
ws.onmessage = e => {
const m = JSON.parse(e.data);
if (m.id && offen.has(m.id)) { offen.get(m.id)(m); offen.delete(m.id); }
else if (m.method) horcher.forEach(h => h(m));
};
await new Promise((ok, weg) => { ws.onopen = ok; ws.onerror = weg; });
let n = 0;
const ruf = (method, params = {}) => new Promise((ok, weg) => {
const id = ++n;
offen.set(id, m => m.error ? weg(new Error(method + ": " + m.error.message)) : ok(m.result));
ws.send(JSON.stringify({ id, method, params }));
});
const einmal = ereignis => new Promise(ok => {
const h = m => { if (m.method === ereignis) { horcher.splice(horcher.indexOf(h), 1); ok(m); } };
horcher.push(h);
});
return { ruf, einmal, zu: () => ws.close() };
}
/* Ein Ausdruck in der Seite. Fehler dort sollen hier ankommen und nicht als
stilles undefined durchrutschen. */
async function werte(cdp, ausdruck) {
const r = await cdp.ruf("Runtime.evaluate", {
expression: ausdruck, awaitPromise: true, returnByValue: true,
});
if (r.exceptionDetails) {
const d = r.exceptionDetails;
throw new Error("in der Seite: " + (d.exception?.description || d.text));
}
return r.result.value;
}
async function aufnehmen(cdp, kasten) {
const r = await cdp.ruf("Page.captureScreenshot", {
format: "png",
captureBeyondViewport: true,
clip: { x: kasten.x, y: kasten.y, width: kasten.w, height: kasten.h, scale: FAKTOR },
});
return Buffer.from(r.data, "base64");
}
/* ── Ablauf ──────────────────────────────────────────────────────────────── */
const chrome = spawn(CHROME, [
"--headless", "--disable-gpu", "--hide-scrollbars",
"--remote-debugging-port=" + PORT,
"--user-data-dir=" + path.join(HIER, ".chrome"),
"about:blank",
], { stdio: "ignore" });
let cdp;
try {
cdp = await verbinden(PORT);
await cdp.ruf("Page.enable");
await cdp.ruf("Runtime.enable");
// Ohne das steht Weiss unter der Seite und jedes PNG waere deckend.
await cdp.ruf("Emulation.setDefaultBackgroundColorOverride",
{ color: { r: 0, g: 0, b: 0, a: 0 } });
const laden = async adresse => {
await cdp.ruf("Page.navigate", { url: adresse });
await cdp.einmal("Page.loadEventFired");
await werte(cdp, "window.KOMP_BEREIT");
};
await laden(BASIS + "?k=");
const liste = (await werte(cdp, "window.KOMP_LISTE")).filter(k => passt(k.key));
if (!liste.length) throw new Error("kein Schluessel passt auf: " + filter.join(", "));
const pngDir = path.join(HIER, "png");
const seqDir = path.join(HIER, "sequenz");
await mkdir(pngDir, { recursive: true });
await mkdir(seqDir, { recursive: true });
let dateien = 0, bytes = 0;
const bericht = [];
for (const k of liste) {
// Die Buehne gross genug stellen, sonst schneidet der Viewport ab, bevor
// ueberhaupt gemessen werden kann.
await cdp.ruf("Emulation.setDeviceMetricsOverride", {
width: k.w, height: k.h, deviceScaleFactor: 1, mobile: false,
});
await laden(`${BASIS}?k=${encodeURIComponent(k.key)}&t=1`);
const kasten = await werte(cdp, "window.KOMP_KASTEN()");
// Standbild: der gehaltene Zustand.
const still = await aufnehmen(cdp, kasten);
await writeFile(path.join(pngDir, k.key + ".png"), still);
dateien++; bytes += still.length;
// Sequenzen. Ein bis der Kasten steht, aus rueckwaerts und knapper.
const einN = k.bilder;
const ausN = Math.max(4, Math.round(k.bilder * AUS_ANTEIL));
for (const [name, anzahl, richtung] of [["ein", einN, 1], ["aus", ausN, -1]]) {
const ordner = path.join(seqDir, k.key + "-" + name);
await rm(ordner, { recursive: true, force: true });
await mkdir(ordner, { recursive: true });
for (let i = 0; i < anzahl; i++) {
const p = i / (anzahl - 1);
await werte(cdp, "window.setT(" + (richtung > 0 ? p : 1 - p) + ")");
const bild = await aufnehmen(cdp, kasten);
await writeFile(path.join(ordner, `${k.key}-${name}_${String(i).padStart(4, "0")}.png`), bild);
dateien++; bytes += bild.length;
}
}
const zeile = `${k.key.padEnd(24)} ${String(kasten.w * FAKTOR).padStart(5)} x ` +
`${String(kasten.h * FAKTOR).padEnd(5)} still + ${einN} ein + ${ausN} aus`;
console.log(" " + zeile);
bericht.push({ key: k.key, name: k.name, w: kasten.w * FAKTOR, h: kasten.h * FAKTOR,
ein: einN, aus: ausN });
}
/* Der Teilbau darf die Liste nicht auf sich selbst eindampfen: wer nur die
Bauchbinde neu baut, will danach trotzdem alle dreizehn darin finden.
Also wird zusammengefuehrt statt ueberschrieben, in der Reihenfolge der
Vorlage -- die bestimmt, was zusammengehoert, nicht der Zufall des
letzten Aufrufs. */
const vorher = JSON.parse(
await readFile(path.join(HIER, "liste.json"), "utf8").catch(() => '{"komponenten":[]}'));
const nachKey = new Map(vorher.komponenten.map(k => [k.key, k]));
bericht.forEach(k => nachKey.set(k.key, k));
const alle = (await werte(cdp, "window.KOMP_LISTE")).map(k => nachKey.get(k.key)).filter(Boolean);
await writeFile(path.join(HIER, "liste.json"),
JSON.stringify({ faktor: FAKTOR, komponenten: alle }, null, 1) + "\n");
console.log(`\n${liste.length} Komponenten, ${dateien} Dateien, ` +
`${(bytes / 1024 / 1024).toFixed(1)} MB`);
} finally {
if (cdp) cdp.zu();
chrome.kill();
/* Erst warten, bis Chrome wirklich weg ist. Ein Kill ist eine Bitte, kein
Vollzug: beim Herunterfahren schreibt der Browser noch in sein Profil,
und ein rm davor liess den Ordner wieder auferstehen. */
await new Promise(ok => chrome.once("exit", ok));
// Das Profil ist Wegwerfware und hat im Repo nichts verloren.
await rm(path.join(HIER, ".chrome"), { recursive: true, force: true });
}
File diff suppressed because it is too large. Load diff
+109
View File
@@ -0,0 +1,109 @@
{
"faktor": 2,
"komponenten": [
{
"key": "bauchbinde-noah",
"name": "Bauchbinde · Noah",
"w": 1202,
"h": 490,
"ein": 20,
"aus": 12
},
{
"key": "bauchbinde-betrieb",
"name": "Bauchbinde · Betrieb",
"w": 1506,
"h": 490,
"ein": 20,
"aus": 12
},
{
"key": "chip-generation",
"name": "Chip · Vierte Generation",
"w": 1220,
"h": 496,
"ein": 16,
"aus": 10
},
{
"key": "satz-erp-bleibt",
"name": "Satz · Dein ERP bleibt",
"w": 1140,
"h": 490,
"ein": 18,
"aus": 11
},
{
"key": "satz-kein-ersatz",
"name": "Satz · Kein Ersatz",
"w": 984,
"h": 588,
"ein": 18,
"aus": 11
},
{
"key": "satz-miteinander-reden",
"name": "Satz · Miteinander reden",
"w": 1722,
"h": 490,
"ein": 18,
"aus": 11
},
{
"key": "satz-handwerksalltag",
"name": "Satz · Handwerksalltag",
"w": 1390,
"h": 588,
"ein": 18,
"aus": 11
},
{
"key": "stapel-haengt-drin",
"name": "Stapel · Was drin haengt",
"w": 858,
"h": 824,
"ein": 22,
"aus": 13
},
{
"key": "stapel-was-fehlte",
"name": "Stapel · Was gefehlt hat",
"w": 1038,
"h": 824,
"ein": 22,
"aus": 13
},
{
"key": "ring-kern",
"name": "Ring · Dein Programm bleibt",
"w": 2184,
"h": 1668,
"ein": 12,
"aus": 7
},
{
"key": "wabenfeld",
"name": "Wabenfeld · Alles verbunden",
"w": 2348,
"h": 2114,
"ein": 12,
"aus": 7
},
{
"key": "logo-bug",
"name": "Logo · Ecke",
"w": 1620,
"h": 380,
"ein": 14,
"aus": 8
},
{
"key": "endkarte",
"name": "Endkarte · Warteliste",
"w": 2274,
"h": 656,
"ein": 20,
"aus": 12
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 356 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Loaded 100 of 391 files, more files were not shown because too many files have changed in this diff. Show more