@littlecarlito/blorktools 0.50.3 → 0.51.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.
Files changed (114) hide show
  1. package/bin/cli.js +69 -0
  2. package/package.json +13 -7
  3. package/src/asset_debugger/axis-indicator/axis-indicator.css +6 -0
  4. package/src/asset_debugger/axis-indicator/axis-indicator.html +20 -0
  5. package/src/asset_debugger/axis-indicator/axis-indicator.js +822 -0
  6. package/src/asset_debugger/debugger-scene/debugger-scene.css +142 -0
  7. package/src/asset_debugger/debugger-scene/debugger-scene.html +80 -0
  8. package/src/asset_debugger/debugger-scene/debugger-scene.js +791 -0
  9. package/src/asset_debugger/header/header.css +73 -0
  10. package/src/asset_debugger/header/header.html +24 -0
  11. package/src/asset_debugger/header/header.js +224 -0
  12. package/src/asset_debugger/index.html +76 -0
  13. package/src/asset_debugger/landing-page/landing-page.css +396 -0
  14. package/src/asset_debugger/landing-page/landing-page.html +81 -0
  15. package/src/asset_debugger/landing-page/landing-page.js +611 -0
  16. package/src/asset_debugger/loading-splash/loading-splash.css +195 -0
  17. package/src/asset_debugger/loading-splash/loading-splash.html +22 -0
  18. package/src/asset_debugger/loading-splash/loading-splash.js +59 -0
  19. package/src/asset_debugger/loading-splash/preview-loading-splash.js +66 -0
  20. package/src/asset_debugger/main.css +14 -0
  21. package/src/asset_debugger/modals/examples-modal/examples-modal.css +41 -0
  22. package/src/asset_debugger/modals/examples-modal/examples-modal.html +18 -0
  23. package/src/asset_debugger/modals/examples-modal/examples-modal.js +111 -0
  24. package/src/asset_debugger/modals/examples-modal/examples.js +125 -0
  25. package/src/asset_debugger/modals/html-editor-modal/html-editor-modal.css +452 -0
  26. package/src/asset_debugger/modals/html-editor-modal/html-editor-modal.html +87 -0
  27. package/src/asset_debugger/modals/html-editor-modal/html-editor-modal.js +675 -0
  28. package/src/asset_debugger/modals/mesh-info-modal/mesh-info-modal.css +219 -0
  29. package/src/asset_debugger/modals/mesh-info-modal/mesh-info-modal.html +20 -0
  30. package/src/asset_debugger/modals/mesh-info-modal/mesh-info-modal.js +548 -0
  31. package/src/asset_debugger/modals/settings-modal/settings-modal.css +103 -0
  32. package/src/asset_debugger/modals/settings-modal/settings-modal.html +158 -0
  33. package/src/asset_debugger/modals/settings-modal/settings-modal.js +475 -0
  34. package/src/asset_debugger/panels/asset-panel/asset-panel.css +263 -0
  35. package/src/asset_debugger/panels/asset-panel/asset-panel.html +123 -0
  36. package/src/asset_debugger/panels/asset-panel/asset-panel.js +136 -0
  37. package/src/asset_debugger/panels/asset-panel/atlas-heading/atlas-heading.css +94 -0
  38. package/src/asset_debugger/panels/asset-panel/atlas-heading/atlas-heading.js +312 -0
  39. package/src/asset_debugger/panels/asset-panel/mesh-heading/mesh-heading.css +129 -0
  40. package/src/asset_debugger/panels/asset-panel/mesh-heading/mesh-heading.js +486 -0
  41. package/src/asset_debugger/panels/asset-panel/rig-heading/rig-heading.css +545 -0
  42. package/src/asset_debugger/panels/asset-panel/rig-heading/rig-heading.js +538 -0
  43. package/src/asset_debugger/panels/asset-panel/uv-heading/uv-heading.css +70 -0
  44. package/src/asset_debugger/panels/asset-panel/uv-heading/uv-heading.js +586 -0
  45. package/src/asset_debugger/panels/world-panel/world-panel.css +364 -0
  46. package/src/asset_debugger/panels/world-panel/world-panel.html +173 -0
  47. package/src/asset_debugger/panels/world-panel/world-panel.js +1891 -0
  48. package/src/asset_debugger/router.js +190 -0
  49. package/src/asset_debugger/util/animation/playback/animation-playback-controller.js +150 -0
  50. package/src/asset_debugger/util/animation/playback/animation-preview-controller.js +316 -0
  51. package/src/asset_debugger/util/animation/playback/css3d-bounce-controller.js +400 -0
  52. package/src/asset_debugger/util/animation/playback/css3d-reversal-controller.js +821 -0
  53. package/src/asset_debugger/util/animation/render/css3d-prerender-controller.js +696 -0
  54. package/src/asset_debugger/util/animation/render/debug-texture-factory.js +0 -0
  55. package/src/asset_debugger/util/animation/render/iframe2texture-render-controller.js +199 -0
  56. package/src/asset_debugger/util/animation/render/image2texture-prerender-controller.js +461 -0
  57. package/src/asset_debugger/util/animation/render/pbr-material-factory.js +82 -0
  58. package/src/asset_debugger/util/common.css +280 -0
  59. package/src/asset_debugger/util/data/animation-classifier.js +323 -0
  60. package/src/asset_debugger/util/data/duplicate-handler.js +20 -0
  61. package/src/asset_debugger/util/data/glb-buffer-manager.js +407 -0
  62. package/src/asset_debugger/util/data/glb-classifier.js +290 -0
  63. package/src/asset_debugger/util/data/html-formatter.js +76 -0
  64. package/src/asset_debugger/util/data/html-linter.js +276 -0
  65. package/src/asset_debugger/util/data/localstorage-manager.js +265 -0
  66. package/src/asset_debugger/util/data/mesh-html-manager.js +295 -0
  67. package/src/asset_debugger/util/data/string-serder.js +303 -0
  68. package/src/asset_debugger/util/data/texture-classifier.js +663 -0
  69. package/src/asset_debugger/util/data/upload/background-file-handler.js +292 -0
  70. package/src/asset_debugger/util/data/upload/dropzone-preview-controller.js +396 -0
  71. package/src/asset_debugger/util/data/upload/file-upload-manager.js +495 -0
  72. package/src/asset_debugger/util/data/upload/glb-file-handler.js +36 -0
  73. package/src/asset_debugger/util/data/upload/glb-preview-controller.js +317 -0
  74. package/src/asset_debugger/util/data/upload/lighting-file-handler.js +194 -0
  75. package/src/asset_debugger/util/data/upload/model-file-manager.js +104 -0
  76. package/src/asset_debugger/util/data/upload/texture-file-handler.js +166 -0
  77. package/src/asset_debugger/util/data/upload/zip-handler.js +686 -0
  78. package/src/asset_debugger/util/loaders/html2canvas-loader.js +107 -0
  79. package/src/asset_debugger/util/rig/bone-kinematics.js +403 -0
  80. package/src/asset_debugger/util/rig/rig-constraint-manager.js +618 -0
  81. package/src/asset_debugger/util/rig/rig-controller.js +612 -0
  82. package/src/asset_debugger/util/rig/rig-factory.js +628 -0
  83. package/src/asset_debugger/util/rig/rig-handle-factory.js +46 -0
  84. package/src/asset_debugger/util/rig/rig-label-factory.js +441 -0
  85. package/src/asset_debugger/util/rig/rig-mouse-handler.js +377 -0
  86. package/src/asset_debugger/util/rig/rig-state-manager.js +175 -0
  87. package/src/asset_debugger/util/rig/rig-tooltip-manager.js +267 -0
  88. package/src/asset_debugger/util/rig/rig-ui-factory.js +700 -0
  89. package/src/asset_debugger/util/scene/background-manager.js +284 -0
  90. package/src/asset_debugger/util/scene/camera-controller.js +243 -0
  91. package/src/asset_debugger/util/scene/css3d-debug-controller.js +406 -0
  92. package/src/asset_debugger/util/scene/css3d-frame-factory.js +113 -0
  93. package/src/asset_debugger/util/scene/css3d-scene-manager.js +529 -0
  94. package/src/asset_debugger/util/scene/glb-controller.js +208 -0
  95. package/src/asset_debugger/util/scene/lighting-manager.js +690 -0
  96. package/src/asset_debugger/util/scene/threejs-model-manager.js +437 -0
  97. package/src/asset_debugger/util/scene/threejs-preview-manager.js +207 -0
  98. package/src/asset_debugger/util/scene/threejs-preview-setup.js +478 -0
  99. package/src/asset_debugger/util/scene/threejs-scene-controller.js +286 -0
  100. package/src/asset_debugger/util/scene/ui-manager.js +107 -0
  101. package/src/asset_debugger/util/state/animation-state.js +128 -0
  102. package/src/asset_debugger/util/state/css3d-state.js +83 -0
  103. package/src/asset_debugger/util/state/glb-preview-state.js +31 -0
  104. package/src/asset_debugger/util/state/log-util.js +197 -0
  105. package/src/asset_debugger/util/state/scene-state.js +452 -0
  106. package/src/asset_debugger/util/state/threejs-state.js +54 -0
  107. package/src/asset_debugger/util/workers/lighting-worker.js +61 -0
  108. package/src/asset_debugger/util/workers/model-worker.js +109 -0
  109. package/src/asset_debugger/util/workers/texture-worker.js +54 -0
  110. package/src/asset_debugger/util/workers/worker-manager.js +212 -0
  111. package/src/asset_debugger/widgets/mesh-info-widget.js +280 -0
  112. package/src/index.html +261 -0
  113. package/src/index.js +8 -0
  114. package/vite.config.js +66 -0
