Sie bauen die Galerie, wir liefern die Produkte. Dieses Beispiel lädt die Produkte Ihres Mandanten per REST-API, zeigt sie als eigene Galerie und schaltet beim Klick das eingebettete iframe auf das Produkt um. Ein eigener Export-Button daneben rendert das MP4 – alles mit ein paar Zeilen JavaScript, ohne Framework, kopierbar, lokal testbar.
Ein API-Key für die REST-API (Authorization: Bearer …). Zum Ausprobieren dürfen Sie den öffentlichen Demo-Key unten verwenden – er liest nur die Demo-Produkte. Ihren eigenen Key bekommen Sie mit Ihrem Mandanten.
Eine freigeschaltete Domain für das iframe (frame-ancestors, siehe iframe-Anleitung, Schritt 1). Für den Demo-Mandanten ist zusätzlich http://localhost:8080 freigeschaltet – so testen Sie lokal, bevor Ihre Domain live ist.
Öffentlicher Demo-API-Key (nur Demo-Mandant, nur lesend)
Der Demo-Key kann jederzeit rotiert werden. Im Produktivbetrieb den Key nicht in öffentlichem Frontend-Code ablegen, wenn er mehr als Produkt-Lesen darf – dann die Produktliste über Ihr eigenes Backend durchreichen.
Genau dieser Port: Freigeschaltet ist http://localhost:8080 – nicht 127.0.0.1, nicht ein anderer Port. Auf jedem anderen Origin bleibt das iframe leer (die Galerie lädt trotzdem, weil die API von überall erreichbar ist).
CORS ist offen: Die API antwortet mit Access-Control-Allow-Origin: * und erlaubt den Authorization-Header – ein fetch direkt aus dem Browser funktioniert, ohne Proxy.
Schritt 2
Produkte per API laden
GET /v1/products liefert alle Produkte, die der API-Key sehen darf – paginiert (limit/offset, max. 200), optional gefiltert (?type=film|bild|clip, ?search=). Die vollständige Referenz steht in der API-Dokumentation.
Code ansehen JS
JS
const API = "https://api.virtualcampaign.dev";const API_KEY = "vc_4b3efe1be755a77ba77b95e67067d8e84176fdf7621ebd3b2d94e"; // demo key – replace with your ownconst SIMULATOR = "https://demo.kreativsimulator.com/"; // your tenant: https://<tenant>.kreativsimulator.com/// GET /v1/products → { products: [...], total, limit, offset, hasMore }async function loadProducts() { const res = await fetch(API + "/v1/products?limit=50", { headers: { Authorization: "Bearer " + API_KEY }, }); if (!res.ok) throw new Error("API error " + res.status); const data = await res.json(); return data.products; // [{ productCode, productName, productType, thumbnailUrl, placeholders, … }]}loadProducts().then((products) => console.log(products));
Die Felder, die Sie für eine Galerie brauchen
Feld
Bedeutung
productCode
Der Code, den das iframe für setProduct erwartet – der Schlüssel zwischen API und Simulator.
productName
Anzeigename für die Karte.
productType
film · clip · bild (Foto-Produkt mit Ken-Burns-Effekten).
durationSek
Länge in Sekunden – z. B. als Badge auf der Karte.
thumbnailUrl
Relative URL des Vorschaubilds (/v1/products/<code>/thumbnail). Ohne API-Key abrufbar – direkt in <img src> verwenden; ?res=ldpi|mdpi|hdpi wählt die Größe.
placeholders[]
loaderName + aspectRatio je Plakatfläche – für eigene Upload-Felder (siehe setCreative).
Die thumbnailUrl ist relativ – setzen Sie https://api.virtualcampaign.dev davor. Der Endpunkt leitet (302) auf das eigentliche JPEG weiter; der Browser folgt automatisch.
Schritt 3
Galerie bauen, iframe steuern
Aus der Produktliste entsteht pro Produkt eine Karte mit Vorschaubild und Name. Ein Klick sendet setProduct mit dem productCode an das iframe. Das iframe läuft im Kiosk-Modus (?embed=kiosk): nur der Canvas, Ihre Galerie ist die Bedienung.
Zwei Details machen die Galerie robust: Die Karten bleiben deaktiviert, bis das ready-Event kommt (vorher nimmt das iframe keine Befehle an). Und es werden nur Karten aktiv, deren Code das iframe in ready.products gemeldet hat – die aktive Karte wird über productChanged markiert.
Code ansehen HTML + JS
HTML + JS
<!-- iframe on the left, your gallery on the right --><div style="display:grid; grid-template-columns: 2fr 1fr; gap: 16px;"> <iframe id="ks" src="https://demo.kreativsimulator.com/?embed=kiosk" title="Kreativsimulator" style="width:100%; aspect-ratio:16/9; border:0" allow="fullscreen"></iframe> <div id="gallery" style="display:grid; gap:8px; align-content:start"></div></div><script> const API = "https://api.virtualcampaign.dev"; const API_KEY = "vc_4b3efe1be755a77ba77b95e67067d8e84176fdf7621ebd3b2d94e"; // demo key – replace with your own const SIMULATOR = "https://demo.kreativsimulator.com/"; // your tenant: https://<tenant>.kreativsimulator.com/ const iframe = document.getElementById("ks"); const send = (msg) => iframe.contentWindow.postMessage(msg, "*"); const gallery = document.getElementById("gallery"); let simulatorCodes = null; // product codes the iframe knows (from the ready event) // 1) Build one card per API product. Cards stay disabled until the iframe is ready. function renderGallery(products) { gallery.innerHTML = ""; for (const p of products) { const card = document.createElement("button"); card.type = "button"; card.dataset.code = p.productCode; card.disabled = true; card.style.cssText = "display:flex; gap:10px; align-items:center; text-align:left; padding:6px; border:2px solid #ddd; border-radius:8px; background:#fff; cursor:pointer"; const img = document.createElement("img"); img.src = API + p.thumbnailUrl + "?res=mdpi"; // public, no key needed img.alt = ""; img.style.cssText = "width:96px; aspect-ratio:16/9; object-fit:cover; border-radius:4px"; const name = document.createElement("span"); name.textContent = p.productName; card.append(img, name); // 2) Click → the iframe switches to this product. card.onclick = () => send({ type: "kreativsimulator:setProduct", code: p.productCode }); gallery.appendChild(card); } syncCards(); } // Enable only cards the iframe can show; highlight the active one. function syncCards(activeCode) { for (const card of gallery.children) { const known = !simulatorCodes || simulatorCodes.has(card.dataset.code); card.disabled = !simulatorCodes || !known; card.title = known ? "" : "Not available in this simulator tenant"; card.style.borderColor = card.dataset.code === activeCode ? "#ff5722" : "#ddd"; } } // 3) Events from the iframe: ALWAYS filter on source === "kreativsimulator". window.addEventListener("message", (e) => { const m = e.data; if (!m || m.source !== "kreativsimulator") return; // Canvas shape: ready/productChanged carry `render`, stageChanged carries it // itself. A photo product is NOT 16:9 — follow it, then there is no letterbox. const r = m.type === "stageChanged" ? m : m.render; if (r) iframe.style.aspectRatio = r.width + " / " + r.height; if (m.type === "ready") { simulatorCodes = new Set(m.products.map((p) => p.code)); syncCards(m.product); } if (m.type === "productChanged") syncCards(m.code); if (m.type === "error") console.warn("Kreativsimulator:", m.message); }); fetch(API + "/v1/products?limit=50", { headers: { Authorization: "Bearer " + API_KEY } }) .then((r) => r.json()) .then((data) => renderGallery(data.products)) .catch((err) => (gallery.textContent = "Could not load products: " + err.message));</script>
Reihenfolge: Motive per setCreative erst senden, wenn productChanged für das neue Produkt eingetroffen ist – ein Produktwechsel setzt alle Motive zurück.
Vorschaubild:<img> braucht keinen Key. Nur der Produkt-Liste muss der Bearer-Header mitgegeben werden.
Namen sicher einsetzen: Produktnamen mit textContent setzen, nicht per innerHTML – wie im Beispiel.
Canvas-Form mitführen: Nicht jedes Produkt ist 16:9 – Foto-Produkte haben das Seitenverhältnis des Fotos. ready und productChanged liefern dazu einen render-Block (width/height/aspectRatio), das Event stageChanged die gleichen Felder direkt. Setzen Sie damit die aspect-ratio Ihres iframes, dann bleibt keine Letterbox – so macht es das Beispiel unten.
Schritt 4
Eigene Vorschaubilder statt API-Thumbnails
Sie wollen Ihre eigenen Produktbilder zeigen – eigene Fotos, eigenes Design, eigener Host? Dann brauchen Sie die API zur Laufzeit gar nicht. Eine kleine Liste in Ihrem Code ordnet jedem Bild den productCode zu; der Klick sendet setProduct mit genau diesem Code. Alles andere bleibt gleich.
Den Code holen Sie einmalig: aus GET /v1/products (Feld productCode), aus dem ready-Event des iframes (products[].code) oder aus der VirtualCampaign-Oberfläche.
Code ansehen HTML + JS
HTML + JS
<!-- Your own images (hosted by you) mapped to product codes. --><div style="display:grid; grid-template-columns: 2fr 1fr; gap: 16px;"> <iframe id="ks" src="https://demo.kreativsimulator.com/?embed=kiosk" title="Kreativsimulator" style="width:100%; aspect-ratio:16/9; border:0" allow="fullscreen"></iframe> <div id="gallery" style="display:grid; gap:8px; align-content:start"></div></div><script> // The product code is the key. Take it from GET /v1/products (productCode), // from the iframe's `ready` event (products[].code) or from the VirtualCampaign UI. const MY_PRODUCTS = [ { code: "29a5c31b", name: "City-Light München", image: "/img/citylight-muenchen.jpg" }, { code: "2aff892d", name: "Billboard Bursa", image: "/img/billboard-bursa.jpg" }, { code: "e6bc7001", name: "ePanel Bahnhof", image: "/img/epanel.jpg" }, ]; const iframe = document.getElementById("ks"); const send = (msg) => iframe.contentWindow.postMessage(msg, "*"); const gallery = document.getElementById("gallery"); let simulatorCodes = null; for (const p of MY_PRODUCTS) { const card = document.createElement("button"); card.type = "button"; card.dataset.code = p.code; card.disabled = true; // until `ready` card.style.cssText = "display:flex; gap:10px; align-items:center; text-align:left; padding:6px; border:2px solid #ddd; border-radius:8px; background:#fff; cursor:pointer"; const img = document.createElement("img"); img.src = p.image; // your own file – any host, no CORS needed for <img> img.alt = ""; img.style.cssText = "width:96px; aspect-ratio:16/9; object-fit:cover; border-radius:4px"; const name = document.createElement("span"); name.textContent = p.name; card.append(img, name); card.onclick = () => send({ type: "kreativsimulator:setProduct", code: p.code }); gallery.appendChild(card); } function syncCards(activeCode) { for (const card of gallery.children) { const known = simulatorCodes && simulatorCodes.has(card.dataset.code); card.disabled = !known; card.title = simulatorCodes && !known ? "Unknown product code in this tenant" : ""; card.style.borderColor = card.dataset.code === activeCode ? "#ff5722" : "#ddd"; } } window.addEventListener("message", (e) => { const m = e.data; if (!m || m.source !== "kreativsimulator") return; // Canvas shape: ready/productChanged carry `render`, stageChanged carries it // itself. A photo product is NOT 16:9 — follow it, then there is no letterbox. const r = m.type === "stageChanged" ? m : m.render; if (r) iframe.style.aspectRatio = r.width + " / " + r.height; if (m.type === "ready") { simulatorCodes = new Set(m.products.map((p) => p.code)); syncCards(m.product); } if (m.type === "productChanged") syncCards(m.code); if (m.type === "error") console.warn("Kreativsimulator:", m.message); });</script>
Eigene Bilder brauchen kein CORS – sie landen in einem <img> auf Ihrer Seite, nicht im iframe. Nur Motive für setCreative brauchen CORS.
Falscher Code? Karten, deren Code das iframe in ready.products nicht meldet, bleiben deaktiviert (Tooltip). So merken Sie Tippfehler sofort.
Mischen geht auch: Liste aus der API laden und nur die Bild-URL durch Ihre eigene ersetzen (Map productCode → Bild).
Schritt 5
Export-Button fernsteuern
Der eingebaute Export-Button ist im Kiosk-Modus ausgeblendet – der Export funktioniert per postMessage trotzdem. Ein eigener Button sendet export mit width/height; die Auflösungen kommen aus ready.render.presets. Ein zweiter Button speichert per exportImage ein Einzelbild (JPG).
Code ansehen HTML + JS
HTML + JS
<select id="res"></select><button id="export">Export MP4</button><button id="still">Save image (JPG)</button><script> const P = "kreativsimulator:"; const res = document.getElementById("res"); // Fill the resolution picker from the presets the iframe reports in `ready`. window.addEventListener("message", (e) => { const m = e.data; if (!m || m.source !== "kreativsimulator" || m.type !== "ready") return; res.innerHTML = ""; for (const r of m.render.presets) { const o = document.createElement("option"); o.value = r.width + "x" + r.height; o.textContent = r.label; o.selected = r.width === m.render.width; // native resolution preselected res.appendChild(o); } }); const size = () => res.value.split("x").map(Number); // Export works even when the built-in export button is hidden (?embed=kiosk). document.getElementById("export").onclick = () => { const [width, height] = size(); send({ type: P + "export", filename: "kreation", width, height }); }; // Still image of the current frame (photo products: the whole photo). document.getElementById("still").onclick = () => { const [width, height] = size(); send({ type: P + "exportImage", filename: "kreation", width, height }); };</script>
Demo-Mandant: Jeder Export trägt das „Vorschau“-Wasserzeichen – nicht abschaltbar. Bei Ihrem Mandanten ist es aus (oder gezielt per watermark: true bzw. ?watermark=1 zuschaltbar).
Der Download landet beim Nutzer (Browser-Download aus dem iframe) – Ihre Seite bekommt die Datei nicht. Für serverseitige Renderings nutzen Sie die Orders-API.
Bestimmter Frame als Bild: erst pause/seek, dann exportImage.
Schritt 6
Hinweis-Dialog vor dem Download
Oft muss der Nutzer vor dem Download etwas bestätigen – z. B. einen Urheberrechts-Hinweis. Das Muster: Ihr Export-Button öffnet zuerst ein natives <dialog>; erst „Bestätigen“ sendet export bzw. exportImage an das iframe. „Abbrechen“, das ×, Esc oder ein Klick daneben senden nichts.
Die Funktion confirmThen(action) merkt sich die gewünschte Aktion und führt sie beim submit-Event des Formulars aus – also bei „Bestätigen“ per Klick oder Enter. Text, Farben und Buttons gehören Ihnen – das Beispiel ist im VirtualCampaign-Look gehalten (dunkel, orange), passen Sie es an Ihre Seite an.
Code ansehen HTML + CSS + JS
HTML + CSS + JS
<!-- Notice dialog — shown before every download. Wording is yours. --><dialog id="notice" class="notice"> <form method="dialog"> <button type="button" class="notice-close" aria-label="Schließen" data-cancel>×</button> <h2>Hinweis</h2> <p> Die erstellte Simulation ist urheberrechtlich geschützt. Sie darf nur intern von Ihnen bzw. bei Agenturen auch gegenüber (potentiellen) Kunden zu Präsentationszwecken genutzt werden. Eine darüber hinausgehende Nutzung, insbesondere eine Veröffentlichung (Website, Social Media etc.), ist nicht gestattet. </p> <div class="notice-actions"> <button type="button" class="notice-secondary" data-cancel>Abbrechen</button> <button type="submit" value="confirm" class="notice-primary">Bestätigen →</button> </div> </form></dialog><style>/* Notice dialog — dark, orange accent (matches the VirtualCampaign look) */.notice { border: 1px solid #2a2a2a; border-radius: 16px; background: #141414; color: #eee; padding: 0; margin: auto; width: min(640px, calc(100vw - 32px)); box-shadow: 0 30px 80px rgba(0,0,0,.6); }.notice::backdrop { background: rgba(0,0,0,.7); backdrop-filter: blur(4px); }.notice form { position: relative; padding: 32px 36px 28px; margin: 0; }.notice h2 { margin: 0 0 14px; font-size: 28px; font-weight: 600; letter-spacing: -.01em; }.notice p { margin: 0 0 26px; color: #b5b5b5; line-height: 1.65; font-size: 15px; }.notice-actions { display: flex; justify-content: flex-end; gap: 10px; }.notice-primary { background: #ff5722; color: #fff; border: 0; border-radius: 999px; padding: 12px 24px; font: inherit; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; cursor: pointer; }.notice-primary:hover { background: #e64a19; }.notice-secondary { background: transparent; color: #b5b5b5; border: 1px solid #333; border-radius: 999px; padding: 12px 20px; font: inherit; cursor: pointer; }.notice-secondary:hover { color: #fff; border-color: #666; }.notice-close { position: absolute; top: 14px; right: 16px; background: none; border: 0; color: #ff5722; font-size: 26px; line-height: 1; cursor: pointer; }</style><script> // Ask first, download afterwards: confirmThen(fn) opens the dialog and runs fn // only when the user confirms. Works for export (MP4) and exportImage (JPG) alike. const notice = document.getElementById("notice"); let pendingAction = null; function confirmThen(action) { pendingAction = action; notice.showModal(); } // "Bestätigen" submits the <form method="dialog"> (click or Enter) → run the action. notice.querySelector("form").addEventListener("submit", () => { if (pendingAction) pendingAction(); pendingAction = null; }); // "Abbrechen" / × / Esc just close the dialog — nothing is sent. notice.querySelectorAll("[data-cancel]").forEach((b) => (b.onclick = () => notice.close())); notice.addEventListener("cancel", () => (pendingAction = null)); // Usage: wrap the export calls (send() and P from the gallery example). document.getElementById("export").onclick = () => confirmThen(() => send({ type: P + "export", filename: "kreation", width: 1280, height: 720 })); document.getElementById("still").onclick = () => confirmThen(() => send({ type: P + "exportImage", filename: "kreation", width: 1280, height: 720 }));</script>
Kein Framework nötig:<dialog> mit showModal() bringt Fokus-Falle, Esc und den Backdrop mit – in allen aktuellen Browsern.
Nur einmal fragen? Nach dem ersten „Bestätigen“ z. B. sessionStorage.setItem("noticeOk", "1") setzen und in confirmThen direkt ausführen, wenn der Wert gesetzt ist.
Der Export läuft weiter im iframe: Der Dialog liegt auf Ihrer Seite, der Download kommt wie gewohnt aus dem iframe (Demo-Mandant: mit Wasserzeichen).
Schritt 7
Komplettes Beispiel (eine Datei)
Alles zusammen in einer index.html: iframe links, Galerie rechts, Export-Leiste mit Hinweis-Dialog darunter. Speichern, lokal auf Port 8080 ausliefern, öffnen – fertig. Zum Anpassen genügen die drei Konstanten am Anfang des Scripts.
Datei als index.html speichern.
API_KEY und SIMULATOR durch Ihre Werte ersetzen (Demo-Werte funktionieren sofort).
Lokal auf http://localhost:8080 ausliefern (Schritt 8) oder auf Ihrer freigeschalteten Domain veröffentlichen.
index.html
<!doctype html><html lang="de"><head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>Kreativsimulator – Produkt-Galerie</title> <style> body { margin: 0; font-family: system-ui, sans-serif; background: #111; color: #eee; } .layout { display: grid; grid-template-columns: minmax(0, 2fr) minmax(260px, 1fr); gap: 16px; padding: 16px; } @media (max-width: 900px) { .layout { grid-template-columns: 1fr; } } iframe { width: 100%; aspect-ratio: 16 / 9; border: 0; background: #000; border-radius: 8px; } #gallery { display: grid; gap: 8px; align-content: start; } .card { display: flex; gap: 10px; align-items: center; text-align: left; padding: 6px; border: 2px solid #333; border-radius: 8px; background: #1b1b1b; color: #eee; cursor: pointer; } .card:disabled { opacity: .4; cursor: default; } .card.active { border-color: #ff5722; } .card img { width: 96px; aspect-ratio: 16 / 9; object-fit: cover; border-radius: 4px; background: #000; } .bar { display: flex; gap: 8px; align-items: center; margin-top: 12px; } button, select { font: inherit; padding: 8px 12px; border-radius: 6px; border: 1px solid #444; background: #222; color: #eee; } button:not(:disabled):hover { border-color: #ff5722; } #status { font-size: 12px; color: #999; margin-left: auto; } /* Notice dialog — dark, orange accent (matches the VirtualCampaign look) */ .notice { border: 1px solid #2a2a2a; border-radius: 16px; background: #141414; color: #eee; padding: 0; margin: auto; width: min(640px, calc(100vw - 32px)); box-shadow: 0 30px 80px rgba(0,0,0,.6); } .notice::backdrop { background: rgba(0,0,0,.7); backdrop-filter: blur(4px); } .notice form { position: relative; padding: 32px 36px 28px; margin: 0; } .notice h2 { margin: 0 0 14px; font-size: 28px; font-weight: 600; letter-spacing: -.01em; } .notice p { margin: 0 0 26px; color: #b5b5b5; line-height: 1.65; font-size: 15px; } .notice-actions { display: flex; justify-content: flex-end; gap: 10px; } .notice-primary { background: #ff5722; color: #fff; border: 0; border-radius: 999px; padding: 12px 24px; font: inherit; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; cursor: pointer; } .notice-primary:hover { background: #e64a19; } .notice-secondary { background: transparent; color: #b5b5b5; border: 1px solid #333; border-radius: 999px; padding: 12px 20px; font: inherit; cursor: pointer; } .notice-secondary:hover { color: #fff; border-color: #666; } .notice-close { position: absolute; top: 14px; right: 16px; background: none; border: 0; color: #ff5722; font-size: 26px; line-height: 1; cursor: pointer; } </style></head><body> <div class="layout"> <div> <!-- Only the canvas: header, sidebar and the built-in export button are hidden. --> <iframe id="ks" src="https://demo.kreativsimulator.com/?embed=kiosk" title="Kreativsimulator" allow="fullscreen"></iframe> <div class="bar"> <select id="res"></select> <button id="export" disabled>Export MP4</button> <button id="still" disabled>Save image (JPG)</button> <span id="status">Loading …</span> </div> </div> <div id="gallery"></div> </div> <!-- Notice dialog — shown before every download. Wording is yours. --> <dialog id="notice" class="notice"> <form method="dialog"> <button type="button" class="notice-close" aria-label="Schließen" data-cancel>×</button> <h2>Hinweis</h2> <p> Die erstellte Simulation ist urheberrechtlich geschützt. Sie darf nur intern von Ihnen bzw. bei Agenturen auch gegenüber (potentiellen) Kunden zu Präsentationszwecken genutzt werden. Eine darüber hinausgehende Nutzung, insbesondere eine Veröffentlichung (Website, Social Media etc.), ist nicht gestattet. </p> <div class="notice-actions"> <button type="button" class="notice-secondary" data-cancel>Abbrechen</button> <button type="submit" value="confirm" class="notice-primary">Bestätigen →</button> </div> </form> </dialog> <script> const API = "https://api.virtualcampaign.dev"; const API_KEY = "vc_4b3efe1be755a77ba77b95e67067d8e84176fdf7621ebd3b2d94e"; // demo key – replace with your own const SIMULATOR = "https://demo.kreativsimulator.com/"; // your tenant: https://<tenant>.kreativsimulator.com/ const P = "kreativsimulator:"; const iframe = document.getElementById("ks"); const gallery = document.getElementById("gallery"); const res = document.getElementById("res"); const status = document.getElementById("status"); const exportBtn = document.getElementById("export"); const stillBtn = document.getElementById("still"); const send = (msg) => iframe.contentWindow.postMessage(msg, "*"); let simulatorCodes = null; // ── 1. Gallery from the REST API ──────────────────────────────────── async function loadProducts() { const r = await fetch(API + "/v1/products?limit=50", { headers: { Authorization: "Bearer " + API_KEY }, }); if (!r.ok) throw new Error("API error " + r.status); return (await r.json()).products; } function renderGallery(products) { gallery.innerHTML = ""; for (const p of products) { const card = document.createElement("button"); card.type = "button"; card.className = "card"; card.dataset.code = p.productCode; card.disabled = true; // until the iframe says `ready` const img = document.createElement("img"); img.src = API + p.thumbnailUrl + "?res=mdpi"; img.alt = ""; const name = document.createElement("span"); name.textContent = p.productName; card.append(img, name); card.onclick = () => send({ type: P + "setProduct", code: p.productCode }); gallery.appendChild(card); } syncCards(); } function syncCards(activeCode) { for (const card of gallery.children) { const known = simulatorCodes && simulatorCodes.has(card.dataset.code); card.disabled = !known; card.title = simulatorCodes && !known ? "Not available in this simulator tenant" : ""; card.classList.toggle("active", card.dataset.code === activeCode); } } // ── 2. Events from the iframe ─────────────────────────────────────── window.addEventListener("message", (e) => { const m = e.data; if (!m || m.source !== "kreativsimulator") return; // Canvas shape + export presets: ready/productChanged carry `render`, // stageChanged carries the fields itself. A photo product is NOT 16:9. const shape = m.type === "stageChanged" ? m : m.render; if (shape) { iframe.style.aspectRatio = shape.width + " / " + shape.height; res.innerHTML = ""; for (const r of shape.presets) { const o = document.createElement("option"); o.value = r.width + "x" + r.height; o.textContent = r.label; o.selected = r.width === shape.width; res.appendChild(o); } } if (m.type === "ready") { simulatorCodes = new Set(m.products.map((p) => p.code)); exportBtn.disabled = stillBtn.disabled = false; syncCards(m.product); status.textContent = "Ready"; } if (m.type === "productChanged") syncCards(m.code); if (m.type === "error") status.textContent = "Error: " + m.message; }); // ── 3. Notice dialog + remote export (works with the built-in button hidden) // Ask first, download afterwards: confirmThen(fn) opens the dialog and runs fn // only when the user confirms. Works for export (MP4) and exportImage (JPG) alike. const notice = document.getElementById("notice"); let pendingAction = null; function confirmThen(action) { pendingAction = action; notice.showModal(); } // "Bestätigen" submits the <form method="dialog"> (click or Enter) → run the action. notice.querySelector("form").addEventListener("submit", () => { if (pendingAction) pendingAction(); pendingAction = null; }); // "Abbrechen" / × / Esc just close the dialog — nothing is sent. notice.querySelectorAll("[data-cancel]").forEach((b) => (b.onclick = () => notice.close())); notice.addEventListener("cancel", () => (pendingAction = null)); const size = () => res.value.split("x").map(Number); exportBtn.onclick = () => { const [width, height] = size(); confirmThen(() => send({ type: P + "export", filename: "kreation", width, height })); }; stillBtn.onclick = () => { const [width, height] = size(); confirmThen(() => send({ type: P + "exportImage", filename: "kreation", width, height })); }; loadProducts() .then(renderGallery) .catch((err) => (status.textContent = "Could not load products: " + err.message)); </script></body></html>
Schritt 8
Lokal testen auf localhost:8080
Ein statischer Webserver reicht. Die Datei direkt per Doppelklick (file://) öffnen funktioniert nicht – der Browser hat dann keinen Origin, das iframe bleibt leer.
Im Ordner der index.html:
Terminal
# Option A – Node (npx, no install)npx serve -l 8080 .# Option B – Python 3python -m http.server 8080# then open: http://localhost:8080
Wenn etwas nicht klappt
Die Galerie lädt, das iframe bleibt schwarz/leer
Der Origin ist nicht freigeschaltet. Lokal muss es exakt http://localhost:8080 sein (nicht 127.0.0.1, kein anderer Port, kein file://). Die Konsole (F12) zeigt „Refused to frame … frame-ancestors“. Für Ihre Domain: Freischaltung anfragen.
API antwortet 401
Der Bearer-Key fehlt oder ist falsch/rotiert. Header prüfen: Authorization: Bearer vc_… (mit Leerzeichen nach Bearer).
API antwortet 403 GROUP_REQUIRED
Der Key ist keiner Benutzergruppe zugeordnet und sieht deshalb keine Produkte. Melden Sie sich bei uns – wir ordnen ihn zu.
Karten bleiben ausgegraut
Das ready-Event ist noch nicht angekommen (iframe lädt oder ist blockiert, s. o.) – oder der productCode ist im Simulator-Mandanten nicht aktiv. Die Karte zeigt dann einen Tooltip.
Vorschaubild 404
Für dieses Produkt ist noch kein Thumbnail hinterlegt. Fallback im img-onerror setzen (z. B. Platzhalterfarbe).
Live
Genau dieses Beispiel – live
Links das echte iframe, rechts die Galerie, die diese Seite gerade per fetch von api.virtualcampaign.dev geladen hat. Klick → setProduct. Darunter der ferngesteuerte Export.