From 06d6283ff7b23b2c589d20e54773057a7163bafd Mon Sep 17 00:00:00 2001 From: lewis Date: Fri, 17 Apr 2026 22:50:34 +0800 Subject: [PATCH] fix(presenter): use iframe srcdoc for pixel-perfect slide preview Root cause: CSS viewport units (vw/vh), clamp(), and percentage-based layouts resolve against the actual viewport, not a fixed-size div. DOM clones in a 1920x1080 container still render differently from the audience view because the iframe-less context has the wrong viewport. Fix: replace DOM clone approach with \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'