Post 1 Alt und Post 2 entfernt

Beide Projekte werden nicht mehr gebraucht. Weg sind post1-alt.html,
index.html und state/post2.json, dazu der lokale state/post1-alt.json.

Mitgezogen, damit nichts ins Leere zeigt: die Projektumschaltung im
Bedienfeld samt ihrer CSS-Regeln, die Projekttabelle und die Liste der
Stand-Dateien in der README, die Startzeile und der Aufbau in rules.md.

Achtung: Ohne index.html liefert http://127.0.0.1:4321/ nur noch die
Verzeichnisliste. Der Einstieg ist /post1.html.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
dejanandClaude Opus 5 committed 2026-08-07 17:18:56 +02:00
1 parent 713cac113d
commit 21e1d3009f
6 files changed
+6 -3059

No files matched your search

+5 -7
View File
@@ -21,11 +21,10 @@ Downloads landen direkt in `~/Downloads`, ohne Browser-Dialog.
| Projekt | Datei | Inhalt |
|---|---|---|
| Post 1 · Lösung | `post1.html` | Carousel, 5 Slides |
| Post 2 · Statement | `index.html` | Einzelbild |
Oben im Bedienfeld wechselt man zwischen ihnen. **Jedes Projekt speichert
seinen Stand** — Einstellungen, Layouts, Muster und alle Verschiebungen aus dem
Canvas überleben einen Reload. „Projekt zurücksetzen" wirft ihn weg.
**Das Projekt speichert seinen Stand** — Einstellungen, Layouts, Muster und
alle Verschiebungen aus dem Canvas überleben einen Reload. „Projekt
zurücksetzen" wirft ihn weg.
### Der Stand liegt im Repo
@@ -34,9 +33,8 @@ Nicht nur im Browser: jede Änderung landet auch in `state/<projekt>.json`.
nach `git pull` denselben Stand.
```
state/post1.json Post 1 · Lösung
state/post1-alt.json Post 1 · Alt
state/post2.json Post 2 · Statement
state/post1.json Post 1 · Lösung
state/post1-merk.json Rückfallpunkt
```
Beim Laden gewinnt die Datei, der localStorage ist nur der Rückfall, wenn der
-626
View File
@@ -1,626 +0,0 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>HiveGrid Post-Generator</title>
<link rel="stylesheet" href="./assets/fonts/manrope.css">
<style>
/* ─── HiveGrid design tokens (mirrored from app/globals.css) ─────────────
Deliberately plain hex, no Tailwind/oklch: the export libraries choke on
modern colour functions, so the slide subtree stays on values they can
serialise. */
:root {
--accent-yellow: #F1B729;
--accent-teal: #0D6E6E;
--surface: #FFFFFF;
--bg-primary: #FAFAFA;
--bg-dark: #0F1A2E;
--text-primary: #1A1A1A;
--text-secondary:#555555;
--text-on-dark: #C8D0DC;
--text-white: #FFFFFF;
}
* { 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;
height: 100vh; overflow: hidden; display: flex;
}
/* ─── Projekt-Navigation ─────────────────────────────────────────────── */
.projects { display: flex; gap: 6px; margin-bottom: 18px; }
.projects a {
flex: 1; text-align: center; text-decoration: none;
font-size: 12px; font-weight: 700; padding: 9px 8px; border-radius: 8px;
background: #22242C; color: #8A8F9B; border: 1px solid #2E313B;
}
.projects a:hover { background: #2A2D36; color: #C7CCD6; }
.projects a.here { background: var(--accent-yellow); color: #1A1A1A; border-color: var(--accent-yellow); }
.saved {
font-size: 11px; color: #6FCF97; opacity: 0; transition: opacity .2s;
margin-left: 6px;
}
.saved.on { opacity: 1; }
.forget { font-size: 11px; color: #6E7480; background: none; border: 0;
cursor: pointer; padding: 0; font-family: inherit; }
.forget:hover { color: #EB7C6B; }
/* ─── Control panel ──────────────────────────────────────────────────── */
.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; }
.group { margin-bottom: 22px; }
.group > label {
display: block; font-size: 11px; font-weight: 700; letter-spacing: 0.09em;
text-transform: uppercase; color: #6E7480; margin-bottom: 9px;
}
.opts { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.opts button {
font-family: inherit; font-size: 13px; font-weight: 600;
padding: 11px 10px; border-radius: 8px; cursor: pointer;
background: #22242C; color: #C7CCD6; border: 1px solid #2E313B;
transition: background .15s, border-color .15s, color .15s;
display: flex; align-items: center; gap: 8px;
}
.opts button:hover { background: #2A2D36; }
.opts button[aria-pressed="true"] {
background: var(--accent-yellow); color: #1A1A1A; border-color: var(--accent-yellow);
}
.swatch { width: 12px; height: 12px; border-radius: 3px; flex: none; border: 1px solid rgba(0,0,0,.18); }
.row { display: flex; gap: 8px; }
.row button, .btn {
font-family: inherit; font-size: 13.5px; font-weight: 700;
padding: 13px 14px; border-radius: 8px; cursor: pointer; border: none;
background: var(--accent-yellow); color: #1A1A1A; flex: 1; transition: filter .15s;
}
.row button:hover, .btn:hover { filter: brightness(1.07); }
.btn.secondary { background: #22242C; color: #C7CCD6; border: 1px solid #2E313B; }
.btn:disabled { opacity: .45; cursor: not-allowed; filter: none; }
.keys { font-size: 12px; color: #6E7480; line-height: 1.9; }
.keys kbd {
background: #22242C; border: 1px solid #343843; border-bottom-width: 2px;
border-radius: 4px; padding: 1px 6px; font-family: ui-monospace, monospace;
font-size: 11px; color: #C7CCD6;
}
.status { margin-top: 14px; font-size: 12.5px; color: #6E7480; min-height: 18px; }
.status.ok { color: #6FCF97; }
.status.err { color: #EB7C6B; }
/* ─── Stage ──────────────────────────────────────────────────────────── */
/* 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;
the slide itself always stays at true 1080×1350 so the export is
pixel-exact with no resampling. */
.slide-wrap { width: 486px; height: 607.5px; flex: none; }
.slide-scaler { transform: scale(0.45); transform-origin: top left; }
/* ─── The slide — exactly Instagram 4:5 (1080 × 1350) ────────────────── */
.slide {
width: 1080px; height: 1350px;
position: relative; overflow: hidden;
display: flex; align-items: center; justify-content: center;
font-family: 'Manrope', system-ui, sans-serif;
}
/* Honeycomb — a real seamless tile across the full 1080×1350, generated in
JS from the same G polygon as the mark. Full-bleed on purpose: cropping it
inside a card made it read as a stray texture instead of a ground. */
.pattern { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
.slide[data-pattern="aus"] .pattern { display: none; }
/* Chamfered frame as an outline, not a filled plate — a filled card would
cover the pattern, which is exactly what made the last version look cut off. */
.frame { position: absolute; inset: 40px; pointer-events: none; }
.slide[data-frame="aus"] .frame { display: none; }
.content {
position: relative; z-index: 1;
display: flex; flex-direction: column; align-items: center;
text-align: center; padding: 0 104px;
}
/* Eyebrow as a proper label: uppercase, tracked out, single line. Sentence
case at this size wrapped onto two lines and read like body copy. */
.eyebrow {
font-size: 30px; font-weight: 700;
letter-spacing: 0.15em; text-transform: uppercase;
line-height: 1.2; white-space: nowrap;
margin-bottom: 84px;
}
/* Top accent = colour (the marker is reserved for the payoff below). */
.eyebrow .a { color: var(--accent-on); }
.mark { height: 320px; width: auto; display: block; }
.rule { width: 104px; height: 6px; border-radius: 3px; margin: 74px 0 34px; }
.tagline {
font-size: 88px; font-weight: 800;
letter-spacing: -0.028em; line-height: 1.04;
}
/* Bottom accent = marker. */
.tagline .a {
background: linear-gradient(transparent 58%, var(--marker) 58%, var(--marker) 94%, transparent 94%);
-webkit-box-decoration-break: clone;
box-decoration-break: clone;
padding: 0 6px;
}
/* ─── Variant: hell ──────────────────────────────────────────────────── */
.slide[data-variant="hell"] {
background: var(--surface);
--accent-on: #C8901A; --marker: var(--accent-yellow);
--pattern-color: var(--accent-yellow); --pattern-opacity: .085;
--frame-color: #E5E5E5;
}
.slide[data-variant="hell"] .eyebrow { color: var(--text-secondary); }
.slide[data-variant="hell"] .tagline { color: var(--text-primary); }
.slide[data-variant="hell"] .mark { color: var(--accent-yellow); }
.slide[data-variant="hell"] .rule { background: var(--accent-yellow); }
/* ─── Variant: dunkel ────────────────────────────────────────────────── */
.slide[data-variant="dunkel"] {
background: var(--bg-dark);
--accent-on: var(--accent-yellow); --marker: var(--accent-yellow);
--pattern-color: var(--accent-yellow); --pattern-opacity: .10;
--frame-color: rgba(255,255,255,.16);
}
.slide[data-variant="dunkel"] .eyebrow { color: var(--text-on-dark); }
.slide[data-variant="dunkel"] .tagline { color: var(--text-white); }
.slide[data-variant="dunkel"] .mark { color: var(--accent-yellow); }
.slide[data-variant="dunkel"] .rule { background: var(--accent-yellow); }
.slide[data-variant="dunkel"] .tagline .a { color: #1A1A1A; }
/* ─── Variant: gelb ──────────────────────────────────────────────────── */
.slide[data-variant="gelb"] {
background: var(--accent-yellow);
--accent-on: #FFFFFF; --marker: #1A1A1A;
--pattern-color: #1A1A1A; --pattern-opacity: .07;
--frame-color: rgba(26,26,26,.22);
}
.slide[data-variant="gelb"] .eyebrow { color: #6B5210; }
.slide[data-variant="gelb"] .tagline { color: #1A1A1A; }
.slide[data-variant="gelb"] .mark { color: #1A1A1A; }
.slide[data-variant="gelb"] .rule { background: #1A1A1A; }
.slide[data-variant="gelb"] .tagline .a { color: var(--accent-yellow); }
/* ─── Variant: teal ──────────────────────────────────────────────────── */
.slide[data-variant="teal"] {
background: var(--accent-teal);
--accent-on: var(--accent-yellow); --marker: var(--accent-yellow);
--pattern-color: #FFFFFF; --pattern-opacity: .07;
--frame-color: rgba(255,255,255,.18);
}
.slide[data-variant="teal"] .eyebrow { color: #A8D4D4; }
.slide[data-variant="teal"] .tagline { color: var(--text-white); }
.slide[data-variant="teal"] .mark { color: var(--accent-yellow); }
.slide[data-variant="teal"] .rule { background: var(--accent-yellow); }
.slide[data-variant="teal"] .tagline .a { color: var(--accent-teal); }
</style>
</head>
<body>
<aside class="panel">
<h1>Post-Generator</h1>
<div class="projects"><a class="" href="./post1.html">Post 1</a><a class="" href="./post1-alt.html">Post 1 · Alt</a><a class="here" href="./index.html">Post 2</a></div>
<p class="sub">Einzelbild · 1080 × 1350 px<span class="saved" id="saved"></span><br><button class="forget" id="forget">Projekt zurücksetzen</button></p>
<div class="group">
<label>Variante</label>
<div class="opts" id="variants">
<button data-v="hell" aria-pressed="true"><span class="swatch" style="background:#FFFFFF"></span>Hell</button>
<button data-v="dunkel" aria-pressed="false"><span class="swatch" style="background:#0F1A2E"></span>Dunkel</button>
<button data-v="gelb" aria-pressed="false"><span class="swatch" style="background:#F1B729"></span>Gelb</button>
<button data-v="teal" aria-pressed="false"><span class="swatch" style="background:#0D6E6E"></span>Teal</button>
</div>
</div>
<div class="group">
<label>Wabenraster</label>
<div class="opts" id="patterns">
<button data-p="an" aria-pressed="true">An</button>
<button data-p="aus" aria-pressed="false">Aus</button>
</div>
</div>
<div class="group">
<label>Dichte</label>
<div class="opts" id="densities">
<button data-d="0.40" aria-pressed="false">Fein</button>
<button data-d="0.55" aria-pressed="true">Mittel</button>
<button data-d="0.75" aria-pressed="false">Grob</button>
<button data-d="1.00" aria-pressed="false">XL</button>
</div>
</div>
<div class="group">
<label>Rahmen</label>
<div class="opts" id="frames">
<button data-f="an" aria-pressed="true">An</button>
<button data-f="aus" aria-pressed="false">Aus</button>
</div>
</div>
<div class="group">
<label>Download</label>
<div class="row" style="margin-bottom:8px">
<button id="dl-png" disabled>PNG</button>
<button id="dl-jpg" class="btn secondary" disabled>JPG</button>
</div>
<button id="dl-all" class="btn secondary" style="width:100%" disabled>Alle 4 Varianten als ZIP</button>
</div>
<div class="group" style="margin-bottom:0">
<label>Tastatur</label>
<div class="keys">
<kbd>1</kbd>–<kbd>4</kbd> Variante · <kbd>P</kbd> Raster · <kbd>R</kbd> Rahmen<br>
<kbd>D</kbd> PNG laden · <kbd>A</kbd> alle als ZIP
</div>
</div>
<div class="status" id="status">Lade Schriften und Assets ...</div>
</aside>
<main class="stage">
<div class="slide-wrap">
<div class="slide-scaler">
<!-- ─── SLIDE ─────────────────────────────────────────────────────── -->
<div class="slide" id="slide" data-variant="hell" data-pattern="an" data-frame="an">
<svg class="pattern" id="pattern" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"></svg>
<svg class="frame" viewBox="0 0 1000 1270" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<polygon points="34,0 966,0 1000,34 1000,1236 966,1270 34,1270 0,1236 0,34"
stroke="var(--frame-color)" stroke-width="3"/>
</svg>
<div class="content">
<p class="eyebrow">Vom <span class="a">Handwerker</span> für den <span class="a">Handwerker</span></p>
<!-- G mark only, lifted straight out of logo.svg (the single yellow
polygon), normalised to a 0,0-based 384.5 × 332.9 box. Inline
rather than <img> so each variant recolours it via currentColor. -->
<svg class="mark" viewBox="0 0 384.5 332.9" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="HiveGrid">
<polygon fill="currentColor" 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"/>
</svg>
<div class="rule"></div>
<p class="tagline"><span class="a">Verbinden</span><br>statt ersetzen</p>
</div>
</div>
<!-- ─── /SLIDE ────────────────────────────────────────────────────── -->
</div>
</div>
<p class="stage-meta">Vorschau 45 %, Export in voller Auflösung</p>
</main>
<script src="./vendor/html-to-image.js"></script>
<script src="./vendor/jszip.min.js"></script>
<script>
(function () {
"use strict";
/* A syntax or temporal-dead-zone error at this level kills the whole script
silently: the panel renders from static markup, but nothing is ever built
and no status is set. Surface it instead of showing an empty stage. */
window.addEventListener("error", e => {
const el = document.getElementById("status");
if (el) {
el.textContent = "Skriptfehler: " + (e.message || e.error);
el.className = "status err";
}
});
const slide = document.getElementById("slide");
const statusEl = document.getElementById("status");
const patternEl = document.getElementById("pattern");
const VARIANTS = ["hell", "dunkel", "gelb", "teal"];
const SVG_NS = "http://www.w3.org/2000/svg";
// The G mark, normalised to a 0,0-based box.
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;
let fontEmbedCss = "";
/* ─── Projektstand ────────────────────────────────────────────────────────
Gleiches Prinzip wie bei Post 1: Das Werkzeug hält den Stand. */
const PROJECT = "post2";
const STORE = "hivegrid.projekt." + PROJECT;
const STATE_URL = "./state/" + PROJECT + ".json";
let savedTimer = null;
function markSaved() {
const el = document.getElementById("saved");
if (!el) return;
el.textContent = "gespeichert";
el.classList.add("on");
clearTimeout(savedTimer);
savedTimer = setTimeout(() => el.classList.remove("on"), 1200);
}
function currentState() {
return {
v: 1,
variant: slide.dataset.variant,
pattern: slide.dataset.pattern,
frame: slide.dataset.frame,
density: currentDensity,
};
}
/* Der Stand geht in zwei Ablagen. Sofort in den localStorage, damit die
Rückmeldung nicht auf das Netz wartet, und gebündelt in
state/<projekt>.json. Nur die Datei lässt sich pushen, also ist sie die
Wahrheit; der localStorage ist der Rückfall, wenn der Server nicht läuft. */
let pushTimer = null;
function saveState() {
const state = currentState();
try { localStorage.setItem(STORE, JSON.stringify(state)); }
catch (err) { console.warn("Speichern fehlgeschlagen", err); }
markSaved();
clearTimeout(pushTimer);
pushTimer = setTimeout(() => pushState(state), 600);
}
async function pushState(state) {
pushTimer = null;
try {
const res = await fetch("/state?project=" + PROJECT, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(state),
});
if (!res.ok) throw new Error("HTTP " + res.status);
} catch (err) { console.warn("Stand nicht in die Datei geschrieben", err); }
}
// Ein Reload darf keine Änderung verschlucken, die noch im Timer hängt.
addEventListener("beforeunload", () => {
if (!pushTimer) return;
clearTimeout(pushTimer);
navigator.sendBeacon("/state?project=" + PROJECT,
new Blob([JSON.stringify(currentState())], { type: "application/json" }));
});
async function loadState() {
let d = null, fromFile = false;
// Erst die Datei aus dem Repo, dann der Browser. Wer das Projekt frisch
// gezogen hat, soll den gepushten Stand sehen und nicht seinen eigenen alten.
try {
const res = await fetch(STATE_URL, { cache: "no-store" });
if (res.ok) { d = await res.json(); fromFile = true; }
} catch (_) {}
if (!d) {
try { d = JSON.parse(localStorage.getItem(STORE) || "null"); } catch (_) {}
}
if (!d || d.v !== 1) return false;
// Stand aus dem Browser, aber noch keine Datei: einmalig hinueberschreiben,
// sonst bliebe ein ueber Wochen gewachsener Stand ungeteilt liegen.
if (!fromFile) setTimeout(() => pushState(currentState()), 0);
if (d.variant) setVariant(d.variant);
if (d.pattern) setPattern(d.pattern);
if (d.frame) setFrame(d.frame);
if (d.density) setDensity(d.density);
return true;
}
function forgetProject() {
clearTimeout(pushTimer); pushTimer = null;
try { localStorage.removeItem(STORE); } catch (_) {}
// Auch die Datei muss weg, sonst spielt sie den alten Stand beim nächsten
// Laden wieder ein.
fetch("/state?project=" + PROJECT, { method: "DELETE" })
.catch(() => {})
.then(() => location.reload());
}
function setStatus(msg, kind) {
statusEl.textContent = msg;
statusEl.className = "status" + (kind ? " " + kind : "");
}
/* Builds a seamless flat-top honeycomb tile.
For flat-top hexagons the columns step by 3/4 of the width and neighbouring
columns are offset vertically by half a row, so one tile = two columns wide
and one row high. The copies hanging outside the tile are what makes the
repeat seamless — without them you get the visible seams of the last try. */
function buildPattern(scale) {
const colStep = G_W * 0.75; // 288.4
const rowStep = G_H; // 332.9
const tileW = colStep * 2; // 576.8
const tileH = rowStep;
// Hex centres that touch this tile, including the ones bleeding over the edges.
const centres = [
[0, 0], [tileW, 0], [0, tileH], [tileW, tileH],
[colStep, rowStep / 2], [colStep, -rowStep / 2], [colStep, tileH + rowStep / 2],
];
while (patternEl.firstChild) patternEl.removeChild(patternEl.firstChild);
const defs = document.createElementNS(SVG_NS, "defs");
const pat = document.createElementNS(SVG_NS, "pattern");
pat.setAttribute("id", "hexpat");
pat.setAttribute("patternUnits", "userSpaceOnUse");
pat.setAttribute("width", tileW);
pat.setAttribute("height", tileH);
pat.setAttribute("patternTransform", "scale(" + scale + ")");
for (const [cx, cy] of centres) {
const p = document.createElementNS(SVG_NS, "polygon");
p.setAttribute("points", G_POINTS);
p.setAttribute("fill", "currentColor");
p.setAttribute("transform", "translate(" + (cx - G_W / 2) + "," + (cy - G_H / 2) + ")");
pat.appendChild(p);
}
defs.appendChild(pat);
patternEl.appendChild(defs);
const rect = document.createElementNS(SVG_NS, "rect");
rect.setAttribute("width", "100%");
rect.setAttribute("height", "100%");
rect.setAttribute("fill", "url(#hexpat)");
patternEl.appendChild(rect);
// Colour + opacity come from the active variant.
patternEl.style.color = "var(--pattern-color)";
patternEl.style.opacity = "var(--pattern-opacity)";
}
function press(containerId, attr, value) {
document.querySelectorAll("#" + containerId + " button").forEach(b =>
b.setAttribute("aria-pressed", String(b.dataset[attr] === value))
);
}
function setVariant(v) { slide.dataset.variant = v; press("variants", "v", v); }
function setPattern(p) { slide.dataset.pattern = p; press("patterns", "p", p); }
function setFrame(f) { slide.dataset.frame = f; press("frames", "f", f); }
let currentDensity = "0.55";
function setDensity(d) { currentDensity = d; buildPattern(Number(d)); press("densities", "d", d); }
/* Renders at pixelRatio 1 against a node that is already 1080×1350, so the
output is exactly Instagram's spec with no upscaling artefacts. */
function renderOptions() {
return {
width: 1080, height: 1350, pixelRatio: 1,
fontEmbedCSS: fontEmbedCss,
// The scaler transform is a preview-only concern — neutralise it for the
// export, otherwise the bitmap comes out at 45 %.
style: { transform: "none", transformOrigin: "top left" },
};
}
/* A 1080x1350 PNG as a data: URL runs to several megabytes. Browsers routinely
ignore the download attribute on those and navigate to the URL instead,
which shows up as a blank tab. Blob URLs are handled properly. */
/* Local-only save endpoint, see serve.py. */
async function downloadBlob(blob, filename) {
const res = await fetch("/save", {
method: "POST",
headers: { "X-Filename": filename },
body: blob,
});
if (!res.ok) throw new Error("Speichern fehlgeschlagen: HTTP " + res.status);
return (await res.json()).path;
}
const nameFor = ext => "hivegrid-post2-statement-" + slide.dataset.variant + "." + ext;
async function downloadPng() {
setStatus("Rendere PNG ...");
const blob = await htmlToImage.toBlob(slide, renderOptions());
if (!blob) throw new Error("Rendern lieferte kein Bild");
downloadBlob(blob, nameFor("png"));
setStatus("PNG gespeichert. 1080 × 1350", "ok");
}
async function downloadJpg() {
setStatus("Rendere JPG ...");
const opts = Object.assign(renderOptions(), { quality: 0.9 });
const url = await htmlToImage.toJpeg(slide, opts);
const blob = await (await fetch(url)).blob();
downloadBlob(blob, nameFor("jpg"));
setStatus("JPG gespeichert. Qualität 90 %", "ok");
}
async function downloadAll() {
const zip = new JSZip();
const keep = slide.dataset.variant;
for (const v of VARIANTS) {
setStatus("Rendere " + v + " ...");
setVariant(v);
await document.fonts.ready;
zip.file("hivegrid-post2-statement-" + v + ".png",
await htmlToImage.toBlob(slide, renderOptions()));
}
setVariant(keep);
setStatus("Packe ZIP ...");
downloadBlob(await zip.generateAsync({ type: "blob" }), "hivegrid-post2-statement.zip");
setStatus("4 Varianten als ZIP gespeichert", "ok");
}
function guard(fn) {
return () => fn().catch(err => {
console.error(err);
setStatus("Fehler: " + err.message, "err");
});
}
document.getElementById("variants").addEventListener("click", e => {
const b = e.target.closest("button"); if (b) { setVariant(b.dataset.v); saveState(); }
});
document.getElementById("patterns").addEventListener("click", e => {
const b = e.target.closest("button"); if (b) { setPattern(b.dataset.p); saveState(); }
});
document.getElementById("frames").addEventListener("click", e => {
const b = e.target.closest("button"); if (b) { setFrame(b.dataset.f); saveState(); }
});
document.getElementById("densities").addEventListener("click", e => {
const b = e.target.closest("button"); if (b) { setDensity(b.dataset.d); saveState(); }
});
document.getElementById("dl-png").addEventListener("click", guard(downloadPng));
document.getElementById("dl-jpg").addEventListener("click", guard(downloadJpg));
document.getElementById("dl-all").addEventListener("click", guard(downloadAll));
document.addEventListener("keydown", e => {
if (e.metaKey || e.ctrlKey || e.altKey) return;
const k = e.key.toLowerCase();
if (k >= "1" && k <= "4") setVariant(VARIANTS[Number(k) - 1]);
else if (k === "p") setPattern(slide.dataset.pattern === "an" ? "aus" : "an");
else if (k === "r") setFrame(slide.dataset.frame === "an" ? "aus" : "an");
else if (k === "d") guard(downloadPng)();
else if (k === "a") guard(downloadAll)();
});
/* Boot: build the tile, wait for Manrope to actually be resident (otherwise
the first export silently falls back to a system font), and pre-compute the
font CSS once instead of on every render. */
(async function boot() {
try {
buildPattern(0.55);
const restored = await loadState();
document.getElementById("forget").addEventListener("click", forgetProject);
await document.fonts.ready;
fontEmbedCss = await htmlToImage.getFontEmbedCSS(slide);
["dl-png", "dl-jpg", "dl-all"].forEach(id => document.getElementById(id).disabled = false);
setStatus(restored ? "Projektstand geladen" : "Bereit. 1080 × 1350 px", "ok");
} catch (err) {
console.error(err);
setStatus("Fehler beim Laden: " + err.message, "err");
}
})();
})();
</script>
</body>
</html>
-2407
View File
File diff suppressed because it is too large. Load diff
-10
View File
@@ -33,15 +33,6 @@
height: 100vh; overflow: hidden; display: flex;
}
/* ─── Projekt-Navigation ─────────────────────────────────────────────── */
.projects { display: flex; gap: 6px; margin-bottom: 18px; }
.projects a {
flex: 1; text-align: center; text-decoration: none;
font-size: 12px; font-weight: 700; padding: 9px 8px; border-radius: 8px;
background: #22242C; color: #8A8F9B; border: 1px solid #2E313B;
}
.projects a:hover { background: #2A2D36; color: #C7CCD6; }
.projects a.here { background: var(--accent-yellow); color: #1A1A1A; border-color: var(--accent-yellow); }
.saved {
font-size: 11px; color: #6FCF97; opacity: 0; transition: opacity .2s;
margin-left: 6px;
@@ -777,7 +768,6 @@
<aside class="panel">
<h1>Post 1 · Lösung</h1>
<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">
+1 -2
View File
@@ -4,7 +4,7 @@ Arbeitsgrundlage für alles, was in diesem Repo an Social-Grafiken entsteht.
**Stand:** 31.07.2026
Der Generator läuft lokal: `./serve.sh` → <http://127.0.0.1:4321/post1.html>
(Post 1, Carousel) bzw. `/index.html` (Post 2, Statement).
(Post 1, Carousel).
---
@@ -714,7 +714,6 @@ etwas ins Statusfeld schreibt. **Nicht entfernen.**
posts/
├── serve.py / serve.sh Statischer Server + /save-Endpunkt (Port 4321)
├── post1.html Post 1 · Lösung (Carousel, 5 Slides)
├── index.html Post 2 · Statement (Einzelbild)
├── rules.md diese Datei
├── assets/
│ ├── logo.svg, logo-white.svg, cta-honeycomb.svg
-7
View File
@@ -1,7 +0,0 @@
{
"density": "0.75",
"frame": "an",
"pattern": "an",
"v": 1,
"variant": "hell"
}