@@ -0,0 +1,199 @@
1
+ import * as THREE from 'three';
2
+ import { injectUnifiedAnimationDetectionScript } from '../../data/animation-classifier';
3
+ import { loadHtml2Canvas } from '../../loaders/html2canvas-loader';
4
+
5
+ const HTML2CANVAS_DEBUG_FLAG = false;
6
+
7
+ /**
8
+ * Create a texture from the iframe content using html2canvas
9
+ * @param {HTMLIFrameElement} iframe - The iframe containing the HTML content
10
+ * @returns {Promise<THREE.Texture>} A promise that resolves to a Three.js texture
11
+ */
12
+ export async function createTextureFromIframe(iframe) {
13
+ return new Promise(async (resolve, reject) => {
14
+ try {
15
+
16
+ // Make sure we can access the iframe
17
+ if (!iframe || !document.body.contains(iframe)) {
18
+ reject(new Error('Iframe not found in DOM or removed'));
19
+ return;
20
+ }
21
+
22
+ // Ensure html2canvas is loaded
23
+ const html2canvasAvailable = await loadHtml2Canvas();
24
+ if (!html2canvasAvailable) {
25
+ reject(new Error('html2canvas library not available - cannot render HTML to texture'));
26
+ return;
27
+ }
28
+
29
+ // Create a simple delay function
30
+ const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
31
+
32
+ // Give more time for the iframe to fully load - increase from 300ms to 1000ms
33
+ delay(1000).then(async () => {
34
+ try {
35
+ // Check if we can access the iframe content safely
36
+ if (!iframe.contentDocument || !iframe.contentWindow) {
37
+ reject(new Error('Cannot access iframe content - security restriction or iframe removed'));
38
+ return;
39
+ }
40
+ // Make sure the body is fully loaded
41
+ if (!iframe.contentDocument.body) {
42
+ reject(new Error('Iframe body not available - iframe content failed to load'));
43
+ return;
44
+ }
45
+
46
+ // Inject animation detection script if not already injected
47
+ if (!iframe.contentWindow.__animationDetection) {
48
+ injectUnifiedAnimationDetectionScript(iframe, 'image2texture');
49
+ }
50
+
51
+ // Ensure iframe is visible for capture (even if off-screen)
52
+ const originalStyle = iframe.style.cssText;
53
+ iframe.style.position = 'absolute';
54
+ iframe.style.left = '-9999px';
55
+ iframe.style.visibility = 'visible';
56
+ iframe.style.opacity = '1';
57
+
58
+ // Apply a frame to the content to make it more visible on the texture
59
+ const styleElement = iframe.contentDocument.createElement('style');
60
+
61
+ // Never show borders when capturing for long exposure
62
+ const shouldShowBorders = window.showPreviewBorders;
63
+
64
+ styleElement.textContent = `
65
+ body {
66
+ margin: 0;
67
+ padding: 15px;
68
+ ${shouldShowBorders ? 'border: 5px solid #3498db;' : ''}
69
+ box-sizing: border-box;
70
+ background-color: white !important; /* Force white background */
71
+ font-size: 20px !important; /* Increase base font size for better readability */
72
+ min-height: 100vh;
73
+ width: 100%;
74
+ overflow: auto;
75
+ }
76
+
77
+ /* Add a subtle grid to help with alignment */
78
+ body::before {
79
+ content: "";
80
+ position: absolute;
81
+ top: 0;
82
+ left: 0;
83
+ right: 0;
84
+ bottom: 0;
85
+ background-image: ${shouldShowBorders ?
86
+ 'linear-gradient(rgba(0,0,0,0.03) 1px, transparent 1px), linear-gradient(90deg, rgba(0,0,0,0.03) 1px, transparent 1px)' :
87
+ 'none'};
88
+ background-size: 20px 20px;
89
+ pointer-events: none;
90
+ z-index: -1;
91
+ }
92
+
93
+ /* Increase font size of common elements for better readability */
94
+ h1, h2, h3, h4, h5, h6 {
95
+ font-size: 1.5em !important;
96
+ }
97
+
98
+ p, div, span, a, li, td, th {
99
+ font-size: 1.2em !important;
100
+ }
101
+
102
+ /* Make sure buttons and inputs are readable */
103
+ button, input, select, textarea {
104
+ font-size: 1.2em !important;
105
+ padding: 5px !important;
106
+ background-color: #fff;
107
+ }
108
+ `;
109
+
110
+ try {
111
+ // Add the style element temporarily for rendering
112
+ iframe.contentDocument.head.appendChild(styleElement);
113
+
114
+ // Force a layout/repaint in the iframe
115
+ iframe.contentWindow.scrollTo(0, 0);
116
+
117
+ // Wait a bit longer for styles to apply
118
+ await delay(200);
119
+
120
+ // Use html2canvas to capture the iframe content
121
+ const targetElement = iframe.contentDocument.body;
122
+
123
+ try {
124
+ // Increase scale factor for better quality
125
+ const canvas = await window.html2canvas(targetElement, {
126
+ backgroundColor: '#FFFFFF', // Explicitly set to white to match HTML default
127
+ scale: 8, // Significantly increased from 4 for higher resolution textures
128
+ logging: HTML2CANVAS_DEBUG_FLAG, // Enable logging
129
+ allowTaint: true,
130
+ useCORS: true,
131
+ foreignObjectRendering: true
132
+ });
133
+
134
+ // Remove the temporary style element after rendering
135
+ if (styleElement && styleElement.parentNode) {
136
+ styleElement.parentNode.removeChild(styleElement);
137
+ }
138
+
139
+ // Restore original iframe style
140
+ iframe.style.cssText = originalStyle;
141
+
142
+ // Check if canvas has content (not a blank capture)
143
+ const ctx = canvas.getContext('2d');
144
+ const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
145
+ const data = imageData.data;
146
+
147
+ // Check if the canvas is entirely white/transparent
148
+ let hasContent = false;
149
+ let nonWhitePixelCount = 0;
150
+ for (let i = 0; i < data.length; i += 4) {
151
+ // If any pixel has non-white color or non-zero alpha, there's content
152
+ if (data[i] < 255 || data[i+1] < 255 || data[i+2] < 255 || data[i+3] > 0) {
153
+ hasContent = true;
154
+ nonWhitePixelCount++;
155
+ if (nonWhitePixelCount > 100) break; // No need to count all of them
156
+ }
157
+ }
158
+
159
+ if (!hasContent) {
160
+ reject(new Error('Canvas capture appears to be blank - no content was rendered'));
161
+ return;
162
+ }
163
+
164
+ // Create texture from the canvas
165
+ const texture = new THREE.CanvasTexture(canvas);
166
+
167
+ // Improve texture quality settings
168
+ texture.anisotropy = 16; // Doubled from 8 for better quality at angles
169
+ texture.minFilter = THREE.LinearFilter;
170
+ texture.magFilter = THREE.LinearFilter;
171
+ texture.generateMipmaps = false;
172
+ texture.needsUpdate = true;
173
+
174
+ resolve(texture);
175
+ } catch (error) {
176
+
177
+ // Remove the temporary style element if it exists
178
+ if (styleElement && styleElement.parentNode) {
179
+ styleElement.parentNode.removeChild(styleElement);
180
+ }
181
+
182
+ // Restore original iframe style
183
+ iframe.style.cssText = originalStyle;
184
+
185
+ // Properly reject with error
186
+ reject(new Error(`Failed to capture HTML content with html2canvas: ${error.message}`));
187
+ }
188
+ } catch (error) {
189
+ reject(new Error(`Error creating texture from iframe: ${error.message}`));
190
+ }
191
+ } catch (error) {
192
+ reject(new Error(`Error in iframe content processing: ${error.message}`));
193
+ }
194
+ });
195
+ } catch (error) {
196
+ reject(new Error(`Failed to create texture from iframe: ${error.message}`));
197
+ }
198
+ });
199
+ }
@@ -0,0 +1,461 @@
1
+ import * as THREE from 'three';
2
+ import {
3
+ animationDetected,
4
+ animationDetectionSensitivity,
5
+ animationDuration,
6
+ animationStartDetected,
7
+ finalProgressAnimation,
8
+ finalProgressDuration,
9
+ finalProgressStartTime,
10
+ isAnimationFinite,
11
+ isPreviewActive,
12
+ preRenderedFrames,
13
+ preRenderingInProgress,
14
+ preRenderMaxDuration,
15
+ setAnimationDuration,
16
+ setFinalProgressAnimation,
17
+ setFinalProgressStartTime,
18
+ setIsAnimationFinite,
19
+ setIsPreviewAnimationPaused,
20
+ setPreRenderedFrames,
21
+ setPreRenderingInProgress,
22
+ } from "../../state/animation-state";
23
+ import { showStatus } from '../../../modals/html-editor-modal/html-editor-modal';
24
+ import { createMeshInfoPanel } from '../../../widgets/mesh-info-widget';
25
+ import { logAnimationAnalysisReport } from '../../state/log-util';
26
+ import { startPlayback, updateMeshTexture } from '../playback/animation-playback-controller';
27
+ import { createTextureFromIframe } from './iframe2texture-render-controller';
28
+
29
+ /**
30
+ * Start pre-rendering animation frames
31
+ * @param {HTMLIFrameElement} iframe - The iframe containing the HTML content
32
+ * @param {Function} callback - Function to call when pre-rendering is complete
33
+ * @param {HTMLElement} progressBar - Optional progress bar element to update
34
+ * @param {CustomTextureSettings} settings - Optional settings object for texture configuration
35
+ * @param {THREE.Mesh} previewPlane - The mesh to apply textures to
36
+ */
37
+ export function startImage2TexturePreRendering(iframe, callback, progressBar = null, settings = null, previewPlane = null) {
38
+ if (!iframe) {
39
+ console.error('No iframe provided for pre-rendering');
40
+ if (callback) callback();
41
+ return;
42
+ }
43
+
44
+ // Reset state
45
+ setPreRenderingInProgress(true);
46
+ setPreRenderedFrames([]);
47
+
48
+ // Set the start time
49
+ const preRenderStartTime = Date.now();
50
+
51
+ // Track progress metrics
52
+ let totalFramesEstimate = 120; // Initial estimate
53
+ let lastProgressUpdate = 0;
54
+ let progressUpdateInterval = 100; // Update progress every 100ms
55
+ let maxProgressBeforeFinalAnimation = 92; // Cap progress at this value until final animation
56
+ setFinalProgressAnimation(false);
57
+ setFinalProgressStartTime(0);
58
+
59
+ // Track animation detection variables
60
+ let loopDetected = false;
61
+ let endDetected = false;
62
+ let analysisMetrics = {};
63
+
64
+ // Get animation settings from passed settings object instead of DOM
65
+ let isLongExposureMode = false;
66
+ let playbackSpeed = 1.0;
67
+
68
+ if (settings) {
69
+ // Use settings parameters instead of DOM elements
70
+ isLongExposureMode = settings.isLongExposureMode;
71
+ playbackSpeed = settings.playbackSpeed || 1.0;
72
+ } else {
73
+ // Fallback to DOM access if settings not provided (for backward compatibility)
74
+ const animationTypeSelect = document.getElementById('html-animation-type');
75
+ isLongExposureMode = animationTypeSelect && animationTypeSelect.value === 'longExposure';
76
+ }
77
+
78
+ // Set flag if we're capturing for long exposure
79
+ if (isLongExposureMode) {
80
+ setCapturingForLongExposure(true);
81
+
82
+ // Temporarily disable borders during capture
83
+ const originalBorderSetting = window.showPreviewBorders;
84
+ window.showPreviewBorders = false;
85
+ console.log('Borders temporarily disabled for long exposure capture');
86
+
87
+ // Store original setting to restore later
88
+ window._originalBorderSetting = originalBorderSetting;
89
+ }
90
+
91
+ // Function to update progress bar
92
+ const updateProgress = (percent) => {
93
+ if (progressBar) {
94
+ // Ensure progress never exceeds maxProgressBeforeFinalAnimation unless in final animation
95
+ if (!finalProgressAnimation && percent > maxProgressBeforeFinalAnimation) {
96
+ percent = maxProgressBeforeFinalAnimation;
97
+ }
98
+ progressBar.style.width = `${percent}%`;
99
+ }
100
+ };
101
+
102
+ // Function to create the long exposure texture and apply it
103
+ const createAndApplyLongExposure = () => {
104
+ if (preRenderedFrames.length > 0) {
105
+ // Use the playbackSpeed from settings instead of DOM
106
+ const longExposureTexture = createLongExposureTexture(preRenderedFrames, playbackSpeed);
107
+
108
+ // Update the mesh with the long exposure texture
109
+ if (previewPlane) {
110
+ updateMeshTexture(longExposureTexture, previewPlane);
111
+ }
112
+
113
+ // Show a message about the long exposure
114
+ showStatus(`Long exposure created from ${preRenderedFrames.length} frames`, 'success');
115
+
116
+ // Pause animation since we just want to display the static image
117
+ setIsPreviewAnimationPaused(true);
118
+ }
119
+ };
120
+
121
+ // Function to start final progress animation
122
+ const startFinalProgressAnimation = () => {
123
+ if (finalProgressAnimation) return; // Already animating
124
+
125
+ setFinalProgressAnimation(true);
126
+ setFinalProgressStartTime(Date.now());
127
+
128
+ // Start the animation loop
129
+ animateFinalProgress();
130
+ };
131
+
132
+ // Function to animate progress to 100%
133
+ const animateFinalProgress = () => {
134
+ const now = Date.now();
135
+ const elapsed = now - finalProgressStartTime;
136
+
137
+ if (elapsed >= finalProgressDuration) {
138
+ // Animation complete, set to 100%
139
+ updateProgress(100);
140
+
141
+ // Log animation analysis report
142
+ logAnimationAnalysisReport('Image2Texture', {
143
+ frameCount: preRenderedFrames.length,
144
+ duration: animationDuration,
145
+ isFinite: isAnimationFinite,
146
+ loopDetected,
147
+ endDetected,
148
+ analysisTime: now - preRenderStartTime,
149
+ metrics: analysisMetrics
150
+ });
151
+
152
+ // Hide loading overlay with fade out
153
+ const loadingOverlay = document.getElementById('pre-rendering-overlay');
154
+ if (loadingOverlay) {
155
+ loadingOverlay.style.transition = 'opacity 0.5s ease';
156
+ loadingOverlay.style.opacity = '0';
157
+
158
+ // Remove after fade out
159
+ setTimeout(() => {
160
+ if (loadingOverlay.parentNode) {
161
+ loadingOverlay.parentNode.removeChild(loadingOverlay);
162
+ }
163
+
164
+ // Now create the info panel after pre-rendering is complete
165
+ const canvasContainer = document.querySelector('#html-preview-content');
166
+ if (canvasContainer) {
167
+ const modal = document.getElementById('html-editor-modal');
168
+ const currentMeshId = parseInt(modal.dataset.meshId);
169
+ createMeshInfoPanel(canvasContainer, currentMeshId);
170
+ }
171
+
172
+ // For long exposure, create the static image now that all frames are captured
173
+ if (isLongExposureMode && preRenderedFrames.length > 0) {
174
+ // Use playbackSpeed from settings instead of DOM
175
+ const longExposureTexture = createLongExposureTexture(preRenderedFrames, playbackSpeed);
176
+
177
+ // Update the mesh with the long exposure texture
178
+ if (previewPlane) {
179
+ updateMeshTexture(longExposureTexture, previewPlane);
180
+ }
181
+
182
+ // Show a message about the long exposure
183
+ showStatus(`Long exposure created from ${preRenderedFrames.length} frames`, 'success');
184
+
185
+ // Pause animation since we just want to display the static image
186
+ setIsPreviewAnimationPaused(true);
187
+ } else {
188
+ // Reset animation start time to now
189
+ startPlayback();
190
+
191
+ // Start the animation
192
+ setIsPreviewAnimationPaused(false);
193
+
194
+ // Show a message that playback is starting
195
+ showStatus(`Animation playback starting at ${playbackSpeed}x speed`, 'success');
196
+ }
197
+ }, 500);
198
+
199
+ // Don't continue the animation
200
+ return;
201
+ }
202
+ } else {
203
+ // Calculate progress based on easing function
204
+ const progress = easeOutCubic(elapsed / finalProgressDuration);
205
+ const currentProgress = maxProgressBeforeFinalAnimation + (100 - maxProgressBeforeFinalAnimation) * progress;
206
+ updateProgress(currentProgress);
207
+
208
+ // Update loading text
209
+ const progressText = document.getElementById('loading-progress-text');
210
+ if (progressText) {
211
+ if (isLongExposureMode) {
212
+ progressText.textContent = 'Creating long exposure...';
213
+ } else {
214
+ progressText.textContent = 'Finalizing animation...';
215
+ }
216
+ }
217
+
218
+ // Continue animation
219
+ requestAnimationFrame(animateFinalProgress);
220
+ }
221
+ };
222
+
223
+ // Easing function for smooth animation
224
+ const easeOutCubic = (x) => {
225
+ return 1 - Math.pow(1 - x, 3);
226
+ };
227
+
228
+ // Create high-quality texture from iframe for better visuals
229
+ const createHighQualityTexture = async (iframe) => {
230
+ try {
231
+ const texture = await createTextureFromIframe(iframe);
232
+
233
+ // Apply higher quality settings
234
+ texture.anisotropy = 16; // Increased from 8 for sharper textures
235
+ texture.minFilter = THREE.LinearFilter;
236
+ texture.magFilter = THREE.LinearFilter;
237
+ texture.generateMipmaps = false;
238
+ texture.needsUpdate = true;
239
+
240
+ return texture;
241
+ } catch (error) {
242
+ console.error('Error creating high-quality texture:', error);
243
+ throw error;
244
+ }
245
+ };
246
+
247
+ // Function to capture frames until animation completes or times out
248
+ const captureFrames = async () => {
249
+ if (!isPreviewActive || !preRenderingInProgress) {
250
+ setPreRenderingInProgress(false);
251
+ startFinalProgressAnimation();
252
+ return;
253
+ }
254
+
255
+ const now = Date.now();
256
+
257
+ // Update progress based on more accurate metrics
258
+ if (now - lastProgressUpdate > progressUpdateInterval) {
259
+ lastProgressUpdate = now;
260
+
261
+ // Calculate elapsed time percentage
262
+ const elapsedTime = now - preRenderStartTime;
263
+ const timeProgress = Math.min(90, (elapsedTime / preRenderMaxDuration) * 100);
264
+
265
+ // Calculate frame-based progress
266
+ let frameProgress = 0;
267
+ if (animationDetected) {
268
+ // If we've detected animation, adjust the total frames estimate
269
+ if (preRenderedFrames.length > totalFramesEstimate * 0.5) {
270
+ // If we've captured more than half our estimate, update the estimate
271
+ totalFramesEstimate = Math.max(totalFramesEstimate, Math.ceil(preRenderedFrames.length * 1.2));
272
+ }
273
+ frameProgress = Math.min(90, (preRenderedFrames.length / totalFramesEstimate) * 100);
274
+ } else {
275
+ // If no animation detected, use time-based progress
276
+ frameProgress = timeProgress;
277
+ }
278
+
279
+ // Use a weighted combination of time and frame progress
280
+ // Cap at maxProgressBeforeFinalAnimation to leave room for final animation
281
+ const combinedProgress = Math.min(
282
+ maxProgressBeforeFinalAnimation,
283
+ (timeProgress * 0.3) + (frameProgress * 0.7)
284
+ );
285
+ updateProgress(combinedProgress);
286
+
287
+ // Update the loading text to show more information
288
+ const progressText = document.getElementById('loading-progress-text');
289
+ if (progressText) {
290
+ progressText.textContent = `Pre-rendering animation... ${preRenderedFrames.length} frames captured`;
291
+ }
292
+ }
293
+
294
+ // Check if we've detected a loop and have enough frames
295
+ // Use the improved detection logic with higher sensitivity for pre-rendering
296
+ const sensitivity = animationDetectionSensitivity + 0.1; // Slight boost during pre-rendering
297
+ const isLoopDetected = preRenderedFrames.length > 20 &&
298
+ detectAnimationLoop(preRenderedFrames, sensitivity);
299
+
300
+ if (isLoopDetected && preRenderedFrames.length > 20) {
301
+ console.log('Animation loop detected after ' + preRenderedFrames.length + ' frames');
302
+ setPreRenderingInProgress(false);
303
+ setIsAnimationFinite(true);
304
+ setAnimationDuration(preRenderedFrames[preRenderedFrames.length - 1].timestamp - preRenderedFrames[0].timestamp);
305
+
306
+ // Update analysis metrics
307
+ loopDetected = true;
308
+
309
+ // Show success message
310
+ if (isLongExposureMode) {
311
+ showStatus(`Animation loop detected, creating long exposure from ${preRenderedFrames.length} frames`, 'info');
312
+ } else {
313
+ showStatus(`Animation loop detected (${(animationDuration/1000).toFixed(1)}s), ${preRenderedFrames.length} frames captured`, 'success');
314
+ }
315
+
316
+ // Start final progress animation instead of immediately calling callback
317
+ startFinalProgressAnimation();
318
+ return;
319
+ }
320
+
321
+ // Check for animation end using the new analysis function
322
+ if (preRenderedFrames.length > 20) { // Reduced from 30
323
+ // Calculate the latest frame hash
324
+ const latestFrameHash = preRenderedFrames[preRenderedFrames.length - 1].hash;
325
+
326
+ // Analyze frames to detect animation end with higher sensitivity
327
+ const analysisResult = analyzeAnimationFrames(
328
+ preRenderedFrames.slice(0, -1), // All frames except the latest
329
+ latestFrameHash,
330
+ sensitivity
331
+ );
332
+
333
+ // Store analysis metrics
334
+ analysisMetrics = analysisResult.metrics;
335
+
336
+ // If end detected and we have enough frames, stop pre-rendering
337
+ if (analysisResult.endDetected && preRenderedFrames.length > 20) {
338
+ console.log('Animation end detected during pre-rendering after ' + preRenderedFrames.length + ' frames');
339
+ console.log('Detection metrics:', analysisResult.metrics);
340
+ setPreRenderingInProgress(false);
341
+ setIsAnimationFinite(true);
342
+ setAnimationDuration(preRenderedFrames[preRenderedFrames.length - 1].timestamp - preRenderedFrames[0].timestamp);
343
+
344
+ // Update analysis metrics
345
+ endDetected = true;
346
+
347
+ // Show success message
348
+ showStatus(`Animation end detected (${(animationDuration/1000).toFixed(1)}s), ${preRenderedFrames.length} frames captured`, 'success');
349
+
350
+ // Start final progress animation instead of immediately calling callback
351
+ startFinalProgressAnimation();
352
+ return;
353
+ }
354
+ }
355
+
356
+ // Check if we've exceeded the maximum pre-rendering time
357
+ if (now - preRenderStartTime > preRenderMaxDuration) {
358
+ console.log('Pre-rendering time limit reached after ' + preRenderMaxDuration + 'ms');
359
+ setPreRenderingInProgress(false);
360
+
361
+ if (loopDetected) {
362
+ setIsAnimationFinite(true);
363
+ setAnimationDuration(preRenderedFrames[preRenderedFrames.length - 1].timestamp - preRenderedFrames[0].timestamp);
364
+ console.log(`Animation loop detected, duration: ${animationDuration}ms, ${preRenderedFrames.length} frames captured`);
365
+ showStatus(`Animation loop detected (${(animationDuration/1000).toFixed(1)}s), ${preRenderedFrames.length} frames captured`, 'success');
366
+ } else if (animationStartDetected) {
367
+ setIsAnimationFinite(true);
368
+ setAnimationDuration(animationEndTime - animationStartTime);
369
+ console.log(`Animation start/end detected, duration: ${animationDuration}ms, ${preRenderedFrames.length} frames captured`);
370
+ showStatus(`Animation start/end detected (${(animationDuration/1000).toFixed(1)}s), ${preRenderedFrames.length} frames captured`, 'success');
371
+
372
+ // Update analysis metrics
373
+ endDetected = true;
374
+ } else {
375
+ console.log(`No animation loop detected, ${preRenderedFrames.length} frames captured`);
376
+ showStatus(`Animation appears infinite, ${preRenderedFrames.length} frames captured for playback`, 'info');
377
+ }
378
+
379
+ // Start final progress animation instead of immediately calling callback
380
+ startFinalProgressAnimation();
381
+ return;
382
+ }
383
+
384
+ try {
385
+ // Capture a high-quality frame
386
+ const texture = await createHighQualityTexture(iframe);
387
+
388
+ // Calculate a hash of the texture to detect changes
389
+ const frameHash = calculateTextureHash(texture);
390
+
391
+ // Add frame to pre-rendered frames
392
+ preRenderedFrames.push({
393
+ texture: texture,
394
+ timestamp: now,
395
+ hash: frameHash
396
+ });
397
+
398
+ // For long exposure mode, if we have enough frames, create the texture immediately
399
+ // This prevents showing the first frame before the long exposure
400
+ if (isLongExposureMode && preRenderedFrames.length >= 15) {
401
+ // Create the long exposure immediately
402
+ createAndApplyLongExposure();
403
+ }
404
+
405
+ // Use a shorter delay for more frequent frame capture to increase smoothness
406
+ setTimeout(() => {
407
+ requestAnimationFrame(captureFrames);
408
+ }, 5); // 5ms delay allows for more frames to be captured in the same time
409
+ } catch (error) {
410
+ console.error('Error during pre-rendering:', error);
411
+ setPreRenderingInProgress(false);
412
+
413
+ // Start final progress animation instead of immediately calling callback
414
+ startFinalProgressAnimation();
415
+ }
416
+ };
417
+
418
+ // Start capturing frames
419
+ captureFrames();
420
+
421
+ // Store callback to be called after final animation completes
422
+ window._preRenderCallback = callback;
423
+ }
424
+
425
+ /**
426
+ * Calculate a simple hash of a texture to detect changes between frames
427
+ * @param {THREE.Texture} texture - The texture to hash
428
+ * @returns {string} A simple hash of the texture
429
+ */
430
+ function calculateTextureHash(texture) {
431
+ if (!texture || !texture.image) return '';
432
+
433
+ try {
434
+ // Create a small canvas to sample the texture
435
+ const canvas = document.createElement('canvas');
436
+ const size = 16; // Small sample size for performance
437
+ canvas.width = size;
438
+ canvas.height = size;
439
+ const ctx = canvas.getContext('2d');
440
+
441
+ // Draw the texture to the canvas
442
+ ctx.drawImage(texture.image, 0, 0, size, size);
443
+
444
+ // Get image data
445
+ const imageData = ctx.getImageData(0, 0, size, size).data;
446
+
447
+ // Sample pixels at regular intervals
448
+ const samples = [];
449
+ const step = 4 * 4; // Sample every 4th pixel (RGBA)
450
+ for (let i = 0; i < imageData.length; i += step) {
451
+ // Use just the RGB values (skip alpha)
452
+ samples.push(imageData[i], imageData[i+1], imageData[i+2]);
453
+ }
454
+
455
+ // Create a simple hash from the samples
456
+ return samples.join(',');
457
+ } catch (e) {
458
+ console.error('Error calculating texture hash:', e);
459
+ return '';
460
+ }
461
+ }