Laptop-Stand uebernommen, Projektstand von hier behalten

Beide Seiten hatten state/post1.json geaendert. Der Code, das neue
Titel-Mockup und die umbenannten Tags kommen vom Laptop; der Projektstand
ist der von hier (Rahmen "rund", Positionen von 23:30). Bewusst so
entschieden -- die Positionsarbeit vom Laptop faellt damit weg.

Der Stand steht noch auf v2. Beim naechsten Laden rechnet ihn das Skript auf
v3 hoch und entfernt dabei den szene-Eintrag, weil das Titel-Mockup ein
anderes Bild mit anderem Format geworden ist.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
dejanandClaude Opus 5 committed 2026-08-06 23:32:40 +02:00
commit 3c0b22aa9a
4 files changed
+63 -125

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1011 KiB

+57 -114
View File
@@ -651,70 +651,13 @@
width: 430px; height: auto;
}
/* ─── Szenen-Mockup ──────────────────────────────────────────────────────
Macbook Air + iPad Pro in Perspektive (Pixeden-PSD, Hintergrund entfernt,
beide Displays ausgeknockt). Anders als beim frontalen Laptop reicht kein
Rechteck: die Displayflächen sind Vierecke. Die Screens werden deshalb
per matrix3d hineinprojiziert. Die Matrizen wurden aus den Eckpunkten der
Smart-Objekte im PSD gerechnet und bilden Design-Pixel auf Bildpixel des
5742 px breiten Originals ab; --sk skaliert von dort auf die Breite in
der Slide und wird beim Anwenden der Größe gesetzt. */
.szene { position: relative; width: 980px; }
.szene-dev { width: 100%; display: block; position: relative; z-index: 2; }
.szene-screen {
position: absolute; top: 0; left: 0; z-index: 1;
overflow: hidden; background: #fff;
transform-origin: 0 0;
}
.szene-mb {
width: 3456px; height: 2234px;
transform: scale(var(--sk, .1707))
matrix3d(0.853327,-0.145151,0,-0.000016242,
-0.025684,1.027061,0,-0.000007324,
0,0,1,0, 1614.15,493.94,0,1);
}
/* Das iPad-Viereck stammt aus der Glasflaeche, nicht aus dem Smart-Object
des PSD: das sass zu hoch und liess unten einen dreimal so breiten
schwarzen Streifen stehen. Die Glasecken sind ueber Ausgleichsgeraden auf
den vier Kanten bestimmt und geschnitten, nicht ueber Extrempunkte: die
lagen auf den Rundungen und ergaben ein zu kleines, schiefes Viereck.
Glas 1514 x 2240 mit Eckradius 95, davon 38 px Rand nach innen, also
Display 1438 x 2164 mit Radius 57. Der Radius ist konzentrisch, aussen
minus Randbreite, damit der schwarze Rand ueberall gleich breit bleibt. */
.szene-ip {
width: 1438px; height: 2164px;
border-radius: 57px; overflow: hidden;
transform: scale(var(--sk, .1707))
matrix3d(0.852061,-0.170328,0,-0.000024605,
0.005789,0.984772,0,0.000001055,
0,0,1,0, 4244.78,536.57,0,1);
}
/* Die Aufnahme fuellt das Display randlos, wie auf einem echten Bildschirm.
Vorher stand sie als kleineres Rechteck an per Hand gemessenen
Prozentwerten darin (Breite 78,877 %, Versatz 2,715 % / 14,645 %,
abgeleitet aus einem angenommenen "groessten vollstaendig sichtbaren
Rechteck" von 2726 x 1682 bei 74/154). Zwei Fehler steckten darin:
Die Konstanten stammten nicht aus der Glasflaeche des Mockups, deshalb
blieben 656 px rechts und 470 px unten leer waehrend die Werkzeugleiste
des Boards oben unter der Deckelblende verschwand. Und Prozentwerte in
translate() beziehen sich auf die Bildkante, nicht auf das Display —
Vorschau und Export loesten sie unterschiedlich auf, die Luecke war im
Export groesser als im Canvas.
object-fit uebernimmt das jetzt: kein Versatz, der auseinanderlaufen
koennte, und die Blende beschneidet von selbst. */
.szene-screen > img {
position: absolute; top: 0; left: 0;
width: 100%; height: 100%;
object-fit: cover; object-position: center;
transform-origin: top left;
}
/* Der Hintergrundton bleibt als Rueckfall stehen, falls eine Aufnahme
einmal nicht ankommt — dann faellt es als Flaeche auf, nicht als Loch. */
.szene-mb { background: #F5F4F4; }
.szene-ip { background: #FAFAFA; }
/* Das Board wird beim Fuellen seitlich beschnitten. Was wegfaellt, soll
rechts wegfallen: dort steht ohnehin das iPad davor. */
.szene-mb > img { object-position: left center; }
/* ─── Laptop mit Board ───────────────────────────────────────────────────
Fertiges Mockup: der Screen ist Teil des Bildes. Kein ausgeknocktes
Display, keine Projektion per matrix3d, kein zweiter Screenshot dahinter.
Damit fallen auch die gemessenen Vierecke und Eckradien weg, die das
vorherige Szenen-Mockup gebraucht hat. */
.szene { position: relative; width: 720px; }
.szene-dev { width: 100%; height: auto; display: block; }
.laptop-missing {
width: 940px; padding: 80px 40px; text-align: center;
@@ -1027,15 +970,15 @@
const SLIDES = [
{
key: "titel", type: "title",
layouts: [{ id: "klassisch", label: "Klassisch" }, { id: "wabe", label: "Wabe" }, { id: "laptop", label: "Laptop" }, { id: "laptop-logo", label: "Laptop · Logo" }, { id: "laptop-blur", label: "Laptop · Blur" }, { id: "laptop-footer", label: "Laptop · Logo unten" }, { id: "szene", label: "Szene" }],
layouts: [{ id: "klassisch", label: "Klassisch" }, { id: "wabe", label: "Wabe" }, { id: "laptop", label: "Laptop" }, { id: "laptop-logo", label: "Laptop · Logo" }, { id: "laptop-blur", label: "Laptop · Blur" }, { id: "laptop-footer", label: "Laptop · Logo unten" }, { id: "szene", label: "Laptop · Board" }],
head: 'Vom <span class="a">Handwerker</span><br>f\u00fcr Handwerker',
sub: "Die Software, die deine Branchenprogramme nicht ersetzt, sondern verbindet.",
},
{
key: "projektmanagement", type: "feature",
layouts: [{ id: "standard", label: "Standard" }],
eyebrow: "Projektmanagement",
head: 'Du siehst auf einen Blick,<br><span class="a">wo jeder Auftrag steht.</span>',
eyebrow: "Vertrieb",
head: 'Du siehst auf einen Blick,<br><span class="a">wo alle Auftr\u00e4ge stehen.</span>',
body: "Projekte, Kunden, Angebote, F\u00f6rderantr\u00e4ge. Kein Suchen, kein Nachfragen, kein Zettelchaos.",
img: "./assets/screens/pipelines.png",
// Nur die linke Bildhälfte: Zoom 2 bei Versatz 0 zeigt exakt den Anteil
@@ -1052,7 +995,7 @@
{
key: "workflow-engine", type: "feature",
layouts: [{ id: "standard", label: "Standard" }],
eyebrow: "Workflow-Engine",
eyebrow: "Automation & KI",
head: '<span class="a">Wenn-dann</span><br>statt Handarbeit.',
body: "Auslöser wählen, Aktionen dranhängen, fertig. Alles automatisch und ohne eine Zeile Code.",
img: "./assets/screens/workflow.png",
@@ -1073,15 +1016,18 @@
{
key: "chat", type: "feature",
layouts: [{ id: "standard", label: "Standard" }],
eyebrow: "Team-Chat",
// "automatisch seinen Kanal." misst mit Marker 933 px bei 896
// verfuegbaren und bricht deshalb in eine dritte Zeile. "von selbst"
// sagt dasselbe und bleibt mit 846 px in der Zeile.
head: 'Jedes Projekt bekommt<br>von selbst <span class="a">seinen Kanal.</span>',
eyebrow: "Chat",
// Zweizeilig, aber knapp: Zeile 2 misst mit Marker 880 px bei 896
// verfuegbaren. Ein laengeres Wort bricht sofort um.
head: 'Projekte bekommen<br>automatisch <span class="a">ihren Kanal.</span>',
body: "Foto von der Baustelle rein, alle sehen's sofort. Aus jeder Nachricht wird mit einem Klick eine Aufgabe.",
img: "./assets/screens/chat.png",
mockW: 1030, mockAR: 1.703,
imgScale: 1, imgX: "0%", imgY: "0%",
// Das hinterste Blatt zeigt sonst den Form Builder bei 3,8-fachem Zoom:
// unscharf, und die Kopieren- und Papierkorb-Symbole gehören zu einem
// anderen Feature. Stattdessen der Chat im Hochformat, ungezoomt.
back: { img: "./assets/screens/chat-tablet.png", imgScale: 1, imgX: "0%", imgY: "0%" },
// Das Anhang-Menü zeigt, was aus einer Nachricht alles werden kann.
media2: {
img: "./assets/screens/chat-menue.png",
@@ -1091,7 +1037,7 @@
{
key: "form-builder", type: "feature",
layouts: [{ id: "standard", label: "Standard" }],
eyebrow: "Form Builder",
eyebrow: "Formulare",
head: 'Formulare selbst bauen.<br><span class="a">Per Drag-and-drop.</span>',
body: "Von der Kontaktanfrage bis zum Aufmaß mit Unterschrift vor Ort. Teil sie per Link oder QR-Code, jede Einsendung wird automatisch zu Lead, Kontakt oder Auftrag.",
img: "./assets/screens/form-builder.png",
@@ -1115,7 +1061,7 @@
"logo-full": { label: "Logo", size: 560, prop: "width" },
"screen-logo": { label: "Logo im Screen", size: 430, prop: "width" },
"laptop": { label: "Laptop", size: 960, prop: "width" },
"szene": { label: "Szene", size: 980, prop: "width" },
"szene": { label: "Laptop · Board", size: 720, prop: "width" },
"mock": { label: "Bild", size: null, prop: "width" },
"mock-2": { label: "Zweites Bild", size: null, prop: "width" },
"note": { label: "Notizzettel", size: null, prop: "width" },
@@ -1158,7 +1104,7 @@
const STORE = "hivegrid.projekt." + PROJECT;
const STATE_URL = "./state/" + PROJECT + ".json";
const STATE_V = 2;
const STATE_V = 3;
function currentState() {
return {
@@ -1175,16 +1121,25 @@
der seinen Stand im Speicher und schreibt jede Änderung an der Datei beim
nächsten Klick wieder zu. Im Ladepfad kann er das nicht. */
function migrateState(d) {
if (!d || (d.v || 1) >= 2) return false;
const feat = SLIDES.map((s, i) => (s.type === "feature" ? i : -1)).filter(i => i >= 0);
const ref = feat.length && Array.isArray(d.edits) ? (d.edits[feat[0]] || {}).badge : null;
if (ref) {
for (const i of feat.slice(1)) {
const e = d.edits[i] || (d.edits[i] = {});
e.badge = Object.assign({}, e.badge, { dx: ref.dx || 0, dy: ref.dy || 0 });
if (!d || (d.v || 1) >= STATE_V) return false;
const alt = d.v || 1;
if (alt < 2) {
const feat = SLIDES.map((s, i) => (s.type === "feature" ? i : -1)).filter(i => i >= 0);
const ref = feat.length && Array.isArray(d.edits) ? (d.edits[feat[0]] || {}).badge : null;
if (ref) {
for (const i of feat.slice(1)) {
const e = d.edits[i] || (d.edits[i] = {});
e.badge = Object.assign({}, e.badge, { dx: ref.dx || 0, dy: ref.dy || 0 });
}
}
}
d.v = 2;
// Stand 3: Das Titel-Mockup ist ein anderes Bild mit anderem Format
// geworden. Breite und Versatz von vorher passen dazu nicht mehr und
// wuerden das Geraet ueber die Slide hinaus schieben.
if (alt < 3 && Array.isArray(d.edits)) {
for (const e of d.edits) { if (e) delete e.szene; }
}
d.v = STATE_V;
return true;
}
@@ -1426,16 +1381,10 @@
}
const logo = variant === "dunkel" ? assets["./assets/brand/logo-white.svg"] : assets["./assets/brand/logo.svg"];
if (layoutState[i] === "szene") {
const dev = assets["./assets/mockups/macbook-szene.png"]
? '<div class="szene">' +
'<div class="szene-screen szene-mb"><img alt="" src="' +
assets["./assets/screens/pipelines.png"] + '"></div>' +
'<div class="szene-screen szene-ip"><img alt="" src="' +
assets["./assets/screens/chat-tablet.png"] + '"></div>' +
'<img class="szene-dev" alt="" src="' +
assets["./assets/mockups/macbook-szene.png"] + '">' +
'</div>'
: '<div class="laptop-missing">Szenen-Mockup fehlt.<br>Das freigestellte PNG als <b>assets/mockups/macbook-szene.png</b> ablegen.</div>';
const dev = assets["./assets/mockups/macbook-board.png"]
? '<div class="szene"><img class="szene-dev" alt="" src="' +
assets["./assets/mockups/macbook-board.png"] + '"></div>'
: '<div class="laptop-missing">Mockup fehlt.<br>Das freigestellte PNG als <b>assets/mockups/macbook-board.png</b> ablegen.</div>';
return '<div class="title-top">' +
'<p class="wabe-tagline">Vom Handwerker<br>für <span class="a">Handwerker</span></p>' +
'<img class="wabe-logo" alt="HiveGrid" src="' + logo + '">' +
@@ -1527,20 +1476,8 @@
e.size ? e.size + "px" : "";
}
});
// Die Perspektiv-Matrizen der Szene rechnen in Bildpixeln des Originals.
// Der Faktor auf die Breite in der Slide haengt also an der Geraetebreite
// und muss jede Groessenaenderung mitmachen.
const sz = slideEl.querySelector(".szene");
if (sz) {
const w = (set["szene"] || {}).size || STANDARD["szene"].size;
sz.style.setProperty("--sk", w / SZENE_PX);
}
}
// Breite des freigestellten Mockups im Originalmassstab. Die matrix3d-Werte
// sind darauf bezogen, deshalb steht die Zahl hier und nicht im CSS.
const SZENE_PX = 5742;
// Eine Seite = der Inhalt genau einer Slide, unabhängig davon, in welcher
// Slide sie gerade mitgerendert wird.
function buildPage(j) {
@@ -1560,9 +1497,14 @@
if (sl.mockW) mock.style.setProperty("--mock-w", sl.mockW + "px");
if (sl.mockAR) mock.style.setProperty("--mock-ar", sl.mockAR);
if (sl.mockLeft) mock.style.setProperty("--mock-left", sl.mockLeft + "px");
// Hinterstes Blatt bekommt den Screen der nächsten Feature-Slide.
/* Hinterstes Blatt bekommt standardmässig den Screen der nächsten
Feature-Slide. Das kann danebengehen: der nächste Screen ist
womöglich stark gezoomt und damit unscharf, und er zeigt Bedienung,
die zum Feature dieser Slide gar nicht passt. Deshalb ist er über
"back" pro Slide überschreibbar. */
const withImg = SLIDES.filter(x => x.img);
const other = withImg[(withImg.findIndex(x => x.img === sl.img) + 1) % withImg.length];
const auto = withImg[(withImg.findIndex(x => x.img === sl.img) + 1) % withImg.length];
const other = sl.back || { img: auto.img, imgScale: auto.imgScale, imgX: auto.imgX, imgY: auto.imgY };
mock.innerHTML =
'<div class="sheet-back"><img alt="" src="' + assets[other.img] +
'" style="transform:scale(' + other.imgScale + ') translate(' +
@@ -1695,11 +1637,12 @@
async function preloadAssets() {
const paths = ["./assets/brand/logo.svg", "./assets/brand/logo-white.svg"]
.concat(SLIDES.filter(s => s.img).map(s => s.img))
.concat(SLIDES.filter(s => s.back).map(s => s.back.img))
.concat(SLIDES.filter(s => s.media2).map(s => s.media2.img));
paths.push("./assets/screens/pipelines.png", "./assets/screens/chat-tablet.png");
// Optional: the layout falls back to a hint if the file is not there yet.
const optional = new Set(["./assets/mockups/laptop.png", "./assets/mockups/macbook-szene.png"]);
paths.push("./assets/mockups/laptop.png", "./assets/mockups/macbook-szene.png");
const optional = new Set(["./assets/mockups/laptop.png", "./assets/mockups/macbook-board.png"]);
paths.push("./assets/mockups/laptop.png", "./assets/mockups/macbook-board.png");
for (const path of [...new Set(paths)]) {
const res = await fetch(path, { cache: "no-store" }).catch(() => null);
if (!res || !res.ok) {
@@ -1851,7 +1794,7 @@
Geräterahmen selbst. Die Rahmen fehlten in dieser Liste, deshalb blieb ein
Export ohne Macbook unbemerkt. */
const SHOT_BOXES =
".mock-inner, .laptop-screen, .hex-shot, .szene-screen, .szene-dev, .laptop-frame";
".mock-inner, .laptop-screen, .hex-shot, .szene-dev, .laptop-frame";
const MAX_TRIES = 5;
// Ab hier gilt eine Fläche als leer. Da nur noch das Innere des Vierecks
// abgetastet wird, ist kein Beiwerk mehr dabei, das die Spanne aufbläht.
@@ -1859,9 +1802,9 @@
/* Die vier Ecken eines Kindes in Slide-Koordinaten.
Vorher stand hier das achsenparallele Umrechteck. Das war der Fehler:
.szene-mb und .szene-ip stehen per matrix3d schräg im Raum, ihr Umrechteck
umfasst Raster, Hintergrund und das Nachbargerät. Darin fand die Prüfung
immer genug Helligkeitsspanne und winkte eine leere Displayfläche durch —
ein gedrehtes oder perspektivisch gestelltes Bild hat ein Umrechteck, das
Raster, Hintergrund und Nachbarelemente mit einschliesst. Darin fand die
Prüfung immer genug Helligkeitsspanne und winkte eine leere Fläche durch —
genau so kam ein Export ohne Screenshots durch die Kontrolle.
getBoxQuads gäbe die echten Ecken, kennt Chrome aber nicht. Also werden
vier 1-px-Marken in die Ecken gesetzt und ausgemessen: die Layout-Engine
+6 -11
View File
@@ -34,11 +34,6 @@
"dx": -40,
"dy": 597
},
"szene": {
"dx": 1,
"dy": -88,
"size": 1020
},
"wabe-claim": {
"dx": 4,
"dy": -891
@@ -67,7 +62,7 @@
},
"mock": {
"dx": 96,
"dy": 18,
"dy": 15,
"size": 706
},
"mock-2": {
@@ -91,7 +86,7 @@
},
"mock": {
"dx": -36,
"dy": -371,
"dy": -357,
"size": 965
},
"mock-2": {
@@ -120,7 +115,7 @@
},
"mock": {
"dx": -20,
"dy": -232,
"dy": -222,
"size": 931
},
"mock-2": {
@@ -143,12 +138,12 @@
},
"mock": {
"dx": -24,
"dy": 0,
"dy": -8,
"size": 584
}
}
],
"frameStyle": "stapel",
"frameStyle": "rund",
"layoutState": [
"szene",
"standard",
@@ -159,7 +154,7 @@
"superSample": 3,
"tagOut": "an",
"tilt": "aus",
"v": 2,
"v": 3,
"variant": "hell",
"view": "streifen"
}