cyberia 2.89.2 → 2.89.45

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.
Files changed (64) hide show
  1. package/.env.development +2 -0
  2. package/.env.production +2 -0
  3. package/.env.test +2 -0
  4. package/.github/workflows/engine-cyberia.cd.yml +4 -0
  5. package/.github/workflows/release.cd.yml +2 -0
  6. package/bin/cyberia.js +10 -7
  7. package/bin/deploy.js +22 -15
  8. package/bin/index.js +10 -7
  9. package/cli.md +105 -54
  10. package/deployment.yaml +34 -6
  11. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  12. package/manifests/deployment/dd-test-development/deployment.yaml +18 -6
  13. package/manifests/deployment/dd-test-development/proxy.yaml +2 -0
  14. package/manifests/deployment/kafka/deployment.yaml +0 -2
  15. package/manifests/deployment/spark/spark-pi-py.yaml +0 -1
  16. package/manifests/deployment/tensorflow/tf-gpu-test.yaml +0 -2
  17. package/manifests/envoy-service-nodeport.yaml +0 -1
  18. package/manifests/kubeadm-calico-config.yaml +10 -115
  19. package/manifests/letsencrypt-prod.yaml +0 -1
  20. package/manifests/mariadb/statefulset.yaml +1 -1
  21. package/manifests/mongodb/statefulset.yaml +11 -11
  22. package/manifests/mongodb-4.4/service-deployment.yaml +1 -3
  23. package/manifests/mysql/pv-pvc.yaml +1 -1
  24. package/manifests/mysql/statefulset.yaml +1 -1
  25. package/manifests/pv-pvc-dd.yaml +34 -0
  26. package/manifests/valkey/service.yaml +0 -1
  27. package/manifests/valkey/statefulset.yaml +2 -3
  28. package/package.json +1 -1
  29. package/proxy.yaml +6 -0
  30. package/scripts/device-scan.sh +43 -21
  31. package/scripts/gen-fqdns.sh +14 -0
  32. package/scripts/ip-info.sh +118 -0
  33. package/scripts/rpmfusion-ffmpeg-setup.sh +1 -0
  34. package/src/api/object-layer/object-layer.controller.js +19 -0
  35. package/src/api/object-layer/object-layer.router.js +4 -0
  36. package/src/api/object-layer/object-layer.service.js +111 -0
  37. package/src/api/user/user.model.js +10 -1
  38. package/src/cli/cluster.js +88 -75
  39. package/src/cli/deploy.js +165 -85
  40. package/src/cli/index.js +44 -3
  41. package/src/cli/monitor.js +12 -6
  42. package/src/cli/repository.js +13 -1
  43. package/src/cli/run.js +127 -60
  44. package/src/client/components/core/Logger.js +1 -1
  45. package/src/client/components/core/Modal.js +5 -0
  46. package/src/client/components/core/ObjectLayerEngineModal.js +336 -72
  47. package/src/client/components/core/ObjectLayerEngineViewer.js +239 -420
  48. package/src/client/components/core/Router.js +10 -1
  49. package/src/client/components/cyberia-portal/LogInCyberiaPortal.js +1 -1
  50. package/src/client/components/cyberia-portal/LogOutCyberiaPortal.js +1 -1
  51. package/src/client/components/cyberia-portal/MenuCyberiaPortal.js +1 -1
  52. package/src/client/components/cyberia-portal/ObjectLayerCyberiaPortal.js +44 -4
  53. package/src/client/services/default/default.management.js +25 -5
  54. package/src/client/services/object-layer/object-layer.management.js +8 -8
  55. package/src/client/services/object-layer/object-layer.service.js +34 -0
  56. package/src/index.js +1 -1
  57. package/src/server/client-build.js +5 -4
  58. package/src/server/conf.js +1 -1
  59. package/src/server/start.js +3 -1
  60. package/manifests/kubelet-config.yaml +0 -65
  61. package/manifests/mongodb/backup-access.yaml +0 -16
  62. package/manifests/mongodb/backup-cronjob.yaml +0 -42
  63. package/manifests/mongodb/backup-pv-pvc.yaml +0 -22
  64. package/manifests/mongodb/configmap.yaml +0 -26
