DEV-004: M17环节6-在线预览与分享·预览模式+Data URL生成+嵌入代码+全屏播放
This commit is contained in:
parent
fef78187f4
commit
75fcaf4467
|
|
@ -1,348 +1,379 @@
|
|||
/* ==================== 基础重置 ==================== */
|
||||
/* === 全局重置 === */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: 'Segoe UI', 'PingFang SC', Roboto, system-ui, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', sans-serif;
|
||||
background: #1a1a2e;
|
||||
color: #eee;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* ==================== 布局框架 ==================== */
|
||||
#app {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
/* === 主容器 === */
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 24px;
|
||||
padding: 24px;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
/* ==================== 顶部工具栏 ==================== */
|
||||
#toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 20px;
|
||||
background: #16213e;
|
||||
border-bottom: 2px solid #0f3460;
|
||||
/* === 标题区域 === */
|
||||
h1 {
|
||||
font-size: 2.5em;
|
||||
margin-bottom: 24px;
|
||||
background: linear-gradient(45deg, #fff, #e94560);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
text-shadow: 0 0 20px rgba(233, 69, 96, 0.3);
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
#toolbar h1 {
|
||||
font-size: 18px;
|
||||
color: #e94560;
|
||||
/* === 两列布局 === */
|
||||
.main-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 280px 1fr 300px;
|
||||
gap: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.toolbar-actions button {
|
||||
margin-left: 10px;
|
||||
padding: 6px 12px;
|
||||
background: #0f3460;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s;
|
||||
/* === 左侧面板通用样式 === */
|
||||
.left-panel, .right-panel {
|
||||
background: rgba(10, 10, 20, 0.6);
|
||||
border-radius: 20px;
|
||||
padding: 20px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
|
||||
.toolbar-actions button:hover {
|
||||
background: #e94560;
|
||||
/* === 素材分类标题 === */
|
||||
.left-panel h3 {
|
||||
color: #e94560;
|
||||
margin: 16px 0 12px 0;
|
||||
font-size: 1.2em;
|
||||
letter-spacing: 1px;
|
||||
border-left: 4px solid #e94560;
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
/* ==================== 主工作区 ==================== */
|
||||
#workspace {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
.left-panel h3:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* ==================== 左侧素材面板 ==================== */
|
||||
#assets-panel {
|
||||
width: 200px;
|
||||
background: #16213e;
|
||||
border-right: 2px solid #0f3460;
|
||||
padding: 15px;
|
||||
overflow-y: auto;
|
||||
/* === 素材网格 === */
|
||||
.material-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
#assets-panel h3 {
|
||||
color: #e94560;
|
||||
margin-bottom: 15px;
|
||||
font-size: 16px;
|
||||
/* === 素材项 === */
|
||||
.material-item {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
color: #fff;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.asset-category {
|
||||
margin-bottom: 20px;
|
||||
.material-item:hover {
|
||||
background: rgba(233, 69, 96, 0.2);
|
||||
border-color: #e94560;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.asset-category h4 {
|
||||
color: #533483;
|
||||
font-size: 14px;
|
||||
margin-bottom: 10px;
|
||||
padding-left: 5px;
|
||||
border-left: 3px solid #e94560;
|
||||
/* === 中间画布区域 === */
|
||||
.canvas-area {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 20px;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.asset-items {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
/* === 画布 === */
|
||||
#mainCanvas {
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
aspect-ratio: 16/9;
|
||||
background: #0a0a14;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
|
||||
border: 2px solid rgba(233, 69, 96, 0.3);
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.asset-item {
|
||||
padding: 8px 12px;
|
||||
background: #0f3460;
|
||||
border-radius: 6px;
|
||||
cursor: move;
|
||||
font-size: 13px;
|
||||
transition: all 0.3s;
|
||||
user-select: none;
|
||||
/* === 画布控制栏 === */
|
||||
.canvas-controls {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.asset-item:hover {
|
||||
background: #e94560;
|
||||
transform: translateY(-2px);
|
||||
/* === 按钮样式 === */
|
||||
.btn {
|
||||
background: linear-gradient(45deg, #2a1a3a, #1a1a2e);
|
||||
border: 1px solid #533483;
|
||||
color: #fff;
|
||||
padding: 10px 24px;
|
||||
border-radius: 30px;
|
||||
font-size: 0.95em;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* ==================== 画布区域 ==================== */
|
||||
#canvas-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #0f0f1a;
|
||||
.btn:hover {
|
||||
background: linear-gradient(45deg, #3a2a4a, #2a1a3a);
|
||||
border-color: #e94560;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 15px rgba(233, 69, 96, 0.3);
|
||||
}
|
||||
|
||||
#scene-tabs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
background: #16213e;
|
||||
border-bottom: 2px solid #0f3460;
|
||||
.btn-primary {
|
||||
background: linear-gradient(45deg, #e94560, #533483);
|
||||
border-color: #e94560;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
#tabs-list {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
.btn-primary:hover {
|
||||
background: linear-gradient(45deg, #ff5570, #634393);
|
||||
box-shadow: 0 6px 20px rgba(233, 69, 96, 0.5);
|
||||
}
|
||||
|
||||
.scene-tab {
|
||||
padding: 8px 16px;
|
||||
background: #0f3460;
|
||||
border-radius: 4px 4px 0 0;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
position: relative;
|
||||
/* === 右侧面板 === */
|
||||
.right-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.scene-tab.active {
|
||||
background: #e94560;
|
||||
/* === 场景列表 === */
|
||||
.scene-list {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
min-height: 120px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.scene-tab .close-btn {
|
||||
margin-left: 8px;
|
||||
opacity: 0.6;
|
||||
.scene-item {
|
||||
background: rgba(233, 69, 96, 0.1);
|
||||
border: 1px solid #533483;
|
||||
border-radius: 8px;
|
||||
padding: 8px 12px;
|
||||
margin-bottom: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.scene-tab .close-btn:hover {
|
||||
opacity: 1;
|
||||
.scene-item:hover {
|
||||
background: rgba(233, 69, 96, 0.2);
|
||||
border-color: #e94560;
|
||||
}
|
||||
|
||||
#add-scene-btn {
|
||||
padding: 8px 16px;
|
||||
background: #533483;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
margin-left: 10px;
|
||||
.scene-item.active {
|
||||
background: rgba(233, 69, 96, 0.3);
|
||||
border-color: #e94560;
|
||||
box-shadow: 0 0 15px rgba(233, 69, 96, 0.3);
|
||||
}
|
||||
|
||||
#canvas-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
overflow: auto;
|
||||
/* === 时间轴 === */
|
||||
.timeline {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 16px;
|
||||
padding: 16px;
|
||||
margin-top: 20px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
#main-canvas {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.5);
|
||||
cursor: crosshair;
|
||||
.timeline h3 {
|
||||
color: #e94560;
|
||||
margin-bottom: 12px;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
/* ==================== 底部时间轴面板 ==================== */
|
||||
#timeline-panel {
|
||||
background: #16213e;
|
||||
border-top: 2px solid #0f3460;
|
||||
padding: 15px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
.timeline-controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* 播放控制栏 */
|
||||
#playback-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
#playback-controls button {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: #0f3460;
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#playback-controls button:hover {
|
||||
background: #e94560;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
#frame-counter {
|
||||
font-size: 14px;
|
||||
color: #aaa;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
#playback-controls label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 14px;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
#fps-slider {
|
||||
width: 120px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#fps-display {
|
||||
color: #e94560;
|
||||
font-weight: bold;
|
||||
min-width: 50px;
|
||||
}
|
||||
|
||||
/* 帧缩略图时间轴 */
|
||||
#timeline-frames {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 10px;
|
||||
background: #0f0f1a;
|
||||
border-radius: 6px;
|
||||
overflow-x: auto;
|
||||
min-height: 70px;
|
||||
align-items: center;
|
||||
.frames-container {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
overflow-x: auto;
|
||||
padding: 12px 0;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.frame-thumb {
|
||||
width: 80px;
|
||||
height: 60px;
|
||||
background: #0f3460;
|
||||
border: 2px solid #533483;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
color: #aaa;
|
||||
position: relative;
|
||||
transition: all 0.3s;
|
||||
flex-shrink: 0;
|
||||
min-width: 120px;
|
||||
height: 68px;
|
||||
background: #0f0f1a;
|
||||
border: 2px solid #533483;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
font-size: 0.8em;
|
||||
color: #aaa;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.frame-thumb:hover {
|
||||
border-color: #e94560;
|
||||
transform: translateY(-2px);
|
||||
border-color: #e94560;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.frame-thumb.active {
|
||||
border-color: #e94560;
|
||||
background: #533483;
|
||||
box-shadow: 0 0 10px rgba(233, 69, 96, 0.5);
|
||||
border-color: #e94560;
|
||||
background: rgba(233, 69, 96, 0.2);
|
||||
box-shadow: 0 0 15px rgba(233, 69, 96, 0.3);
|
||||
}
|
||||
|
||||
.frame-thumb::before {
|
||||
content: attr(data-frame);
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 4px;
|
||||
font-size: 10px;
|
||||
color: #fff;
|
||||
background: rgba(0,0,0,0.5);
|
||||
padding: 1px 4px;
|
||||
border-radius: 2px;
|
||||
.frame-index {
|
||||
font-size: 1.2em;
|
||||
font-weight: bold;
|
||||
color: #e94560;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
/* 时间轴操作按钮 */
|
||||
#timeline-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
/* === 预览模式样式 === */
|
||||
.preview-mode .left-panel,
|
||||
.preview-mode .right-panel,
|
||||
.preview-mode .timeline,
|
||||
.preview-mode .canvas-controls .btn:not(#exitPreviewBtn):not(#playPreviewBtn):not(#pausePreviewBtn):not(#stopPreviewBtn) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
#timeline-actions button {
|
||||
padding: 8px 16px;
|
||||
background: #0f3460;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s;
|
||||
.preview-mode .canvas-area {
|
||||
grid-column: 1 / -1;
|
||||
justify-content: center;
|
||||
min-height: 80vh;
|
||||
}
|
||||
|
||||
#timeline-actions button:hover {
|
||||
background: #e94560;
|
||||
.preview-mode #mainCanvas {
|
||||
max-width: 1200px;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
/* ==================== 画布上的素材样式 ==================== */
|
||||
.canvas-asset {
|
||||
position: absolute;
|
||||
cursor: move;
|
||||
user-select: none;
|
||||
font-size: 48px;
|
||||
filter: drop-shadow(2px 2px 4px rgba(0,0,0,0.3));
|
||||
transition: transform 0.1s;
|
||||
.preview-mode .preview-controls {
|
||||
display: flex !important;
|
||||
gap: 12px;
|
||||
margin-top: 20px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* === 分享面板样式 === */
|
||||
.share-panel {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: #1a1a2e;
|
||||
border: 2px solid #e94560;
|
||||
border-radius: 24px;
|
||||
padding: 30px;
|
||||
z-index: 1000;
|
||||
min-width: 400px;
|
||||
box-shadow: 0 30px 60px rgba(0, 0, 0, 0.8);
|
||||
backdrop-filter: blur(20px);
|
||||
}
|
||||
|
||||
.share-panel h3 {
|
||||
color: #e94560;
|
||||
margin-bottom: 20px;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.share-url {
|
||||
background: #0a0a14;
|
||||
border: 1px solid #533483;
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
margin: 15px 0;
|
||||
word-break: break-all;
|
||||
font-size: 0.9em;
|
||||
color: #fff;
|
||||
max-height: 100px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.share-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: flex-end;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* === 遮罩层 === */
|
||||
.overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
backdrop-filter: blur(5px);
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
/* === 工具类 === */
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* === canvas-asset(来自原来文件,保留) === */
|
||||
.canvas-asset:hover {
|
||||
transform: scale(1.1);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.canvas-asset.selected {
|
||||
outline: 2px dashed #e94560;
|
||||
outline-offset: 4px;
|
||||
outline: 2px dashed #e94560;
|
||||
outline-offset: 4px;
|
||||
}
|
||||
|
||||
/* ==================== 滚动条样式 ==================== */
|
||||
/* === 滚动条样式(来自原来文件,保留) === */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #0f0f1a;
|
||||
background: #0f0f1a;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #533483;
|
||||
border-radius: 4px;
|
||||
background: #533483;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #e94560;
|
||||
background: #e94560;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,84 +3,106 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>HoloLake 动态漫制作系统 - 环节5</title>
|
||||
<title>HoloLake 动态漫制作系统 - 环节6</title>
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<!-- 顶部工具栏 -->
|
||||
<header id="toolbar">
|
||||
<h1>🎬 HoloLake 动态漫制作系统</h1>
|
||||
<div class="toolbar-actions">
|
||||
<button onclick="exportScene()">📤 导出JSON</button>
|
||||
<button onclick="importScene()">📥 导入JSON</button>
|
||||
<button onclick="exportScreenshot()">📷 截图</button>
|
||||
<input type="file" id="import-file" style="display:none" accept=".json">
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 主工作区 -->
|
||||
<main id="workspace">
|
||||
<div class="container">
|
||||
<h1>🎬 HoloLake 动态漫制作系统</h1>
|
||||
|
||||
<!-- 主布局容器 -->
|
||||
<div class="main-layout">
|
||||
<!-- 左侧素材面板 -->
|
||||
<aside id="assets-panel">
|
||||
<h3>🎨 素材库</h3>
|
||||
<div class="asset-category">
|
||||
<h4>角色</h4>
|
||||
<div class="asset-items">
|
||||
<div class="asset-item" draggable="true" data-type="character" data-emoji="😊">😊 开心</div>
|
||||
<div class="asset-item" draggable="true" data-type="character" data-emoji="😢">😢 难过</div>
|
||||
<div class="asset-item" draggable="true" data-type="character" data-emoji="😠">😠 生气</div>
|
||||
<div class="asset-item" draggable="true" data-type="character" data-emoji="😴">😴 睡觉</div>
|
||||
</div>
|
||||
<div class="left-panel">
|
||||
<h3>📦 素材库</h3>
|
||||
|
||||
<h3>角色</h3>
|
||||
<div class="material-grid">
|
||||
<div class="material-item" draggable="true" data-type="character" data-emoji="😊">😊 开心</div>
|
||||
<div class="material-item" draggable="true" data-type="character" data-emoji="😢">😢 难过</div>
|
||||
<div class="material-item" draggable="true" data-type="character" data-emoji="😠">😠 生气</div>
|
||||
<div class="material-item" draggable="true" data-type="character" data-emoji="😴">😴 睡觉</div>
|
||||
</div>
|
||||
<div class="asset-category">
|
||||
<h4>物品</h4>
|
||||
<div class="asset-items">
|
||||
<div class="asset-item" draggable="true" data-type="item" data-emoji="🌸">🌸 花</div>
|
||||
<div class="asset-item" draggable="true" data-type="item" data-emoji="⭐">⭐ 星星</div>
|
||||
<div class="asset-item" draggable="true" data-type="item" data-emoji="💎">💎 宝石</div>
|
||||
<div class="asset-item" draggable="true" data-type="item" data-emoji="📖">📖 书</div>
|
||||
</div>
|
||||
|
||||
<h3>物品</h3>
|
||||
<div class="material-grid">
|
||||
<div class="material-item" draggable="true" data-type="item" data-emoji="🌸">🌸 花</div>
|
||||
<div class="material-item" draggable="true" data-type="item" data-emoji="⭐">⭐ 星星</div>
|
||||
<div class="material-item" draggable="true" data-type="item" data-emoji="💎">💎 宝石</div>
|
||||
<div class="material-item" draggable="true" data-type="item" data-emoji="📖">📖 书</div>
|
||||
</div>
|
||||
<div class="asset-category">
|
||||
<h4>场景</h4>
|
||||
<div class="asset-items">
|
||||
<div class="asset-item" draggable="true" data-type="background" data-emoji="🏠">🏠 房子</div>
|
||||
<div class="asset-item" draggable="true" data-type="background" data-emoji="🌳">🌳 树林</div>
|
||||
<div class="asset-item" draggable="true" data-type="background" data-emoji="🏔️">🏔️ 山</div>
|
||||
</div>
|
||||
|
||||
<h3>场景</h3>
|
||||
<div class="material-grid">
|
||||
<div class="material-item" draggable="true" data-type="background" data-emoji="🏠">🏠 房子</div>
|
||||
<div class="material-item" draggable="true" data-type="background" data-emoji="🌳">🌳 树林</div>
|
||||
<div class="material-item" draggable="true" data-type="background" data-emoji="⛰️">⛰️ 山</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 中间画布区域 -->
|
||||
<section id="canvas-container">
|
||||
<div id="scene-tabs">
|
||||
<div id="tabs-list"></div>
|
||||
<button onclick="addScene()" id="add-scene-btn">+ 新场景</button>
|
||||
<div class="canvas-area">
|
||||
<canvas id="mainCanvas" width="800" height="450"></canvas>
|
||||
|
||||
<!-- 画布控制栏 -->
|
||||
<div class="canvas-controls">
|
||||
<button class="btn" onclick="exportScreenshot()">📷 截图</button>
|
||||
<button class="btn" onclick="exportScene()">📤 导出JSON</button>
|
||||
<button class="btn" onclick="document.getElementById('import-file').click()">📥 导入JSON</button>
|
||||
<input type="file" id="import-file" style="display:none" accept=".json" onchange="importScene(event)">
|
||||
<button class="btn btn-primary" id="previewBtn">👁️ 预览</button>
|
||||
<button class="btn btn-primary" id="shareBtn">🔗 分享作品</button>
|
||||
</div>
|
||||
<div id="canvas-wrapper">
|
||||
<canvas id="main-canvas" width="800" height="600"></canvas>
|
||||
|
||||
<!-- 预览模式专用控制按钮(默认隐藏) -->
|
||||
<div class="preview-controls hidden">
|
||||
<button class="btn btn-primary" id="playPreviewBtn">▶️ 播放</button>
|
||||
<button class="btn" id="pausePreviewBtn">⏸️ 暂停</button>
|
||||
<button class="btn" id="stopPreviewBtn">⏹️ 停止</button>
|
||||
<button class="btn" id="exitPreviewBtn">✖️ 退出预览</button>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<!-- 底部时间轴面板 -->
|
||||
<footer id="timeline-panel">
|
||||
<div id="playback-controls">
|
||||
<button onclick="stopAnimation()" title="停止(回到第1帧)">⏹️</button>
|
||||
<button onclick="togglePlayback()" id="btn-play" title="播放/暂停">▶️</button>
|
||||
<span id="frame-counter">帧 1/1</span>
|
||||
<label>速度:
|
||||
<input type="range" id="fps-slider" min="1" max="12" value="4" oninput="setFPS(this.value)">
|
||||
<span id="fps-display">4 FPS</span>
|
||||
</label>
|
||||
</div>
|
||||
<div id="timeline-frames"></div>
|
||||
<div id="timeline-actions">
|
||||
<button onclick="addFrame()" title="添加新帧(复制当前帧)">➕ 新帧</button>
|
||||
<button onclick="deleteFrame()" title="删除当前帧">🗑️ 删帧</button>
|
||||
|
||||
<!-- 右侧场景面板 -->
|
||||
<div class="right-panel">
|
||||
<h3>🎬 场景列表</h3>
|
||||
<div class="scene-list" id="sceneList"></div>
|
||||
<button class="btn" onclick="addScene()" style="width:100%; margin-top:10px;">➕ 新建场景</button>
|
||||
|
||||
<!-- 时间轴预览区域 -->
|
||||
<div class="timeline">
|
||||
<h3>⏱️ 时间轴</h3>
|
||||
<div class="timeline-controls">
|
||||
<button class="btn" onclick="addFrame()" style="padding:5px 15px;">➕ 新帧</button>
|
||||
<button class="btn" onclick="deleteFrame()" style="padding:5px 15px;">🗑️ 删帧</button>
|
||||
<button class="btn" onclick="togglePlayback()" id="timelinePlayBtn" style="padding:5px 15px;">▶️ 播放</button>
|
||||
</div>
|
||||
<div class="frames-container" id="framesContainer"></div>
|
||||
<div style="margin-top:10px; display:flex; align-items:center; gap:10px;">
|
||||
<span>速度:</span>
|
||||
<input type="range" id="fpsSlider" min="1" max="12" value="4" style="flex:1;">
|
||||
<span id="fpsValue">4 FPS</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分享弹窗(默认隐藏) -->
|
||||
<div id="shareOverlay" class="overlay hidden"></div>
|
||||
<div id="sharePanel" class="share-panel hidden">
|
||||
<h3>🔗 分享作品</h3>
|
||||
<div>
|
||||
<button class="btn" id="generateUrlBtn" style="margin-right:10px;">生成分享链接</button>
|
||||
<button class="btn" id="generateEmbedBtn">生成嵌入代码</button>
|
||||
</div>
|
||||
<div id="shareUrlContainer" style="margin-top:20px;">
|
||||
<div class="share-url" id="shareUrl"></div>
|
||||
<div class="share-actions">
|
||||
<button class="btn" id="copyShareBtn" style="display:none;">📋 复制</button>
|
||||
<button class="btn" id="closeShareBtn">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="js/app.js"></script>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,584 @@
|
|||
// ==================== 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();cd ~/Desktop/guanghulab/dynamic-comic-studio
|
||||
|
||||
|
||||
|
||||
// 导出 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);
|
||||
Loading…
Reference in New Issue