diff --git a/index.html b/index.html index 1b2b1b1..586006b 100644 --- a/index.html +++ b/index.html @@ -24,11 +24,11 @@ * { box-sizing: border-box; margin: 0; padding: 0; } + html, body { overscroll-behavior: none; } body { font-family: 'Manrope', system-ui, sans-serif; background: #17181F; color: #E8EAEE; - min-height: 100vh; display: flex; gap: 40px; padding: 32px; - align-items: flex-start; + height: 100vh; overflow: hidden; display: flex; } /* ─── Projekt-Navigation ─────────────────────────────────────────────── */ @@ -50,7 +50,11 @@ .forget:hover { color: #EB7C6B; } /* ─── Control panel ──────────────────────────────────────────────────── */ - .panel { width: 300px; flex: none; position: sticky; top: 32px; } + .panel { + width: 306px; flex: none; height: 100vh; overflow-y: auto; + padding: 28px 22px 40px 28px; + border-right: 1px solid #24262F; background: #17181F; + } .panel h1 { font-size: 19px; font-weight: 800; letter-spacing: -0.01em; margin-bottom: 4px; } .panel .sub { font-size: 13px; color: #8A8F9B; margin-bottom: 24px; line-height: 1.5; } @@ -95,7 +99,16 @@ .status.err { color: #EB7C6B; } /* ─── Stage ──────────────────────────────────────────────────────────── */ - .stage { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 14px; } + /* Arbeitsfläche, klar abgesetzt von der Bedienleiste. */ + .stage { + flex: 1; position: relative; height: 100vh; overflow: auto; + overscroll-behavior: contain; + background-color: #101116; + background-image: radial-gradient(circle, #282B35 1.6px, transparent 1.6px); + background-size: 24px 24px; + display: flex; flex-direction: column; align-items: center; justify-content: center; + gap: 14px; padding: 48px; + } .stage-meta { font-size: 12px; color: #6E7480; letter-spacing: .04em; } /* The wrapper reserves the *scaled* footprint so the page doesn't jump; diff --git a/post1.html b/post1.html index 1e62ba4..e8b2295 100644 --- a/post1.html +++ b/post1.html @@ -26,11 +26,11 @@ * { box-sizing: border-box; margin: 0; padding: 0; } + html, body { overscroll-behavior: none; } body { font-family: 'Manrope', system-ui, sans-serif; background: #17181F; color: #E8EAEE; - min-height: 100vh; display: flex; gap: 36px; padding: 28px; - align-items: flex-start; + height: 100vh; overflow: hidden; display: flex; } /* ─── Projekt-Navigation ─────────────────────────────────────────────── */ @@ -52,16 +52,37 @@ .forget:hover { color: #EB7C6B; } /* ─── Control panel ──────────────────────────────────────────────────── */ - .panel { width: 290px; flex: none; position: sticky; top: 28px; } + .panel { + width: 306px; flex: none; height: 100vh; overflow-y: auto; + padding: 28px 22px 40px 28px; + border-right: 1px solid #24262F; background: #17181F; + } .panel h1 { font-size: 19px; font-weight: 800; letter-spacing: -0.01em; margin-bottom: 4px; } .panel .sub { font-size: 13px; color: #8A8F9B; margin-bottom: 22px; line-height: 1.5; } .panel a { color: #8A8F9B; } .group { margin-bottom: 20px; } - .group > label { + .group > label, + .group > summary { display: block; font-size: 11px; font-weight: 700; letter-spacing: 0.09em; text-transform: uppercase; color: #6E7480; margin-bottom: 9px; } + /* Zugeklappte Kategorien: der Marker ersetzt den Standard-Pfeil, damit die + Zeile bündig zur offenen Gruppe bleibt. */ + details.group > summary { + cursor: pointer; list-style: none; margin-bottom: 0; + padding: 5px 0; display: flex; align-items: center; gap: 7px; + transition: color .15s; + } + details.group > summary::-webkit-details-marker { display: none; } + details.group > summary::before { + content: "▸"; font-size: 10px; color: #4E5360; transition: transform .15s; + } + details.group[open] > summary::before { transform: rotate(90deg); } + details.group > summary:hover { color: #C7CCD6; } + details.group[open] > summary { color: #8A8F9B; margin-bottom: 9px; } + details.group { margin-bottom: 4px; } + details.group[open] { margin-bottom: 20px; } .opts { display: grid; grid-template-columns: 1fr 1fr; gap: 7px; } .opts.one { grid-template-columns: 1fr; } @@ -123,13 +144,64 @@ .status.err { color: #EB7C6B; } /* ─── Stage ──────────────────────────────────────────────────────────── */ - .stage { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 12px; } + /* ─── Arbeitsfläche ─────────────────────────────────────────────────────── + Klar abgesetzt von der Sidebar: eigener Grund, eigener Scroll, eigener + Zoom. Was hier passiert, lässt die Bedienleiste unberührt. */ + .stage { + flex: 1; position: relative; height: 100vh; overflow: hidden; + background-color: #101116; + background-image: radial-gradient(circle, #282B35 1.6px, transparent 1.6px); + background-size: 24px 24px; + } + /* overscroll-behavior contain hält die Wisch-Geste des Browsers zurück: + Ohne das springt horizontales Trackpad-Scrollen am linken Rand auf den + vorigen Tab, statt im Canvas zu bleiben. */ + .scroller { + position: absolute; inset: 0; overflow: auto; + overscroll-behavior: contain; + } + .scroller.panning { cursor: grabbing; } + .scroller.panready { cursor: grab; } + /* width/height: max-content ist hier entscheidend. Mit reinem + "justify-content: center" ragt breiter Inhalt über beide Seiten hinaus, + aber scrollLeft kann nicht negativ werden: Der linke Teil wäre dann + unerreichbar. So wächst die Fläche mit dem Inhalt, das Zentrieren greift + nur noch, solange etwas Platz übrig ist. */ + .canvas { + width: max-content; min-width: 100%; + height: max-content; min-height: 100%; + box-sizing: border-box; padding: 60px; + display: flex; align-items: center; justify-content: center; + } + + .hud { + position: absolute; left: 0; right: 0; bottom: 0; + display: flex; align-items: center; justify-content: space-between; + gap: 16px; padding: 14px 20px; pointer-events: none; + background: linear-gradient(transparent, rgba(16,17,22,.92) 55%); + } + .hud > * { pointer-events: auto; } .stage-meta { font-size: 12px; color: #6E7480; letter-spacing: .04em; } + + .zoom { display: flex; align-items: center; gap: 4px; } + .zoom button { + font-family: inherit; font-size: 13px; font-weight: 700; cursor: pointer; + background: #1D1F26; color: #C7CCD6; border: 1px solid #2E313B; + border-radius: 7px; padding: 6px 10px; min-width: 32px; + } + .zoom button:hover { background: #262932; } + .zoom .level { min-width: 58px; text-align: center; color: #8A8F9B; font-variant-numeric: tabular-nums; } /* Der Streifen zeigt alle Slides nebeneinander und lückenlos, damit der durchgehende Aufbau beim Arbeiten sichtbar ist. Der Rahmen reserviert die skalierte Fläche; die Slides selbst bleiben immer echte 1080 x 1350. */ .strip-wrap { overflow: hidden; flex: none; align-self: flex-start; } - .strip { display: flex; transform-origin: top left; } + /* Bei gebrochenem Maßstab (etwa 41 %) ist eine Slide 442,8 px breit. Der + Browser rundet jede Box einzeln, und zwischen ihnen blitzt für Sekunden- + bruchteile eines Pixels der dunkle Grund durch. Der Streifen bekommt + deshalb die Slide-Farbe als Untergrund: Die Fuge zeigt dann dieselbe Farbe + und verschwindet. Im Export gibt es sie ohnehin nicht, dort wird jede + Slide einzeln in exakten 1080 x 1350 gerendert. */ + .strip { display: flex; transform-origin: top left; background: #30313D; } .holder { position: relative; flex: none; cursor: pointer; } /* Auswahlrahmen sitzt auf dem Holder, nicht auf der Slide, damit er nie im Export landet. */ @@ -345,6 +417,9 @@ 22px 0, calc(100% - 22px) 0, 100% 22px, 100% calc(100% - 22px), calc(100% - 22px) 100%, 22px 100%, 0 calc(100% - 22px), 0 22px ); + /* 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)); @@ -363,6 +438,57 @@ calc(100% - 14px) 100%, 14px 100%, 0 calc(100% - 14px), 0 14px ); } + + .slide[data-frame="rund"] .mock { clip-path: none; border-radius: 30px; } + .slide[data-frame="rund"] .mock-inner { clip-path: none; border-radius: 20px; } + + .slide[data-frame="aus"] .mock { + clip-path: none; padding: 0; background: none; border-radius: 0; + } + .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. */ + .sheet-back, .sheet-mid { + display: none; position: absolute; inset: 0; z-index: 0; + background: #fff; border-radius: 22px; overflow: hidden; + } + .slide[data-frame="stapel"] .mock { + clip-path: none; padding: 0; background: none; filter: 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); + } + .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. */ + .sheet-back img { + position: absolute; top: 0; left: 0; width: 100%; + 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. */ @@ -539,29 +665,29 @@
-
- +
+ Variante
-
+ -
- +
+ Layout (nur diese Slide)
-
+ -
- +
+ Tag optisch ausgerückt
-
+ -
- +
+ Bearbeiten
@@ -575,31 +701,31 @@
-
+
Standardwerte
-
- +
+ Ansicht
-
+ -
- +
+ Durchgehend (alle Slides)
-
+ -
- +
+ Muster (nur diese Slide)
@@ -609,32 +735,47 @@
-
+ -
- +
+ Intensität
-
+ -
- +
+ Bildrahmen +
+ + + + +
+ +
+ + +
+
+ +
+ Mockup-Perspektive
-
+ -
- +
+ Renderqualität
-
+
@@ -644,26 +785,40 @@
-
- +
+ Tastatur
←→ Slide · V Variante
L Layout · M Muster · I Intensität
C Durchgehend · W Ansicht · G Tag
+ R Bildrahmen · S Schatten
T Perspektive
E Bearbeiten · Pfeile verschieben (⇧ = 10 px)
+ + − 0 Zoom · ⌘/Strg + Rad
+ Leer + ziehen verschiebt die Fläche
D Slide laden · A alle als ZIP
-
+
Lade Schriften und Screens ...
-
-
-
+
+
+
+
+
+
+
+
+
+

+
+ + + +
-

Vorschau 45 %, Export in voller Auflösung

@@ -707,7 +862,8 @@ head: 'Du siehst auf einen Blick,
wo jeder Auftrag steht.', body: "Projekte, Kunden, Angebote, F\u00f6rderantr\u00e4ge. Kein Suchen, kein Nachfragen, kein Zettelchaos.", img: "./assets/screens/pipelines.png", - imgScale: 1.28, imgX: "-2%", imgY: "-4%", + mockAR: 1.8519, // 2000 x 1080 + imgScale: 1, imgX: "0%", imgY: "0%", }, { key: "workflow-engine", type: "feature", @@ -716,7 +872,8 @@ head: 'Wenn-dann
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.", img: "./assets/screens/workflow.png", - imgScale: 1.34, imgX: "-8%", imgY: "-22%", + mockAR: 1.5038, // 2000 x 1330 + imgScale: 1, imgX: "0%", imgY: "0%", }, { key: "chat", type: "feature", @@ -725,7 +882,9 @@ head: 'Jedes Projekt bekommt
automatisch seinen Kanal.', 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.", img: "./assets/screens/chat.png", - imgScale: 1.30, imgX: "-14%", imgY: "-8%", + // 2000 x 1357. Etwas schmaler, sonst reicht die Hoehe unter dem Text nicht. + mockW: 1070, mockAR: 1.4738, + imgScale: 1, imgX: "0%", imgY: "0%", }, { key: "form-builder", type: "feature", @@ -754,18 +913,35 @@ "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" }, - "mock": { label: "Bild", size: null }, - "hex-frame": { label: "Wabe", size: null }, + "mock": { label: "Bild", size: null, prop: "width" }, + "hex-frame": { label: "Wabe", size: 778, prop: "width" }, }; const EDIT_KEYS = Object.keys(STANDARD); + /* Der Standard des Bildrahmens hängt an der Slide: Der Form Builder hat ein + 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; + return (STANDARD[key] || {}).size; + } + + // Schriftgrößen in 2er-Schritten, Breiten in 2 % ihrer Ausgangsgröße: + // 2 px auf ein 1090 px breites Bild würde man nie sehen. + function stepFor(key, i) { + const std = STANDARD[key]; + if (!std || !std.prop) return 0; + if (std.prop === "font-size") return 2; + return Math.max(4, Math.round(stdSize(key, i) * 0.02)); + } + let edits = SLIDES.map(() => ({})); let picked = null; // { slide, key } let editMode = "aus"; let current = 0; - let variant = "dunkel", tilt = "an", superSample = 2, continuous = "aus", view = "streifen", tagOut = "an"; + let variant = "dunkel", tilt = "an", superSample = 2, continuous = "aus", view = "streifen", tagOut = "an", frameStyle = "chamfer", shadow = "an"; /* ─── Projektstand ──────────────────────────────────────────────────────── Alles, was am Projekt hängt, wird gespeichert: Einstellungen, Layouts, @@ -777,7 +953,7 @@ function saveState() { try { localStorage.setItem(STORE, JSON.stringify({ - v: 1, variant, tilt, superSample, continuous, view, tagOut, + v: 1, variant, tilt, superSample, continuous, view, tagOut, frameStyle, shadow, current, layoutState, bgState, edits, })); markSaved(); @@ -805,6 +981,8 @@ continuous = d.continuous || continuous; 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; } @@ -834,14 +1012,40 @@ /* Der Streifen wird auf die verfügbare Breite skaliert, die Einzelansicht auf feste 45 %. Der Rahmen bekommt die skalierte Größe, damit die Seite nicht springt. */ + /* zoom === null bedeutet: automatisch einpassen. Sobald der Nutzer zoomt, + hält der Wert, bis er auf "Fit" zurückgeht. */ + let zoom = null; + + function stripWidth() { return view === "streifen" ? 1080 * SLIDES.length : 1080; } + + function autoFit() { + const scroller = document.getElementById("scroller"); + const availW = Math.max(320, scroller.clientWidth - 120); + const availH = Math.max(240, scroller.clientHeight - 120); + return Math.min(availW / stripWidth(), availH / 1350, 0.45); + } + function fitStrip() { const wrap = document.getElementById("strip-wrap"); - const total = 1080 * SLIDES.length; - const avail = Math.max(320, wrap.parentElement.clientWidth); - const k = view === "streifen" ? Math.min(avail / total, 0.45) : 0.45; + const k = zoom || autoFit(); deck.style.transform = "scale(" + k + ")"; - wrap.style.width = Math.round((view === "streifen" ? total : 1080) * k) + "px"; + wrap.style.width = Math.round(stripWidth() * k) + "px"; wrap.style.height = Math.round(1350 * k) + "px"; + const lvl = document.getElementById("zoom-fit"); + if (lvl) lvl.textContent = zoom ? Math.round(k * 100) + " %" : "Fit"; + } + + function setZoom(k, anchor) { + const scroller = document.getElementById("scroller"); + const before = zoom || autoFit(); + zoom = Math.min(2, Math.max(0.05, k)); + // Um den Mauszeiger zoomen, nicht um die linke obere Ecke. + const ratio = zoom / before; + const ax = anchor ? anchor.x : scroller.clientWidth / 2; + const ay = anchor ? anchor.y : scroller.clientHeight / 2; + fitStrip(); + scroller.scrollLeft = (scroller.scrollLeft + ax) * ratio - ax; + scroller.scrollTop = (scroller.scrollTop + ay) * ratio - ay; } const setStatus = (msg, kind) => { @@ -1030,7 +1234,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. + const withImg = SLIDES.filter(x => x.img); + const other = withImg[(withImg.findIndex(x => x.img === sl.img) + 1) % withImg.length]; mock.innerHTML = + '
' + + '
' + '
' + '
'; @@ -1066,6 +1277,8 @@ el.dataset.layout = layoutState[i]; 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; @@ -1115,6 +1328,7 @@ } function applyAll() { + deck.style.background = variant === "hell" ? "#FFFFFF" : "#30313D"; deck.querySelectorAll(".slide").forEach((el, i) => { el.dataset.variant = variant; el.dataset.tilt = tilt; @@ -1123,6 +1337,8 @@ el.dataset.layout = layoutState[i]; 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; }); @@ -1431,12 +1647,13 @@ saveState(); } - function resize(delta) { + function resize(dir) { if (!picked) return; const std = STANDARD[picked.key]; if (!std || !std.prop) return; + const base = stdSize(picked.key, picked.slide); const set = (edits[picked.slide][picked.key] = edits[picked.slide][picked.key] || {}); - set.size = Math.max(8, (set.size || std.size) + delta); + set.size = Math.max(8, (set.size || base) + dir * stepFor(picked.key, picked.slide)); applyEditsAll(); pick(picked.slide, picked.key); saveState(); @@ -1465,13 +1682,30 @@ if (!picked) { box.innerHTML = 'Nichts ausgewählt'; return; } const std = STANDARD[picked.key] || {}; const e = (edits[picked.slide] || {})[picked.key] || {}; - const val = e.size || std.size; + const base = stdSize(picked.key, picked.slide); + const val = e.size || base; const unit = std.prop === "font-size" ? "px Schrift" : "px breit"; box.innerHTML = '' + (std.label || picked.key) + ' · Slide ' + (picked.slide + 1) + '
' + - (std.prop ? val + " " + unit + (e.size ? " (Standard " + std.size + ")" : "") + "
" : "") + + (std.prop ? val + " " + unit + (e.size ? " (Standard " + base + ")" : "") + "
" : "") + "Versatz " + (e.dx || 0) + " / " + (e.dy || 0) + " px"; } + /* Welche Kategorien offen sind, ist Bedienvorliebe und gehört nicht in den + Projektstand: gilt für beide Projekte gleichermaßen. */ + (function rememberGroups() { + const KEY = "hivegrid.ui.gruppen"; + let open = []; + try { open = JSON.parse(localStorage.getItem(KEY) || "[]"); } catch (_) {} + document.querySelectorAll("details.group[data-grp]").forEach(d => { + if (open.includes(d.dataset.grp)) d.open = true; + d.addEventListener("toggle", () => { + const now = [...document.querySelectorAll("details.group[open][data-grp]")] + .map(x => x.dataset.grp); + try { localStorage.setItem(KEY, JSON.stringify(now)); } catch (_) {} + }); + }); + })(); + document.getElementById("forget").addEventListener("click", forgetProject); document.getElementById("editmodes").addEventListener("click", e => { const b = e.target.closest("button"); @@ -1485,8 +1719,8 @@ syncEditPanel(); } }); - document.getElementById("sz-plus").addEventListener("click", () => resize(2)); - document.getElementById("sz-minus").addEventListener("click", () => resize(-2)); + document.getElementById("sz-plus").addEventListener("click", () => resize(1)); + document.getElementById("sz-minus").addEventListener("click", () => resize(-1)); document.getElementById("reset-one").addEventListener("click", resetOne); document.getElementById("reset-all").addEventListener("click", resetAll); @@ -1520,10 +1754,56 @@ }); document.getElementById("views").addEventListener("click", e => { const b = e.target.closest("button"); - if (b) { view = b.dataset.w; deck.dataset.view = view; press("views", "w", view); show(current); saveState(); } + if (b) { view = b.dataset.w; deck.dataset.view = view; press("views", "w", view); zoom = null; show(current); saveState(); } }); window.addEventListener("resize", fitStrip); + /* Strg/Cmd + Rad zoomt, blankes Rad scrollt. Trackpad-Pinch schickt + ctrlKey mit, verhält sich also automatisch richtig. */ + document.getElementById("scroller").addEventListener("wheel", e => { + if (!e.ctrlKey && !e.metaKey) return; + e.preventDefault(); + const r = e.currentTarget.getBoundingClientRect(); + setZoom((zoom || autoFit()) * (e.deltaY < 0 ? 1.12 : 1 / 1.12), + { x: e.clientX - r.left, y: e.clientY - r.top }); + }, { passive: false }); + + /* Panning als verlässlicher Weg, unabhängig von Trackpad-Gesten: + Leertaste gedrückt halten und ziehen, oder mittlere Maustaste. */ + (function panning() { + const sc = document.getElementById("scroller"); + let space = false, active = null; + + document.addEventListener("keydown", e => { + if (e.code !== "Space" || e.target.matches("input, textarea")) return; + e.preventDefault(); + if (!space) { space = true; sc.classList.add("panready"); } + }); + document.addEventListener("keyup", e => { + if (e.code !== "Space") return; + space = false; sc.classList.remove("panready"); + }); + + sc.addEventListener("mousedown", e => { + if (!space && e.button !== 1) return; + e.preventDefault(); + active = { x: e.clientX, y: e.clientY, l: sc.scrollLeft, t: sc.scrollTop }; + sc.classList.add("panning"); + }); + document.addEventListener("mousemove", e => { + if (!active) return; + sc.scrollLeft = active.l - (e.clientX - active.x); + sc.scrollTop = active.t - (e.clientY - active.y); + }); + document.addEventListener("mouseup", () => { + active = null; sc.classList.remove("panning"); + }); + })(); + + document.getElementById("zoom-in").addEventListener("click", () => setZoom((zoom || autoFit()) * 1.25)); + document.getElementById("zoom-out").addEventListener("click", () => setZoom((zoom || autoFit()) / 1.25)); + document.getElementById("zoom-fit").addEventListener("click", () => { zoom = null; fitStrip(); }); + nav.addEventListener("click", e => { const b = e.target.closest("button"); if (b) show(Number(b.dataset.n)); }); @@ -1547,6 +1827,14 @@ const b = e.target.closest("button"); if (b) { bgState[current].int = b.dataset.i; syncBgButtons(); applyAll(); saveState(); } }); + document.getElementById("frames").addEventListener("click", e => { + 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(); } @@ -1593,8 +1881,18 @@ syncBgButtons(); applyAll(); } else if (k === "t") { tilt = tilt === "an" ? "aus" : "an"; press("tilts", "t", tilt); applyAll(); saveState(); } - else if (k === "w") { view = view === "streifen" ? "einzeln" : "streifen"; deck.dataset.view = view; press("views", "w", view); show(current); } + else if (e.key === "+" || e.key === "=") { setZoom((zoom || autoFit()) * 1.25); } + else if (e.key === "-") { setZoom((zoom || autoFit()) / 1.25); } + else if (e.key === "0") { zoom = null; fitStrip(); } + else if (k === "w") { view = view === "streifen" ? "einzeln" : "streifen"; deck.dataset.view = view; press("views", "w", view); zoom = null; show(current); } else if (k === "e") { document.querySelector('#editmodes button[data-e="' + (editMode === "an" ? "aus" : "an") + '"]').click(); } + 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(); + } + 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)(); @@ -1616,6 +1914,8 @@ press("quality", "q", String(superSample)); 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");