221 lines
9.6 KiB
JavaScript
221 lines
9.6 KiB
JavaScript
(() => {
|
|
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 };
|
|
})();
|