(() => { const chatStates = new Map(); 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; media.dispatchEvent(new CustomEvent("lumi-media-restart")); 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 = false; 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 = "none"; if (renderType === "video") { media.style.objectFit = values.fit || "contain"; media.hidden = true; media.dataset.playbackState = "waiting"; const show = () => { media.hidden = false; media.dataset.playbackState = "playing"; }; const hide = () => { media.hidden = true; media.dataset.playbackState = "stopped"; }; media.addEventListener("playing", show); for (const eventName of ["ended", "error", "abort", "emptied"]) media.addEventListener(eventName, hide); media.addEventListener("lumi-media-stop", hide); media.addEventListener("lumi-media-restart", hide); } else { media.hidden = true; media.setAttribute("aria-hidden", "true"); } 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"); const sourceUrl = values.renderUrl || values.url; const injectedDocument = Boolean(values.renderUrl); frame.className = "lumi-overlay-frame"; frame.src = sourceUrl; frame.dataset.originalUrl = sourceUrl; frame.dataset.moduleId = module.id; let sandbox = "allow-scripts allow-forms allow-popups allow-presentation"; try { if (!injectedDocument && new URL(sourceUrl, 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.setAttribute("allowtransparency", "true"); 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; background-color: rgba(0, 0, 0, 0); } .lumi-overlay-frame { position: absolute; display: block; border: 0; background-color: rgba(0, 0, 0, 0); width: ${layout.width}%; height: ${layout.height}%; left: ${layout.left}%; top: ${layout.top}%; } `; shadow.append(style, frame); return root; } function normalizedChannel(value) { return String(value || "").trim().toLowerCase().replace(/^#/, ""); } function chatAccepts(values, payload) { if (!(values.platforms || []).includes(payload.platform) || chatUserBlocked(values, payload)) return false; const filters = Array.isArray(values.channels) ? values.channels : []; if (!filters.length) return true; const aliases = [payload.channel?.id, payload.channel?.name, payload.channel?.key] .map(normalizedChannel) .filter(Boolean); return filters.some((entry) => { const raw = String(entry || "").trim(); const separator = raw.indexOf(":"); const platform = separator > 0 ? raw.slice(0, separator).trim().toLowerCase() : ""; const channel = normalizedChannel(separator > 0 ? raw.slice(separator + 1) : raw); return (!platform || platform === payload.platform) && (channel === "*" || aliases.includes(channel)); }); } function normalizedIdentity(value) { return String(value || "").trim().toLowerCase().replace(/^@/, ""); } function chatUserBlocked(values, payload) { return (values.blockedUsers || []).some((entry) => { const raw = String(entry || "").trim(); const separator = raw.indexOf(":"); if (separator < 1) return false; const platform = raw.slice(0, separator).toLowerCase(); const blocked = raw.slice(separator + 1).split("|").map(normalizedIdentity).filter(Boolean); const aliases = platform === "lumi" ? [payload.author?.lumi?.id, payload.author?.lumi?.username] : platform === payload.platform ? [payload.author?.id, payload.author?.username, payload.author?.name] : []; const normalizedAliases = aliases.map(normalizedIdentity).filter(Boolean); return blocked.some((value) => normalizedAliases.includes(value)); }); } function chatMessageElement(payload, values, { animate = true } = {}) { const row = document.createElement("article"); row.className = `message platform-${payload.platform}`; row.dataset.messageId = payload.id; row.dataset.platform = payload.platform; if (values.showAvatars && payload.author?.avatar) { const avatar = document.createElement("img"); avatar.className = "avatar"; avatar.src = payload.author.avatar; avatar.alt = ""; avatar.referrerPolicy = "no-referrer"; avatar.addEventListener("error", () => avatar.remove(), { once: true }); row.appendChild(avatar); } const content = document.createElement("div"); content.className = "content"; const meta = document.createElement("div"); meta.className = "meta"; if (values.showPlatform) { const platform = document.createElement("img"); platform.className = "platform"; platform.src = `/icons/platforms/${["twitch", "youtube", "discord"].includes(payload.platform) ? payload.platform : "lumi"}.svg`; platform.alt = payload.platform === "youtube" ? "YouTube" : payload.platform[0].toUpperCase() + payload.platform.slice(1); platform.title = platform.alt; meta.appendChild(platform); } const author = document.createElement("span"); author.className = "author"; author.textContent = payload.author?.name || "Viewer"; author.style.color = payload.author?.color || values.usernameColor || "#67e8f9"; meta.appendChild(author); if (values.showBadges) { for (const badgeData of payload.author?.badges || []) { const badge = document.createElement("img"); badge.className = "badge"; if (badgeData.image) { badge.src = badgeData.image; } else { const label = String(badgeData.label || "").toLowerCase(); const kind = /owner|broadcaster/.test(label) ? "owner" : /moderator|mod|staff/.test(label) ? "moderator" : /member|subscriber|sub|vip/.test(label) ? "member" : "generic"; badge.src = `/icons/badges/${kind}.svg`; } badge.alt = badgeData.label || "Badge"; badge.title = badge.alt; badge.referrerPolicy = "no-referrer"; meta.appendChild(badge); } } if (values.showTimestamp) { const timestamp = document.createElement("time"); timestamp.className = "timestamp"; timestamp.dateTime = new Date(payload.timestamp).toISOString(); timestamp.textContent = new Date(payload.timestamp).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); meta.appendChild(timestamp); } const messageText = String(payload.text || ""); if (messageText) { const text = document.createElement("div"); text.className = "text"; let cursor = 0; const emotes = [...(payload.emotes || [])].sort((a, b) => a.start - b.start); for (const emoteData of emotes) { if (emoteData.start < cursor || emoteData.start >= messageText.length) continue; text.append(document.createTextNode(messageText.slice(cursor, emoteData.start))); const emote = document.createElement("img"); emote.className = "emote"; emote.src = emoteData.image; emote.alt = emoteData.label || messageText.slice(emoteData.start, Math.min(messageText.length, emoteData.end + 1)) || "Emote"; emote.referrerPolicy = "no-referrer"; text.appendChild(emote); cursor = Math.min(messageText.length, emoteData.end + 1); } text.append(document.createTextNode(messageText.slice(cursor))); content.appendChild(text); } if (payload.media?.length) { const mediaRow = document.createElement("div"); mediaRow.className = "media-row"; for (const mediaData of payload.media) { const media = document.createElement(mediaData.type === "video" ? "video" : "img"); media.className = "media"; media.src = mediaData.url; media.setAttribute("aria-label", mediaData.alt || "Animated image"); media.referrerPolicy = "no-referrer"; if (media.tagName === "VIDEO") { media.autoplay = true; media.loop = true; media.muted = true; media.playsInline = true; } else media.alt = mediaData.alt || "Animated image"; media.addEventListener("error", () => media.remove(), { once: true }); mediaRow.appendChild(media); } content.appendChild(mediaRow); } content.prepend(meta); row.appendChild(content); if (animate && values.inAnimation !== "none" && Number(values.animationDurationMs) > 0) { row.classList.add("enter", `enter-${values.inAnimation}`); requestAnimationFrame(() => requestAnimationFrame(() => row.classList.add("enter-active"))); setTimeout(() => row.classList.remove("enter", "enter-active", `enter-${values.inAnimation}`), Number(values.animationDurationMs)); } return row; } function removeChatMessage(moduleId, messageId) { const state = chatStates.get(moduleId); if (!state) return; state.messages = state.messages.filter((message) => message.id !== messageId); const row = state.list?.querySelector?.(`[data-message-id="${CSS.escape(messageId)}"]`); if (!row) return; const values = state.values || {}; if (values.outAnimation !== "none" && Number(values.animationDurationMs) > 0) { row.classList.add("leave", `leave-${values.outAnimation}`); requestAnimationFrame(() => row.classList.add("leave-active")); setTimeout(() => row.remove(), Number(values.animationDurationMs)); } else row.remove(); } function scheduleChatRemoval(moduleId, payload, values) { if (!(Number(values.messageTimeoutSeconds) > 0)) return; const remaining = Math.max(0, payload.timestamp + Number(values.messageTimeoutSeconds) * 1000 - Date.now()); setTimeout(() => removeChatMessage(moduleId, payload.id), remaining); } const previewChatMessages = [ { platform: "twitch", text: "Hi chat!", author: { name: "CozyViewer", color: "#f472b6", avatar: "/icons/avatars/preview-pink.svg", badges: [{ label: "Subscriber", image: null }] } }, { platform: "discord", text: "The badges, avatar, and service icon move together.", author: { name: "PackMember", color: "#a78bfa", avatar: "/icons/avatars/preview-purple.svg", badges: [{ label: "Moderator", image: null }] } }, { platform: "youtube", text: "Short message", author: { name: "Lumi Fan", color: "#fb7185", avatar: "/icons/avatars/preview-red.svg", badges: [{ label: "Member", image: null }] } }, { platform: "twitch", text: "This longer sample wraps naturally so you can compare it with compact one-line messages.", author: { name: "StreamFriend", color: "#22d3ee", avatar: "/icons/avatars/preview-cyan.svg", badges: [] } }, { platform: "discord", text: "Animated arrivals and departures repeat in this preview.", author: { name: "Lumi", color: "#67e8f9", avatar: "/icons/platforms/lumi.svg", badges: [{ label: "Owner", image: null }] } }, { platform: "youtube", text: "Looks good!", author: { name: "ChatRegular", color: "#fbbf24", avatar: "/icons/avatars/preview-gold.svg", badges: [] } } ]; function nextPreviewMessage(moduleId, state) { const allowed = previewChatMessages.filter((message) => (state.values.platforms || []).includes(message.platform)); const samples = allowed.length ? allowed : previewChatMessages; const sample = samples[state.previewIndex % samples.length]; state.previewIndex += 1; return { ...sample, id: `${moduleId}-preview-${state.previewIndex}-${Date.now()}`, preview: true, timestamp: Date.now(), channel: { name: sample.platform === "discord" ? "#community" : "Live chat" }, author: { ...sample.author } }; } function schedulePreviewChat(moduleId, state) { clearTimeout(state.previewTimer); const delay = Math.max(1400, Number(state.values.animationDurationMs || 0) * 2 + 500); state.previewTimer = setTimeout(() => { if (!state.editor || !state.list?.isConnected) { state.previewTimer = null; return; } const payload = nextPreviewMessage(moduleId, state); state.messages.push(payload); const row = chatMessageElement(payload, state.values); if (state.values.newestAt === "top") state.list.prepend(row); else state.list.appendChild(row); const visibleLimit = Math.max(1, Math.min(Number(state.values.maxMessages || 8), 5)); while (state.messages.length > visibleLimit) removeChatMessage(moduleId, state.messages[0].id); schedulePreviewChat(moduleId, state); }, delay); } function buildChat(module, values, options) { const root = document.createElement("div"); root.className = "lumi-overlay-chat-root"; const shadow = root.attachShadow({ mode: "open" }); const style = document.createElement("style"); const chatCustomCss = values.customCss || ""; const alignment = values.horizontalAlign === "right" ? "flex-end" : values.horizontalAlign === "center" ? "center" : "flex-start"; const dock = Boolean(options.chatDock); style.textContent = ` :host { display:block; width:100%; ${dock ? "min-height:100%; height:auto; overflow:visible;" : "height:100%; overflow:hidden;"} box-sizing:border-box; background:${values.background}; color:${values.color}; font:${values.fontWeight} ${values.fontSize}px/${values.lineHeight} ${values.fontFamily}; } .chat { width:100%; ${dock ? "min-height:100vh; height:auto; overflow:visible;" : "height:100%; overflow:hidden;"} box-sizing:border-box; display:flex; flex-direction:column; align-items:${alignment}; justify-content:${values.newestAt === "bottom" ? "flex-end" : "flex-start"}; gap:${values.gap}px; padding:${values.padding}px; text-align:${values.horizontalAlign}; } .message { display:inline-flex; align-items:flex-start; align-self:${alignment}; gap:.55em; box-sizing:border-box; width:fit-content; max-width:100%; padding:.55em .7em; border-radius:${values.borderRadius}px; background:${values.messageBackground}; overflow:hidden; transition:opacity ${values.animationDurationMs}ms ease, transform ${values.animationDurationMs}ms ease; } .avatar { width:1.85em; height:1.85em; flex:0 0 auto; border-radius:50%; object-fit:cover; } .content { min-width:0; max-width:100%; flex:0 1 auto; } .meta { display:flex; align-items:center; justify-content:${alignment}; gap:.4em; min-width:0; margin-bottom:.12em; font-size:.78em; line-height:1.15; } .author { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-weight:800; } .platform { flex:0 0 auto; width:1.05em; height:1.05em; border-radius:.24em; object-fit:contain; } .badge { flex:0 0 auto; width:1.1em; height:1.1em; border-radius:.2em; object-fit:contain; } .timestamp { flex:0 0 auto; opacity:.65; font-size:.85em; } .text { overflow-wrap:anywhere; white-space:pre-wrap; } .emote { display:inline-block; width:auto; height:1.35em; margin:0 .08em; vertical-align:-.3em; object-fit:contain; } .media-row { display:flex; flex-wrap:wrap; justify-content:${alignment}; gap:.3em; margin-top:.2em; max-width:100%; } .media { display:block; width:auto; height:${Math.max(1.6, Number(values.lineHeight || 1.25) * 2)}em; max-width:100%; border-radius:.25em; object-fit:contain; } .enter-fade, .leave-active { opacity:0; } .enter-slide-left { opacity:0; transform:translateX(-1.5em); } .enter-slide-right { opacity:0; transform:translateX(1.5em); } .enter-slide-up { opacity:0; transform:translateY(-1em); } .enter-slide-down { opacity:0; transform:translateY(1em); } .enter-scale { opacity:0; transform:scale(.92); } .enter-active { opacity:1; transform:none; } .leave-slide-left.leave-active { transform:translateX(-1.5em); } .leave-slide-right.leave-active { transform:translateX(1.5em); } .leave-slide-up.leave-active { transform:translateY(-1em); } .leave-slide-down.leave-active { transform:translateY(1em); } .leave-scale.leave-active { transform:scale(.92); } ${chatCustomCss} ${dock ? ":host { width:100% !important; min-height:100% !important; height:auto !important; overflow:visible !important; } .chat { width:100% !important; min-height:100vh !important; height:auto !important; overflow:visible !important; } .message, .message * { animation:none !important; transition:none !important; } .message { transform:none !important; opacity:1 !important; }" : ""} `; const list = document.createElement("div"); list.className = "chat"; list.setAttribute("role", "log"); list.setAttribute("aria-live", "off"); shadow.append(style, list); let state = chatStates.get(module.id); if (!state) state = { messages: [], previewIndex: 0 }; state.values = values; state.list = list; state.editor = Boolean(options.editor); if (options.editor && !state.messages.length) { const initialCount = Math.max(1, Math.min(Number(values.maxMessages || 8), 5)); for (let index = 0; index < initialCount; index += 1) state.messages.push(nextPreviewMessage(module.id, state)); } state.messages = state.messages.slice(-Number(values.maxMessages || 8)); const acceptedMessages = state.messages.filter((message) => message.preview ? (values.platforms || []).includes(message.platform) : chatAccepts(values, message)); const display = values.newestAt === "top" ? [...acceptedMessages].reverse() : acceptedMessages; for (const message of display) list.appendChild(chatMessageElement(message, values, { animate: false })); chatStates.set(module.id, state); if (options.editor) schedulePreviewChat(module.id, state); else clearTimeout(state.previewTimer); return root; } function handleChatMessage(root, payload) { if (!payload?.id || !payload?.platform || (!payload?.text && !payload?.media?.length)) return 0; let accepted = 0; root?.querySelectorAll?.('.lumi-overlay-module[data-render-type="chat"]').forEach((box) => { const moduleId = box.dataset.moduleId; const state = chatStates.get(moduleId); if (!state?.list || !chatAccepts(state.values, payload) || state.messages.some((message) => message.id === payload.id)) return; state.messages.push(payload); const row = chatMessageElement(payload, state.values); if (state.values.newestAt === "top") state.list.prepend(row); else state.list.appendChild(row); while (state.messages.length > Number(state.values.maxMessages || 8)) removeChatMessage(moduleId, state.messages[0].id); scheduleChatRemoval(moduleId, payload, state.values); accepted += 1; }); return accepted; } 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) || root?.querySelector?.(`.lumi-overlay-managed-media[data-module-id="${CSS.escape(moduleId)}"]${selector === "video, audio" ? "" : selector}`) || null; } function buildModule(module, options = {}) { const renderType = module.renderType || module.type; const values = module.config || {}; if (renderType === "audio") return null; 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 (values.eventOnly && !options.editor) { box.hidden = true; box.dataset.eventOnly = "true"; } 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 === "chat") { box.dataset.renderType = "chat"; box.appendChild(buildChat(module, values, options)); } 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" && values.url) { const media = buildMedia(module, values, options, renderType); box.appendChild(media); } 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.tabIndex = -1; 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 layout = stageLayout(root, canvas, options.scale); const stage = document.createElement("div"); stage.className = "lumi-overlay-stage"; stage.style.width = `${layout.width}px`; stage.style.height = `${layout.height}px`; stage.style.transform = `scale(${layout.scaleX}, ${layout.scaleY})`; if (options.editor) { const editorScale = Math.max(0.01, Math.min(layout.scaleX, layout.scaleY)); const handleSize = window.matchMedia?.("(max-width: 700px)").matches ? 14 : 10; stage.style.setProperty("--lumi-editor-handle-size", `${handleSize / editorScale}px`); stage.style.setProperty("--lumi-editor-outline-size", `${2 / editorScale}px`); stage.style.setProperty("--lumi-editor-label-font-size", `${11.2 / editorScale}px`); stage.style.setProperty("--lumi-editor-label-max-width", `${192 / editorScale}px`); stage.style.setProperty("--lumi-editor-label-padding-y", `${3.2 / editorScale}px`); stage.style.setProperty("--lumi-editor-label-padding-x", `${6.4 / editorScale}px`); stage.style.setProperty("--lumi-editor-label-radius", `${4 / editorScale}px`); } const elements = new Map(); if (state?.enabled !== false && state?.scene) { for (const module of state.scene.modules || []) { if (module.enabled === false) continue; const renderType = module.renderType || module.type; if (renderType === "audio") { if (module.config?.url) { const media = buildMedia(module, module.config, options, "audio"); media.classList.add("lumi-overlay-managed-media"); stage.appendChild(media); } continue; } const element = buildModule(module, { ...options, canvas, scale: 1 }); if (!element) continue; elements.set(module.id, element); stage.appendChild(element); } } root.replaceChildren(stage); return elements; } function renderChatDock(root, module) { if (!root) return null; if (!module || (module.renderType || module.type) !== "chat") { root.replaceChildren(); return null; } const wrapper = document.createElement("section"); wrapper.className = "lumi-overlay-module lumi-overlay-chat-dock"; wrapper.dataset.moduleId = module.id; wrapper.dataset.moduleType = module.type; wrapper.dataset.renderType = "chat"; wrapper.appendChild(buildChat({ ...module, config: { ...(module.config || {}), inAnimation: "none", outAnimation: "none", animationDurationMs: 0 } }, { chatDock: true })); root.replaceChildren(wrapper); return wrapper; } function stageLayout(root, canvas, scaleOverride) { const width = Math.max(1, finite(canvas?.width, 1920)); const height = Math.max(1, finite(canvas?.height, 1080)); const override = finite(scaleOverride, 0); return { width, height, scaleX: override > 0 ? override : (root?.clientWidth ? root.clientWidth / width : 1), scaleY: override > 0 ? override : (root?.clientHeight ? root.clientHeight / height : 1) }; } window.LumiOverlayRenderer = { anchors, applyBox, buildModule, findSourceContent, handleChatMessage, render, renderChatDock, restartMediaElement, stageLayout }; })();