From 75fcaf44678ed11f5e81e0f99d2c072eadb29533 Mon Sep 17 00:00:00 2001 From: zhizhi200271 Date: Tue, 10 Mar 2026 17:34:43 +0800 Subject: [PATCH] =?UTF-8?q?DEV-004:=20M17=E7=8E=AF=E8=8A=826-=E5=9C=A8?= =?UTF-8?q?=E7=BA=BF=E9=A2=84=E8=A7=88=E4=B8=8E=E5=88=86=E4=BA=AB=C2=B7?= =?UTF-8?q?=E9=A2=84=E8=A7=88=E6=A8=A1=E5=BC=8F+Data=20URL=E7=94=9F?= =?UTF-8?q?=E6=88=90+=E5=B5=8C=E5=85=A5=E4=BB=A3=E7=A0=81+=E5=85=A8?= =?UTF-8?q?=E5=B1=8F=E6=92=AD=E6=94=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dynamic-comic-studio/css/style.css | 543 ++++++------ dynamic-comic-studio/index.html | 154 ++-- dynamic-comic-studio/js/app.js | 985 ++++++++++----------- dynamic-comic-studio/js/app.js.backup | 1113 ++++++++++++++++++++++++ dynamic-comic-studio/js/app.js.backup2 | 1029 ++++++++++++++++++++++ dynamic-comic-studio/js/app.js.broken | 1113 ++++++++++++++++++++++++ dynamic-comic-studio/js/app.js.save | 584 +++++++++++++ 7 files changed, 4684 insertions(+), 837 deletions(-) create mode 100644 dynamic-comic-studio/js/app.js.backup create mode 100644 dynamic-comic-studio/js/app.js.backup2 create mode 100644 dynamic-comic-studio/js/app.js.broken create mode 100644 dynamic-comic-studio/js/app.js.save diff --git a/dynamic-comic-studio/css/style.css b/dynamic-comic-studio/css/style.css index 3243d659..b82de1d3 100644 --- a/dynamic-comic-studio/css/style.css +++ b/dynamic-comic-studio/css/style.css @@ -1,348 +1,379 @@ -/* ==================== 基础重置 ==================== */ +/* === 全局重置 === */ * { - margin: 0; - padding: 0; - box-sizing: border-box; + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: 'Segoe UI', 'PingFang SC', Roboto, system-ui, sans-serif; } body { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', sans-serif; - background: #1a1a2e; - color: #eee; - overflow: hidden; + background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); + min-height: 100vh; + padding: 20px; + color: #fff; } -/* ==================== 布局框架 ==================== */ -#app { - display: flex; - flex-direction: column; - height: 100vh; +/* === 主容器 === */ +.container { + max-width: 1400px; + margin: 0 auto; + background: rgba(255, 255, 255, 0.05); + backdrop-filter: blur(10px); + border-radius: 24px; + padding: 24px; + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4); + border: 1px solid rgba(255, 255, 255, 0.1); } -/* ==================== 顶部工具栏 ==================== */ -#toolbar { - display: flex; - justify-content: space-between; - align-items: center; - padding: 10px 20px; - background: #16213e; - border-bottom: 2px solid #0f3460; +/* === 标题区域 === */ +h1 { + font-size: 2.5em; + margin-bottom: 24px; + background: linear-gradient(45deg, #fff, #e94560); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + text-shadow: 0 0 20px rgba(233, 69, 96, 0.3); + letter-spacing: 2px; } -#toolbar h1 { - font-size: 18px; - color: #e94560; +/* === 两列布局 === */ +.main-layout { + display: grid; + grid-template-columns: 280px 1fr 300px; + gap: 20px; + margin-top: 20px; } -.toolbar-actions button { - margin-left: 10px; - padding: 6px 12px; - background: #0f3460; - color: #fff; - border: none; - border-radius: 4px; - cursor: pointer; - transition: background 0.3s; +/* === 左侧面板通用样式 === */ +.left-panel, .right-panel { + background: rgba(10, 10, 20, 0.6); + border-radius: 20px; + padding: 20px; + border: 1px solid rgba(255, 255, 255, 0.05); + backdrop-filter: blur(5px); } -.toolbar-actions button:hover { - background: #e94560; +/* === 素材分类标题 === */ +.left-panel h3 { + color: #e94560; + margin: 16px 0 12px 0; + font-size: 1.2em; + letter-spacing: 1px; + border-left: 4px solid #e94560; + padding-left: 12px; } -/* ==================== 主工作区 ==================== */ -#workspace { - display: flex; - flex: 1; - overflow: hidden; +.left-panel h3:first-of-type { + margin-top: 0; } -/* ==================== 左侧素材面板 ==================== */ -#assets-panel { - width: 200px; - background: #16213e; - border-right: 2px solid #0f3460; - padding: 15px; - overflow-y: auto; +/* === 素材网格 === */ +.material-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 10px; + margin-bottom: 10px; } -#assets-panel h3 { - color: #e94560; - margin-bottom: 15px; - font-size: 16px; +/* === 素材项 === */ +.material-item { + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 12px; + padding: 10px; + text-align: center; + cursor: pointer; + transition: all 0.2s ease; + color: #fff; + font-size: 0.9em; } -.asset-category { - margin-bottom: 20px; +.material-item:hover { + background: rgba(233, 69, 96, 0.2); + border-color: #e94560; + transform: translateY(-2px); } -.asset-category h4 { - color: #533483; - font-size: 14px; - margin-bottom: 10px; - padding-left: 5px; - border-left: 3px solid #e94560; +/* === 中间画布区域 === */ +.canvas-area { + background: rgba(0, 0, 0, 0.3); + border-radius: 20px; + padding: 20px; + display: flex; + flex-direction: column; + align-items: center; + border: 1px solid rgba(255, 255, 255, 0.05); } -.asset-items { - display: flex; - flex-wrap: wrap; - gap: 8px; +/* === 画布 === */ +#mainCanvas { + width: 100%; + max-width: 800px; + aspect-ratio: 16/9; + background: #0a0a14; + border-radius: 16px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); + border: 2px solid rgba(233, 69, 96, 0.3); + cursor: crosshair; } -.asset-item { - padding: 8px 12px; - background: #0f3460; - border-radius: 6px; - cursor: move; - font-size: 13px; - transition: all 0.3s; - user-select: none; +/* === 画布控制栏 === */ +.canvas-controls { + margin-top: 16px; + display: flex; + gap: 12px; + flex-wrap: wrap; + justify-content: center; } -.asset-item:hover { - background: #e94560; - transform: translateY(-2px); +/* === 按钮样式 === */ +.btn { + background: linear-gradient(45deg, #2a1a3a, #1a1a2e); + border: 1px solid #533483; + color: #fff; + padding: 10px 24px; + border-radius: 30px; + font-size: 0.95em; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3); + letter-spacing: 0.5px; } -/* ==================== 画布区域 ==================== */ -#canvas-container { - flex: 1; - display: flex; - flex-direction: column; - background: #0f0f1a; +.btn:hover { + background: linear-gradient(45deg, #3a2a4a, #2a1a3a); + border-color: #e94560; + transform: translateY(-2px); + box-shadow: 0 6px 15px rgba(233, 69, 96, 0.3); } -#scene-tabs { - display: flex; - align-items: center; - padding: 10px; - background: #16213e; - border-bottom: 2px solid #0f3460; +.btn-primary { + background: linear-gradient(45deg, #e94560, #533483); + border-color: #e94560; + font-weight: 600; } -#tabs-list { - display: flex; - gap: 8px; - flex: 1; +.btn-primary:hover { + background: linear-gradient(45deg, #ff5570, #634393); + box-shadow: 0 6px 20px rgba(233, 69, 96, 0.5); } -.scene-tab { - padding: 8px 16px; - background: #0f3460; - border-radius: 4px 4px 0 0; - cursor: pointer; - font-size: 14px; - position: relative; +/* === 右侧面板 === */ +.right-panel { + display: flex; + flex-direction: column; + gap: 20px; } -.scene-tab.active { - background: #e94560; +/* === 场景列表 === */ +.scene-list { + background: rgba(0, 0, 0, 0.2); + border-radius: 12px; + padding: 12px; + min-height: 120px; + border: 1px solid rgba(255, 255, 255, 0.05); } -.scene-tab .close-btn { - margin-left: 8px; - opacity: 0.6; +.scene-item { + background: rgba(233, 69, 96, 0.1); + border: 1px solid #533483; + border-radius: 8px; + padding: 8px 12px; + margin-bottom: 8px; + cursor: pointer; + transition: all 0.2s; + display: flex; + justify-content: space-between; + align-items: center; } -.scene-tab .close-btn:hover { - opacity: 1; +.scene-item:hover { + background: rgba(233, 69, 96, 0.2); + border-color: #e94560; } -#add-scene-btn { - padding: 8px 16px; - background: #533483; - color: #fff; - border: none; - border-radius: 4px; - cursor: pointer; - margin-left: 10px; +.scene-item.active { + background: rgba(233, 69, 96, 0.3); + border-color: #e94560; + box-shadow: 0 0 15px rgba(233, 69, 96, 0.3); } -#canvas-wrapper { - flex: 1; - display: flex; - justify-content: center; - align-items: center; - padding: 20px; - overflow: auto; +/* === 时间轴 === */ +.timeline { + background: rgba(0, 0, 0, 0.3); + border-radius: 16px; + padding: 16px; + margin-top: 20px; + border: 1px solid rgba(255, 255, 255, 0.05); } -#main-canvas { - background: #fff; - border-radius: 8px; - box-shadow: 0 4px 20px rgba(0,0,0,0.5); - cursor: crosshair; +.timeline h3 { + color: #e94560; + margin-bottom: 12px; + font-size: 1.1em; } -/* ==================== 底部时间轴面板 ==================== */ -#timeline-panel { - background: #16213e; - border-top: 2px solid #0f3460; - padding: 15px 20px; - display: flex; - flex-direction: column; - gap: 12px; +.timeline-controls { + display: flex; + gap: 10px; + margin-bottom: 16px; + flex-wrap: wrap; } -/* 播放控制栏 */ -#playback-controls { - display: flex; - align-items: center; - gap: 15px; -} - -#playback-controls button { - width: 40px; - height: 40px; - border-radius: 50%; - border: none; - background: #0f3460; - color: #fff; - font-size: 18px; - cursor: pointer; - transition: all 0.3s; - display: flex; - align-items: center; - justify-content: center; -} - -#playback-controls button:hover { - background: #e94560; - transform: scale(1.1); -} - -#frame-counter { - font-size: 14px; - color: #aaa; - min-width: 80px; -} - -#playback-controls label { - display: flex; - align-items: center; - gap: 10px; - font-size: 14px; - color: #aaa; -} - -#fps-slider { - width: 120px; - cursor: pointer; -} - -#fps-display { - color: #e94560; - font-weight: bold; - min-width: 50px; -} - -/* 帧缩略图时间轴 */ -#timeline-frames { - display: flex; - gap: 8px; - padding: 10px; - background: #0f0f1a; - border-radius: 6px; - overflow-x: auto; - min-height: 70px; - align-items: center; +.frames-container { + display: flex; + gap: 12px; + overflow-x: auto; + padding: 12px 0; + min-height: 100px; } .frame-thumb { - width: 80px; - height: 60px; - background: #0f3460; - border: 2px solid #533483; - border-radius: 4px; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - font-size: 12px; - color: #aaa; - position: relative; - transition: all 0.3s; - flex-shrink: 0; + min-width: 120px; + height: 68px; + background: #0f0f1a; + border: 2px solid #533483; + border-radius: 8px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s; + font-size: 0.8em; + color: #aaa; + position: relative; } .frame-thumb:hover { - border-color: #e94560; - transform: translateY(-2px); + border-color: #e94560; + transform: scale(1.05); } .frame-thumb.active { - border-color: #e94560; - background: #533483; - box-shadow: 0 0 10px rgba(233, 69, 96, 0.5); + border-color: #e94560; + background: rgba(233, 69, 96, 0.2); + box-shadow: 0 0 15px rgba(233, 69, 96, 0.3); } -.frame-thumb::before { - content: attr(data-frame); - position: absolute; - top: 2px; - left: 4px; - font-size: 10px; - color: #fff; - background: rgba(0,0,0,0.5); - padding: 1px 4px; - border-radius: 2px; +.frame-index { + font-size: 1.2em; + font-weight: bold; + color: #e94560; + margin-bottom: 4px; } -/* 时间轴操作按钮 */ -#timeline-actions { - display: flex; - gap: 10px; +/* === 预览模式样式 === */ +.preview-mode .left-panel, +.preview-mode .right-panel, +.preview-mode .timeline, +.preview-mode .canvas-controls .btn:not(#exitPreviewBtn):not(#playPreviewBtn):not(#pausePreviewBtn):not(#stopPreviewBtn) { + display: none !important; } -#timeline-actions button { - padding: 8px 16px; - background: #0f3460; - color: #fff; - border: none; - border-radius: 4px; - cursor: pointer; - font-size: 14px; - transition: all 0.3s; +.preview-mode .canvas-area { + grid-column: 1 / -1; + justify-content: center; + min-height: 80vh; } -#timeline-actions button:hover { - background: #e94560; +.preview-mode #mainCanvas { + max-width: 1200px; + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.6); } -/* ==================== 画布上的素材样式 ==================== */ -.canvas-asset { - position: absolute; - cursor: move; - user-select: none; - font-size: 48px; - filter: drop-shadow(2px 2px 4px rgba(0,0,0,0.3)); - transition: transform 0.1s; +.preview-mode .preview-controls { + display: flex !important; + gap: 12px; + margin-top: 20px; + justify-content: center; } +/* === 分享面板样式 === */ +.share-panel { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: #1a1a2e; + border: 2px solid #e94560; + border-radius: 24px; + padding: 30px; + z-index: 1000; + min-width: 400px; + box-shadow: 0 30px 60px rgba(0, 0, 0, 0.8); + backdrop-filter: blur(20px); +} + +.share-panel h3 { + color: #e94560; + margin-bottom: 20px; + font-size: 1.5em; +} + +.share-url { + background: #0a0a14; + border: 1px solid #533483; + border-radius: 12px; + padding: 12px; + margin: 15px 0; + word-break: break-all; + font-size: 0.9em; + color: #fff; + max-height: 100px; + overflow-y: auto; +} + +.share-actions { + display: flex; + gap: 12px; + justify-content: flex-end; + margin-top: 20px; +} + +/* === 遮罩层 === */ +.overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.7); + backdrop-filter: blur(5px); + z-index: 999; +} + +/* === 工具类 === */ +.hidden { + display: none !important; +} + +/* === canvas-asset(来自原来文件,保留) === */ .canvas-asset:hover { - transform: scale(1.1); + transform: scale(1.1); } .canvas-asset.selected { - outline: 2px dashed #e94560; - outline-offset: 4px; + outline: 2px dashed #e94560; + outline-offset: 4px; } -/* ==================== 滚动条样式 ==================== */ +/* === 滚动条样式(来自原来文件,保留) === */ ::-webkit-scrollbar { - width: 8px; - height: 8px; + width: 8px; + height: 8px; } ::-webkit-scrollbar-track { - background: #0f0f1a; + background: #0f0f1a; } ::-webkit-scrollbar-thumb { - background: #533483; - border-radius: 4px; + background: #533483; + border-radius: 4px; } ::-webkit-scrollbar-thumb:hover { - background: #e94560; + background: #e94560; } diff --git a/dynamic-comic-studio/index.html b/dynamic-comic-studio/index.html index b5be2dcb..f95a8ace 100644 --- a/dynamic-comic-studio/index.html +++ b/dynamic-comic-studio/index.html @@ -3,84 +3,106 @@ - HoloLake 动态漫制作系统 - 环节5 + HoloLake 动态漫制作系统 - 环节6 -
- -
-

🎬 HoloLake 动态漫制作系统

-
- - - - -
-
- - -
+
+

🎬 HoloLake 动态漫制作系统

+ + +
- - +
+ -
-
-
- +
+ + + +
+ + + + + +
-
- + + + -
-
- - -
-
- - - 帧 1/1 -
-
-
- - + + +
+

🎬 场景列表

+
+ + + +
+

⏱️ 时间轴

+
+ + + +
+
+
+ 速度: + + 4 FPS +
+
-
+
+ + + + + diff --git a/dynamic-comic-studio/js/app.js b/dynamic-comic-studio/js/app.js index 184d1b06..29be2ab8 100644 --- a/dynamic-comic-studio/js/app.js +++ b/dynamic-comic-studio/js/app.js @@ -1,581 +1,536 @@ -// ==================== HoloLake 动态漫制作系统 v5.0 ==================== -// 环节5:动画时间轴(帧序列 + 播放控制 + 帧率调节) +// HoloLake 动态漫制作系统 - 环节6 (预览与分享版) +// 基于环节5增强,新增预览模式、分享链接、嵌入代码功能 -const STORAGE_KEY = 'hololake-comic-studio-data'; -const canvas = document.getElementById('main-canvas'); -const ctx = canvas.getContext('2d'); - -// ==================== 全局状态 ==================== -let appState = { - currentSceneId: 1, - nextSceneId: 2, - nextAssetId: 1, - nextFrameId: 1, - scenes: [], - playback: { - isPlaying: false, - fps: 4, - intervalId: null, - currentFrameIndex: 0 +// ==================== 全局变量 ==================== +let scenes = [ + { + id: 'scene-1', + name: '场景1', + frames: [ + { + id: 'frame-1-1', + assets: [] // 存储素材 { id, type, emoji, x, y, width, height } + } + ] } -}; +]; +let currentSceneIndex = 0; +let currentFrameIndex = 0; +let animationInterval = null; +let isPlaying = false; +let fps = 4; +let canvas = document.getElementById('mainCanvas'); +let ctx = canvas.getContext('2d'); // ==================== 初始化 ==================== function init() { - loadFromStorage(); - if (appState.scenes.length === 0) { - createDefaultScene(); - } + console.log('HoloLake 动态漫制作系统 v6.0 初始化...'); + updateUI(); setupEventListeners(); - renderSceneTabs(); - renderCanvas(); - renderTimeline(); - updatePlaybackControls(); + drawCurrentFrame(); } -// 创建默认场景 -function createDefaultScene() { - const defaultScene = { - id: 1, - name: '场景 1', - currentFrameIndex: 0, - frames: [{ - id: 1, - assets: [] - }], - nextFrameId: 2 - }; - appState.scenes.push(defaultScene); - appState.currentSceneId = 1; - saveToStorage(); -} - -// ==================== 存储管理 ==================== -function saveToStorage() { - localStorage.setItem(STORAGE_KEY, JSON.stringify(appState)); -} - -function loadFromStorage() { - const data = localStorage.getItem(STORAGE_KEY); - if (data) { - const parsed = JSON.parse(data); - // 向后兼容:旧数据没有 frames 字段 - if (parsed.scenes && parsed.scenes.length > 0 && !parsed.scenes[0].frames) { - parsed.scenes = parsed.scenes.map(scene => ({ - ...scene, - currentFrameIndex: 0, - frames: [{ - id: 1, - assets: scene.assets || [] - }], - nextFrameId: 2 - })); - delete parsed.scenes[0].assets; // 删除旧的 assets 字段 - } - // 确保 playback 对象存在 - if (!parsed.playback) { - parsed.playback = { isPlaying: false, fps: 4, intervalId: null, currentFrameIndex: 0 }; - } - appState = parsed; - } -} - -// ==================== 场景管理 ==================== -function addScene() { - const newScene = { - id: appState.nextSceneId++, - name: `场景 ${appState.scenes.length + 1}`, - currentFrameIndex: 0, - frames: [{ - id: 1, - assets: [] - }], - nextFrameId: 2 - }; - appState.scenes.push(newScene); - appState.currentSceneId = newScene.id; - saveToStorage(); - renderSceneTabs(); - renderCanvas(); - renderTimeline(); -} - -function switchScene(sceneId) { - if (appState.playback.isPlaying) { - stopAnimation(); - } - appState.currentSceneId = sceneId; - saveToStorage(); - renderSceneTabs(); - renderCanvas(); - renderTimeline(); - updateFrameCounter(); -} - -function deleteScene(sceneId) { - if (appState.scenes.length <= 1) { - alert('至少保留一个场景!'); - return; - } - appState.scenes = appState.scenes.filter(s => s.id !== sceneId); - if (appState.currentSceneId === sceneId) { - appState.currentSceneId = appState.scenes[0].id; - } - saveToStorage(); - renderSceneTabs(); - renderCanvas(); - renderTimeline(); -} - -function getCurrentScene() { - return appState.scenes.find(s => s.id === appState.currentSceneId); -} - -function getCurrentFrame() { - const scene = getCurrentScene(); - if (!scene) return null; - return scene.frames[scene.currentFrameIndex]; -}// ==================== 帧管理 ==================== -function addFrame() { - const scene = getCurrentScene(); - if (!scene) return; - - // 深拷贝当前帧的素材状态 - const currentFrame = scene.frames[scene.currentFrameIndex]; - const newFrame = { - id: scene.nextFrameId++, - assets: JSON.parse(JSON.stringify(currentFrame.assets)) - }; - - // 在当前帧后插入新帧 - scene.frames.splice(scene.currentFrameIndex + 1, 0, newFrame); - scene.currentFrameIndex++; - - saveToStorage(); - renderTimeline(); - renderCanvas(); - updateFrameCounter(); -} - -function deleteFrame() { - const scene = getCurrentScene(); - if (!scene) return; - - if (scene.frames.length <= 1) { - alert('至少保留一帧!'); - return; +// ==================== 事件监听设置 ==================== +function setupEventListeners() { + // FPS滑块 + const fpsSlider = document.getElementById('fpsSlider'); + if (fpsSlider) { + fpsSlider.addEventListener('input', function(e) { + fps = parseInt(e.target.value); + document.getElementById('fpsValue').textContent = fps + ' FPS'; + if (isPlaying) { + stopAnimation(); + startAnimation(); + } + }); } - scene.frames.splice(scene.currentFrameIndex, 1); - if (scene.currentFrameIndex >= scene.frames.length) { - scene.currentFrameIndex = scene.frames.length - 1; + // 预览按钮 + const previewBtn = document.getElementById('previewBtn'); + if (previewBtn) { + previewBtn.addEventListener('click', enterPreviewMode); } - saveToStorage(); - renderTimeline(); - renderCanvas(); - updateFrameCounter(); -} - -function switchFrame(frameIndex) { - const scene = getCurrentScene(); - if (!scene || frameIndex < 0 || frameIndex >= scene.frames.length) return; - - // 保存当前画布状态到当前帧 - captureFrame(); - - scene.currentFrameIndex = frameIndex; - saveToStorage(); - renderTimeline(); - renderCanvas(); - updateFrameCounter(); -} - -function captureFrame() { - // 画布状态实时保存在 assets 中,无需额外操作 - saveToStorage(); -} - -// ==================== 播放引擎 ==================== -function playAnimation() { - const scene = getCurrentScene(); - if (!scene || scene.frames.length <= 1) return; - - appState.playback.isPlaying = true; - updatePlaybackControls(); - - const intervalMs = 1000 / appState.playback.fps; - - appState.playback.intervalId = setInterval(() => { - const scene = getCurrentScene(); - if (!scene) return; - - scene.currentFrameIndex++; - if (scene.currentFrameIndex >= scene.frames.length) { - scene.currentFrameIndex = 0; // 循环播放 - } - - renderCanvas(); - renderTimeline(); - updateFrameCounter(); - saveToStorage(); - }, intervalMs); -} - -function pauseAnimation() { - appState.playback.isPlaying = false; - if (appState.playback.intervalId) { - clearInterval(appState.playback.intervalId); - appState.playback.intervalId = null; + // 预览模式按钮 + const playPreviewBtn = document.getElementById('playPreviewBtn'); + if (playPreviewBtn) { + playPreviewBtn.addEventListener('click', function() { + if (!isPlaying) { + startAnimation(); + } + }); } - updatePlaybackControls(); -} - -function stopAnimation() { - pauseAnimation(); - const scene = getCurrentScene(); - if (scene) { - scene.currentFrameIndex = 0; - renderCanvas(); - renderTimeline(); - updateFrameCounter(); - saveToStorage(); - } -} - -function togglePlayback() { - if (appState.playback.isPlaying) { - pauseAnimation(); - } else { - playAnimation(); - } -} - -function setFPS(fps) { - appState.playback.fps = parseInt(fps); - document.getElementById('fps-display').textContent = fps + ' FPS'; - saveToStorage(); - // 如果正在播放,重启定时器以应用新帧率 - if (appState.playback.isPlaying) { - pauseAnimation(); - playAnimation(); + const pausePreviewBtn = document.getElementById('pausePreviewBtn'); + if (pausePreviewBtn) { + pausePreviewBtn.addEventListener('click', function() { + if (isPlaying) { + stopAnimation(); + } + }); } -} - -function updatePlaybackControls() { - const btnPlay = document.getElementById('btn-play'); - if (appState.playback.isPlaying) { - btnPlay.textContent = '⏸️'; - btnPlay.title = '暂停'; - } else { - btnPlay.textContent = '▶️'; - btnPlay.title = '播放'; - } -} - -function updateFrameCounter() { - const scene = getCurrentScene(); - if (!scene) return; - const counter = document.getElementById('frame-counter'); - counter.textContent = `帧 ${scene.currentFrameIndex + 1}/${scene.frames.length}`; -}// ==================== 渲染函数 ==================== -function renderSceneTabs() { - const tabsList = document.getElementById('tabs-list'); - tabsList.innerHTML = ''; - appState.scenes.forEach(scene => { - const tab = document.createElement('div'); - tab.className = 'scene-tab' + (scene.id === appState.currentSceneId ? ' active' : ''); - tab.innerHTML = ` - ${scene.name} - × - `; - tab.onclick = () => switchScene(scene.id); - tabsList.appendChild(tab); + const stopPreviewBtn = document.getElementById('stopPreviewBtn'); + if (stopPreviewBtn) { + stopPreviewBtn.addEventListener('click', function() { + stopAnimation(); + currentFrameIndex = 0; + updateUI(); + drawCurrentFrame(); + }); + } + + const exitPreviewBtn = document.getElementById('exitPreviewBtn'); + if (exitPreviewBtn) { + exitPreviewBtn.addEventListener('click', exitPreviewMode); + } + + // 分享按钮 + const shareBtn = document.getElementById('shareBtn'); + if (shareBtn) { + shareBtn.addEventListener('click', showSharePanel); + } + + // 分享面板按钮 + const generateUrlBtn = document.getElementById('generateUrlBtn'); + if (generateUrlBtn) { + generateUrlBtn.addEventListener('click', generateShareUrl); + } + + const generateEmbedBtn = document.getElementById('generateEmbedBtn'); + if (generateEmbedBtn) { + generateEmbedBtn.addEventListener('click', generateEmbedCode); + } + + const closeShareBtn = document.getElementById('closeShareBtn'); + if (closeShareBtn) { + closeShareBtn.addEventListener('click', hideSharePanel); + } + + const copyShareBtn = document.getElementById('copyShareBtn'); + if (copyShareBtn) { + copyShareBtn.addEventListener('click', copyToClipboard); + } + + // 素材拖拽 + const materialItems = document.querySelectorAll('.material-item'); + materialItems.forEach(item => { + item.addEventListener('dragstart', handleDragStart); }); + + // 画布放置 + canvas.addEventListener('dragover', (e) => e.preventDefault()); + canvas.addEventListener('drop', handleDrop); + + // 画布点击选择素材 + canvas.addEventListener('click', handleCanvasClick); } -function renderTimeline() { - const timelineFrames = document.getElementById('timeline-frames'); - timelineFrames.innerHTML = ''; - - const scene = getCurrentScene(); - if (!scene) return; - - scene.frames.forEach((frame, index) => { - const thumb = document.createElement('div'); - thumb.className = 'frame-thumb' + (index === scene.currentFrameIndex ? ' active' : ''); - thumb.setAttribute('data-frame', index + 1); - thumb.textContent = `帧 ${index + 1}`; - thumb.onclick = () => switchFrame(index); - timelineFrames.appendChild(thumb); - }); +// ==================== 素材拖拽 ==================== +function handleDragStart(e) { + const type = e.target.dataset.type; + const emoji = e.target.dataset.emoji; + e.dataTransfer.setData('text/plain', JSON.stringify({ + type: type, + emoji: emoji + })); } -function renderCanvas() { - // 清空画布 - ctx.fillStyle = '#ffffff'; +// ==================== 素材放置 ==================== +function handleDrop(e) { + e.preventDefault(); + const rect = canvas.getBoundingClientRect(); + const scaleX = canvas.width / rect.width; + const scaleY = canvas.height / rect.height; + + const x = (e.clientX - rect.left) * scaleX; + const y = (e.clientY - rect.top) * scaleY; + + try { + const data = JSON.parse(e.dataTransfer.getData('text/plain')); + + const currentScene = scenes[currentSceneIndex]; + const currentFrame = currentScene.frames[currentFrameIndex]; + + const newAsset = { + id: 'asset-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9), + type: data.type, + emoji: data.emoji, + x: x - 25, + y: y - 25, + width: 50, + height: 50 + }; + + currentFrame.assets.push(newAsset); + drawCurrentFrame(); + updateUI(); + } catch (error) { + console.error('放置素材失败:', error); + } +} + +// ==================== 画布点击选择 ==================== +function handleCanvasClick(e) { + const rect = canvas.getBoundingClientRect(); + const scaleX = canvas.width / rect.width; + const scaleY = canvas.height / rect.height; + + const clickX = (e.clientX - rect.left) * scaleX; + const clickY = (e.clientY - rect.top) * scaleY; + + const currentScene = scenes[currentSceneIndex]; + const currentFrame = currentScene.frames[currentFrameIndex]; + + // 从后往前遍历,以便选中上层的素材 + let selectedAsset = null; + for (let i = currentFrame.assets.length - 1; i >= 0; i--) { + const asset = currentFrame.assets[i]; + if (clickX >= asset.x && clickX <= asset.x + asset.width && + clickY >= asset.y && clickY <= asset.y + asset.height) { + selectedAsset = asset; + break; + } + } + + // 移除所有选中状态 + currentFrame.assets.forEach(a => delete a.selected); + + // 设置新的选中状态 + if (selectedAsset) { + selectedAsset.selected = true; + } + + drawCurrentFrame(); +} + +// ==================== 绘制当前帧 ==================== +function drawCurrentFrame() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + + const currentScene = scenes[currentSceneIndex]; + if (!currentScene) return; + + const currentFrame = currentScene.frames[currentFrameIndex]; + if (!currentFrame) return; + + // 绘制背景色 + ctx.fillStyle = '#0a0a14'; ctx.fillRect(0, 0, canvas.width, canvas.height); - const frame = getCurrentFrame(); - if (!frame) return; - // 绘制所有素材 - frame.assets.forEach(asset => { - ctx.font = '48px Arial'; + currentFrame.assets.forEach(asset => { + ctx.font = '40px "Segoe UI Emoji", "Apple Color Emoji", sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; + ctx.fillText(asset.emoji, asset.x + asset.width/2, asset.y + asset.height/2); - // 绘制阴影 - ctx.shadowColor = 'rgba(0,0,0,0.3)'; - ctx.shadowBlur = 4; - ctx.shadowOffsetX = 2; - ctx.shadowOffsetY = 2; - - ctx.fillText(asset.emoji, asset.x, asset.y); - - // 重置阴影 - ctx.shadowColor = 'transparent'; - ctx.shadowBlur = 0; - ctx.shadowOffsetX = 0; - ctx.shadowOffsetY = 0; - - // 如果是选中状态,绘制边框 + // 如果被选中,绘制边框 if (asset.selected) { ctx.strokeStyle = '#e94560'; ctx.lineWidth = 2; ctx.setLineDash([5, 5]); - ctx.strokeRect(asset.x - 30, asset.y - 30, 60, 60); + ctx.strokeRect(asset.x, asset.y, asset.width, asset.height); ctx.setLineDash([]); } }); } -// ==================== 素材拖放 ==================== -function setupEventListeners() { - // 素材库拖放 - const assetItems = document.querySelectorAll('.asset-item'); - assetItems.forEach(item => { - item.addEventListener('dragstart', (e) => { - e.dataTransfer.setData('type', item.dataset.type); - e.dataTransfer.setData('emoji', item.dataset.emoji); - }); - }); - - // 画布接收拖放 - canvas.addEventListener('dragover', (e) => { - e.preventDefault(); - }); - - canvas.addEventListener('drop', (e) => { - e.preventDefault(); - const type = e.dataTransfer.getData('type'); - const emoji = e.dataTransfer.getData('emoji'); - - if (emoji) { - const rect = canvas.getBoundingClientRect(); - const x = e.clientX - rect.left; - const y = e.clientY - rect.top; - - addAssetToCanvas(emoji, x, y); - } - }); - - // 画布点击选择/移动 - let isDragging = false; - let dragStartX, dragStartY; - let selectedAsset = null; - - canvas.addEventListener('mousedown', (e) => { - const rect = canvas.getBoundingClientRect(); - const x = e.clientX - rect.left; - const y = e.clientY - rect.top; - - const frame = getCurrentFrame(); - if (!frame) return; - - // 查找点击的素材(从后往前,优先选上层的) - selectedAsset = null; - for (let i = frame.assets.length - 1; i >= 0; i--) { - const asset = frame.assets[i]; - const dist = Math.sqrt((x - asset.x) ** 2 + (y - asset.y) ** 2); - if (dist < 30) { - selectedAsset = asset; - // 更新选中状态 - frame.assets.forEach(a => a.selected = false); - asset.selected = true; - isDragging = true; - dragStartX = x - asset.x; - dragStartY = y - asset.y; - renderCanvas(); - break; +// ==================== 场景管理 ==================== +function addScene() { + const newScene = { + id: 'scene-' + Date.now(), + name: '场景' + (scenes.length + 1), + frames: [ + { + id: 'frame-' + Date.now() + '-1', + assets: [] } - } - - // 如果没点到素材,取消所有选中 - if (!selectedAsset) { - frame.assets.forEach(a => a.selected = false); - renderCanvas(); - } - }); - - canvas.addEventListener('mousemove', (e) => { - if (!isDragging || !selectedAsset) return; - - const rect = canvas.getBoundingClientRect(); - const x = e.clientX - rect.left; - const y = e.clientY - rect.top; - - selectedAsset.x = x - dragStartX; - selectedAsset.y = y - dragStartY; - - // 边界限制 - selectedAsset.x = Math.max(30, Math.min(canvas.width - 30, selectedAsset.x)); - selectedAsset.y = Math.max(30, Math.min(canvas.height - 30, selectedAsset.y)); - - renderCanvas(); - }); - - canvas.addEventListener('mouseup', () => { - if (isDragging) { - isDragging = false; - saveToStorage(); - } - }); - - // 键盘删除 - document.addEventListener('keydown', (e) => { - if (e.key === 'Delete' || e.key === 'Backspace') { - const frame = getCurrentFrame(); - if (!frame) return; - - const selectedIndex = frame.assets.findIndex(a => a.selected); - if (selectedIndex !== -1) { - frame.assets.splice(selectedIndex, 1); - renderCanvas(); - saveToStorage(); - } - } - }); - - // 导入文件监听 - document.getElementById('import-file').addEventListener('change', handleImport); + ] + }; + scenes.push(newScene); + currentSceneIndex = scenes.length - 1; + currentFrameIndex = 0; + updateUI(); + drawCurrentFrame(); } -function addAssetToCanvas(emoji, x, y) { - const frame = getCurrentFrame(); - if (!frame) return; +// ==================== 帧管理 ==================== +function addFrame() { + const currentScene = scenes[currentSceneIndex]; + const currentFrame = currentScene.frames[currentFrameIndex]; - // 取消其他选中 - frame.assets.forEach(a => a.selected = false); + // 深拷贝当前帧的素材 + const newAssets = JSON.parse(JSON.stringify(currentFrame.assets)); + // 移除选中状态 + newAssets.forEach(a => delete a.selected); - const newAsset = { - id: appState.nextAssetId++, - type: 'emoji', - emoji: emoji, - x: x, - y: y, - selected: true + const newFrame = { + id: 'frame-' + Date.now(), + assets: newAssets }; - frame.assets.push(newAsset); - renderCanvas(); - saveToStorage(); -}// ==================== 导出导入 ==================== + currentScene.frames.splice(currentFrameIndex + 1, 0, newFrame); + currentFrameIndex++; + updateUI(); + drawCurrentFrame(); +} + +function deleteFrame() { + const currentScene = scenes[currentSceneIndex]; + if (currentScene.frames.length <= 1) { + alert('每个场景至少保留一帧'); + return; + } + + currentScene.frames.splice(currentFrameIndex, 1); + if (currentFrameIndex >= currentScene.frames.length) { + currentFrameIndex = currentScene.frames.length - 1; + } + updateUI(); + drawCurrentFrame(); +} + +// ==================== 动画控制 ==================== +function togglePlayback() { + if (isPlaying) { + stopAnimation(); + } else { + startAnimation(); + } +} + +function startAnimation() { + if (isPlaying) return; + isPlaying = true; + updatePlayButton(); + + animationInterval = setInterval(() => { + const currentScene = scenes[currentSceneIndex]; + if (currentFrameIndex < currentScene.frames.length - 1) { + currentFrameIndex++; + } else { + currentFrameIndex = 0; + } + updateUI(); + drawCurrentFrame(); + }, 1000 / fps); +} + +function stopAnimation() { + if (animationInterval) { + clearInterval(animationInterval); + animationInterval = null; + } + isPlaying = false; + updatePlayButton(); +} + +function updatePlayButton() { + const playBtn = document.getElementById('timelinePlayBtn'); + if (playBtn) { + playBtn.textContent = isPlaying ? '⏸️ 暂停' : '▶️ 播放'; + } + + const previewPlayBtn = document.getElementById('playPreviewBtn'); + if (previewPlayBtn) { + // 预览模式的播放按钮文字不变 + } +} + +// ==================== 预览模式 ==================== +function enterPreviewMode() { + document.body.classList.add('preview-mode'); + // 如果正在播放动画,继续播放 +} + +function exitPreviewMode() { + document.body.classList.remove('preview-mode'); +} + +// ==================== 分享功能 ==================== +function showSharePanel() { + document.getElementById('shareOverlay').classList.remove('hidden'); + document.getElementById('sharePanel').classList.remove('hidden'); +} + +function hideSharePanel() { + document.getElementById('shareOverlay').classList.add('hidden'); + document.getElementById('sharePanel').classList.add('hidden'); + document.getElementById('shareUrl').innerHTML = ''; + document.getElementById('copyShareBtn').style.display = 'none'; +} + +function generateShareUrl() { + // 序列化作品数据 + const workData = { + scenes: scenes, + version: '1.0', + timestamp: Date.now() + }; + + const jsonStr = JSON.stringify(workData); + // 使用encodeURIComponent编码,然后生成Data URL + const encodedData = encodeURIComponent(jsonStr); + const dataUrl = 'data:text/json;charset=utf-8,' + encodedData; + + // 显示分享链接 + const shareUrlDiv = document.getElementById('shareUrl'); + shareUrlDiv.innerHTML = `${dataUrl.substring(0, 50)}...`; + + document.getElementById('copyShareBtn').style.display = 'inline-block'; + document.getElementById('copyShareBtn').dataset.url = dataUrl; +} + +function generateEmbedCode() { + // 生成iframe嵌入代码 + const embedCode = ``; + + const shareUrlDiv = document.getElementById('shareUrl'); + shareUrlDiv.innerHTML = `
${embedCode}
`; + + document.getElementById('copyShareBtn').style.display = 'inline-block'; + document.getElementById('copyShareBtn').dataset.code = embedCode; +} + +function copyToClipboard(e) { + const btn = e.target; + if (btn.dataset.url) { + navigator.clipboard.writeText(btn.dataset.url); + } else if (btn.dataset.code) { + navigator.clipboard.writeText(btn.dataset.code); + } + alert('已复制到剪贴板!'); +} + +// ==================== 导入导出 ==================== function exportScene() { - const dataStr = JSON.stringify(appState, null, 2); - const blob = new Blob([dataStr], { type: 'application/json' }); + const dataStr = JSON.stringify(scenes, null, 2); + const blob = new Blob([dataStr], {type: 'application/json'}); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; - a.download = `hololake-scene-${Date.now()}.json`; - document.body.appendChild(a); + a.download = 'hololake-scene.json'; a.click(); - document.body.removeChild(a); + URL.revokeObjectURL(url); } -function importScene() { - document.getElementById('import-file').click(); -} - -function handleImport(e) { - const file = e.target.files[0]; +function importScene(event) { + const file = event.target.files[0]; if (!file) return; const reader = new FileReader(); - reader.onload = (event) => { + reader.onload = function(e) { try { - const imported = JSON.parse(event.target.result); - - // 验证数据结构 - if (!imported.scenes || !Array.isArray(imported.scenes)) { - throw new Error('无效的数据格式'); + const importedScenes = JSON.parse(e.target.result); + if (Array.isArray(importedScenes) && importedScenes.length > 0) { + scenes = importedScenes; + currentSceneIndex = 0; + currentFrameIndex = 0; + updateUI(); + drawCurrentFrame(); + alert('导入成功!'); + } else { + alert('无效的场景数据'); } - - // 向后兼容处理 - imported.scenes = imported.scenes.map(scene => { - if (!scene.frames) { - return { - ...scene, - currentFrameIndex: 0, - frames: [{ - id: 1, - assets: scene.assets || [] - }], - nextFrameId: 2 - }; - } - return scene; - }); - - if (!imported.playback) { - imported.playback = { isPlaying: false, fps: 4, intervalId: null, currentFrameIndex: 0 }; - } - - appState = imported; - saveToStorage(); - renderSceneTabs(); - renderCanvas(); - renderTimeline(); - updatePlaybackControls(); - updateFrameCounter(); - - alert('导入成功!'); - } catch (err) { - alert('导入失败:' + err.message); + } catch (error) { + alert('导入失败:' + error.message); } }; reader.readAsText(file); - // 清空 input,允许重复导入同一文件 - e.target.value = ''; + // 清空input,以便再次导入同一个文件 + event.target.value = ''; } -// ==================== 截图导出 ==================== function exportScreenshot() { - // 临时取消选中状态 - const frame = getCurrentFrame(); - if (!frame) return; - - const originalSelected = frame.assets.map(a => a.selected); - frame.assets.forEach(a => a.selected = false); - renderCanvas(); - - // 导出 PNG - const link = document.createElement('a'); - link.download = `hololake-frame-${Date.now()}.png`; - link.href = canvas.toDataURL(); - link.click(); - - // 恢复选中状态 - frame.assets.forEach((a, i) => { - a.selected = originalSelected[i]; - }); - renderCanvas(); + const dataUrl = canvas.toDataURL('image/png'); + const a = document.createElement('a'); + a.href = dataUrl; + a.download = 'hololake-screenshot-' + Date.now() + '.png'; + a.click(); } -// ==================== 启动应用 ==================== -document.addEventListener('DOMContentLoaded', init); +// ==================== 更新UI ==================== +function updateUI() { + // 更新场景列表 + const sceneList = document.getElementById('sceneList'); + if (sceneList) { + sceneList.innerHTML = ''; + scenes.forEach((scene, index) => { + const sceneDiv = document.createElement('div'); + sceneDiv.className = 'scene-item' + (index === currentSceneIndex ? ' active' : ''); + sceneDiv.innerHTML = ` + ${scene.name} + + `; + sceneDiv.addEventListener('click', (e) => { + if (e.target.tagName !== 'BUTTON') { + switchScene(index); + } + }); + sceneList.appendChild(sceneDiv); + }); + } + + // 更新帧列表 + const framesContainer = document.getElementById('framesContainer'); + if (framesContainer && scenes[currentSceneIndex]) { + framesContainer.innerHTML = ''; + scenes[currentSceneIndex].frames.forEach((frame, index) => { + const frameDiv = document.createElement('div'); + frameDiv.className = 'frame-thumb' + (index === currentFrameIndex ? ' active' : ''); + frameDiv.innerHTML = ` +
${index + 1}
+
${frame.assets.length}个素材
+ `; + frameDiv.addEventListener('click', () => { + currentFrameIndex = index; + drawCurrentFrame(); + updateUI(); + }); + framesContainer.appendChild(frameDiv); + }); + } + + // 更新帧计数器 + const frameCounter = document.getElementById('frame-counter'); + if (frameCounter && scenes[currentSceneIndex]) { + frameCounter.textContent = `帧 ${currentFrameIndex + 1}/${scenes[currentSceneIndex].frames.length}`; + } +} + +function switchScene(index) { + if (index >= 0 && index < scenes.length) { + currentSceneIndex = index; + currentFrameIndex = 0; + if (isPlaying) { + stopAnimation(); + } + updateUI(); + drawCurrentFrame(); + } +} + +function deleteScene(index) { + if (scenes.length <= 1) { + alert('至少保留一个场景'); + return; + } + + scenes.splice(index, 1); + if (currentSceneIndex >= scenes.length) { + currentSceneIndex = scenes.length - 1; + } + currentFrameIndex = 0; + if (isPlaying) { + stopAnimation(); + } + updateUI(); + drawCurrentFrame(); +} + +// ==================== 启动初始化 ==================== +window.onload = init; diff --git a/dynamic-comic-studio/js/app.js.backup b/dynamic-comic-studio/js/app.js.backup new file mode 100644 index 00000000..42fc6145 --- /dev/null +++ b/dynamic-comic-studio/js/app.js.backup @@ -0,0 +1,1113 @@ +// ==================== HoloLake 动态漫制作系统 v5.0 ==================== +// 环节5:动画时间轴(帧序列 + 播放控制 + 帧率调节) + +const STORAGE_KEY = 'hololake-comic-studio-data'; +const canvas = document.getElementById('main-canvas'); +const ctx = canvas.getContext('2d'); + +// ==================== 全局状态 ==================== +let appState = { + currentSceneId: 1, + nextSceneId: 2, + nextAssetId: 1, + nextFrameId: 1, + scenes: [], + playback: { + isPlaying: false, + fps: 4, + intervalId: null, + currentFrameIndex: 0 + } +}; + +// ==================== 初始化 ==================== +function init() { + loadFromStorage(); + if (appState.scenes.length === 0) { + createDefaultScene(); + } + setupEventListeners(); + renderSceneTabs(); + renderCanvas(); + renderTimeline(); + updatePlaybackControls(); +} + +// 创建默认场景 +function createDefaultScene() { + const defaultScene = { + id: 1, + name: '场景 1', + currentFrameIndex: 0, + frames: [{ + id: 1, + assets: [] + }], + nextFrameId: 2 + }; + appState.scenes.push(defaultScene); + appState.currentSceneId = 1; + saveToStorage(); +} + +// ==================== 存储管理 ==================== +function saveToStorage() { + localStorage.setItem(STORAGE_KEY, JSON.stringify(appState)); +} + +function loadFromStorage() { + const data = localStorage.getItem(STORAGE_KEY); + if (data) { + const parsed = JSON.parse(data); + // 向后兼容:旧数据没有 frames 字段 + if (parsed.scenes && parsed.scenes.length > 0 && !parsed.scenes[0].frames) { + parsed.scenes = parsed.scenes.map(scene => ({ + ...scene, + currentFrameIndex: 0, + frames: [{ + id: 1, + assets: scene.assets || [] + }], + nextFrameId: 2 + })); + delete parsed.scenes[0].assets; // 删除旧的 assets 字段 + } + // 确保 playback 对象存在 + if (!parsed.playback) { + parsed.playback = { isPlaying: false, fps: 4, intervalId: null, currentFrameIndex: 0 }; + } + appState = parsed; + } +} + +// ==================== 场景管理 ==================== +function addScene() { + const newScene = { + id: appState.nextSceneId++, + name: `场景 ${appState.scenes.length + 1}`, + currentFrameIndex: 0, + frames: [{ + id: 1, + assets: [] + }], + nextFrameId: 2 + }; + appState.scenes.push(newScene); + appState.currentSceneId = newScene.id; + saveToStorage(); + renderSceneTabs(); + renderCanvas(); + renderTimeline(); +} + +function switchScene(sceneId) { + if (appState.playback.isPlaying) { + stopAnimation(); + } + appState.currentSceneId = sceneId; + saveToStorage(); + renderSceneTabs(); + renderCanvas(); + renderTimeline(); + updateFrameCounter(); +} + +function deleteScene(sceneId) { + if (appState.scenes.length <= 1) { + alert('至少保留一个场景!'); + return; + } + appState.scenes = appState.scenes.filter(s => s.id !== sceneId); + if (appState.currentSceneId === sceneId) { + appState.currentSceneId = appState.scenes[0].id; + } + saveToStorage(); + renderSceneTabs(); + renderCanvas(); + renderTimeline(); +} + +function getCurrentScene() { + return appState.scenes.find(s => s.id === appState.currentSceneId); +} + +function getCurrentFrame() { + const scene = getCurrentScene(); + if (!scene) return null; + return scene.frames[scene.currentFrameIndex]; +}// ==================== 帧管理 ==================== +function addFrame() { + const scene = getCurrentScene(); + if (!scene) return; + + // 深拷贝当前帧的素材状态 + const currentFrame = scene.frames[scene.currentFrameIndex]; + const newFrame = { + id: scene.nextFrameId++, + assets: JSON.parse(JSON.stringify(currentFrame.assets)) + }; + + // 在当前帧后插入新帧 + scene.frames.splice(scene.currentFrameIndex + 1, 0, newFrame); + scene.currentFrameIndex++; + + saveToStorage(); + renderTimeline(); + renderCanvas(); + updateFrameCounter(); +} + +function deleteFrame() { + const scene = getCurrentScene(); + if (!scene) return; + + if (scene.frames.length <= 1) { + alert('至少保留一帧!'); + return; + } + + scene.frames.splice(scene.currentFrameIndex, 1); + if (scene.currentFrameIndex >= scene.frames.length) { + scene.currentFrameIndex = scene.frames.length - 1; + } + + saveToStorage(); + renderTimeline(); + renderCanvas(); + updateFrameCounter(); +} + +function switchFrame(frameIndex) { + const scene = getCurrentScene(); + if (!scene || frameIndex < 0 || frameIndex >= scene.frames.length) return; + + // 保存当前画布状态到当前帧 + captureFrame(); + + scene.currentFrameIndex = frameIndex; + saveToStorage(); + renderTimeline(); + renderCanvas(); + updateFrameCounter(); +} + +function captureFrame() { + // 画布状态实时保存在 assets 中,无需额外操作 + saveToStorage(); +} + +// ==================== 播放引擎 ==================== +function playAnimation() { + const scene = getCurrentScene(); + if (!scene || scene.frames.length <= 1) return; + + appState.playback.isPlaying = true; + updatePlaybackControls(); + + const intervalMs = 1000 / appState.playback.fps; + + appState.playback.intervalId = setInterval(() => { + const scene = getCurrentScene(); + if (!scene) return; + + scene.currentFrameIndex++; + if (scene.currentFrameIndex >= scene.frames.length) { + scene.currentFrameIndex = 0; // 循环播放 + } + + renderCanvas(); + renderTimeline(); + updateFrameCounter(); + saveToStorage(); + }, intervalMs); +} + +function pauseAnimation() { + appState.playback.isPlaying = false; + if (appState.playback.intervalId) { + clearInterval(appState.playback.intervalId); + appState.playback.intervalId = null; + } + updatePlaybackControls(); +} + +function stopAnimation() { + pauseAnimation(); + const scene = getCurrentScene(); + if (scene) { + scene.currentFrameIndex = 0; + renderCanvas(); + renderTimeline(); + updateFrameCounter(); + saveToStorage(); + } +} + +function togglePlayback() { + if (appState.playback.isPlaying) { + pauseAnimation(); + } else { + playAnimation(); + } +} + +function setFPS(fps) { + appState.playback.fps = parseInt(fps); + document.getElementById('fps-display').textContent = fps + ' FPS'; + saveToStorage(); + + // 如果正在播放,重启定时器以应用新帧率 + if (appState.playback.isPlaying) { + pauseAnimation(); + playAnimation(); + } +} + +function updatePlaybackControls() { + const btnPlay = document.getElementById('btn-play'); + if (appState.playback.isPlaying) { + btnPlay.textContent = '⏸️'; + btnPlay.title = '暂停'; + } else { + btnPlay.textContent = '▶️'; + btnPlay.title = '播放'; + } +} + +function updateFrameCounter() { + const scene = getCurrentScene(); + if (!scene) return; + const counter = document.getElementById('frame-counter'); + counter.textContent = `帧 ${scene.currentFrameIndex + 1}/${scene.frames.length}`; +}// ==================== 渲染函数 ==================== +function renderSceneTabs() { + const tabsList = document.getElementById('tabs-list'); + tabsList.innerHTML = ''; + + appState.scenes.forEach(scene => { + const tab = document.createElement('div'); + tab.className = 'scene-tab' + (scene.id === appState.currentSceneId ? ' active' : ''); + tab.innerHTML = ` + ${scene.name} + × + `; + tab.onclick = () => switchScene(scene.id); + tabsList.appendChild(tab); + }); +} + +function renderTimeline() { + const timelineFrames = document.getElementById('timeline-frames'); + timelineFrames.innerHTML = ''; + + const scene = getCurrentScene(); + if (!scene) return; + + scene.frames.forEach((frame, index) => { + const thumb = document.createElement('div'); + thumb.className = 'frame-thumb' + (index === scene.currentFrameIndex ? ' active' : ''); + thumb.setAttribute('data-frame', index + 1); + thumb.textContent = `帧 ${index + 1}`; + thumb.onclick = () => switchFrame(index); + timelineFrames.appendChild(thumb); + }); +} + +function renderCanvas() { + // 清空画布 + ctx.fillStyle = '#ffffff'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + const frame = getCurrentFrame(); + if (!frame) return; + + // 绘制所有素材 + frame.assets.forEach(asset => { + ctx.font = '48px Arial'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + + // 绘制阴影 + ctx.shadowColor = 'rgba(0,0,0,0.3)'; + ctx.shadowBlur = 4; + ctx.shadowOffsetX = 2; + ctx.shadowOffsetY = 2; + + ctx.fillText(asset.emoji, asset.x, asset.y); + + // 重置阴影 + ctx.shadowColor = 'transparent'; + ctx.shadowBlur = 0; + ctx.shadowOffsetX = 0; + ctx.shadowOffsetY = 0; + + // 如果是选中状态,绘制边框 + if (asset.selected) { + ctx.strokeStyle = '#e94560'; + ctx.lineWidth = 2; + ctx.setLineDash([5, 5]); + ctx.strokeRect(asset.x - 30, asset.y - 30, 60, 60); + ctx.setLineDash([]); + } + }); +} + +// ==================== 素材拖放 ==================== +function setupEventListeners() { + // 素材库拖放 + const assetItems = document.querySelectorAll('.asset-item'); + assetItems.forEach(item => { + item.addEventListener('dragstart', (e) => { + e.dataTransfer.setData('type', item.dataset.type); + e.dataTransfer.setData('emoji', item.dataset.emoji); + }); + }); + + // 画布接收拖放 + canvas.addEventListener('dragover', (e) => { + e.preventDefault(); + }); + + canvas.addEventListener('drop', (e) => { + e.preventDefault(); + const type = e.dataTransfer.getData('type'); + const emoji = e.dataTransfer.getData('emoji'); + + if (emoji) { + const rect = canvas.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + + addAssetToCanvas(emoji, x, y); + } + }); + + // 画布点击选择/移动 + let isDragging = false; + let dragStartX, dragStartY; + let selectedAsset = null; + + canvas.addEventListener('mousedown', (e) => { + const rect = canvas.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + + const frame = getCurrentFrame(); + if (!frame) return; + + // 查找点击的素材(从后往前,优先选上层的) + selectedAsset = null; + for (let i = frame.assets.length - 1; i >= 0; i--) { + const asset = frame.assets[i]; + const dist = Math.sqrt((x - asset.x) ** 2 + (y - asset.y) ** 2); + if (dist < 30) { + selectedAsset = asset; + // 更新选中状态 + frame.assets.forEach(a => a.selected = false); + asset.selected = true; + isDragging = true; + dragStartX = x - asset.x; + dragStartY = y - asset.y; + renderCanvas(); + break; + } + } + + // 如果没点到素材,取消所有选中 + if (!selectedAsset) { + frame.assets.forEach(a => a.selected = false); + renderCanvas(); + } + }); + + canvas.addEventListener('mousemove', (e) => { + if (!isDragging || !selectedAsset) return; + + const rect = canvas.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + + selectedAsset.x = x - dragStartX; + selectedAsset.y = y - dragStartY; + + // 边界限制 + selectedAsset.x = Math.max(30, Math.min(canvas.width - 30, selectedAsset.x)); + selectedAsset.y = Math.max(30, Math.min(canvas.height - 30, selectedAsset.y)); + + renderCanvas(); + }); + + canvas.addEventListener('mouseup', () => { + if (isDragging) { + isDragging = false; + saveToStorage(); + } + }); + + // 键盘删除 + document.addEventListener('keydown', (e) => { + if (e.key === 'Delete' || e.key === 'Backspace') { + const frame = getCurrentFrame(); + if (!frame) return; + + const selectedIndex = frame.assets.findIndex(a => a.selected); + if (selectedIndex !== -1) { + frame.assets.splice(selectedIndex, 1); + renderCanvas(); + saveToStorage(); + } + } + }); + + // 导入文件监听 + document.getElementById('import-file').addEventListener('change', handleImport); +} + +function addAssetToCanvas(emoji, x, y) { + const frame = getCurrentFrame(); + if (!frame) return; + + // 取消其他选中 + frame.assets.forEach(a => a.selected = false); + + const newAsset = { + id: appState.nextAssetId++, + type: 'emoji', + emoji: emoji, + x: x, + y: y, + selected: true + }; + + frame.assets.push(newAsset); + renderCanvas(); + saveToStorage(); +}// ==================== 导出导入 ==================== +function exportScene() { + const dataStr = JSON.stringify(appState, null, 2); + const blob = new Blob([dataStr], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + + const a = document.createElement('a'); + a.href = url; + a.download = `hololake-scene-${Date.now()}.json`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); +} + +function importScene() { + document.getElementById('import-file').click(); +} + +function handleImport(e) { + const file = e.target.files[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = (event) => { + try { + const imported = JSON.parse(event.target.result); + + // 验证数据结构 + if (!imported.scenes || !Array.isArray(imported.scenes)) { + throw new Error('无效的数据格式'); + } + + // 向后兼容处理 + imported.scenes = imported.scenes.map(scene => { + if (!scene.frames) { + return { + ...scene, + currentFrameIndex: 0, + frames: [{ + id: 1, + assets: scene.assets || [] + }], + nextFrameId: 2 + }; + } + return scene; + }); + + if (!imported.playback) { + imported.playback = { isPlaying: false, fps: 4, intervalId: null, currentFrameIndex: 0 }; + } + + appState = imported; + saveToStorage(); + renderSceneTabs(); + renderCanvas(); + renderTimeline(); + updatePlaybackControls(); + updateFrameCounter(); + + alert('导入成功!'); + } catch (err) { + alert('导入失败:' + err.message); + } + }; + reader.readAsText(file); + + // 清空 input,允许重复导入同一文件 + e.target.value = ''; +} + +// ==================== 截图导出 ==================== +function exportScreenshot() { + // 临时取消选中状态 + const frame = getCurrentFrame(); + if (!frame) return; + + const originalSelected = frame.assets.map(a => a.selected); + frame.assets.forEach(a => a.selected = false); + renderCanvas(); + + // 导出 PNG + const link = document.createElement('a'); + link.download = `hololake-frame-${Date.now()}.png`; + link.href = canvas.toDataURL(); + link.click(); + + // 恢复选中状态 + frame.assets.forEach((a, i) => { + a.selected = originalSelected[i]; + }); + renderCanvas(); +} + +// ==================== 启动应用 ==================== +document.addEventListener('DOMContentLoaded', init); + +// ==================== 环节6:预览与分享功能 ==================== + +// 预览模式状态 +let previewState = { + isPlaying: false, + currentFrameIndex: 0, + animationInterval: null, + fps: 12 +}; + +// 进入预览模式 +function enterPreviewMode() { + const container = document.getElementById('preview-container'); + const previewCanvas = document.getElementById('preview-canvas'); + + // 设置预览画布尺寸 + previewCanvas.width = canvas.width; + previewCanvas.height = canvas.height; + + // 显示预览容器 + container.style.display = 'block'; + + // 初始化预览状态 + previewState.currentFrameIndex = 0; + previewState.isPlaying = false; + + // 渲染第一帧 + renderPreviewFrame(); + + // 停止编辑器的动画 + stopAnimation(); +} + +// 退出预览模式 +function exitPreviewMode() { + const container = document.getElementById('preview-container'); + container.style.display = 'none'; + + // 停止预览动画 + previewStop(); +} + +// 渲染预览帧 + +// 在指定画布上绘制素材 + +// 预览播放控制 +function previewPlay() { + if (previewState.isPlaying) return; + + previewState.isPlaying = true; + const currentScene = scenes[currentSceneIndex]; + if (!currentScene) return; + + const frameInterval = 1000 / previewState.fps; + + previewState.animationInterval = setInterval(() => { + previewState.currentFrameIndex = (previewState.currentFrameIndex + 1) % currentScene.frames.length; + renderPreviewFrame(); + }, frameInterval); +} + +function previewPause() { + previewState.isPlaying = false; + if (previewState.animationInterval) { + clearInterval(previewState.animationInterval); + previewState.animationInterval = null; + } +} + +function previewStop() { + previewPause(); + previewState.currentFrameIndex = 0; + renderPreviewFrame(); +} + +// 生成分享链接 +function generateShareLink() { + // 序列化所有场景数据 + const exportData = { + version: '1.0', + scenes: scenes.map(scene => ({ + name: scene.name, + frames: scene.frames.map(frame => ({ + assets: frame.assets.map(asset => ({ + type: asset.type, + x: asset.x, + y: asset.y, + width: asset.width, + height: asset.height, + text: asset.text, + font: asset.font, + color: asset.color, + src: asset.src // 图片的dataURL + })) + })) + })), + exportTime: new Date().toISOString() + }; + + // 编码为JSON字符串 + const jsonStr = JSON.stringify(exportData); + + // 使用encodeURIComponent编码,适合URL + const encoded = encodeURIComponent(jsonStr); + + // 生成Data URL + const shareUrl = `${window.location.origin}${window.location.pathname}?data=${encoded}`; + + // 显示分享对话框 + showShareDialog(shareUrl, exportData); +} + +// 显示分享对话框 +function showShareDialog(url, data) { + // 创建对话框 + const dialog = document.createElement('div'); + dialog.style.cssText = ` + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0,0,0,0.8); + display: flex; + justify-content: center; + align-items: center; + z-index: 10000; + `; + + const content = document.createElement('div'); + content.style.cssText = ` + background: #2a2a4e; + padding: 30px; + border-radius: 10px; + max-width: 600px; + width: 90%; + color: #fff; + `; + + content.innerHTML = ` +

