第一屏 · 出门前

出门前

出门前先确认电子票、节目单和随身物品,把观演礼仪从这一刻放进口袋。

第二屏 · 检票入场

检票入场

提前打开票码,按座位入场;手机调至静音,拍摄和闪光灯规则在入座前确认。

第三屏 · 戏文开场

楚帐定景

幕布拉开,一桌二椅与楚帐旗定住舞台。项羽困于垓下,虞姬登场,舞剑高潮的情绪从此开始蓄势。

第四屏 · 四面楚歌

四面楚歌

楚歌从四面压来,项羽惊疑,虞姬已明白大势难回。观演时看两人的停顿和眼神,悲剧不是突然发生,而是一层层逼近。

第五屏 · 虞姬劝酒

虞姬劝酒

虞姬劝霸王饮酒,把不舍藏进唱腔和身段。酒盏到位后,舞剑不再只是表演,而是她给霸王的最后安慰。

第六屏 · 舞剑高潮

舞剑高潮

剑光、水袖与身段在同一处舞台中流转。虞姬以舞剑诀别,动作越利落,情绪越沉重,这是全段戏文的高潮。

第七屏 · 幕布落下

幕布落下

舞剑之后,舞台归于静默。谢幕时把掌声留给台前幕后,也把虞姬这一段悲壮的身段留在心里。

