imlixinyang commited on
Commit
e14ba11
·
verified ·
1 Parent(s): 918938f

Upload 9 files

Browse files
examples/1.json ADDED
The diff for this file is too large to render. See raw diff
 
examples/2.json ADDED
The diff for this file is too large to render. See raw diff
 
examples/3.json ADDED
The diff for this file is too large to render. See raw diff
 
examples/4.json ADDED
The diff for this file is too large to render. See raw diff
 
examples/5.json ADDED
The diff for this file is too large to render. See raw diff
 
examples/6.json ADDED
The diff for this file is too large to render. See raw diff
 
examples/7.json ADDED
The diff for this file is too large to render. See raw diff
 
examples/8.json ADDED
The diff for this file is too large to render. See raw diff
 
index.html CHANGED
@@ -1,19 +1,2360 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>FlashWorld Demo</title>
7
+ <meta name="description" content="">
8
+ <style>
9
+ body {
10
+ margin: 0;
11
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
12
+ background: #1a1a1a;
13
+ color: #ffffff;
14
+ overflow: hidden;
15
+ }
16
+
17
+ .main-container {
18
+ display: flex;
19
+ height: 100vh;
20
+ flex-direction: column;
21
+ }
22
+
23
+ .header {
24
+ background: rgba(0, 0, 0, 0.8);
25
+ padding: 15px 20px;
26
+ text-align: center;
27
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
28
+ flex-shrink: 0;
29
+ }
30
+
31
+ .header h1 {
32
+ margin: 0;
33
+ color: white;
34
+ font-size: 1.8em;
35
+ font-weight: 600;
36
+ margin-bottom: 8px;
37
+ }
38
+ .header-title-wrap {
39
+ display: inline-flex;
40
+ align-items: center;
41
+ gap: 8px;
42
+ position: relative;
43
+ }
44
+
45
+ .header-links {
46
+ display: flex;
47
+ justify-content: center;
48
+ gap: 20px;
49
+ margin-top: 8px;
50
+ }
51
+
52
+ .header-links a {
53
+ color: #60a5fa;
54
+ text-decoration: none;
55
+ font-size: 0.9em;
56
+ padding: 5px 10px;
57
+ border: 1px solid #60a5fa;
58
+ border-radius: 5px;
59
+ transition: all 0.3s ease;
60
+ }
61
+
62
+ .header-links a:hover {
63
+ background: #60a5fa;
64
+ color: white;
65
+ }
66
+
67
+ .content-container {
68
+ display: flex;
69
+ flex: 1;
70
+ overflow: visible; /* Allow tooltips to extend beyond container */
71
+ }
72
+
73
+ .left-panel {
74
+ width: 280px;
75
+ background: rgba(0, 0, 0, 0.7);
76
+ border-right: 1px solid rgba(255, 255, 255, 0.1);
77
+ padding: 20px;
78
+ overflow-y: auto;
79
+ overflow-x: visible; /* Allow tooltips to extend beyond panel */
80
+ flex-shrink: 0;
81
+ }
82
+
83
+ .center-panel {
84
+ flex: 1;
85
+ position: relative;
86
+ background: #000;
87
+ display: flex;
88
+ justify-content: center;
89
+ align-items: center;
90
+ z-index: 1; /* Lower z-index to allow tooltips to appear above */
91
+ }
92
+
93
+ .right-panel {
94
+ width: 300px;
95
+ background: rgba(0, 0, 0, 0.7);
96
+ border-left: 1px solid rgba(255, 255, 255, 0.1);
97
+ padding: 20px;
98
+ overflow-y: auto;
99
+ flex-shrink: 0;
100
+ z-index: 1; /* Lower z-index to allow tooltips to appear above */
101
+ }
102
+
103
+ .guidance {
104
+ color: #e5e7eb;
105
+ }
106
+
107
+ .guidance h2 {
108
+ color: #ffffff;
109
+ margin-top: 0;
110
+ font-size: 1.3em;
111
+ border-bottom: 2px solid #60a5fa;
112
+ padding-bottom: 8px;
113
+ margin-bottom: 20px;
114
+ }
115
+
116
+ .gui-container h2{
117
+ color: #ffffff;
118
+ margin-top: 0;
119
+ font-size: 1.3em;
120
+ border-bottom: 2px solid #60fae5;
121
+ padding-bottom: 8px;
122
+ margin-bottom: 20px;
123
+ }
124
+
125
+ .step {
126
+ margin: 12px 0;
127
+ padding: 12px;
128
+ background: rgba(96, 165, 250, 0.1);
129
+ border-radius: 6px;
130
+ border-left: 3px solid #60a5fa;
131
+ }
132
+
133
+ .step h3 {
134
+ margin: 0 0 8px 0;
135
+ color: #ffffff;
136
+ font-size: 1em;
137
+ }
138
+
139
+ .step p {
140
+ margin: 4px 0;
141
+ line-height: 1.4;
142
+ font-size: 0.85em;
143
+ color: #d1d5db;
144
+ }
145
+
146
+ .controls-info {
147
+ background: rgba(168, 85, 247, 0.1);
148
+ border-left: 3px solid #a855f7;
149
+ }
150
+
151
+ .keyboard-shortcuts {
152
+ background: rgba(34, 197, 94, 0.1);
153
+ border-left: 3px solid #22c55e;
154
+ }
155
+
156
+ .loading {
157
+ position: absolute;
158
+ top: 50%;
159
+ left: 50%;
160
+ min-width: 300px;
161
+ min-height: 200px;
162
+ transform: translate(-50%, -50%);
163
+ background: rgba(0, 0, 0, 0.9);
164
+ color: white;
165
+ padding: 20px;
166
+ border-radius: 10px;
167
+ display: none;
168
+ z-index: 1000;
169
+ text-align: center;
170
+ vertical-align: middle;
171
+ }
172
+
173
+ .generation-info {
174
+ background: rgba(34, 197, 94, 0.1);
175
+ border: 1px solid #22c55e;
176
+ border-radius: 8px;
177
+ padding: 15px;
178
+ margin: 10px 0;
179
+ color: #22c55e;
180
+ font-family: 'Courier New', monospace;
181
+ font-size: 0.9em;
182
+ }
183
+
184
+ .progress-container {
185
+ width: 100%;
186
+ background: rgba(255, 255, 255, 0.1);
187
+ border-radius: 10px;
188
+ overflow: hidden;
189
+ margin: 10px 0;
190
+ position: relative;
191
+ }
192
+
193
+ .progress-bar {
194
+ height: 20px;
195
+ background: linear-gradient(90deg, #60a5fa, #3b82f6);
196
+ width: 0%;
197
+ transition: width 0.3s ease;
198
+ border-radius: 10px;
199
+ position: relative;
200
+ }
201
+
202
+ .progress-text {
203
+ position: absolute;
204
+ top: 50%;
205
+ left: 50%;
206
+ transform: translate(-50%, -50%);
207
+ color: white;
208
+ font-weight: bold;
209
+ font-size: 0.8em;
210
+ white-space: nowrap;
211
+ }
212
+
213
+ /* Info tooltip */
214
+ .info-tip {
215
+ display: inline-block;
216
+ position: relative;
217
+ margin-left: 8px;
218
+ width: 16px;
219
+ height: 16px;
220
+ line-height: 16px;
221
+ text-align: center;
222
+ border-radius: 50%;
223
+ background: #3b82f6;
224
+ color: #fff;
225
+ font-size: 12px;
226
+ cursor: default;
227
+ user-select: none;
228
+ z-index: 100000; /* Ensure the tip itself is above everything */
229
+ }
230
+ .info-tip .tooltip {
231
+ display: none;
232
+ position: absolute;
233
+ left: 0;
234
+ top: calc(100% + 8px); /* show below the icon */
235
+ transform: none;
236
+ background: rgba(0,0,0,0.95);
237
+ color: #e5e7eb;
238
+ border: 1px solid rgba(255,255,255,0.2);
239
+ border-radius: 8px;
240
+ padding: 10px 12px;
241
+ font-size: 12px;
242
+ width: 480px;
243
+ white-space: normal;
244
+ z-index: 999999; /* Even higher z-index to ensure it's above everything */
245
+ box-shadow: 0 8px 24px rgba(0,0,0,0.6);
246
+ text-align: left;
247
+ }
248
+ .info-tip:hover .tooltip {
249
+ display: block;
250
+ }
251
+
252
+ .status-bar {
253
+ background: rgba(0, 0, 0, 0.9);
254
+ color: #60a5fa;
255
+ padding: 8px 15px;
256
+ font-family: 'Courier New', monospace;
257
+ font-size: 0.8em;
258
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
259
+ flex-shrink: 0;
260
+ }
261
+
262
+ .canvas-container {
263
+ width: 100%;
264
+ height: 100%;
265
+ display: flex;
266
+ justify-content: center;
267
+ align-items: center;
268
+ background:
269
+ repeating-linear-gradient(
270
+ 45deg,
271
+ #1a1a1a 0px,
272
+ #1a1a1a 10px,
273
+ #2a2a2a 10px,
274
+ #2a2a2a 20px
275
+ );
276
+ position: relative;
277
+ }
278
+
279
+ .canvas-wrapper {
280
+ position: relative;
281
+ border: 2px solid #444;
282
+ background: #111;
283
+ box-shadow:
284
+ 0 0 20px rgba(0, 0, 0, 0.5),
285
+ inset 0 0 10px rgba(0, 0, 0, 0.3);
286
+ border-radius: 4px;
287
+ }
288
+
289
+ .canvas-wrapper canvas {
290
+ display: block;
291
+ border-radius: 2px;
292
+ }
293
+
294
+ /* Add a subtle animation to the canvas wrapper */
295
+ .canvas-wrapper:hover {
296
+ border-color: #666;
297
+ box-shadow:
298
+ 0 0 30px rgba(0, 0, 0, 0.7),
299
+ inset 0 0 15px rgba(0, 0, 0, 0.4);
300
+ }
301
+
302
+ /* Progress & status beautify */
303
+ .progress-container {
304
+ width: 100%;
305
+ height: 18px;
306
+ background: linear-gradient(180deg, rgba(255,255,255,0.06), rgba(255,255,255,0.02));
307
+ border: 1px solid rgba(255,255,255,0.12);
308
+ border-radius: 999px;
309
+ overflow: hidden;
310
+ box-shadow: 0 2px 10px rgba(0,0,0,0.35) inset;
311
+ position: relative;
312
+ }
313
+ .progress-bar {
314
+ height: 100%;
315
+ background: linear-gradient(90deg, #60a5fa, #8b5cf6);
316
+ box-shadow: 0 0 10px rgba(96,165,250,0.65);
317
+ position: relative;
318
+ transition: width .15s ease;
319
+ }
320
+ .progress-text {
321
+ position: absolute;
322
+ top: 50%;
323
+ left: 50%;
324
+ transform: translate(-50%, -50%);
325
+ font-size: 11px;
326
+ color: #f8fafc;
327
+ text-shadow: 0 1px 2px rgba(0,0,0,0.5);
328
+ pointer-events: none;
329
+ white-space: nowrap;
330
+ }
331
+
332
+ .status-badges {
333
+ display: flex;
334
+ gap: 8px;
335
+ flex-wrap: wrap;
336
+ margin-top: 8px;
337
+ }
338
+ .badge {
339
+ display: inline-flex;
340
+ align-items: center;
341
+ gap: 6px;
342
+ padding: 6px 10px;
343
+ border-radius: 8px;
344
+ font-size: 12px;
345
+ border: 1px solid rgba(255,255,255,0.12);
346
+ background: rgba(255,255,255,0.06);
347
+ }
348
+ .badge .dot { width: 8px; height: 8px; border-radius: 999px; }
349
+ .badge.queue .dot { background: #f59e0b; }
350
+ .badge.running .dot { background: #22c55e; }
351
+ .badge.time .dot { background: #60a5fa; }
352
+ .badge.bytes .dot { background: #a78bfa; }
353
+
354
+ .details-grid {
355
+ display: grid;
356
+ grid-template-columns: repeat(2, minmax(0, 1fr));
357
+ gap: 6px 12px;
358
+ margin-top: 8px;
359
+ font-size: 12px;
360
+ color: #cbd5e1;
361
+ }
362
+ .details-grid div { opacity: 0.9; }
363
+
364
+ /* Canvas resizing indicator */
365
+ .canvas-wrapper.resizing {
366
+ border-color: #60a5fa;
367
+ box-shadow:
368
+ 0 0 25px rgba(96, 165, 250, 0.3),
369
+ inset 0 0 10px rgba(96, 165, 250, 0.1);
370
+ }
371
+
372
+ .canvas-wrapper.resizing::after {
373
+ content: "Resizing...";
374
+ position: absolute;
375
+ top: 50%;
376
+ left: 50%;
377
+ transform: translate(-50%, -50%);
378
+ color: #60a5fa;
379
+ font-size: 12px;
380
+ font-weight: bold;
381
+ z-index: 10;
382
+ pointer-events: none;
383
+ }
384
+
385
+ /* GUI Panel Styling */
386
+ .gui-panel {
387
+ background: rgba(0, 0, 0, 0.8);
388
+ border-radius: 8px;
389
+ padding: 15px;
390
+ min-height: 400px;
391
+ }
392
+
393
+ .gui-panel .lil-gui {
394
+ --background-color: rgba(0, 0, 0, 0.8);
395
+ --text-color: #ffffff;
396
+ --title-background-color: rgba(96, 165, 250, 0.2);
397
+ --title-text-color: #ffffff;
398
+ --widget-color: rgba(96, 165, 250, 0.3);
399
+ --hover-color: rgba(96, 165, 250, 0.5);
400
+ }
401
+
402
+ /* Ensure GUI is visible */
403
+ .lil-gui {
404
+ position: relative !important;
405
+ z-index: 1000 !important;
406
+ }
407
+
408
+ /* Examples gallery */
409
+ .examples-section {
410
+ margin-top: 16px;
411
+ padding-top: 12px;
412
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
413
+ }
414
+ .examples-section h3 {
415
+ margin: 0 0 8px 0;
416
+ color: #ffffff;
417
+ font-size: 1em;
418
+ }
419
+ .examples-grid {
420
+ display: grid;
421
+ grid-template-columns: repeat(4, 1fr);
422
+ gap: 8px;
423
+ }
424
+ .example-item {
425
+ position: relative;
426
+ width: 100%;
427
+ padding-top: 100%;
428
+ background: rgba(255,255,255,0.06);
429
+ border: 1px solid rgba(255,255,255,0.12);
430
+ border-radius: 6px;
431
+ overflow: hidden;
432
+ cursor: pointer;
433
+ transition: transform 0.15s ease, box-shadow 0.15s ease;
434
+ }
435
+ .example-item:hover {
436
+ transform: translateY(-2px);
437
+ box-shadow: 0 6px 16px rgba(0,0,0,0.35);
438
+ }
439
+ .example-item img {
440
+ position: absolute;
441
+ top: 0; left: 0; right: 0; bottom: 0;
442
+ width: 100%; height: 100%; object-fit: cover;
443
+ }
444
+ .example-item .label {
445
+ position: absolute;
446
+ bottom: 0; left: 0; right: 0;
447
+ background: linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(0,0,0,0.65) 100%);
448
+ color: #e5e7eb;
449
+ font-size: 11px;
450
+ padding: 4px 6px;
451
+ text-align: center;
452
+ }
453
+
454
+ @media (max-width: 1200px) {
455
+ .left-panel {
456
+ width: 250px;
457
+ }
458
+
459
+ .right-panel {
460
+ width: 280px;
461
+ }
462
+ }
463
+
464
+ @media (max-width: 768px) {
465
+ .content-container {
466
+ flex-direction: column;
467
+ }
468
+
469
+ .left-panel, .right-panel {
470
+ width: 100%;
471
+ height: auto;
472
+ max-height: 200px;
473
+ }
474
+
475
+ .center-panel {
476
+ flex: 1;
477
+ min-height: 400px;
478
+ }
479
+ }
480
+ </style>
481
+ <script type="importmap">
482
+ {
483
+ "imports": {
484
+ "three": "https://cdnjs.cloudflare.com/ajax/libs/three.js/0.178.0/three.module.js",
485
+ "@sparkjsdev/spark": "https://sparkjs.dev/releases/spark/0.1.9/spark.module.js",
486
+ "lil-gui": "https://cdn.jsdelivr.net/npm/[email protected]/+esm"
487
+ }
488
+ }
489
+ </script>
490
+ </head>
491
+ <body>
492
+ <div class="main-container">
493
+ <!-- Header Section -->
494
+ <header class="header">
495
+ <div style="display: flex; justify-content: space-between; align-items: center; width: 100%;">
496
+ <h1 style="margin: 0; flex: 1; text-align: left;">
497
+ <span class="header-title-wrap">FlashWorld Spark Demo
498
+ <span class="info-tip">!
499
+ <span class="tooltip" style="max-width: 260px; text-align: left;">Front-end real-time rendering in Spark uses compressed and pruned Gaussians. Visual quality in this demo may be lower than offline/back-end rendering.
500
+ </span>
501
+ </span>
502
+ </span>
503
+ </h1>
504
+ <div class="header-links" style="margin-left: 20px;">
505
+ <a href="https://arxiv.org/pdf/2510.13678" target="_blank">Paper</a>
506
+ <a href="https://github.com/imlixinyang/FlashWorld" target="_blank">Code</a>
507
+ <a href="https://imlixinyang.github.io/FlashWorld-Project-Page/" target="_blank">Project Page</a>
508
+ </div>
509
+ </div>
510
+ </header>
511
+
512
+ <!-- Main Content Container -->
513
+ <div class="content-container">
514
+ <!-- Left Panel: Simplified Guidance -->
515
+ <div class="left-panel">
516
+ <div class="guidance">
517
+ <h2>Instructions</h2>
518
+
519
+ <div class="step">
520
+ <h3>1. Configure</h3>
521
+ <p>Set FOV and Resolution and Click "Fix Configurations"</p>
522
+ <p><strong>Important: You also need to specify your Hugging Face Access Token with READ permission to use the online free ZeroGPU service.</strong></p>
523
+ </div>
524
+
525
+
526
+ <div class="step">
527
+ <h3>2. Set Camera Trajectory</h3>
528
+ <p><b>Manual:</b> Navigate with mouse and keyboard, press <kbd>Space</kbd> to record</p>
529
+ <p><b>Template:</b> Select template type and click "Generate Trajectory"</p>
530
+ <p><b>JSON:</b> Load trajectory from JSON file</p>
531
+ </div>
532
+
533
+ <div class="step">
534
+ <h3>3. Add Prompts</h3>
535
+ <p>Upload image or enter text description</p>
536
+ </div>
537
+
538
+ <div class="step">
539
+ <h3>4. Generate</h3>
540
+ <p>Click "Generate!" to create your scene</p>
541
+ </div>
542
+
543
+ <div class="step controls-info">
544
+ <h3>Controls</h3>
545
+ <p><strong>Mouse/QE:</strong> Rotate view</p>
546
+ <p><strong>WASD/RF:</strong> Move</p>
547
+ <p><strong>Space:</strong> Record camera</p>
548
+ </div>
549
+
550
+ </div>
551
+
552
+ <!-- Examples Gallery -->
553
+ <div id="examples-section" class="examples-section">
554
+ <h3>Examples</h3>
555
+ <div id="examples-grid" class="examples-grid"></div>
556
+ </div>
557
+ </div>
558
+
559
+ <!-- Center Panel: Canvas -->
560
+ <div class="center-panel">
561
+ <div class="canvas-container" id="canvas-container">
562
+ <div class="canvas-wrapper" id="canvas-wrapper">
563
+ <div class="loading" id="loading">
564
+ <h3>🎬 Generating Scene...</h3>
565
+ <p>Please wait while we create your 3D scene</p>
566
+ <div id="generation-info" class="generation-info" style="display: none;">
567
+ <div><strong>Generation Time:</strong> <span id="generation-time">-</span> seconds</div>
568
+ <div><strong>File Size:</strong> <span id="file-size">-</span> MB</div>
569
+ </div>
570
+ <div id="download-progress" style="display: none;">
571
+ <div class="progress-container">
572
+ <div class="progress-bar" id="progress-bar"></div>
573
+ <div class="progress-text" id="progress-text">0%</div>
574
+ </div>
575
+ <div class="status-badges" id="status-badges" style="display: none;">
576
+ <div class="badge queue" id="badge-queue"><span class="dot"></span><span id="badge-queue-text">Queue</span></div>
577
+ <div class="badge running" id="badge-running" style="display: none;"><span class="dot"></span><span id="badge-running-text">Running</span></div>
578
+ <div class="badge time" id="badge-time" style="display: none;"><span class="dot"></span><span id="badge-time-text">00:00</span></div>
579
+ </div>
580
+ <div id="queue-details" class="details-grid" style="display: none;"></div>
581
+ <div id="download-details" class="details-grid" style="display: none;"></div>
582
+ </div>
583
+ </div>
584
+ </div>
585
+ </div>
586
+ </div>
587
+
588
+ <!-- Right Panel: GUI -->
589
+ <div class="right-panel">
590
+ <div class="gui-container">
591
+ <!-- <h2>GUI</h2> -->
592
+ <div class="gui-panel" id="gui-container">
593
+ <!-- GUI will be inserted here -->
594
+ </div>
595
+ </div>
596
+
597
+ <!-- Image Preview Area -->
598
+ <div id="image-preview-area" style="padding: 10px; display: none;">
599
+ <div style="font-size: 12px; color: #ccc; margin-bottom: 8px; text-align: left;">Input Image Preview</div>
600
+ <div style="text-align: center;">
601
+ <img id="preview-img" style="max-width: 100%; max-height: 200px; border-radius: 4px; box-shadow: 0 2px 8px rgba(0,0,0,0.3);" />
602
+ </div>
603
+ </div>
604
+ </div>
605
+ </div>
606
+
607
+ <!-- Status Bar -->
608
+ <div class="status-bar" id="status-bar">
609
+ Ready to generate 3D scenes | Cameras: 0 | Status: Waiting for input
610
+ </div>
611
+ </div>
612
+
613
+ <!-- Hidden File Inputs -->
614
+ <input id="file-input" type="file" accept=".jpg,.png,.jpeg" multiple="true" style="display: none;" />
615
+ <input id="json-input" type="file" accept=".json" multiple="false" style="display: none;" />
616
+
617
+ <script type="module">
618
+ // =========================
619
+ // Imports & Global Variables
620
+ // =========================
621
+ import * as THREE from "three";
622
+ import { SplatMesh, SparkControls, textSplats } from "@sparkjsdev/spark";
623
+ import GUI from "lil-gui";
624
+
625
+ // Scene, Camera, Renderer, Controls
626
+ const scene = new THREE.Scene();
627
+ const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
628
+ camera.position.set(0, 0, 1.5);
629
+ const renderer = new THREE.WebGLRenderer();
630
+ renderer.setSize(window.innerWidth, window.innerHeight);
631
+
632
+ // Wait for DOM to be ready
633
+ function initializeRenderer() {
634
+ const canvasWrapper = document.getElementById('canvas-wrapper');
635
+ if (canvasWrapper) {
636
+ canvasWrapper.appendChild(renderer.domElement);
637
+
638
+ // Set initial canvas size based on current resolution
639
+ updateCanvasSize();
640
+ console.log('Canvas initialized in wrapper');
641
+ } else {
642
+ console.error('Canvas wrapper not found');
643
+ }
644
+ }
645
+
646
+ // Update canvas size based on selected resolution
647
+ function updateCanvasSize() {
648
+ const canvasWrapper = document.getElementById('canvas-wrapper');
649
+ if (!canvasWrapper) return;
650
+
651
+ // Show resizing indicator
652
+ canvasWrapper.classList.add('resizing');
653
+
654
+ // Get current resolution from GUI options
655
+ const resolution = guiOptions.Resolution.split('x');
656
+ const width = parseInt(resolution[2]) || 704; // W
657
+ const height = parseInt(resolution[1]) || 480; // H
658
+
659
+ // Set canvas size
660
+ renderer.setSize(width, height);
661
+ camera.aspect = width / height;
662
+ camera.updateProjectionMatrix();
663
+
664
+ // Update wrapper size to match canvas
665
+ canvasWrapper.style.width = width + 'px';
666
+ canvasWrapper.style.height = height + 'px';
667
+
668
+ // Remove resizing indicator after a short delay
669
+ setTimeout(() => {
670
+ canvasWrapper.classList.remove('resizing');
671
+ }, 300);
672
+
673
+ console.log('Canvas size updated:', width, 'x', height);
674
+ }
675
+
676
+ const controls = new SparkControls({ canvas: renderer.domElement });
677
+
678
+ // Camera splats and params
679
+ const cameraSplats = [];
680
+ const cameraParams = [];
681
+ const interpolatedCamerasSplats = [];
682
+
683
+ // State
684
+ let fixGenerationFOV = false;
685
+ let inputImageBase64 = null;
686
+ let inputImageResolution = null;
687
+ let currentGeneratedSplat = null; // 跟踪当前生成的场景
688
+
689
+ // UI Elements
690
+ const loadingElement = document.getElementById('loading');
691
+ const statusBar = document.getElementById('status-bar');
692
+
693
+ // GUI variable - declare early
694
+ let gui = null;
695
+
696
+ // Status update function
697
+ function updateStatus(message, cameraCount = null) {
698
+ const cameraText = cameraCount !== null ? `Cameras: ${cameraCount}` : `Cameras: ${cameraParams.length}`;
699
+ statusBar.textContent = `${message} | ${cameraText} | Status: ${fixGenerationFOV ? 'Ready to record' : 'Configure settings'}`;
700
+
701
+ // Update save trajectory button state
702
+ updateSaveTrajectoryButton();
703
+ }
704
+
705
+ // Update save trajectory button state based on camera count
706
+ function updateSaveTrajectoryButton() {
707
+ if (window.saveTrajectoryController) {
708
+ if (cameraParams.length >= 2) {
709
+ window.saveTrajectoryController.enable();
710
+ } else {
711
+ window.saveTrajectoryController.disable();
712
+ }
713
+ }
714
+ }
715
+
716
+ // Auth-aware fetch helper that injects Authorization header when HF_TOKEN is set
717
+ function fetchWithAuth(url, options = {}) {
718
+ const mergedOptions = { ...options };
719
+ const headers = new Headers(options && options.headers ? options.headers : undefined);
720
+ if (guiOptions && guiOptions.HF_TOKEN && String(guiOptions.HF_TOKEN).trim().length > 0) {
721
+ headers.set('Authorization', `Bearer ${guiOptions.HF_TOKEN}`);
722
+ }
723
+ mergedOptions.headers = headers;
724
+ return fetch(url, mergedOptions);
725
+ }
726
+
727
+ // Show/hide loading
728
+ function showLoading(show) {
729
+ loadingElement.style.display = show ? 'block' : 'none';
730
+ }
731
+
732
+ // Show generation info
733
+ function showGenerationInfo(generationTime, fileSize) {
734
+ const generationInfo = document.getElementById('generation-info');
735
+ const generationTimeElement = document.getElementById('generation-time');
736
+ const fileSizeElement = document.getElementById('file-size');
737
+
738
+ generationTimeElement.textContent = generationTime.toFixed(2);
739
+ fileSizeElement.textContent = (fileSize / (1024 * 1024)).toFixed(2);
740
+ generationInfo.style.display = 'block';
741
+ }
742
+
743
+ // Show download progress
744
+ function showDownloadProgress() {
745
+ const downloadProgress = document.getElementById('download-progress');
746
+ downloadProgress.style.display = 'block';
747
+ const qd = document.getElementById('queue-details');
748
+ const dd = document.getElementById('download-details');
749
+ const badges = document.getElementById('status-badges');
750
+ if (qd) qd.style.display = 'none';
751
+ if (dd) dd.style.display = 'none';
752
+ if (badges) badges.style.display = 'none';
753
+ }
754
+
755
+ // Update progress bar
756
+ function updateProgressBar(percentage) {
757
+ const progressBar = document.getElementById('progress-bar');
758
+ const progressText = document.getElementById('progress-text');
759
+
760
+ progressBar.style.width = percentage + '%';
761
+ progressText.textContent = `${Math.round(percentage)}%`;
762
+ }
763
+
764
+ // Update progress label text (stage indicator)
765
+ function setProgressLabel(text) {
766
+ const progressText = document.getElementById('progress-text');
767
+ if (progressText) progressText.textContent = text;
768
+ }
769
+
770
+ // ==============
771
+ // Queue handling
772
+ // ==============
773
+ let queuePollTimer = null;
774
+ let currentTaskId = null;
775
+ let initialQueuePosition = null;
776
+ let latestGenerationTime = null;
777
+ let lastDownloadPct = 0;
778
+ let lastDownloadUpdateTs = 0;
779
+
780
+ function showQueueWaiting(position, runningCount, queuedCount) {
781
+ // Use only the progress bar to show queue progress (from initial position to 0)
782
+ showDownloadProgress();
783
+ if (initialQueuePosition === null) {
784
+ // Initialize from first seen position; ensure >= 1 so 0 -> 100%
785
+ const initPos = (typeof position === 'number') ? position : 0;
786
+ initialQueuePosition = Math.max(initPos, 1);
787
+ }
788
+ const percent = initialQueuePosition && initialQueuePosition > 0
789
+ ? Math.max(0, Math.min(100, ((initialQueuePosition - (position || 0)) / initialQueuePosition) * 100))
790
+ : 0;
791
+ updateProgressBar(percent);
792
+ const totalWaiting = (position || 0) + (queuedCount || 0);
793
+ if (position !== null && position !== undefined) {
794
+ const pctText = `${Math.round(percent)}%`;
795
+ if (totalWaiting > 0) {
796
+ setProgressLabel(`Queued ${position}/${totalWaiting} (${pctText})`);
797
+ } else {
798
+ setProgressLabel(`Queued ${position} (${pctText})`);
799
+ }
800
+ } else {
801
+ setProgressLabel('Queued');
802
+ }
803
+ }
804
+
805
+ async function pollTaskUntilReady(taskId) {
806
+ currentTaskId = taskId;
807
+ initialQueuePosition = null;
808
+ if (queuePollTimer) {
809
+ clearInterval(queuePollTimer);
810
+ queuePollTimer = null;
811
+ }
812
+ const queueStartTs = Date.now();
813
+
814
+ const pollOnce = async () => {
815
+ try {
816
+ const resp = await fetchWithAuth(`${guiOptions.BackendAddress}/task/${taskId}`);
817
+ if (!resp.ok) return;
818
+ const info = await resp.json();
819
+ if (!info || !info.success) return;
820
+
821
+ const pos = info.queue && typeof info.queue.position === 'number' ? info.queue.position : 0;
822
+ const running = info.queue ? info.queue.running_count : 0;
823
+ const queued = info.queue ? info.queue.queued_count : 0;
824
+ if (info.status === 'queued' || info.status === 'running') {
825
+ // Only progress bar; set stage label
826
+ if (info.status === 'queued') {
827
+ showQueueWaiting(pos, running, queued);
828
+ } else {
829
+ // Transitioned to running: finalize queue progress visually
830
+ updateProgressBar(100);
831
+ showDownloadProgress();
832
+ setProgressLabel('Generating...');
833
+ }
834
+ }
835
+
836
+ if (info.status === 'completed' && info.download_url) {
837
+ clearInterval(queuePollTimer);
838
+ queuePollTimer = null;
839
+ latestGenerationTime = typeof info.generation_time === 'number' ? info.generation_time : null;
840
+ // Proceed to download the generated file like the normal path
841
+ updateStatus('Downloading generated scene...', cameraParams.length);
842
+ const response = await fetchWithAuth(guiOptions.BackendAddress + info.download_url);
843
+ if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
844
+ const contentLength = response.headers.get('content-length');
845
+ const total = parseInt(contentLength || '0', 10);
846
+ // Show generation info immediately once we know it and total size from headers
847
+ showGenerationInfo(latestGenerationTime || 0, total);
848
+ let loaded = 0;
849
+ const reader = response.body.getReader();
850
+ const chunks = [];
851
+ updateProgressBar(0);
852
+ setProgressLabel('Downloading 0%');
853
+ lastDownloadPct = 0;
854
+ lastDownloadUpdateTs = 0;
855
+ while (true) {
856
+ const { done, value } = await reader.read();
857
+ if (done) break;
858
+ chunks.push(value);
859
+ loaded += value.length;
860
+ if (total) {
861
+ const pct = Math.min(100, (loaded / total) * 100);
862
+ const now = Date.now();
863
+ const rounded = Math.round(pct);
864
+ // Throttle and enforce monotonic increase
865
+ if (rounded > Math.round(lastDownloadPct) || (now - lastDownloadUpdateTs) > 200) {
866
+ lastDownloadPct = Math.max(lastDownloadPct, pct);
867
+ updateProgressBar(lastDownloadPct);
868
+ setProgressLabel(`Downloading ${Math.round(lastDownloadPct)}%`);
869
+ lastDownloadUpdateTs = now;
870
+ }
871
+ }
872
+ }
873
+
874
+ if (instructionSplat) {
875
+ scene.remove(instructionSplat);
876
+ console.log('Instruction splat removed');
877
+ instructionSplat = null;
878
+ }
879
+
880
+ const blob = new Blob(chunks);
881
+ const url = URL.createObjectURL(blob);
882
+ // Continue to load the splat
883
+ updateStatus('Loading generated scene...', cameraParams.length);
884
+
885
+ const GeneratedSplat = new SplatMesh({ url });
886
+ scene.add(GeneratedSplat);
887
+ currentGeneratedSplat = GeneratedSplat;
888
+ updateStatus('Scene generated successfully!', cameraParams.length);
889
+ // Show generation time and total file size (MB)
890
+ showGenerationInfo(latestGenerationTime || 0, total || blob.size);
891
+ // Notify backend to delete the server file after client has downloaded it
892
+ try {
893
+ if (info.file_id) {
894
+ const resp = await fetchWithAuth(`${guiOptions.BackendAddress}/delete/${info.file_id}`, { method: 'POST' });
895
+ if (!resp.ok) console.warn('Delete notify failed');
896
+ }
897
+ } catch (e) {
898
+ console.warn('Delete notify error', e);
899
+ }
900
+ hideDownloadProgress();
901
+ showLoading(false);
902
+ } else if (info.status === 'failed') {
903
+ clearInterval(queuePollTimer);
904
+ queuePollTimer = null;
905
+ throw new Error(info.error || 'Generation failed');
906
+ }
907
+ } catch (e) {
908
+ console.debug('Polling error:', e);
909
+ }
910
+ };
911
+
912
+ await pollOnce();
913
+ queuePollTimer = setInterval(pollOnce, 2000);
914
+ }
915
+
916
+ // Hide download progress
917
+ function hideDownloadProgress() {
918
+ const downloadProgress = document.getElementById('download-progress');
919
+ downloadProgress.style.display = 'none';
920
+ }
921
+
922
+ // Playback scrubber (0..1)
923
+ let userCameraState = null; // 存储用户播放前的相机状态
924
+
925
+ // 根据时间比例获取插值相机
926
+ function getInterpolatedCameraAtTime(t) {
927
+ if (cameraParams.length === 0) {
928
+ return camera;
929
+ }
930
+
931
+ if (cameraParams.length === 1) {
932
+ return cameraParams[0];
933
+ }
934
+
935
+ // 确保t在有效范围内
936
+ const clampedT = Math.max(0, Math.min(1, t));
937
+
938
+ // 计算在相机序列中的位置
939
+ const cameraIndex = clampedT * (cameraParams.length - 1);
940
+ const startIndex = Math.min(Math.floor(cameraIndex), cameraParams.length - 2);
941
+ const endIndex = startIndex + 1;
942
+ const startCamera = cameraParams[startIndex];
943
+ const endCamera = cameraParams[endIndex];
944
+
945
+ // 计算两个相机之间的插值比例
946
+ const _t = cameraIndex - startIndex;
947
+
948
+ // 使用interpolateTwoCameras进行插值
949
+ return interpolateTwoCameras(startCamera, endCamera, _t);
950
+ }
951
+
952
+ function setCameraByScrub(t) {
953
+ if (cameraParams.length === 0) return;
954
+ const clampedT = Math.max(0, Math.min(1, t));
955
+ const camT = getInterpolatedCameraAtTime(clampedT);
956
+ camera.position.copy(camT.position);
957
+ camera.quaternion.copy(camT.quaternion);
958
+ camera.fov = camT.fov;
959
+ camera.updateProjectionMatrix();
960
+ }
961
+
962
+ // Supported resolutions
963
+ const supportedResolutions = [
964
+ { frame: 24, width: 704, height: 480 },
965
+ { frame: 24, width: 480, height: 704 }
966
+ ];
967
+
968
+ // GUI Options - declare early
969
+ const guiOptions = {
970
+ // 后端地址,默认为本页面ip
971
+ BackendAddress: `${window.location.protocol}//${window.location.hostname}:7860`,
972
+ HF_TOKEN: "",
973
+ FOV: 60,
974
+ LoadFromJson: () => {
975
+ const jsonInput = document.querySelector("#json-input");
976
+ if (jsonInput) jsonInput.click();
977
+ },
978
+ LoadTrajectoryFromJson: () => {
979
+ if (!fixGenerationFOV) {
980
+ updateStatus('Warning: Please fix configuration first before loading trajectory', cameraParams.length);
981
+ return;
982
+ }
983
+ // 设置标志,表示只加载轨迹
984
+ window.loadTrajectoryOnly = true;
985
+ const jsonInput = document.querySelector("#json-input");
986
+ if (jsonInput) jsonInput.click();
987
+ },
988
+ fixGenerationFOV: () => {
989
+ // These controllers will be set when GUI is initialized
990
+ if (window.fixGenerationFOVController) window.fixGenerationFOVController.disable();
991
+ fixGenerationFOV = true;
992
+
993
+ const new_camera = new THREE.PerspectiveCamera(guiOptions.FOV, guiOptions.Resolution.split('x')[2] / guiOptions.Resolution.split('x')[1]);
994
+ new_camera.position.set(0, 0, 0);
995
+ new_camera.quaternion.set(0, 0, 0, 1);
996
+ new_camera.updateProjectionMatrix();
997
+
998
+ const cameraSplat = createCameraSplat(new_camera);
999
+ cameraSplats.push(cameraSplat);
1000
+ cameraParams.push({
1001
+ position: new_camera.position.clone(),
1002
+ quaternion: new_camera.quaternion.clone(),
1003
+ fov: new_camera.fov,
1004
+ aspect: new_camera.aspect,
1005
+ });
1006
+ scene.add(cameraSplat);
1007
+
1008
+ updateStatus('Camera settings fixed. Press Space to record cameras.', cameraParams.length);
1009
+ },
1010
+ Resolution: `${supportedResolutions[0].frame}x${supportedResolutions[0].height}x${supportedResolutions[0].width}`,
1011
+ VisualizeCameraSplats: true,
1012
+ VisualizeInterpolatedCameras: true,
1013
+ inputImagePrompt: () => {
1014
+ const fileInput = document.querySelector("#file-input");
1015
+ if (fileInput) {
1016
+ // 仅触发选择,由全局处理程序完成裁剪与预览更新
1017
+ fileInput.click();
1018
+ }
1019
+ },
1020
+ imageIndex: 0,
1021
+ inputTextPrompt: "",
1022
+ // Step 6: All-in-one JSON IO
1023
+ LoadAllFromJson: () => {
1024
+ // Full load (image/text/index/resolution/cameras)
1025
+ window.loadTrajectoryOnly = false;
1026
+ const jsonInput = document.querySelector("#json-input");
1027
+ if (jsonInput) jsonInput.click();
1028
+ },
1029
+ SaveAllToJson: () => {
1030
+ // Build JSON payload matching transmission format
1031
+ const [nStr, hStr, wStr] = guiOptions.Resolution.split('x');
1032
+ const n = parseInt(nStr), h = parseInt(hStr), w = parseInt(wStr);
1033
+ const fovDeg = guiOptions.FOV;
1034
+ const fy = 0.5 / Math.tan(0.5 * fovDeg * Math.PI / 180) * h;
1035
+ const fx = fy; // keep fx consistent with fy-derived FOV
1036
+ const cx = 0.5 * w;
1037
+ const cy = 0.5 * h;
1038
+
1039
+ const payload = {
1040
+ image_prompt: inputImageBase64 ? inputImageBase64 : null,
1041
+ text_prompt: guiOptions.inputTextPrompt || "",
1042
+ image_index: guiOptions.imageIndex || 0,
1043
+ resolution: [n, h, w],
1044
+ cameras: cameraParams.map(cam => ({
1045
+ position: [cam.position.x, cam.position.y, cam.position.z],
1046
+ quaternion: [cam.quaternion.w, cam.quaternion.x, cam.quaternion.y, cam.quaternion.z],
1047
+ fx, fy, cx, cy
1048
+ }))
1049
+ };
1050
+
1051
+ const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' });
1052
+ const url = URL.createObjectURL(blob);
1053
+ const a = document.createElement('a');
1054
+ a.href = url;
1055
+ a.download = `scene_all_${Date.now()}.json`;
1056
+ document.body.appendChild(a);
1057
+ a.click();
1058
+ document.body.removeChild(a);
1059
+ URL.revokeObjectURL(url);
1060
+ updateStatus('All data saved to JSON.', cameraParams.length);
1061
+ },
1062
+
1063
+ // Camera trajectory templates
1064
+ trajectoryMode: "Manual",
1065
+ templateType: "Move Forward",
1066
+ cameraTrajectory: "Manual",
1067
+ trajectorySettings: {
1068
+ angle: 180, // 角度 (180, 360)
1069
+ tilt: 15 // 倾斜角 (15, 30, 45)
1070
+ },
1071
+ generateTrajectory: () => {
1072
+ generateCameraTrajectory(guiOptions.templateType);
1073
+ },
1074
+ saveTrajectoryToJson: () => {
1075
+
1076
+ // Build JSON payload compatible with loader
1077
+ const [nStr, hStr, wStr] = guiOptions.Resolution.split('x');
1078
+ const n = parseInt(nStr), h = parseInt(hStr), w = parseInt(wStr);
1079
+ const payload = {
1080
+ // image_prompt: null,
1081
+ // text_prompt: guiOptions.inputTextPrompt || "",
1082
+ // image_index: guiOptions.imageIndex || 0,
1083
+ // resolution: [n, h, w],
1084
+ cameras: cameraParams.map(cam => ({
1085
+ position: [cam.position.x, cam.position.y, cam.position.z],
1086
+ quaternion: [cam.quaternion.w, cam.quaternion.x, cam.quaternion.y, cam.quaternion.z]
1087
+ }))
1088
+ };
1089
+
1090
+ const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' });
1091
+ const url = URL.createObjectURL(blob);
1092
+ const a = document.createElement('a');
1093
+ a.href = url;
1094
+ a.download = `trajectory_${Date.now()}.json`;
1095
+ document.body.appendChild(a);
1096
+ a.click();
1097
+ document.body.removeChild(a);
1098
+ URL.revokeObjectURL(url);
1099
+ updateStatus('Trajectory saved to JSON.', cameraParams.length);
1100
+ },
1101
+ clearAllCameras: () => {
1102
+ if (cameraParams.length <= 1) {
1103
+ updateStatus('No cameras to clear (first camera is always preserved)', cameraParams.length);
1104
+ return;
1105
+ }
1106
+
1107
+ // Keep the first camera, remove all others
1108
+ const firstCamera = cameraParams[0];
1109
+ const firstSplat = cameraSplats[0];
1110
+
1111
+ // Remove all camera splats except the first one
1112
+ for (let i = cameraSplats.length - 1; i >= 1; i--) {
1113
+ scene.remove(cameraSplats[i]);
1114
+ }
1115
+
1116
+ // Keep only the first camera in arrays
1117
+ cameraSplats.length = 1;
1118
+ cameraParams.length = 1;
1119
+
1120
+ // Clear all interpolated camera splats from scene
1121
+ interpolatedCamerasSplats.forEach(splat => scene.remove(splat));
1122
+ interpolatedCamerasSplats.length = 0;
1123
+
1124
+ updateStatus('Cameras cleared (first camera preserved). Ready to add more cameras.', 1);
1125
+ console.log('Cameras cleared, first camera preserved');
1126
+ },
1127
+ // Playback scrub value (0..1)
1128
+ playbackT: 0,
1129
+
1130
+ generate: () => {
1131
+ // 检查是否有足够的相机
1132
+ if (cameraParams.length < 2) {
1133
+ console.error('Need at least 2 cameras to generate. Please press Space to record more cameras.');
1134
+ updateStatus('Error: Need at least 2 cameras', cameraParams.length);
1135
+ return;
1136
+ }
1137
+
1138
+ updateStatus('Preparing generation...', cameraParams.length);
1139
+
1140
+ // 删除之前生成的场景
1141
+ if (currentGeneratedSplat) {
1142
+ scene.remove(currentGeneratedSplat);
1143
+ currentGeneratedSplat = null;
1144
+ console.log('Previous generated scene removed');
1145
+ }
1146
+
1147
+ // 初始化进度条信息
1148
+ const generationTimeElement = document.getElementById('generation-time');
1149
+ const fileSizeElement = document.getElementById('file-size');
1150
+ const progressBar = document.getElementById('progress-bar');
1151
+ const progressText = document.getElementById('progress-text');
1152
+
1153
+ if (generationTimeElement) generationTimeElement.textContent = '-';
1154
+ if (fileSizeElement) fileSizeElement.textContent = '-';
1155
+ if (progressBar) progressBar.style.width = '0%';
1156
+ if (progressText) progressText.textContent = '0%';
1157
+
1158
+ // 隐藏生成信息和下载进度
1159
+ const generationInfo = document.getElementById('generation-info');
1160
+ const downloadProgress = document.getElementById('download-progress');
1161
+ if (generationInfo) generationInfo.style.display = 'none';
1162
+ if (downloadProgress) downloadProgress.style.display = 'none';
1163
+
1164
+ showLoading(true);
1165
+
1166
+ // 生成插值相机并可视化
1167
+ const interpolatedCameras = interpolateCameras(cameraParams, parseInt(guiOptions.Resolution.split('x')[0]));
1168
+ interpolatedCameras.forEach(cam => {
1169
+ const interpolatedCameraSplat = createCameraSplat(cam, [0.5, 0.5, 0.5]);
1170
+ interpolatedCamerasSplats.push(interpolatedCameraSplat);
1171
+ scene.add(interpolatedCameraSplat);
1172
+ });
1173
+
1174
+ console.log('Sending request to backend...');
1175
+ console.log('Interpolated cameras:', interpolatedCameras.length);
1176
+ updateStatus('Sending request to backend...', cameraParams.length);
1177
+
1178
+ // 调用 Gradio 后端:POST 到 /gradio_api/call/gradio_generate,然后通过 SSE 获取结果
1179
+ const requestUrl = guiOptions.BackendAddress + '/gradio_api/call/gradio_generate';
1180
+ const requestData = {
1181
+ image_prompt: inputImageBase64 ? inputImageBase64 : "",
1182
+ text_prompt: guiOptions.inputTextPrompt,
1183
+ image_index: 0,
1184
+ resolution: [
1185
+ parseInt(guiOptions.Resolution.split('x')[0]),
1186
+ parseInt(guiOptions.Resolution.split('x')[1]),
1187
+ parseInt(guiOptions.Resolution.split('x')[2])
1188
+ ],
1189
+ cameras: interpolatedCameras.map(cam => ({
1190
+ position: [cam.position.x, cam.position.y, cam.position.z],
1191
+ quaternion: [cam.quaternion.w, cam.quaternion.x, cam.quaternion.y, cam.quaternion.z],
1192
+ fx: 0.5 / Math.tan(0.5 * cam.fov * Math.PI / 180) * parseInt(guiOptions.Resolution.split('x')[1]),
1193
+ fy: 0.5 / Math.tan(0.5 * cam.fov * Math.PI / 180) * parseInt(guiOptions.Resolution.split('x')[1]),
1194
+ cx: inputImageBase64 && inputImageResolution
1195
+ ? 0.5 * inputImageResolution.width
1196
+ : 0.5 * parseInt(guiOptions.Resolution.split('x')[2]),
1197
+ cy: inputImageBase64 && inputImageResolution
1198
+ ? 0.5 * inputImageResolution.height
1199
+ : 0.5 * parseInt(guiOptions.Resolution.split('x')[1]),
1200
+ }))
1201
+ };
1202
+
1203
+ fetchWithAuth(requestUrl, {
1204
+ method: 'POST',
1205
+ headers: { 'Content-Type': 'application/json' },
1206
+ mode: 'cors',
1207
+ body: JSON.stringify({ data: [JSON.stringify(requestData)] })
1208
+ })
1209
+ .then(response => response.json())
1210
+ .then(data => {
1211
+ // Gradio 总是返回 event_id,需要使用 SSE 获取生成结果
1212
+ if (!data || !data.event_id) {
1213
+ throw new Error('Invalid Gradio response format - no event_id');
1214
+ }
1215
+ return fetchWithAuth(guiOptions.BackendAddress + `/gradio_api/call/gradio_generate/${data.event_id}`)
1216
+ .then(resp => {
1217
+ if (!resp.ok) throw new Error(`HTTP error! status: ${resp.status}`);
1218
+ return resp.text();
1219
+ })
1220
+ .then(sseText => {
1221
+ const lines = sseText.split('\n');
1222
+ let eventType = null;
1223
+ let dataContent = null;
1224
+ for (const line of lines) {
1225
+ if (line.startsWith('event: ')) eventType = line.substring(7);
1226
+ else if (line.startsWith('data: ')) dataContent = line.substring(6);
1227
+ }
1228
+ if (eventType !== 'complete' || !dataContent) {
1229
+ throw new Error('Gradio SSE response not complete or missing data');
1230
+ }
1231
+ const resultData = JSON.parse(dataContent);
1232
+ if (!resultData || resultData.length === 0) {
1233
+ throw new Error('Invalid Gradio generation result format');
1234
+ }
1235
+ const responseData = JSON.parse(resultData[0]);
1236
+ if (!responseData.success) {
1237
+ throw new Error('Gradio generation failed: ' + (responseData.error || 'Unknown error'));
1238
+ }
1239
+
1240
+ // 显示生成信息
1241
+ showGenerationInfo(responseData.generation_time, responseData.file_size);
1242
+ showDownloadProgress();
1243
+ updateStatus('Downloading generated scene...', cameraParams.length);
1244
+
1245
+ // 下载文件:调用 download_file 获取下载 event_id,然后通过 SSE 拿到 URL,再实际下载
1246
+ return fetchWithAuth(guiOptions.BackendAddress + '/gradio_api/call/download_file', {
1247
+ method: 'POST',
1248
+ headers: { 'Content-Type': 'application/json' },
1249
+ body: JSON.stringify({ data: [responseData.file_id] })
1250
+ })
1251
+ .then(r => r.json())
1252
+ .then(downloadEvent => {
1253
+ return fetchWithAuth(guiOptions.BackendAddress + `/gradio_api/call/download_file/${downloadEvent.event_id}`)
1254
+ .then(r => {
1255
+ if (!r.ok) throw new Error(`HTTP error! status: ${r.status}`);
1256
+ return r.text();
1257
+ })
1258
+ .then(downloadSseText => {
1259
+ const lines = downloadSseText.split('\n');
1260
+ let eventType = null;
1261
+ let dataContent = null;
1262
+ for (const line of lines) {
1263
+ if (line.startsWith('event: ')) eventType = line.substring(7);
1264
+ else if (line.startsWith('data: ')) dataContent = line.substring(6);
1265
+ }
1266
+ if (eventType !== 'complete' || !dataContent) {
1267
+ throw new Error('Gradio download SSE response not complete or missing data');
1268
+ }
1269
+ const fileData = JSON.parse(dataContent);
1270
+ if (!fileData || fileData.length === 0 || !fileData[0].url) {
1271
+ throw new Error('Invalid file data format from Gradio');
1272
+ }
1273
+ return fileData[0].url;
1274
+ });
1275
+ });
1276
+ })
1277
+ .then(fileUrl => {
1278
+ return fetchWithAuth(fileUrl).then(response => {
1279
+ if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
1280
+ const contentLength = response.headers.get('content-length');
1281
+ const total = parseInt(contentLength || '0', 10);
1282
+ let loaded = 0;
1283
+ const reader = response.body.getReader();
1284
+ const chunks = [];
1285
+ function pump() {
1286
+ return reader.read().then(({ done, value }) => {
1287
+ if (done) return new Blob(chunks);
1288
+ chunks.push(value);
1289
+ loaded += value.length;
1290
+ if (total) updateProgressBar((loaded / total) * 100);
1291
+ return pump();
1292
+ });
1293
+ }
1294
+ return pump().then(blob => {
1295
+ const url = URL.createObjectURL(blob);
1296
+ return { url, __deleteAfterDownloadFileId: (typeof responseData !== 'undefined' ? responseData.file_id : null) };
1297
+ });
1298
+ });
1299
+ });
1300
+ })
1301
+ .then(data => {
1302
+ if (data && data.url) {
1303
+ updateStatus('Loading 3D scene...', cameraParams.length);
1304
+ if (instructionSplat) {
1305
+ scene.remove(instructionSplat);
1306
+ console.log('Instruction splat removed');
1307
+ }
1308
+ const GeneratedSplat = new SplatMesh({ url: data.url });
1309
+ scene.add(GeneratedSplat);
1310
+ currentGeneratedSplat = GeneratedSplat;
1311
+ console.log('3D scene loaded successfully!');
1312
+ updateStatus('Scene generated successfully!', cameraParams.length);
1313
+ hideDownloadProgress();
1314
+ showLoading(false);
1315
+
1316
+ // 通知后端删除文件(如果有 file_id)
1317
+ if (data.__deleteAfterDownloadFileId) {
1318
+ fetchWithAuth(guiOptions.BackendAddress + '/delete/' + data.__deleteAfterDownloadFileId, { method: 'POST' })
1319
+ .then(() => console.log('Delete notify sent'))
1320
+ .catch(err => console.warn('Delete notify failed', err));
1321
+ }
1322
+ }
1323
+ })
1324
+ .catch(error => {
1325
+ console.error('Error:', error);
1326
+ updateStatus('Generation failed: ' + error.message, cameraParams.length);
1327
+ hideDownloadProgress();
1328
+ showLoading(false);
1329
+ });
1330
+ }
1331
+ };
1332
+
1333
+ // =========================
1334
+ // Examples & JSON load utils
1335
+ // =========================
1336
+ const EXAMPLE_FILES = Array.from({ length: 8 }, (_, i) => `examples/${i + 1}.json`);
1337
+
1338
+ function processJsonLoad(jsonData, loadTrajectoryOnlyProvided) {
1339
+ // Determine mode and reset flag if coming from global
1340
+ const loadTrajectoryOnly = !!loadTrajectoryOnlyProvided;
1341
+
1342
+ // Clear existing cameras and interpolated splats
1343
+ cameraSplats.forEach(splat => scene.remove(splat));
1344
+ cameraSplats.length = 0;
1345
+ cameraParams.length = 0;
1346
+ interpolatedCamerasSplats.forEach(splat => scene.remove(splat));
1347
+ interpolatedCamerasSplats.length = 0;
1348
+
1349
+ try {
1350
+ const imagePrompt = jsonData.image_prompt || jsonData.imagePrompt || null;
1351
+ const textPrompt = jsonData.text_prompt || jsonData.textPrompt || "";
1352
+ const cameras = jsonData.cameras || [];
1353
+ const resolution = jsonData.resolution || [16, 480, 640];
1354
+ const imageIndex = jsonData.image_index || jsonData.imageIndex || 0;
1355
+
1356
+ // Prompt/image only for full load
1357
+ if (!loadTrajectoryOnly && imagePrompt) {
1358
+ inputImageBase64 = imagePrompt;
1359
+ const previewArea = document.getElementById('image-preview-area');
1360
+ const previewImg = document.getElementById('preview-img');
1361
+ if (previewImg && previewArea) {
1362
+ previewImg.src = inputImageBase64;
1363
+ previewArea.style.display = 'block';
1364
+ }
1365
+ }
1366
+
1367
+ if (!loadTrajectoryOnly) {
1368
+ guiOptions.inputTextPrompt = textPrompt;
1369
+ guiOptions.imageIndex = imageIndex;
1370
+ syncGuiPromptControls();
1371
+ }
1372
+
1373
+ // Infer FOV from first camera fy for full load
1374
+ if (!loadTrajectoryOnly && Array.isArray(resolution) && resolution.length === 3 && cameras && cameras.length > 0) {
1375
+ const H = resolution[1];
1376
+ const firstCam = cameras[0];
1377
+ if (firstCam && typeof firstCam.fy === 'number' && isFinite(firstCam.fy) && firstCam.fy > 0) {
1378
+ const inferredFov = 2 * Math.atan(0.5 * H / firstCam.fy) * 180 / Math.PI;
1379
+ guiOptions.FOV = inferredFov;
1380
+ }
1381
+ }
1382
+
1383
+ if (cameras && cameras.length > 0) {
1384
+ let jsonFirstPosition = null;
1385
+ let jsonFirstQuaternion = null;
1386
+ const firstCameraData = cameras[0];
1387
+ if (Array.isArray(firstCameraData?.position) && firstCameraData.position.length === 3) {
1388
+ jsonFirstPosition = new THREE.Vector3(
1389
+ firstCameraData.position[0],
1390
+ firstCameraData.position[1],
1391
+ firstCameraData.position[2]
1392
+ );
1393
+ }
1394
+ if (Array.isArray(firstCameraData?.quaternion) && firstCameraData.quaternion.length === 4) {
1395
+ jsonFirstQuaternion = new THREE.Quaternion(
1396
+ firstCameraData.quaternion[1],
1397
+ firstCameraData.quaternion[2],
1398
+ firstCameraData.quaternion[3],
1399
+ firstCameraData.quaternion[0]
1400
+ );
1401
+ }
1402
+
1403
+ cameras.forEach((cameraData) => {
1404
+ let aspect = 1.0;
1405
+ if (Array.isArray(resolution) && resolution.length === 3) {
1406
+ aspect = resolution[2] / resolution[1];
1407
+ } else {
1408
+ aspect = guiOptions.Resolution.split('x')[2] / guiOptions.Resolution.split('x')[1];
1409
+ }
1410
+
1411
+ let fov = 60;
1412
+ if (loadTrajectoryOnly) {
1413
+ fov = guiOptions.FOV;
1414
+ } else {
1415
+ if (Array.isArray(resolution) && resolution.length === 3 && typeof cameraData.fy === 'number' && cameraData.fy > 0) {
1416
+ const H = resolution[1];
1417
+ fov = 2 * Math.atan(0.5 * H / cameraData.fy) * 180 / Math.PI;
1418
+ guiOptions.FOV = fov;
1419
+ } else {
1420
+ fov = guiOptions.FOV;
1421
+ }
1422
+ }
1423
+
1424
+ const cam = new THREE.PerspectiveCamera(fov, aspect);
1425
+ if (Array.isArray(cameraData.position) && cameraData.position.length === 3) {
1426
+ cam.position.set(cameraData.position[0], cameraData.position[1], cameraData.position[2]);
1427
+ }
1428
+ if (Array.isArray(cameraData.quaternion) && cameraData.quaternion.length === 4) {
1429
+ cam.quaternion.set(
1430
+ cameraData.quaternion[1],
1431
+ cameraData.quaternion[2],
1432
+ cameraData.quaternion[3],
1433
+ cameraData.quaternion[0]
1434
+ );
1435
+ }
1436
+
1437
+ if (jsonFirstPosition && jsonFirstQuaternion) {
1438
+ const jsonFirstC2W = new THREE.Matrix4();
1439
+ jsonFirstC2W.compose(jsonFirstPosition, jsonFirstQuaternion, new THREE.Vector3(1, 1, 1));
1440
+ const currentC2W = new THREE.Matrix4();
1441
+ currentC2W.compose(cam.position, cam.quaternion, new THREE.Vector3(1, 1, 1));
1442
+ const refW2C = jsonFirstC2W.clone().invert();
1443
+ const relativeTransform = refW2C.clone().multiply(currentC2W);
1444
+ const fixedC2W = new THREE.Matrix4();
1445
+ fixedC2W.compose(new THREE.Vector3(0, 0, 0), new THREE.Quaternion(0, 0, 0, 1), new THREE.Vector3(1, 1, 1));
1446
+ const newTransform = fixedC2W.clone().multiply(relativeTransform);
1447
+ const newPosition = new THREE.Vector3();
1448
+ const newQuaternion = new THREE.Quaternion();
1449
+ const newScale = new THREE.Vector3();
1450
+ newTransform.decompose(newPosition, newQuaternion, newScale);
1451
+ cam.position.copy(newPosition);
1452
+ cam.quaternion.copy(newQuaternion);
1453
+ }
1454
+
1455
+ cam.fov = fov;
1456
+ cam.aspect = aspect;
1457
+ cam.updateProjectionMatrix();
1458
+
1459
+ const cameraSplat = createCameraSplat(cam);
1460
+ cameraSplats.push(cameraSplat);
1461
+ cameraParams.push({
1462
+ position: cam.position.clone(),
1463
+ quaternion: cam.quaternion.clone(),
1464
+ fov: cam.fov,
1465
+ aspect: cam.aspect,
1466
+ });
1467
+ scene.add(cameraSplat);
1468
+ });
1469
+ }
1470
+
1471
+ if (!loadTrajectoryOnly && Array.isArray(resolution) && resolution.length === 3) {
1472
+ guiOptions.Resolution = `${resolution[0]}x${resolution[1]}x${resolution[2]}`;
1473
+ }
1474
+
1475
+ if (loadTrajectoryOnly) {
1476
+ updateStatus(`Trajectory loaded: ${jsonData.cameras ? jsonData.cameras.length : 0} cameras`, cameraParams.length);
1477
+ } else {
1478
+ updateStatus(`JSON loaded: ${jsonData.cameras ? jsonData.cameras.length : 0} cameras`, cameraParams.length);
1479
+ }
1480
+ } catch (error) {
1481
+ console.error("JSON data processing error:", error);
1482
+ }
1483
+ }
1484
+
1485
+ async function renderExamples() {
1486
+ const grid = document.getElementById('examples-grid');
1487
+ if (!grid) return;
1488
+ grid.innerHTML = '';
1489
+ const items = [];
1490
+ for (const path of EXAMPLE_FILES) {
1491
+ try {
1492
+ const resp = await fetch(path);
1493
+ if (!resp.ok) continue;
1494
+ const data = await resp.json();
1495
+ const thumb = data.image_prompt || data.imagePrompt || null;
1496
+ items.push({ path, data, thumb });
1497
+ } catch (_) { /* ignore */ }
1498
+ }
1499
+ items.slice(0, 10).forEach((item, idx) => {
1500
+ const div = document.createElement('div');
1501
+ div.className = 'example-item';
1502
+ div.title = item.path;
1503
+ if (item.thumb) {
1504
+ const img = document.createElement('img');
1505
+ img.src = item.thumb;
1506
+ div.appendChild(img);
1507
+ } else {
1508
+ const label = document.createElement('div');
1509
+ label.className = 'label';
1510
+ label.textContent = `Example ${idx + 1}`;
1511
+ div.appendChild(label);
1512
+ }
1513
+ div.addEventListener('click', () => {
1514
+ // Full JSON load behavior
1515
+ processJsonLoad(item.data, false);
1516
+ });
1517
+ grid.appendChild(div);
1518
+ });
1519
+ }
1520
+
1521
+ // Initialize renderer and GUI when DOM is ready
1522
+ function initializeApp() {
1523
+ try {
1524
+ // Debug layout
1525
+ console.log('Initializing app...');
1526
+ console.log('Center panel:', document.querySelector('.center-panel'));
1527
+ console.log('GUI container:', document.getElementById('gui-container'));
1528
+ console.log('Right panel:', document.querySelector('.right-panel'));
1529
+
1530
+ initializeRenderer();
1531
+ initializeGUI();
1532
+ console.log('App initialization complete');
1533
+ } catch (error) {
1534
+ console.error('App initialization failed:', error);
1535
+ }
1536
+ }
1537
+
1538
+ if (document.readyState === 'loading') {
1539
+ document.addEventListener('DOMContentLoaded', () => { initializeApp(); renderExamples(); });
1540
+ } else {
1541
+ initializeApp();
1542
+ renderExamples();
1543
+ }
1544
+
1545
+ // =========================
1546
+ // Utility & Core Functions
1547
+ // =========================
1548
+
1549
+ // 计算插值相机
1550
+ function interpolateTwoCameras(startCamera, endCamera, _t) {
1551
+ const interpolatedCamera = new THREE.PerspectiveCamera(startCamera.fov, startCamera.aspect);
1552
+
1553
+ // 如果_t接近0,直接使用startCamera
1554
+ if (_t < 1e-6) {
1555
+ interpolatedCamera.position.copy(startCamera.position);
1556
+ interpolatedCamera.quaternion.copy(startCamera.quaternion);
1557
+ }
1558
+ // 如果_t接近1,直接使用endCamera
1559
+ else if (_t > 1 - 1e-6) {
1560
+ interpolatedCamera.position.copy(endCamera.position);
1561
+ interpolatedCamera.quaternion.copy(endCamera.quaternion);
1562
+ }
1563
+ // 否则进行插值
1564
+ else {
1565
+ interpolatedCamera.position.copy(startCamera.position).lerp(endCamera.position, _t);
1566
+ interpolatedCamera.quaternion.copy(startCamera.quaternion).slerp(endCamera.quaternion, _t);
1567
+ }
1568
+
1569
+ return interpolatedCamera;
1570
+ }
1571
+
1572
+ function interpolateCameras(cameras, M) {
1573
+ const interpolatedCameras = [];
1574
+
1575
+ if (cameras.length === 0) {
1576
+ return interpolatedCameras;
1577
+ }
1578
+
1579
+ if (cameras.length === 1) {
1580
+ // 如果只有一个相机,重复使用它
1581
+ for (let i = 0; i < M; i++) {
1582
+ interpolatedCameras.push(cameras[0]);
1583
+ }
1584
+ return interpolatedCameras;
1585
+ }
1586
+
1587
+ for (let i = 0; i < M; i++) {
1588
+ const t = i / (M - 1);
1589
+ const startIndex = Math.min(Math.floor(t * (cameras.length - 1)), cameras.length - 2);
1590
+ const endIndex = startIndex + 1;
1591
+ const startCamera = cameras[startIndex];
1592
+ const endCamera = cameras[endIndex];
1593
+ const _t = t * (cameras.length - 1) - startIndex;
1594
+ const interpolatedCamera = interpolateTwoCameras(startCamera, endCamera, _t);
1595
+ interpolatedCameras.push(interpolatedCamera);
1596
+ }
1597
+ return interpolatedCameras;
1598
+ }
1599
+
1600
+ // 强制同步文本与索引控件显示
1601
+ function syncGuiPromptControls() {
1602
+ try {
1603
+ if (window.inputTextPromptController) {
1604
+ window.inputTextPromptController.setValue(guiOptions.inputTextPrompt);
1605
+ if (typeof window.inputTextPromptController.updateDisplay === 'function') {
1606
+ window.inputTextPromptController.updateDisplay();
1607
+ }
1608
+ }
1609
+ if (window.imageIndexController) {
1610
+ window.imageIndexController.setValue(guiOptions.imageIndex);
1611
+ if (typeof window.imageIndexController.updateDisplay === 'function') {
1612
+ window.imageIndexController.updateDisplay();
1613
+ }
1614
+ }
1615
+ } catch (e) {
1616
+ console.debug('syncGuiPromptControls error:', e);
1617
+ }
1618
+ // 再尝试一次,防止控件尚未就绪
1619
+ requestAnimationFrame(() => {
1620
+ try {
1621
+ if (window.inputTextPromptController && typeof window.inputTextPromptController.updateDisplay === 'function') {
1622
+ window.inputTextPromptController.updateDisplay();
1623
+ }
1624
+ if (window.imageIndexController && typeof window.imageIndexController.updateDisplay === 'function') {
1625
+ window.imageIndexController.updateDisplay();
1626
+ }
1627
+ } catch (_) {}
1628
+ });
1629
+ }
1630
+
1631
+ // 创建立方体的splat可视化
1632
+ function createCubeSplat(size = 0.1, pointColor = [1, 1, 1]) {
1633
+ const cubeSplat = new SplatMesh({
1634
+ constructSplats: (splats) => {
1635
+ const NUM_SPLATS_PER_EDGE = 1000;
1636
+ const scales = new THREE.Vector3().setScalar(0.002);
1637
+ const quaternion = new THREE.Quaternion();
1638
+ const opacity = 1;
1639
+ const color = new THREE.Color(...pointColor);
1640
+
1641
+ // 立方体的8个顶点
1642
+ const halfSize = size / 2;
1643
+ const vertices = [
1644
+ new THREE.Vector3(-halfSize, -halfSize, -halfSize), // 0: 左下后
1645
+ new THREE.Vector3(halfSize, -halfSize, -halfSize), // 1: 右下后
1646
+ new THREE.Vector3(halfSize, halfSize, -halfSize), // 2: 右上后
1647
+ new THREE.Vector3(-halfSize, halfSize, -halfSize), // 3: 左上后
1648
+ new THREE.Vector3(-halfSize, -halfSize, halfSize), // 4: 左下前
1649
+ new THREE.Vector3(halfSize, -halfSize, halfSize), // 5: 右下前
1650
+ new THREE.Vector3(halfSize, halfSize, halfSize), // 6: 右上前
1651
+ new THREE.Vector3(-halfSize, halfSize, halfSize), // 7: 左上前
1652
+ ];
1653
+
1654
+ // 立方体的12条边
1655
+ const edges = [
1656
+ [0, 1], [1, 2], [2, 3], [3, 0], // 后面4条边
1657
+ [4, 5], [5, 6], [6, 7], [7, 4], // 前面4条边
1658
+ [0, 4], [1, 5], [2, 6], [3, 7], // 连接前后4条边
1659
+ ];
1660
+
1661
+ // 为每条边生成splat点
1662
+ for (let i = 0; i < edges.length; i++) {
1663
+ const start = vertices[edges[i][0]];
1664
+ const end = vertices[edges[i][1]];
1665
+ for (let j = 0; j < NUM_SPLATS_PER_EDGE; j++) {
1666
+ const point = new THREE.Vector3().lerpVectors(start, end, j / NUM_SPLATS_PER_EDGE);
1667
+ splats.pushSplat(point, scales, quaternion, opacity, color);
1668
+ }
1669
+ }
1670
+ },
1671
+ });
1672
+ return cubeSplat;
1673
+ }
1674
+
1675
+ // 创建相机锥体的splat可视化
1676
+ function createCameraSplat(camera, pointColor = [1, 1, 1]) {
1677
+ const cameraSplat = new SplatMesh({
1678
+ constructSplats: (splats) => {
1679
+ const NUM_SPLATS_PER_EDGE = 1000;
1680
+ const LENGTH_PER_EDGE = 0.1;
1681
+ const center = new THREE.Vector3();
1682
+ const scales = new THREE.Vector3().setScalar(0.001);
1683
+ const quaternion = new THREE.Quaternion();
1684
+ const opacity = 1;
1685
+ const color = new THREE.Color(...pointColor);
1686
+
1687
+ const H = 1000;
1688
+ const W = 1000 * camera.aspect;
1689
+ const fx = 0.5 * H / Math.tan(0.5 * camera.fov * Math.PI / 180);
1690
+ const fy = 0.5 * H / Math.tan(0.5 * camera.fov * Math.PI / 180);
1691
+
1692
+ const xt = (0 - W / 2 + 0.5) / fy;
1693
+ const xb = (W - W / 2 + 0.5) / fy;
1694
+ const yl = - (0 - H / 2 + 0.5) / fx;
1695
+ const yr = - (H - H / 2 + 0.5) / fx;
1696
+
1697
+ const lt = new THREE.Vector3(xt * LENGTH_PER_EDGE, yl * LENGTH_PER_EDGE, -1 * LENGTH_PER_EDGE);
1698
+ const rt = new THREE.Vector3(xt * LENGTH_PER_EDGE, yr * LENGTH_PER_EDGE, -1 * LENGTH_PER_EDGE);
1699
+ const lb = new THREE.Vector3(xb * LENGTH_PER_EDGE, yl * LENGTH_PER_EDGE, -1 * LENGTH_PER_EDGE);
1700
+ const rb = new THREE.Vector3(xb * LENGTH_PER_EDGE, yr * LENGTH_PER_EDGE, -1 * LENGTH_PER_EDGE);
1701
+
1702
+ const lines = [
1703
+ [center, lt], [center, rt], [center, lb], [center, rb],
1704
+ [lt, rt], [lt, lb], [rt, rb], [lb, rb],
1705
+ ];
1706
+
1707
+ for (let i = 0; i < lines.length; i++) {
1708
+ for (let j = 0; j < NUM_SPLATS_PER_EDGE; j++) {
1709
+ const point = new THREE.Vector3().lerpVectors(lines[i][0], lines[i][1], j / NUM_SPLATS_PER_EDGE);
1710
+ splats.pushSplat(point, scales, quaternion, opacity, color);
1711
+ }
1712
+ }
1713
+ },
1714
+ });
1715
+ cameraSplat.quaternion.copy(camera.quaternion);
1716
+ cameraSplat.position.copy(camera.position);
1717
+ return cameraSplat;
1718
+ }
1719
+
1720
+ // 生成相机轨迹模板
1721
+ function generateCameraTrajectory(trajectoryType) {
1722
+ if (trajectoryType === "Manual") {
1723
+ updateStatus('Manual mode: Use Space to record cameras manually', cameraParams.length);
1724
+ return;
1725
+ }
1726
+
1727
+ // 检查FOV是否已固定
1728
+ if (!fixGenerationFOV) {
1729
+ updateStatus('Error: Please fix FOV first before generating trajectory', cameraParams.length);
1730
+ return;
1731
+ }
1732
+
1733
+ // 获取最后一个相机作为参考点
1734
+ let referenceCamera;
1735
+ if (cameraParams.length > 0) {
1736
+ // 使用最后一个已保存的相机作为参考
1737
+ const lastCamera = cameraParams[cameraParams.length - 1];
1738
+ referenceCamera = new THREE.PerspectiveCamera(guiOptions.FOV, camera.aspect);
1739
+ referenceCamera.position.copy(lastCamera.position);
1740
+ referenceCamera.quaternion.copy(lastCamera.quaternion);
1741
+ referenceCamera.updateProjectionMatrix();
1742
+ } else {
1743
+ // 如果没有已保存的相机,从原点开始
1744
+ referenceCamera = new THREE.PerspectiveCamera(guiOptions.FOV, camera.aspect);
1745
+ referenceCamera.position.set(0, 0, 0);
1746
+ referenceCamera.quaternion.set(0, 0, 0, 1);
1747
+ referenceCamera.updateProjectionMatrix();
1748
+ }
1749
+
1750
+ // 对于orbit,计算所有相机围绕的目标点
1751
+ // 始终使用当前参考相机(最后一个相机)来计算目标点
1752
+ let orbitTarget = null;
1753
+ let orbitStartCamera = null;
1754
+ if (trajectoryType.includes("Orbit") && cameraParams.length > 0) {
1755
+ // 使用最后一个相机作为参考,计算其前方1单位的目标点
1756
+ orbitStartCamera = cameraParams[cameraParams.length - 1];
1757
+ orbitTarget = orbitStartCamera.position.clone().add(
1758
+ new THREE.Vector3(0, 0, -1).applyQuaternion(orbitStartCamera.quaternion)
1759
+ );
1760
+ console.log("Orbit target calculated from last camera:", orbitStartCamera.position, "->", orbitTarget);
1761
+ } else if (trajectoryType.includes("Orbit")) {
1762
+ // 如果没有已记录的相机,使用当前相机作为参考
1763
+ orbitStartCamera = referenceCamera;
1764
+ orbitTarget = referenceCamera.position.clone().add(
1765
+ new THREE.Vector3(0, 0, -1).applyQuaternion(referenceCamera.quaternion)
1766
+ );
1767
+ console.log("Orbit target calculated from current camera:", referenceCamera.position, "->", orbitTarget);
1768
+ }
1769
+
1770
+ const cameras = [];
1771
+ const stepSize = 0.5; // 移动步长
1772
+ const totalOrbitAngle = 15 * Math.PI / 180; // 总共15度轨道
1773
+
1774
+ // 根据轨迹类型生成相机
1775
+ let numCameras = 1; // 默认生成1个相机
1776
+ if (trajectoryType.includes("Orbit")) {
1777
+ numCameras = 1; // 轨道运动生成1个相机
1778
+ console.log(`Generating ${numCameras} orbit camera with total angle ${totalOrbitAngle * 180 / Math.PI}°`);
1779
+ }
1780
+
1781
+ for (let i = 1; i <= numCameras; i++) {
1782
+ const newCamera = new THREE.PerspectiveCamera(guiOptions.FOV, camera.aspect);
1783
+ let position, quaternion;
1784
+
1785
+ switch (trajectoryType) {
1786
+ case "Move Forward":
1787
+ position = referenceCamera.position.clone();
1788
+ position.z -= stepSize;
1789
+ quaternion = referenceCamera.quaternion.clone();
1790
+ break;
1791
+
1792
+ case "Move Backward":
1793
+ position = referenceCamera.position.clone();
1794
+ position.z += stepSize;
1795
+ quaternion = referenceCamera.quaternion.clone();
1796
+ break;
1797
+
1798
+ case "Move Left":
1799
+ position = referenceCamera.position.clone();
1800
+ position.x -= stepSize;
1801
+ quaternion = referenceCamera.quaternion.clone();
1802
+ break;
1803
+
1804
+ case "Move Right":
1805
+ position = referenceCamera.position.clone();
1806
+ position.x += stepSize;
1807
+ quaternion = referenceCamera.quaternion.clone();
1808
+ break;
1809
+
1810
+ case "Orbit Left 15°":
1811
+ const radius = 1.0;
1812
+ // 左轨道:-15度
1813
+ const angle = -totalOrbitAngle;
1814
+
1815
+ console.log(`Camera ${i}: angle=${angle * 180 / Math.PI}° (Left)`);
1816
+
1817
+ // 计算轨道位置:在参考相机的局部坐标系中
1818
+ const localOrbitPos = new THREE.Vector3(
1819
+ Math.sin(angle) * radius,
1820
+ 0,
1821
+ Math.cos(angle) * radius
1822
+ );
1823
+
1824
+ // 转换到世界坐标系:旋转到参考相机的方向
1825
+ const worldOrbitPos = localOrbitPos.applyQuaternion(orbitStartCamera.quaternion);
1826
+
1827
+ // 最终位置:从目标点出发,加上世界坐标系中的偏移
1828
+ position = orbitTarget.clone().add(worldOrbitPos);
1829
+
1830
+ console.log(`Orbit Left camera ${i}: localPos=`, localOrbitPos, 'worldPos=', worldOrbitPos, 'finalPos=', position);
1831
+
1832
+ // 朝向:所有相机都朝向圆心(目标点)
1833
+ const lookDirection = orbitTarget.clone().sub(position).normalize();
1834
+ quaternion = new THREE.Quaternion().setFromUnitVectors(
1835
+ new THREE.Vector3(0, 0, -1),
1836
+ lookDirection
1837
+ );
1838
+
1839
+ console.log(`Orbit Left camera ${i}: quaternion=`, quaternion);
1840
+ break;
1841
+
1842
+ case "Orbit Right 15°":
1843
+ const radiusRight = 1.0;
1844
+ // 右轨道:+15度
1845
+ const angleRight = totalOrbitAngle;
1846
+
1847
+ console.log(`Camera ${i}: angle=${angleRight * 180 / Math.PI}° (Right)`);
1848
+
1849
+ // 计算轨道位置:在参考相机的局部坐标系中
1850
+ const localOrbitPosRight = new THREE.Vector3(
1851
+ Math.sin(angleRight) * radiusRight,
1852
+ 0,
1853
+ Math.cos(angleRight) * radiusRight
1854
+ );
1855
+
1856
+ // 转换到世界坐标系:旋转到参考相机的方向
1857
+ const worldOrbitPosRight = localOrbitPosRight.applyQuaternion(orbitStartCamera.quaternion);
1858
+
1859
+ // 最终位置:从目标点出发,加上世界坐标系中的偏移
1860
+ position = orbitTarget.clone().add(worldOrbitPosRight);
1861
+
1862
+ console.log(`Orbit Right camera ${i}: localPos=`, localOrbitPosRight, 'worldPos=', worldOrbitPosRight, 'finalPos=', position);
1863
+
1864
+ // 朝向:所有相机都朝向圆心(目标点)
1865
+ const lookDirectionRight = orbitTarget.clone().sub(position).normalize();
1866
+ quaternion = new THREE.Quaternion().setFromUnitVectors(
1867
+ new THREE.Vector3(0, 0, -1),
1868
+ lookDirectionRight
1869
+ );
1870
+
1871
+ console.log(`Orbit Right camera ${i}: quaternion=`, quaternion);
1872
+ break;
1873
+
1874
+
1875
+ default:
1876
+ position = referenceCamera.position.clone();
1877
+ quaternion = referenceCamera.quaternion.clone();
1878
+ }
1879
+
1880
+ newCamera.position.copy(position);
1881
+ newCamera.quaternion.copy(quaternion);
1882
+ newCamera.updateProjectionMatrix();
1883
+ cameras.push(newCamera);
1884
+ }
1885
+
1886
+ // 添加相机到场景
1887
+ cameras.forEach(cam => {
1888
+ const cameraSplat = createCameraSplat(cam);
1889
+ cameraSplats.push(cameraSplat);
1890
+ cameraParams.push({
1891
+ position: cam.position.clone(),
1892
+ quaternion: cam.quaternion.clone(),
1893
+ fov: cam.fov,
1894
+ aspect: cam.aspect,
1895
+ });
1896
+ scene.add(cameraSplat);
1897
+ });
1898
+
1899
+ updateStatus(`Added ${cameras.length} cameras using ${trajectoryType} trajectory`, cameraParams.length);
1900
+ console.log(`Added ${cameras.length} cameras using ${trajectoryType} trajectory`);
1901
+ }
1902
+
1903
+ // =========================
1904
+ // GUI & User Interaction
1905
+ // =========================
1906
+
1907
+ // GUI 控件 - 延迟初始化
1908
+ function initializeGUI() {
1909
+ const guiContainer = document.getElementById('gui-container');
1910
+ if (guiContainer && !gui) {
1911
+ // Clear any existing content
1912
+ guiContainer.innerHTML = '';
1913
+
1914
+ gui = new GUI({ title: "FlashWorld Controls", container: guiContainer });
1915
+ console.log('GUI initialized in container:', guiContainer);
1916
+
1917
+ // Step 1: Configure Generation Settings
1918
+ const step1Folder = gui.addFolder('1. Configure Settings');
1919
+ step1Folder.add(guiOptions, "BackendAddress").name("Backend Address");
1920
+ step1Folder.add(guiOptions, "HF_TOKEN").name("HF Token");
1921
+
1922
+ // FOV和Resolution控制器,初始时启用
1923
+ const fovController = step1Folder.add(guiOptions, "FOV", 0, 120, 1).name("FOV").onChange((value) => {
1924
+ camera.fov = value;
1925
+ camera.updateProjectionMatrix();
1926
+ });
1927
+ const resolutionController = step1Folder.add(guiOptions, "Resolution", supportedResolutions.map(
1928
+ r => `${r.frame}x${r.height}x${r.width}`
1929
+ )).name("Resolution (NxHxW)").onChange((value) => {
1930
+ updateCanvasSize();
1931
+ });
1932
+ // Expose for programmatic updates after JSON load
1933
+ window.fovController = fovController;
1934
+ window.resolutionController = resolutionController;
1935
+
1936
+ // Fix Configuration按钮放在最下面
1937
+ const fixGenerationFOVController = step1Folder.add(guiOptions, "fixGenerationFOV").name("Fix Configuration");
1938
+ step1Folder.open();
1939
+
1940
+ // Step 2: Set Up Camera Path
1941
+ const step2Folder = gui.addFolder('2. Set Up Camera Path');
1942
+
1943
+ // Camera trajectory templates
1944
+ const trajectoryFolder = step2Folder.addFolder('Camera Trajectory');
1945
+
1946
+ // 轨迹模式选择
1947
+ const trajectoryModeController = trajectoryFolder.add(guiOptions, "trajectoryMode", [
1948
+ "Manual",
1949
+ "Template",
1950
+ "JSON"
1951
+ ]).name("Trajectory Mode");
1952
+
1953
+ // 模板类型选择(仅在Template模式下可用)
1954
+ const templateTypeController = trajectoryFolder.add(guiOptions, "templateType", [
1955
+ "Move Forward",
1956
+ "Move Backward",
1957
+ "Move Left",
1958
+ "Move Right",
1959
+ "Orbit Left 15°",
1960
+ "Orbit Right 15°"
1961
+ ]).name("Template Type");
1962
+
1963
+ // 生成轨迹按钮
1964
+ const generateTrajectoryController = trajectoryFolder.add(guiOptions, "generateTrajectory").name("Generate Trajectory");
1965
+
1966
+ // 加载/保存JSON轨迹按钮
1967
+ const loadTrajectoryController = trajectoryFolder.add(guiOptions, "LoadTrajectoryFromJson").name("Load from JSON");
1968
+ const saveTrajectoryController = trajectoryFolder.add(guiOptions, "saveTrajectoryToJson").name("Save Trajectory");
1969
+
1970
+ // 初始状态:禁用保存按钮(相机数量不够)
1971
+ saveTrajectoryController.disable();
1972
+
1973
+ // 清理相机按钮
1974
+ const clearAllCamerasController = trajectoryFolder.add(guiOptions, "clearAllCameras").name("Clear All Cameras");
1975
+
1976
+ // 初始状态:禁用所有轨迹相关控件
1977
+ templateTypeController.disable();
1978
+ generateTrajectoryController.disable();
1979
+ loadTrajectoryController.disable();
1980
+
1981
+ // 轨迹模式变化时的处理
1982
+ trajectoryModeController.onChange((value) => {
1983
+ if (value === "Manual") {
1984
+ templateTypeController.disable();
1985
+ generateTrajectoryController.disable();
1986
+ loadTrajectoryController.disable();
1987
+ } else if (value === "Template") {
1988
+ templateTypeController.enable();
1989
+ if (fixGenerationFOV) {
1990
+ generateTrajectoryController.enable();
1991
+ } else {
1992
+ generateTrajectoryController.disable();
1993
+ }
1994
+ loadTrajectoryController.disable();
1995
+ } else if (value === "JSON") {
1996
+ templateTypeController.disable();
1997
+ generateTrajectoryController.disable();
1998
+ if (fixGenerationFOV) {
1999
+ loadTrajectoryController.enable();
2000
+ } else {
2001
+ loadTrajectoryController.disable();
2002
+ }
2003
+ }
2004
+ });
2005
+
2006
+ // 当Configuration固定时启用轨迹生成
2007
+ const originalFixFOV = guiOptions.fixGenerationFOV;
2008
+ guiOptions.fixGenerationFOV = () => {
2009
+ originalFixFOV();
2010
+
2011
+ // Fix Configuration后禁用所有Step 1的控制器
2012
+ fovController.disable();
2013
+ resolutionController.disable();
2014
+
2015
+ // 根据当前轨迹模式启用相应控件
2016
+ if (guiOptions.trajectoryMode === "Template") {
2017
+ generateTrajectoryController.enable();
2018
+ } else if (guiOptions.trajectoryMode === "JSON") {
2019
+ loadTrajectoryController.enable();
2020
+ }
2021
+ updateStatus('Configuration fixed. You can now generate camera trajectory.', cameraParams.length);
2022
+ };
2023
+
2024
+ trajectoryFolder.open();
2025
+
2026
+ step2Folder.add(guiOptions, "VisualizeCameraSplats").name("Visualize Cameras").onChange((value) => {
2027
+ cameraSplats.forEach(cameraSplat => {
2028
+ cameraSplat.opacity = value ? 1 : 0;
2029
+ });
2030
+ });
2031
+ step2Folder.add(guiOptions, "VisualizeInterpolatedCameras").name("Visualize Interpolated Cameras").onChange((value) => {
2032
+ interpolatedCamerasSplats.forEach(interpolatedCameraSplat => {
2033
+ interpolatedCameraSplat.opacity = value ? 1 : 0;
2034
+ });
2035
+ });
2036
+
2037
+ // Store controllers globally so they can be accessed from guiOptions
2038
+ window.fixGenerationFOVController = fixGenerationFOVController;
2039
+ window.saveTrajectoryController = saveTrajectoryController;
2040
+
2041
+ // Step 3: Add Scene Prompts
2042
+ const step3Folder = gui.addFolder('3. Add Scene Prompts');
2043
+ step3Folder.add(guiOptions, "inputImagePrompt").name("Input Image Prompt");
2044
+ const inputTextPromptController = step3Folder.add(guiOptions, "inputTextPrompt").name("Input Text Prompt");
2045
+ const imageIndexController = step3Folder.add(guiOptions, "imageIndex", 0, 24, 1).name("Image Index");
2046
+ // Expose for programmatic updates after JSON load
2047
+ window.inputTextPromptController = inputTextPromptController;
2048
+ window.imageIndexController = imageIndexController;
2049
+
2050
+
2051
+ // Step 4: Generate Your Scene
2052
+ const step4Folder = gui.addFolder('4. Generate Scene');
2053
+ step4Folder.add(guiOptions, "generate").name("Generate!");
2054
+ step4Folder.open();
2055
+
2056
+ // Step 5: Trajectory Playback (Scrubber)
2057
+ const step5Folder = gui.addFolder('5. Trajectory Playback');
2058
+ step5Folder.add(guiOptions, 'playbackT', 0, 1, 0.001).name('Scrub (0-1)').onChange((value) => {
2059
+ // 首次拖动时记录用户相机状态,便于需要时恢复(可选)
2060
+ if (!userCameraState) {
2061
+ userCameraState = {
2062
+ position: camera.position.clone(),
2063
+ quaternion: camera.quaternion.clone(),
2064
+ fov: camera.fov
2065
+ };
2066
+ }
2067
+ setCameraByScrub(value);
2068
+ updateStatus(`Scrubbing trajectory: t=${value.toFixed(3)}`, cameraParams.length);
2069
+ });
2070
+ step5Folder.open();
2071
+
2072
+ // Step 6: Load/Save All JSON
2073
+ const step6Folder = gui.addFolder('6. JSON (All-in-one)');
2074
+ step6Folder.add(guiOptions, "LoadAllFromJson").name("Load All from JSON");
2075
+ step6Folder.add(guiOptions, "SaveAllToJson").name("Save All to JSON");
2076
+ step6Folder.open();
2077
+
2078
+ }
2079
+ }
2080
+
2081
+
2082
+ // =========================
2083
+ // File Input (Image Prompt)
2084
+ // =========================
2085
+ const fileInput = document.querySelector("#file-input");
2086
+ fileInput.onchange = (event) => {
2087
+ const files = event.target.files;
2088
+ if (!files || files.length === 0) return;
2089
+ Array.from(files).forEach(file => {
2090
+ const reader = new FileReader();
2091
+ reader.onload = function(e) {
2092
+ console.log("Loaded image:", file.name, e.target.result);
2093
+
2094
+ // 获取当前Resolution
2095
+ let resolutionStr = guiOptions.Resolution;
2096
+ let [n, h, w] = resolutionStr.split('x').map(Number);
2097
+
2098
+ // 加载图片
2099
+ const img = new Image();
2100
+ img.onload = function() {
2101
+ window.inputImageResolution = { width: img.width, height: img.height };
2102
+ console.log("Input image resolution:", window.inputImageResolution);
2103
+
2104
+ // 计算center crop参数
2105
+ let scaleH = h / img.height;
2106
+ let scaleW = w / img.width;
2107
+ let scale = Math.max(scaleH, scaleW);
2108
+ let newW = Math.round(w / scale);
2109
+ let newH = Math.round(h / scale);
2110
+ let sx = Math.floor((img.width - newW) / 2);
2111
+ let sy = Math.floor((img.height - newH) / 2);
2112
+
2113
+ // 创建canvas进行center crop和resize
2114
+ const canvas = document.createElement('canvas');
2115
+ canvas.width = w;
2116
+ canvas.height = h;
2117
+ const ctx = canvas.getContext('2d');
2118
+ ctx.drawImage(
2119
+ img,
2120
+ sx, sy, newW, newH, // source crop
2121
+ 0, 0, w, h // destination size
2122
+ );
2123
+ // 得到裁剪+缩放后的base64(用于后端)
2124
+ inputImageBase64 = canvas.toDataURL('image/png');
2125
+ // 更新预览为裁剪后的图
2126
+ const previewArea = document.getElementById('image-preview-area');
2127
+ const previewImg = document.getElementById('preview-img');
2128
+ if (previewImg && previewArea) {
2129
+ previewImg.src = inputImageBase64;
2130
+ previewArea.style.display = 'block';
2131
+ }
2132
+ // 记录传给后端的分辨率(已对齐为当前Resolution)
2133
+ window.inputImageResolution = { width: w, height: h };
2134
+ console.log("Cropped and resized image to:", w, h);
2135
+ };
2136
+ img.src = e.target.result;
2137
+ };
2138
+ reader.readAsDataURL(file);
2139
+ });
2140
+
2141
+ };
2142
+
2143
+ // =========================
2144
+ // File Input (JSON)
2145
+ // =========================
2146
+ // const jsonInput = document.querySelector("#json-input");
2147
+ // jsonInput.onchange = (event) => {
2148
+ // const files = event.target.files;
2149
+ // if (!files || files.length === 0) return;
2150
+ // const file = files[0];
2151
+ // const reader = new FileReader();
2152
+ // reader.onload = function(e) {
2153
+ // let jsonData;
2154
+ // try {
2155
+ // jsonData = JSON.parse(e.target.result);
2156
+ // } catch (error) {
2157
+ // alert("JSON parsing error: " + error);
2158
+ // console.error("JSON parsing error:", error);
2159
+ // return;
2160
+ // }
2161
+
2162
+ // // 清理所有已有的相机和插值相机
2163
+ // cameraSplats.forEach(splat => scene.remove(splat));
2164
+ // cameraSplats.length = 0;
2165
+ // cameraParams.length = 0;
2166
+ // interpolatedCamerasSplats.forEach(splat => scene.remove(splat));
2167
+ // interpolatedCamerasSplats.length = 0;
2168
+
2169
+ // try {
2170
+ // // 兼容不同命名的字段
2171
+ // const imagePrompt = jsonData.image_prompt || jsonData.imagePrompt || null;
2172
+ // const textPrompt = jsonData.text_prompt || jsonData.textPrompt || "";
2173
+ // const cameras = jsonData.cameras || [];
2174
+ // const resolution = jsonData.resolution || [16, 480, 640];
2175
+ // const imageIndex = jsonData.image_index || jsonData.imageIndex || 0;
2176
+
2177
+ // console.log("Loaded JSON data:", {
2178
+ // imagePrompt,
2179
+ // textPrompt,
2180
+ // cameras: cameras.length,
2181
+ // resolution,
2182
+ // imageIndex
2183
+ // });
2184
+
2185
+ // // 处理图像提示
2186
+ // if (imagePrompt) {
2187
+ // inputImageBase64 = imagePrompt;
2188
+ // console.log("Image prompt loaded");
2189
+ // }
2190
+
2191
+ // // 设置文本提示
2192
+ // guiOptions.inputTextPrompt = textPrompt;
2193
+ // guiOptions.imageIndex = imageIndex;
2194
+
2195
+ // // 处理相机数据
2196
+ // if (cameras && cameras.length > 0) {
2197
+ // cameras.forEach(cameraData => {
2198
+ // // 解析分辨率
2199
+ // let aspect = 1.0;
2200
+ // if (Array.isArray(resolution) && resolution.length === 3) {
2201
+ // aspect = resolution[2] / resolution[1];
2202
+ // }
2203
+ // const cam = new THREE.PerspectiveCamera(60, aspect);
2204
+
2205
+ // // 设置位置
2206
+ // if (Array.isArray(cameraData.position) && cameraData.position.length === 3) {
2207
+ // cam.position.set(cameraData.position[0], cameraData.position[1], cameraData.position[2]);
2208
+ // }
2209
+
2210
+ // // 设置四元数
2211
+ // if (Array.isArray(cameraData.quaternion) && cameraData.quaternion.length === 4) {
2212
+ // // 注意:three.js的顺序是 (x, y, z, w)
2213
+ // cam.quaternion.set(
2214
+ // cameraData.quaternion[1],
2215
+ // cameraData.quaternion[2],
2216
+ // cameraData.quaternion[3],
2217
+ // cameraData.quaternion[0]
2218
+ // );
2219
+ // }
2220
+
2221
+ // // 设置FOV和焦距
2222
+ // if (cameraData.fx && cameraData.fy) {
2223
+ // // fx, fy: 焦距(像素)
2224
+ // // 假设分辨率为 [N, H, W]
2225
+ // // fov = 2 * atan(0.5 * H / fy) * 180 / PI
2226
+ // // 但原代码用的是 fx
2227
+ // let fov = 60;
2228
+ // if (cameraData.fx) {
2229
+ // fov = 2 * Math.atan(0.5 / cameraData.fx) * 180 / Math.PI;
2230
+ // }
2231
+ // cam.fov = fov;
2232
+ // cam.aspect = cameraData.fx / cameraData.fy;
2233
+ // cam.updateProjectionMatrix();
2234
+ // }
2235
+
2236
+ // const cameraSplat = createCameraSplat(cam);
2237
+ // cameraSplats.push(cameraSplat);
2238
+ // cameraParams.push({
2239
+ // position: cam.position.clone(),
2240
+ // quaternion: cam.quaternion.clone(),
2241
+ // fov: cam.fov,
2242
+ // aspect: cam.aspect,
2243
+ // });
2244
+ // scene.add(cameraSplat);
2245
+ // });
2246
+ // console.log(`Loaded ${cameras.length} cameras`);
2247
+ // }
2248
+
2249
+ // // 设置分辨率
2250
+ // if (Array.isArray(resolution) && resolution.length === 3) {
2251
+ // guiOptions.Resolution = `${resolution[0]}x${resolution[1]}x${resolution[2]}`;
2252
+ // }
2253
+
2254
+ // alert("JSON loaded");
2255
+ // } catch (error) {
2256
+ // alert("JSON data processing error: " + error);
2257
+ // console.error("JSON data processing error:", error);
2258
+ // }
2259
+ // };
2260
+ // reader.readAsText(file);
2261
+ // };
2262
+
2263
+ const jsonInput = document.querySelector("#json-input");
2264
+ jsonInput.onchange = (event) => {
2265
+ const files = event.target.files;
2266
+ if (!files || files.length === 0) return;
2267
+ const file = files[0];
2268
+ const reader = new FileReader();
2269
+ reader.onload = function(e) {
2270
+ let jsonData;
2271
+ try {
2272
+ jsonData = JSON.parse(e.target.result);
2273
+ } catch (error) {
2274
+ console.error("JSON parsing error:", error);
2275
+ return;
2276
+ }
2277
+ const loadTrajectoryOnly = !!window.loadTrajectoryOnly;
2278
+ window.loadTrajectoryOnly = false;
2279
+ processJsonLoad(jsonData, loadTrajectoryOnly);
2280
+ };
2281
+ reader.readAsText(file);
2282
+ };
2283
+
2284
+ // =========================
2285
+ // Keyboard Controls
2286
+ // =========================
2287
+ document.addEventListener('keypress', (event) => {
2288
+ if (event.code === 'Space') {
2289
+ if (!fixGenerationFOV) {
2290
+ updateStatus('Please fix Generation FOV first', cameraParams.length);
2291
+ return;
2292
+ }
2293
+ // 记录当前相机的pose
2294
+ const new_camera = camera.clone();
2295
+ new_camera.fov = guiOptions.FOV;
2296
+ new_camera.aspect = guiOptions.Resolution.split('x')[2] / guiOptions.Resolution.split('x')[1];
2297
+ new_camera.updateProjectionMatrix();
2298
+
2299
+ const cameraSplat = createCameraSplat(new_camera);
2300
+ cameraSplats.push(cameraSplat);
2301
+ cameraParams.push({
2302
+ position: new_camera.position.clone(),
2303
+ quaternion: new_camera.quaternion.clone(),
2304
+ fov: new_camera.fov,
2305
+ aspect: new_camera.aspect,
2306
+ });
2307
+ scene.add(cameraSplat);
2308
+
2309
+ updateStatus(`Camera ${cameraParams.length} recorded. Press Space for more or Generate!`, cameraParams.length);
2310
+
2311
+ console.log(new_camera.getFocalLength());
2312
+ }
2313
+ });
2314
+
2315
+ // =========================
2316
+ // Scene Initialization
2317
+ // =========================
2318
+
2319
+ // Initialize status
2320
+ updateStatus('FlashWorld initialized. Configure settings to begin.', 0);
2321
+
2322
+ // Add cube splat to the scene
2323
+ let instructionSplat = createCubeSplat(0.25, [1, 1, 1]);
2324
+ instructionSplat.position.set(0, 0, -1);
2325
+ scene.add(instructionSplat);
2326
+ console.log('Cube splat added to scene');
2327
+
2328
+ // Handle window resize
2329
+ window.addEventListener('resize', () => {
2330
+ console.log('Window resized, updating canvas...');
2331
+ // Update canvas size based on current resolution
2332
+ updateCanvasSize();
2333
+ });
2334
+
2335
+ // =========================
2336
+ // Animation Loop
2337
+ // =========================
2338
+ let lastTime = null;
2339
+
2340
+ renderer.setAnimationLoop(function animate(time) {
2341
+ const deltaTime = time - (lastTime || time);
2342
+ lastTime = time;
2343
+
2344
+ // Rotate the cube splat
2345
+ if (instructionSplat) {
2346
+ // instructionSplat.rotation.x += deltaTime / 4000; // 绕X轴旋转
2347
+ instructionSplat.rotation.y += deltaTime / 5000; // 绕Y轴旋转
2348
+ instructionSplat.rotation.z += deltaTime / 6000; // 绕Z轴旋转
2349
+ }
2350
+
2351
+ // No active playback loop; scrubber directly sets camera
2352
+
2353
+ controls.update(camera);
2354
+ renderer.render(scene, camera);
2355
+
2356
+ });
2357
+
2358
+ </script>
2359
+ </body>
2360
+ </html>