underpost 2.89.37 → 2.89.44

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 (42) hide show
  1. package/README.md +3 -2
  2. package/bin/deploy.js +22 -15
  3. package/cli.md +22 -2
  4. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  5. package/manifests/deployment/dd-test-development/deployment.yaml +6 -2
  6. package/manifests/deployment/dd-test-development/proxy.yaml +2 -0
  7. package/manifests/deployment/kafka/deployment.yaml +0 -2
  8. package/manifests/deployment/spark/spark-pi-py.yaml +0 -1
  9. package/manifests/deployment/tensorflow/tf-gpu-test.yaml +0 -2
  10. package/manifests/envoy-service-nodeport.yaml +0 -1
  11. package/manifests/kubeadm-calico-config.yaml +10 -115
  12. package/manifests/letsencrypt-prod.yaml +0 -1
  13. package/manifests/mariadb/statefulset.yaml +1 -1
  14. package/manifests/mongodb/statefulset.yaml +11 -11
  15. package/manifests/mongodb-4.4/service-deployment.yaml +1 -3
  16. package/manifests/mysql/pv-pvc.yaml +1 -1
  17. package/manifests/mysql/statefulset.yaml +1 -1
  18. package/manifests/valkey/service.yaml +0 -1
  19. package/manifests/valkey/statefulset.yaml +2 -3
  20. package/package.json +1 -1
  21. package/scripts/device-scan.sh +43 -21
  22. package/scripts/rpmfusion-ffmpeg-setup.sh +1 -0
  23. package/src/cli/cluster.js +51 -26
  24. package/src/cli/deploy.js +52 -28
  25. package/src/cli/index.js +22 -1
  26. package/src/cli/monitor.js +9 -5
  27. package/src/cli/repository.js +1 -1
  28. package/src/cli/run.js +30 -18
  29. package/src/client/components/core/Logger.js +1 -1
  30. package/src/client/components/core/Modal.js +5 -0
  31. package/src/client/components/core/ObjectLayerEngineModal.js +334 -71
  32. package/src/client/components/core/ObjectLayerEngineViewer.js +170 -403
  33. package/src/client/components/core/Router.js +10 -1
  34. package/src/client/services/default/default.management.js +25 -5
  35. package/src/index.js +1 -1
  36. package/src/server/client-build.js +5 -4
  37. package/src/server/conf.js +1 -1
  38. package/manifests/kubelet-config.yaml +0 -65
  39. package/manifests/mongodb/backup-access.yaml +0 -16
  40. package/manifests/mongodb/backup-cronjob.yaml +0 -42
  41. package/manifests/mongodb/backup-pv-pvc.yaml +0 -22
  42. package/manifests/mongodb/configmap.yaml +0 -26
@@ -3,10 +3,13 @@ import { getProxyPath, listenQueryPathInstance, setPath, setQueryParams } from '
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
8
  import { ObjectLayerManagement } from '../../services/object-layer/object-layer.management.js';
9
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';
10
13
 
11
14
  const logger = loggerFactory(import.meta);
12
15
 
@@ -16,12 +19,8 @@ const ObjectLayerEngineViewer = {
16
19
  frameCounts: null,
17
20
  currentDirection: 'down',
18
21
  currentMode: 'idle',
19
- gif: null,
20
- gifWorkerBlob: null,
22
+ webp: null,
21
23
  isGenerating: false,
22
- // Binary transparency settings for GIF export
23
- gifTransparencyPlaceholder: { r: 100, g: 100, b: 100 }, // magenta - unlikely to exist in sprites
24
- transparencyThreshold: 16, // alpha threshold (0-255) for binary transparency
25
24
  },
26
25
 
27
26
  // Map user-friendly direction/mode to numeric direction codes
@@ -40,24 +39,13 @@ const ObjectLayerEngineViewer = {
40
39
  return directionCodeMap[key] || null;
41
40
  },
42
41
 
