diff --git a/dynamic-comic-studio/css/style.css b/dynamic-comic-studio/css/style.css
new file mode 100644
index 00000000..3243d659
--- /dev/null
+++ b/dynamic-comic-studio/css/style.css
@@ -0,0 +1,348 @@
+/* ==================== 基础重置 ==================== */
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', sans-serif;
+ background: #1a1a2e;
+ color: #eee;
+ overflow: hidden;
+}
+
+/* ==================== 布局框架 ==================== */
+#app {
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+}
+
+/* ==================== 顶部工具栏 ==================== */
+#toolbar {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 10px 20px;
+ background: #16213e;
+ border-bottom: 2px solid #0f3460;
+}
+
+#toolbar h1 {
+ font-size: 18px;
+ color: #e94560;
+}
+
+.toolbar-actions button {
+ margin-left: 10px;
+ padding: 6px 12px;
+ background: #0f3460;
+ color: #fff;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ transition: background 0.3s;
+}
+
+.toolbar-actions button:hover {
+ background: #e94560;
+}
+
+/* ==================== 主工作区 ==================== */
+#workspace {
+ display: flex;
+ flex: 1;
+ overflow: hidden;
+}
+
+/* ==================== 左侧素材面板 ==================== */
+#assets-panel {
+ width: 200px;
+ background: #16213e;
+ border-right: 2px solid #0f3460;
+ padding: 15px;
+ overflow-y: auto;
+}
+
+#assets-panel h3 {
+ color: #e94560;
+ margin-bottom: 15px;
+ font-size: 16px;
+}
+
+.asset-category {
+ margin-bottom: 20px;
+}
+
+.asset-category h4 {
+ color: #533483;
+ font-size: 14px;
+ margin-bottom: 10px;
+ padding-left: 5px;
+ border-left: 3px solid #e94560;
+}
+
+.asset-items {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+}
+
+.asset-item {
+ padding: 8px 12px;
+ background: #0f3460;
+ border-radius: 6px;
+ cursor: move;
+ font-size: 13px;
+ transition: all 0.3s;
+ user-select: none;
+}
+
+.asset-item:hover {
+ background: #e94560;
+ transform: translateY(-2px);
+}
+
+/* ==================== 画布区域 ==================== */
+#canvas-container {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ background: #0f0f1a;
+}
+
+#scene-tabs {
+ display: flex;
+ align-items: center;
+ padding: 10px;
+ background: #16213e;
+ border-bottom: 2px solid #0f3460;
+}
+
+#tabs-list {
+ display: flex;
+ gap: 8px;
+ flex: 1;
+}
+
+.scene-tab {
+ padding: 8px 16px;
+ background: #0f3460;
+ border-radius: 4px 4px 0 0;
+ cursor: pointer;
+ font-size: 14px;
+ position: relative;
+}
+
+.scene-tab.active {
+ background: #e94560;
+}
+
+.scene-tab .close-btn {
+ margin-left: 8px;
+ opacity: 0.6;
+}
+
+.scene-tab .close-btn:hover {
+ opacity: 1;
+}
+
+#add-scene-btn {
+ padding: 8px 16px;
+ background: #533483;
+ color: #fff;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ margin-left: 10px;
+}
+
+#canvas-wrapper {
+ flex: 1;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ padding: 20px;
+ overflow: auto;
+}
+
+#main-canvas {
+ background: #fff;
+ border-radius: 8px;
+ box-shadow: 0 4px 20px rgba(0,0,0,0.5);
+ cursor: crosshair;
+}
+
+/* ==================== 底部时间轴面板 ==================== */
+#timeline-panel {
+ background: #16213e;
+ border-top: 2px solid #0f3460;
+ padding: 15px 20px;
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+
+/* 播放控制栏 */
+#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;
+}
+
+.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;
+}
+
+.frame-thumb:hover {
+ border-color: #e94560;
+ transform: translateY(-2px);
+}
+
+.frame-thumb.active {
+ border-color: #e94560;
+ background: #533483;
+ box-shadow: 0 0 10px rgba(233, 69, 96, 0.5);
+}
+
+.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;
+}
+
+/* 时间轴操作按钮 */
+#timeline-actions {
+ display: flex;
+ gap: 10px;
+}
+
+#timeline-actions button {
+ padding: 8px 16px;
+ background: #0f3460;
+ color: #fff;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 14px;
+ transition: all 0.3s;
+}
+
+#timeline-actions button:hover {
+ background: #e94560;
+}
+
+/* ==================== 画布上的素材样式 ==================== */
+.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;
+}
+
+.canvas-asset:hover {
+ transform: scale(1.1);
+}
+
+.canvas-asset.selected {
+ outline: 2px dashed #e94560;
+ outline-offset: 4px;
+}
+
+/* ==================== 滚动条样式 ==================== */
+::-webkit-scrollbar {
+ width: 8px;
+ height: 8px;
+}
+
+::-webkit-scrollbar-track {
+ background: #0f0f1a;
+}
+
+::-webkit-scrollbar-thumb {
+ background: #533483;
+ border-radius: 4px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background: #e94560;
+}
diff --git a/dynamic-comic-studio/index.html b/dynamic-comic-studio/index.html
new file mode 100644
index 00000000..b5be2dcb
--- /dev/null
+++ b/dynamic-comic-studio/index.html
@@ -0,0 +1,88 @@
+
+
+
+
+
+ HoloLake 动态漫制作系统 - 环节5
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dynamic-comic-studio/js/app.js b/dynamic-comic-studio/js/app.js
new file mode 100644
index 00000000..184d1b06
--- /dev/null
+++ b/dynamic-comic-studio/js/app.js
@@ -0,0 +1,581 @@
+// ==================== 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);