537 lines
16 KiB
JavaScript
537 lines
16 KiB
JavaScript
// HoloLake 动态漫制作系统 - 环节6 (预览与分享版)
|
||
// 基于环节5增强,新增预览模式、分享链接、嵌入代码功能
|
||
|
||
// ==================== 全局变量 ====================
|
||
let scenes = [
|
||
{
|
||
id: 'scene-1',
|
||
name: '场景1',
|
||
frames: [
|
||
{
|
||
id: 'frame-1-1',
|
||
assets: [] // 存储素材 { id, type, emoji, x, y, width, height }
|
||
}
|
||
]
|
||
}
|
||
];
|
||
let currentSceneIndex = 0;
|
||
let currentFrameIndex = 0;
|
||
let animationInterval = null;
|
||
let isPlaying = false;
|
||
let fps = 4;
|
||
let canvas = document.getElementById('mainCanvas');
|
||
let ctx = canvas.getContext('2d');
|
||
|
||
// ==================== 初始化 ====================
|
||
function init() {
|
||
console.log('HoloLake 动态漫制作系统 v6.0 初始化...');
|
||
updateUI();
|
||
setupEventListeners();
|
||
drawCurrentFrame();
|
||
}
|
||
|
||
// ==================== 事件监听设置 ====================
|
||
function setupEventListeners() {
|
||
// FPS滑块
|
||
const fpsSlider = document.getElementById('fpsSlider');
|
||
if (fpsSlider) {
|
||
fpsSlider.addEventListener('input', function(e) {
|
||
fps = parseInt(e.target.value);
|
||
document.getElementById('fpsValue').textContent = fps + ' FPS';
|
||
if (isPlaying) {
|
||
stopAnimation();
|
||
startAnimation();
|
||
}
|
||
});
|
||
}
|
||
|
||
// 预览按钮
|
||
const previewBtn = document.getElementById('previewBtn');
|
||
if (previewBtn) {
|
||
previewBtn.addEventListener('click', enterPreviewMode);
|
||
}
|
||
|
||
// 预览模式按钮
|
||
const playPreviewBtn = document.getElementById('playPreviewBtn');
|
||
if (playPreviewBtn) {
|
||
playPreviewBtn.addEventListener('click', function() {
|
||
if (!isPlaying) {
|
||
startAnimation();
|
||
}
|
||
});
|
||
}
|
||
|
||
const pausePreviewBtn = document.getElementById('pausePreviewBtn');
|
||
if (pausePreviewBtn) {
|
||
pausePreviewBtn.addEventListener('click', function() {
|
||
if (isPlaying) {
|
||
stopAnimation();
|
||
}
|
||
});
|
||
}
|
||
|
||
const stopPreviewBtn = document.getElementById('stopPreviewBtn');
|
||
if (stopPreviewBtn) {
|
||
stopPreviewBtn.addEventListener('click', function() {
|
||
stopAnimation();
|
||
currentFrameIndex = 0;
|
||
updateUI();
|
||
drawCurrentFrame();
|
||
});
|
||
}
|
||
|
||
const exitPreviewBtn = document.getElementById('exitPreviewBtn');
|
||
if (exitPreviewBtn) {
|
||
exitPreviewBtn.addEventListener('click', exitPreviewMode);
|
||
}
|
||
|
||
// 分享按钮
|
||
const shareBtn = document.getElementById('shareBtn');
|
||
if (shareBtn) {
|
||
shareBtn.addEventListener('click', showSharePanel);
|
||
}
|
||
|
||
// 分享面板按钮
|
||
const generateUrlBtn = document.getElementById('generateUrlBtn');
|
||
if (generateUrlBtn) {
|
||
generateUrlBtn.addEventListener('click', generateShareUrl);
|
||
}
|
||
|
||
const generateEmbedBtn = document.getElementById('generateEmbedBtn');
|
||
if (generateEmbedBtn) {
|
||
generateEmbedBtn.addEventListener('click', generateEmbedCode);
|
||
}
|
||
|
||
const closeShareBtn = document.getElementById('closeShareBtn');
|
||
if (closeShareBtn) {
|
||
closeShareBtn.addEventListener('click', hideSharePanel);
|
||
}
|
||
|
||
const copyShareBtn = document.getElementById('copyShareBtn');
|
||
if (copyShareBtn) {
|
||
copyShareBtn.addEventListener('click', copyToClipboard);
|
||
}
|
||
|
||
// 素材拖拽
|
||
const materialItems = document.querySelectorAll('.material-item');
|
||
materialItems.forEach(item => {
|
||
item.addEventListener('dragstart', handleDragStart);
|
||
});
|
||
|
||
// 画布放置
|
||
canvas.addEventListener('dragover', (e) => e.preventDefault());
|
||
canvas.addEventListener('drop', handleDrop);
|
||
|
||
// 画布点击选择素材
|
||
canvas.addEventListener('click', handleCanvasClick);
|
||
}
|
||
|
||
// ==================== 素材拖拽 ====================
|
||
function handleDragStart(e) {
|
||
const type = e.target.dataset.type;
|
||
const emoji = e.target.dataset.emoji;
|
||
e.dataTransfer.setData('text/plain', JSON.stringify({
|
||
type: type,
|
||
emoji: emoji
|
||
}));
|
||
}
|
||
|
||
// ==================== 素材放置 ====================
|
||
function handleDrop(e) {
|
||
e.preventDefault();
|
||
const rect = canvas.getBoundingClientRect();
|
||
const scaleX = canvas.width / rect.width;
|
||
const scaleY = canvas.height / rect.height;
|
||
|
||
const x = (e.clientX - rect.left) * scaleX;
|
||
const y = (e.clientY - rect.top) * scaleY;
|
||
|
||
try {
|
||
const data = JSON.parse(e.dataTransfer.getData('text/plain'));
|
||
|
||
const currentScene = scenes[currentSceneIndex];
|
||
const currentFrame = currentScene.frames[currentFrameIndex];
|
||
|
||
const newAsset = {
|
||
id: 'asset-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9),
|
||
type: data.type,
|
||
emoji: data.emoji,
|
||
x: x - 25,
|
||
y: y - 25,
|
||
width: 50,
|
||
height: 50
|
||
};
|
||
|
||
currentFrame.assets.push(newAsset);
|
||
drawCurrentFrame();
|
||
updateUI();
|
||
} catch (error) {
|
||
console.error('放置素材失败:', error);
|
||
}
|
||
}
|
||
|
||
// ==================== 画布点击选择 ====================
|
||
function handleCanvasClick(e) {
|
||
const rect = canvas.getBoundingClientRect();
|
||
const scaleX = canvas.width / rect.width;
|
||
const scaleY = canvas.height / rect.height;
|
||
|
||
const clickX = (e.clientX - rect.left) * scaleX;
|
||
const clickY = (e.clientY - rect.top) * scaleY;
|
||
|
||
const currentScene = scenes[currentSceneIndex];
|
||
const currentFrame = currentScene.frames[currentFrameIndex];
|
||
|
||
// 从后往前遍历,以便选中上层的素材
|
||
let selectedAsset = null;
|
||
for (let i = currentFrame.assets.length - 1; i >= 0; i--) {
|
||
const asset = currentFrame.assets[i];
|
||
if (clickX >= asset.x && clickX <= asset.x + asset.width &&
|
||
clickY >= asset.y && clickY <= asset.y + asset.height) {
|
||
selectedAsset = asset;
|
||
break;
|
||
}
|
||
}
|
||
|
||
// 移除所有选中状态
|
||
currentFrame.assets.forEach(a => delete a.selected);
|
||
|
||
// 设置新的选中状态
|
||
if (selectedAsset) {
|
||
selectedAsset.selected = true;
|
||
}
|
||
|
||
drawCurrentFrame();
|
||
}
|
||
|
||
// ==================== 绘制当前帧 ====================
|
||
function drawCurrentFrame() {
|
||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||
|
||
const currentScene = scenes[currentSceneIndex];
|
||
if (!currentScene) return;
|
||
|
||
const currentFrame = currentScene.frames[currentFrameIndex];
|
||
if (!currentFrame) return;
|
||
|
||
// 绘制背景色
|
||
ctx.fillStyle = '#0a0a14';
|
||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||
|
||
// 绘制所有素材
|
||
currentFrame.assets.forEach(asset => {
|
||
ctx.font = '40px "Segoe UI Emoji", "Apple Color Emoji", sans-serif';
|
||
ctx.textAlign = 'center';
|
||
ctx.textBaseline = 'middle';
|
||
ctx.fillText(asset.emoji, asset.x + asset.width/2, asset.y + asset.height/2);
|
||
|
||
// 如果被选中,绘制边框
|
||
if (asset.selected) {
|
||
ctx.strokeStyle = '#e94560';
|
||
ctx.lineWidth = 2;
|
||
ctx.setLineDash([5, 5]);
|
||
ctx.strokeRect(asset.x, asset.y, asset.width, asset.height);
|
||
ctx.setLineDash([]);
|
||
}
|
||
});
|
||
}
|
||
|
||
// ==================== 场景管理 ====================
|
||
function addScene() {
|
||
const newScene = {
|
||
id: 'scene-' + Date.now(),
|
||
name: '场景' + (scenes.length + 1),
|
||
frames: [
|
||
{
|
||
id: 'frame-' + Date.now() + '-1',
|
||
assets: []
|
||
}
|
||
]
|
||
};
|
||
scenes.push(newScene);
|
||
currentSceneIndex = scenes.length - 1;
|
||
currentFrameIndex = 0;
|
||
updateUI();
|
||
drawCurrentFrame();
|
||
}
|
||
|
||
// ==================== 帧管理 ====================
|
||
function addFrame() {
|
||
const currentScene = scenes[currentSceneIndex];
|
||
const currentFrame = currentScene.frames[currentFrameIndex];
|
||
|
||
// 深拷贝当前帧的素材
|
||
const newAssets = JSON.parse(JSON.stringify(currentFrame.assets));
|
||
// 移除选中状态
|
||
newAssets.forEach(a => delete a.selected);
|
||
|
||
const newFrame = {
|
||
id: 'frame-' + Date.now(),
|
||
assets: newAssets
|
||
};
|
||
|
||
currentScene.frames.splice(currentFrameIndex + 1, 0, newFrame);
|
||
currentFrameIndex++;
|
||
updateUI();
|
||
drawCurrentFrame();
|
||
}
|
||
|
||
function deleteFrame() {
|
||
const currentScene = scenes[currentSceneIndex];
|
||
if (currentScene.frames.length <= 1) {
|
||
alert('每个场景至少保留一帧');
|
||
return;
|
||
}
|
||
|
||
currentScene.frames.splice(currentFrameIndex, 1);
|
||
if (currentFrameIndex >= currentScene.frames.length) {
|
||
currentFrameIndex = currentScene.frames.length - 1;
|
||
}
|
||
updateUI();
|
||
drawCurrentFrame();
|
||
}
|
||
|
||
// ==================== 动画控制 ====================
|
||
function togglePlayback() {
|
||
if (isPlaying) {
|
||
stopAnimation();
|
||
} else {
|
||
startAnimation();
|
||
}
|
||
}
|
||
|
||
function startAnimation() {
|
||
if (isPlaying) return;
|
||
isPlaying = true;
|
||
updatePlayButton();
|
||
|
||
animationInterval = setInterval(() => {
|
||
const currentScene = scenes[currentSceneIndex];
|
||
if (currentFrameIndex < currentScene.frames.length - 1) {
|
||
currentFrameIndex++;
|
||
} else {
|
||
currentFrameIndex = 0;
|
||
}
|
||
updateUI();
|
||
drawCurrentFrame();
|
||
}, 1000 / fps);
|
||
}
|
||
|
||
function stopAnimation() {
|
||
if (animationInterval) {
|
||
clearInterval(animationInterval);
|
||
animationInterval = null;
|
||
}
|
||
isPlaying = false;
|
||
updatePlayButton();
|
||
}
|
||
|
||
function updatePlayButton() {
|
||
const playBtn = document.getElementById('timelinePlayBtn');
|
||
if (playBtn) {
|
||
playBtn.textContent = isPlaying ? '⏸️ 暂停' : '▶️ 播放';
|
||
}
|
||
|
||
const previewPlayBtn = document.getElementById('playPreviewBtn');
|
||
if (previewPlayBtn) {
|
||
// 预览模式的播放按钮文字不变
|
||
}
|
||
}
|
||
|
||
// ==================== 预览模式 ====================
|
||
function enterPreviewMode() {
|
||
document.body.classList.add('preview-mode');
|
||
// 如果正在播放动画,继续播放
|
||
}
|
||
|
||
function exitPreviewMode() {
|
||
document.body.classList.remove('preview-mode');
|
||
}
|
||
|
||
// ==================== 分享功能 ====================
|
||
function showSharePanel() {
|
||
document.getElementById('shareOverlay').classList.remove('hidden');
|
||
document.getElementById('sharePanel').classList.remove('hidden');
|
||
}
|
||
|
||
function hideSharePanel() {
|
||
document.getElementById('shareOverlay').classList.add('hidden');
|
||
document.getElementById('sharePanel').classList.add('hidden');
|
||
document.getElementById('shareUrl').innerHTML = '';
|
||
document.getElementById('copyShareBtn').style.display = 'none';
|
||
}
|
||
|
||
function generateShareUrl() {
|
||
// 序列化作品数据
|
||
const workData = {
|
||
scenes: scenes,
|
||
version: '1.0',
|
||
timestamp: Date.now()
|
||
};
|
||
|
||
const jsonStr = JSON.stringify(workData);
|
||
// 使用encodeURIComponent编码,然后生成Data URL
|
||
const encodedData = encodeURIComponent(jsonStr);
|
||
const dataUrl = 'data:text/json;charset=utf-8,' + encodedData;
|
||
|
||
// 显示分享链接
|
||
const shareUrlDiv = document.getElementById('shareUrl');
|
||
shareUrlDiv.innerHTML = `<a href="${dataUrl}" target="_blank">${dataUrl.substring(0, 50)}...</a>`;
|
||
|
||
document.getElementById('copyShareBtn').style.display = 'inline-block';
|
||
document.getElementById('copyShareBtn').dataset.url = dataUrl;
|
||
}
|
||
|
||
function generateEmbedCode() {
|
||
// 生成iframe嵌入代码
|
||
const embedCode = `<iframe src="preview-player.html" width="800" height="450" frameborder="0" allowfullscreen></iframe>`;
|
||
|
||
const shareUrlDiv = document.getElementById('shareUrl');
|
||
shareUrlDiv.innerHTML = `<pre style="white-space: pre-wrap; background:#0a0a14; padding:10px; border-radius:8px;">${embedCode}</pre>`;
|
||
|
||
document.getElementById('copyShareBtn').style.display = 'inline-block';
|
||
document.getElementById('copyShareBtn').dataset.code = embedCode;
|
||
}
|
||
|
||
function copyToClipboard(e) {
|
||
const btn = e.target;
|
||
if (btn.dataset.url) {
|
||
navigator.clipboard.writeText(btn.dataset.url);
|
||
} else if (btn.dataset.code) {
|
||
navigator.clipboard.writeText(btn.dataset.code);
|
||
}
|
||
alert('已复制到剪贴板!');
|
||
}
|
||
|
||
// ==================== 导入导出 ====================
|
||
function exportScene() {
|
||
const dataStr = JSON.stringify(scenes, null, 2);
|
||
const blob = new Blob([dataStr], {type: 'application/json'});
|
||
const url = URL.createObjectURL(blob);
|
||
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = 'hololake-scene.json';
|
||
a.click();
|
||
|
||
URL.revokeObjectURL(url);
|
||
}
|
||
|
||
function importScene(event) {
|
||
const file = event.target.files[0];
|
||
if (!file) return;
|
||
|
||
const reader = new FileReader();
|
||
reader.onload = function(e) {
|
||
try {
|
||
const importedScenes = JSON.parse(e.target.result);
|
||
if (Array.isArray(importedScenes) && importedScenes.length > 0) {
|
||
scenes = importedScenes;
|
||
currentSceneIndex = 0;
|
||
currentFrameIndex = 0;
|
||
updateUI();
|
||
drawCurrentFrame();
|
||
alert('导入成功!');
|
||
} else {
|
||
alert('无效的场景数据');
|
||
}
|
||
} catch (error) {
|
||
alert('导入失败:' + error.message);
|
||
}
|
||
};
|
||
reader.readAsText(file);
|
||
|
||
// 清空input,以便再次导入同一个文件
|
||
event.target.value = '';
|
||
}
|
||
|
||
function exportScreenshot() {
|
||
const dataUrl = canvas.toDataURL('image/png');
|
||
const a = document.createElement('a');
|
||
a.href = dataUrl;
|
||
a.download = 'hololake-screenshot-' + Date.now() + '.png';
|
||
a.click();
|
||
}
|
||
|
||
// ==================== 更新UI ====================
|
||
function updateUI() {
|
||
// 更新场景列表
|
||
const sceneList = document.getElementById('sceneList');
|
||
if (sceneList) {
|
||
sceneList.innerHTML = '';
|
||
scenes.forEach((scene, index) => {
|
||
const sceneDiv = document.createElement('div');
|
||
sceneDiv.className = 'scene-item' + (index === currentSceneIndex ? ' active' : '');
|
||
sceneDiv.innerHTML = `
|
||
<span>${scene.name}</span>
|
||
<button onclick="deleteScene(${index})" style="background:none; border:none; color:#e94560; cursor:pointer;">✖️</button>
|
||
`;
|
||
sceneDiv.addEventListener('click', (e) => {
|
||
if (e.target.tagName !== 'BUTTON') {
|
||
switchScene(index);
|
||
}
|
||
});
|
||
sceneList.appendChild(sceneDiv);
|
||
});
|
||
}
|
||
|
||
// 更新帧列表
|
||
const framesContainer = document.getElementById('framesContainer');
|
||
if (framesContainer && scenes[currentSceneIndex]) {
|
||
framesContainer.innerHTML = '';
|
||
scenes[currentSceneIndex].frames.forEach((frame, index) => {
|
||
const frameDiv = document.createElement('div');
|
||
frameDiv.className = 'frame-thumb' + (index === currentFrameIndex ? ' active' : '');
|
||
frameDiv.innerHTML = `
|
||
<div class="frame-index">${index + 1}</div>
|
||
<div>${frame.assets.length}个素材</div>
|
||
`;
|
||
frameDiv.addEventListener('click', () => {
|
||
currentFrameIndex = index;
|
||
drawCurrentFrame();
|
||
updateUI();
|
||
});
|
||
framesContainer.appendChild(frameDiv);
|
||
});
|
||
}
|
||
|
||
// 更新帧计数器
|
||
const frameCounter = document.getElementById('frame-counter');
|
||
if (frameCounter && scenes[currentSceneIndex]) {
|
||
frameCounter.textContent = `帧 ${currentFrameIndex + 1}/${scenes[currentSceneIndex].frames.length}`;
|
||
}
|
||
}
|
||
|
||
function switchScene(index) {
|
||
if (index >= 0 && index < scenes.length) {
|
||
currentSceneIndex = index;
|
||
currentFrameIndex = 0;
|
||
if (isPlaying) {
|
||
stopAnimation();
|
||
}
|
||
updateUI();
|
||
drawCurrentFrame();
|
||
}
|
||
}
|
||
|
||
function deleteScene(index) {
|
||
if (scenes.length <= 1) {
|
||
alert('至少保留一个场景');
|
||
return;
|
||
}
|
||
|
||
scenes.splice(index, 1);
|
||
if (currentSceneIndex >= scenes.length) {
|
||
currentSceneIndex = scenes.length - 1;
|
||
}
|
||
currentFrameIndex = 0;
|
||
if (isPlaying) {
|
||
stopAnimation();
|
||
}
|
||
updateUI();
|
||
drawCurrentFrame();
|
||
}
|
||
|
||
// ==================== 启动初始化 ====================
|
||
window.onload = init;
|