Guanghu Domestic Migration d1e47f4565
Some checks are pending
自动更新代码和重启 / update-and-restart (push) Waiting to run
CI检查 + 自动部署 / check (push) Waiting to run
CI检查 + 自动部署 / deploy (push) Blocked by required conditions
重启聊天服务 / restart (push) Waiting to run
chore: import sanitized domestic snapshot for REPO-002
Source snapshot: ca48d3ddf926d79aa138306164169baf764bb829
2026-07-17 15:54:41 +08:00

1114 lines
32 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,
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 = `
<h2 style="margin-top:0;">🔗 分享链接</h2>
<p>复制下面的链接发给朋友,他们打开就能看到你的作品:</p>
<textarea id="share-url" style="width:100%;height:80px;margin:10px 0;padding:10px;background:#1a1a2e;color:#00ff88;border:1px solid #444;border-radius:5px;resize:none;">${url}</textarea>
<div style="display:flex;gap:10px;margin-top:15px;">
<button onclick="copyShareUrl()" style="flex:1;padding:10px;background:#00ff88;color:#1a1a2e;border:none;border-radius:5px;cursor:pointer;font-weight:bold;">📋 复制链接</button>
<button onclick="closeShareDialog()" style="flex:1;padding:10px;background:#444;color:#fff;border:none;border-radius:5px;cursor:pointer;">关闭</button>
</div>
<p style="margin-top:15px;font-size:12px;color:#888;">
提示链接包含完整作品数据如果太长可能会被某些应用截断。建议导出HTML文件分享更稳定。
</p>
`;
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 `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HoloLake 动态漫 - ${data.scenes[0]?.name || '未命名作品'}</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #1a1a2e;
color: #fff;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
padding: 20px;
}
h1 { margin-bottom: 20px; color: #00ff88; }
#canvas-container {
background: #2a2a4e;
border-radius: 10px;
padding: 20px;
margin-bottom: 20px;
}
canvas { display: block; background: #1a1a2e; border-radius: 5px; }
.controls {
display: flex;
gap: 15px;
margin-bottom: 20px;
}
button {
padding: 12px 24px;
font-size: 16px;
border: none;
border-radius: 5px;
cursor: pointer;
background: #00ff88;
color: #1a1a2e;
font-weight: bold;
transition: opacity 0.2s;
}
button:hover { opacity: 0.8; }
button:disabled { background: #444; cursor: not-allowed; }
.scene-selector {
display: flex;
gap: 10px;
flex-wrap: wrap;
justify-content: center;
max-width: 800px;
}
.scene-btn {
padding: 8px 16px;
background: #2a2a4e;
color: #fff;
border: 1px solid #00ff88;
}
.scene-btn.active { background: #00ff88; color: #1a1a2e; }
.info { margin-top: 20px; color: #888; font-size: 14px; }
</style>
</head>
<body>
<h1>🎬 HoloLake 动态漫</h1>
<div id="canvas-container">
<canvas id="player" width="800" height="600"></canvas>
</div>
<div class="controls">
<button onclick="player.stop()">⏹ 停止</button>
<button onclick="player.play()">▶ 播放</button>
<button onclick="player.pause()">⏸ 暂停</button>
</div>
<div class="scene-selector" id="scene-buttons"></div>
<div class="info">
<p>作品:${data.scenes[0]?.name || '未命名'} | 场景数:${data.scenes.length} | 导出时间:${new Date(data.exportTime).toLocaleString()}</p>
<p>使用 HoloLake 动态漫制作系统创作</p>
</div>
<script>
// 作品数据
const comicData = ${JSON.stringify(data)};
// 播放器
const player = {
canvas: document.getElementById('player'),
ctx: null,
currentScene: 0,
currentFrame: 0,
isPlaying: false,
interval: null,
fps: 12,
images: {},
init() {
this.ctx = this.canvas.getContext('2d');
this.loadImages().then(() => {
this.render();
this.createSceneButtons();
});
},
async loadImages() {
const promises = [];
comicData.scenes.forEach((scene, sIdx) => {
scene.frames.forEach((frame, fIdx) => {
frame.assets.forEach((asset, aIdx) => {
if (asset.type === 'image' && asset.src) {
promises.push(new Promise((resolve) => {
const img = new Image();
img.onload = () => {
this.images[\`\${sIdx}-\${fIdx}-\${aIdx}\`] = img;
resolve();
};
img.src = asset.src;
}));
}
});
});
});
await Promise.all(promises);
},
render() {
const scene = comicData.scenes[this.currentScene];
if (!scene) return;
const frame = scene.frames[this.currentFrame];
if (!frame) return;
// 清空
this.ctx.fillStyle = '#1a1a2e';
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
// 绘制素材
frame.assets.forEach((asset, idx) => {
if (asset.type === 'image' && asset.src) {
const img = this.images[\`\${this.currentScene}-\${this.currentFrame}-\${idx}\`];
if (img) {
this.ctx.drawImage(img, asset.x, asset.y, asset.width, asset.height);
}
} else if (asset.type === 'text') {
this.ctx.font = asset.font || '20px Arial';
this.ctx.fillStyle = asset.color || '#ffffff';
this.ctx.fillText(asset.text, asset.x, asset.y);
}
});
},
play() {
if (this.isPlaying) return;
this.isPlaying = true;
const scene = comicData.scenes[this.currentScene];
this.interval = setInterval(() => {
this.currentFrame = (this.currentFrame + 1) % scene.frames.length;
this.render();
}, 1000 / this.fps);
},
pause() {
this.isPlaying = false;
if (this.interval) {
clearInterval(this.interval);
this.interval = null;
}
},
stop() {
this.pause();
this.currentFrame = 0;
this.render();
},
switchScene(idx) {
this.stop();
this.currentScene = idx;
this.currentFrame = 0;
this.render();
this.updateSceneButtons();
},
createSceneButtons() {
const container = document.getElementById('scene-buttons');
comicData.scenes.forEach((scene, idx) => {
const btn = document.createElement('button');
btn.className = 'scene-btn' + (idx === 0 ? ' active' : '');
btn.textContent = scene.name || \`场景 \${idx + 1}\`;
btn.onclick = () => this.switchScene(idx);
container.appendChild(btn);
});
},
updateSceneButtons() {
document.querySelectorAll('.scene-btn').forEach((btn, idx) => {
btn.classList.toggle('active', idx === this.currentScene);
});
}
};
// 启动
player.init();
</script>
</body>
</html>`;
}
// 检查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);
}
}