', ` \n ${bootstrap}`); const blob = new Blob([html], { type: 'text/html;charset=utf-8' }); const url = URL.createObjectURL(blob); exportResult.classList.add('show'); exportResult.innerHTML = ''; const manualLink = document.createElement('a'); manualLink.href = url; manualLink.download = '京剧观演指南H5-导出版.html'; manualLink.textContent = '点击下载导出版 HTML'; exportResult.append('已生成:', manualLink, '。如果没有自动下载,请点这个链接。'); const link = document.createElement('a'); link.href = url; link.download = '京剧观演指南H5-导出版.html'; document.body.appendChild(link); link.click(); link.remove(); }; exportHtmlButton.addEventListener('click', exportStandaloneHtml); musicToggle.addEventListener('click', (event) => { event.preventDefault(); event.stopPropagation(); if (!audioConfig.bgMusic) { exportResult?.classList.add('show'); if (exportResult) exportResult.textContent = '还没有上传背景音乐。'; return; } if (bgAudio.paused) { bgAudio.play() .then(() => setMusicPlaying(true)) .catch(() => setMusicPlaying(false)); } else { bgAudio.pause(); setMusicPlaying(false); } }); bgAudio.addEventListener('pause', () => setMusicPlaying(false)); bgAudio.addEventListener('play', () => setMusicPlaying(true)); jumpTopButton.addEventListener('click', (event) => { event.preventDefault(); event.stopPropagation(); goScene(0); }); jumpBottomButton.addEventListener('click', (event) => { event.preventDefault(); event.stopPropagation(); goScene(chapters.length - 1); }); collapsePanelButton.addEventListener('click', (event) => { event.stopPropagation(); editPanel.classList.toggle('collapsed'); collapsePanelButton.textContent = editPanel.classList.contains('collapsed') ? '+' : '-'; }); panelHead.addEventListener('pointerdown', (event) => { if (event.target === collapsePanelButton) return; draggingPanel = true; const rect = editPanel.getBoundingClientRect(); panelDragOffsetX = event.clientX - rect.left; panelDragOffsetY = event.clientY - rect.top; panelHead.setPointerCapture(event.pointerId); }); panelHead.addEventListener('pointermove', (event) => { if (!draggingPanel) return; const maxX = window.innerWidth - editPanel.offsetWidth - 8; const maxY = window.innerHeight - editPanel.offsetHeight - 8; const x = clamp(event.clientX - panelDragOffsetX, 8, Math.max(8, maxX)); const y = clamp(event.clientY - panelDragOffsetY, 8, Math.max(8, maxY)); editPanel.style.setProperty('--panel-left', px(x)); editPanel.style.setProperty('--panel-top', px(y)); }); panelHead.addEventListener('pointerup', () => { draggingPanel = false; }); panelHead.addEventListener('pointercancel', () => { draggingPanel = false; }); document.addEventListener('pointerdown', (event) => { if (!document.body.classList.contains('layer-mode')) return; if (event.target.closest('.edit-panel, #musicToggle, .jump-controls')) return; const layer = event.target.closest('.layer, .scene-person, .text-editable'); if (!layer || !layer.classList.contains('editable')) return; event.preventDefault(); event.stopPropagation(); selectLayer(layer); const cfg = getLayerConfig(layer); draggingLayer = layer; layerDragStartX = event.clientX; layerDragStartY = event.clientY; layerDragBaseX = Number(cfg.x || 0); layerDragBaseY = Number(cfg.y || 0); layer.setPointerCapture?.(event.pointerId); }, true); document.addEventListener('pointermove', (event) => { if (!draggingLayer) return; event.preventDefault(); event.stopPropagation(); const nextX = Math.round(layerDragBaseX + event.clientX - layerDragStartX); const nextY = Math.round(layerDragBaseY + event.clientY - layerDragStartY); objectConfig[draggingLayer.dataset.id] = { ...getLayerConfig(draggingLayer), x: nextX, y: nextY }; saveConfig(); applyLayerConfig(draggingLayer); syncEditor(draggingLayer); requestRender(); }, true); const stopLayerDrag = () => { draggingLayer = null; }; document.addEventListener('pointerup', stopLayerDrag, true); document.addEventListener('pointercancel', stopLayerDrag, true); document.addEventListener('click', (event) => { if (!document.body.classList.contains('layer-mode')) return; const layer = event.target.closest('.editable'); if (!layer) return; event.preventDefault(); event.stopPropagation(); selectLayer(layer); }, true); document.addEventListener('click', (event) => { if (event.target.closest('#musicToggle, .jump-controls, .edit-panel')) return; playObjectSound(event.target); }); document.addEventListener('keydown', (event) => { const toggle = (event.ctrlKey || event.metaKey) && event.shiftKey && event.key.toLowerCase() === 'l'; if (toggle) { event.preventDefault(); if (viewerOnly) return; document.body.classList.toggle('layer-mode'); if (!document.body.classList.contains('layer-mode')) selectLayer(null); return; } if (!document.body.classList.contains('layer-mode')) return; if (event.key === ']') { event.preventDefault(); adjustLayer(1); } if (event.key === '[') { event.preventDefault(); adjustLayer(-1); } if (event.key === 'Escape') { event.preventDefault(); document.body.classList.remove('layer-mode'); selectLayer(null); } }); setViewport(); applyBackgroundAudio(); refreshObjectSelect(); chapters[0].classList.add('is-active'); render(); addEventListener('resize', () => { setViewport(); requestRender(); }, { passive: true }); addEventListener('wheel', (event) => { event.preventDefault(); advance(event.deltaY); }, { passive: false }); addEventListener('touchstart', (event) => { touchStartY = event.touches[0].clientY; }, { passive: false }); addEventListener('touchmove', (event) => { event.preventDefault(); const y = event.touches[0].clientY; advance((touchStartY - y) * .85); touchStartY = y; }, { passive: false }); addEventListener('pointerdown', (event) => { if (document.body.classList.contains('layer-mode')) return; if (event.target.closest?.('.edit-panel, #musicToggle, .jump-controls')) return; pointerScrolling = true; pointerStartY = event.clientY; }, { passive: false }); addEventListener('pointermove', (event) => { if (!pointerScrolling) return; if (document.body.classList.contains('layer-mode')) return; event.preventDefault(); const delta = (pointerStartY - event.clientY) * 1.15; advance(delta); pointerStartY = event.clientY; }, { passive: false }); addEventListener('pointerup', () => { pointerScrolling = false; }, { passive: true }); addEventListener('pointercancel', () => { pointerScrolling = false; }, { passive: true }); addEventListener('keydown', (event) => { if (document.body.classList.contains('layer-mode')) return; if (event.key === 'ArrowDown' || event.key === 'PageDown' || event.key === ' ') { event.preventDefault(); advance(85); } if (event.key === 'ArrowUp' || event.key === 'PageUp') { event.preventDefault(); advance(-85); } });