underpost 2.85.1 → 2.89.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.development +2 -1
- package/.env.production +2 -1
- package/.env.test +2 -1
- package/.github/workflows/release.cd.yml +3 -3
- package/.vscode/zed.keymap.json +22 -0
- package/README.md +3 -3
- package/bin/build.js +8 -10
- package/bin/deploy.js +4 -2
- package/bin/file.js +4 -0
- package/bin/vs.js +4 -4
- package/cli.md +16 -11
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/manifests/deployment/dd-test-development/deployment.yaml +50 -50
- package/manifests/deployment/dd-test-development/proxy.yaml +4 -4
- package/package.json +2 -2
- package/src/api/file/file.service.js +29 -3
- package/src/cli/baremetal.js +4 -5
- package/src/cli/deploy.js +26 -4
- package/src/cli/index.js +8 -3
- package/src/cli/repository.js +42 -45
- package/src/cli/run.js +217 -48
- package/src/client/components/core/AgGrid.js +42 -3
- package/src/client/components/core/CommonJs.js +5 -0
- package/src/client/components/core/Css.js +95 -48
- package/src/client/components/core/CssCore.js +0 -1
- package/src/client/components/core/LoadingAnimation.js +2 -2
- package/src/client/components/core/Logger.js +2 -9
- package/src/client/components/core/Modal.js +22 -14
- package/src/client/components/core/ObjectLayerEngine.js +300 -9
- package/src/client/components/core/ObjectLayerEngineModal.js +686 -148
- package/src/client/components/core/ObjectLayerEngineViewer.js +1061 -0
- package/src/client/components/core/Pagination.js +15 -5
- package/src/client/components/core/Router.js +5 -1
- package/src/client/components/core/SocketIo.js +5 -1
- package/src/client/components/core/Translate.js +4 -0
- package/src/client/components/core/Worker.js +8 -1
- package/src/client/services/default/default.management.js +86 -16
- package/src/client/sw/default.sw.js +193 -97
- package/src/client.dev.js +1 -1
- package/src/db/mariadb/MariaDB.js +2 -2
- package/src/index.js +1 -1
- package/src/proxy.js +1 -1
- package/src/runtime/express/Express.js +4 -1
- package/src/server/auth.js +2 -1
- package/src/server/client-build.js +57 -2
- package/src/server/conf.js +132 -15
- package/src/server/object-layer.js +44 -0
- package/src/server/proxy.js +53 -26
- package/src/server/start.js +25 -3
- package/src/server/tls.js +1 -1
- package/src/ws/IoInterface.js +2 -3
- package/AUTHORS.md +0 -21
- package/src/server/network.js +0 -72
|
@@ -0,0 +1,1061 @@
|
|
|
1
|
+
import { loggerFactory } from './Logger.js';
|
|
2
|
+
import { getProxyPath, listenQueryPathInstance } from './Router.js';
|
|
3
|
+
import { ObjectLayerService } from '../../services/object-layer/object-layer.service.js';
|
|
4
|
+
import { NotificationManager } from './NotificationManager.js';
|
|
5
|
+
import { htmls, s } from './VanillaJs.js';
|
|
6
|
+
import { BtnIcon } from './BtnIcon.js';
|
|
7
|
+
import { darkTheme, ThemeEvents } from './Css.js';
|
|
8
|
+
import { ObjectLayerCyberiaPortal } from '../cyberia-portal/ObjectLayerCyberiaPortal.js';
|
|
9
|
+
|
|
10
|
+
const logger = loggerFactory(import.meta);
|
|
11
|
+
|
|
12
|
+
const ObjectLayerEngineViewer = {
|
|
13
|
+
Data: {
|
|
14
|
+
objectLayer: null,
|
|
15
|
+
frameCounts: null,
|
|
16
|
+
currentDirection: 'down',
|
|
17
|
+
currentMode: 'idle',
|
|
18
|
+
gif: null,
|
|
19
|
+
gifWorkerBlob: null,
|
|
20
|
+
isGenerating: false,
|
|
21
|
+
// Binary transparency settings for GIF export
|
|
22
|
+
gifTransparencyPlaceholder: { r: 100, g: 100, b: 100 }, // magenta - unlikely to exist in sprites
|
|
23
|
+
transparencyThreshold: 16, // alpha threshold (0-255) for binary transparency
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
// Map user-friendly direction/mode to numeric direction codes
|
|
27
|
+
getDirectionCode: function (direction, mode) {
|
|
28
|
+
const key = `${direction}_${mode}`;
|
|
29
|
+
const directionCodeMap = {
|
|
30
|
+
down_idle: '08',
|
|
31
|
+
down_walking: '18',
|
|
32
|
+
up_idle: '02',
|
|
33
|
+
up_walking: '12',
|
|
34
|
+
left_idle: '04',
|
|
35
|
+
left_walking: '14',
|
|
36
|
+
right_idle: '06',
|
|
37
|
+
right_walking: '16',
|
|
38
|
+
};
|
|
39
|
+
return directionCodeMap[key] || null;
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
// Get all possible direction names for a direction code
|
|
43
|
+
getDirectionsFromDirectionCode: function (directionCode) {
|
|
44
|
+
const directionMap = {
|
|
45
|
+
'08': ['down_idle', 'none_idle', 'default_idle'],
|
|
46
|
+
18: ['down_walking'],
|
|
47
|
+
'02': ['up_idle'],
|
|
48
|
+
12: ['up_walking'],
|
|
49
|
+
'04': ['left_idle', 'up_left_idle', 'down_left_idle'],
|
|
50
|
+
14: ['left_walking', 'up_left_walking', 'down_left_walking'],
|
|
51
|
+
'06': ['right_idle', 'up_right_idle', 'down_right_idle'],
|
|
52
|
+
16: ['right_walking', 'up_right_walking', 'down_right_walking'],
|
|
53
|
+
};
|
|
54
|
+
return directionMap[directionCode] || [];
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
Render: async function ({ Elements }) {
|
|
58
|
+
const id = 'object-layer-engine-viewer';
|
|
59
|
+
|
|
60
|
+
// Listen for cid query parameter
|
|
61
|
+
listenQueryPathInstance(
|
|
62
|
+
{
|
|
63
|
+
id: `${id}-query-listener`,
|
|
64
|
+
routeId: 'object-layer-engine-viewer',
|
|
65
|
+
event: async (cid) => {
|
|
66
|
+
if (cid) {
|
|
67
|
+
await this.loadObjectLayer(cid);
|
|
68
|
+
} else {
|
|
69
|
+
this.renderEmpty();
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
'cid',
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
setTimeout(async () => {
|
|
77
|
+
htmls(
|
|
78
|
+
`#${id}`,
|
|
79
|
+
html` <div class="inl section-mp">
|
|
80
|
+
<div class="in">
|
|
81
|
+
<div class="fl">
|
|
82
|
+
<div class="in fll">
|
|
83
|
+
${await BtnIcon.Render({
|
|
84
|
+
class: 'section-mp main-button',
|
|
85
|
+
label: html`<i class="fa-solid fa-arrow-left"></i> ${' Back'}`,
|
|
86
|
+
attrs: `data-id="btn-back"`,
|
|
87
|
+
})}
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
</div>`,
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
return html`
|
|
96
|
+
<div class="fl">
|
|
97
|
+
<div class="in ${id}" id="${id}">
|
|
98
|
+
<div class="in section-mp">
|
|
99
|
+
<div class="in">Loading object layer...</div>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
`;
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
renderEmpty: async function () {
|
|
107
|
+
const id = 'object-layer-engine-viewer';
|
|
108
|
+
htmls(`#${id}`, await ObjectLayerCyberiaPortal.Render());
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
loadObjectLayer: async function (objectLayerId) {
|
|
112
|
+
const id = 'object-layer-engine-viewer';
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
// Load metadata first
|
|
116
|
+
const { status: metaStatus, data: metadata } = await ObjectLayerService.getMetadata({ id: objectLayerId });
|
|
117
|
+
|
|
118
|
+
if (metaStatus !== 'success' || !metadata) {
|
|
119
|
+
throw new Error('Failed to load object layer metadata');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
this.Data.objectLayer = metadata;
|
|
123
|
+
|
|
124
|
+
// Load frame counts for all directions
|
|
125
|
+
const { status: frameStatus, data: frameData } = await ObjectLayerService.getFrameCounts({ id: objectLayerId });
|
|
126
|
+
|
|
127
|
+
if (frameStatus !== 'success' || !frameData) {
|
|
128
|
+
throw new Error('Failed to load frame counts');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
this.Data.frameCounts = frameData.frameCounts;
|
|
132
|
+
|
|
133
|
+
// Auto-select first available direction/mode combination
|
|
134
|
+
this.selectFirstAvailableDirectionMode();
|
|
135
|
+
|
|
136
|
+
// Render the viewer UI
|
|
137
|
+
await this.renderViewer();
|
|
138
|
+
|
|
139
|
+
// Initialize gif.js worker
|
|
140
|
+
await this.initGifJs();
|
|
141
|
+
|
|
142
|
+
// Generate initial GIF
|
|
143
|
+
await this.generateGif();
|
|
144
|
+
} catch (error) {
|
|
145
|
+
logger.error('Error loading object layer:', error);
|
|
146
|
+
NotificationManager.Push({
|
|
147
|
+
html: `Failed to load object layer: ${error.message}`,
|
|
148
|
+
status: 'error',
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
htmls(
|
|
152
|
+
`#${id}`,
|
|
153
|
+
html`
|
|
154
|
+
<div class="in section-mp">
|
|
155
|
+
<div class="in">
|
|
156
|
+
<h3>Error</h3>
|
|
157
|
+
<p>Failed to load object layer. Please try again.</p>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
`,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
renderViewer: async function () {
|
|
166
|
+
const id = 'object-layer-engine-viewer';
|
|
167
|
+
const { objectLayer, frameCounts } = this.Data;
|
|
168
|
+
|
|
169
|
+
if (!objectLayer || !frameCounts) return;
|
|
170
|
+
|
|
171
|
+
const itemType = objectLayer.data.item.type;
|
|
172
|
+
const itemId = objectLayer.data.item.id;
|
|
173
|
+
const itemDescription = objectLayer.data.item.description || '';
|
|
174
|
+
const itemActivable = objectLayer.data.item.activable || false;
|
|
175
|
+
const frameDuration = objectLayer.data.render.frame_duration || 100;
|
|
176
|
+
const isStateless = objectLayer.data.render.is_stateless || false;
|
|
177
|
+
|
|
178
|
+
// Get stats data
|
|
179
|
+
const stats = objectLayer.data.stats || {};
|
|
180
|
+
|
|
181
|
+
// Helper function to check if direction/mode has frames
|
|
182
|
+
const hasFrames = (direction, mode) => {
|
|
183
|
+
const numericCode = this.getDirectionCode(direction, mode);
|
|
184
|
+
return numericCode && frameCounts[numericCode] && frameCounts[numericCode] > 0;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
// Helper function to get frame count
|
|
188
|
+
const getFrameCount = (direction, mode) => {
|
|
189
|
+
const numericCode = this.getDirectionCode(direction, mode);
|
|
190
|
+
return numericCode ? frameCounts[numericCode] || 0 : 0;
|
|
191
|
+
};
|
|
192
|
+
ThemeEvents[id] = () => {
|
|
193
|
+
if (!s(`.style-${id}`)) return;
|
|
194
|
+
htmls(
|
|
195
|
+
`.style-${id}`,
|
|
196
|
+
html` <style>
|
|
197
|
+
.object-layer-viewer-container {
|
|
198
|
+
max-width: 800px;
|
|
199
|
+
margin: 0 auto;
|
|
200
|
+
padding: 20px;
|
|
201
|
+
font-family: 'retro-font';
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.viewer-header {
|
|
205
|
+
text-align: center;
|
|
206
|
+
margin-bottom: 30px;
|
|
207
|
+
padding-bottom: 20px;
|
|
208
|
+
border-bottom: 2px solid ${darkTheme ? '#444' : '#ddd'};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.viewer-header h2 {
|
|
212
|
+
margin: 0 0 10px 0;
|
|
213
|
+
color: ${darkTheme ? '#fff' : '#333'};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.gif-display-area {
|
|
217
|
+
background: ${darkTheme ? '#2a2a2a' : '#f5f5f5'};
|
|
218
|
+
border: 2px solid ${darkTheme ? '#444' : '#ddd'};
|
|
219
|
+
border-radius: 12px;
|
|
220
|
+
padding: 30px;
|
|
221
|
+
margin-bottom: 30px;
|
|
222
|
+
display: flex;
|
|
223
|
+
justify-content: center;
|
|
224
|
+
align-items: center;
|
|
225
|
+
min-height: 300px;
|
|
226
|
+
height: auto;
|
|
227
|
+
max-height: 600px;
|
|
228
|
+
position: relative;
|
|
229
|
+
overflow: auto;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.gif-canvas-container {
|
|
233
|
+
position: relative;
|
|
234
|
+
display: flex;
|
|
235
|
+
justify-content: center;
|
|
236
|
+
align-items: center;
|
|
237
|
+
width: 100%;
|
|
238
|
+
height: 100%;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.gif-canvas-container canvas,
|
|
242
|
+
.gif-canvas-container img {
|
|
243
|
+
image-rendering: pixelated;
|
|
244
|
+
image-rendering: -moz-crisp-edges;
|
|
245
|
+
image-rendering: crisp-edges;
|
|
246
|
+
-ms-interpolation-mode: nearest-neighbor;
|
|
247
|
+
background: repeating-conic-gradient(#80808020 0% 25%, #fff0 0% 50%) 50% / 20px 20px;
|
|
248
|
+
border-radius: 8px;
|
|
249
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
250
|
+
max-width: 100%;
|
|
251
|
+
max-height: 540px;
|
|
252
|
+
width: auto !important;
|
|
253
|
+
height: auto !important;
|
|
254
|
+
object-fit: contain;
|
|
255
|
+
display: block;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.gif-canvas-container canvas {
|
|
259
|
+
background: repeating-conic-gradient(#80808020 0% 25%, #fff0 0% 50%) 50% / 20px 20px;
|
|
260
|
+
min-width: 128px;
|
|
261
|
+
min-height: 128px;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.gif-info-badge {
|
|
265
|
+
position: absolute;
|
|
266
|
+
bottom: 10px;
|
|
267
|
+
right: 10px;
|
|
268
|
+
background: rgba(0, 0, 0, 0.2);
|
|
269
|
+
color: ${darkTheme ? 'white' : 'black'};
|
|
270
|
+
padding: 6px 12px;
|
|
271
|
+
border-radius: 4px;
|
|
272
|
+
font-size: 12px;
|
|
273
|
+
font-family: monospace;
|
|
274
|
+
backdrop-filter: blur(4px);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.gif-info-badge .info-label {
|
|
278
|
+
opacity: 0.7;
|
|
279
|
+
margin-right: 4px;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.loading-overlay {
|
|
283
|
+
position: absolute;
|
|
284
|
+
top: 0;
|
|
285
|
+
left: 0;
|
|
286
|
+
right: 0;
|
|
287
|
+
bottom: 0;
|
|
288
|
+
background: rgba(0, 0, 0, 0.7);
|
|
289
|
+
display: flex;
|
|
290
|
+
justify-content: center;
|
|
291
|
+
align-items: center;
|
|
292
|
+
color: white;
|
|
293
|
+
border-radius: 8px;
|
|
294
|
+
z-index: 10;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
.controls-container {
|
|
298
|
+
display: flex;
|
|
299
|
+
flex-direction: column;
|
|
300
|
+
gap: 20px;
|
|
301
|
+
margin-bottom: 20px;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.control-group {
|
|
305
|
+
background: ${darkTheme ? '#2a2a2a' : '#fff'};
|
|
306
|
+
border: 1px solid ${darkTheme ? '#444' : '#ddd'};
|
|
307
|
+
border-radius: 8px;
|
|
308
|
+
padding: 15px 20px;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.control-group h4 {
|
|
312
|
+
margin: 0 0 15px 0;
|
|
313
|
+
color: ${darkTheme ? '#fff' : '#333'};
|
|
314
|
+
font-size: 20px;
|
|
315
|
+
text-transform: uppercase;
|
|
316
|
+
letter-spacing: 1px;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.button-group {
|
|
320
|
+
display: flex;
|
|
321
|
+
gap: 10px;
|
|
322
|
+
flex-wrap: wrap;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
.control-btn {
|
|
326
|
+
flex: 1;
|
|
327
|
+
min-width: 80px;
|
|
328
|
+
padding: 12px 20px;
|
|
329
|
+
border: 2px solid ${darkTheme ? '#444' : '#ddd'};
|
|
330
|
+
background: ${darkTheme ? '#333' : '#f9f9f9'};
|
|
331
|
+
color: ${darkTheme ? '#fff' : '#333'};
|
|
332
|
+
border-radius: 6px;
|
|
333
|
+
cursor: pointer;
|
|
334
|
+
transition: all 0.2s ease;
|
|
335
|
+
font-size: 14px;
|
|
336
|
+
font-weight: 600;
|
|
337
|
+
display: flex;
|
|
338
|
+
align-items: center;
|
|
339
|
+
justify-content: center;
|
|
340
|
+
gap: 8px;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
.control-btn:hover {
|
|
344
|
+
background: ${darkTheme ? '#444' : '#f0f0f0'};
|
|
345
|
+
transform: translateY(-2px);
|
|
346
|
+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
.control-btn.active {
|
|
350
|
+
background: ${darkTheme ? '#4a9eff' : '#2196F3'};
|
|
351
|
+
color: white;
|
|
352
|
+
border-color: ${darkTheme ? '#4a9eff' : '#2196F3'};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
.control-btn i {
|
|
356
|
+
font-size: 16px;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
.download-btn {
|
|
360
|
+
width: 100%;
|
|
361
|
+
padding: 15px;
|
|
362
|
+
background: ${darkTheme ? '#4caf50' : '#4CAF50'};
|
|
363
|
+
color: white;
|
|
364
|
+
border: none;
|
|
365
|
+
border-radius: 8px;
|
|
366
|
+
cursor: pointer;
|
|
367
|
+
font-size: 16px;
|
|
368
|
+
font-weight: 600;
|
|
369
|
+
transition: all 0.2s ease;
|
|
370
|
+
display: flex;
|
|
371
|
+
align-items: center;
|
|
372
|
+
justify-content: center;
|
|
373
|
+
gap: 10px;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
.download-btn:hover {
|
|
377
|
+
background: ${darkTheme ? '#45a049' : '#45a049'};
|
|
378
|
+
transform: translateY(-2px);
|
|
379
|
+
box-shadow: 0 4px 12px rgba(76, 175, 80, 0.3);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
.control-btn:disabled {
|
|
383
|
+
background: ${darkTheme ? '#555' : '#ccc'};
|
|
384
|
+
cursor: not-allowed;
|
|
385
|
+
transform: none;
|
|
386
|
+
opacity: 0.5;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
.control-btn .frame-count {
|
|
390
|
+
font-size: 11px;
|
|
391
|
+
opacity: 0.7;
|
|
392
|
+
margin-left: 4px;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
.download-btn:disabled {
|
|
396
|
+
background: ${darkTheme ? '#555' : '#ccc'};
|
|
397
|
+
cursor: not-allowed;
|
|
398
|
+
transform: none;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
@media (max-width: 768px) {
|
|
402
|
+
.gif-display-area {
|
|
403
|
+
max-height: 500px;
|
|
404
|
+
min-height: 300px;
|
|
405
|
+
padding: 20px;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
.gif-canvas-container canvas,
|
|
409
|
+
.gif-canvas-container img {
|
|
410
|
+
max-width: 100%;
|
|
411
|
+
max-height: 440px;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
@media (max-width: 600px) {
|
|
416
|
+
.gif-display-area {
|
|
417
|
+
max-height: 400px;
|
|
418
|
+
min-height: 250px;
|
|
419
|
+
padding: 15px;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
.gif-canvas-container canvas,
|
|
423
|
+
.gif-canvas-container img {
|
|
424
|
+
max-height: 340px;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
.button-group {
|
|
428
|
+
flex-direction: column;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
.control-btn {
|
|
432
|
+
min-width: 100%;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
.item-data-key-label {
|
|
436
|
+
font-size: 16px;
|
|
437
|
+
color: ${darkTheme ? '#aaa' : '#666'};
|
|
438
|
+
text-transform: uppercase;
|
|
439
|
+
}
|
|
440
|
+
.item-data-value-label {
|
|
441
|
+
font-size: 20px;
|
|
442
|
+
font-weight: 700;
|
|
443
|
+
color: ${darkTheme ? '#4a9eff' : '#2196F3'};
|
|
444
|
+
}
|
|
445
|
+
.item-stat-entry {
|
|
446
|
+
display: flex;
|
|
447
|
+
flex-direction: column;
|
|
448
|
+
gap: 6px;
|
|
449
|
+
padding: 12px;
|
|
450
|
+
background: ${darkTheme ? '#1a1a1a' : '#f9f9f9'};
|
|
451
|
+
border-radius: 6px;
|
|
452
|
+
border: 1px solid ${darkTheme ? '#333' : '#e0e0e0'};
|
|
453
|
+
}
|
|
454
|
+
.no-data-container {
|
|
455
|
+
grid-column: 1 / -1;
|
|
456
|
+
text-align: center;
|
|
457
|
+
color: ${darkTheme ? '#666' : '#999'};
|
|
458
|
+
padding: 20px;
|
|
459
|
+
}
|
|
460
|
+
</style>`,
|
|
461
|
+
);
|
|
462
|
+
};
|
|
463
|
+
htmls(
|
|
464
|
+
`#${id}`,
|
|
465
|
+
html`
|
|
466
|
+
<div class="hide style-${id}"></div>
|
|
467
|
+
|
|
468
|
+
<div class="object-layer-viewer-container">
|
|
469
|
+
<!-- Item Data Section -->
|
|
470
|
+
<div class="control-group" style="margin-bottom: 20px;">
|
|
471
|
+
<h4><i class="fa-solid fa-cube"></i> Item Data</h4>
|
|
472
|
+
<div
|
|
473
|
+
style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px; padding: 10px 0;"
|
|
474
|
+
>
|
|
475
|
+
<div style="display: flex; flex-direction: column; gap: 4px;">
|
|
476
|
+
<span class="item-data-key-label">Item ID</span>
|
|
477
|
+
<span style="font-weight: 600;">${itemId}</span>
|
|
478
|
+
</div>
|
|
479
|
+
<div style="display: flex; flex-direction: column; gap: 4px;">
|
|
480
|
+
<span class="item-data-key-label">Type</span>
|
|
481
|
+
<span style="font-weight: 600;">${itemType}</span>
|
|
482
|
+
</div>
|
|
483
|
+
${itemDescription
|
|
484
|
+
? html`<div style="display: flex; flex-direction: column; gap: 4px;">
|
|
485
|
+
<span class="item-data-key-label">Description</span>
|
|
486
|
+
<span style="font-weight: 600;">${itemDescription}</span>
|
|
487
|
+
</div>`
|
|
488
|
+
: ''}
|
|
489
|
+
<div style="display: flex; flex-direction: column; gap: 4px;">
|
|
490
|
+
<span class="item-data-key-label">Activable</span>
|
|
491
|
+
<span style="font-weight: 600;">${itemActivable ? 'Yes' : 'No'}</span>
|
|
492
|
+
</div>
|
|
493
|
+
</div>
|
|
494
|
+
</div>
|
|
495
|
+
|
|
496
|
+
<div class="gif-display-area">
|
|
497
|
+
<div class="gif-canvas-container" id="gif-canvas-container">
|
|
498
|
+
<div style="text-align: center; color: ${darkTheme ? '#aaa' : '#666'};">
|
|
499
|
+
<i class="fa-solid fa-image" style="font-size: 48px; opacity: 0.3; margin-bottom: 16px;"></i>
|
|
500
|
+
<p style="margin: 0; font-size: 14px;">GIF preview will appear here</p>
|
|
501
|
+
</div>
|
|
502
|
+
<div id="gif-loading-overlay" class="loading-overlay" style="display: none;">
|
|
503
|
+
<div>
|
|
504
|
+
<i class="fa-solid fa-spinner fa-spin"></i>
|
|
505
|
+
<span style="margin-left: 10px;">Generating GIF...</span>
|
|
506
|
+
</div>
|
|
507
|
+
</div>
|
|
508
|
+
</div>
|
|
509
|
+
</div>
|
|
510
|
+
|
|
511
|
+
<div class="controls-container">
|
|
512
|
+
<div class="control-group">
|
|
513
|
+
<h4><i class="fa-solid fa-compass"></i> Direction</h4>
|
|
514
|
+
<div class="button-group">
|
|
515
|
+
<button
|
|
516
|
+
class="control-btn ${this.Data.currentDirection === 'up' ? 'active' : ''}"
|
|
517
|
+
data-direction="up"
|
|
518
|
+
${!hasFrames('up', this.Data.currentMode) ? 'disabled' : ''}
|
|
519
|
+
>
|
|
520
|
+
<i class="fa-solid fa-arrow-up"></i>
|
|
521
|
+
<span>Up</span>
|
|
522
|
+
${hasFrames('up', this.Data.currentMode)
|
|
523
|
+
? html`<span class="frame-count">(${getFrameCount('up', this.Data.currentMode)})</span>`
|
|
524
|
+
: ''}
|
|
525
|
+
</button>
|
|
526
|
+
<button
|
|
527
|
+
class="control-btn ${this.Data.currentDirection === 'down' ? 'active' : ''}"
|
|
528
|
+
data-direction="down"
|
|
529
|
+
${!hasFrames('down', this.Data.currentMode) ? 'disabled' : ''}
|
|
530
|
+
>
|
|
531
|
+
<i class="fa-solid fa-arrow-down"></i>
|
|
532
|
+
<span>Down</span>
|
|
533
|
+
${hasFrames('down', this.Data.currentMode)
|
|
534
|
+
? html`<span class="frame-count">(${getFrameCount('down', this.Data.currentMode)})</span>`
|
|
535
|
+
: ''}
|
|
536
|
+
</button>
|
|
537
|
+
<button
|
|
538
|
+
class="control-btn ${this.Data.currentDirection === 'left' ? 'active' : ''}"
|
|
539
|
+
data-direction="left"
|
|
540
|
+
${!hasFrames('left', this.Data.currentMode) ? 'disabled' : ''}
|
|
541
|
+
>
|
|
542
|
+
<i class="fa-solid fa-arrow-left"></i>
|
|
543
|
+
<span>Left</span>
|
|
544
|
+
${hasFrames('left', this.Data.currentMode)
|
|
545
|
+
? html`<span class="frame-count">(${getFrameCount('left', this.Data.currentMode)})</span>`
|
|
546
|
+
: ''}
|
|
547
|
+
</button>
|
|
548
|
+
<button
|
|
549
|
+
class="control-btn ${this.Data.currentDirection === 'right' ? 'active' : ''}"
|
|
550
|
+
data-direction="right"
|
|
551
|
+
${!hasFrames('right', this.Data.currentMode) ? 'disabled' : ''}
|
|
552
|
+
>
|
|
553
|
+
<i class="fa-solid fa-arrow-right"></i>
|
|
554
|
+
<span>Right</span>
|
|
555
|
+
${hasFrames('right', this.Data.currentMode)
|
|
556
|
+
? html`<span class="frame-count">(${getFrameCount('right', this.Data.currentMode)})</span>`
|
|
557
|
+
: ''}
|
|
558
|
+
</button>
|
|
559
|
+
</div>
|
|
560
|
+
</div>
|
|
561
|
+
|
|
562
|
+
<div class="control-group">
|
|
563
|
+
<h4><i class="fa-solid fa-person-running"></i> Mode</h4>
|
|
564
|
+
<div class="button-group">
|
|
565
|
+
<button
|
|
566
|
+
class="control-btn ${this.Data.currentMode === 'idle' ? 'active' : ''}"
|
|
567
|
+
data-mode="idle"
|
|
568
|
+
${!hasFrames(this.Data.currentDirection, 'idle') ? 'disabled' : ''}
|
|
569
|
+
>
|
|
570
|
+
<i class="fa-solid fa-user"></i>
|
|
571
|
+
<span>Idle</span>
|
|
572
|
+
${hasFrames(this.Data.currentDirection, 'idle')
|
|
573
|
+
? html`<span class="frame-count">(${getFrameCount(this.Data.currentDirection, 'idle')})</span>`
|
|
574
|
+
: ''}
|
|
575
|
+
</button>
|
|
576
|
+
<button
|
|
577
|
+
class="control-btn ${this.Data.currentMode === 'walking' ? 'active' : ''}"
|
|
578
|
+
data-mode="walking"
|
|
579
|
+
${!hasFrames(this.Data.currentDirection, 'walking') ? 'disabled' : ''}
|
|
580
|
+
>
|
|
581
|
+
<i class="fa-solid fa-person-walking"></i>
|
|
582
|
+
<span>Walking</span>
|
|
583
|
+
${hasFrames(this.Data.currentDirection, 'walking')
|
|
584
|
+
? html`<span class="frame-count">(${getFrameCount(this.Data.currentDirection, 'walking')})</span>`
|
|
585
|
+
: ''}
|
|
586
|
+
</button>
|
|
587
|
+
</div>
|
|
588
|
+
</div>
|
|
589
|
+
</div>
|
|
590
|
+
<!-- Stats Data Section -->
|
|
591
|
+
<div class="control-group" style="margin-bottom: 20px;">
|
|
592
|
+
<h4><i class="fa-solid fa-chart-bar"></i> Stats Data</h4>
|
|
593
|
+
<div
|
|
594
|
+
style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 15px; padding: 10px 0;"
|
|
595
|
+
>
|
|
596
|
+
${Object.keys(stats).length > 0
|
|
597
|
+
? Object.entries(stats)
|
|
598
|
+
.map(
|
|
599
|
+
([statKey, statValue]) => html`
|
|
600
|
+
<div class="item-stat-entry">
|
|
601
|
+
<span class="item-data-key-label"> ${statKey} </span>
|
|
602
|
+
<span style="item-data-value-label"> ${statValue} </span>
|
|
603
|
+
</div>
|
|
604
|
+
`,
|
|
605
|
+
)
|
|
606
|
+
.join('')
|
|
607
|
+
: html`<div class="no-data-container">No stats data available</div>`}
|
|
608
|
+
</div>
|
|
609
|
+
</div>
|
|
610
|
+
<button class="download-btn" id="download-gif-btn">
|
|
611
|
+
<i class="fa-solid fa-download"></i>
|
|
612
|
+
<span>Download GIF</span>
|
|
613
|
+
</button>
|
|
614
|
+
</div>
|
|
615
|
+
`,
|
|
616
|
+
);
|
|
617
|
+
ThemeEvents[id]();
|
|
618
|
+
// Attach event listeners
|
|
619
|
+
this.attachEventListeners();
|
|
620
|
+
},
|
|
621
|
+
|
|
622
|
+
attachEventListeners: function () {
|
|
623
|
+
// Direction buttons
|
|
624
|
+
const directionButtons = document.querySelectorAll('[data-direction]');
|
|
625
|
+
directionButtons.forEach((btn) => {
|
|
626
|
+
btn.addEventListener('click', async (e) => {
|
|
627
|
+
if (e.currentTarget.disabled) return;
|
|
628
|
+
const direction = e.currentTarget.getAttribute('data-direction');
|
|
629
|
+
if (direction !== this.Data.currentDirection) {
|
|
630
|
+
this.Data.currentDirection = direction;
|
|
631
|
+
await this.renderViewer();
|
|
632
|
+
await this.attachEventListeners();
|
|
633
|
+
await this.generateGif();
|
|
634
|
+
}
|
|
635
|
+
});
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
// Mode buttons
|
|
639
|
+
const modeButtons = document.querySelectorAll('[data-mode]');
|
|
640
|
+
modeButtons.forEach((btn) => {
|
|
641
|
+
btn.addEventListener('click', async (e) => {
|
|
642
|
+
if (e.currentTarget.disabled) return;
|
|
643
|
+
const mode = e.currentTarget.getAttribute('data-mode');
|
|
644
|
+
if (mode !== this.Data.currentMode) {
|
|
645
|
+
this.Data.currentMode = mode;
|
|
646
|
+
await this.renderViewer();
|
|
647
|
+
await this.attachEventListeners();
|
|
648
|
+
await this.generateGif();
|
|
649
|
+
}
|
|
650
|
+
});
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
// Download button
|
|
654
|
+
const downloadBtn = s('#download-gif-btn');
|
|
655
|
+
if (downloadBtn) {
|
|
656
|
+
downloadBtn.addEventListener('click', () => {
|
|
657
|
+
this.downloadGif();
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// Back button
|
|
662
|
+
setTimeout(() => {
|
|
663
|
+
const backBtn = s('[data-id="btn-back"]');
|
|
664
|
+
if (backBtn) {
|
|
665
|
+
backBtn.addEventListener('click', () => {
|
|
666
|
+
window.history.back();
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
}, 100);
|
|
670
|
+
},
|
|
671
|
+
|
|
672
|
+
selectFirstAvailableDirectionMode: function () {
|
|
673
|
+
const { frameCounts } = this.Data;
|
|
674
|
+
if (!frameCounts) return;
|
|
675
|
+
|
|
676
|
+
// Priority order for directions
|
|
677
|
+
const directions = ['down', 'up', 'left', 'right'];
|
|
678
|
+
// Priority order for modes
|
|
679
|
+
const modes = ['idle', 'walking'];
|
|
680
|
+
|
|
681
|
+
// Try to find first available combination using numeric codes
|
|
682
|
+
for (const mode of modes) {
|
|
683
|
+
for (const direction of directions) {
|
|
684
|
+
const numericCode = this.getDirectionCode(direction, mode);
|
|
685
|
+
if (numericCode && frameCounts[numericCode] && frameCounts[numericCode] > 0) {
|
|
686
|
+
this.Data.currentDirection = direction;
|
|
687
|
+
this.Data.currentMode = mode;
|
|
688
|
+
logger.info(`Auto-selected: ${direction} ${mode} (code: ${numericCode}, ${frameCounts[numericCode]} frames)`);
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// If no frames found, log warning
|
|
695
|
+
logger.warn('No frames found for any direction/mode combination');
|
|
696
|
+
},
|
|
697
|
+
|
|
698
|
+
initGifJs: async function () {
|
|
699
|
+
if (this.Data.gifWorkerBlob) return; // Already initialized
|
|
700
|
+
|
|
701
|
+
try {
|
|
702
|
+
// Load gif.js library
|
|
703
|
+
await this.loadScript('https://cdn.jsdelivr.net/npm/gif.js@0.2.0/dist/gif.min.js');
|
|
704
|
+
|
|
705
|
+
// Fetch worker script
|
|
706
|
+
const response = await fetch('https://cdn.jsdelivr.net/npm/gif.js@0.2.0/dist/gif.worker.js');
|
|
707
|
+
if (!response.ok) {
|
|
708
|
+
throw new Error('Failed to fetch gif.worker.js');
|
|
709
|
+
}
|
|
710
|
+
const workerBlob = await response.blob();
|
|
711
|
+
this.Data.gifWorkerBlob = URL.createObjectURL(workerBlob);
|
|
712
|
+
|
|
713
|
+
logger.info('gif.js initialized successfully');
|
|
714
|
+
} catch (error) {
|
|
715
|
+
logger.error('Error initializing gif.js:', error);
|
|
716
|
+
throw error;
|
|
717
|
+
}
|
|
718
|
+
},
|
|
719
|
+
|
|
720
|
+
loadScript: function (src) {
|
|
721
|
+
return new Promise((resolve, reject) => {
|
|
722
|
+
// Check if already loaded
|
|
723
|
+
if (document.querySelector(`script[src="${src}"]`)) {
|
|
724
|
+
resolve();
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const script = document.createElement('script');
|
|
729
|
+
script.src = src;
|
|
730
|
+
script.onload = resolve;
|
|
731
|
+
script.onerror = reject;
|
|
732
|
+
document.head.appendChild(script);
|
|
733
|
+
});
|
|
734
|
+
},
|
|
735
|
+
|
|
736
|
+
generateGif: async function () {
|
|
737
|
+
if (this.Data.isGenerating) return;
|
|
738
|
+
|
|
739
|
+
const { objectLayer, frameCounts, currentDirection, currentMode } = this.Data;
|
|
740
|
+
if (!objectLayer || !frameCounts) return;
|
|
741
|
+
|
|
742
|
+
// Get numeric direction code
|
|
743
|
+
const numericCode = this.getDirectionCode(currentDirection, currentMode);
|
|
744
|
+
if (!numericCode) {
|
|
745
|
+
NotificationManager.Push({
|
|
746
|
+
html: `Invalid direction/mode combination: ${currentDirection} ${currentMode}`,
|
|
747
|
+
status: 'error',
|
|
748
|
+
});
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
const frameCount = frameCounts[numericCode];
|
|
753
|
+
|
|
754
|
+
if (!frameCount || frameCount === 0) {
|
|
755
|
+
NotificationManager.Push({
|
|
756
|
+
html: `No frames available for ${currentDirection} ${currentMode}`,
|
|
757
|
+
status: 'warning',
|
|
758
|
+
});
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
const itemType = objectLayer.data.item.type;
|
|
763
|
+
const itemId = objectLayer.data.item.id;
|
|
764
|
+
const frameDuration = objectLayer.data.render.frame_duration || 100;
|
|
765
|
+
|
|
766
|
+
this.Data.isGenerating = true;
|
|
767
|
+
this.showLoading(true);
|
|
768
|
+
|
|
769
|
+
try {
|
|
770
|
+
// Build frame paths based on frame count using numeric code
|
|
771
|
+
const frames = [];
|
|
772
|
+
for (let i = 0; i < frameCount; i++) {
|
|
773
|
+
frames.push(`${getProxyPath()}assets/${itemType}/${itemId}/${numericCode}/${i}.png`);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// Update loading message
|
|
777
|
+
const loadingOverlay = s('#gif-loading-overlay');
|
|
778
|
+
if (loadingOverlay) {
|
|
779
|
+
loadingOverlay.querySelector('span').textContent = `Loading frames... (0/${frames.length})`;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// Load all frames to find maximum dimensions
|
|
783
|
+
const loadedImages = [];
|
|
784
|
+
let maxWidth = 0;
|
|
785
|
+
let maxHeight = 0;
|
|
786
|
+
|
|
787
|
+
for (let i = 0; i < frames.length; i++) {
|
|
788
|
+
const img = await this.loadImage(frames[i]);
|
|
789
|
+
loadedImages.push(img);
|
|
790
|
+
maxWidth = Math.max(maxWidth, img.naturalWidth);
|
|
791
|
+
maxHeight = Math.max(maxHeight, img.naturalHeight);
|
|
792
|
+
|
|
793
|
+
// Update progress
|
|
794
|
+
if (loadingOverlay && (i === 0 || i % 5 === 0)) {
|
|
795
|
+
loadingOverlay.querySelector('span').textContent = `Loading frames... (${i + 1}/${frames.length})`;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// Update loading message for GIF generation
|
|
800
|
+
if (loadingOverlay) {
|
|
801
|
+
loadingOverlay.querySelector('span').textContent = 'Generating GIF...';
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
logger.info(`GIF dimensions calculated: ${maxWidth}x${maxHeight} from ${frames.length} frames`);
|
|
805
|
+
|
|
806
|
+
// Use binary transparency with placeholder color (magenta)
|
|
807
|
+
const placeholder = this.Data.gifTransparencyPlaceholder;
|
|
808
|
+
const transparentColorHex = (placeholder.r << 16) | (placeholder.g << 8) | placeholder.b;
|
|
809
|
+
|
|
810
|
+
// Create new GIF instance with binary transparency
|
|
811
|
+
const gif = new GIF({
|
|
812
|
+
workers: 2,
|
|
813
|
+
workerScript: this.Data.gifWorkerBlob,
|
|
814
|
+
quality: 10,
|
|
815
|
+
width: maxWidth,
|
|
816
|
+
height: maxHeight,
|
|
817
|
+
transparent: transparentColorHex, // Use magenta as transparent color
|
|
818
|
+
repeat: 0,
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
// Process each frame with binary transparency threshold
|
|
822
|
+
for (let i = 0; i < loadedImages.length; i++) {
|
|
823
|
+
const img = loadedImages[i];
|
|
824
|
+
|
|
825
|
+
// Create canvas for this frame
|
|
826
|
+
const canvas = document.createElement('canvas');
|
|
827
|
+
canvas.width = maxWidth;
|
|
828
|
+
canvas.height = maxHeight;
|
|
829
|
+
const ctx = canvas.getContext('2d', { alpha: true, willReadFrequently: true });
|
|
830
|
+
|
|
831
|
+
// Start with transparent canvas (don't fill with magenta yet)
|
|
832
|
+
ctx.clearRect(0, 0, maxWidth, maxHeight);
|
|
833
|
+
|
|
834
|
+
// Center the image
|
|
835
|
+
const x = Math.floor((maxWidth - img.naturalWidth) / 2);
|
|
836
|
+
const y = Math.floor((maxHeight - img.naturalHeight) / 2);
|
|
837
|
+
|
|
838
|
+
// Disable smoothing to keep pixel-art sharp
|
|
839
|
+
ctx.imageSmoothingEnabled = false;
|
|
840
|
+
|
|
841
|
+
// Draw the original image centered on transparent canvas
|
|
842
|
+
ctx.drawImage(img, x, y);
|
|
843
|
+
|
|
844
|
+
// Apply binary transparency threshold: replace ONLY transparent pixels with placeholder color
|
|
845
|
+
const threshold = this.Data.transparencyThreshold;
|
|
846
|
+
try {
|
|
847
|
+
const imageData = ctx.getImageData(0, 0, maxWidth, maxHeight);
|
|
848
|
+
const data = imageData.data;
|
|
849
|
+
|
|
850
|
+
for (let p = 0; p < data.length; p += 4) {
|
|
851
|
+
const alpha = data[p + 3];
|
|
852
|
+
// If alpha is below threshold, replace with opaque placeholder color (for GIF transparency)
|
|
853
|
+
if (alpha < threshold) {
|
|
854
|
+
data[p] = placeholder.r; // R
|
|
855
|
+
data[p + 1] = placeholder.g; // G
|
|
856
|
+
data[p + 2] = placeholder.b; // B
|
|
857
|
+
data[p + 3] = 255; // A (fully opaque)
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
ctx.putImageData(imageData, 0, 0);
|
|
862
|
+
} catch (err) {
|
|
863
|
+
logger.warn(
|
|
864
|
+
'Could not access image data for transparency threshold (CORS issue). Transparency may not work correctly.',
|
|
865
|
+
err,
|
|
866
|
+
);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// Add frame to GIF with dispose mode to clear between frames
|
|
870
|
+
gif.addFrame(canvas, {
|
|
871
|
+
delay: frameDuration,
|
|
872
|
+
copy: true,
|
|
873
|
+
dispose: 2, // Restore to background color before drawing next frame (prevents overlap)
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// Handle GIF finished event
|
|
878
|
+
gif.on('finished', (blob) => {
|
|
879
|
+
this.displayGif(blob, maxWidth, maxHeight, frameDuration, frameCount);
|
|
880
|
+
this.Data.gif = blob;
|
|
881
|
+
this.Data.isGenerating = false;
|
|
882
|
+
this.showLoading(false);
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
// Render the GIF
|
|
886
|
+
gif.render();
|
|
887
|
+
} catch (error) {
|
|
888
|
+
logger.error('Error generating GIF:', error);
|
|
889
|
+
NotificationManager.Push({
|
|
890
|
+
html: `Failed to generate GIF: ${error.message}`,
|
|
891
|
+
status: 'error',
|
|
892
|
+
});
|
|
893
|
+
this.Data.isGenerating = false;
|
|
894
|
+
this.showLoading(false);
|
|
895
|
+
}
|
|
896
|
+
},
|
|
897
|
+
|
|
898
|
+
loadImage: function (src) {
|
|
899
|
+
return new Promise((resolve, reject) => {
|
|
900
|
+
const img = new Image();
|
|
901
|
+
img.crossOrigin = 'anonymous';
|
|
902
|
+
img.onload = () => resolve(img);
|
|
903
|
+
img.onerror = reject;
|
|
904
|
+
img.src = src;
|
|
905
|
+
});
|
|
906
|
+
},
|
|
907
|
+
|
|
908
|
+
displayGif: function (blob, originalWidth, originalHeight, frameDuration, frameCount) {
|
|
909
|
+
const container = s('#gif-canvas-container');
|
|
910
|
+
if (!container) return;
|
|
911
|
+
|
|
912
|
+
const url = URL.createObjectURL(blob);
|
|
913
|
+
|
|
914
|
+
// Create img element for the animated GIF
|
|
915
|
+
const gifImg = document.createElement('img');
|
|
916
|
+
gifImg.src = url;
|
|
917
|
+
|
|
918
|
+
gifImg.onload = () => {
|
|
919
|
+
// Use provided dimensions or get from image
|
|
920
|
+
const naturalWidth = originalWidth || gifImg.naturalWidth;
|
|
921
|
+
const naturalHeight = originalHeight || gifImg.naturalHeight;
|
|
922
|
+
|
|
923
|
+
// Calculate intelligent scaling based on container and image size
|
|
924
|
+
const containerEl = s('.gif-display-area');
|
|
925
|
+
const containerWidth = containerEl ? containerEl.clientWidth - 60 : 400; // subtract padding
|
|
926
|
+
const containerHeight = containerEl ? containerEl.clientHeight - 60 : 400;
|
|
927
|
+
|
|
928
|
+
// Calculate scale to fit container while maintaining aspect ratio
|
|
929
|
+
const scaleToFitWidth = containerWidth / naturalWidth;
|
|
930
|
+
const scaleToFitHeight = containerHeight / naturalHeight;
|
|
931
|
+
const scaleToFit = Math.min(scaleToFitWidth, scaleToFitHeight);
|
|
932
|
+
|
|
933
|
+
// For pixel art, use integer scaling for better visuals
|
|
934
|
+
// Minimum 2x for small sprites, but respect container size
|
|
935
|
+
let scale = Math.max(1, Math.floor(scaleToFit));
|
|
936
|
+
|
|
937
|
+
// For very small sprites (< 100px), try to scale up more
|
|
938
|
+
if (Math.max(naturalWidth, naturalHeight) < 100) {
|
|
939
|
+
scale = Math.min(4, Math.floor(scaleToFit));
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// Make sure scaled image fits in container
|
|
943
|
+
const displayWidth = naturalWidth * scale;
|
|
944
|
+
const displayHeight = naturalHeight * scale;
|
|
945
|
+
|
|
946
|
+
if (displayWidth > containerWidth || displayHeight > containerHeight) {
|
|
947
|
+
scale = Math.max(1, scale - 1);
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
gifImg.style.width = `${naturalWidth * scale}px !important`;
|
|
951
|
+
gifImg.style.height = `${naturalHeight * scale}px !important`;
|
|
952
|
+
gifImg.style.maxWidth = '100%';
|
|
953
|
+
gifImg.style.maxHeight = '540px';
|
|
954
|
+
|
|
955
|
+
// Force pixel-perfect rendering (no antialiasing/blur)
|
|
956
|
+
// gifImg.style.imageRendering = 'pixelated';
|
|
957
|
+
// gifImg.style.imageRendering = '-moz-crisp-edges';
|
|
958
|
+
// gifImg.style.imageRendering = 'crisp-edges';
|
|
959
|
+
// gifImg.style.msInterpolationMode = 'nearest-neighbor';
|
|
960
|
+
|
|
961
|
+
// Prevent any browser scaling optimizations
|
|
962
|
+
// gifImg.style.transform = 'translateZ(0)'; // Force GPU rendering
|
|
963
|
+
// gifImg.style.backfaceVisibility = 'hidden'; // Prevent subpixel rendering
|
|
964
|
+
|
|
965
|
+
// Clear container and add the GIF
|
|
966
|
+
container.innerHTML = '';
|
|
967
|
+
container.appendChild(gifImg);
|
|
968
|
+
|
|
969
|
+
// Re-add loading overlay
|
|
970
|
+
const overlay = document.createElement('div');
|
|
971
|
+
overlay.id = 'gif-loading-overlay';
|
|
972
|
+
overlay.className = 'loading-overlay';
|
|
973
|
+
overlay.style.display = 'none';
|
|
974
|
+
overlay.innerHTML = html`
|
|
975
|
+
<div>
|
|
976
|
+
<i class="fa-solid fa-spinner fa-spin"></i>
|
|
977
|
+
<span style="margin-left: 10px;">Generating GIF...</span>
|
|
978
|
+
</div>
|
|
979
|
+
`;
|
|
980
|
+
container.appendChild(overlay);
|
|
981
|
+
|
|
982
|
+
// Add info badge with dimensions and scale
|
|
983
|
+
const infoBadge = document.createElement('div');
|
|
984
|
+
infoBadge.className = 'gif-info-badge';
|
|
985
|
+
const displayW = Math.round(naturalWidth * scale);
|
|
986
|
+
const displayH = Math.round(naturalHeight * scale);
|
|
987
|
+
infoBadge.innerHTML = html`
|
|
988
|
+
<span class="info-label">Dimensions:</span> ${naturalWidth}x${naturalHeight}px<br />
|
|
989
|
+
<span class="info-label">Display:</span> ${displayW}x${displayH}px<br />
|
|
990
|
+
${scale > 1 ? `<span class="info-label">Scale:</span> ${scale}x<br />` : ''}
|
|
991
|
+
<span class="info-label">Frames:</span> ${frameCount}<br />
|
|
992
|
+
<span class="info-label">Frame Duration:</span> ${frameDuration}ms<br />
|
|
993
|
+
<span class="info-label">Total Duration:</span> ${(frameDuration * frameCount) / 1000}s
|
|
994
|
+
`;
|
|
995
|
+
s(`.gif-display-area`).appendChild(infoBadge);
|
|
996
|
+
|
|
997
|
+
logger.info(`Displaying GIF: ${naturalWidth}x${naturalHeight} at ${scale}x scale (${displayW}x${displayH})`);
|
|
998
|
+
};
|
|
999
|
+
|
|
1000
|
+
gifImg.onerror = () => {
|
|
1001
|
+
logger.error('Failed to load GIF image');
|
|
1002
|
+
NotificationManager.Push({
|
|
1003
|
+
html: 'Failed to display GIF',
|
|
1004
|
+
status: 'error',
|
|
1005
|
+
});
|
|
1006
|
+
};
|
|
1007
|
+
},
|
|
1008
|
+
|
|
1009
|
+
showLoading: function (show) {
|
|
1010
|
+
const overlay = s('#gif-loading-overlay');
|
|
1011
|
+
if (overlay) {
|
|
1012
|
+
overlay.style.display = show ? 'flex' : 'none';
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
const downloadBtn = s('#download-gif-btn');
|
|
1016
|
+
if (downloadBtn) {
|
|
1017
|
+
downloadBtn.disabled = show;
|
|
1018
|
+
}
|
|
1019
|
+
},
|
|
1020
|
+
|
|
1021
|
+
downloadGif: function () {
|
|
1022
|
+
if (!this.Data.gif) {
|
|
1023
|
+
NotificationManager.Push({
|
|
1024
|
+
html: 'No GIF available to download',
|
|
1025
|
+
status: 'warning',
|
|
1026
|
+
});
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
const { objectLayer, currentDirection, currentMode } = this.Data;
|
|
1031
|
+
const numericCode = this.getDirectionCode(currentDirection, currentMode);
|
|
1032
|
+
const filename = `${objectLayer.data.item.id}_${currentDirection}_${currentMode}_${numericCode}.gif`;
|
|
1033
|
+
|
|
1034
|
+
const url = URL.createObjectURL(this.Data.gif);
|
|
1035
|
+
const a = document.createElement('a');
|
|
1036
|
+
a.href = url;
|
|
1037
|
+
a.download = filename;
|
|
1038
|
+
document.body.appendChild(a);
|
|
1039
|
+
a.click();
|
|
1040
|
+
document.body.removeChild(a);
|
|
1041
|
+
URL.revokeObjectURL(url);
|
|
1042
|
+
|
|
1043
|
+
NotificationManager.Push({
|
|
1044
|
+
html: `GIF downloaded: ${filename}`,
|
|
1045
|
+
status: 'success',
|
|
1046
|
+
});
|
|
1047
|
+
},
|
|
1048
|
+
|
|
1049
|
+
Reload: async function () {
|
|
1050
|
+
const queryParams = new URLSearchParams(window.location.search);
|
|
1051
|
+
const cid = queryParams.get('cid');
|
|
1052
|
+
|
|
1053
|
+
if (cid) {
|
|
1054
|
+
await this.loadObjectLayer(cid);
|
|
1055
|
+
} else {
|
|
1056
|
+
this.renderEmpty();
|
|
1057
|
+
}
|
|
1058
|
+
},
|
|
1059
|
+
};
|
|
1060
|
+
|
|
1061
|
+
export { ObjectLayerEngineViewer };
|