🔗 分享链接

+

复制下面的链接发给朋友,他们打开就能看到你的作品:

+ +
+ + +
+

+ 提示:链接包含完整作品数据,如果太长可能会被某些应用截断。建议导出HTML文件分享更稳定。 +

+ `; + + dialog.appendChild(content); + document.body.appendChild(dialog); + + // 保存对话框引用 + window.shareDialog = dialog; +} + +// 复制分享链接 +function copyShareUrl() { + const textarea = document.getElementById('share-url'); + textarea.select(); + document.execCommand('copy'); + alert('链接已复制到剪贴板!'); +} + +// 关闭分享对话框 +function closeShareDialog() { + if (window.shareDialog) { + window.shareDialog.remove(); + window.shareDialog = null; + } +} + +// 导出独立HTML +function exportHTML() { + // 准备作品数据 + const exportData = { + version: '1.0', + scenes: scenes.map(scene => ({ + name: scene.name, + frames: scene.frames.map(frame => ({ + assets: frame.assets.map(asset => ({ + type: asset.type, + x: asset.x, + y: asset.y, + width: asset.width, + height: asset.height, + text: asset.text, + font: asset.font, + color: asset.color, + src: asset.src + })) + })) + })), + exportTime: new Date().toISOString(), + exporter: 'HoloLake Dynamic Comic Studio' + }; + + // 生成完整的HTML文件内容 + const htmlContent = generateStandaloneHTML(exportData); + + // 创建下载 + const blob = new Blob([htmlContent], { type: 'text/html' }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.download = `hololake-comic-${Date.now()}.html`; + link.href = url; + link.click(); + + // 清理 + setTimeout(() => URL.revokeObjectURL(url), 1000); +} + +// 生成独立HTML文件内容 +function generateStandaloneHTML(data) { + return ` + + + + + HoloLake 动态漫 - ${data.scenes[0]?.name || '未命名作品'} + + + +

