+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
🔗 分享作品
+
+
+
+
+
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);