Wähle Symbole für Hobbys, Reisen, Insider und Lieblingsmomente.
★★★★★ · Über 100 Shirts gestaltet · 5,0 auf Etsy
Wähle einen Startpunkt — du bekommst sofort ein fertiges Design und tauschst nur die Symbole, die nicht passen.
Es landet automatisch auf der nächsten freien Position. Oder tippe zuerst auf ein Feld im Shirt, um genau dort zu platzieren. Symbole lassen sich auch per Drag & Drop zwischen Positionen verschieben.
Bis zu drei Texte — Datum, Namen, Insider. Jeden direkt auf dem Shirt an seine Position ziehen.
Den Inhalt von „_design_json" aus der Shopify-Bestellung einfügen — das Kundendesign wird exakt wiederhergestellt, dann oben „Druckdatei" klicken. Wunsch-Slots erscheinen als Skizze: Symbol nachzeichnen, einsetzen, exportieren.
Beschreib, was wir für dich von Hand zeichnen sollen — je konkreter, desto besser. Zum Beispiel: „unser roter Wohnwagen mit Fahrrädern auf dem Dach" oder „ein silberner Sportwagen".
Gern zeichnen wir dein Auto, dein Instrument, euren Lieblingsort — nur ohne Markenlogos und Embleme. Nicht möglich: geschützte Figuren (Film, Comic, Vereinswappen) sowie Gewalt- oder Hassmotive.
Ein Motiv pro Wunschfeld — für ein zweites Motiv einfach ein weiteres Wunschfeld setzen (je 4,99 €).
Diese Properties würden an der Shopify-Order hängen — du siehst pro Bestellung exakt, welche Symbole wohin gehören:
';
throw new Error("cockpit locked");
})();
// ===== Shopify-Anbindung (vom Betreiber zu setzen) ==========================
// Handle des Shopify-Produkts, das in den Warenkorb gelegt wird (= der Teil
// hinter /products/ in der Produkt-URL). WICHTIG: Das Produkt MUSS veröffentlicht
// sein (Status „Aktiv" + Vertriebskanal „Online Store"), sonst liefert der
// Storefront nichts und Größen/Warenkorb bleiben leer. Zum Sofort-Testen ohne
// Veröffentlichen vorübergehend "this-is-me-tshirt" (Bestseller) eintragen.
const PP_PRODUCT_HANDLE = "personalisiertes-doodle-t-shirt-aus-bio-baumwolle-kopie";
const PP_WISH_HANDLE = "wunschsymbol"; // Aufpreis-Produkt (4,99 €) für handgezeichnete Wunschmotive
const PP_REDIRECT_AFTER_ADD = "/cart"; // nach dem Hinzufügen: "/cart" oder "/checkout"
let PP_PRODUCT = null; // wird von ppLoadProduct() aus /products/
Fehlt dir was?
+4,99 €";
b.addEventListener("click", () => placeSymbol("wunsch"));
gridEl.appendChild(b);
};
pool.filter(s => !q || s.label.toLowerCase().includes(q)).forEach(sym => {
gridEl.appendChild(_makeSymBtn(sym));
});
if (state.activeCat !== "🔒 Privat") addWishTile();
if (!gridEl.children.length) {
const empty = document.createElement("div");
empty.style.cssText = "grid-column:1/-1; font-size:12.5px; color:var(--sand-700); padding:14px 4px;";
empty.textContent = q
? "Kein Symbol gefunden — andere Schreibweise probieren."
: "Diese Kategorie ist noch leer — die Symbole kommen, sobald sie hochgeladen sind.";
gridEl.appendChild(empty);
}
}
function ensureTextEl(i) {
if (textEls[i]) return textEls[i];
const el = document.createElement("div");
el.className = "name-line";
let dragging = false;
el.addEventListener("pointerdown", e => { dragging = true; el.setPointerCapture(e.pointerId); e.preventDefault(); });
el.addEventListener("pointermove", e => {
if (!dragging) return;
const r = zone.getBoundingClientRect();
const t = state.texts[i];
t.x = Math.max(2, Math.min(98, ((e.clientX - r.left) / r.width) * 100));
t.y = Math.max(2, Math.min(98, ((e.clientY - r.top) / r.height) * 100));
el.style.left = t.x + "%"; // nur Styles anfassen — kein DOM-Umbau,
el.style.top = t.y + "%"; // sonst bricht das Pointer-Capture und ruckelt
});
el.addEventListener("pointerup", () => { dragging = false; });
textEls[i] = el;
return el;
}
function renderName() {
while (textEls.length > state.texts.length) textEls.pop().remove();
state.texts.forEach((t, i) => {
const el = ensureTextEl(i);
if (el.textContent !== t.text) el.textContent = t.text;
el.style.fontSize = ((t.fs || 7) / 100) * zone.getBoundingClientRect().width + "px";
el.style.color = state.inkColor;
el.style.left = t.x + "%";
el.style.top = t.y + "%";
el.style.display = t.text ? "block" : "none";
if (!el.isConnected) zone.appendChild(el);
});
renderTextInputs();
}
function renderTextInputs() {
const box = document.getElementById("textFields");
if (box.children.length !== state.texts.length) {
box.innerHTML = "";
state.texts.forEach((t, i) => {
const row = document.createElement("div");
row.className = "row";
row.style.marginBottom = "6px";
const inp = document.createElement("input");
inp.type = "text"; inp.maxLength = 40;
inp.placeholder = i === 0 ? "z. B. Lena & Mara · seit 2014" : "z. B. 2504 oder ein Insider";
inp.value = t.text;
inp.style.flex = "1";
inp.addEventListener("input", () => { state.texts[i].text = inp.value; renderName(); });
row.appendChild(inp);
const dn = document.createElement("button");
dn.className = "btn ghost small"; dn.textContent = "A−";
dn.addEventListener("click", () => { t.fs = Math.max(3.5, (t.fs || 7) - 1.2); renderName(); });
const up = document.createElement("button");
up.className = "btn ghost small"; up.textContent = "A+";
up.addEventListener("click", () => { t.fs = Math.min(16, (t.fs || 7) + 1.2); renderName(); });
row.appendChild(dn); row.appendChild(up);
if (state.texts.length > 1) {
const del = document.createElement("button");
del.className = "btn ghost small"; del.textContent = "✕";
del.addEventListener("click", () => {
state.texts.splice(i, 1);
textEls.forEach(e2 => e2.remove()); textEls.length = 0; // Indizes neu binden
document.getElementById("textFields").innerHTML = "";
renderName();
});
row.appendChild(del);
}
box.appendChild(row);
});
document.getElementById("addText").disabled = state.texts.length >= MAX_TEXTS;
}
}
document.getElementById("addText").addEventListener("click", () => {
if (state.texts.length >= MAX_TEXTS) return;
state.texts.push({ text: "", x: 50, y: 18 + state.texts.length * 30, fs: 7 });
document.getElementById("textFields").innerHTML = "";
renderName();
});
// ---- Undo -------------------------------------------------------------------
const HISTORY = [];
const MAX_HISTORY = 20;
function pushHistory() {
HISTORY.push({
slots: JSON.parse(JSON.stringify(state.slots)),
texts: JSON.parse(JSON.stringify(state.texts)),
});
if (HISTORY.length > MAX_HISTORY) HISTORY.shift();
_syncUndoBtn();
}
function undo() {
if (!HISTORY.length) return;
const prev = HISTORY.pop();
state.slots = prev.slots;
state.texts = prev.texts;
state.selected = null;
toolsEl.style.display = "none";
titleEl.textContent = "2 · Tippe direkt auf ein Symbol";
document.getElementById("textFields").innerHTML = "";
renderSlots();
renderName();
_syncUndoBtn();
}
function _syncUndoBtn() {
const b = document.getElementById("undoBtn");
if (b) b.disabled = HISTORY.length === 0;
}
document.addEventListener("keydown", e => {
if ((e.metaKey || e.ctrlKey) && e.key === "z" && !e.shiftKey) { e.preventDefault(); undo(); }
});
// ---- Actions ------------------------------------------------------------------
function selectSlot(i) {
state.selected = i;
const slot = state.slots[i];
titleEl.textContent = "Position " + (i + 1) + (slot.symbolId ? " · " + bySym(slot.symbolId).label : " · leer");
toolsEl.style.display = slot.symbolId ? "flex" : "none";
renderSlots();
}
function placeSymbol(symbolId) {
if (symbolId !== "wunsch") pushHistory();
const wasAutoSelect = state.selected === null;
let i = state.selected;
if (i === null) {
i = state.slots.findIndex(s => !s.symbolId); // nächster freier Slot
if (i === -1) i = 0;
}
const wasEmpty = !state.slots[i].symbolId;
if (symbolId === "wunsch") {
openWishModal(i);
return; // Slot wird erst nach Bestätigung im Formular gesetzt
}
delete state.slots[i].wish;
state.slots[i].symbolId = symbolId;
if (wasEmpty) {
state.slots[i].scale = 1; // Größen-Varianz steckt im Layout
state.slots[i].rot = ROT_PATTERN[i % ROT_PATTERN.length]; // leichte Default-Neigung
}
// Auto-Advance: wenn kein Slot manuell gewählt war → nächsten leeren vorauswählen
if (wasAutoSelect) {
const nextEmpty = state.slots.findIndex((s, idx) => idx > i && !s.symbolId);
state.selected = nextEmpty !== -1 ? nextEmpty : i;
titleEl.textContent = nextEmpty !== -1
? "Position " + (nextEmpty + 1) + " · klicke das nächste Symbol"
: "Position " + (i + 1) + " · " + bySym(symbolId).label;
toolsEl.style.display = nextEmpty !== -1 ? "none" : "flex";
renderSlots();
} else {
selectSlot(i);
}
// Bounce-Animation auf dem platzierten Slot
const placedEl = zone.querySelector(`.slot[data-idx="${i}"]`);
if (placedEl) {
placedEl.classList.add("sym-bounce");
placedEl.addEventListener("animationend", () => placedEl.classList.remove("sym-bounce"), { once: true });
}
}
function swapSlots(a, b) {
pushHistory();
[state.slots[a], state.slots[b]] = [state.slots[b], state.slots[a]];
state.selected = b;
renderSlots();
}
document.getElementById("clearSlot").addEventListener("click", () => {
if (state.selected === null) return;
pushHistory();
state.slots[state.selected] = { symbolId: null, scale: 1, rot: 0 };
selectSlot(state.selected);
});
const rotateSelected = delta => {
if (state.selected === null) return;
const s = state.slots[state.selected];
s.rot = Math.max(-20, Math.min(20, (s.rot || 0) + delta));
renderSlots();
};
const resizeField = f => {
if (state.selected === null) return;
const L = activeLayout()[state.selected];
L.w = Math.max(4, Math.round(L.w * f * 10) / 10);
if (L.h) L.h = Math.max(3, Math.round(L.h * f * 10) / 10);
saveLayouts(); renderSlots();
};
document.getElementById("fieldFit").addEventListener("click", () => {
if (state.selected === null) return;
const slot = state.slots[state.selected];
const L = activeLayout()[state.selected];
if (!slot.symbolId || !L) return;
const vb = (bySym(slot.symbolId).svg.match(/viewBox="([^"]+)"/) || [])[1];
if (!vb) return;
const p = vb.split(" ").map(Number);
const ar = p[2] / p[3];
L.h = Math.round((L.w * (305 / 406) / ar) * 10) / 10;
saveLayouts(); renderSlots();
});
const nudge = (dx, dy) => {
if (state.selected === null) return;
if (state.editMode) {
// Justier-Modus: verschiebt das Master-Layout (für alle Kunden)
const L = activeLayout()[state.selected];
if (!L) return;
L.x = Math.max(1, Math.min(99, Math.round((L.x + dx) * 10) / 10));
L.y = Math.max(1, Math.min(99, Math.round((L.y + dy) * 10) / 10));
saveLayouts();
} else {
// Kundin: verschiebt nur das eigene Design — begrenzt auf ±10 %
const s = state.slots[state.selected];
s.dx = Math.max(-10, Math.min(10, Math.round(((s.dx || 0) + dx) * 10) / 10));
s.dy = Math.max(-10, Math.min(10, Math.round(((s.dy || 0) + dy) * 10) / 10));
}
renderSlots();
};
document.getElementById("nudgeL").addEventListener("click", () => nudge(-1.2, 0));
document.getElementById("nudgeR").addEventListener("click", () => nudge(1.2, 0));
document.getElementById("nudgeU").addEventListener("click", () => nudge(0, -1.2));
document.getElementById("nudgeD").addEventListener("click", () => nudge(0, 1.2));
document.getElementById("fieldSmaller").addEventListener("click", () => resizeField(1 / 1.08));
document.getElementById("fieldBigger").addEventListener("click", () => resizeField(1.08));
document.getElementById("copyLayout").addEventListener("click", async () => {
const data = JSON.stringify(LAYOUT_STYLES, null, 1);
try { await navigator.clipboard.writeText(data); alert("Kopiert! Einfach im Chat einfügen."); }
catch (e) { window.prompt("Manuell kopieren:", data); }
});
document.getElementById("downloadLayout").addEventListener("click", () => {
// Positionen + aktuelle Symbole zusammenführen
const enriched = {};
for (const style of Object.keys(LAYOUT_STYLES)) {
enriched[style] = LAYOUT_STYLES[style].map((L, i) => {
const s = state.slots[i];
return {
x: L.x, y: L.y, w: L.w, h: L.h || L.w,
symbol: (s && s.symbolId) ? s.symbolId : null,
label: (s && s.symbolId && bySym(s.symbolId)) ? bySym(s.symbolId).label : null,
scale: (s && s.scale) || 1,
rot: (s && s.rot) || 0,
};
});
}
const data = JSON.stringify(enriched, null, 1);
const blob = new Blob([data], { type: "application/json" });
const a = document.createElement("a");
a.href = URL.createObjectURL(blob);
const ts = new Date().toISOString().slice(0,16).replace("T","_").replace(/:/g,"-");
a.download = "pp-layout-" + ts + ".json";
a.click();
});
document.getElementById("resetLayout").addEventListener("click", () => {
if (!confirm("Justierung verwerfen und eingebaute Standard-Layouts laden?")) return;
localStorage.removeItem("pp.layout.custom");
for (const k of Object.keys(BUILTIN_LAYOUTS)) LAYOUT_STYLES[k] = JSON.parse(JSON.stringify(BUILTIN_LAYOUTS[k]));
renderSlots();
});
document.getElementById("editToggle").addEventListener("change", e => {
state.editMode = e.target.checked;
if (!state.editMode && state.activeCat === "🔒 Privat") state.activeCat = CATS[0];
renderTabs(); renderGrid();
document.querySelectorAll(".edit-only").forEach(el2 => (el2.style.display = state.editMode ? "" : "none"));
renderSlots();
});
document.getElementById("rotZero").addEventListener("click", () => {
if (state.selected === null) return;
state.slots[state.selected].rot = 0;
renderSlots();
});
document.getElementById("rotLeft").addEventListener("click", () => rotateSelected(-6));
document.getElementById("rotRight").addEventListener("click", () => rotateSelected(6));
document.getElementById("flipH").addEventListener("click", () => {
if (state.selected === null) return;
const s = state.slots[state.selected];
s.flip = !s.flip;
renderSlots();
});
const scaleSelected = f => {
if (state.selected === null) return;
const s = state.slots[state.selected];
s.scale = Math.max(0.5, Math.min(2, Math.round(((s.scale || 1) * f) * 100) / 100));
renderSlots();
};
document.getElementById("symSmaller").addEventListener("click", () => scaleSelected(1 / 1.1));
document.getElementById("symBigger").addEventListener("click", () => scaleSelected(1.1));
document.querySelectorAll(".swatch:not(.ink-swatch)").forEach(sw =>
sw.addEventListener("click", () => {
document.querySelectorAll(".swatch:not(.ink-swatch)").forEach(s => s.classList.remove("active"));
sw.classList.add("active");
state.shirtColor = sw.dataset.color;
shirtBody.setAttribute("fill", state.shirtColor);
document.querySelector(".stage").style.setProperty("--shirtcol", state.shirtColor);
if (typeof ppRefreshSizeAvailability === "function") ppRefreshSizeAvailability();
renderSlots(); renderName();
})
);
document.querySelectorAll(".ink-swatch").forEach(sw =>
sw.addEventListener("click", () => {
document.querySelectorAll(".ink-swatch").forEach(s => s.classList.remove("active"));
sw.classList.add("active");
state.inkColor = sw.dataset.ink;
renderSlots(); renderName();
})
);
document.getElementById("gridToggle").addEventListener("change", e => {
zone.classList.toggle("show-grid", e.target.checked);
zone.querySelectorAll(".slot.empty").forEach(s => s.classList.toggle("show-hint", e.target.checked));
});
document.getElementById("symbolSearch").addEventListener("input", e => {
state.search = e.target.value;
renderGrid();
});
document.getElementById("randomFill").addEventListener("click", () => {
const shuffled = [...SYMBOLS].sort(() => Math.random() - 0.5);
state.slots = state.slots.map((_, i) => ({
symbolId: shuffled[i % shuffled.length].id,
scale: [1, 1, 1, 0.85, 1.2][Math.floor(Math.random() * 5)],
rot: Math.round((Math.random() * 16 - 8)),
}));
state.selected = null;
renderSlots();
});
document.getElementById("undoBtn").addEventListener("click", undo);
document.getElementById("resetAll").addEventListener("click", () => {
pushHistory();
state.slots = Array.from({ length: SLOT_COUNT }, () => ({ symbolId: null, scale: 1, rot: 0 }));
state.selected = null; state.texts = [{ text: "", x: 50, y: 95, fs: 7 }];
document.getElementById("textFields").innerHTML = "";
toolsEl.style.display = "none";
titleEl.textContent = "2 · Tippe direkt auf ein Symbol";
renderSlots(); renderName();
});
// ---- Layout-Stil-Umschalter ------------------------------------------------
document.querySelectorAll(".layout-btn").forEach(btn =>
btn.addEventListener("click", () => {
state.layoutStyle = btn.dataset.layout;
document.querySelectorAll(".layout-btn").forEach(b => (b.style.borderColor = ""));
btn.style.borderColor = "var(--peach-600)";
renderSlots();
})
);
// ---- „Für wen?"-Templates ----------------------------------------------------
// Startpunkte statt leerer Slots: Kundin wählt Empfänger, bekommt ein fertig
// kuratiertes Design und tauscht nur noch. (Demo-Kuration mit den vorhandenen
// Symbolen — wird mit jeder neuen echten Kategorie nachgeschärft.)
const TEMPLATES = [
{ id: "bestseller", label: "💛 Bestseller-Design", layoutStyle: "Klassisch",
slotData: [
{ symbolId: "bs-berge", scale: 1, rot: 0 },
{ symbolId: "bs-koffer", scale: 1.33, rot: 5 },
{ symbolId: "bs-flugzeug", scale: 1, rot: -3 },
{ symbolId: "bs-globus", scale: 1, rot: 6 },
{ symbolId: "bs-passport", scale: 1, rot: -5 },
{ symbolId: "bs-rennrad", scale: 1.1, rot: 4 },
{ symbolId: "bs-tennis", scale: 1, rot: 8 },
{ symbolId: "herzpaar", scale: 1, rot: -6 },
{ symbolId: "italien", scale: 1, rot: 3 },
{ symbolId: "bs-hundkatze", scale: 1, rot: 0 },
{ symbolId: "bs-kaffeemaschine", scale: 1, rot: 0 },
{ symbolId: "bs-bierkruege", scale: 0.83, rot: -4 },
{ symbolId: "bs-fussball", scale: 1, rot: 5 },
{ symbolId: "bs-ringe", scale: 1, rot: -3 },
{ symbolId: "bs-paerchen", scale: 1, rot: 7 },
{ symbolId: "bs-kochmuetze", scale: 1, rot: -7 },
{ symbolId: "bs-surfbus", scale: 1.21, rot: -1 },
],
layouts: {
Geordnet: [
{ x: 13, y: 11, w: 21, h: 16 }, { x: 38, y: 11, w: 21, h: 16 }, { x: 62, y: 11, w: 21, h: 16 }, { x: 87, y: 11, w: 21, h: 16 },
{ x: 13, y: 37, w: 21, h: 16 }, { x: 38, y: 37, w: 21, h: 16 }, { x: 62, y: 37, w: 21, h: 16 }, { x: 87, y: 37, w: 21, h: 16 },
{ x: 13, y: 63, w: 21, h: 16 }, { x: 38, y: 63, w: 21, h: 16 }, { x: 62, y: 63, w: 21, h: 16 }, { x: 87, y: 63, w: 21, h: 16 },
{ x: 13, y: 89, w: 21, h: 16 }, { x: 38, y: 89, w: 21, h: 16 }, { x: 62, y: 89, w: 21, h: 16 }, { x: 87, y: 89, w: 21, h: 16 },
],
Klassisch: [
{ x: 21.2, y: 14.2, w: 38.9, h: 24.6 }, { x: 94.5, y: 31.1, w: 15.1, h: 11.3 },
{ x: 78.1, y: 10.7, w: 42, h: 18.4 }, { x: 53.7, y: 23.4, w: 22.2, h: 16.8 },
{ x: 76.7, y: 25.1, w: 14.4, h: 12.4 }, { x: 18.1, y: 35.4, w: 31.4, h: 23.6 },
{ x: 43.9, y: 39.1, w: 21.6, h: 14.3 }, { x: 69.2, y: 39.2, w: 27.3, h: 12.1 },
{ x: 20.6, y: 61.6, w: 37.4, h: 33.1 }, { x: 52.8, y: 54.5, w: 38, h: 16.8 },
{ x: 84.7, y: 48.8, w: 18, h: 15.8 }, { x: 53.2, y: 72.4, w: 29.5, h: 20.6 },
{ x: 88, y: 66.4, w: 27, h: 26.2 }, { x: 30.6, y: 87.9, w: 17.7, h: 10.8 },
{ x: 10.1, y: 92.5, w: 20.8, h: 20.6 }, { x: 48.9, y: 91.5, w: 19.9, h: 16 },
{ x: 80.4, y: 88.6, w: 31, h: 23.3 },
],
},
texts: [{ text: "", x: 50, y: 95, fs: 7 }] },
{ id: "bestseller2", label: "💛 Bestseller 2", layoutStyle: "Klassisch", slotCount: 20,
slotData: [
{ symbolId: "bs-berge", scale: 1, rot: 0 },
{ symbolId: "bs-passport", scale: 0.75, rot: 5 },
{ symbolId: "bs-flugzeug", scale: 1, rot: -3 },
{ symbolId: "globus-reisen", scale: 1, rot: 0 },
{ symbolId: "herzen-umarmen", scale: 1.1, rot: 0 },
{ symbolId: "bs-bierkruege", scale: 1, rot: 4 },
{ symbolId: "sushi-teller", scale: 1, rot: 0 },
{ symbolId: "longdrink", scale: 1, rot: 0 },
{ symbolId: "welle-meer", scale: 1, rot: 0 },
{ symbolId: "bs-rennrad", scale: 0.91, rot: -2 },
{ symbolId: "sonne", scale: 0.91, rot: 6 },
{ symbolId: "gitarre", scale: 0.83, rot: -4 },
{ symbolId: "bs-fussball", scale: 0.91, rot: 0 },
{ symbolId: "bs-kochmuetze", scale: 1, rot: 0 },
{ symbolId: "vw-bus", scale: 0.75, rot: 0 },
{ symbolId: "limonade", scale: 1, rot: 0 },
{ symbolId: "cat-tabby", scale: 0.75, rot: 0 },
{ symbolId: "espresso", scale: 1, rot: -3 },
{ symbolId: "bs-globus", scale: 1, rot: 0 },
{ symbolId: "dog-labrador", scale: 1, rot: 0 },
],
layouts: {
Geordnet: [
{ x: 13, y: 8, w: 21, h: 14 }, { x: 38, y: 8, w: 21, h: 14 }, { x: 62, y: 8, w: 21, h: 14 }, { x: 87, y: 8, w: 21, h: 14 },
{ x: 13, y: 28, w: 21, h: 14 }, { x: 38, y: 28, w: 21, h: 14 }, { x: 62, y: 28, w: 21, h: 14 }, { x: 87, y: 28, w: 21, h: 14 },
{ x: 13, y: 48, w: 21, h: 14 }, { x: 38, y: 48, w: 21, h: 14 }, { x: 62, y: 48, w: 21, h: 14 }, { x: 87, y: 48, w: 21, h: 14 },
{ x: 13, y: 68, w: 21, h: 14 }, { x: 38, y: 68, w: 21, h: 14 }, { x: 62, y: 68, w: 21, h: 14 }, { x: 87, y: 68, w: 21, h: 14 },
{ x: 13, y: 88, w: 21, h: 14 }, { x: 38, y: 88, w: 21, h: 14 }, { x: 62, y: 88, w: 21, h: 14 }, { x: 87, y: 88, w: 21, h: 14 },
],
Klassisch: [
{ x: 20.1, y: 12.9, w: 38.9, h: 24.6 }, { x: 47.6, y: 9.6, w: 23.9, h: 17.9 },
{ x: 77.7, y: 10, w: 42, h: 18.4 }, { x: 88, y: 34, w: 20.6, h: 21.6 },
{ x: 49.5, y: 50.2, w: 22.8, h: 17.1 }, { x: 37.8, y: 36.1, w: 19.8, h: 13.9 },
{ x: 22.7, y: 49.8, w: 29.4, h: 19.3 }, { x: 50.9, y: 67.4, w: 9.3, h: 15.8 },
{ x: 20.4, y: 72, w: 47.1, h: 17.2 }, { x: 75.8, y: 56.9, w: 41, h: 30.8 },
{ x: 35.4, y: 68.5, w: 15.5, h: 13.5 }, { x: 65.9, y: 72.4, w: 27.3, h: 19.1 },
{ x: 88.7, y: 69.3, w: 27, h: 26.2 }, { x: 65, y: 90.7, w: 20.6, h: 12.6 },
{ x: 18.1, y: 91.4, w: 44.8, h: 22 }, { x: 43.9, y: 86.8, w: 18.4, h: 18.9 },
{ x: 87, y: 90, w: 33.5, h: 29.5 }, { x: 11.3, y: 59.8, w: 20.5, h: 9.1 },
{ x: 60.7, y: 29.7, w: 25.8, h: 20.4 }, { x: 15.2, y: 34, w: 27.9, h: 22 },
],
}
},
{ id: "partner", label: "💑 Partner-Shirt", layoutStyle: "Klassisch", slotCount: 20,
slotData: [
{ symbolId: "pt-haus", scale: 1, rot: 0 },
{ symbolId: "pt-wein-cheers", scale: 1, rot: 0 },
{ symbolId: "flugzeug", scale: 1, rot: 0 },
{ symbolId: "pt-polaroid", scale: 1, rot: 0 },
{ symbolId: "pt-puzzle", scale: 1, rot: 0 },
{ symbolId: "pt-blumenstrauss", scale: 1, rot: 0 },
{ symbolId: "pt-ehering", scale: 1, rot: 0 },
{ symbolId: "pt-socken", scale: 1, rot: 0 },
{ symbolId: "bs-kochmuetze", scale: 1, rot: 0 },
{ symbolId: "pt-mond", scale: 1, rot: 0 },
{ symbolId: "pt-tictac", scale: 1, rot: 0 },
{ symbolId: "pt-no1", scale: 1, rot: 0 },
{ symbolId: "pt-herzen-figuren", scale: 1, rot: 0 },
{ symbolId: "pt-flamme", scale: 1, rot: 0 },
{ symbolId: "pt-brief-herz", scale: 1, rot: 0 },
{ symbolId: "pt-pizza", scale: 1, rot: 0 },
{ symbolId: "salz", scale: 1, rot: 0 },
{ symbolId: "weltreise", scale: 1.46, rot: 0 },
{ symbolId: "pt-stern", scale: 1, rot: 0 },
{ symbolId: "musiknote", scale: 1.33, rot: -5 },
],
layouts: {
Geordnet: [
{ x: 13, y: 11, w: 21, h: 16 }, { x: 38, y: 11, w: 21, h: 16 }, { x: 62, y: 11, w: 21, h: 16 }, { x: 87, y: 11, w: 21, h: 16 },
{ x: 13, y: 31, w: 21, h: 16 }, { x: 38, y: 31, w: 21, h: 16 }, { x: 62, y: 31, w: 21, h: 16 }, { x: 87, y: 31, w: 21, h: 16 },
{ x: 13, y: 51, w: 21, h: 16 }, { x: 38, y: 51, w: 21, h: 16 }, { x: 62, y: 51, w: 21, h: 16 }, { x: 87, y: 51, w: 21, h: 16 },
{ x: 13, y: 71, w: 21, h: 16 }, { x: 38, y: 71, w: 21, h: 16 }, { x: 62, y: 71, w: 21, h: 16 }, { x: 87, y: 71, w: 21, h: 16 },
{ x: 13, y: 91, w: 21, h: 16 }, { x: 38, y: 91, w: 21, h: 16 }, { x: 62, y: 91, w: 21, h: 16 }, { x: 87, y: 91, w: 21, h: 16 },
],
Klassisch: [
{ x: 17.1, y: 45.6, w: 32.6, h: 24.4 }, { x: 52.5, y: 88.1, w: 27.2, h: 20.5 },
{ x: 20.9, y: 8.6, w: 39.1, h: 16.4 }, { x: 51.1, y: 35.1, w: 29.1, h: 21.9 },
{ x: 85.8, y: 36.2, w: 24.3, h: 18.2 }, { x: 86.7, y: 12.6, w: 28.6, h: 21.6 },
{ x: 54.4, y: 50.3, w: 17.8, h: 11.3 }, { x: 84.1, y: 87.7, w: 28.8, h: 21.6 },
{ x: 55.8, y: 11.9, w: 24, h: 19.5 }, { x: 11.2, y: 26.7, w: 15.9, h: 12.1 },
{ x: 39.5, y: 64.4, w: 21.6, h: 16.2 }, { x: 87.2, y: 67.6, w: 21.1, h: 15.9 },
{ x: 87.7, y: 52.1, w: 30.1, h: 24 }, { x: 61.4, y: 67.6, w: 27.2, h: 20.4 },
{ x: 15.1, y: 66.2, w: 21.6, h: 16.2 }, { x: 15.9, y: 87.4, w: 30.1, h: 22.6 },
{ x: 73.6, y: 23.9, w: 11.5, h: 12.1 }, { x: 28.6, y: 23.5, w: 14.8, h: 11.1 },
{ x: 34, y: 39.6, w: 11.9, h: 8.9 }, { x: 37.6, y: 77.1, w: 11.7, h: 6.3 },
],
},
},
{ id: "bestie", label: "💛 Beste Freundin", layoutStyle: "Klassisch", slotCount: 25,
slotData: [
{ symbolId: "bf-discokugel", scale: 1.1, rot: 0 },
{ symbolId: "bf-reise", scale: 1.21, rot: 0 },
{ symbolId: "bf-versprechen", scale: 0.91, rot: 0 },
{ symbolId: "bf-sekt", scale: 2.0, rot: 6 },
{ symbolId: "bf-prost", scale: 0.83, rot: 6 },
{ symbolId: "bf-film", scale: 1.0, rot: -4 },
{ symbolId: "bf-smiley", scale: 1.1, rot: 0 },
{ symbolId: "bf-unendlich", scale: 0.99, rot: 3 },
{ symbolId: "bf-kino", scale: 1.0, rot: 3 },
{ symbolId: "bf-kommunikation", scale: 1.0, rot: 0 },
{ symbolId: "bf-freundinnen", scale: 1.09, rot: 0 },
{ symbolId: "bf-sushiplatte", scale: 1.0, rot: 5 },
{ symbolId: "bf-sushi", scale: 1.46, rot: 0 },
{ symbolId: "bf-shopping", scale: 1.0, rot: 0 },
{ symbolId: "bf-makeup", scale: 1.45, rot: 9 },
{ symbolId: "bf-bilder", scale: 1.0, rot: -5 },
{ symbolId: "bf-kamera", scale: 1.0, rot: 0 },
{ symbolId: "bf-karten", scale: 1.0, rot: -6 },
{ symbolId: "bf-blumen", scale: 1.0, rot: 0 },
{ symbolId: "bf-musik", scale: 0.9, rot: -2 },
{ symbolId: "bf-kaffee", scale: 1.0, rot: 0 },
{ symbolId: "bf-wellen", scale: 1.33, rot: 0 },
{ symbolId: "bf-kuchen", scale: 1.0, rot: -4 },
{ symbolId: "bf-ticket", scale: 1.0, rot: 0 },
{ symbolId: "bf-pizza", scale: 1.0, rot: 3 },
],
layouts: {
Geordnet: [
{ x: 10, y: 9, w: 17, h: 14 }, { x: 29, y: 9, w: 17, h: 14 }, { x: 50, y: 9, w: 17, h: 14 }, { x: 71, y: 9, w: 17, h: 14 }, { x: 90, y: 9, w: 17, h: 14 },
{ x: 10, y: 27, w: 17, h: 14 }, { x: 29, y: 27, w: 17, h: 14 }, { x: 50, y: 27, w: 17, h: 14 }, { x: 71, y: 27, w: 17, h: 14 }, { x: 90, y: 27, w: 17, h: 14 },
{ x: 10, y: 45, w: 17, h: 14 }, { x: 29, y: 45, w: 17, h: 14 }, { x: 50, y: 45, w: 17, h: 14 }, { x: 71, y: 45, w: 17, h: 14 }, { x: 90, y: 45, w: 17, h: 14 },
{ x: 10, y: 63, w: 17, h: 14 }, { x: 29, y: 63, w: 17, h: 14 }, { x: 50, y: 63, w: 17, h: 14 }, { x: 71, y: 63, w: 17, h: 14 }, { x: 90, y: 63, w: 17, h: 14 },
{ x: 10, y: 81, w: 17, h: 14 }, { x: 29, y: 81, w: 17, h: 14 }, { x: 50, y: 81, w: 17, h: 14 }, { x: 71, y: 81, w: 17, h: 14 }, { x: 90, y: 81, w: 17, h: 14 },
],
Klassisch: [
{ x: 13.4, y: 9.6, w: 22, h: 19 },
{ x: 45.7, y: 10, w: 30, h: 22.5 },
{ x: 82.8, y: 8.2, w: 32.4, h: 25.1 },
{ x: 9.1, y: 30.1, w: 11, h: 8.3 },
{ x: 26, y: 28.7, w: 22, h: 18 },
{ x: 47.6, y: 29.7, w: 22, h: 18 },
{ x: 70, y: 25, w: 16, h: 15 },
{ x: 90.8, y: 23.7, w: 14, h: 10 },
{ x: 91.2, y: 32.2, w: 17, h: 12.8 },
{ x: 18.1, y: 43.1, w: 19, h: 16 },
{ x: 47.6, y: 51.2, w: 35, h: 32 },
{ x: 76.2, y: 48.4, w: 28, h: 19 },
{ x: 87.5, y: 43.8, w: 17, h: 12.8 },
{ x: 12.4, y: 54.4, w: 21, h: 18 },
{ x: 25.4, y: 68.8, w: 11, h: 8.3 },
{ x: 39.6, y: 73.8, w: 17, h: 16 },
{ x: 59.8, y: 75.2, w: 17, h: 15 },
{ x: 83, y: 65, w: 22, h: 18 },
{ x: 12, y: 77.3, w: 24, h: 18 },
{ x: 73.4, y: 80.5, w: 12.6, h: 9.5 },
{ x: 89.8, y: 77.6, w: 17, h: 12.8 },
{ x: 19, y: 90.7, w: 27, h: 20.3 },
{ x: 41, y: 88.6, w: 16.7, h: 11.1 },
{ x: 63.1, y: 92.5, w: 21, h: 15.8 },
{ x: 87.5, y: 93.2, w: 23.3, h: 16.3 },
],
},
texts: [{ text: "", x: 50, y: 97, fs: 7 }] },
{ id: "freund", label: "Freund / Partner", slots: ["sonne", "fussballtor", "berge", "konsole", "pizza", "herzpaar", "dog-labrador", "espresso", "auto-suv", "musiknote", "bierkruege", "kamera", "flugzeug", "grill", "rennrad"] },
{ id: "mama", label: "Mama", slots: ["sonne", "liebesbrief", "blume", "sonnenblume", "buecherstapel", "tulpen", "herzpaar", "espresso", "cat-sitting", "musiknote", "croissant", "kamera", "weinglaeser", "koffer", "palme"] },
{ id: "papa", label: "Papa", slots: ["berge", "fussballtor", "sonne", "auto-suv", "grill", "espresso", "dog-germanshepherd", "herzpaar", "campervan", "musiknote", "bier", "kamera", "weinglaeser", "zeitung", "pickup"] },
{ id: "selbst", label: "Für mich", slots: ["sonne", "musiknote", "palme", "herzpaar", "buecherstapel", "mond", "dog-frenchbulldog", "espresso", "weltreise", "kamera", "croissant", "konsole", "cocktail", "blume", "berge"] },
{ id: "leer", label: "Leer starten", slots: null },
];
function applyTemplate(tpl) {
// Template-spezifische Layouts: überschreiben die globalen LAYOUT_STYLES,
// oder setzen sie auf BUILTIN zurück wenn das Template keine eigenen hat.
if (tpl.layouts) {
for (const k of Object.keys(tpl.layouts)) LAYOUT_STYLES[k] = JSON.parse(JSON.stringify(tpl.layouts[k]));
state.usingTemplateLayout = true;
} else {
for (const k of Object.keys(BUILTIN_LAYOUTS)) LAYOUT_STYLES[k] = JSON.parse(JSON.stringify(BUILTIN_LAYOUTS[k]));
state.usingTemplateLayout = false;
}
// Layout-Stil erzwingen wenn im Template angegeben (z.B. Bestseller 2 → Klassisch)
if (tpl.layoutStyle) {
state.layoutStyle = tpl.layoutStyle;
document.querySelectorAll(".layout-btn").forEach(b => {
b.style.borderColor = b.dataset.layout === tpl.layoutStyle ? "var(--peach-600)" : "";
});
}
if (tpl.texts) {
state.texts = tpl.texts.map(t => ({ ...t }));
document.getElementById("textFields").innerHTML = "";
renderName();
}
const slotCount = tpl.slotCount || SLOT_COUNT;
state.slots = Array.from({ length: slotCount }, (_, i) => {
if (tpl.slotData && tpl.slotData[i]) {
const d = tpl.slotData[i];
return bySym(d.symbolId)
? { symbolId: d.symbolId, scale: d.scale ?? 1, rot: d.rot ?? ROT_PATTERN[i % ROT_PATTERN.length] }
: { symbolId: null, scale: 1, rot: 0 };
}
const id = tpl.slots ? tpl.slots[i] : null;
return id && bySym(id)
? { symbolId: id, scale: 1, rot: ROT_PATTERN[i % ROT_PATTERN.length] }
: { symbolId: null, scale: 1, rot: 0 };
});
state.selected = null;
toolsEl.style.display = "none";
titleEl.textContent = "2 · Tippe direkt auf ein Symbol";
renderSlots();
document.querySelectorAll("#templateChips .tab").forEach(b =>
b.classList.toggle("active", b.dataset.tpl === tpl.id),
);
}
const chipsEl = document.getElementById("templateChips");
TEMPLATES.forEach(tpl => {
const b = document.createElement("button");
b.className = "tab";
b.dataset.tpl = tpl.id;
b.textContent = tpl.label;
b.addEventListener("click", () => applyTemplate(tpl));
chipsEl.appendChild(b);
});
// ---- Warenkorb-Demo (→ Shopify line item properties) ---------------------------
function designProperties() {
const INK_NAMES = { "#1800ad": "Royalblau", "#ffffff": "Weiß", "#111111": "Schwarz", "#1e4632": "Dunkelgrün", "#c62828": "Rot", "#d4628f": "Pink" };
const filled = state.slots.filter(s => s.symbolId).length;
const wishes = state.slots.filter(s => s.symbolId === "wunsch").length;
let summary = filled + (filled === 1 ? " Motiv" : " Motive");
if (wishes > 0) summary += " · inkl. " + wishes + (wishes === 1 ? " Wunschsymbol" : " Wunschsymbole");
const props = {
// --- für den Kunden im Warenkorb sichtbar ---
"Symbol-Farbe": INK_NAMES[state.inkColor] || state.inkColor,
"Dein Design": summary,
// --- nur für dich: Schlüssel mit „_" blendet Shopify beim Kunden aus,
// bleibt aber auf der Bestellung sichtbar (für Druckdatei/Operator) ---
"_design_version": "proto-0.2",
"_Shirt-Farbe": (document.querySelector('.swatch[data-color="' + state.shirtColor + '"]') || {}).ariaLabel || state.shirtColor,
"_Layout": state.layoutStyle,
"_Druck": "Rückseite · 305×406 mm",
...Object.fromEntries(state.texts.filter(t => t.text).map((t, i) => [
"_Text " + (i + 1), t.text + " (" + Math.round(t.x) + "% / " + Math.round(t.y) + "%)"])),
};
state.slots.forEach((s, i) => {
if (s.symbolId) {
const rot = s.rot ? ", " + (s.rot > 0 ? "+" : "") + s.rot + "°" : "";
props["_Position " + (i + 1)] = s.symbolId === "wunsch"
? "✏️ WUNSCHSYMBOL: „" + (s.wish || "?") + "\u201c (+4,99 €)"
: bySym(s.symbolId).label + " (" + Math.round((s.scale || 1) * 100) + " %" + rot + ")";
}
});
// v5: jeder Slot bekommt seine exakte Position (x/y/w/h in % der Druckzone),
// Skalierung und Winkel — genug um das Design 1:1 zu rekonstruieren.
props["_design_json"] = JSON.stringify({
v: 5,
shirt: state.shirtColor,
ink: state.inkColor,
layout: state.layoutStyle,
texts: state.texts.filter(t => t.text),
slots: state.slots.map((s, i) => {
if (!s.symbolId) return null;
const L = activeLayout()[i] || { x: 50, y: 50, w: 14, h: 11 };
return {
id: s.symbolId,
x: Math.round(L.x * 10) / 10,
y: Math.round(L.y * 10) / 10,
w: Math.round(L.w * 10) / 10,
h: Math.round((L.h || L.w) * 10) / 10,
scale: s.scale || 1,
rot: s.rot || 0,
dx: s.dx || 0,
dy: s.dy || 0,
...(s.flip ? { flip: true } : {}),
...(s.wish ? { wish: s.wish } : {}),
};
}),
});
return props;
}
const GESCHUETZT = ["nike","adidas","puma","disney","mickey","micky","harry potter","hogwarts","pokemon","pokémon","pikachu","star wars","darth","yoda","marvel","batman","superman","spiderman","spider-man","bvb","borussia","bayern münchen","fc bayern","schalke","mercedes","bmw","audi","porsche","vw","volkswagen","gucci","louis vuitton","chanel","rolex","barbie","lego","mario","nintendo","minions","paw patrol","elsa","frozen","hello kitty","snoopy","pistole","gewehr","waffe","ak47","ak-47","granate","hakenkreuz","swastika","sieg heil","isis","blutbad"];
let pendingWishSlot = null;
const wishBg = document.getElementById("wishBg");
function openWishModal(i) {
pendingWishSlot = i;
document.getElementById("wishText").value = state.slots[i].wish || "";
wishBg.classList.add("open");
}
document.getElementById("wishCancel").addEventListener("click", () => wishBg.classList.remove("open"));
document.getElementById("wishOk").addEventListener("click", () => {
const txt = document.getElementById("wishText").value.trim();
if (!txt) { wishBg.classList.remove("open"); return; }
const low = txt.toLowerCase();
// mehrere Motive in einem Feld? (Aufzählungen) — nachfragen statt durchwinken
if (/(,| und | sowie | & | \+ )/.test(" " + low + " ")) {
const ok = confirm("Das klingt nach mehreren Motiven. Pro Wunschfeld zeichnen wir EIN Motiv (je 4,99 €) — Bestandteile eines Motivs (z. B. Wohnwagen mit Surfbrett) sind okay. Beschreibst du ein einzelnes Motiv?");
if (!ok) return;
}
const hit = GESCHUETZT.find(g => low.includes(g));
if (hit) {
alert("Dieser Begriff ist markenrechtlich geschützt — das dürfen wir leider nicht zeichnen. Beschreib das Motiv stattdessen allgemein.");
return;
}
const i = pendingWishSlot;
state.slots[i].symbolId = "wunsch";
state.slots[i].wish = txt.slice(0, 120);
state.slots[i].scale = state.slots[i].scale || 1;
wishBg.classList.remove("open");
selectSlot(i);
});
// ============================================================================
// SHOPIFY-WARENKORB · Größen live aus dem Produkt + echter /cart/add.js
// ============================================================================
const PP_SIZE_RE = /^(XXS|XS|S|M|L|XL|XXL|XXXL|2XL|3XL|4XL|5XL)$/i;
function ppEqi(a, b) { return String(a == null ? "" : a).trim().toLowerCase() === String(b == null ? "" : b).trim().toLowerCase(); }
function ppVariantOpts(v) { return [v.option1, v.option2, v.option3].filter(x => x != null && x !== ""); }
function ppVariantSize(v) { return ppVariantOpts(v).find(o => PP_SIZE_RE.test(String(o).trim())); }
function ppVariantColor(v) { return ppVariantOpts(v).find(o => !PP_SIZE_RE.test(String(o).trim())); }
function ppCurrentColorName() {
const sw = document.querySelector(".swatch:not(.ink-swatch).active");
return sw ? (sw.getAttribute("aria-label") || "").trim() : "";
}
function ppFindVariant(colorName, size) {
if (!PP_PRODUCT || !PP_PRODUCT.variants) return null;
return PP_PRODUCT.variants.find(v => ppEqi(ppVariantColor(v), colorName) && ppEqi(ppVariantSize(v), size)) || null;
}
function ppRefreshSizeAvailability() {
const row = document.getElementById("sizeRow");
if (!row || !PP_PRODUCT) return;
const color = ppCurrentColorName();
row.querySelectorAll(".size-btn").forEach(b => {
const v = ppFindVariant(color, b.dataset.size);
const ok = !!(v && v.available);
b.disabled = !ok;
if (!ok && b.classList.contains("active")) { b.classList.remove("active"); if (state.size === b.dataset.size) state.size = null; }
});
}
function ppBuildSizes() {
const row = document.getElementById("sizeRow");
if (!row || !PP_PRODUCT) return;
const seen = [];
PP_PRODUCT.variants.forEach(v => { const s = ppVariantSize(v); if (s && !seen.includes(s)) seen.push(s); });
row.innerHTML = 'Größe:';
seen.forEach(sz => {
const b = document.createElement("button");
b.type = "button"; b.className = "size-btn"; b.textContent = sz; b.dataset.size = sz;
if (state.size === sz) b.classList.add("active");
b.addEventListener("click", () => {
if (b.disabled) return;
state.size = sz;
row.querySelectorAll(".size-btn").forEach(x => x.classList.remove("active"));
b.classList.add("active");
});
row.appendChild(b);
});
ppRefreshSizeAvailability();
}
function ppGateColors() {
if (!PP_PRODUCT) return;
const colors = new Set(PP_PRODUCT.variants.map(v => String(ppVariantColor(v) || "").trim().toLowerCase()));
document.querySelectorAll(".swatch:not(.ink-swatch)").forEach(sw => {
const name = (sw.getAttribute("aria-label") || "").trim().toLowerCase();
const ok = colors.has(name);
sw.disabled = !ok;
sw.style.opacity = ok ? "" : "0.3";
if (!ok) sw.title = "in dieser Farbe (noch) nicht verfügbar";
});
}
async function ppLoadProduct() {
const row = document.getElementById("sizeRow");
try {
const r = await fetch("/products/" + PP_PRODUCT_HANDLE + ".js", { headers: { "Accept": "application/json" } });
if (!r.ok) throw new Error("HTTP " + r.status);
PP_PRODUCT = await r.json();
ppBuildSizes();
ppGateColors();
} catch (e) {
console.warn("[PP] Produkt '" + PP_PRODUCT_HANDLE + "' nicht ladbar:", e);
if (row) row.innerHTML = 'Größe:Produkt nicht gefunden — Handle prüfen.';
}
// Wunschsymbol-Aufpreis-Produkt laden (für die 4,99-€-Positionen)
try {
const rw = await fetch("/products/" + PP_WISH_HANDLE + ".js", { headers: { "Accept": "application/json" } });
if (rw.ok) { const wp = await rw.json(); PP_WISH_VARIANT_ID = wp.variants && wp.variants[0] && wp.variants[0].id; }
} catch (e) { console.warn("[PP] Wunschsymbol-Produkt '" + PP_WISH_HANDLE + "' nicht ladbar:", e); }
}
async function ppAddToCart() {
const colorName = ppCurrentColorName();
const size = state.size;
const row = document.getElementById("sizeRow");
if (!size) {
if (row) { row.scrollIntoView({ behavior: "smooth", block: "center" }); row.querySelectorAll(".size-btn:not(:disabled)").forEach(b => { b.style.boxShadow = "0 0 0 2px var(--peach-600)"; setTimeout(() => { b.style.boxShadow = ""; }, 1200); }); }
alert("Bitte zuerst eine Größe wählen.");
return;
}
if (!PP_PRODUCT) { alert("Das Produkt lädt noch — einen Moment, dann nochmal."); return; }
if (state.slots.filter(s => s.symbolId).length === 0) { alert("Füge mindestens ein Motiv hinzu, bevor du in den Warenkorb legst."); return; }
const v = ppFindVariant(colorName, size);
if (!v) { alert("Die Kombination „" + colorName + " / " + size + "“ ist gerade nicht verfügbar. Bitte andere Farbe oder Größe wählen."); return; }
const props = designProperties();
// Größe steckt bereits in der Variante (zeigt Shopify als „Größe: XS") —
// keine zusätzliche Property nötig, sonst stünde sie doppelt im Warenkorb.
const items = [{ id: v.id, quantity: 1, properties: props }];
// Pro genutztem Wunsch-Slot eine eigene 4,99-€-Position mit dem Wunschtext.
let wishCount = 0;
state.slots.forEach((s, i) => {
if (s.symbolId === "wunsch") {
wishCount++;
if (PP_WISH_VARIANT_ID) items.push({ id: PP_WISH_VARIANT_ID, quantity: 1, properties: { "Wunsch": (s.wish || "—"), "_Slot": String(i + 1) } });
}
});
if (wishCount > 0 && !PP_WISH_VARIANT_ID) { alert("Der Wunschsymbol-Aufpreis konnte gerade nicht geladen werden. Bitte lade die Seite neu und versuch es nochmal."); return; }
const btn = document.getElementById("addToCart");
const cta = document.getElementById("ctaCart");
const prev = btn ? btn.textContent : "";
if (btn) { btn.disabled = true; btn.textContent = "Wird hinzugefügt …"; }
if (cta) cta.disabled = true;
try {
const r = await fetch("/cart/add.js", {
method: "POST",
headers: { "Content-Type": "application/json", "Accept": "application/json" },
body: JSON.stringify({ items: items }),
});
if (!r.ok) { const t = await r.text().catch(() => ""); throw new Error(t || ("HTTP " + r.status)); }
window.location.href = PP_REDIRECT_AFTER_ADD;
} catch (e) {
console.error("[PP] /cart/add.js fehlgeschlagen:", e);
alert("Hoppla — das Hinzufügen hat nicht geklappt. Bitte nochmal versuchen.");
if (btn) { btn.disabled = false; btn.textContent = prev; }
if (cta) cta.disabled = false;
}
}
const modalBg = document.getElementById("modalBg");
document.getElementById("ctaCart").addEventListener("click", () => {
document.getElementById("addToCart").click();
});
document.getElementById("addToCart").addEventListener("click", ppAddToCart);
document.getElementById("copyDesignJson").addEventListener("click", async () => {
const dj = designProperties()["_design_json"];
try { await navigator.clipboard.writeText(dj); alert("Kopiert — unten bei Bestellung-laden einfügen."); }
catch (e) { window.prompt("Manuell kopieren:", dj); }
});
document.getElementById("modalClose").addEventListener("click", () => modalBg.classList.remove("open"));
modalBg.addEventListener("click", e => { if (e.target === modalBg) modalBg.classList.remove("open"); });
// ---- Druckdatei: Gelato-Rückendruck 305 × 406 mm @ 300 DPI ----------------------
// 305/25.4*300 = 3602 px · 406/25.4*300 = 4795 px · transparenter Hintergrund.
// Der pHYs-Patch unten schreibt echte 300-DPI-Metadaten ins PNG (Canvas liefert
// sonst 72 DPI im Header, auch wenn die Pixelmaße stimmen).
// CRC32 für den PNG-Chunk (Standard-Implementierung, einmalige Tabelle)
const CRC_TABLE = (() => {
const t = new Uint32Array(256);
for (let n = 0; n < 256; n++) {
let c = n;
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
t[n] = c >>> 0;
}
return t;
})();
function crc32(bytes) {
let c = 0xffffffff;
for (let i = 0; i < bytes.length; i++) c = CRC_TABLE[(c ^ bytes[i]) & 0xff] ^ (c >>> 8);
return (c ^ 0xffffffff) >>> 0;
}
// Fügt nach dem IHDR-Chunk (endet bei Byte 33) einen pHYs-Chunk mit 300 DPI ein.
function withDpi300(pngBytes) {
const PPM = 11811; // 300 dpi ≈ 11811 Pixel/Meter
const chunk = new Uint8Array(21);
const dv = new DataView(chunk.buffer);
dv.setUint32(0, 9); // length
chunk.set([0x70, 0x48, 0x59, 0x73], 4); // "pHYs"
dv.setUint32(8, PPM); dv.setUint32(12, PPM);
chunk[16] = 1; // unit: meter
dv.setUint32(17, crc32(chunk.subarray(4, 17)));
const out = new Uint8Array(pngBytes.length + 21);
out.set(pngBytes.subarray(0, 33), 0);
out.set(chunk, 33);
out.set(pngBytes.subarray(33), 54);
return out;
}
document.getElementById("exportPng").addEventListener("click", async () => {
const W = 3602, H = 4795; // 305 × 406 mm @ 300 DPI
const canvas = document.createElement("canvas");
canvas.width = W; canvas.height = H;
const ctx = canvas.getContext("2d");
const ink = state.inkColor;
// transparenter Hintergrund (Druck auf Stoff) — Symbole in Stofffarbe-Kontrast
const tasks = state.slots.map((slot, i) => {
if (!slot.symbolId || !activeLayout()[i]) return Promise.resolve();
if (slot.symbolId === "wunsch" && !slot.customSvg) return Promise.resolve(); // leerer Wunsch-Platzhalter wird nicht gedruckt
const mult = slot.scale || 1;
const sym = bySym(slot.symbolId);
const rawSvg = slot.customSvg || sym.svg; // Wunsch-Upload: gezeichnetes SVG statt Platzhalter
const L = activeLayout()[i] || { x: 50, y: 50, w: 14, h: 11 };
// Seitenverhältnis IMMER aus der viewBox — img.width/height ist unzuverlässig
// (viele SVGs haben fixe width/height="960" trotz nicht-quadratischer viewBox → verzerrter Export)
let ar = 1;
const vbm = (rawSvg.match(/viewBox="([^"]+)"/) || [])[1];
if (vbm) { const p = vbm.trim().split(/[\s,]+/).map(Number); if (p.length === 4 && p[2] && p[3]) ar = p[2] / p[3]; }
// identische Geometrie wie die Vorschau: Layout-% → Druck-Pixel, Symbol "meet" in die Box
const boxW = (L.w / 100) * W * mult;
const boxH = ((L.h || L.w) / 100) * H * mult;
let dw = boxW, dh = boxW / ar;
if (dh > boxH) { dh = boxH; dw = boxH * ar; }
// Quelle einfärben + fixe width/height im Root-