🎬 HoloLake 动态漫

+
+ +
+
+ + + +
+
+
+

作品:${data.scenes[0]?.name || '未命名'} | 场景数:${data.scenes.length} | 导出时间:${new Date(data.exportTime).toLocaleString()}

+

使用 HoloLake 动态漫制作系统创作

+
+ + + +`; +} + +// 检查URL参数,自动加载分享的数据 +function checkUrlParams() { + const params = new URLSearchParams(window.location.search); + const dataParam = params.get('data'); + + if (dataParam) { + try { + const data = JSON.parse(decodeURIComponent(dataParam)); + if (data.scenes) { + // 加载分享的数据 + scenes.length = 0; + data.scenes.forEach(sceneData => { + const newScene = { + name: sceneData.name, + frames: sceneData.frames.map(frameData => ({ + assets: frameData.assets.map(assetData => ({ + ...assetData, + selected: false, + img: null + })) + })) + }; + + // 重新加载图片 + newScene.frames.forEach(frame => { + frame.assets.forEach(asset => { + if (asset.type === 'image' && asset.src) { + const img = new Image(); + img.onload = () => { + asset.img = img; + renderCanvas(); + }; + img.src = asset.src; + } + }); + }); + + scenes.push(newScene); + }); + + currentSceneIndex = 0; + currentFrameIndex = 0; + updateSceneList(); + updateFrameList(); + renderCanvas(); + + alert('已加载分享的作品!'); + } + } catch (e) { + console.error('加载分享数据失败:', e); + } + + // 清除URL参数 + window.history.replaceState({}, document.title, window.location.pathname); + } +} + +// 页面加载时检查URL参数 +document.addEventListener('DOMContentLoaded', checkUrlParams); +function renderPreviewFrame() { + const previewCanvas = document.getElementById('preview-canvas'); + const ctx = previewCanvas.getContext('2d'); + const currentScene = scenes[currentSceneIndex]; + if (!currentScene || currentScene.frames.length === 0) return; + const frame = currentScene.frames[previewState.currentFrameIndex]; + ctx.fillStyle = '#1a1a2e'; + ctx.fillRect(0, 0, previewCanvas.width, previewCanvas.height); + frame.assets.forEach(asset => { + if (asset.type === 'image' && asset.img) { + ctx.drawImage(asset.img, asset.x, asset.y, asset.width, asset.height); + } else if (asset.type === 'text') { + ctx.font = asset.font || '20px Arial'; + ctx.fillStyle = asset.color || '#ffffff'; + ctx.fillText(asset.text, asset.x, asset.y); + } + }); +} + +// ==================== 修复版预览功能 ==================== +function renderPreviewFrame() { + try { + const previewCanvas = document.getElementById('preview-canvas'); + if (!previewCanvas) return; + const ctx = previewCanvas.getContext('2d'); + const currentScene = scenes[currentSceneIndex]; + if (!currentScene || !currentScene.frames || currentScene.frames.length === 0) return; + + const frame = currentScene.frames[previewState.currentFrameIndex]; + if (!frame || !frame.assets) return; + + // 清空画布 + ctx.fillStyle = '#1a1a2e'; + ctx.fillRect(0, 0, previewCanvas.width, previewCanvas.height); + + // 绘制所有素材 + frame.assets.forEach(asset => { + if (asset.type === 'image' && asset.img && asset.img.complete) { + ctx.drawImage(asset.img, asset.x, asset.y, asset.width, asset.height); + } else if (asset.type === 'text') { + ctx.font = asset.font || '20px Arial'; + ctx.fillStyle = asset.color || '#ffffff'; + ctx.fillText(asset.text, asset.x, asset.y); + } + }); + } catch (e) { + console.error('预览渲染错误:', e); + } +} diff --git a/dynamic-comic-studio/js/app.js.backup2 b/dynamic-comic-studio/js/app.js.backup2 new file mode 100644 index 00000000..14443565 --- /dev/null +++ b/dynamic-comic-studio/js/app.js.backup2 @@ -0,0 +1,1029 @@ +// ==================== HoloLake 动态漫制作系统 v5.0 ==================== +// 环节5:动画时间轴(帧序列 + 播放控制 + 帧率调节) + +const STORAGE_KEY = 'hololake-comic-studio-data'; +const canvas = document.getElementById('main-canvas'); +const ctx = canvas.getContext('2d'); + +// ==================== 全局状态 ==================== +let appState = { + currentSceneId: 1, + nextSceneId: 2, + nextAssetId: 1, + nextFrameId: 1, + scenes: [], + playback: { + isPlaying: false, + fps: 4, + intervalId: null, + currentFrameIndex: 0 + } +}; + +// ==================== 初始化 ==================== +function init() { + loadFromStorage(); + if (appState.scenes.length === 0) { + createDefaultScene(); + } + setupEventListeners(); + renderSceneTabs(); + renderCanvas(); + renderTimeline(); + updatePlaybackControls(); +} + +// 创建默认场景 +function createDefaultScene() { + const defaultScene = { + id: 1, + name: '场景 1', + currentFrameIndex: 0, + frames: [{ + id: 1, + assets: [] + }], + nextFrameId: 2 + }; + appState.scenes.push(defaultScene); + appState.currentSceneId = 1; + saveToStorage(); +} + +// ==================== 存储管理 ==================== +function saveToStorage() { + localStorage.setItem(STORAGE_KEY, JSON.stringify(appState)); +} + +function loadFromStorage() { + const data = localStorage.getItem(STORAGE_KEY); + if (data) { + const parsed = JSON.parse(data); + // 向后兼容:旧数据没有 frames 字段 + if (parsed.scenes && parsed.scenes.length > 0 && !parsed.scenes[0].frames) { + parsed.scenes = parsed.scenes.map(scene => ({ + ...scene, + currentFrameIndex: 0, + frames: [{ + id: 1, + assets: scene.assets || [] + }], + nextFrameId: 2 + })); + delete parsed.scenes[0].assets; // 删除旧的 assets 字段 + } + // 确保 playback 对象存在 + if (!parsed.playback) { + parsed.playback = { isPlaying: false, fps: 4, intervalId: null, currentFrameIndex: 0 }; + } + appState = parsed; + } +} + +// ==================== 场景管理 ==================== +function addScene() { + const newScene = { + id: appState.nextSceneId++, + name: `场景 ${appState.scenes.length + 1}`, + currentFrameIndex: 0, + frames: [{ + id: 1, + assets: [] + }], + nextFrameId: 2 + }; + appState.scenes.push(newScene); + appState.currentSceneId = newScene.id; + saveToStorage(); + renderSceneTabs(); + renderCanvas(); + renderTimeline(); +} + +function switchScene(sceneId) { + if (appState.playback.isPlaying) { + stopAnimation(); + } + appState.currentSceneId = sceneId; + saveToStorage(); + renderSceneTabs(); + renderCanvas(); + renderTimeline(); + updateFrameCounter(); +} + +function deleteScene(sceneId) { + if (appState.scenes.length <= 1) { + alert('至少保留一个场景!'); + return; + } + appState.scenes = appState.scenes.filter(s => s.id !== sceneId); + if (appState.currentSceneId === sceneId) { + appState.currentSceneId = appState.scenes[0].id; + } + saveToStorage(); + renderSceneTabs(); + renderCanvas(); + renderTimeline(); +} + +function getCurrentScene() { + return appState.scenes.find(s => s.id === appState.currentSceneId); +} + +function getCurrentFrame() { + const scene = getCurrentScene(); + if (!scene) return null; + return scene.frames[scene.currentFrameIndex]; +}// ==================== 帧管理 ==================== +function addFrame() { + const scene = getCurrentScene(); + if (!scene) return; + + // 深拷贝当前帧的素材状态 + const currentFrame = scene.frames[scene.currentFrameIndex]; + const newFrame = { + id: scene.nextFrameId++, + assets: JSON.parse(JSON.stringify(currentFrame.assets)) + }; + + // 在当前帧后插入新帧 + scene.frames.splice(scene.currentFrameIndex + 1, 0, newFrame); + scene.currentFrameIndex++; + + saveToStorage(); + renderTimeline(); + renderCanvas(); + updateFrameCounter(); +} + +function deleteFrame() { + const scene = getCurrentScene(); + if (!scene) return; + + if (scene.frames.length <= 1) { + alert('至少保留一帧!'); + return; + } + + scene.frames.splice(scene.currentFrameIndex, 1); + if (scene.currentFrameIndex >= scene.frames.length) { + scene.currentFrameIndex = scene.frames.length - 1; + } + + saveToStorage(); + renderTimeline(); + renderCanvas(); + updateFrameCounter(); +} + +function switchFrame(frameIndex) { + const scene = getCurrentScene(); + if (!scene || frameIndex < 0 || frameIndex >= scene.frames.length) return; + + // 保存当前画布状态到当前帧 + captureFrame(); + + scene.currentFrameIndex = frameIndex; + saveToStorage(); + renderTimeline(); + renderCanvas(); + updateFrameCounter(); +} + +function captureFrame() { + // 画布状态实时保存在 assets 中,无需额外操作 + saveToStorage(); +} + +// ==================== 播放引擎 ==================== +function playAnimation() { + const scene = getCurrentScene(); + if (!scene || scene.frames.length <= 1) return; + + appState.playback.isPlaying = true; + updatePlaybackControls(); + + const intervalMs = 1000 / appState.playback.fps; + + appState.playback.intervalId = setInterval(() => { + const scene = getCurrentScene(); + if (!scene) return; + + scene.currentFrameIndex++; + if (scene.currentFrameIndex >= scene.frames.length) { + scene.currentFrameIndex = 0; // 循环播放 + } + + renderCanvas(); + renderTimeline(); + updateFrameCounter(); + saveToStorage(); + }, intervalMs); +} + +function pauseAnimation() { + appState.playback.isPlaying = false; + if (appState.playback.intervalId) { + clearInterval(appState.playback.intervalId); + appState.playback.intervalId = null; + } + updatePlaybackControls(); +} + +function stopAnimation() { + pauseAnimation(); + const scene = getCurrentScene(); + if (scene) { + scene.currentFrameIndex = 0; + renderCanvas(); + renderTimeline(); + updateFrameCounter(); + saveToStorage(); + } +} + +function togglePlayback() { + if (appState.playback.isPlaying) { + pauseAnimation(); + } else { + playAnimation(); + } +} + +function setFPS(fps) { + appState.playback.fps = parseInt(fps); + document.getElementById('fps-display').textContent = fps + ' FPS'; + saveToStorage(); + + // 如果正在播放,重启定时器以应用新帧率 + if (appState.playback.isPlaying) { + pauseAnimation(); + playAnimation(); + } +} + +function updatePlaybackControls() { + const btnPlay = document.getElementById('btn-play'); + if (appState.playback.isPlaying) { + btnPlay.textContent = '⏸️'; + btnPlay.title = '暂停'; + } else { + btnPlay.textContent = '▶️'; + btnPlay.title = '播放'; + } +} + +function updateFrameCounter() { + const scene = getCurrentScene(); + if (!scene) return; + const counter = document.getElementById('frame-counter'); + counter.textContent = `帧 ${scene.currentFrameIndex + 1}/${scene.frames.length}`; +}// ==================== 渲染函数 ==================== +function renderSceneTabs() { + const tabsList = document.getElementById('tabs-list'); + tabsList.innerHTML = ''; + + appState.scenes.forEach(scene => { + const tab = document.createElement('div'); + tab.className = 'scene-tab' + (scene.id === appState.currentSceneId ? ' active' : ''); + tab.innerHTML = ` + ${scene.name} + × + `; + tab.onclick = () => switchScene(scene.id); + tabsList.appendChild(tab); + }); +} + +function renderTimeline() { + const timelineFrames = document.getElementById('timeline-frames'); + timelineFrames.innerHTML = ''; + + const scene = getCurrentScene(); + if (!scene) return; + + scene.frames.forEach((frame, index) => { + const thumb = document.createElement('div'); + thumb.className = 'frame-thumb' + (index === scene.currentFrameIndex ? ' active' : ''); + thumb.setAttribute('data-frame', index + 1); + thumb.textContent = `帧 ${index + 1}`; + thumb.onclick = () => switchFrame(index); + timelineFrames.appendChild(thumb); + }); +} + +function renderCanvas() { + // 清空画布 + ctx.fillStyle = '#ffffff'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + const frame = getCurrentFrame(); + if (!frame) return; + + // 绘制所有素材 + frame.assets.forEach(asset => { + ctx.font = '48px Arial'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + + // 绘制阴影 + ctx.shadowColor = 'rgba(0,0,0,0.3)'; + ctx.shadowBlur = 4; + ctx.shadowOffsetX = 2; + ctx.shadowOffsetY = 2; + + ctx.fillText(asset.emoji, asset.x, asset.y); + + // 重置阴影 + ctx.shadowColor = 'transparent'; + ctx.shadowBlur = 0; + ctx.shadowOffsetX = 0; + ctx.shadowOffsetY = 0; + + // 如果是选中状态,绘制边框 + if (asset.selected) { + ctx.strokeStyle = '#e94560'; + ctx.lineWidth = 2; + ctx.setLineDash([5, 5]); + ctx.strokeRect(asset.x - 30, asset.y - 30, 60, 60); + ctx.setLineDash([]); + } + }); +} + +// ==================== 素材拖放 ==================== +function setupEventListeners() { + // 素材库拖放 + const assetItems = document.querySelectorAll('.asset-item'); + assetItems.forEach(item => { + item.addEventListener('dragstart', (e) => { + e.dataTransfer.setData('type', item.dataset.type); + e.dataTransfer.setData('emoji', item.dataset.emoji); + }); + }); + + // 画布接收拖放 + canvas.addEventListener('dragover', (e) => { + e.preventDefault(); + }); + + canvas.addEventListener('drop', (e) => { + e.preventDefault(); + const type = e.dataTransfer.getData('type'); + const emoji = e.dataTransfer.getData('emoji'); + + if (emoji) { + const rect = canvas.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + + addAssetToCanvas(emoji, x, y); + } + }); + + // 画布点击选择/移动 + let isDragging = false; + let dragStartX, dragStartY; + let selectedAsset = null; + + canvas.addEventListener('mousedown', (e) => { + const rect = canvas.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + + const frame = getCurrentFrame(); + if (!frame) return; + + // 查找点击的素材(从后往前,优先选上层的) + selectedAsset = null; + for (let i = frame.assets.length - 1; i >= 0; i--) { + const asset = frame.assets[i]; + const dist = Math.sqrt((x - asset.x) ** 2 + (y - asset.y) ** 2); + if (dist < 30) { + selectedAsset = asset; + // 更新选中状态 + frame.assets.forEach(a => a.selected = false); + asset.selected = true; + isDragging = true; + dragStartX = x - asset.x; + dragStartY = y - asset.y; + renderCanvas(); + break; + } + } + + // 如果没点到素材,取消所有选中 + if (!selectedAsset) { + frame.assets.forEach(a => a.selected = false); + renderCanvas(); + } + }); + + canvas.addEventListener('mousemove', (e) => { + if (!isDragging || !selectedAsset) return; + + const rect = canvas.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + + selectedAsset.x = x - dragStartX; + selectedAsset.y = y - dragStartY; + + // 边界限制 + selectedAsset.x = Math.max(30, Math.min(canvas.width - 30, selectedAsset.x)); + selectedAsset.y = Math.max(30, Math.min(canvas.height - 30, selectedAsset.y)); + + renderCanvas(); + }); + + canvas.addEventListener('mouseup', () => { + if (isDragging) { + isDragging = false; + saveToStorage(); + } + }); + + // 键盘删除 + document.addEventListener('keydown', (e) => { + if (e.key === 'Delete' || e.key === 'Backspace') { + const frame = getCurrentFrame(); + if (!frame) return; + + const selectedIndex = frame.assets.findIndex(a => a.selected); + if (selectedIndex !== -1) { + frame.assets.splice(selectedIndex, 1); + renderCanvas(); + saveToStorage(); + } + } + }); + + // 导入文件监听 + document.getElementById('import-file').addEventListener('change', handleImport); +} + +function addAssetToCanvas(emoji, x, y) { + const frame = getCurrentFrame(); + if (!frame) return; + + // 取消其他选中 + frame.assets.forEach(a => a.selected = false); + + const newAsset = { + id: appState.nextAssetId++, + type: 'emoji', + emoji: emoji, + x: x, + y: y, + selected: true + }; + + frame.assets.push(newAsset); + renderCanvas(); + saveToStorage(); +}// ==================== 导出导入 ==================== +function exportScene() { + const dataStr = JSON.stringify(appState, null, 2); + const blob = new Blob([dataStr], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + + const a = document.createElement('a'); + a.href = url; + a.download = `hololake-scene-${Date.now()}.json`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); +} + +function importScene() { + document.getElementById('import-file').click(); +} + +function handleImport(e) { + const file = e.target.files[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = (event) => { + try { + const imported = JSON.parse(event.target.result); + + // 验证数据结构 + if (!imported.scenes || !Array.isArray(imported.scenes)) { + throw new Error('无效的数据格式'); + } + + // 向后兼容处理 + imported.scenes = imported.scenes.map(scene => { + if (!scene.frames) { + return { + ...scene, + currentFrameIndex: 0, + frames: [{ + id: 1, + assets: scene.assets || [] + }], + nextFrameId: 2 + }; + } + return scene; + }); + + if (!imported.playback) { + imported.playback = { isPlaying: false, fps: 4, intervalId: null, currentFrameIndex: 0 }; + } + + appState = imported; + saveToStorage(); + renderSceneTabs(); + renderCanvas(); + renderTimeline(); + updatePlaybackControls(); + updateFrameCounter(); + + alert('导入成功!'); + } catch (err) { + alert('导入失败:' + err.message); + } + }; + reader.readAsText(file); + + // 清空 input,允许重复导入同一文件 + e.target.value = ''; +} + +// ==================== 截图导出 ==================== +function exportScreenshot() { + // 临时取消选中状态 + const frame = getCurrentFrame(); + if (!frame) return; + + const originalSelected = frame.assets.map(a => a.selected); + frame.assets.forEach(a => a.selected = false); + renderCanvas(); + + // 导出 PNG + const link = document.createElement('a'); + link.download = `hololake-frame-${Date.now()}.png`; + link.href = canvas.toDataURL(); + link.click(); + + // 恢复选中状态 + frame.assets.forEach((a, i) => { + a.selected = originalSelected[i]; + }); + renderCanvas(); +} + +// ==================== 启动应用 ==================== +document.addEventListener('DOMContentLoaded', init); + + +// ==================== 环节6: 在线预览与分享 ==================== + +// 预览模式状态 +let previewState = { + isPlaying: false, + intervalId: null, + currentFrameIndex: 0, + fps: 12 +}; + +// ==================== 预览模式功能 ==================== + +function enterPreviewMode() { + const previewMode = document.getElementById('previewMode'); + const previewCanvas = document.getElementById('previewCanvas'); + const mainCanvas = document.getElementById('main-canvas'); + + // 复制画布尺寸 + previewCanvas.width = mainCanvas.width; + previewCanvas.height = mainCanvas.height; + + // 显示预览模式 + previewMode.classList.remove('hidden'); + + // 渲染当前场景第一帧 + renderPreviewFrame(0); + + console.log('[预览模式] 已进入预览'); +} + +function exitPreviewMode() { + const previewMode = document.getElementById('previewMode'); + + // 停止播放 + stopPreview(); + + // 隐藏预览模式 + previewMode.classList.add('hidden'); + + console.log('[预览模式] 已退出'); +} + +function renderPreviewFrame(frameIndex) { + const previewCanvas = document.getElementById('previewCanvas'); + const ctx = previewCanvas.getContext('2d'); + const currentScene = appState.scenes.find(s => s.id === appState.currentSceneId); + + if (!currentScene || !currentScene.frames || currentScene.frames.length === 0) { + // 清空画布 + ctx.fillStyle = '#f0f0f0'; + ctx.fillRect(0, 0, previewCanvas.width, previewCanvas.height); + ctx.fillStyle = '#999'; + ctx.font = '20px Arial'; + ctx.textAlign = 'center'; + ctx.fillText('暂无帧数据', previewCanvas.width / 2, previewCanvas.height / 2); + return; + } + + const frame = currentScene.frames[frameIndex]; + if (!frame) return; + + // 清空画布 + ctx.fillStyle = '#f0f0f0'; + ctx.fillRect(0, 0, previewCanvas.width, previewCanvas.height); + + // 绘制背景 + if (frame.background) { + ctx.fillStyle = frame.background; + ctx.fillRect(0, 0, previewCanvas.width, previewCanvas.height); + } + + // 绘制素材 + if (frame.assets) { + frame.assets.forEach(asset => { + drawAssetOnPreview(ctx, asset); + }); + } +} + +function drawAssetOnPreview(ctx, asset) { + ctx.save(); + ctx.translate(asset.x + asset.width / 2, asset.y + asset.height / 2); + ctx.rotate((asset.rotation || 0) * Math.PI / 180); + ctx.scale(asset.scaleX || 1, asset.scaleY || 1); + + if (asset.type === 'image' && asset.data) { + const img = new Image(); + img.onload = () => { + ctx.drawImage(img, -asset.width / 2, -asset.height / 2, asset.width, asset.height); + }; + img.src = asset.data; + } else if (asset.type === 'text') { + ctx.fillStyle = asset.color || '#000'; + ctx.font = `${asset.fontSize || 20}px Arial`; + ctx.textAlign = 'center'; + ctx.fillText(asset.text, 0, 0); + } + + ctx.restore(); +} + +function playPreview() { + if (previewState.isPlaying) return; + + const currentScene = appState.scenes.find(s => s.id === appState.currentSceneId); + if (!currentScene || !currentScene.frames || currentScene.frames.length === 0) { + alert('当前场景没有帧,无法播放'); + return; + } + + previewState.isPlaying = true; + const fps = parseInt(document.getElementById('previewFpsValue').textContent) || 12; + const interval = 1000 / fps; + + previewState.intervalId = setInterval(() => { + const frameCount = currentScene.frames.length; + previewState.currentFrameIndex = (previewState.currentFrameIndex + 1) % frameCount; + renderPreviewFrame(previewState.currentFrameIndex); + }, interval); + + console.log('[预览模式] 开始播放,FPS:', fps); +} + +function pausePreview() { + if (!previewState.isPlaying) return; + + previewState.isPlaying = false; + if (previewState.intervalId) { + clearInterval(previewState.intervalId); + previewState.intervalId = null; + } + + console.log('[预览模式] 已暂停'); +} + +function stopPreview() { + previewState.isPlaying = false; + if (previewState.intervalId) { + clearInterval(previewState.intervalId); + previewState.intervalId = null; + } + previewState.currentFrameIndex = 0; + renderPreviewFrame(0); + + console.log('[预览模式] 已停止'); +} + +// ==================== 分享功能 ==================== + +function openSharePanel() { + const sharePanel = document.getElementById('sharePanel'); + sharePanel.classList.remove('hidden'); + + // 默认生成分享链接 + generateShareLink(); +} + +function closeSharePanel() { + const sharePanel = document.getElementById('sharePanel'); + sharePanel.classList.add('hidden'); +} + +function generateShareLink() { + const shareData = { + version: '1.0', + scenes: appState.scenes, + timestamp: Date.now() + }; + + // 序列化为JSON并编码 + const jsonStr = JSON.stringify(shareData); + const encoded = btoa(encodeURIComponent(jsonStr)); + + // 生成Data URL + const dataUrl = `${window.location.origin}${window.location.pathname}?data=${encoded}`; + + document.getElementById('shareLink').value = dataUrl; + console.log('[分享] 链接已生成'); +} + +function copyShareLink() { + const shareLink = document.getElementById('shareLink'); + shareLink.select(); + document.execCommand('copy'); + + // 显示复制成功提示 + const btn = document.getElementById('copyLinkBtn'); + const originalText = btn.textContent; + btn.textContent = '✅ 已复制'; + setTimeout(() => { + btn.textContent = originalText; + }, 2000); + + console.log('[分享] 链接已复制'); +} + +function generateEmbedCode() { + const shareData = { + version: '1.0', + scenes: appState.scenes, + timestamp: Date.now() + }; + + const jsonStr = JSON.stringify(shareData); + const encoded = btoa(encodeURIComponent(jsonStr)); + const dataUrl = `${window.location.origin}${window.location.pathname}?data=${encoded}`; + + const embedCode = ``; + + document.getElementById('embedCode').value = embedCode; + console.log('[分享] 嵌入代码已生成'); +} + +function copyEmbedCode() { + const embedCode = document.getElementById('embedCode'); + embedCode.select(); + document.execCommand('copy'); + + const btn = document.getElementById('copyEmbedBtn'); + const originalText = btn.textContent; + btn.textContent = '✅ 已复制'; + setTimeout(() => { + btn.textContent = originalText; + }, 2000); + + console.log('[分享] 嵌入代码已复制'); +} + +function exportHtmlFile() { + const shareData = { + version: '1.0', + scenes: appState.scenes, + timestamp: Date.now() + }; + + const jsonStr = JSON.stringify(shareData); + const encoded = btoa(encodeURIComponent(jsonStr)); + + // 生成完整的独立HTML + const htmlContent = ` + + + + + 动态漫作品 - HoloLake Studio + + + +

