/* Trilogy Hub — Bild-Positions-Editor (gleicher Rahmen wie Slot-Anzeige) */ (() => { "use strict"; function computeLayout(imgW, imgH, frameW, frameH, focus, zoom) { const fx = focus?.x ?? 0.5; const fy = focus?.y ?? 0.5; const z = Math.max(1, zoom ?? 1); const cover = Math.max(frameW / imgW, frameH / imgH); const scale = cover * z; const w = imgW * scale; const h = imgH * scale; let left = frameW / 2 - fx * w; let top = frameH / 2 - fy * h; left = Math.min(0, Math.max(frameW - w, left)); top = Math.min(0, Math.max(frameH - h, top)); return { width: w, height: h, left, top }; } function focusFromLayout(imgW, imgH, frameW, frameH, layout, zoom) { const z = Math.max(1, zoom ?? 1); const cover = Math.max(frameW / imgW, frameH / imgH); const w = imgW * cover * z; const h = imgH * cover * z; return { x: Math.min(1, Math.max(0, (frameW / 2 - layout.left) / w)), y: Math.min(1, Math.max(0, (frameH / 2 - layout.top) / h)), }; } function applyLayout(img, layout) { img.style.width = layout.width + "px"; img.style.height = layout.height + "px"; img.style.left = layout.left + "px"; img.style.top = layout.top + "px"; } function loadImage(src) { return new Promise((resolve, reject) => { const img = new Image(); img.onload = () => resolve(img); img.onerror = () => reject(new Error("Bild konnte nicht geladen werden")); img.src = src; }); } function slotFrameSize(slot) { const cs = getComputedStyle(slot); let w = slot.offsetWidth; let h = slot.offsetHeight; if (w < 40) w = slot.parentElement?.offsetWidth || 320; if (h < 40) { const mh = cs.minHeight; h = mh && mh !== "0px" ? parseFloat(mh) : 220; } const isHero = slot.classList.contains("!rounded-none") || slot.closest(".min-h-\\[72vh\\]"); const maxPreview = isHero ? 400 : 300; const ar = w / h; if (w > maxPreview) { w = maxPreview; h = w / ar; } return { width: Math.round(w), height: Math.round(h), radius: cs.borderRadius, label: slot.dataset.slot || "Bild" }; } async function open({ file, slot, focus, zoom, imageSrc }) { let src = imageSrc; let revoke = null; if (file) { src = URL.createObjectURL(file); revoke = src; } const frame = slotFrameSize(slot); let img; try { img = await loadImage(src); } catch (e) { if (revoke) URL.revokeObjectURL(revoke); throw e; } const state = { focus: { x: focus?.x ?? 0.5, y: focus?.y ?? 0.5 }, zoom: Math.max(1, zoom ?? 1), dragging: false, lastX: 0, lastY: 0, pinchDist: 0, pinchZoom: 1, }; return new Promise((resolve) => { const backdrop = document.createElement("div"); backdrop.className = "img-editor-backdrop"; backdrop.innerHTML = `

${frame.label} positionieren

So wird das Bild im Rahmen angezeigt — ziehen & zoomen

`; document.body.appendChild(backdrop); document.body.style.overflow = "hidden"; const frameEl = backdrop.querySelector("#ieFrame"); const imgEl = backdrop.querySelector("#ieImg"); const zoomInput = backdrop.querySelector("#ieZoom"); const zoomVal = backdrop.querySelector("#ieZoomVal"); let layout = { left: 0, top: 0, width: 0, height: 0 }; const fw = () => frameEl.clientWidth; const fh = () => frameEl.clientHeight; function syncFromFocus() { layout = computeLayout(img.naturalWidth, img.naturalHeight, fw(), fh(), state.focus, state.zoom); applyLayout(imgEl, layout); } function syncFocusFromLayout() { state.focus = focusFromLayout(img.naturalWidth, img.naturalHeight, fw(), fh(), layout, state.zoom); } function finish(result) { document.body.style.overflow = ""; backdrop.remove(); if (revoke) URL.revokeObjectURL(revoke); resolve(result); } function setZoom(z) { const old = { ...layout }; state.zoom = Math.min(4, Math.max(1, z)); layout = computeLayout(img.naturalWidth, img.naturalHeight, fw(), fh(), state.focus, state.zoom); const cx = fw() / 2, cy = fh() / 2; const rx = (cx - old.left) / old.width; const ry = (cy - old.top) / old.height; layout.left = cx - rx * layout.width; layout.top = cy - ry * layout.height; layout.left = Math.min(0, Math.max(fw() - layout.width, layout.left)); layout.top = Math.min(0, Math.max(fh() - layout.height, layout.top)); syncFocusFromLayout(); applyLayout(imgEl, layout); zoomInput.value = state.zoom; zoomVal.textContent = state.zoom.toFixed(1) + "×"; } imgEl.addEventListener("load", syncFromFocus, { once: true }); if (imgEl.complete) syncFromFocus(); frameEl.addEventListener("pointerdown", (e) => { if (e.target.closest("button, input")) return; state.dragging = true; state.lastX = e.clientX; state.lastY = e.clientY; frameEl.setPointerCapture(e.pointerId); e.preventDefault(); }); frameEl.addEventListener("pointermove", (e) => { if (!state.dragging) return; layout.left = Math.min(0, Math.max(fw() - layout.width, layout.left + e.clientX - state.lastX)); layout.top = Math.min(0, Math.max(fh() - layout.height, layout.top + e.clientY - state.lastY)); state.lastX = e.clientX; state.lastY = e.clientY; applyLayout(imgEl, layout); syncFocusFromLayout(); }); frameEl.addEventListener("pointerup", () => { state.dragging = false; }); frameEl.addEventListener("pointercancel", () => { state.dragging = false; }); frameEl.addEventListener("wheel", (e) => { e.preventDefault(); setZoom(state.zoom + (e.deltaY > 0 ? -0.08 : 0.08)); }, { passive: false }); frameEl.addEventListener("touchstart", (e) => { if (e.touches.length === 2) { state.pinchDist = Math.hypot(e.touches[0].clientX - e.touches[1].clientX, e.touches[0].clientY - e.touches[1].clientY); state.pinchZoom = state.zoom; } }, { passive: true }); frameEl.addEventListener("touchmove", (e) => { if (e.touches.length === 2 && state.pinchDist > 0) { e.preventDefault(); const dist = Math.hypot(e.touches[0].clientX - e.touches[1].clientX, e.touches[0].clientY - e.touches[1].clientY); setZoom(state.pinchZoom * (dist / state.pinchDist)); } }, { passive: false }); zoomInput.addEventListener("input", () => setZoom(+zoomInput.value)); backdrop.addEventListener("click", (e) => { if (e.target === backdrop) finish(null); }); backdrop.querySelector(".img-editor-close").onclick = () => finish(null); backdrop.querySelector('[data-act="reset"]').onclick = () => { state.focus = { x: 0.5, y: 0.5 }; state.zoom = 1; setZoom(1); }; backdrop.querySelector('[data-act="save"]').onclick = () => { syncFocusFromLayout(); finish({ focus: { ...state.focus }, zoom: state.zoom }); }; }); } window.TrilogyImageEditor = { open, computeLayout, applyLayout }; })();