(function () { // --- DOM & 元素 --- const video = document.getElementById("video"); const mainCanvas = document.getElementById("mainCanvas"); const ctxMain = mainCanvas.getContext("2d", { alpha: true }); // temp canvas 用于视频帧处理 const tempCanvas = document.createElement("canvas"); const ctxTemp = tempCanvas.getContext("2d"); // 控件 const controlsPanel = document.getElementById("controlsPanel"); const closePanel = document.getElementById("closePanel"); const thresholdInput = document.getElementById("threshold"); const softnessInput = document.getElementById("softness"); const leftMarginInput = document.getElementById("leftMargin"); const topOffsetInput = document.getElementById("topOffset"); const scaleInput = document.getElementById("scale"); const rotationInput = document.getElementById("rotation"); const opacityInput = document.getElementById("opacity"); // labels const thVal = document.getElementById("thVal"); const sfVal = document.getElementById("sfVal"); const marginVal = document.getElementById("marginVal"); const topVal = document.getElementById("topVal"); const scaleVal = document.getElementById("scaleVal"); const rotVal = document.getElementById("rotVal"); const opaVal = document.getElementById("opaVal"); // reset / help const resetBtn = document.getElementById("resetBtn"); const applyHint = document.getElementById("applyHint"); // 默认参数 const defaults = { threshold: 30, softness: 10, // leftMargin: 0, // topOffset: -200, // scalePercent: 125 leftMargin: 650, topOffset: 0, scalePercent: 100, rotationDeg: 0, opacityPercent: 100, }; // 当前参数(init to defaults) let THRESHOLD = defaults.threshold; let SOFTNESS = defaults.softness; let LEFT_MARGIN = defaults.leftMargin; let TOP_OFFSET = defaults.topOffset; let SCALE = defaults.scalePercent / 100; let ROTATION = defaults.rotationDeg; let OPACITY = defaults.opacityPercent / 100; // 工具函数:同步控制值到 UI function syncUI() { thresholdInput.value = THRESHOLD; softnessInput.value = SOFTNESS; leftMarginInput.value = LEFT_MARGIN; topOffsetInput.value = TOP_OFFSET; scaleInput.value = Math.round(SCALE * 100); rotationInput.value = ROTATION; opacityInput.value = Math.round(OPACITY * 100); thVal.textContent = THRESHOLD; sfVal.textContent = SOFTNESS; marginVal.textContent = LEFT_MARGIN; topVal.textContent = TOP_OFFSET; scaleVal.textContent = Math.round(SCALE * 100); rotVal.textContent = ROTATION; opaVal.textContent = Math.round(OPACITY * 100); } // 初始化 UI syncUI(); // 事件监听:控件变化 thresholdInput.addEventListener("input", (e) => { THRESHOLD = +e.target.value; thVal.textContent = THRESHOLD; }); softnessInput.addEventListener("input", (e) => { SOFTNESS = +e.target.value; sfVal.textContent = SOFTNESS; }); leftMarginInput.addEventListener("input", (e) => { LEFT_MARGIN = +e.target.value; marginVal.textContent = LEFT_MARGIN; }); topOffsetInput.addEventListener("input", (e) => { TOP_OFFSET = +e.target.value; topVal.textContent = TOP_OFFSET; }); scaleInput.addEventListener("input", (e) => { SCALE = +e.target.value / 100; scaleVal.textContent = Math.round(SCALE * 100); }); rotationInput.addEventListener("input", (e) => { ROTATION = +e.target.value; rotVal.textContent = ROTATION; }); opacityInput.addEventListener("input", (e) => { OPACITY = +e.target.value / 100; opaVal.textContent = Math.round(OPACITY * 100); }); // 重置 function resetParams() { THRESHOLD = defaults.threshold; SOFTNESS = defaults.softness; LEFT_MARGIN = defaults.leftMargin; TOP_OFFSET = defaults.topOffset; SCALE = defaults.scalePercent / 100; ROTATION = defaults.rotationDeg; OPACITY = defaults.opacityPercent / 100; syncUI(); } resetBtn.addEventListener("click", resetParams); document.addEventListener("keydown", (e) => { if (e.key.toLowerCase() === "r") { resetParams(); } if (e.key.toLowerCase() === "c") { toggleControls(); } }); applyHint.addEventListener("click", () => { alert( "提示:\n• 若画面卡顿,请降低“缩放”或在代码中把 tempCanvas 缩小(注释内说明)。\n• 跨域资源需要 CORS。" ); }); // 控件开关(可关闭/打开) function hideControls() { controlsPanel.style.display = "none"; } function showControls() { controlsPanel.style.display = "block"; } function toggleControls() { if ( controlsPanel.style.display === "none" || getComputedStyle(controlsPanel).display === "none" ) showControls(); else hideControls(); } closePanel.addEventListener("click", hideControls); // 载入背景图(使用背景图做最终合成;如果你希望使用 CSS 背景而不是图片文件,可改这里) const bgImg = new Image(); bgImg.src = "../assets/background.png"; bgImg.onload = () => { // 等背景加载后设置主 canvas 大小 const w = bgImg.naturalWidth || 1080; const h = bgImg.naturalHeight || Math.floor((w * 9) / 16); mainCanvas.width = w; mainCanvas.height = h; mainCanvas.style.width = "100%"; mainCanvas.style.height = "auto"; // tempCanvas 尺寸以视频为准(或缩放以提高性能) const vW = video.videoWidth || 720; const vH = video.videoHeight || 1280; // 性能提示:如果你的原始视频非常大,建议把 tempCanvas 设为更小的尺寸(比如 /2 或 /3) tempCanvas.width = vW; tempCanvas.height = vH; // 如果 video 元数据还没准备好,等待 loadedmetadata 事件 if (video.readyState < 1) { video.addEventListener( "loadedmetadata", () => { requestAnimationFrame(loop); }, { once: true } ); } else { requestAnimationFrame(loop); } }; bgImg.onerror = () => { console.error("背景图片加载失败,请检查 background.png 路径与 CORS 设置。"); // 给 canvas 一个安全的大小以便继续 mainCanvas.width = 960; mainCanvas.height = 540; tempCanvas.width = 720; tempCanvas.height = 1280; requestAnimationFrame(loop); }; // 主渲染循环 let lastTime = performance.now(); function loop(now) { // 绘制背景 ctxMain.clearRect(0, 0, mainCanvas.width, mainCanvas.height); if (bgImg.complete && bgImg.naturalWidth) { ctxMain.drawImage(bgImg, 0, 0, mainCanvas.width, mainCanvas.height); } else { // fallback background ctxMain.fillStyle = "#222"; ctxMain.fillRect(0, 0, mainCanvas.width, mainCanvas.height); } // 只有当视频可用时继续 if (!video.paused && !video.ended && video.readyState >= 2) { // 将视频帧绘制到 tempCanvas(此处使用视频自身分辨率) ctxTemp.clearRect(0, 0, tempCanvas.width, tempCanvas.height); ctxTemp.drawImage(video, 0, 0, tempCanvas.width, tempCanvas.height); // 处理像素(抠像) let processed = false; try { const img = ctxTemp.getImageData( 0, 0, tempCanvas.width, tempCanvas.height ); const data = img.data; const len = data.length; const threshold = THRESHOLD; const softness = Math.max(1, SOFTNESS); // // 基于平均亮度的简单抠像:黑色 -> 透明 // for (let i = 0; i < len; i += 4) { // const r = data[i], // g = data[i + 1], // b = data[i + 2]; // const lum = (r + g + b) / 3; // if (lum < threshold) { // data[i + 3] = 0; // } else if (lum < threshold + softness) { // const t = (lum - threshold) / softness; // data[i + 3] = Math.round(255 * t); // } // else keep alpha // } // 基于绿色背景的抠像(Green Screen / Chroma Key) for (let i = 0; i < len; i += 4) { const r = data[i]; const g = data[i + 1]; const b = data[i + 2]; // “绿色程度”与“非绿色程度”的差距 const greenDiff = g - Math.max(r, b); // 以 threshold / softness 控制去除强度和平滑边缘 if (greenDiff > threshold) { data[i + 3] = 0; // 透明 } else if (greenDiff > threshold - softness) { const t = (threshold - greenDiff) / softness; data[i + 3] = Math.round(255 * t); } // 其他保持原样 } ctxTemp.putImageData(img, 0, 0); processed = true; } catch (err) { // 可能因为跨域导致 SecurityError — 降级处理:不处理像素,直接绘制视频帧(但看不到透明) if (err && err.name === "SecurityError") { // 记录一次性警告到控制台 if (!window._crossOriginWarned) { console.warn( '无法读取像素(可能为跨域资源),请确认 video/background 的 CORS 设置(use crossorigin="anonymous" + Access-Control-Allow-Origin)。' ); window._crossOriginWarned = true; } processed = false; } else { console.error("处理像素时发生错误:", err); } } // 把(处理后或原始的)tempCanvas 绘制到主 canvas // 计算目标大小(以主 canvas 的高度比例为基准) const desiredHeightRatio = 0.9; // 视频占主画布高度的比例(可改) const destH = Math.floor(mainCanvas.height * desiredHeightRatio); const baseScale = destH / tempCanvas.height; const destW = Math.floor(tempCanvas.width * baseScale); const destX = LEFT_MARGIN; const destY = Math.floor((mainCanvas.height - destH) / 2) + TOP_OFFSET; // 应用用户控制的缩放、旋转、不透明度 const scaledW = destW * SCALE; const scaledH = destH * SCALE; const centerX = destX + scaledW / 2; const centerY = destY + scaledH / 2; const rotRad = (ROTATION * Math.PI) / 180; ctxMain.save(); ctxMain.globalAlpha = OPACITY; // translate -> rotate -> draw centered ctxMain.translate(centerX, centerY); ctxMain.rotate(rotRad); // drawImage 参数:绘制 tempCanvas 的内容到以中心为原点的位置 ctxMain.drawImage( tempCanvas, -scaledW / 2, -scaledH / 2, scaledW, scaledH ); ctxMain.restore(); // 如果你想在无法处理像素(跨域)时仍然保持 alpha,必须确保视频资源启用 CORS 和服务器返回允许头 // 可在这里根据 processed 变量显示调试信息(未开启) } // 下一帧 requestAnimationFrame(loop); } // 自动播放策略:尝试播放视频(静音大多数浏览器允许) video.play().catch(() => { /* 静音应该可以自动播放,否则需用户交互触发 */ }); // 初始 UI 状态 syncUI(); })();