Notizzettel, keine Schatten, Szenen-Mockup
Drei Aenderungen am Generator, im selben Bereich der Datei verzahnt. Notizzettel: die StickyNote aus dem Pitch-Projekt als HTML nachgebaut, Variante purple, Farbwerte uebernommen und die Masse auf das 1080er Blatt hochskaliert. Liegt auf der Workflow-Slide und ist wie jedes andere Element verschiebbar. Schatten: raus, ueberall. Kein box-shadow, kein drop-shadow. Tiefe entsteht ueber Ueberlappung, Rahmenfarbe und Versatz. Der Umschalter faellt mit weg, er haette nichts mehr zu schalten gehabt. Die Blaetter im Stapel-Rahmen bekommen eine Haarlinie, sonst laege auf der hellen Variante Weiss auf Weiss. Szenen-Mockup: Macbook Air und iPad Pro in Perspektive, aus einem Pixeden-PSD freigestellt. Beide Displays sind ausgeknockt, die Screenshots liegen dahinter und werden per matrix3d in die Flaechen projiziert. Die Eckpunkte sind gemessen, nicht geschaetzt: beim Macbook aus dem transform_box des Smart-Objekts, beim iPad ueber Ausgleichsgeraden auf den vier Glaskanten, weil das Smart-Object dort zu hoch sitzt. Der Spalt zur weggefallenen Glasplatte wird spaltenweise aus den Nachbarfarben interpoliert, damit am Displayrand Schwarz und am Gehaeuse Metallic steht. Das PSD selbst bleibt draussen: Pixeden erlaubt die Nutzung, nicht die Weitergabe. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
cdc979fa71
commit
89434d728e
4 files changed
+423
-85
No files matched your search
@@ -48,7 +48,7 @@ pixelweise schieben (`⇧` = 10 px) und über `A−`/`A+` in der Größe ändern
|
||||
assets/
|
||||
├── brand/ Wortmarke hell/dunkel, G-Marke (SVG + PNG), Wabe, Geerkens
|
||||
├── partner/ DATEV, KWP, Google Calendar, Google Drive, MS Outlook, MS Teams
|
||||
├── mockups/ Geräte-Mockups (Display ausgeknockt, Screenshot liegt dahinter)
|
||||
├── mockups/ Geräte-Mockups (Displays ausgeknockt, Screenshot liegt dahinter)
|
||||
├── screens/ Software-Screenshots
|
||||
└── fonts/ Manrope 400–800, lokal
|
||||
vendor/ html-to-image, JSZip
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1011 KiB |
+284
-82
@@ -348,7 +348,7 @@
|
||||
Textfluss: So verschiebt ein Element nie seine Nachbarn und lässt sich
|
||||
jederzeit exakt auf 0 zurücksetzen. Elemente mit eigener Transformation
|
||||
komponieren die Offsets in ihre bestehende Regel hinein. */
|
||||
[data-edit] { transform: translate(var(--ex, 0px), var(--ey, 0px)); }
|
||||
[data-edit] { transform: translate(var(--ex, 0px), var(--ey, 0px)) rotate(var(--rot, 0deg)); }
|
||||
|
||||
.slide[data-editmode="an"] [data-edit] { cursor: grab; }
|
||||
.slide[data-editmode="an"] [data-edit]:hover { outline: 3px dashed rgba(241,183,41,.75); outline-offset: 6px; }
|
||||
@@ -419,14 +419,14 @@
|
||||
);
|
||||
/* Rahmenform ist umschaltbar. Bei "rund" muss clip-path weichen, sonst
|
||||
gewinnt der Schnitt gegen border-radius; bei "aus" fällt auch Polsterung
|
||||
und Fläche weg, der Schatten bleibt als einzige Tiefe. */
|
||||
/* clip-path also clips box-shadow, so depth has to come from a filter —
|
||||
documented caveat in lib/chamfer.ts. */
|
||||
filter: drop-shadow(0 34px 60px rgba(0,0,0,.30));
|
||||
und Fläche weg.
|
||||
Kein Schatten: die Gestaltung arbeitet flächig, Tiefe entsteht über
|
||||
Überlappung und Rahmenfarbe, nicht über Weichzeichnung. */
|
||||
}
|
||||
.slide[data-tilt="an"] .mock {
|
||||
transform: translate(var(--ex, 0px), var(--ey, 0px))
|
||||
perspective(2600px) rotateY(-7deg) rotateX(2.5deg) rotate(-1.2deg);
|
||||
perspective(2600px) rotateY(-7deg) rotateX(2.5deg)
|
||||
rotate(calc(-1.2deg + var(--rot, 0deg)));
|
||||
}
|
||||
|
||||
.mock-inner {
|
||||
@@ -439,6 +439,57 @@
|
||||
);
|
||||
}
|
||||
|
||||
/* Ein zweites Bild pro Slide. Es ist bewusst wieder ein .mock, damit alle
|
||||
Rahmenformen, Schatten und Perspektiven ohne Sonderregeln greifen; nur
|
||||
Position, Größe und Stapelebene kommen von aussen. */
|
||||
.mock-2 {
|
||||
z-index: var(--m2-z, 2);
|
||||
bottom: var(--m2-bottom, 96px);
|
||||
}
|
||||
.slide[data-frame="stapel"] .mock-2 .sheet-back,
|
||||
.slide[data-frame="stapel"] .mock-2 .sheet-mid { display: none; }
|
||||
|
||||
/* ─── Notizzettel ────────────────────────────────────────────────────────
|
||||
Nachbau der StickyNote aus dem Pitch-Projekt
|
||||
(ui/components/StickyNote/StickyNote.tsx), Variante "purple". Farbwerte
|
||||
und Aufbau sind von dort übernommen, die Maße hochskaliert: die Vorlage
|
||||
ist 200 px breit bei 14 px Text, hier liegt sie auf einem 1080er Blatt.
|
||||
Bewusst echtes HTML statt Screenshot, damit die Kanten beim Export
|
||||
genauso scharf sind wie die Schrift und der Text austauschbar bleibt. */
|
||||
.note {
|
||||
position: absolute; z-index: var(--note-z, 3);
|
||||
left: var(--note-left, 60px);
|
||||
bottom: var(--note-bottom, 150px);
|
||||
width: var(--note-w, 320px);
|
||||
border-radius: 10px; overflow: hidden;
|
||||
font-family: Manrope, sans-serif;
|
||||
}
|
||||
.note-head {
|
||||
height: 50px; background: #ce93d8;
|
||||
display: flex; align-items: center; justify-content: flex-end;
|
||||
padding-right: 12px;
|
||||
}
|
||||
.note-head svg { width: 26px; height: 26px; display: block; }
|
||||
.note-body {
|
||||
background: #f3e5f5; color: #424242;
|
||||
padding: 18px 20px 26px; min-height: 168px;
|
||||
font-size: 23px; font-weight: 400; line-height: 1.5;
|
||||
}
|
||||
/* Der Anfasser unten rechts: zwei ineinanderliegende Ecken, wie im Original
|
||||
zwei Spans mit border-b/border-r in zwei Deckkraften. */
|
||||
.note-grip { position: absolute; right: 9px; bottom: 9px; }
|
||||
.note-grip i {
|
||||
position: absolute; right: 0; bottom: 0; display: block;
|
||||
border-right: 2px solid rgba(66,66,66,.45);
|
||||
border-bottom: 2px solid rgba(66,66,66,.45);
|
||||
}
|
||||
.note-grip i:first-child { width: 17px; height: 17px; }
|
||||
.note-grip i:last-child {
|
||||
width: 10px; height: 10px;
|
||||
border-right-color: rgba(66,66,66,.65);
|
||||
border-bottom-color: rgba(66,66,66,.65);
|
||||
}
|
||||
|
||||
.slide[data-frame="rund"] .mock { clip-path: none; border-radius: 30px; }
|
||||
.slide[data-frame="rund"] .mock-inner { clip-path: none; border-radius: 20px; }
|
||||
|
||||
@@ -448,30 +499,29 @@
|
||||
.slide[data-frame="aus"] .mock-inner { clip-path: none; border-radius: 0; }
|
||||
|
||||
/* ─── Stapel ─────────────────────────────────────────────────────────────
|
||||
Zwei versetzte Blätter hinter dem Screenshot. Der Rahmen selbst fällt weg,
|
||||
jedes Blatt trägt seinen eigenen Schatten: box-shadow statt drop-shadow,
|
||||
weil ohne clip-path nichts mehr beschnitten wird und jede Karte einzeln
|
||||
abheben soll. */
|
||||
Zwei versetzte Blätter hinter dem Screenshot. Der Rahmen selbst fällt weg.
|
||||
Getrennt werden die Blätter nicht durch Schatten, sondern durch den
|
||||
Versatz und eine Haarlinie: auf der hellen Variante läge sonst Weiss auf
|
||||
Weiss und der Stapel wäre nicht mehr zu erkennen. */
|
||||
.sheet-back, .sheet-mid {
|
||||
display: none; position: absolute; inset: 0; z-index: 0;
|
||||
background: #fff; border-radius: 22px; overflow: hidden;
|
||||
border: 1px solid rgba(48,49,61,.18);
|
||||
}
|
||||
.slide[data-frame="stapel"] .mock {
|
||||
clip-path: none; padding: 0; background: none; filter: none;
|
||||
clip-path: none; padding: 0; background: none;
|
||||
}
|
||||
.slide[data-frame="stapel"] .mock-inner {
|
||||
clip-path: none; border-radius: 22px; z-index: 2;
|
||||
box-shadow: 0 34px 64px rgba(0,0,0,.30), 0 3px 10px rgba(0,0,0,.14);
|
||||
border: 1px solid rgba(48,49,61,.18);
|
||||
}
|
||||
.slide[data-frame="stapel"] .sheet-mid {
|
||||
display: block; z-index: 1;
|
||||
transform: rotate(-2.4deg) translate(-2.2%, 3%) scale(.975);
|
||||
box-shadow: 0 22px 44px rgba(0,0,0,.20);
|
||||
}
|
||||
.slide[data-frame="stapel"] .sheet-back {
|
||||
display: block;
|
||||
transform: rotate(3.2deg) translate(3%, 5.5%) scale(.95);
|
||||
box-shadow: 0 18px 38px rgba(0,0,0,.16);
|
||||
}
|
||||
/* Das hinterste Blatt zeigt bewusst einen anderen Screen: der Stapel soll
|
||||
nach mehreren Ansichten aussehen, nicht nach einer Kopie. */
|
||||
@@ -480,15 +530,6 @@
|
||||
transform-origin: top left;
|
||||
}
|
||||
|
||||
/* Schatten global abschaltbar. Zwei Techniken im Spiel: drop-shadow für die
|
||||
chamferten Rahmen (clip-path würde box-shadow wegschneiden) und box-shadow
|
||||
für den Stapel. Beide müssen mit. */
|
||||
.slide[data-shadow="aus"] .mock,
|
||||
.slide[data-shadow="aus"] .laptop,
|
||||
.slide[data-shadow="aus"] .hex-frame { filter: none; }
|
||||
.slide[data-shadow="aus"] .mock-inner,
|
||||
.slide[data-shadow="aus"] .sheet-mid,
|
||||
.slide[data-shadow="aus"] .sheet-back { box-shadow: none; }
|
||||
/* Screens are ~2000px wide landscape captures; showing them whole makes the
|
||||
UI text unreadable at this size, so each one is zoomed onto its telling
|
||||
region via the per-slide vars below. */
|
||||
@@ -505,7 +546,6 @@
|
||||
.hex-frame {
|
||||
padding: 15px; background: var(--frame-bg);
|
||||
clip-path: polygon(25% 0%, 75% 0%, 100% 50%, 75% 100%, 25% 100%, 0% 50%);
|
||||
filter: drop-shadow(0 26px 48px rgba(0,0,0,.26));
|
||||
}
|
||||
.hex-shot {
|
||||
position: relative; overflow: hidden;
|
||||
@@ -516,14 +556,14 @@
|
||||
.hex-shot img {
|
||||
position: absolute; top: 0; left: 0; width: 100%;
|
||||
transform-origin: top left;
|
||||
transform: scale(1.75) translate(-21%, -4%);
|
||||
transform: scale(1.533) translate(-17.4%, 0%);
|
||||
}
|
||||
|
||||
/* Laptop mockup. The display was knocked out of the PNG, so the screenshot
|
||||
sits BEHIND the device and the notch plus the rounded corners stay on top,
|
||||
the way a real mockup reads. Rect measured from the file: 739 x 489 of
|
||||
1050 x 700. */
|
||||
.laptop { position: relative; width: 960px; filter: drop-shadow(0 28px 50px rgba(0,0,0,.30)); }
|
||||
.laptop { position: relative; width: 960px; }
|
||||
.laptop-frame { width: 100%; display: block; position: relative; z-index: 2; }
|
||||
.laptop-screen {
|
||||
position: absolute; z-index: 1; overflow: hidden;
|
||||
@@ -535,7 +575,7 @@
|
||||
.laptop-screen > img.laptop-shot {
|
||||
position: absolute; top: 0; left: 0; width: 100%;
|
||||
transform-origin: top left;
|
||||
transform: scale(1) translate(0%, 0%);
|
||||
transform: scale(1.566) translate(-18.1%, 0%);
|
||||
}
|
||||
/* Blurred variant: just enough softening to seat the wordmark, the app stays
|
||||
readable underneath. Slightly overscaled so the blur never pulls the empty
|
||||
@@ -554,6 +594,59 @@
|
||||
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);
|
||||
}
|
||||
.szene-screen > img {
|
||||
position: absolute; top: 0; left: 0; width: 100%;
|
||||
transform-origin: top left;
|
||||
}
|
||||
/* Beide Aufnahmen liegen originalgetreu auf ihrem Display, ohne Zoom.
|
||||
Sie sind flacher als der Bildschirm, es bleibt also Flaeche frei; die
|
||||
traegt mit #FAFAFA den Hintergrundton der Anwendung und ist deshalb nicht
|
||||
als Rand zu erkennen.
|
||||
Das Board rueckt 5,15 % nach unten: das Smart-Object des PSD ragt oben
|
||||
hinter die Blende des Deckels, ohne Versatz verschwindet die Kopfzeile.
|
||||
Der Chat steht mittig, sein Display verdeckt nichts. */
|
||||
.szene-mb, .szene-ip { background: #FAFAFA; }
|
||||
.szene-mb > img { transform: translate(0, 5.15%); }
|
||||
.szene-ip > img { transform: translate(0, 16.49%); }
|
||||
|
||||
.laptop-missing {
|
||||
width: 940px; padding: 80px 40px; text-align: center;
|
||||
border: 3px dashed var(--shape-line); color: var(--body-color);
|
||||
@@ -591,7 +684,8 @@
|
||||
.page.title[data-layout="laptop"] .content,
|
||||
.page.title[data-layout="laptop-logo"] .content,
|
||||
.page.title[data-layout="laptop-blur"] .content,
|
||||
.page.title[data-layout="laptop-footer"] .content {
|
||||
.page.title[data-layout="laptop-footer"] .content,
|
||||
.page.title[data-layout="szene"] .content {
|
||||
padding: 92px 96px; justify-content: space-between;
|
||||
}
|
||||
.logo-full { width: 560px; height: auto; display: block; margin-bottom: 74px; }
|
||||
@@ -657,7 +751,7 @@
|
||||
|
||||
<aside class="panel">
|
||||
<h1>Post 1 · Lösung</h1>
|
||||
<div class="projects"><a class="here" href="./post1.html">Post 1 · Lösung</a><a class="" href="./index.html">Post 2 · Statement</a></div>
|
||||
<div class="projects"><a class="here" href="./post1.html">Post 1</a><a class="" href="./post1-alt.html">Post 1 · Alt</a><a class="" href="./index.html">Post 2</a></div>
|
||||
<p class="sub">Carousel, 5 Slides · 1080 × 1350 px<span class="saved" id="saved"></span><br><button class="forget" id="forget">Projekt zurücksetzen</button></p>
|
||||
|
||||
<div class="group">
|
||||
@@ -753,11 +847,6 @@
|
||||
<button data-f="stapel" aria-pressed="false">Stapel</button>
|
||||
<button data-f="aus" aria-pressed="false">Aus</button>
|
||||
</div>
|
||||
<label style="display:block;margin:14px 0 9px">Schatten</label>
|
||||
<div class="opts" id="shadows">
|
||||
<button data-s="an" aria-pressed="true">An</button>
|
||||
<button data-s="aus" aria-pressed="false">Aus</button>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="group" data-grp="mockup-perspektive">
|
||||
@@ -791,7 +880,7 @@
|
||||
<kbd>←</kbd><kbd>→</kbd> Slide · <kbd>V</kbd> Variante<br>
|
||||
<kbd>L</kbd> Layout · <kbd>M</kbd> Muster · <kbd>I</kbd> Intensität<br>
|
||||
<kbd>C</kbd> Durchgehend · <kbd>W</kbd> Ansicht · <kbd>G</kbd> Tag<br>
|
||||
<kbd>R</kbd> Bildrahmen · <kbd>S</kbd> Schatten<br>
|
||||
<kbd>R</kbd> Bildrahmen<br>
|
||||
<kbd>T</kbd> Perspektive<br>
|
||||
<kbd>E</kbd> Bearbeiten · Pfeile verschieben (<kbd>⇧</kbd> = 10 px)<br>
|
||||
<kbd>+</kbd> <kbd>−</kbd> <kbd>0</kbd> Zoom · ⌘/Strg + Rad<br>
|
||||
@@ -851,8 +940,8 @@
|
||||
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" }],
|
||||
head: 'Vom <span class="a">Handwerker</span><br>f\u00fcr den Handwerker',
|
||||
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" }],
|
||||
head: 'Vom <span class="a">Handwerker</span><br>f\u00fcr Handwerker',
|
||||
sub: "Die Software, die deine Branchenprogramme nicht ersetzt, sondern verbindet.",
|
||||
},
|
||||
{
|
||||
@@ -862,36 +951,59 @@
|
||||
head: 'Du siehst auf einen Blick,<br><span class="a">wo jeder Auftrag steht.</span>',
|
||||
body: "Projekte, Kunden, Angebote, F\u00f6rderantr\u00e4ge. Kein Suchen, kein Nachfragen, kein Zettelchaos.",
|
||||
img: "./assets/screens/pipelines.png",
|
||||
mockAR: 1.8519, // 2000 x 1080
|
||||
imgScale: 1, imgX: "0%", imgY: "0%",
|
||||
// Nur die linke Bildhälfte: Zoom 2 bei Versatz 0 zeigt exakt den Anteil
|
||||
// 0 bis 0,5. Das Rahmenverhältnis ist die halbierte Bildbreite.
|
||||
mockW: 580, mockAR: 0.8855,
|
||||
imgScale: 2, imgX: "0%", imgY: "0%",
|
||||
// Detailspalte eines Projekts vor dem Board: zeigt, was hinter einer
|
||||
// Karte steckt, ohne das Board zu verdecken.
|
||||
media2: {
|
||||
img: "./assets/screens/pipelines-detail.png",
|
||||
w: 300, ar: 0.781, left: 500, bottom: 52, z: 2, rot: -2,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "workflow-engine", type: "feature",
|
||||
layouts: [{ id: "standard", label: "Standard" }],
|
||||
eyebrow: "Workflow-Engine",
|
||||
head: '<span class="a">Wenn-dann</span><br>statt Handarbeit.',
|
||||
body: "Ausl\u00f6ser w\u00e4hlen, Aktionen dranh\u00e4ngen, fertig. Anfrage rein, Aufgabe raus, Best\u00e4tigung an den Kunden, alles automatisch und ohne eine Zeile Code.",
|
||||
body: "Auslöser wählen, Aktionen dranhängen, fertig. Alles automatisch und ohne eine Zeile Code.",
|
||||
img: "./assets/screens/workflow.png",
|
||||
mockAR: 1.5038, // 2000 x 1330
|
||||
mockW: 940, mockAR: 2.366,
|
||||
imgScale: 1, imgX: "0%", imgY: "0%",
|
||||
// Die Bausteinliste ragt über den Ablauf hinaus: sie zeigt, woraus er entsteht.
|
||||
media2: {
|
||||
img: "./assets/screens/workflow-bausteine.png",
|
||||
w: 340, ar: 0.8326, left: 700, bottom: 96, z: 2, rot: 0,
|
||||
},
|
||||
// Der Zettel liegt bewusst schief und leicht über der linken Blattkante:
|
||||
// er gehört zum Ablauf, ist aber sichtbar von Hand dazugelegt.
|
||||
note: {
|
||||
text: "Eigene Notizen direkt am Ablauf hinterlegen.",
|
||||
w: 320, left: 44, bottom: 150, z: 3, rot: -3,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "chat", type: "feature",
|
||||
layouts: [{ id: "standard", label: "Standard" }],
|
||||
eyebrow: "Team-Chat",
|
||||
head: 'Jedes Projekt bekommt<br>automatisch <span class="a">seinen Kanal.</span>',
|
||||
body: "Foto von der Baustelle rein, alle sehen\u2019s sofort und es landet direkt in den Projektdateien. Aus jeder Nachricht wird mit einem Klick eine Aufgabe.",
|
||||
body: "Foto von der Baustelle rein, alle sehen's sofort. Aus jeder Nachricht wird mit einem Klick eine Aufgabe.",
|
||||
img: "./assets/screens/chat.png",
|
||||
// 2000 x 1357. Etwas schmaler, sonst reicht die Hoehe unter dem Text nicht.
|
||||
mockW: 1070, mockAR: 1.4738,
|
||||
mockW: 1030, mockAR: 1.703,
|
||||
imgScale: 1, imgX: "0%", imgY: "0%",
|
||||
// Das Anhang-Menü zeigt, was aus einer Nachricht alles werden kann.
|
||||
media2: {
|
||||
img: "./assets/screens/chat-menue.png",
|
||||
w: 300, ar: 0.866, left: 120, bottom: 55, z: 2, rot: -3,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "form-builder", type: "feature",
|
||||
layouts: [{ id: "standard", label: "Standard" }],
|
||||
eyebrow: "Form Builder",
|
||||
head: 'Formulare selbst bauen.<br><span class="a">Per Drag-and-drop.</span>',
|
||||
body: "Von der Kontaktanfrage bis zum Aufma\u00df mit Unterschrift vor Ort. Teil sie per Link oder QR-Code, jede Einsendung wird automatisch zu Lead, Kontakt oder Auftrag.",
|
||||
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",
|
||||
mockW: 540, mockAR: 0.75, mockLeft: 270,
|
||||
imgScale: 3.8, imgX: "-36.9%", imgY: "-10%",
|
||||
@@ -913,7 +1025,10 @@
|
||||
"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" },
|
||||
"mock": { label: "Bild", size: null, prop: "width" },
|
||||
"mock-2": { label: "Zweites Bild", size: null, prop: "width" },
|
||||
"note": { label: "Notizzettel", size: null, prop: "width" },
|
||||
"hex-frame": { label: "Wabe", size: 778, prop: "width" },
|
||||
};
|
||||
const EDIT_KEYS = Object.keys(STANDARD);
|
||||
@@ -922,7 +1037,9 @@
|
||||
Hochformat von 540, alle anderen 1090. Deshalb nicht aus der Tabelle,
|
||||
sondern aus der Slide selbst. */
|
||||
function stdSize(key, i) {
|
||||
if (key === "mock") return SLIDES[i].mockW || 1090;
|
||||
if (key === "mock") return SLIDES[i].mockW || 1090;
|
||||
if (key === "mock-2") return (SLIDES[i].media2 || {}).w || 400;
|
||||
if (key === "note") return (SLIDES[i].note || {}).w || 320;
|
||||
return (STANDARD[key] || {}).size;
|
||||
}
|
||||
|
||||
@@ -941,7 +1058,7 @@
|
||||
let editMode = "aus";
|
||||
|
||||
let current = 0;
|
||||
let variant = "dunkel", tilt = "an", superSample = 2, continuous = "aus", view = "streifen", tagOut = "an", frameStyle = "chamfer", shadow = "an";
|
||||
let variant = "dunkel", tilt = "an", superSample = 2, continuous = "aus", view = "streifen", tagOut = "an", frameStyle = "chamfer";
|
||||
|
||||
/* ─── Projektstand ────────────────────────────────────────────────────────
|
||||
Alles, was am Projekt hängt, wird gespeichert: Einstellungen, Layouts,
|
||||
@@ -953,7 +1070,7 @@
|
||||
function saveState() {
|
||||
try {
|
||||
localStorage.setItem(STORE, JSON.stringify({
|
||||
v: 1, variant, tilt, superSample, continuous, view, tagOut, frameStyle, shadow,
|
||||
v: 1, variant, tilt, superSample, continuous, view, tagOut, frameStyle,
|
||||
current, layoutState, bgState, edits,
|
||||
}));
|
||||
markSaved();
|
||||
@@ -982,7 +1099,6 @@
|
||||
view = d.view || view;
|
||||
tagOut = d.tagOut || tagOut;
|
||||
frameStyle = d.frameStyle || frameStyle;
|
||||
shadow = d.shadow || shadow;
|
||||
current = Number.isInteger(d.current) ? d.current : 0;
|
||||
return true;
|
||||
}
|
||||
@@ -1057,6 +1173,12 @@
|
||||
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.6" ' +
|
||||
'stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M13 6l6 6-6 6"/></svg>';
|
||||
|
||||
// FeatherMoreVertical aus dem Kopf der StickyNote: drei Punkte, keine Linien.
|
||||
const NOTE_MENU_SVG =
|
||||
'<svg viewBox="0 0 24 24" fill="rgba(66,66,66,.72)">' +
|
||||
'<circle cx="12" cy="5" r="1.9"/><circle cx="12" cy="12" r="1.9"/>' +
|
||||
'<circle cx="12" cy="19" r="1.9"/></svg>';
|
||||
|
||||
const SVG_NS = "http://www.w3.org/2000/svg";
|
||||
const G_POINTS = "384.5,166.5 336.4,83.2 192.2,166.5 288.3,166.5 240.3,249.7 144.2,249.7 96.1,166.5 144.2,83.2 336.4,83.2 298,16.8 288.3,0 96.1,0 0,166.5 96.1,332.9 288.3,332.9";
|
||||
const G_W = 384.5, G_H = 332.9;
|
||||
@@ -1132,6 +1254,23 @@
|
||||
'<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") {
|
||||
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>';
|
||||
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 + '">' +
|
||||
'</div>' + dev +
|
||||
'<p class="wabe-claim">' + s.sub + '</p>';
|
||||
}
|
||||
if (layoutState[i] === "laptop-footer") {
|
||||
const dev = assets["./assets/mockups/laptop.png"]
|
||||
? '<div class="laptop">' +
|
||||
@@ -1199,6 +1338,10 @@
|
||||
for (const key of EDIT_KEYS) {
|
||||
slideEl.querySelectorAll("." + key).forEach(n => { n.dataset.edit = key; });
|
||||
}
|
||||
// ".mock" trifft auch das zweite Bild, weil es dieselbe Klasse trägt.
|
||||
// Der spezifischere Schlüssel muss gewinnen, sonst teilen sich beide
|
||||
// Bilder eine Bearbeitung.
|
||||
slideEl.querySelectorAll(".mock-2").forEach(n => { n.dataset.edit = "mock-2"; });
|
||||
}
|
||||
|
||||
function applyEdits(slideEl, i) {
|
||||
@@ -1213,8 +1356,20 @@
|
||||
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) {
|
||||
@@ -1246,6 +1401,35 @@
|
||||
';--img-x:' + sl.imgX + ';--img-y:' + sl.imgY + '">' +
|
||||
'<img class="laptop-shot" alt="" src="' + assets[sl.img] + '"></div>';
|
||||
page.appendChild(mock);
|
||||
|
||||
if (sl.media2) {
|
||||
const m2 = sl.media2;
|
||||
const el2 = document.createElement("div");
|
||||
el2.className = "mock mock-2";
|
||||
el2.style.cssText =
|
||||
"--mock-w:" + m2.w + "px;--mock-ar:" + m2.ar +
|
||||
";--mock-left:" + m2.left + "px;--m2-bottom:" + m2.bottom +
|
||||
"px;--m2-z:" + m2.z + ";--rot:" + (m2.rot || 0) + "deg";
|
||||
el2.innerHTML =
|
||||
'<div class="mock-inner"><img class="laptop-shot" alt="" src="' +
|
||||
assets[m2.img] + '"></div>';
|
||||
page.appendChild(el2);
|
||||
}
|
||||
|
||||
if (sl.note) {
|
||||
const nt = sl.note;
|
||||
const el3 = document.createElement("div");
|
||||
el3.className = "note";
|
||||
el3.style.cssText =
|
||||
"--note-w:" + nt.w + "px;--note-left:" + nt.left +
|
||||
"px;--note-bottom:" + nt.bottom + "px;--note-z:" + nt.z +
|
||||
";--rot:" + (nt.rot || 0) + "deg";
|
||||
el3.innerHTML =
|
||||
'<div class="note-head">' + NOTE_MENU_SVG + '</div>' +
|
||||
'<div class="note-body">' + nt.text + '</div>' +
|
||||
'<div class="note-grip"><i></i><i></i></div>';
|
||||
page.appendChild(el3);
|
||||
}
|
||||
} else {
|
||||
const sw = document.createElement("div");
|
||||
sw.className = "swipe";
|
||||
@@ -1278,7 +1462,6 @@
|
||||
el.dataset.cont = continuous;
|
||||
el.dataset.tagout = tagOut;
|
||||
el.dataset.frame = frameStyle;
|
||||
el.dataset.shadow = shadow;
|
||||
el.dataset.editmode = editMode;
|
||||
el.style.setProperty("--slide-i", i);
|
||||
el.innerHTML = BG_HTML;
|
||||
@@ -1301,13 +1484,14 @@
|
||||
re-render without touching the network. */
|
||||
async function preloadAssets() {
|
||||
const paths = ["./assets/brand/logo.svg", "./assets/brand/logo-white.svg"]
|
||||
.concat(SLIDES.filter(s => s.img).map(s => s.img));
|
||||
paths.push("./assets/screens/pipelines.png");
|
||||
.concat(SLIDES.filter(s => s.img).map(s => s.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"]);
|
||||
paths.push("./assets/mockups/laptop.png");
|
||||
const optional = new Set(["./assets/mockups/laptop.png", "./assets/mockups/macbook-szene.png"]);
|
||||
paths.push("./assets/mockups/laptop.png", "./assets/mockups/macbook-szene.png");
|
||||
for (const path of [...new Set(paths)]) {
|
||||
const res = await fetch(path).catch(() => null);
|
||||
const res = await fetch(path, { cache: "no-store" }).catch(() => null);
|
||||
if (!res || !res.ok) {
|
||||
if (optional.has(path)) continue;
|
||||
throw new Error(path + " -> HTTP " + (res ? res.status : "kein Zugriff"));
|
||||
@@ -1338,7 +1522,6 @@
|
||||
el.dataset.cont = continuous;
|
||||
el.dataset.tagout = tagOut;
|
||||
el.dataset.frame = frameStyle;
|
||||
el.dataset.shadow = shadow;
|
||||
if (SLIDES[i].type === "title") rebuildPage(i);
|
||||
el.dataset.editmode = editMode;
|
||||
});
|
||||
@@ -1433,13 +1616,29 @@
|
||||
return out;
|
||||
}
|
||||
|
||||
/* Die Bibliothek baut ein SVG in Knotengröße und zieht es beim Zeichnen auf
|
||||
die Canvas-Größe hoch. WebKit rasterisiert das SVG dabei in seiner
|
||||
*intrinsischen* Größe und interpoliert danach: Ein höherer pixelRatio
|
||||
erzeugt so keine zusätzlichen Details, nur weichere Kanten. Genau deshalb
|
||||
wirkte 3x nicht schärfer als 1x.
|
||||
|
||||
Der Ausweg: Das SVG bekommt gleich die Zielgröße über width/height, die
|
||||
Slide behält per style ihre echten 1080 x 1350 und wird darin skaliert.
|
||||
Die Bibliothek setzt style nach width/height, das Überschreiben greift.
|
||||
pixelRatio bleibt 1, weil die Vergrößerung schon im SVG steckt. */
|
||||
function renderOptions() {
|
||||
const k = superSample;
|
||||
return {
|
||||
width: 1080, height: 1350, pixelRatio: superSample,
|
||||
width: 1080 * k,
|
||||
height: 1350 * k,
|
||||
pixelRatio: 1,
|
||||
fontEmbedCSS: fontEmbedCss,
|
||||
// The scaler transform is preview-only — neutralise it for the export,
|
||||
// otherwise the bitmap comes out at 45 %.
|
||||
style: { transform: "none", transformOrigin: "top left" },
|
||||
style: {
|
||||
width: "1080px",
|
||||
height: "1350px",
|
||||
transform: k === 1 ? "none" : "scale(" + k + ")",
|
||||
transformOrigin: "top left",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1466,7 +1665,7 @@
|
||||
missing every few exports while the rest of the slide was fine. It is a
|
||||
race, so the fix is not to hope but to verify: render, check that every
|
||||
screenshot area actually carries pixels, and repeat if it does not. */
|
||||
const SHOT_BOXES = ".mock-inner, .laptop-screen, .hex-shot";
|
||||
const SHOT_BOXES = ".mock-inner, .laptop-screen, .hex-shot, .szene-screen";
|
||||
const MAX_TRIES = 5;
|
||||
|
||||
// Rect of a child inside the slide, in slide coordinates (the preview is
|
||||
@@ -1485,19 +1684,24 @@
|
||||
|
||||
/* A screenshot area is never a flat surface: it carries UI, text and colour.
|
||||
If the sampled pixels are all but identical, the image did not make it. */
|
||||
function areaHasContent(ctx, b) {
|
||||
const x = Math.max(0, b.x + 4), y = Math.max(0, b.y + 4);
|
||||
const w = Math.min(1080 - x, b.w - 8), h = Math.min(1350 - y, b.h - 8);
|
||||
if (w < 8 || h < 8) return true;
|
||||
/* Ein fehlendes Bild hinterlässt die reine Hintergrundfarbe des Rahmens,
|
||||
also eine vollkommen gleichmäßige Fläche. Genau darauf wird geprüft, nicht
|
||||
auf "genug Kontrast": Eine helle Formular-Oberfläche hat wenig Spanne, ist
|
||||
aber nie gleichmäßig. Ein zu hoher Schwellenwert hat sie fälschlich als
|
||||
fehlend gemeldet. */
|
||||
function areaRange(ctx, b) {
|
||||
const ix = Math.round(b.w * 0.08), iy = Math.round(b.h * 0.08);
|
||||
const x = Math.max(0, b.x + ix), y = Math.max(0, b.y + iy);
|
||||
const w = Math.min(1080 - x, b.w - ix * 2), h = Math.min(1350 - y, b.h - iy * 2);
|
||||
if (w < 8 || h < 8) return 255;
|
||||
const data = ctx.getImageData(x, y, w, h).data;
|
||||
const step = Math.max(4, Math.floor((w * h) / 4000)) * 4;
|
||||
let min = 255, max = 0;
|
||||
for (let i = 0; i < data.length; i += step) {
|
||||
for (let i = 0; i < data.length; i += 16) { // jedes vierte Pixel
|
||||
const lum = (data[i] * 299 + data[i + 1] * 587 + data[i + 2] * 114) / 1000;
|
||||
if (lum < min) min = lum;
|
||||
if (lum > max) max = lum;
|
||||
}
|
||||
return max - min > 12;
|
||||
return max - min;
|
||||
}
|
||||
|
||||
async function renderSlide(i) {
|
||||
@@ -1513,9 +1717,12 @@
|
||||
el.dataset.editmode = "aus";
|
||||
try {
|
||||
await document.fonts.ready;
|
||||
await Promise.all([...el.querySelectorAll("img")].map(im =>
|
||||
const imgs = [...el.querySelectorAll("img")];
|
||||
await Promise.all(imgs.map(im =>
|
||||
im.complete ? im.decode().catch(() => {})
|
||||
: new Promise(r => { im.onload = r; im.onerror = r; })));
|
||||
const broken = imgs.filter(im => !im.naturalWidth);
|
||||
if (broken.length) throw new Error(broken.length + " Bild(er) nicht geladen");
|
||||
|
||||
const boxes = [...el.querySelectorAll(SHOT_BOXES)].map(c => boxInSlide(el, c));
|
||||
|
||||
@@ -1523,20 +1730,22 @@
|
||||
const raw = await htmlToImage.toCanvas(el, renderOptions());
|
||||
const canvas = downscaleTo(raw, 1080, 1350);
|
||||
const ctx = canvas.getContext("2d", { willReadFrequently: true });
|
||||
const missing = boxes.filter(b => !areaHasContent(ctx, b)).length;
|
||||
const ranges = boxes.map(b => areaRange(ctx, b));
|
||||
const missing = ranges.filter(r => r <= 3).length;
|
||||
|
||||
if (!missing) {
|
||||
const blob = await new Promise(r => canvas.toBlob(r, "image/png"));
|
||||
if (blob) return blob;
|
||||
}
|
||||
if (attempt < MAX_TRIES) {
|
||||
console.warn("Slide " + (i + 1) + ": Versuch " + attempt + " unvollstaendig" +
|
||||
(missing ? " (" + missing + " Screenshot-Flaeche leer)" : ""));
|
||||
console.warn("Slide " + (i + 1) + ": Versuch " + attempt + " unvollstaendig. " +
|
||||
"Luminanzspannen je Bildflaeche: " + ranges.map(r => r.toFixed(1)).join(", "));
|
||||
setStatus("Slide " + (i + 1) + ": Screenshot fehlte, Versuch " + (attempt + 1) + " ...");
|
||||
await new Promise(r => setTimeout(r, 180 * attempt));
|
||||
}
|
||||
}
|
||||
throw new Error("Slide " + (i + 1) + " nach " + MAX_TRIES + " Versuchen unvollstaendig");
|
||||
throw new Error("Slide " + (i + 1) + " nach " + MAX_TRIES +
|
||||
" Versuchen unvollstaendig. Details in der Konsole.");
|
||||
} finally {
|
||||
el.dataset.editmode = modeBefore;
|
||||
holder.style.display = restore;
|
||||
@@ -1728,7 +1937,7 @@
|
||||
(function renderStandards() {
|
||||
const groups = [
|
||||
["Schrift", ["badge", "headline", "body", "wabe-tagline", "wabe-claim", "title-head", "title-sub"]],
|
||||
["Breite", ["wabe-logo", "logo-full", "screen-logo", "laptop"]],
|
||||
["Breite", ["wabe-logo", "logo-full", "screen-logo", "laptop", "szene"]],
|
||||
];
|
||||
let html = "";
|
||||
for (const [title, keys] of groups) {
|
||||
@@ -1831,10 +2040,6 @@
|
||||
const b = e.target.closest("button");
|
||||
if (b) { frameStyle = b.dataset.f; press("frames", "f", frameStyle); applyAll(); saveState(); }
|
||||
});
|
||||
document.getElementById("shadows").addEventListener("click", e => {
|
||||
const b = e.target.closest("button");
|
||||
if (b) { shadow = b.dataset.s; press("shadows", "s", shadow); applyAll(); saveState(); }
|
||||
});
|
||||
document.getElementById("tilts").addEventListener("click", e => {
|
||||
const b = e.target.closest("button");
|
||||
if (b) { tilt = b.dataset.t; press("tilts", "t", tilt); applyAll(); saveState(); }
|
||||
@@ -1889,10 +2094,8 @@
|
||||
else if (k === "r") {
|
||||
const cyc = ["chamfer", "rund", "stapel", "aus"];
|
||||
frameStyle = cyc[(cyc.indexOf(frameStyle) + 1) % cyc.length];
|
||||
press("frames", "f", frameStyle);
|
||||
press("shadows", "s", shadow); applyAll(); saveState();
|
||||
press("frames", "f", frameStyle); applyAll(); saveState();
|
||||
}
|
||||
else if (k === "s") { shadow = shadow === "an" ? "aus" : "an"; press("shadows", "s", shadow); applyAll(); saveState(); }
|
||||
else if (k === "g") { tagOut = tagOut === "an" ? "aus" : "an"; press("tagouts", "o", tagOut); applyAll(); saveState(); }
|
||||
else if (k === "c") { continuous = continuous === "an" ? "aus" : "an"; press("conts", "c", continuous); applyAll(); saveState(); }
|
||||
else if (k === "d") guard(downloadOne)();
|
||||
@@ -1915,7 +2118,6 @@
|
||||
press("conts", "c", continuous);
|
||||
press("tagouts", "o", tagOut);
|
||||
press("frames", "f", frameStyle);
|
||||
press("shadows", "s", shadow);
|
||||
press("views", "w", view);
|
||||
show(current);
|
||||
if (restored) setStatus("Projektstand geladen", "ok");
|
||||
|
||||
@@ -104,7 +104,7 @@ clip-path: polygon(
|
||||
|
||||
**Drei Fallstricke** (stehen auch in `chamfer.ts`):
|
||||
|
||||
- `clip-path` beschneidet **auch den Schatten** → Tiefe über `filter: drop-shadow()`, nie `box-shadow`
|
||||
- `clip-path` beschneidet **auch den Schatten** → wäre über `filter: drop-shadow()` zu lösen, nie über `box-shadow`. Praktisch irrelevant, seit die Posts schattenfrei sind (siehe §5)
|
||||
- `clip-path` beschneidet **auch den Rahmen** → Rand über eine Hintergrundfläche lösen, nicht über `border`
|
||||
- Kinder, die aus der Box ragen (Popups, Tooltips), werden abgeschnitten → solche Elemente nicht chamfern
|
||||
|
||||
@@ -287,6 +287,27 @@ x 72 — immer noch weit innerhalb der Safe Zone.
|
||||
| 2 | Inhalt: Badge, Headline, Fließtext |
|
||||
| 3 | Swipe-Hinweis |
|
||||
|
||||
### Keine Schatten
|
||||
|
||||
**Nichts wirft einen Schatten** — weder Bilder noch Mockups, Logos, Zettel oder
|
||||
Text. Kein `box-shadow`, kein `filter: drop-shadow()`, kein `text-shadow`.
|
||||
|
||||
Tiefe entsteht stattdessen über:
|
||||
|
||||
- **Überlappung** — ein zweites Bild vor dem ersten liest sich auch ohne Schlagschatten als davor
|
||||
- **Rahmenfarbe** — der chamferte Rahmen (`--frame-bg`) setzt das Bild von der Fläche ab
|
||||
- **Versatz und Drehung** — beim Stapel und beim Notizzettel
|
||||
|
||||
**Ausnahme, und nur diese:** Die Blätter im Stapel-Rahmen bekommen eine
|
||||
Haarlinie (`1px rgba(48,49,61,.18)`). Ohne sie läge auf der hellen Variante
|
||||
Weiß auf Weiß und der Stapel wäre nicht mehr zu erkennen. Eine Linie ist kein
|
||||
Schatten.
|
||||
|
||||
Warum: Weichgezeichnete Schatten sind das erste, was einen Post nach Vorlage
|
||||
aussehen lässt. Flächig gesetzt wirkt es ruhiger und passt zur Website, die
|
||||
denselben Weg geht. Nebenbei ist es beim Export billiger — jeder unscharfe
|
||||
Filter muss beim 3-fachen Rendern dreifach berechnet werden.
|
||||
|
||||
### Was global gilt und was pro Slide
|
||||
|
||||
| Einstellung | Geltung | Warum |
|
||||
@@ -403,7 +424,122 @@ soll ein Bildschirm zeigen, was ein Bildschirm zeigt. Die Displayfläche misst
|
||||
Screenshot dahinter liegen kann. Nur so bleiben Notch und Rundungen oben
|
||||
3. Displayfläche ausmessen und als Prozentwerte eintragen — sie hängt am
|
||||
konkreten Bild
|
||||
4. Schatten über `filter: drop-shadow()` auf dem Container, nicht im PNG
|
||||
4. **Kein Schatten im PNG** — die Posts sind schattenfrei (§5), ein eingebrannter
|
||||
Schlagschatten im Mockup-Bild lässt sich nicht mehr abschalten
|
||||
|
||||
### Szenen-Mockup (Perspektive)
|
||||
|
||||
`assets/mockups/macbook-szene.png` — Macbook Air + iPad Pro, freigestellt aus
|
||||
einem Pixeden-PSD. Anders als `laptop.png` steht die Szene schräg, die
|
||||
Displayflächen sind also **Vierecke, keine Rechtecke**.
|
||||
|
||||
Damit reicht die übliche Prozent-Box nicht. Der Screenshot wird per
|
||||
`matrix3d` in die Fläche projiziert:
|
||||
|
||||
| | Macbook | iPad |
|
||||
|---|---|---|
|
||||
| Design-Größe | 3456 × 2234 | 1438 × 2164 |
|
||||
| Format | 1,547 | **0,6645** |
|
||||
| Eckradius | 0 | 57 px |
|
||||
| Viereck aus | Smart-Object | vermessene Glasfläche |
|
||||
|
||||
**Zulieferung: ein iPad-Screenshot muss 0,6645 haben** (1 : 1,505), also z. B.
|
||||
1440 × 2167. Beim Macbook 1,547, dort verschwinden die unteren 15 % hinter der
|
||||
Tastaturschale.
|
||||
|
||||
Die Matrizen bilden **Design-Pixel auf Bildpixel des 5742 px breiten
|
||||
Originals** ab; `--sk` skaliert von dort auf die Breite in der Slide und wird
|
||||
in `applyEdits()` gesetzt, damit eine Größenänderung mitläuft. Die Eckpunkte
|
||||
stammen aus dem `transform_box` der Smart-Objekte im PSD, sind also gemessen
|
||||
und nicht angepasst.
|
||||
|
||||
**Beide Displays sind ausgeknockt**, der Ausschnitt am Macbook endet dort, wo
|
||||
das iPad davor liegt. Sonst läge der Screenshot vor dem Tablet. Der Ausschnitt
|
||||
kommt aus der Deckung der Screen-Ebene, **nicht** aus dem Design-Rechteck: das
|
||||
reicht hinter das Gehäuse und würde die Tastaturschale anschneiden.
|
||||
|
||||
**Die Geräte standen auf einer Glasplatte.** Mit dem Hintergrund fällt die
|
||||
Platte weg, ihre Spiegelung bleibt aber als zweiter Körper stehen, und der
|
||||
Spalt dazwischen wird auf hellem Grund zur weißen Linie.
|
||||
|
||||
**Der Spalt bekommt keinen festen Farbwert, sondern wird spaltenweise
|
||||
interpoliert.** Für jede Bildspalte: Medianfarbe der fünf Pixel über dem
|
||||
Spalt, Medianfarbe der fünf darunter, linear dazwischen. Am Display bleibt so
|
||||
Schwarz, am Gehäuse das Metallic, an der Kante der Verlauf. Es gibt keinen
|
||||
Farbsprung mehr und damit auch keinen Streifen.
|
||||
|
||||
> Ein einheitlicher Ton reicht nicht. Der Spalt läuft quer über Displayrand
|
||||
> **und** Aluminium, also über zwei völlig verschiedene Töne. Jeder feste Wert
|
||||
> passt an einer Stelle und sitzt an der anderen als Fremdkörper.
|
||||
|
||||
> Nicht wegschneiden: die Spiegelung hängt am Fuß des iPads, ein Schnitt nach
|
||||
> Zusammenhangskomponenten trennt den mit ab. Das war der erste Versuch.
|
||||
|
||||
Die Füllung läuft über die **volle Breite**, auch unter dem iPad: dessen
|
||||
eigener Spalt ist sonst als weiße Linie unter dem Rahmen zu sehen. Mit einem
|
||||
festen Ton ging das nicht, da saß dort ein harter dunkler Block auf dem
|
||||
Aluminium; mit der Interpolation passt es.
|
||||
|
||||
**Das iPad-Viereck kommt nicht aus dem Smart-Object.** Das sitzt im PSD zu
|
||||
hoch und lässt unten einen dreimal so breiten schwarzen Streifen stehen wie
|
||||
oben. Statt dessen wird die schwarze Glasfläche vermessen:
|
||||
|
||||
1. Randpunkte der vier Kanten sammeln, **nur im mittleren Drittel** — an den
|
||||
Ecken liegen sie auf den Rundungen
|
||||
2. Ausgleichsgerade je Kante, benachbarte schneiden → die **echten** Ecken.
|
||||
Über Extrempunkte kommt ein zu kleines, verdrehtes Viereck heraus, weil
|
||||
die Extrema auf den Rundungen sitzen
|
||||
3. Eckradius aus dem Abstand Ecke zu Glas: bei einem rechten Winkel liegt der
|
||||
Scheitel der Rundung bei `r · (√2 − 1)` → **Glas 1514 × 2240, r = 95**
|
||||
4. **38 px** Rand nach innen (2,5 % der Breite) → Display 1438 × 2164
|
||||
5. Innenradius **konzentrisch**: `95 − 38 = 57`. Nur so bleibt der schwarze
|
||||
Rand auch in den Ecken gleich breit
|
||||
|
||||
Der Ausschnitt wird als gerundetes Rechteck im Design-Raum gezeichnet und dann
|
||||
durch die Homographie geschoben, im CSS macht `border-radius: 57px` mit
|
||||
`overflow: hidden` dasselbe. Ohne das stößt eine scharfe Bildecke in die runde
|
||||
Glasecke, und genau das sieht man sofort.
|
||||
|
||||
Beim Macbook stimmt das Smart-Object, dort bleibt es dabei.
|
||||
|
||||
Belegung: Kanban-Board auf dem Macbook, Projekt-Chat hochkant auf dem iPad.
|
||||
**Beide liegen originalgetreu auf ihrem Display, ohne Zoom.** Sie sind flacher
|
||||
als der Bildschirm, es bleibt also Fläche frei; die trägt mit `#FAFAFA` den
|
||||
Hintergrundton der Anwendung und ist deshalb nicht als Rand zu erkennen.
|
||||
|
||||
> **Das Board rückt 5,15 % nach unten.** Das Smart-Object des PSD ragt oben
|
||||
> hinter die Blende des Deckels. Ohne Versatz verschwindet genau die Kopfzeile
|
||||
> der Anwendung, und der Screen sieht angeschnitten aus, obwohl gar nicht
|
||||
> gezoomt wurde. Unten wird der Screen von der Tastaturschale beschnitten —
|
||||
> das ist so gewollt und im Pixeden-Original genauso.
|
||||
|
||||
> **Lizenz beachten.** Pixeden-Ressourcen sind für kommerzielle Projekte frei
|
||||
> und ohne Namensnennung nutzbar, dürfen aber **nicht weitergegeben** werden.
|
||||
> Das PSD gehört deshalb nicht ins Repo, nur das für uns abgeleitete PNG.
|
||||
|
||||
### Nachgebaute UI-Bausteine
|
||||
|
||||
Manche Produkt-Elemente liegen **nicht** als Screenshot, sondern als HTML im
|
||||
Generator. Bisher: der **Notizzettel** auf der Workflow-Slide.
|
||||
|
||||
Nachbau aus `hivegrid-pitch/ui/components/StickyNote/StickyNote.tsx`,
|
||||
Variante `purple`:
|
||||
|
||||
| Teil | Wert |
|
||||
|---|---|
|
||||
| Kopfleiste | `#ce93d8` |
|
||||
| Fläche | `#f3e5f5` |
|
||||
| Text | `#424242`, Gewicht 400, Zeilenhöhe 1,5 |
|
||||
| Menü | drei Punkte (FeatherMoreVertical), kein Balken |
|
||||
| Anfasser | zwei Ecken unten rechts, 45 % und 65 % Deckkraft |
|
||||
|
||||
Die Vorlage ist 200 px breit bei 14 px Text; auf dem 1080er Blatt sind alle
|
||||
Maße rund 1,6× hochskaliert. **Farbwerte nicht neu erfinden** — sie stammen aus
|
||||
der Software und müssen wiedererkennbar bleiben.
|
||||
|
||||
Warum HTML und nicht Screenshot: Kanten und Schrift bleiben beim 3×-Export
|
||||
genauso scharf wie der Rest der Slide, der Text ist austauschbar, und es gibt
|
||||
keine Scrollbalken oder Cursor-Reste im Bild.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in new issue
Block a user