diff --git a/20251012/commitee-list/background.js b/20251012/commitee-list/background.js
new file mode 100644
index 0000000..9f35fe7
--- /dev/null
+++ b/20251012/commitee-list/background.js
@@ -0,0 +1,327 @@
+(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: 0,
+ topOffset: -200,
+ scalePercent: 125,
+ 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();
+})();
diff --git a/20251012/commitee-list/commitee.js b/20251012/commitee-list/commitee.js
new file mode 100644
index 0000000..3f90a73
--- /dev/null
+++ b/20251012/commitee-list/commitee.js
@@ -0,0 +1,86 @@
+// 🎯 筹委会主要名单(简体)
+const committeeData = [
+ {
+ role: "主席",
+ name: "黄祖全(小龙)",
+ subRole: "副主席",
+ subName: "周秉聪",
+ },
+ { role: "总务", name: "江德景", subRole: "副总务", subName: "林泓明" },
+ { role: "文书", name: "苏俊文", subRole: "副文书", subName: "谢进勇" },
+ { role: "财政", name: "陈祖平", subRole: "副财政", subName: "郑礼靖" },
+ { role: "查账", name: "黄杨顺" },
+ { role: "交通", name: "胡千鸿", subRole: "副交通", subName: "颜楷庆" },
+ { role: "交际", name: "黄伟汉", subRole: "副交际", subName: "陈俊宇" },
+];
+
+// 🎯 工委名单
+const members = [
+ "蓝皓贤",
+ "黄寿琪",
+ "赵汶德",
+ "Dinesh",
+ "李善业",
+ "郑杰瑞",
+ "颜俊宇",
+ "黄靖杰",
+ "潘俊轩",
+ "张祥明",
+ "黄升文",
+ "陈照明",
+ "赵汶德",
+ "陈世勇",
+ "王畯杰",
+ "黄伟宏",
+ "杜恩浩",
+ "林育森",
+ "陈仲旺",
+ "刘伟杰",
+ "刘伟鸿",
+ "林鸿旺",
+];
+
+const body = document.getElementById("committeeBody");
+const membersGrid = document.getElementById("committeeMembers");
+
+// 🧩 动态生成主要职位(四列)
+committeeData.forEach((item) => {
+ // 正职
+ const roleEl = document.createElement("div");
+ roleEl.className = "text-yellow-300 font-semibold text-right pr-2";
+ roleEl.textContent = item.role;
+ body.appendChild(roleEl);
+
+ const nameEl = document.createElement("div");
+ nameEl.className = "text-left";
+ nameEl.textContent = item.name;
+ body.appendChild(nameEl);
+
+ // 副职(若存在)
+ if (item.subRole && item.subName) {
+ const subRoleEl = document.createElement("div");
+ subRoleEl.className = "text-yellow-300 font-semibold text-right pr-2";
+ subRoleEl.textContent = item.subRole;
+ body.appendChild(subRoleEl);
+
+ const subNameEl = document.createElement("div");
+ subNameEl.className = "text-left";
+ subNameEl.textContent = item.subName;
+ body.appendChild(subNameEl);
+ } else {
+ // 没有副职则补空格,保持四列对齐
+ const empty1 = document.createElement("div");
+ empty1.textContent = "";
+ const empty2 = document.createElement("div");
+ empty2.textContent = "";
+ body.appendChild(empty1);
+ body.appendChild(empty2);
+ }
+});
+
+// 🧩 工委名单生成(六列)
+members.forEach((name) => {
+ const div = document.createElement("div");
+ div.textContent = name;
+ membersGrid.appendChild(div);
+});
diff --git a/20251012/sponsor-list/index.html b/20251012/commitee-list/index.html
similarity index 65%
rename from 20251012/sponsor-list/index.html
rename to 20251012/commitee-list/index.html
index c3b3d03..54cabe1 100644
--- a/20251012/sponsor-list/index.html
+++ b/20251012/commitee-list/index.html
@@ -5,6 +5,7 @@
Canvas 实时抠像(可控面板)
+
-
-
-
+
+
+
-
-
- 永平赵子龙庙
-
-
-
-
- 庆祝赵子龙元帅暨众神圣诞千秋
-
-
-
-
- 庆祝赵子龙元帅暨众神圣诞千秋
-
-
-
-
-
+
- 血染征袍透甲红
当阳谁敢与争锋
-
+ 永平赵子龙庙
+
-
+
- 古来冲锋扶危主
唯有常山赵子龙
-
+ 庆祝赵子龙元帅暨众神圣诞千秋
+
-
-
-
+
- 血染征袍透甲红
当阳谁敢与争锋
-
+ 庆祝赵子龙元帅暨众神圣诞千秋
+
-
+
- 古来冲锋扶危主
唯有常山赵子龙
-
+
+ 筹委会名单
+
+
+
+
+
+
+
diff --git a/20251012/sponsor-list/main.css b/20251012/commitee-list/main.css
similarity index 100%
rename from 20251012/sponsor-list/main.css
rename to 20251012/commitee-list/main.css
diff --git a/20251012/index.html b/20251012/index.html
new file mode 100644
index 0000000..1df7417
--- /dev/null
+++ b/20251012/index.html
@@ -0,0 +1,124 @@
+
+
+
+
+
+
Canvas 实时抠像(可控面板)
+
+
+
+
+
+
+
+
+
+

