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>
No files matched your search
@@ -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/
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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 });
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 137 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 356 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 7.3 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 7.3 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 35 KiB |