43
- // Get all possible direction names for a direction code
44
- getDirectionsFromDirectionCode: function (directionCode) {
45
- const directionMap = {
46
- '08': ['down_idle', 'none_idle', 'default_idle'],
47
- 18: ['down_walking'],
48
- '02': ['up_idle'],
49
- 12: ['up_walking'],
50
- '04': ['left_idle', 'up_left_idle', 'down_left_idle'],
51
- 14: ['left_walking', 'up_left_walking', 'down_left_walking'],
52
- '06': ['right_idle', 'up_right_idle', 'down_right_idle'],
53
- 16: ['right_walking', 'up_right_walking', 'down_right_walking'],
54
- };
55
- return directionMap[directionCode] || [];
56
- },
57
-
58
42
  Render: async function ({ Elements }) {
59
43
  const id = 'object-layer-engine-viewer';
60
44
 
45
+ Modal.Data[`modal-${id}`].onReloadModalListener[id] = async () => {
46
+ ObjectLayerEngineViewer.Reload({ Elements });
47
+ };
48
+
61
49
  // Listen for cid query parameter
62
50
  listenQueryPathInstance(
63
51
  {
@@ -65,7 +53,7 @@ const ObjectLayerEngineViewer = {
65
53
  routeId: 'object-layer-engine-viewer',
66
54
  event: async (cid) => {
67
55
  if (cid) {
68
- await this.loadObjectLayer(cid);
56
+ await this.loadObjectLayer(cid, Elements);
69
57
  } else {
70
58
  this.renderEmpty({ Elements });
71
59
  }
@@ -74,25 +62,6 @@ const ObjectLayerEngineViewer = {
74
62
  'cid',
75
63
  );
76
64
 
77
- setTimeout(async () => {
78
- htmls(
79
- `#${id}`,
80
- html` <div class="inl section-mp">
81
- <div class="in">
82
- <div class="fl">
83
- <div class="in fll">
84
- ${await BtnIcon.Render({
85
- class: 'section-mp main-button',
86
- label: html`<i class="fa-solid fa-arrow-left"></i> ${' Back'}`,
87
- attrs: `data-id="btn-back"`,
88
- })}
89
- </div>
90
- </div>
91
- </div>
92
- </div>`,
93
- );
94
- });
95
-
96
65
  return html`
97
66
  <div class="fl">
98
67
  <div class="in ${id}" id="${id}">
@@ -106,16 +75,22 @@ const ObjectLayerEngineViewer = {
106
75
 
107
76
  renderEmpty: async function ({ Elements }) {
108
77
  const id = 'object-layer-engine-viewer';
109
- htmls(
110
- `#${id}`,
111
- await ObjectLayerManagement.RenderTable({
112
- Elements,
113
- idModal: 'modal-object-layer-engine-viewer',
114
- }),
115
- );
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
+ );
116
91
  },
117
92
 
118
- loadObjectLayer: async function (objectLayerId) {
93
+ loadObjectLayer: async function (objectLayerId, Elements) {
119
94
  const id = 'object-layer-engine-viewer';
120
95
 
121
96
  try {
@@ -136,18 +111,18 @@ const ObjectLayerEngineViewer = {
136
111
  }
137
112
 
138
113
  this.Data.frameCounts = frameData.frameCounts;
139
-
140
- // Auto-select first available direction/mode combination
141
- 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';
142
120
 
143
121
  // Render the viewer UI
144
- await this.renderViewer();
145
-
146
- // Initialize gif.js worker
147
- await this.initGifJs();
122
+ await this.renderViewer({ Elements });
148
123
 
149
- // Generate initial GIF
150
- await this.generateGif();
124
+ // Generate WebP
125
+ await this.generateWebp();
151
126
  } catch (error) {
152
127
  logger.error('Error loading object layer:', error);
153
128
  NotificationManager.Push({
@@ -169,7 +144,7 @@ const ObjectLayerEngineViewer = {
169
144
  }
170
145
  },
171
146
 
172
- renderViewer: async function () {
147
+ renderViewer: async function ({ Elements }) {
173
148
  const id = 'object-layer-engine-viewer';
174
149
  const { objectLayer, frameCounts } = this.Data;
175
150
 
@@ -179,8 +154,6 @@ const ObjectLayerEngineViewer = {
179
154
  const itemId = objectLayer.data.item.id;
180
155
  const itemDescription = objectLayer.data.item.description || '';
181
156
  const itemActivable = objectLayer.data.item.activable || false;
182
- const frameDuration = objectLayer.data.render.frame_duration || 100;
183
- const isStateless = objectLayer.data.render.is_stateless || false;
184
157
 
185
158
  // Get stats data
186
159
  const stats = objectLayer.data.stats || {};
@@ -220,7 +193,7 @@ const ObjectLayerEngineViewer = {
220
193
  color: ${darkTheme ? '#fff' : '#333'};
221
194
  }
222
195
 
223
- .gif-display-area {
196
+ .webp-display-area {
224
197
  background: ${darkTheme ? '#2a2a2a' : '#f5f5f5'};
225
198
  border: 2px solid ${darkTheme ? '#444' : '#ddd'};
226
199
  border-radius: 12px;
@@ -236,7 +209,7 @@ const ObjectLayerEngineViewer = {
236
209
  overflow: auto;
237
210
  }
238
211
 
239
- .gif-canvas-container {
212
+ .webp-canvas-container {
240
213
  position: relative;
241
214
  display: flex;
242
215
  justify-content: center;
@@ -245,8 +218,8 @@ const ObjectLayerEngineViewer = {
245
218
  height: 100%;
246
219
  }
247
220
 
248
- .gif-canvas-container canvas,
249
- .gif-canvas-container img {
221
+ .webp-canvas-container canvas,
222
+ .webp-canvas-container img {
250
223
  image-rendering: pixelated;
251
224
  image-rendering: -moz-crisp-edges;
252
225
  image-rendering: crisp-edges;
@@ -262,13 +235,13 @@ const ObjectLayerEngineViewer = {
262
235
  display: block;
263
236
  }
264
237
 
265
- .gif-canvas-container canvas {
238
+ .webp-canvas-container canvas {
266
239
  background: repeating-conic-gradient(#80808020 0% 25%, #fff0 0% 50%) 50% / 20px 20px;
267
240
  min-width: 128px;
268
241
  min-height: 128px;
269
242
  }
270
243
 
271
- .gif-info-badge {
244
+ .webp-info-badge {
272
245
  position: absolute;
273
246
  bottom: 10px;
274
247
  right: 10px;
@@ -281,7 +254,7 @@ const ObjectLayerEngineViewer = {
281
254
  backdrop-filter: blur(4px);
282
255
  }
283
256
 
284
- .gif-info-badge .info-label {
257
+ .webp-info-badge .info-label {
285
258
  opacity: 0.7;
286
259
  margin-right: 4px;
287
260
  }
@@ -363,7 +336,7 @@ const ObjectLayerEngineViewer = {
363
336
  font-size: 16px;
364
337
  }
365
338
 
366
- .download-btn {
339
+ .default-viewer-btn {
367
340
  width: 100%;
368
341
  padding: 15px;
369
342
  background: ${darkTheme ? '#4caf50' : '#4CAF50'};
@@ -380,7 +353,7 @@ const ObjectLayerEngineViewer = {
380
353
  gap: 10px;
381
354
  }
382
355
 
383
- .download-btn:hover {
356
+ .default-viewer-btn:hover {
384
357
  background: ${darkTheme ? '#45a049' : '#45a049'};
385
358
  transform: translateY(-2px);
386
359
  box-shadow: 0 4px 12px rgba(76, 175, 80, 0.3);
@@ -399,7 +372,7 @@ const ObjectLayerEngineViewer = {
399
372
  margin-left: 4px;
400
373
  }
401
374
 
402
- .download-btn:disabled {
375
+ .default-viewer-btn:disabled {
403
376
  background: ${darkTheme ? '#555' : '#ccc'};
404
377
  cursor: not-allowed;
405
378
  transform: none;
@@ -414,28 +387,28 @@ const ObjectLayerEngineViewer = {
414
387
  }
415
388
 
416
389
  @media (max-width: 768px) {
417
- .gif-display-area {
390
+ .webp-display-area {
418
391
  max-height: 500px;
419
392
  min-height: 300px;
420
393
  padding: 20px;
421
394
  }
422
395
 
423
- .gif-canvas-container canvas,
424
- .gif-canvas-container img {
396
+ .webp-canvas-container canvas,
397
+ .webp-canvas-container img {
425
398
  max-width: 100%;
426
399
  max-height: 440px;
427
400
  }
428
401
  }
429
402
 
430
403
  @media (max-width: 600px) {
431
- .gif-display-area {
404
+ .webp-display-area {
432
405
  max-height: 400px;
433
406
  min-height: 250px;
434
407
  padding: 15px;
435
408
  }
436
409
 
437
- .gif-canvas-container canvas,
438
- .gif-canvas-container img {
410
+ .webp-canvas-container canvas,
411
+ .webp-canvas-container img {
439
412
  max-height: 340px;
440
413
  }
441
414
 
@@ -473,6 +446,12 @@ const ObjectLayerEngineViewer = {
473
446
  color: ${darkTheme ? '#666' : '#999'};
474
447
  padding: 20px;
475
448
  }
449
+
450
+ @media (max-width: 850px) {
451
+ .object-layer-viewer-container {
452
+ padding: 5px;
453
+ }
454
+ }
476
455
  </style>`,
477
456
  );
478
457
  };
@@ -535,16 +514,16 @@ const ObjectLayerEngineViewer = {
535
514
  </div>
536
515
  </div>
537
516
 
538
- <div class="gif-display-area">
539
- <div class="gif-canvas-container" id="gif-canvas-container">
517
+ <div class="webp-display-area">
518
+ <div class="webp-canvas-container" id="webp-canvas-container">
540
519
  <div style="text-align: center; color: ${darkTheme ? '#aaa' : '#666'};">
541
520
  <i class="fa-solid fa-image" style="font-size: 48px; opacity: 0.3; margin-bottom: 16px;"></i>
542
- <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>
543
522
  </div>
544
- <div id="gif-loading-overlay" class="loading-overlay" style="display: none;">
523
+ <div id="webp-loading-overlay" class="loading-overlay" style="display: none;">
545
524
  <div>
546
525
  <i class="fa-solid fa-spinner fa-spin"></i>
547
- <span style="margin-left: 10px;">Generating GIF...</span>
526
+ <span style="margin-left: 10px;">Generating WebP...</span>
548
527
  </div>
549
528
  </div>
550
529
  </div>
@@ -631,11 +610,15 @@ const ObjectLayerEngineViewer = {
631
610
  </div>
632
611
 
633
612
  <div style="display: flex; gap: 10px; margin-top: 20px;">
634
- <button class="download-btn" id="download-gif-btn" style="width: 100%;">
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">
635
618
  <i class="fa-solid fa-download"></i>
636
- <span>Download GIF</span>
619
+ <span>Download WebP</span>
637
620
  </button>
638
- <button class="download-btn edit-btn" id="edit-object-layer-btn" style="width: 100%;">
621
+ <button class="default-viewer-btn edit-btn" id="edit-object-layer-btn">
639
622
  <i class="fa-solid fa-edit"></i>
640
623
  <span>Edit</span>
641
624
  </button>
@@ -645,10 +628,10 @@ const ObjectLayerEngineViewer = {
645
628
  );
646
629
  ThemeEvents[id]();
647
630
  // Attach event listeners
648
- this.attachEventListeners();
631
+ this.attachEventListeners({ Elements });
649
632
  },
650
633
 
651
- attachEventListeners: function () {
634
+ attachEventListeners: function ({ Elements }) {
652
635
  // Direction buttons
653
636
  const directionButtons = document.querySelectorAll('[data-direction]');
654
637
  directionButtons.forEach((btn) => {
@@ -657,9 +640,9 @@ const ObjectLayerEngineViewer = {
657
640
  const direction = e.currentTarget.getAttribute('data-direction');
658
641
  if (direction !== this.Data.currentDirection) {
659
642
  this.Data.currentDirection = direction;
660
- await this.renderViewer();
661
- await this.attachEventListeners();
662
- await this.generateGif();
643
+ await this.renderViewer({ Elements });
644
+ await this.attachEventListeners({ Elements });
645
+ await this.generateWebp();
663
646
  }
664
647
  });
665
648
  });
@@ -672,104 +655,41 @@ const ObjectLayerEngineViewer = {
672
655
  const mode = e.currentTarget.getAttribute('data-mode');
673
656
  if (mode !== this.Data.currentMode) {
674
657
  this.Data.currentMode = mode;
675
- await this.renderViewer();
676
- await this.attachEventListeners();
677
- await this.generateGif();
658
+ await this.renderViewer({ Elements });
659
+ await this.attachEventListeners({ Elements });
660
+ await this.generateWebp();
678
661
  }
679
662
  });
680
663
  });
681
664
 
682
665
  // Download button
683
- const downloadBtn = s('#download-gif-btn');
666
+ const downloadBtn = s('#download-webp-btn');
684
667
  if (downloadBtn) {
685
668
  downloadBtn.addEventListener('click', () => {
686
- this.downloadGif();
669
+ this.downloadWebp();
670
+ });
671
+ }
672
+
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 });
687
680
  });
688
681
  }
689
682
 
683
+ // Edit button
690
684
  const editBtn = s('#edit-object-layer-btn');
691
685
  if (editBtn) {
692
686
  editBtn.addEventListener('click', () => {
693
687
  this.toEngine();
694
688
  });
695
689
  }
696
-
697
- // Back button
698
- setTimeout(() => {
699
- const backBtn = s('[data-id="btn-back"]');
700
- if (backBtn) {
701
- backBtn.addEventListener('click', () => {
702
- window.history.back();
703
- });
704
- }
705
- }, 100);
706
- },
707
-
708
- selectFirstAvailableDirectionMode: function () {
709
- const { frameCounts } = this.Data;
710
- if (!frameCounts) return;
711
-
712
- // Priority order for directions
713
- const directions = ['down', 'up', 'left', 'right'];
714
- // Priority order for modes
715
- const modes = ['idle', 'walking'];
716
-
717
- // Try to find first available combination using numeric codes
718
- for (const mode of modes) {
719
- for (const direction of directions) {
720
- const numericCode = this.getDirectionCode(direction, mode);
721
- if (numericCode && frameCounts[numericCode] && frameCounts[numericCode] > 0) {
722
- this.Data.currentDirection = direction;
723
- this.Data.currentMode = mode;
724
- logger.info(`Auto-selected: ${direction} ${mode} (code: ${numericCode}, ${frameCounts[numericCode]} frames)`);
725
- return;
726
- }
727
- }
728
- }
729
-
730
- // If no frames found, log warning
731
- logger.warn('No frames found for any direction/mode combination');
732
- },
733
-
734
- initGifJs: async function () {
735
- if (this.Data.gifWorkerBlob) return; // Already initialized
736
-
737
- try {
738
- // Load gif.js library
739
- await this.loadScript('https://cdn.jsdelivr.net/npm/gif.js@0.2.0/dist/gif.min.js');
740
-
741
- // Fetch worker script
742
- const response = await fetch('https://cdn.jsdelivr.net/npm/gif.js@0.2.0/dist/gif.worker.js');
743
- if (!response.ok) {
744
- throw new Error('Failed to fetch gif.worker.js');
745
- }
746
- const workerBlob = await response.blob();
747
- this.Data.gifWorkerBlob = URL.createObjectURL(workerBlob);
748
-
749
- logger.info('gif.js initialized successfully');
750
- } catch (error) {
751
- logger.error('Error initializing gif.js:', error);
752
- throw error;
753
- }
754
690
  },
755
691
 
756
- loadScript: function (src) {
757
- return new Promise((resolve, reject) => {
758
- // Check if already loaded
759
- if (document.querySelector(`script[src="${src}"]`)) {
760
- resolve();
761
- return;
762
- }
763
-
764
- const script = document.createElement('script');
765
- script.src = src;
766
- script.onload = resolve;
767
- script.onerror = reject;
768
- document.head.appendChild(script);
769
- });
770
- },
771
-
772
- generateGif: async function () {
692
+ generateWebp: async function () {
773
693
  if (this.Data.isGenerating) return;
774
694
 
775
695
  const { objectLayer, frameCounts, currentDirection, currentMode } = this.Data;
@@ -802,128 +722,74 @@ const ObjectLayerEngineViewer = {
802
722
  this.Data.isGenerating = true;
803
723
  this.showLoading(true);
804
724
 
805
- try {
806
- // Build frame paths based on frame count using numeric code
807
- const frames = [];
808
- for (let i = 0; i < frameCount; i++) {
809
- frames.push(`${getProxyPath()}assets/${itemType}/${itemId}/${numericCode}/${i}.png`);
810
- }
811
-
812
- // Update loading message
813
- const loadingOverlay = s('#gif-loading-overlay');
814
- if (loadingOverlay) {
815
- loadingOverlay.querySelector('span').textContent = `Loading frames... (0/${frames.length})`;
816
- }
817
-
818
- // Load all frames to find maximum dimensions
819
- const loadedImages = [];
820
- let maxWidth = 0;
821
- let maxHeight = 0;
822
-
823
- for (let i = 0; i < frames.length; i++) {
824
- const img = await this.loadImage(frames[i]);
825
- loadedImages.push(img);
826
- maxWidth = Math.max(maxWidth, img.naturalWidth);
827
- maxHeight = Math.max(maxHeight, img.naturalHeight);
828
-
829
- // Update progress
830
- if (loadingOverlay && (i === 0 || i % 5 === 0)) {
831
- loadingOverlay.querySelector('span').textContent = `Loading frames... (${i + 1}/${frames.length})`;
832
- }
833
- }
834
-
835
- // Update loading message for GIF generation
836
- if (loadingOverlay) {
837
- 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}...`;
838
731
  }
732
+ }
839
733
 
840
- logger.info(`GIF dimensions calculated: ${maxWidth}x${maxHeight} from ${frames.length} frames`);
841
-
842
- // Use binary transparency with placeholder color (magenta)
843
- const placeholder = this.Data.gifTransparencyPlaceholder;
844
- const transparentColorHex = (placeholder.r << 16) | (placeholder.g << 8) | placeholder.b;
845
-
846
- // Create new GIF instance with binary transparency
847
- const gif = new GIF({
848
- workers: 2,
849
- workerScript: this.Data.gifWorkerBlob,
850
- quality: 10,
851
- width: maxWidth,
852
- height: maxHeight,
853
- transparent: transparentColorHex, // Use magenta as transparent color
854
- 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,
855
740
  });
856
741
 
857
- // Process each frame with binary transparency threshold
858
- for (let i = 0; i < loadedImages.length; i++) {
859
- const img = loadedImages[i];
860
-
861
- // Create canvas for this frame
862
- const canvas = document.createElement('canvas');
863
- canvas.width = maxWidth;
864
- canvas.height = maxHeight;
865
- const ctx = canvas.getContext('2d', { alpha: true, willReadFrequently: true });
866
-
867
- // Start with transparent canvas (don't fill with magenta yet)
868
- ctx.clearRect(0, 0, maxWidth, maxHeight);
869
-
870
- // Center the image
871
- const x = Math.floor((maxWidth - img.naturalWidth) / 2);
872
- const y = Math.floor((maxHeight - img.naturalHeight) / 2);
873
-
874
- // Disable smoothing to keep pixel-art sharp
875
- ctx.imageSmoothingEnabled = false;
876
-
877
- // Draw the original image centered on transparent canvas
878
- ctx.drawImage(img, x, y);
879
-
880
- // Apply binary transparency threshold: replace ONLY transparent pixels with placeholder color
881
- const threshold = this.Data.transparencyThreshold;
882
- try {
883
- const imageData = ctx.getImageData(0, 0, maxWidth, maxHeight);
884
- const data = imageData.data;
885
-
886
- for (let p = 0; p < data.length; p += 4) {
887
- const alpha = data[p + 3];
888
- // If alpha is below threshold, replace with opaque placeholder color (for GIF transparency)
889
- if (alpha < threshold) {
890
- data[p] = placeholder.r; // R
891
- data[p + 1] = placeholder.g; // G
892
- data[p + 2] = placeholder.b; // B
893
- data[p + 3] = 255; // A (fully opaque)
894
- }
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);
895
776
  }
896
-
897
- ctx.putImageData(imageData, 0, 0);
898
- } catch (err) {
899
- logger.warn(
900
- 'Could not access image data for transparency threshold (CORS issue). Transparency may not work correctly.',
901
- err,
902
- );
903
777
  }
904
778
 
905
- // Add frame to GIF with dispose mode to clear between frames
906
- gif.addFrame(canvas, {
907
- delay: frameDuration,
908
- copy: true,
909
- dispose: 2, // Restore to background color before drawing next frame (prevents overlap)
910
- });
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');
911
785
  }
912
786
 
913
- // Handle GIF finished event
914
- gif.on('finished', (blob) => {
915
- this.displayGif(blob, maxWidth, maxHeight, frameDuration, frameCount);
916
- this.Data.gif = blob;
917
- this.Data.isGenerating = false;
918
- this.showLoading(false);
919
- });
920
-
921
- // Render the GIF
922
- gif.render();
787
+ this.Data.isGenerating = false;
788
+ this.showLoading(false);
923
789
  } catch (error) {
924
- logger.error('Error generating GIF:', error);
790
+ logger.error('Error generating WebP:', error);
925
791
  NotificationManager.Push({
926
- html: `Failed to generate GIF: ${error.message}`,
792
+ html: `Failed to generate WebP: ${error.message}`,
927
793
  status: 'error',
928
794
  });
929
795
  this.Data.isGenerating = false;
@@ -931,133 +797,35 @@ const ObjectLayerEngineViewer = {
931
797
  }
932
798
  },
933
799
 
934
- loadImage: function (src) {
935
- return new Promise((resolve, reject) => {
936
- const img = new Image();
937
- img.crossOrigin = 'anonymous';
938
- img.onload = () => resolve(img);
939
- img.onerror = reject;
940
- img.src = src;
941
- });
942
- },
943
-
944
- displayGif: function (blob, originalWidth, originalHeight, frameDuration, frameCount) {
945
- const container = s('#gif-canvas-container');
946
- if (!container) return;
947
-
948
- const url = URL.createObjectURL(blob);
949
-
950
- // Create img element for the animated GIF
951
- const gifImg = document.createElement('img');
952
- gifImg.src = url;
953
-
954
- gifImg.onload = () => {
955
- // Use provided dimensions or get from image
956
- const naturalWidth = originalWidth || gifImg.naturalWidth;
957
- const naturalHeight = originalHeight || gifImg.naturalHeight;
958
-
959
- // Calculate intelligent scaling based on container and image size
960
- const containerEl = s('.gif-display-area');
961
- const containerWidth = containerEl ? containerEl.clientWidth - 60 : 400; // subtract padding
962
- const containerHeight = containerEl ? containerEl.clientHeight - 60 : 400;
963
-
964
- // Calculate scale to fit container while maintaining aspect ratio
965
- const scaleToFitWidth = containerWidth / naturalWidth;
966
- const scaleToFitHeight = containerHeight / naturalHeight;
967
- const scaleToFit = Math.min(scaleToFitWidth, scaleToFitHeight);
968
-
969
- // For pixel art, use integer scaling for better visuals
970
- // Minimum 2x for small sprites, but respect container size
971
- let scale = Math.max(1, Math.floor(scaleToFit));
972
-
973
- // For very small sprites (< 100px), try to scale up more
974
- if (Math.max(naturalWidth, naturalHeight) < 100) {
975
- scale = Math.min(4, Math.floor(scaleToFit));
976
- }
977
-
978
- // Make sure scaled image fits in container
979
- const displayWidth = naturalWidth * scale;
980
- const displayHeight = naturalHeight * scale;
981
-
982
- if (displayWidth > containerWidth || displayHeight > containerHeight) {
983
- scale = Math.max(1, scale - 1);
984
- }
985
-
986
- gifImg.style.width = `${naturalWidth * scale}px !important`;
987
- gifImg.style.height = `${naturalHeight * scale}px !important`;
988
- gifImg.style.maxWidth = '100%';
989
- gifImg.style.maxHeight = '540px';
990
-
991
- // Force pixel-perfect rendering (no antialiasing/blur)
992
- // gifImg.style.imageRendering = 'pixelated';
993
- // gifImg.style.imageRendering = '-moz-crisp-edges';
994
- // gifImg.style.imageRendering = 'crisp-edges';
995
- // gifImg.style.msInterpolationMode = 'nearest-neighbor';
996
-
997
- // Prevent any browser scaling optimizations
998
- // gifImg.style.transform = 'translateZ(0)'; // Force GPU rendering
999
- // gifImg.style.backfaceVisibility = 'hidden'; // Prevent subpixel rendering
1000
-
1001
- // Clear container and add the GIF
1002
- container.innerHTML = '';
1003
- container.appendChild(gifImg);
1004
-
1005
- // Re-add loading overlay
1006
- const overlay = document.createElement('div');
1007
- overlay.id = 'gif-loading-overlay';
1008
- overlay.className = 'loading-overlay';
1009
- overlay.style.display = 'none';
1010
- overlay.innerHTML = html`
1011
- <div>
1012
- <i class="fa-solid fa-spinner fa-spin"></i>
1013
- <span style="margin-left: 10px;">Generating GIF...</span>
1014
- </div>
1015
- `;
1016
- container.appendChild(overlay);
1017
-
1018
- // Add info badge with dimensions and scale
1019
- const infoBadge = document.createElement('div');
1020
- infoBadge.className = 'gif-info-badge';
1021
- const displayW = Math.round(naturalWidth * scale);
1022
- const displayH = Math.round(naturalHeight * scale);
1023
- infoBadge.innerHTML = html`
1024
- <span class="info-label">Dimensions:</span> ${naturalWidth}x${naturalHeight}px<br />
1025
- <span class="info-label">Display:</span> ${displayW}x${displayH}px<br />
1026
- ${scale > 1 ? `<span class="info-label">Scale:</span> ${scale}x<br />` : ''}
1027
- <span class="info-label">Frames:</span> ${frameCount}<br />
1028
- <span class="info-label">Frame Duration:</span> ${frameDuration}ms<br />
1029
- <span class="info-label">Total Duration:</span> ${(frameDuration * frameCount) / 1000}s
1030
- `;
1031
- s(`.gif-display-area`).appendChild(infoBadge);
1032
-
1033
- logger.info(`Displaying GIF: ${naturalWidth}x${naturalHeight} at ${scale}x scale (${displayW}x${displayH})`);
1034
- };
1035
-
1036
- gifImg.onerror = () => {
1037
- logger.error('Failed to load GIF image');
1038
- NotificationManager.Push({
1039
- html: 'Failed to display GIF',
1040
- status: 'error',
1041
- });
1042
- };
1043
- },
1044
-
1045
800
  showLoading: function (show) {
1046
- const overlay = s('#gif-loading-overlay');
801
+ const overlay = s('#webp-loading-overlay');
1047
802
  if (overlay) {
1048
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
+ }
1049
811
  }
1050
812
 
1051
- const downloadBtn = s('#download-gif-btn');
813
+ const downloadBtn = s('#download-webp-btn');
1052
814
  if (downloadBtn) {
1053
815
  downloadBtn.disabled = show;
1054
816
  }
817
+
818
+ // Remove old info badge if exists
819
+ const oldBadge = s('.webp-info-badge');
820
+ if (oldBadge && show) {
821
+ oldBadge.remove();
822
+ }
1055
823
  },
1056
824
 
1057
- downloadGif: function () {
1058
- if (!this.Data.gif) {
825
+ downloadWebp: function () {
826
+ if (!this.Data.webp) {
1059
827
  NotificationManager.Push({
1060
- html: 'No GIF available to download',
828
+ html: 'No WebP available to download',
1061
829
  status: 'warning',
1062
830
  });
1063
831
  return;
@@ -1065,19 +833,18 @@ const ObjectLayerEngineViewer = {
1065
833
 
1066
834
  const { objectLayer, currentDirection, currentMode } = this.Data;
1067
835
  const numericCode = this.getDirectionCode(currentDirection, currentMode);
1068
- const filename = `${objectLayer.data.item.id}_${currentDirection}_${currentMode}_${numericCode}.gif`;
836
+ const filename = `${objectLayer.data.item.id}_${currentDirection}_${currentMode}_${numericCode}.webp`;
1069
837
 
1070
- const url = URL.createObjectURL(this.Data.gif);
838
+ // Create a temporary anchor element to trigger download
1071
839
  const a = document.createElement('a');
1072
- a.href = url;
840
+ a.href = this.Data.webp;
1073
841
  a.download = filename;
1074
842
  document.body.appendChild(a);
1075
843
  a.click();
1076
844
  document.body.removeChild(a);
1077
- URL.revokeObjectURL(url);
1078
845
 
1079
846
  NotificationManager.Push({
1080
- html: `GIF downloaded: ${filename}`,
847
+ html: `WebP downloaded: ${filename}`,
1081
848
  status: 'success',
1082
849
  });
1083
850
  },
@@ -1103,7 +870,7 @@ const ObjectLayerEngineViewer = {
1103
870
  const cid = queryParams.get('cid');
1104
871
 
1105
872
  if (cid) {
1106
- await this.loadObjectLayer(cid);
873
+ await this.loadObjectLayer(cid, Elements);
1107
874
  } else {
1108
875
  this.renderEmpty({ Elements });
1109
876
  }