@@ -1,11 +1,15 @@
1
1
  import { loggerFactory } from './Logger.js';
2
- import { getProxyPath, listenQueryPathInstance } from './Router.js';
2
+ import { getProxyPath, listenQueryPathInstance, setPath, setQueryParams } from './Router.js';
3
3
  import { ObjectLayerService } from '../../services/object-layer/object-layer.service.js';
4
4
  import { NotificationManager } from './NotificationManager.js';
5
5
  import { htmls, s } from './VanillaJs.js';
6
- import { BtnIcon } from './BtnIcon.js';
6
+
7
7
  import { darkTheme, ThemeEvents } from './Css.js';
8
- import { ObjectLayerCyberiaPortal } from '../cyberia-portal/ObjectLayerCyberiaPortal.js';
8
+ import { ObjectLayerManagement } from '../../services/object-layer/object-layer.management.js';
9
+ import { ObjectLayerEngineModal } from './ObjectLayerEngineModal.js';
10
+ import { Modal } from './Modal.js';
11
+ import { DefaultManagement } from '../../services/default/default.management.js';
12
+ import { AgGrid } from './AgGrid.js';
9
13
 
10
14
  const logger = loggerFactory(import.meta);
11
15
 
@@ -15,12 +19,8 @@ const ObjectLayerEngineViewer = {
15
19
  frameCounts: null,
16
20
  currentDirection: 'down',
17
21
  currentMode: 'idle',
18
- gif: null,
19
- gifWorkerBlob: null,
22
+ webp: null,
20
23
  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
24
  },
25
25
 
26
26
  // Map user-friendly direction/mode to numeric direction codes
@@ -39,24 +39,13 @@ const ObjectLayerEngineViewer = {
39
39
  return directionCodeMap[key] || null;
40
40
  },
