1030 lines
31 KiB
Plaintext
1030 lines
31 KiB
Plaintext
// ==================== 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] 预览与分享功能已初始化');
|
||
};
|