🎬 动态漫作品

+ +
+ + + + FPS: 12 +
+ + +`; +} + +// 检查URL参数,自动加载分享的数据 +function checkUrlParams() { + const params = new URLSearchParams(window.location.search); + const dataParam = params.get('data'); + + if (dataParam) { + try { + const data = JSON.parse(decodeURIComponent(dataParam)); + if (data.scenes) { + // 加载分享的数据 + scenes.length = 0; + data.scenes.forEach(sceneData => { + const newScene = { + name: sceneData.name, + frames: sceneData.frames.map(frameData => ({ + assets: frameData.assets.map(assetData => ({ + ...assetData, + selected: false, + img: null + })) + })) + }; + + // 重新加载图片 + newScene.frames.forEach(frame => { + frame.assets.forEach(asset => { + if (asset.type === 'image' && asset.src) { + const img = new Image(); + img.onload = () => { + asset.img = img; + renderCanvas(); + }; + img.src = asset.src; + } + }); + }); + + scenes.push(newScene); + }); + + currentSceneIndex = 0; + currentFrameIndex = 0; + updateSceneList(); + updateFrameList(); + renderCanvas(); + + alert('已加载分享的作品!'); + } + } catch (e) { + console.error('加载分享数据失败:', e); + } + + // 清除URL参数 + window.history.replaceState({}, document.title, window.location.pathname); + } +} + +// 页面加载时检查URL参数 +document.addEventListener('DOMContentLoaded', checkUrlParams); +function renderPreviewFrame() { + const previewCanvas = document.getElementById('preview-canvas'); + const ctx = previewCanvas.getContext('2d'); + const currentScene = scenes[currentSceneIndex]; + if (!currentScene || currentScene.frames.length === 0) return; + const frame = currentScene.frames[previewState.currentFrameIndex]; + ctx.fillStyle = '#1a1a2e'; + ctx.fillRect(0, 0, previewCanvas.width, previewCanvas.height); + frame.assets.forEach(asset => { + if (asset.type === 'image' && asset.img) { + ctx.drawImage(asset.img, asset.x, asset.y, asset.width, asset.height); + } else if (asset.type === 'text') { + ctx.font = asset.font || '20px Arial'; + ctx.fillStyle = asset.color || '#ffffff'; + ctx.fillText(asset.text, asset.x, asset.y); + } + }); +} + +// ==================== 修复版预览功能 ==================== +function renderPreviewFrame() { + try { + const previewCanvas = document.getElementById('preview-canvas'); + if (!previewCanvas) return; + const ctx = previewCanvas.getContext('2d'); + const currentScene = scenes[currentSceneIndex]; + if (!currentScene || !currentScene.frames || currentScene.frames.length === 0) return; + + const frame = currentScene.frames[previewState.currentFrameIndex]; + if (!frame || !frame.assets) return; + + // 清空画布 + ctx.fillStyle = '#1a1a2e'; + ctx.fillRect(0, 0, previewCanvas.width, previewCanvas.height); + + // 绘制所有素材 + frame.assets.forEach(asset => { + if (asset.type === 'image' && asset.img && asset.img.complete) { + ctx.drawImage(asset.img, asset.x, asset.y, asset.width, asset.height); + } else if (asset.type === 'text') { + ctx.font = asset.font || '20px Arial'; + ctx.fillStyle = asset.color || '#ffffff'; + ctx.fillText(asset.text, asset.x, asset.y); + } + }); + } catch (e) { + console.error('预览渲染错误:', e); + } +} diff --git a/dynamic-comic-studio/js/app.js.save b/dynamic-comic-studio/js/app.js.save new file mode 100644 index 00000000..fb080d54 --- /dev/null +++ b/dynamic-comic-studio/js/app.js.save @@ -0,0 +1,584 @@ +// ==================== HoloLake 动态漫制作系统 v5.0 ==================== +// 环节5:动画时间轴(帧序列 + 播放控制 + 帧率调节) + +const STORAGE_KEY = 'hololake-comic-studio-data'; +const canvas = document.getElementById('main-canvas'); +const ctx = canvas.getContext('2d'); + +// ==================== 全局状态 ==================== +let appState = { + currentSceneId: 1, + nextSceneId: 2, + nextAssetId: 1, + nextFrameId: 1, + scenes: [], + playback: { + isPlaying: false, + fps: 4, + intervalId: null, + + currentFrameIndex: 0 + } +}; + +// ==================== 初始化 ==================== +function init() { + loadFromStorage(); + if (appState.scenes.length === 0) { + createDefaultScene(); + } + setupEventListeners(); + renderSceneTabs(); + renderCanvas(); + renderTimeline(); + updatePlaybackControls(); +} + +// 创建默认场景 +function createDefaultScene() { + const defaultScene = { + id: 1, + name: '场景 1', + currentFrameIndex: 0, + frames: [{ + id: 1, + assets: [] + }], + nextFrameId: 2 + }; + appState.scenes.push(defaultScene); + appState.currentSceneId = 1; + saveToStorage(); +} + +// ==================== 存储管理 ==================== +function saveToStorage() { + localStorage.setItem(STORAGE_KEY, JSON.stringify(appState)); +} + +function loadFromStorage() { + const data = localStorage.getItem(STORAGE_KEY); + if (data) { + const parsed = JSON.parse(data); + // 向后兼容:旧数据没有 frames 字段 + if (parsed.scenes && parsed.scenes.length > 0 && !parsed.scenes[0].frames) { + parsed.scenes = parsed.scenes.map(scene => ({ + ...scene, + currentFrameIndex: 0, + frames: [{ + id: 1, + assets: scene.assets || [] + }], + nextFrameId: 2 + })); + delete parsed.scenes[0].assets; // 删除旧的 assets 字段 + } + // 确保 playback 对象存在 + if (!parsed.playback) { + parsed.playback = { isPlaying: false, fps: 4, intervalId: null, currentFrameIndex: 0 }; + } + appState = parsed; + } +} + +// ==================== 场景管理 ==================== +function addScene() { + const newScene = { + id: appState.nextSceneId++, + name: `场景 ${appState.scenes.length + 1}`, + currentFrameIndex: 0, + frames: [{ + id: 1, + assets: [] + }], + nextFrameId: 2 + }; + appState.scenes.push(newScene); + appState.currentSceneId = newScene.id; + saveToStorage(); + renderSceneTabs(); + renderCanvas(); + renderTimeline(); +} + +function switchScene(sceneId) { + if (appState.playback.isPlaying) { + stopAnimation(); + } + appState.currentSceneId = sceneId; + saveToStorage(); + renderSceneTabs(); + renderCanvas(); + renderTimeline(); + updateFrameCounter(); +} + +function deleteScene(sceneId) { + if (appState.scenes.length <= 1) { + alert('至少保留一个场景!'); + return; + } + appState.scenes = appState.scenes.filter(s => s.id !== sceneId); + if (appState.currentSceneId === sceneId) { + appState.currentSceneId = appState.scenes[0].id; + } + saveToStorage(); + renderSceneTabs(); + renderCanvas(); + renderTimeline(); +} + +function getCurrentScene() { + return appState.scenes.find(s => s.id === appState.currentSceneId); +} + +function getCurrentFrame() { + const scene = getCurrentScene(); + if (!scene) return null; + return scene.frames[scene.currentFrameIndex]; +}// ==================== 帧管理 ==================== +function addFrame() { + const scene = getCurrentScene(); + if (!scene) return; + + // 深拷贝当前帧的素材状态 + const currentFrame = scene.frames[scene.currentFrameIndex]; + const newFrame = { + id: scene.nextFrameId++, + assets: JSON.parse(JSON.stringify(currentFrame.assets)) + }; + + // 在当前帧后插入新帧 + scene.frames.splice(scene.currentFrameIndex + 1, 0, newFrame); + scene.currentFrameIndex++; + + saveToStorage(); + renderTimeline(); + renderCanvas(); + updateFrameCounter(); +} + +function deleteFrame() { + const scene = getCurrentScene(); + if (!scene) return; + + if (scene.frames.length <= 1) { + alert('至少保留一帧!'); + return; + } + + scene.frames.splice(scene.currentFrameIndex, 1); + if (scene.currentFrameIndex >= scene.frames.length) { + scene.currentFrameIndex = scene.frames.length - 1; + } + + saveToStorage(); + renderTimeline(); + renderCanvas(); + updateFrameCounter(); +} + +function switchFrame(frameIndex) { + const scene = getCurrentScene(); + if (!scene || frameIndex < 0 || frameIndex >= scene.frames.length) return; + + // 保存当前画布状态到当前帧 + captureFrame(); + + scene.currentFrameIndex = frameIndex; + saveToStorage(); + renderTimeline(); + renderCanvas(); + updateFrameCounter(); +} + +function captureFrame() { + // 画布状态实时保存在 assets 中,无需额外操作 + saveToStorage(); +} + +// ==================== 播放引擎 ==================== +function playAnimation() { + const scene = getCurrentScene(); + if (!scene || scene.frames.length <= 1) return; + + appState.playback.isPlaying = true; + updatePlaybackControls(); + + const intervalMs = 1000 / appState.playback.fps; + + appState.playback.intervalId = setInterval(() => { + const scene = getCurrentScene(); + if (!scene) return; + + scene.currentFrameIndex++; + if (scene.currentFrameIndex >= scene.frames.length) { + scene.currentFrameIndex = 0; // 循环播放 + } + + renderCanvas(); + renderTimeline(); + updateFrameCounter(); + saveToStorage(); + }, intervalMs); +} + +function pauseAnimation() { + appState.playback.isPlaying = false; + if (appState.playback.intervalId) { + clearInterval(appState.playback.intervalId); + appState.playback.intervalId = null; + } + updatePlaybackControls(); +} + +function stopAnimation() { + pauseAnimation(); + const scene = getCurrentScene(); + if (scene) { + scene.currentFrameIndex = 0; + renderCanvas(); + renderTimeline(); + updateFrameCounter(); + saveToStorage(); + } +} + +function togglePlayback() { + if (appState.playback.isPlaying) { + pauseAnimation(); + } else { + playAnimation(); + } +} + +function setFPS(fps) { + appState.playback.fps = parseInt(fps); + document.getElementById('fps-display').textContent = fps + ' FPS'; + saveToStorage(); + + // 如果正在播放,重启定时器以应用新帧率 + if (appState.playback.isPlaying) { + pauseAnimation(); + playAnimation(); + } +} + +function updatePlaybackControls() { + const btnPlay = document.getElementById('btn-play'); + if (appState.playback.isPlaying) { + btnPlay.textContent = '⏸️'; + btnPlay.title = '暂停'; + } else { + btnPlay.textContent = '▶️'; + btnPlay.title = '播放'; + } +} + +function updateFrameCounter() { + const scene = getCurrentScene(); + if (!scene) return; + const counter = document.getElementById('frame-counter'); + counter.textContent = `帧 ${scene.currentFrameIndex + 1}/${scene.frames.length}`; +}// ==================== 渲染函数 ==================== +function renderSceneTabs() { + const tabsList = document.getElementById('tabs-list'); + tabsList.innerHTML = ''; + + appState.scenes.forEach(scene => { + const tab = document.createElement('div'); + tab.className = 'scene-tab' + (scene.id === appState.currentSceneId ? ' active' : ''); + tab.innerHTML = ` + ${scene.name} + × + `; + tab.onclick = () => switchScene(scene.id); + tabsList.appendChild(tab); + }); +} + +function renderTimeline() { + const timelineFrames = document.getElementById('timeline-frames'); + timelineFrames.innerHTML = ''; + + const scene = getCurrentScene(); + if (!scene) return; + + scene.frames.forEach((frame, index) => { + const thumb = document.createElement('div'); + thumb.className = 'frame-thumb' + (index === scene.currentFrameIndex ? ' active' : ''); + thumb.setAttribute('data-frame', index + 1); + thumb.textContent = `帧 ${index + 1}`; + thumb.onclick = () => switchFrame(index); + timelineFrames.appendChild(thumb); + }); +} + +function renderCanvas() { + // 清空画布 + ctx.fillStyle = '#ffffff'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + const frame = getCurrentFrame(); + if (!frame) return; + + // 绘制所有素材 + frame.assets.forEach(asset => { + ctx.font = '48px Arial'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + + // 绘制阴影 + ctx.shadowColor = 'rgba(0,0,0,0.3)'; + ctx.shadowBlur = 4; + ctx.shadowOffsetX = 2; + ctx.shadowOffsetY = 2; + + ctx.fillText(asset.emoji, asset.x, asset.y); + + // 重置阴影 + ctx.shadowColor = 'transparent'; + ctx.shadowBlur = 0; + ctx.shadowOffsetX = 0; + ctx.shadowOffsetY = 0; + + // 如果是选中状态,绘制边框 + if (asset.selected) { + ctx.strokeStyle = '#e94560'; + ctx.lineWidth = 2; + ctx.setLineDash([5, 5]); + ctx.strokeRect(asset.x - 30, asset.y - 30, 60, 60); + ctx.setLineDash([]); + } + }); +} + +// ==================== 素材拖放 ==================== +function setupEventListeners() { + // 素材库拖放 + const assetItems = document.querySelectorAll('.asset-item'); + assetItems.forEach(item => { + item.addEventListener('dragstart', (e) => { + e.dataTransfer.setData('type', item.dataset.type); + e.dataTransfer.setData('emoji', item.dataset.emoji); + }); + }); + + // 画布接收拖放 + canvas.addEventListener('dragover', (e) => { + e.preventDefault(); + }); + + canvas.addEventListener('drop', (e) => { + e.preventDefault(); + const type = e.dataTransfer.getData('type'); + const emoji = e.dataTransfer.getData('emoji'); + + if (emoji) { + const rect = canvas.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + + addAssetToCanvas(emoji, x, y); + } + }); + + // 画布点击选择/移动 + let isDragging = false; + let dragStartX, dragStartY; + let selectedAsset = null; + + canvas.addEventListener('mousedown', (e) => { + const rect = canvas.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + + const frame = getCurrentFrame(); + if (!frame) return; + + // 查找点击的素材(从后往前,优先选上层的) + selectedAsset = null; + for (let i = frame.assets.length - 1; i >= 0; i--) { + const asset = frame.assets[i]; + const dist = Math.sqrt((x - asset.x) ** 2 + (y - asset.y) ** 2); + if (dist < 30) { + selectedAsset = asset; + // 更新选中状态 + frame.assets.forEach(a => a.selected = false); + asset.selected = true; + isDragging = true; + dragStartX = x - asset.x; + dragStartY = y - asset.y; + renderCanvas(); + break; + } + } + + // 如果没点到素材,取消所有选中 + if (!selectedAsset) { + frame.assets.forEach(a => a.selected = false); + renderCanvas(); + } + }); + + canvas.addEventListener('mousemove', (e) => { + if (!isDragging || !selectedAsset) return; + + const rect = canvas.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + + selectedAsset.x = x - dragStartX; + selectedAsset.y = y - dragStartY; + + // 边界限制 + selectedAsset.x = Math.max(30, Math.min(canvas.width - 30, selectedAsset.x)); + selectedAsset.y = Math.max(30, Math.min(canvas.height - 30, selectedAsset.y)); + + renderCanvas(); + }); + + canvas.addEventListener('mouseup', () => { + if (isDragging) { + isDragging = false; + saveToStorage(); + } + }); + + // 键盘删除 + document.addEventListener('keydown', (e) => { + if (e.key === 'Delete' || e.key === 'Backspace') { + const frame = getCurrentFrame(); + if (!frame) return; + + const selectedIndex = frame.assets.findIndex(a => a.selected); + if (selectedIndex !== -1) { + frame.assets.splice(selectedIndex, 1); + renderCanvas(); + saveToStorage(); + } + } + }); + + // 导入文件监听 + document.getElementById('import-file').addEventListener('change', handleImport); +} + +function addAssetToCanvas(emoji, x, y) { + const frame = getCurrentFrame(); + if (!frame) return; + + // 取消其他选中 + frame.assets.forEach(a => a.selected = false); + + const newAsset = { + id: appState.nextAssetId++, + type: 'emoji', + emoji: emoji, + x: x, + y: y, + selected: true + }; + + frame.assets.push(newAsset); + renderCanvas(); + saveToStorage(); +}// ==================== 导出导入 ==================== +function exportScene() { + const dataStr = JSON.stringify(appState, null, 2); + const blob = new Blob([dataStr], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + + const a = document.createElement('a'); + a.href = url; + a.download = `hololake-scene-${Date.now()}.json`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); +} + +function importScene() { + document.getElementById('import-file').click(); +} + +function handleImport(e) { + const file = e.target.files[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = (event) => { + try { + const imported = JSON.parse(event.target.result); + + // 验证数据结构 + if (!imported.scenes || !Array.isArray(imported.scenes)) { + throw new Error('无效的数据格式'); + } + + // 向后兼容处理 + imported.scenes = imported.scenes.map(scene => { + if (!scene.frames) { + return { + ...scene, + currentFrameIndex: 0, + frames: [{ + id: 1, + assets: scene.assets || [] + }], + nextFrameId: 2 + }; + } + return scene; + }); + + if (!imported.playback) { + imported.playback = { isPlaying: false, fps: 4, intervalId: null, currentFrameIndex: 0 }; + } + + appState = imported; + saveToStorage(); + renderSceneTabs(); + renderCanvas(); + renderTimeline(); + updatePlaybackControls(); + updateFrameCounter(); + + alert('导入成功!'); + } catch (err) { + alert('导入失败:' + err.message); + } + }; + reader.readAsText(file); + + // 清空 input,允许重复导入同一文件 + e.target.value = ''; +} + +// ==================== 截图导出 ==================== +function exportScreenshot() { + // 临时取消选中状态 + const frame = getCurrentFrame(); + if (!frame) return; + + const originalSelected = frame.assets.map(a => a.selected); + frame.assets.forEach(a => a.selected = false); + renderCanvas();cd ~/Desktop/guanghulab/dynamic-comic-studio + + + + // 导出 PNG + const link = document.createElement('a'); + link.download = `hololake-frame-${Date.now()}.png`; + link.href = canvas.toDataURL(); + link.click(); + + // 恢复选中状态 + frame.assets.forEach((a, i) => { + a.selected = originalSelected[i]; + }); + renderCanvas(); +} + +// ==================== 启动应用 ==================== +document.addEventListener('DOMContentLoaded', init);