41
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
42
  Render: async function ({ Elements }) {
58
43
  const id = 'object-layer-engine-viewer';
59
44
 
45
+ Modal.Data[`modal-${id}`].onReloadModalListener[id] = async () => {
46
+ ObjectLayerEngineViewer.Reload({ Elements });
47
+ };
48
+
60
49
  // Listen for cid query parameter
61
50
  listenQueryPathInstance(
62
51
  {
@@ -64,34 +53,15 @@ const ObjectLayerEngineViewer = {
64
53
  routeId: 'object-layer-engine-viewer',
65
54
  event: async (cid) => {
66
55
  if (cid) {
67
- await this.loadObjectLayer(cid);
56
+ await this.loadObjectLayer(cid, Elements);
68
57
  } else {
69
- this.renderEmpty();
58
+ this.renderEmpty({ Elements });
70
59
  }
71
60
  },
72
61
  },
73
62
  'cid',
74
63
  );
75
64
 
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
65
  return html`
96
66
  <div class="fl">
97
67
  <div class="in ${id}" id="${id}">
@@ -103,12 +73,24 @@ const ObjectLayerEngineViewer = {
103
73
  `;
104
74
  },
105
75
 
106
- renderEmpty: async function () {
76
+ renderEmpty: async function ({ Elements }) {
107
77
  const id = 'object-layer-engine-viewer';
108
- htmls(`#${id}`, await ObjectLayerCyberiaPortal.Render());
78
+ const idModal = 'modal-object-layer-engine-viewer';
79
+ const serviceId = 'object-layer-engine-management';
80
+ const gridId = `${serviceId}-grid-${idModal}`;
81
+ if (s(`.${serviceId}-grid-${idModal}`) && AgGrid.grids[gridId])
82
+ await DefaultManagement.loadTable(idModal, { reload: true });
83
+ else
84
+ htmls(
85
+ `#${id}`,
86
+ await ObjectLayerManagement.RenderTable({
87
+ Elements,
88
+ idModal,
89
+ }),
90
+ );
109
91
  },
110
92
 
111
- loadObjectLayer: async function (objectLayerId) {
93
+ loadObjectLayer: async function (objectLayerId, Elements) {
112
94
  const id = 'object-layer-engine-viewer';
113
95
 
114
96
  try {
@@ -129,18 +111,18 @@ const ObjectLayerEngineViewer = {
129
111
  }
130
112
 
131
113
  this.Data.frameCounts = frameData.frameCounts;
132
-
133
- // Auto-select first available direction/mode combination
134
- this.selectFirstAvailableDirectionMode();
114
+ // Priority order for directions
115
+ const directions = ['down', 'up', 'left', 'right'];
116
+ // Priority order for modes
117
+ const modes = ['idle', 'walking'];
118
+ this.Data.currentDirection = 'down';
119
+ this.Data.currentMode = 'idle';
135
120
 
136
121
  // Render the viewer UI
137
- await this.renderViewer();
138
-
139
- // Initialize gif.js worker
140
- await this.initGifJs();
122
+ await this.renderViewer({ Elements });
141
123
 
142
- // Generate initial GIF
143
- await this.generateGif();
124
+ // Generate WebP
125
+ await this.generateWebp();
144
126
  } catch (error) {
145
127
  logger.error('Error loading object layer:', error);
146
128
  NotificationManager.Push({
@@ -162,7 +144,7 @@ const ObjectLayerEngineViewer = {
162
144
  }
163
145
  },
164
146
 
165
- renderViewer: async function () {
147
+ renderViewer: async function ({ Elements }) {
166
148
  const id = 'object-layer-engine-viewer';
167
149
  const { objectLayer, frameCounts } = this.Data;
168
150
 
@@ -172,8 +154,6 @@ const ObjectLayerEngineViewer = {
172
154
  const itemId = objectLayer.data.item.id;
173
155
  const itemDescription = objectLayer.data.item.description || '';
174
156
  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
157
 
178
158
  // Get stats data
179
159
  const stats = objectLayer.data.stats || {};
@@ -213,7 +193,7 @@ const ObjectLayerEngineViewer = {
213
193
  color: ${darkTheme ? '#fff' : '#333'};
214
194
  }
215
195
 
216
- .gif-display-area {
196
+ .webp-display-area {
217
197
  background: ${darkTheme ? '#2a2a2a' : '#f5f5f5'};
218
198
  border: 2px solid ${darkTheme ? '#444' : '#ddd'};
219
199
  border-radius: 12px;
@@ -229,7 +209,7 @@ const ObjectLayerEngineViewer = {
229
209
  overflow: auto;
230
210
  }
231
211
 
232
- .gif-canvas-container {
212
+ .webp-canvas-container {
233
213
  position: relative;
234
214
  display: flex;
235
215
  justify-content: center;
@@ -238,8 +218,8 @@ const ObjectLayerEngineViewer = {
238
218
  height: 100%;
239
219
  }
240
220
 
241
- .gif-canvas-container canvas,
242
- .gif-canvas-container img {
221
+ .webp-canvas-container canvas,
222
+ .webp-canvas-container img {
243
223
  image-rendering: pixelated;
244
224
  image-rendering: -moz-crisp-edges;
245
225
  image-rendering: crisp-edges;
@@ -255,13 +235,13 @@ const ObjectLayerEngineViewer = {
255
235
  display: block;
256
236
  }
257
237
 
258
- .gif-canvas-container canvas {
238
+ .webp-canvas-container canvas {
259
239
  background: repeating-conic-gradient(#80808020 0% 25%, #fff0 0% 50%) 50% / 20px 20px;
260
240
  min-width: 128px;
261
241
  min-height: 128px;
262
242
  }
263
243
 
264
- .gif-info-badge {
244
+ .webp-info-badge {
265
245
  position: absolute;
266
246
  bottom: 10px;
267
247
  right: 10px;
@@ -274,7 +254,7 @@ const ObjectLayerEngineViewer = {
274
254
  backdrop-filter: blur(4px);
275
255
  }
276
256
 
277
- .gif-info-badge .info-label {
257
+ .webp-info-badge .info-label {
278
258
  opacity: 0.7;
279
259
  margin-right: 4px;
280
260
  }
@@ -356,7 +336,7 @@ const ObjectLayerEngineViewer = {
356
336
  font-size: 16px;
357
337
  }
358
338
 
359
- .download-btn {
339
+ .default-viewer-btn {
360
340
  width: 100%;
361
341
  padding: 15px;
362
342
  background: ${darkTheme ? '#4caf50' : '#4CAF50'};
@@ -373,7 +353,7 @@ const ObjectLayerEngineViewer = {
373
353
  gap: 10px;
374
354
  }
375
355
 
376
- .download-btn:hover {
356
+ .default-viewer-btn:hover {
377
357
  background: ${darkTheme ? '#45a049' : '#45a049'};
378
358
  transform: translateY(-2px);
379
359
  box-shadow: 0 4px 12px rgba(76, 175, 80, 0.3);
@@ -392,35 +372,43 @@ const ObjectLayerEngineViewer = {
392
372
  margin-left: 4px;
393
373
  }
394
374
 
395
- .download-btn:disabled {
375
+ .default-viewer-btn:disabled {
396
376
  background: ${darkTheme ? '#555' : '#ccc'};
397
377
  cursor: not-allowed;
398
378
  transform: none;
399
379
  }
400
380
 
381
+ .edit-btn {
382
+ background: ${darkTheme ? '#4a9eff' : '#2196F3'};
383
+ }
384
+
385
+ .edit-btn:hover {
386
+ background: ${darkTheme ? '#3a8eff' : '#1186f2'};
387
+ }
388
+
401
389
  @media (max-width: 768px) {
402
- .gif-display-area {
390
+ .webp-display-area {
403
391
  max-height: 500px;
404
392
  min-height: 300px;
405
393
  padding: 20px;
406
394
  }
407
395
 
408
- .gif-canvas-container canvas,
409
- .gif-canvas-container img {
396
+ .webp-canvas-container canvas,
397
+ .webp-canvas-container img {
410
398
  max-width: 100%;
411
399
  max-height: 440px;
412
400
  }
413
401
  }
414
402
 
415
403
  @media (max-width: 600px) {
416
- .gif-display-area {
404
+ .webp-display-area {
417
405
  max-height: 400px;
418
406
  min-height: 250px;
419
407
  padding: 15px;
420
408
  }
421
409
 
422
- .gif-canvas-container canvas,
423
- .gif-canvas-container img {
410
+ .webp-canvas-container canvas,
411
+ .webp-canvas-container img {
424
412
  max-height: 340px;
425
413
  }
426
414
 
@@ -440,7 +428,8 @@ const ObjectLayerEngineViewer = {
440
428
  .item-data-value-label {
441
429
  font-size: 20px;
442
430
  font-weight: 700;
443
- color: ${darkTheme ? '#4a9eff' : '#2196F3'};
431
+ color: ${darkTheme ? '#aaa' : '#666'};
432
+ text-align: center;
444
433
  }
445
434
  .item-stat-entry {
446
435
  display: flex;
@@ -457,6 +446,12 @@ const ObjectLayerEngineViewer = {
457
446
  color: ${darkTheme ? '#666' : '#999'};
458
447
  padding: 20px;
459
448
  }
449
+
450
+ @media (max-width: 850px) {
451
+ .object-layer-viewer-container {
452
+ padding: 5px;
453
+ }
454
+ }
460
455
  </style>`,
461
456
  );
462
457
  };
@@ -493,16 +488,42 @@ const ObjectLayerEngineViewer = {
493
488
  </div>
494
489
  </div>
495
490
 
496
- <div class="gif-display-area">
497
- <div class="gif-canvas-container" id="gif-canvas-container">
491
+ <!-- Stats Data Section -->
492
+ <div class="control-group" style="margin-bottom: 20px;">
493
+ <h4><i class="fa-solid fa-chart-bar"></i> Stats Data</h4>
494
+ <div
495
+ style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 15px; padding: 10px 0;"
496
+ >
497
+ ${Object.keys(stats).length > 0
498
+ ? Object.entries(stats)
499
+ .map(([statKey, statValue]) => {
500
+ const statInfo = ObjectLayerEngineModal.statDescriptions[statKey];
501
+ if (!statInfo) return '';
502
+ return html`
503
+ <div class="item-stat-entry">
504
+ <div style="display: flex; align-items: center; gap: 8px;">
505
+ <i class="${statInfo.icon}" id="stat-icon-${statKey}-${id}"></i>
506
+ <span class="item-data-key-label">${statInfo.title}</span>
507
+ </div>
508
+ <span class="item-data-value-label">${statValue}</span>
509
+ </div>
510
+ `;
511
+ })
512
+ .join('')
513
+ : html`<div class="no-data-container">No stats data available</div>`}
514
+ </div>
515
+ </div>
516
+
517
+ <div class="webp-display-area">
518
+ <div class="webp-canvas-container" id="webp-canvas-container">
498
519
  <div style="text-align: center; color: ${darkTheme ? '#aaa' : '#666'};">
499
520
  <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>
521
+ <p style="margin: 0; font-size: 14px;">WebP preview will appear here</p>
501
522
  </div>
502
- <div id="gif-loading-overlay" class="loading-overlay" style="display: none;">
523
+ <div id="webp-loading-overlay" class="loading-overlay" style="display: none;">
503
524
  <div>
504
525
  <i class="fa-solid fa-spinner fa-spin"></i>
505
- <span style="margin-left: 10px;">Generating GIF...</span>
526
+ <span style="margin-left: 10px;">Generating WebP...</span>
506
527
  </div>
507
528
  </div>
508
529
  </div>
@@ -587,39 +608,30 @@ const ObjectLayerEngineViewer = {
587
608
  </div>
588
609
  </div>
589
610
  </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>
611
+
612
+ <div style="display: flex; gap: 10px; margin-top: 20px;">
613
+ <button class="default-viewer-btn" id="return-to-list-btn">
614
+ <i class="fa-solid fa-arrow-left"></i>
615
+ <span>Return to List</span>
616
+ </button>
617
+ <button class="default-viewer-btn" id="download-webp-btn">
618
+ <i class="fa-solid fa-download"></i>
619
+ <span>Download WebP</span>
620
+ </button>
621
+ <button class="default-viewer-btn edit-btn" id="edit-object-layer-btn">
622
+ <i class="fa-solid fa-edit"></i>
623
+ <span>Edit</span>
624
+ </button>
609
625
  </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
626
  </div>
615
627
  `,
616
628
  );
617
629
  ThemeEvents[id]();
618
630
  // Attach event listeners
619
- this.attachEventListeners();
631
+ this.attachEventListeners({ Elements });
620
632
  },
621
633
 
622
- attachEventListeners: function () {
634
+ attachEventListeners: function ({ Elements }) {
623
635
  // Direction buttons
624
636
  const directionButtons = document.querySelectorAll('[data-direction]');
625
637
  directionButtons.forEach((btn) => {
@@ -628,9 +640,9 @@ const ObjectLayerEngineViewer = {
628
640
  const direction = e.currentTarget.getAttribute('data-direction');
629
641
  if (direction !== this.Data.currentDirection) {
630
642
  this.Data.currentDirection = direction;
631
- await this.renderViewer();
632
- await this.attachEventListeners();
633
- await this.generateGif();
643
+ await this.renderViewer({ Elements });
644
+ await this.attachEventListeners({ Elements });
645
+ await this.generateWebp();
634
646
  }
635
647
  });
636
648
  });
@@ -643,97 +655,41 @@ const ObjectLayerEngineViewer = {
643
655
  const mode = e.currentTarget.getAttribute('data-mode');
644
656
  if (mode !== this.Data.currentMode) {
645
657
  this.Data.currentMode = mode;
646
- await this.renderViewer();
647
- await this.attachEventListeners();
648
- await this.generateGif();
658
+ await this.renderViewer({ Elements });
659
+ await this.attachEventListeners({ Elements });
660
+ await this.generateWebp();
649
661
  }
650
662
  });
651
663
  });
652
664
 
653
665
  // Download button
654
- const downloadBtn = s('#download-gif-btn');
666
+ const downloadBtn = s('#download-webp-btn');
655
667
  if (downloadBtn) {
656
668
  downloadBtn.addEventListener('click', () => {
657
- this.downloadGif();
669
+ this.downloadWebp();
658
670
  });
659
671
  }
660
672
 
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
- }
673
+ // Return to list button
674
+ const listBtn = s('#return-to-list-btn');
675
+ if (listBtn) {
676
+ listBtn.addEventListener('click', () => {
677
+ setPath(`${getProxyPath()}object-layer-engine-viewer`);
678
+ setQueryParams({ cid: null });
679
+ ObjectLayerEngineViewer.renderEmpty({ Elements });
680
+ });
692
681
  }
693
682
 
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;
683
+ // Edit button
684
+ const editBtn = s('#edit-object-layer-btn');
685
+ if (editBtn) {
686
+ editBtn.addEventListener('click', () => {
687
+ this.toEngine();
688
+ });
717
689
  }
718
690
  },
719
691
 
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 () {
692
+ generateWebp: async function () {
737
693
  if (this.Data.isGenerating) return;
738
694
 
739
695
  const { objectLayer, frameCounts, currentDirection, currentMode } = this.Data;
@@ -766,128 +722,74 @@ const ObjectLayerEngineViewer = {
766
722
  this.Data.isGenerating = true;
767
723
  this.showLoading(true);
768
724
 
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...';
725
+ // Update loading overlay text
726
+ const loadingOverlay = s('#webp-loading-overlay');
727
+ if (loadingOverlay) {
728
+ const loadingText = loadingOverlay.querySelector('span');
729
+ if (loadingText) {
730
+ loadingText.textContent = `Loading WebP animation for ${currentDirection} ${currentMode}...`;
802
731
  }
732
+ }
803
733
 
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,
734
+ try {
735
+ // Call the WebP generation API endpoint
736
+ const { status, data } = await ObjectLayerService.generateWebp({
737
+ itemType,
738
+ itemId,
739
+ directionCode: numericCode,
819
740
  });
820
741
 
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
- }
742
+ if (status === 'success' && data) {
743
+ // Store the blob URL
744
+ this.Data.webp = data;
745
+
746
+ // Display the WebP in the viewer
747
+ const container = s('#webp-canvas-container');
748
+ if (container) {
749
+ // Clear container
750
+ container.innerHTML = '';
751
+
752
+ // Create and append image
753
+ const img = document.createElement('img');
754
+ img.src = data;
755
+ img.alt = 'WebP Animation';
756
+ container.appendChild(img);
757
+
758
+ // Create and append info badge
759
+ const infoBadge = document.createElement('div');
760
+ infoBadge.className = 'webp-info-badge';
761
+ infoBadge.innerHTML = html`
762
+ <span class="info-label" style="margin-left: 8px;">Frames:</span>
763
+ <span>${frameCount}</span><br />
764
+ <span class="info-label" style="margin-left: 8px;">Duration:</span>
765
+ <span>${frameDuration}ms</span><br />
766
+ <span class="info-label" style="margin-left: 8px;">Direction:</span>
767
+ <span>${currentDirection}</span><br />
768
+ <span class="info-label" style="margin-left: 8px;">Mode:</span>
769
+ <span>${currentMode}</span><br />
770
+ <span class="info-label" style="margin-left: 8px;">Code:</span>
771
+ <span>${numericCode}</span>
772
+ `;
773
+ const displayArea = s('.webp-display-area');
774
+ if (displayArea) {
775
+ displayArea.appendChild(infoBadge);
859
776
  }
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
777
  }
868
778
 
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
- });
779
+ // NotificationManager.Push({
780
+ // html: `WebP generated successfully (${frameCount} frames, ${frameDuration}ms duration)`,
781
+ // status: 'success',
782
+ // });
783
+ } else {
784
+ throw new Error('Failed to generate WebP');
875
785
  }
876
786
 
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();
787
+ this.Data.isGenerating = false;
788
+ this.showLoading(false);
887
789
  } catch (error) {
888
- logger.error('Error generating GIF:', error);
790
+ logger.error('Error generating WebP:', error);
889
791
  NotificationManager.Push({
890
- html: `Failed to generate GIF: ${error.message}`,
792
+ html: `Failed to generate WebP: ${error.message}`,
891
793
  status: 'error',
892
794
  });
893
795
  this.Data.isGenerating = false;
@@ -895,133 +797,35 @@ const ObjectLayerEngineViewer = {
895
797
  }
896
798
  },
897
799
 
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
800
  showLoading: function (show) {
1010
- const overlay = s('#gif-loading-overlay');
801
+ const overlay = s('#webp-loading-overlay');
1011
802
  if (overlay) {
1012
803
  overlay.style.display = show ? 'flex' : 'none';
804
+ if (!show) {
805
+ // Reset loading text when hiding
806
+ const loadingText = overlay.querySelector('span');
807
+ if (loadingText) {
808
+ loadingText.textContent = 'Generating WebP...';
809
+ }
810
+ }
1013
811
  }
1014
812
 
1015
- const downloadBtn = s('#download-gif-btn');
813
+ const downloadBtn = s('#download-webp-btn');
1016
814
  if (downloadBtn) {
1017
815
  downloadBtn.disabled = show;
1018
816
  }
817
+
818
+ // Remove old info badge if exists
819
+ const oldBadge = s('.webp-info-badge');
820
+ if (oldBadge && show) {
821
+ oldBadge.remove();
822
+ }
1019
823
  },
1020
824
 
1021
- downloadGif: function () {
1022
- if (!this.Data.gif) {
825
+ downloadWebp: function () {
826
+ if (!this.Data.webp) {
1023
827
  NotificationManager.Push({
1024
- html: 'No GIF available to download',
828
+ html: 'No WebP available to download',
1025
829
  status: 'warning',
1026
830
  });
1027
831
  return;
@@ -1029,31 +833,46 @@ const ObjectLayerEngineViewer = {
1029
833
 
1030
834
  const { objectLayer, currentDirection, currentMode } = this.Data;
1031
835
  const numericCode = this.getDirectionCode(currentDirection, currentMode);
1032
- const filename = `${objectLayer.data.item.id}_${currentDirection}_${currentMode}_${numericCode}.gif`;
836
+ const filename = `${objectLayer.data.item.id}_${currentDirection}_${currentMode}_${numericCode}.webp`;
1033
837
 
1034
- const url = URL.createObjectURL(this.Data.gif);
838
+ // Create a temporary anchor element to trigger download
1035
839
  const a = document.createElement('a');
1036
- a.href = url;
840
+ a.href = this.Data.webp;
1037
841
  a.download = filename;
1038
842
  document.body.appendChild(a);
1039
843
  a.click();
1040
844
  document.body.removeChild(a);
1041
- URL.revokeObjectURL(url);
1042
845
 
1043
846
  NotificationManager.Push({
1044
- html: `GIF downloaded: ${filename}`,
847
+ html: `WebP downloaded: ${filename}`,
1045
848
  status: 'success',
1046
849
  });
1047
850
  },
1048
851
 
1049
- Reload: async function () {
852
+ toEngine: function () {
853
+ const { objectLayer } = this.Data;
854
+ if (!objectLayer || !objectLayer._id) return;
855
+
856
+ // Navigate to editor route first
857
+ setPath(`${getProxyPath()}object-layer-engine`);
858
+ // Then add query param without replacing history
859
+ setQueryParams({ cid: objectLayer._id }, { replace: true });
860
+
861
+ if (s(`.modal-object-layer-engine`)) {
862
+ ObjectLayerEngineModal.Reload();
863
+ } else {
864
+ s(`.main-btn-object-layer-engine`)?.click();
865
+ }
866
+ },
867
+
868
+ Reload: async function ({ Elements }) {
1050
869
  const queryParams = new URLSearchParams(window.location.search);
1051
870
  const cid = queryParams.get('cid');
1052
871
 
1053
872
  if (cid) {
1054
- await this.loadObjectLayer(cid);
873
+ await this.loadObjectLayer(cid, Elements);
1055
874
  } else {
1056
- this.renderEmpty();
875
+ this.renderEmpty({ Elements });
1057
876
  }
1058
877
  },
1059
878
  };