2026-05-10 13:12:44 +08:00

1030 lines
31 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ==================== 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}
<span class="close-btn" onclick="event.stopPropagation(); deleteScene(${scene.id})">×</span>
`;
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 = `<iframe src="${dataUrl}" width="800" height="600" frameborder="0" allowfullscreen></iframe>`;
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 = `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>动态漫作品 - HoloLake Studio</title>
<style>
body { margin: 0; padding: 20px; background: #1a1a2e; font-family: Arial, sans-serif; display: flex; flex-direction: column; align-items: center; }
h1 { color: #fff; margin-bottom: 20px; }
#canvas { border-radius: 12px; box-shadow: 0 8px 32px rgba(0,0,0,0.5); background: #fff; }
.controls { margin-top: 20px; display: flex; gap: 15px; align-items: center; background: rgba(255,255,255,0.1); padding: 15px 30px; border-radius: 50px; }
button { padding: 10px 25px; border: none; border-radius: 25px; cursor: pointer; font-size: 14px; transition: all 0.3s; }
.btn-primary { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; }
.btn-primary:hover { transform: scale(1.05); box-shadow: 0 5px 20px rgba(102,126,234,0.4); }
.btn-secondary { background: rgba(255,255,255,0.2); color: white; }
.btn-secondary:hover { background: rgba(255,255,255,0.3); }
span { color: #fff; margin-left: 15px; font-size: 14px; }
</style>
</head>
<body>
<h1>🎬 动态漫作品</h1>
<canvas id="canvas" width="800" height="600"></canvas>
<div class="controls">
<button onclick="play()" class="btn-primary">▶ 播放</button>
<button onclick="pause()" class="btn-secondary">⏸ 暂停</button>
<button onclick="stop()" class="btn-secondary">⏹ 停止</button>
<span>FPS: <span id="fps">12</span></span>
</div>
<script>
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
let isPlaying = false;
let intervalId = null;
let currentFrameIndex = 0;
let fps = 12;
const shareData = JSON.parse(decodeURIComponent(atob('${encoded}')));
const scenes = shareData.scenes;
const currentScene = scenes[0] || { frames: [] };
const frames = currentScene.frames || [];
function renderFrame(index) {
if (!frames[index]) return;
const frame = frames[index];
ctx.fillStyle = frame.background || '#f0f0f0';
ctx.fillRect(0, 0, canvas.width, canvas.height);
if (frame.assets) {
frame.assets.forEach(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 play() {
if (isPlaying || frames.length === 0) return;
isPlaying = true;
intervalId = setInterval(() => {
currentFrameIndex = (currentFrameIndex + 1) % frames.length;
renderFrame(currentFrameIndex);
}, 1000 / fps);
}
function pause() {
isPlaying = false;
if (intervalId) clearInterval(intervalId);
}
function stop() {
isPlaying = false;
if (intervalId) clearInterval(intervalId);
currentFrameIndex = 0;
renderFrame(0);
}
renderFrame(0);
<\/script>
</body>
</html>`;
// 创建下载
const blob = new Blob([htmlContent], { type: 'text/html' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `动态漫作品_${Date.now()}.html`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
console.log('[导出] HTML文件已下载');
}
// ==================== 分享面板标签切换 ====================
function switchShareTab(tabName) {
// 切换按钮状态
document.querySelectorAll('.share-tab').forEach(tab => {
tab.classList.remove('active');
if (tab.dataset.tab === tabName) {
tab.classList.add('active');
}
});
// 切换内容
document.querySelectorAll('.share-tab-content').forEach(content => {
content.classList.add('hidden');
});
document.getElementById(tabName + 'Tab').classList.remove('hidden');
// 生成对应内容
if (tabName === 'link') {
generateShareLink();
} else if (tabName === 'embed') {
generateEmbedCode();
}
}
// ==================== 从URL加载分享数据 ====================
function loadFromUrl() {
const urlParams = new URLSearchParams(window.location.search);
const data = urlParams.get('data');
if (data) {
try {
const decoded = decodeURIComponent(atob(data));
const shareData = JSON.parse(decoded);
if (shareData.scenes) {
appState.scenes = shareData.scenes;
saveToStorage();
renderSceneList();
if (appState.scenes.length > 0) {
selectScene(appState.scenes[0].id);
}
console.log('[URL加载] 作品数据已加载');
}
} catch (e) {
console.error('[URL加载] 解析失败:', e);
}
}
}
// ==================== 绑定预览和分享事件 ====================
function bindPreviewAndShareEvents() {
// 预览模式按钮
const previewBtn = document.getElementById('previewBtn');
const exitPreviewBtn = document.getElementById('exitPreviewBtn');
const previewPlayBtn = document.getElementById('previewPlayBtn');
const previewPauseBtn = document.getElementById('previewPauseBtn');
const previewStopBtn = document.getElementById('previewStopBtn');
if (previewBtn) previewBtn.addEventListener('click', enterPreviewMode);
if (exitPreviewBtn) exitPreviewBtn.addEventListener('click', exitPreviewMode);
if (previewPlayBtn) previewPlayBtn.addEventListener('click', playPreview);
if (previewPauseBtn) previewPauseBtn.addEventListener('click', pausePreview);
if (previewStopBtn) previewStopBtn.addEventListener('click', stopPreview);
// 分享面板按钮
const shareBtn = document.getElementById('shareBtn');
const closeShareBtn = document.getElementById('closeShareBtn');
const copyLinkBtn = document.getElementById('copyLinkBtn');
const copyEmbedBtn = document.getElementById('copyEmbedBtn');
const exportHtmlBtn = document.getElementById('exportHtmlBtn');
if (shareBtn) shareBtn.addEventListener('click', openSharePanel);
if (closeShareBtn) closeShareBtn.addEventListener('click', closeSharePanel);
if (copyLinkBtn) copyLinkBtn.addEventListener('click', copyShareLink);
if (copyEmbedBtn) copyEmbedBtn.addEventListener('click', copyEmbedCode);
if (exportHtmlBtn) exportHtmlBtn.addEventListener('click', exportHtmlFile);
// 分享标签切换
document.querySelectorAll('.share-tab').forEach(tab => {
tab.addEventListener('click', () => switchShareTab(tab.dataset.tab));
});
}
// ==================== 修改 init 函数,添加新功能初始化 ====================
const originalInit = init;
init = function() {
originalInit();
bindPreviewAndShareEvents();
loadFromUrl();
console.log('[环节6] 预览与分享功能已初始化');
};