diff --git a/assets/runtime.js b/assets/runtime.js index 835659a..7b3aaa9 100644 --- a/assets/runtime.js +++ b/assets/runtime.js @@ -186,35 +186,45 @@ function buildPresenterHTML(slideData, styleSheets, total, startIdx, bodyClasses, htmlAttrs) { const slidesJSON = JSON.stringify(slideData); + // Build the srcdoc template for slide iframes. + // Each iframe gets its own viewport (1920x1080) so all CSS + // (vw/vh/clamp/percentage) resolves identically to the audience view. + // We use a placeholder %%SLIDE_HTML%% that gets replaced per-slide. + const iframeSrcdocTemplate = '' + + '' + + '' + + styleSheets.replace(/"/g, '"') + + '' + + '' + + '
%%SLIDE_HTML%%
' + + ''; + return '\n' -+ '\n\n\n' ++ '\n\n\n' + 'Presenter View\n' -+ styleSheets + '\n' + '\n' -+ '\n\n' ++ '\n\n' + '
\n' + '
\n' + '
CURRENT
\n' -+ '
\n' ++ '
\n' + '
\n' + '
\n' + '
\n' + '
NEXT
\n' -+ '
\n' ++ '
\n' + '
\n' + '
\n' + '
SPEAKER SCRIPT · 逐字稿
\n' @@ -257,10 +267,11 @@ + ' var total = ' + total + ';\n' + ' var idx = ' + startIdx + ';\n' + ' var CHANNEL_NAME = ' + JSON.stringify(CHANNEL_NAME) + ';\n' ++ ' var SRCDOC_TPL = ' + JSON.stringify(iframeSrcdocTemplate) + ';\n' + ' var bc; try { bc = new BroadcastChannel(CHANNEL_NAME); } catch(e) {}\n' + '\n' -+ ' var curInner = document.getElementById("pv-cur-inner");\n' -+ ' var nxtInner = document.getElementById("pv-nxt-inner");\n' ++ ' var iframeCur = document.getElementById("iframe-cur");\n' ++ ' var iframeNxt = document.getElementById("iframe-nxt");\n' + ' var pvNotes = document.getElementById("pv-notes");\n' + ' var pvCount = document.getElementById("pv-count");\n' + ' var pvTitle = document.getElementById("pv-title");\n' @@ -273,63 +284,58 @@ + ' pvTimer.textContent = String(Math.floor(s/60)).padStart(2,"0") + ":" + String(s%60).padStart(2,"0");\n' + ' }, 1000);\n' + '\n' -+ ' /* Compute scale to fit 1920x1080 into actual container size */\n' ++ ' /* Scale iframe (1920x1080) to fit its container */\n' + ' function fitScale(container) {\n' + ' var cw = container.clientWidth, ch = container.clientHeight;\n' + ' if (!cw || !ch) return 0.3;\n' + ' return Math.min(cw / 1920, ch / 1080);\n' + ' }\n' + '\n' -+ ' /* Render a slide inside a proper .deck wrapper at 1920x1080 */\n' -+ ' function renderSlide(container, html) {\n' -+ ' container.innerHTML = "
" + html + "
";\n' -+ ' var sl = container.querySelector(".slide");\n' -+ ' if (sl) { sl.classList.add("is-active"); }\n' ++ ' function setIframeSrcdoc(iframe, slideHTML) {\n' ++ ' var doc = SRCDOC_TPL.replace("%%SLIDE_HTML%%", slideHTML);\n' ++ ' iframe.srcdoc = doc;\n' + ' }\n' + '\n' ++ ' var endEl = null;\n' + ' function update(n) {\n' + ' n = Math.max(0, Math.min(total - 1, n));\n' + ' idx = n;\n' -+ ' /* Current slide */\n' -+ ' renderSlide(curInner, slideData[n].html);\n' ++ ' /* Current slide — render in iframe */\n' ++ ' setIframeSrcdoc(iframeCur, slideData[n].html);\n' + ' /* Next slide */\n' -+ ' var existingEnd = nxtInner.parentElement.querySelector(".pv-next-end");\n' + ' if (n + 1 < total) {\n' -+ ' if (existingEnd) existingEnd.remove();\n' -+ ' nxtInner.style.display = "";\n' -+ ' renderSlide(nxtInner, slideData[n + 1].html);\n' ++ ' iframeNxt.style.display = "";\n' ++ ' if (endEl) { endEl.remove(); endEl = null; }\n' ++ ' setIframeSrcdoc(iframeNxt, slideData[n + 1].html);\n' + ' } else {\n' -+ ' nxtInner.style.display = "none";\n' -+ ' if (!existingEnd) {\n' -+ ' var end = document.createElement("div");\n' -+ ' end.className = "pv-next-end";\n' -+ ' end.textContent = "— END —";\n' -+ ' nxtInner.parentElement.appendChild(end);\n' ++ ' iframeNxt.style.display = "none";\n' ++ ' if (!endEl) {\n' ++ ' endEl = document.createElement("div");\n' ++ ' endEl.className = "pv-next-end";\n' ++ ' endEl.textContent = "— END —";\n' ++ ' document.getElementById("pv-next").appendChild(endEl);\n' + ' }\n' + ' }\n' + ' /* Notes */\n' + ' pvNotes.innerHTML = slideData[n].notes || "(这一页还没有逐字稿)";\n' + ' pvCount.textContent = (n + 1) + " / " + total;\n' + ' pvTitle.textContent = slideData[n].title;\n' -+ ' /* Recompute scale */\n' + ' reScale();\n' + ' }\n' + '\n' + ' function reScale() {\n' + ' var cs = fitScale(document.getElementById("pv-current"));\n' -+ ' curInner.style.transform = "scale(" + cs + ")";\n' ++ ' iframeCur.style.transform = "scale(" + cs + ")";\n' + ' var ns = fitScale(document.getElementById("pv-next"));\n' -+ ' nxtInner.style.transform = "scale(" + ns + ")";\n' ++ ' iframeNxt.style.transform = "scale(" + ns + ")";\n' + ' }\n' + '\n' -+ ' /* Sync from audience window */\n' + ' if (bc) {\n' + ' bc.onmessage = function(e) {\n' + ' if (e.data && e.data.type === "go") update(e.data.idx);\n' + ' };\n' + ' }\n' + '\n' -+ ' /* Keyboard in presenter window */\n' + ' function go(n) {\n' + ' update(n);\n' + ' if (bc) bc.postMessage({ type: "go", idx: idx });\n' @@ -346,7 +352,6 @@ + ' });\n' + '\n' + ' window.addEventListener("resize", reScale);\n' -+ ' /* Initial render */\n' + ' setTimeout(function(){ update(idx); }, 100);\n' + '})();\n' + '\n'