+
+
+
+ 永平赵子龙庙
+
+
+
+
+ 庆祝赵子龙元帅暨众神圣诞千秋
+
+
+
+
+ 庆祝赵子龙元帅暨众神圣诞千秋
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/20251012/main.css b/20251012/main.css
new file mode 100644
index 0000000..f1d8c73
--- /dev/null
+++ b/20251012/main.css
@@ -0,0 +1 @@
+@import "tailwindcss";
diff --git a/20251012/sponsor-list/background.js b/20251012/main/background.js
similarity index 100%
rename from 20251012/sponsor-list/background.js
rename to 20251012/main/background.js
diff --git a/20251012/main/index.html b/20251012/main/index.html
new file mode 100644
index 0000000..e1a550c
--- /dev/null
+++ b/20251012/main/index.html
@@ -0,0 +1,175 @@
+
+
+
+
+
+
Canvas 实时抠像(可控面板)
+
+
+
+
+
+
+
+
+
+
+ 永平赵子龙庙
+
+
+
+
+ 庆祝赵子龙元帅暨众神圣诞千秋
+
+
+
+
+ 庆祝赵子龙元帅暨众神圣诞千秋
+
+
+
+
+
+ 血染征袍透甲红
当阳谁敢与争锋
+
+
+
+ 古来冲锋扶危主
唯有常山赵子龙
+
+
+
+
+
+ 血染征袍透甲红
当阳谁敢与争锋
+
+
+
+ 古来冲锋扶危主
唯有常山赵子龙
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/20251012/main/main.css b/20251012/main/main.css
new file mode 100644
index 0000000..a041a74
--- /dev/null
+++ b/20251012/main/main.css
@@ -0,0 +1,3 @@
+@import "tailwindcss";
+
+
diff --git a/20251012/scripts/nav.js b/20251012/scripts/nav.js
new file mode 100644
index 0000000..54f99d3
--- /dev/null
+++ b/20251012/scripts/nav.js
@@ -0,0 +1,378 @@
+/* nav-20251012.js
+ 作者:ChatGPT(为你量身定制)
+ 功能:按键/触摸导航 —— 支持:入口 /main/ /commitee-list/ /speech/x.html
+ 将此文件在 /20251012/ 相关页面引入即可。
+*/
+
+(function () {
+ "use strict";
+
+ // ========== 配置 ==========
+ const CONFIG = {
+ // 如果没有在 URL 中检测到 8 位日期段,可以在这里硬编码入口(以 '/' 开头并以 '/' 结尾)
+ // 例如 '/20251012/'。若保留 null 会自动从 location.pathname 中寻找第一个 8 位数字段。
+ FORCE_BASE: null, // '/20251012/', or null to auto-detect
+
+ // 如果页面中没有可被自动发现的 speech/x.html 链接,设置一个最大页数(整数 > 0)
+ FALLBACK_SPEECH_COUNT: 3,
+
+ // 当在最后一页按 "下一页" 时的行为: 'wrap' | 'toRoot' | 'noop'
+ ON_FINAL_NEXT: "toRoot", // 推荐 'toRoot'(回到入口)或 'wrap'(回到第一篇)
+
+ // 是否使用 history.pushState 做平滑跳转(默认 true),设 false 则使用 location.href 真正跳转
+ USE_PUSHSTATE: true,
+
+ // 显示键位提示面板(true/false)
+ SHOW_HELP_OVERLAY: true,
+ };
+
+ // ========== 工具函数 ==========
+ function normalizePath(p) {
+ if (!p) return "/";
+ // ensure starts with /
+ if (!p.startsWith("/")) p = "/" + p;
+ // ensure trailing slash for directory-like paths, except for filenames like .html
+ if (!p.endsWith("/") && !p.match(/\.\w+$/)) p = p + "/";
+ return p;
+ }
+
+ function detectBaseFromPath() {
+ if (CONFIG.FORCE_BASE) return normalizePath(CONFIG.FORCE_BASE);
+ // find first /YYYYMMDD/ in pathname
+ const m = location.pathname.match(/\/(\d{8})(?:\/|$)/);
+ if (m) return `/${m[1]}/`;
+ // fallback: try first path segment if looks like a date
+ const segs = location.pathname.split("/").filter(Boolean);
+ if (segs.length && /^\d{8}$/.test(segs[0])) return `/${segs[0]}/`;
+ // last resort
+ return "/20251012/";
+ }
+
+ const BASE = detectBaseFromPath();
+ const MAIN = normalizePath(BASE + "main");
+ const COMMITEE = normalizePath(BASE + "commitee-list");
+ const SPEECH_PREFIX = normalizePath(BASE + "speech"); // like '/20251012/speech/'
+
+ function isRoot(pathname) {
+ const p = normalizePath(pathname);
+ return (
+ p === normalizePath(BASE) || p === normalizePath(BASE.replace(/\/$/, ""))
+ );
+ }
+ function isMain(pathname) {
+ return normalizePath(pathname) === MAIN;
+ }
+ function isCommitee(pathname) {
+ return normalizePath(pathname) === COMMITEE;
+ }
+ function isSpeech(pathname) {
+ // match /.../speech/x.html or /.../speech/x (with or without trailing slash)
+ const m = pathname.match(/\/speech\/(\d+)(?:\.html)?(?:\/|$)/);
+ return m ? parseInt(m[1], 10) : null;
+ }
+
+ // detect available speech pages by scanning
tags in the current document
+ function detectSpeechPagesFromLinks() {
+ const anchors = Array.from(document.querySelectorAll("a[href]"));
+ const nums = new Set();
+ const basePattern = new RegExp(
+ "^" + SPEECH_PREFIX.replace(/\/$/, "\\/") + "(\\d+)(?:\\.html)?(?:\\/?$)"
+ );
+ anchors.forEach((a) => {
+ try {
+ const href = a.getAttribute("href");
+ // convert relative -> absolute path (only pathname part)
+ const tmp = new URL(href, location.href);
+ const path = tmp.pathname;
+ const m = path.match(/\/speech\/(\d+)(?:\.html)?(?:\/|$)/);
+ if (m) nums.add(parseInt(m[1], 10));
+ } catch (e) {
+ // ignore
+ }
+ });
+ if (nums.size === 0) return null;
+ return Array.from(nums).sort((a, b) => a - b);
+ }
+
+ // Determine speech pages list
+ const detectedSpeechList = detectSpeechPagesFromLinks();
+ let SPEECH_LIST = null; // array of numbers like [1,2,3]
+ if (detectedSpeechList && detectedSpeechList.length > 0) {
+ SPEECH_LIST = detectedSpeechList;
+ } else {
+ // fallback to 1..FALLBACK_SPEECH_COUNT
+ const n = Math.max(1, Math.floor(CONFIG.FALLBACK_SPEECH_COUNT));
+ SPEECH_LIST = Array.from({ length: n }, (_, i) => i + 1);
+ }
+
+ function speechUrl(n) {
+ // return '/20251012/speech/1.html'
+ return `${SPEECH_PREFIX}${n}.html`;
+ }
+
+ function goTo(url, opts = {}) {
+ const usePush = CONFIG.USE_PUSHSTATE && opts.push !== false;
+ if (usePush) {
+ try {
+ history.pushState({}, "", url);
+ // optionally fetch and replace content (here we just reload to keep things simple)
+ // best practice: make this an SPA; for now do a full location.assign to ensure page loads.
+ location.assign(url);
+ } catch (e) {
+ location.href = url;
+ }
+ } else {
+ location.href = url;
+ }
+ }
+
+ // ========== Navigation logic ==========
+ function handleNext() {
+ const pathname = location.pathname;
+ const curSpeech = isSpeech(pathname);
+ if (isRoot(pathname)) {
+ // entry -> main
+ goTo(MAIN);
+ return;
+ }
+ if (isMain(pathname)) {
+ goTo(COMMITEE);
+ return;
+ }
+ if (isCommitee(pathname)) {
+ // go to first speech (if exists) else go to first fallback
+ const first = SPEECH_LIST[0];
+ goTo(speechUrl(first));
+ return;
+ }
+ if (curSpeech) {
+ // find index in SPEECH_LIST
+ const idx = SPEECH_LIST.indexOf(curSpeech);
+ if (idx >= 0 && idx < SPEECH_LIST.length - 1) {
+ goTo(speechUrl(SPEECH_LIST[idx + 1]));
+ } else {
+ // at final speech
+ if (CONFIG.ON_FINAL_NEXT === "wrap") {
+ goTo(speechUrl(SPEECH_LIST[0]));
+ } else if (CONFIG.ON_FINAL_NEXT === "toRoot") {
+ goTo(BASE);
+ } else {
+ // noop
+ }
+ }
+ return;
+ }
+ // default fallback: go to main
+ goTo(MAIN);
+ }
+
+ function handlePrev() {
+ const pathname = location.pathname;
+ const curSpeech = isSpeech(pathname);
+ if (isRoot(pathname)) {
+ // nothing to do
+ return;
+ }
+ if (isMain(pathname)) {
+ goTo(BASE);
+ return;
+ }
+ if (isCommitee(pathname)) {
+ goTo(MAIN);
+ return;
+ }
+ if (curSpeech) {
+ const idx = SPEECH_LIST.indexOf(curSpeech);
+ if (idx > 0) {
+ goTo(speechUrl(SPEECH_LIST[idx - 1]));
+ } else {
+ // first speech -> back to commitee-list
+ goTo(COMMITEE);
+ }
+ return;
+ }
+ // default fallback: go to base
+ goTo(BASE);
+ }
+
+ // ========== Key & Touch handling ==========
+ function onKey(e) {
+ // Normalize some keys:
+ // Enter -> Next
+ // ArrowRight / '>' / '.' -> Next
+ // ArrowLeft / '<' / ',' -> Prev
+ const k = e.key;
+ if (k === "Enter") {
+ e.preventDefault();
+ handleNext();
+ return;
+ }
+ if (k === "ArrowRight" || k === "Right") {
+ e.preventDefault();
+ handleNext();
+ return;
+ }
+ if (k === "ArrowLeft" || k === "Left") {
+ e.preventDefault();
+ handlePrev();
+ return;
+ }
+ // characters '>' and '<' (depending on keyboard)
+ if (k === ">" || k === ".") {
+ e.preventDefault();
+ handleNext();
+ return;
+ }
+ if (k === "<" || k === ",") {
+ e.preventDefault();
+ handlePrev();
+ return;
+ }
+
+ // optional: 'h' toggles help overlay
+ if (k === "h" || k === "H") {
+ toggleHelpOverlay();
+ }
+ }
+
+ // Basic touch swipe (horizontal)
+ let touchStartX = null;
+ function onTouchStart(e) {
+ if (e.touches && e.touches.length === 1) {
+ touchStartX = e.touches[0].clientX;
+ }
+ }
+ function onTouchEnd(e) {
+ if (!touchStartX) return;
+ const endX = (e.changedTouches && e.changedTouches[0].clientX) || null;
+ if (endX === null) {
+ touchStartX = null;
+ return;
+ }
+ const dx = endX - touchStartX;
+ const threshold = 50; // pixels
+ if (dx > threshold) {
+ // swipe right -> Prev
+ handlePrev();
+ } else if (dx < -threshold) {
+ // swipe left -> Next
+ handleNext();
+ }
+ touchStartX = null;
+ }
+
+ // ========== UI overlay/help ==========
+ let overlayEl = null;
+ function createHelpOverlay() {
+ if (!CONFIG.SHOW_HELP_OVERLAY) return;
+ overlayEl = document.createElement("div");
+ overlayEl.id = "nav-help-overlay";
+ overlayEl.style.position = "fixed";
+ overlayEl.style.right = "16px";
+ overlayEl.style.bottom = "16px";
+ overlayEl.style.zIndex = 9999;
+ overlayEl.style.padding = "10px 12px";
+ overlayEl.style.borderRadius = "8px";
+ overlayEl.style.boxShadow = "0 6px 20px rgba(0,0,0,0.2)";
+ overlayEl.style.background = "rgba(255,255,255,0.95)";
+ overlayEl.style.fontFamily =
+ 'system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue"';
+ overlayEl.style.fontSize = "13px";
+ overlayEl.style.color = "#111";
+ overlayEl.style.border = "1px solid rgba(0,0,0,0.06)";
+ overlayEl.style.maxWidth = "260px";
+ overlayEl.style.lineHeight = "1.3";
+ overlayEl.innerHTML = `
+ 导航提示
+ Enter / → / '>' : 下一页
+ ← / '<' : 上一页
+ H : 显示/隐藏本提示
+ 支持触摸左右滑动(移动端)
+ `;
+ overlayEl.style.cursor = "pointer";
+ overlayEl.title = "点击隐藏/显示";
+ overlayEl.addEventListener("click", toggleHelpOverlay);
+ document.body.appendChild(overlayEl);
+ }
+
+ function toggleHelpOverlay() {
+ if (!overlayEl) {
+ createHelpOverlay();
+ return;
+ }
+ if (overlayEl.style.display === "none") overlayEl.style.display = "";
+ else overlayEl.style.display = "none";
+ }
+
+ // ========== Init ==========
+ function init() {
+ // Attach handlers
+ window.addEventListener("keydown", onKey, { passive: false });
+ window.addEventListener("touchstart", onTouchStart, { passive: true });
+ window.addEventListener("touchend", onTouchEnd, { passive: true });
+
+ // pushState popstate handling: when user clicks back/forward, allow normal navigation (no extra handling needed)
+ window.addEventListener("popstate", () => {
+ // no-op: browser handles location; you could re-run UI update hooks here if needed
+ });
+
+ // create help overlay
+ if (CONFIG.SHOW_HELP_OVERLAY) createHelpOverlay();
+
+ // Add small visual helper arrows (optional)
+ addOnScreenArrows();
+ }
+
+ function addOnScreenArrows() {
+ // minimal left/right arrow buttons for mouse users
+ const left = document.createElement("button");
+ left.textContent = "←";
+ left.setAttribute("aria-label", "上一页");
+ styleTinyArrow(left, "left");
+ left.addEventListener("click", handlePrev);
+
+ const right = document.createElement("button");
+ right.textContent = "→";
+ right.setAttribute("aria-label", "下一页");
+ styleTinyArrow(right, "right");
+ right.addEventListener("click", handleNext);
+
+ document.body.appendChild(left);
+ document.body.appendChild(right);
+ }
+
+ function styleTinyArrow(btn, which) {
+ btn.style.position = "fixed";
+ btn.style.zIndex = 9998;
+ btn.style.top = "50%";
+ btn.style.transform = "translateY(-50%)";
+ btn.style[which === "left" ? "left" : "right"] = "8px";
+ btn.style.width = "40px";
+ btn.style.height = "40px";
+ btn.style.borderRadius = "6px";
+ btn.style.border = "1px solid rgba(0,0,0,0.08)";
+ btn.style.background = "rgba(255,255,255,0.9)";
+ btn.style.boxShadow = "0 6px 20px rgba(0,0,0,0.08)";
+ btn.style.cursor = "pointer";
+ btn.style.fontSize = "18px";
+ btn.style.lineHeight = "36px";
+ btn.style.padding = "0";
+ btn.style.display = "flex";
+ btn.style.alignItems = "center";
+ btn.style.justifyContent = "center";
+ }
+
+ // run
+ init();
+
+ // expose for debugging
+ window._eventNav = {
+ BASE,
+ MAIN,
+ COMMITEE,
+ SPEECH_PREFIX,
+ SPEECH_LIST,
+ handleNext,
+ handlePrev,
+ };
+})();
diff --git a/20251012/speech/1.html b/20251012/speech/1.html
new file mode 100644
index 0000000..ce3ceed
--- /dev/null
+++ b/20251012/speech/1.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+ 筹委会主席
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/20251012/speech/2.html b/20251012/speech/2.html
new file mode 100644
index 0000000..0371154
--- /dev/null
+++ b/20251012/speech/2.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+ 林添顺
+
+
+
+
+
+
+
\ No newline at end of file