(() => { const anchors = { "top-left": [0, 0], "top-center": [-50, 0], "top-right": [-100, 0], "center-left": [0, -50], center: [-50, -50], "center-right": [-100, -50], "bottom-left": [0, -100], "bottom-center": [-50, -100], "bottom-right": [-100, -100] }; function finite(value, fallback) { const number = Number(value); return Number.isFinite(number) ? number : fallback; } function applyBox(element, config) { const [translateX, translateY] = anchors[config.anchor] || anchors["top-left"]; element.style.left = `${finite(config.x, 0)}%`; element.style.top = `${finite(config.y, 0)}%`; element.style.width = `${finite(config.width, 100)}%`; element.style.height = `${finite(config.height, 100)}%`; element.style.opacity = `${finite(config.opacity, 1)}`; element.style.transform = `translate(${translateX}%, ${translateY}%) translateZ(0)`; } function shrinkText(box, content, preferredSize, minimumSize, scale) { let size = preferredSize; const fit = () => { content.style.fontSize = `${size * scale}px`; if (size <= minimumSize || (content.scrollWidth <= box.clientWidth && content.scrollHeight <= box.clientHeight)) return; size = Math.max(minimumSize, size - Math.max(1, preferredSize / 24)); requestAnimationFrame(fit); }; requestAnimationFrame(fit); } function restartMediaElement(media) { if (!media) return false; const startAt = Math.max(0, finite(media.dataset.startAt, 0)); try { media.currentTime = startAt; } catch {} const playback = media.play?.(); playback?.catch?.(() => {}); return true; } function buildMedia(module, values, options, renderType) { const media = document.createElement(renderType); media.className = `lumi-overlay-${renderType}`; media.src = values.url; media.dataset.originalUrl = values.url; media.dataset.moduleId = module.id; media.dataset.startAt = `${finite(values.startAt, 0)}`; media.preload = "auto"; media.playsInline = true; media.loop = values.playBehavior === "loop"; media.controls = values.showControls === true || values.playBehavior === "manual"; media.muted = values.muted === true; media.volume = Math.min(1, Math.max(0, finite(values.volume, 1))); media.playbackRate = Math.min(4, Math.max(0.25, finite(values.playbackRate, 1))); media.autoplay = !options.editor && values.playBehavior !== "manual"; media.referrerPolicy = "no-referrer"; media.style.pointerEvents = !options.editor && media.controls ? "auto" : "none"; if (renderType === "video") media.style.objectFit = values.fit || "contain"; media.addEventListener("loadedmetadata", () => { const startAt = Math.max(0, finite(values.startAt, 0)); if (startAt && startAt < finite(media.duration, Infinity)) { try { media.currentTime = startAt; } catch {} } media.playbackRate = Math.min(4, Math.max(0.25, finite(values.playbackRate, 1))); if (media.autoplay) media.play().catch(() => {}); }, { once: true }); return media; } function webFrameLayout(values) { const left = Math.min(95, Math.max(0, finite(values.cropLeft, 0))); const right = Math.min(95, Math.max(0, finite(values.cropRight, 0))); const top = Math.min(95, Math.max(0, finite(values.cropTop, 0))); const bottom = Math.min(95, Math.max(0, finite(values.cropBottom, 0))); const zoom = Math.min(5, Math.max(0.1, finite(values.zoom, 1))); const visibleWidth = Math.max(5, 100 - left - right); const visibleHeight = Math.max(5, 100 - top - bottom); return { width: (10000 * zoom) / visibleWidth, height: (10000 * zoom) / visibleHeight, left: (-100 * left * zoom) / visibleWidth, top: (-100 * top * zoom) / visibleHeight }; } function buildWebsite(module, values, options) { const frame = document.createElement("iframe"); frame.className = "lumi-overlay-frame"; frame.src = values.url; frame.dataset.originalUrl = values.url; frame.dataset.moduleId = module.id; let sandbox = "allow-scripts allow-forms allow-popups allow-presentation"; try { if (new URL(values.url, window.location.href).origin !== window.location.origin) sandbox += " allow-same-origin"; } catch {} frame.sandbox = sandbox; frame.referrerPolicy = "no-referrer"; frame.setAttribute("allow", "autoplay; fullscreen"); frame.style.pointerEvents = options.editor ? "none" : (values.allowPointerEvents ? "auto" : "none"); const layout = webFrameLayout(values); const root = document.createElement("div"); root.className = "lumi-overlay-web-root"; const shadow = root.attachShadow({ mode: "open" }); const style = document.createElement("style"); style.textContent = ` :host { display: block; position: relative; width: 100%; height: 100%; overflow: hidden; } .lumi-overlay-frame { position: absolute; display: block; border: 0; background: transparent; width: ${layout.width}%; height: ${layout.height}%; left: ${layout.left}%; top: ${layout.top}%; } ${values.customCss || ""} `; shadow.append(style, frame); return root; } function findSourceContent(root, moduleId, selector) { const box = root?.querySelector?.(`.lumi-overlay-module[data-module-id="${CSS.escape(moduleId)}"]`); return box?.querySelector?.(selector) || box?.querySelector?.(".lumi-overlay-web-root")?.shadowRoot?.querySelector?.(selector) || null; } function buildModule(module, options = {}) { const renderType = module.renderType || module.type; const values = module.config || {}; const scale = finite(options.scale, 1); const box = document.createElement("section"); box.className = `lumi-overlay-module lumi-overlay-module-${renderType}`; if (options.editor) box.classList.add("is-editor-source"); box.dataset.moduleId = module.id; box.dataset.moduleType = module.type; box.dataset.renderType = renderType; box.dataset.moduleName = module.name || "Source"; applyBox(box, values); if (renderType === "text") { box.classList.add("lumi-overlay-text", `overflow-${values.overflow || "wrap"}`); const content = document.createElement("span"); content.className = "lumi-overlay-text-content"; content.textContent = values.text || ""; const horizontal = values.horizontalAlign || values.align || "left"; const vertical = values.verticalAlign || "center"; box.style.textAlign = horizontal; box.style.justifyContent = horizontal === "center" ? "center" : horizontal === "right" ? "flex-end" : "flex-start"; box.style.alignItems = vertical === "center" ? "center" : vertical === "bottom" ? "flex-end" : "flex-start"; box.style.color = values.color || "#ffffff"; box.style.background = values.background || "transparent"; box.style.padding = `${finite(values.padding, 0) * scale}px`; content.style.fontWeight = `${finite(values.fontWeight, 700)}`; content.style.fontSize = `${finite(values.fontSize, 48) * scale}px`; box.appendChild(content); if (values.overflow === "shrink") shrinkText(box, content, finite(values.fontSize, 48), 8, scale); } else if (renderType === "image" && values.url) { const image = document.createElement("img"); image.className = "lumi-overlay-image"; image.alt = ""; image.src = values.url; image.referrerPolicy = "no-referrer"; image.style.objectFit = values.fit || "contain"; box.appendChild(image); } else if ((renderType === "video" || renderType === "audio") && values.url) { const media = buildMedia(module, values, options, renderType); box.appendChild(media); if (renderType === "audio" && options.editor) { const placeholder = document.createElement("span"); placeholder.className = "lumi-overlay-audio-placeholder"; placeholder.textContent = "Audio source"; box.appendChild(placeholder); } } else if (renderType === "web" && values.url) { box.appendChild(buildWebsite(module, values, options)); } if (options.editor) { const tag = document.createElement("span"); tag.className = "overlay-canvas-source-label"; tag.textContent = module.name || "Source"; box.appendChild(tag); for (const direction of ["nw", "n", "ne", "e", "se", "s", "sw", "w"]) { const handle = document.createElement("button"); handle.type = "button"; handle.className = `overlay-resize-handle handle-${direction}`; handle.dataset.resizeHandle = direction; handle.setAttribute("aria-label", `Resize ${module.name || "source"} from ${direction}`); box.appendChild(handle); } } return box; } function render(root, state, options = {}) { const canvas = options.canvas || state?.canvas || { width: 1920, height: 1080 }; const widthScale = root.clientWidth ? root.clientWidth / finite(canvas.width, 1920) : 1; const heightScale = root.clientHeight ? root.clientHeight / finite(canvas.height, 1080) : widthScale; const scale = options.scale || Math.min(widthScale, heightScale); const fragment = document.createDocumentFragment(); const elements = new Map(); if (state?.enabled !== false && state?.scene) { for (const module of state.scene.modules || []) { if (module.enabled === false) continue; const element = buildModule(module, { ...options, scale }); elements.set(module.id, element); fragment.appendChild(element); } } root.replaceChildren(fragment); return elements; } window.LumiOverlayRenderer = { anchors, applyBox, buildModule, findSourceContent, render, restartMediaElement }; })();