pni 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/add-three-app.d.ts +6 -0
- package/dist/add-three-app.js +111 -0
- package/dist/app.d.ts +11 -0
- package/dist/app.js +143 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +71 -0
- package/dist/components/FeatureSelector.d.ts +21 -0
- package/dist/components/FeatureSelector.js +175 -0
- package/dist/components/ProgressIndicator.d.ts +7 -0
- package/dist/components/ProgressIndicator.js +46 -0
- package/dist/components/Summary.d.ts +8 -0
- package/dist/components/Summary.js +51 -0
- package/dist/components/WelcomeHeader.d.ts +2 -0
- package/dist/components/WelcomeHeader.js +8 -0
- package/dist/template_code/three/README.md +146 -0
- package/dist/template_code/three/World.js +133 -0
- package/dist/template_code/three/camera.js +30 -0
- package/dist/template_code/three/components/GlobeSphere.js +608 -0
- package/dist/template_code/three/components/cube.js +27 -0
- package/dist/template_code/three/components/lights.js +16 -0
- package/dist/template_code/three/components/sphere.js +26 -0
- package/dist/template_code/three/components/torus.js +25 -0
- package/dist/template_code/three/scene.js +28 -0
- package/dist/template_code/three/systems/Loop.js +43 -0
- package/dist/template_code/three/systems/Resizer.js +26 -0
- package/dist/template_code/three/systems/controls.js +19 -0
- package/dist/template_code/three/systems/post-processing.js +50 -0
- package/dist/template_code/three/systems/renderer.js +17 -0
- package/dist/template_code/three/utils/deviceDetector.js +141 -0
- package/dist/template_code/three/utils/gltfLoader.js +14 -0
- package/dist/template_code/three/utils/loadKTX2Texture.js +42 -0
- package/dist/template_code/three/utils/textureLoader.js +21 -0
- package/dist/utils/add-three.d.ts +7 -0
- package/dist/utils/add-three.js +288 -0
- package/dist/utils/app-creation.d.ts +4 -0
- package/dist/utils/app-creation.js +35 -0
- package/dist/utils/config-generator.d.ts +6 -0
- package/dist/utils/config-generator.js +508 -0
- package/dist/utils/css-variables.d.ts +4 -0
- package/dist/utils/css-variables.js +316 -0
- package/dist/utils/dependencies.d.ts +11 -0
- package/dist/utils/dependencies.js +68 -0
- package/dist/utils/package-manager.d.ts +4 -0
- package/dist/utils/package-manager.js +56 -0
- package/dist/utils/project-detection.d.ts +2 -0
- package/dist/utils/project-detection.js +60 -0
- package/dist/utils/shadcn-setup.d.ts +2 -0
- package/dist/utils/shadcn-setup.js +46 -0
- package/package.json +81 -0
- package/readme.md +119 -0
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import {gsap} from 'gsap';
|
|
3
|
+
import camera from '../camera';
|
|
4
|
+
import CustomEase from 'gsap/CustomEase';
|
|
5
|
+
import {
|
|
6
|
+
isMobile,
|
|
7
|
+
isTablet,
|
|
8
|
+
initDeviceDetector,
|
|
9
|
+
disposeDeviceDetector,
|
|
10
|
+
} from '../utils/deviceDetector';
|
|
11
|
+
|
|
12
|
+
initDeviceDetector();
|
|
13
|
+
if (typeof window !== 'undefined') {
|
|
14
|
+
gsap.registerPlugin(CustomEase);
|
|
15
|
+
}
|
|
16
|
+
CustomEase.create('fav', '0.785, 0.135, 0.15, 0.86');
|
|
17
|
+
|
|
18
|
+
// Global texture cache - shared across all instances
|
|
19
|
+
const textureCache = new Map();
|
|
20
|
+
|
|
21
|
+
async function createGlobeSphere(
|
|
22
|
+
renderer,
|
|
23
|
+
scene,
|
|
24
|
+
controls,
|
|
25
|
+
onMeshClick = null,
|
|
26
|
+
content = [],
|
|
27
|
+
) {
|
|
28
|
+
console.log(isMobile(), isTablet());
|
|
29
|
+
|
|
30
|
+
const COUNT = 120;
|
|
31
|
+
const R = 1.25;
|
|
32
|
+
const Hgt = 0.2;
|
|
33
|
+
const Wth = Hgt * (9 / 11);
|
|
34
|
+
const phi = Math.PI * (3 - Math.sqrt(5));
|
|
35
|
+
|
|
36
|
+
const loader = new THREE.TextureLoader();
|
|
37
|
+
const group = new THREE.Group();
|
|
38
|
+
const meshes = []; // Store all meshes for raycaster
|
|
39
|
+
let textures = []; // Store textures for updates
|
|
40
|
+
let textureUrls = []; // Store texture URLs for cache checking
|
|
41
|
+
let currentContent = content; // Store current content
|
|
42
|
+
|
|
43
|
+
// Create a single shared plane geometry (reused for all meshes)
|
|
44
|
+
const sharedGeometry = new THREE.PlaneGeometry(Wth, Hgt);
|
|
45
|
+
|
|
46
|
+
// Function to configure texture properties
|
|
47
|
+
const configureTexture = tex => {
|
|
48
|
+
const ir = tex.image.width / tex.image.height;
|
|
49
|
+
const pr = Wth / Hgt;
|
|
50
|
+
let rx = 1,
|
|
51
|
+
ry = 1,
|
|
52
|
+
ox = 0,
|
|
53
|
+
oy = 0;
|
|
54
|
+
if (ir > pr) {
|
|
55
|
+
rx = pr / ir;
|
|
56
|
+
ox = (1 - rx) / 2;
|
|
57
|
+
} else {
|
|
58
|
+
ry = ir / pr;
|
|
59
|
+
oy = (1 - ry) / 2;
|
|
60
|
+
}
|
|
61
|
+
tex.wrapS = tex.wrapT = THREE.ClampToEdgeWrapping;
|
|
62
|
+
tex.anisotropy = renderer.capabilities.getMaxAnisotropy();
|
|
63
|
+
tex.repeat.set(rx, ry);
|
|
64
|
+
tex.offset.set(ox, oy);
|
|
65
|
+
tex.generateMipmaps = true;
|
|
66
|
+
tex.minFilter = THREE.LinearMipMapLinearFilter;
|
|
67
|
+
tex.magFilter = THREE.LinearFilter;
|
|
68
|
+
tex.colorSpace = THREE.SRGBColorSpace;
|
|
69
|
+
return tex;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// Function to load textures with caching
|
|
73
|
+
const loadTextures = async contentData => {
|
|
74
|
+
const imgUrls = contentData.map(item => item.img);
|
|
75
|
+
const loadedTextures = await Promise.all(
|
|
76
|
+
imgUrls.map(
|
|
77
|
+
url =>
|
|
78
|
+
new Promise(resolve => {
|
|
79
|
+
// Check cache first
|
|
80
|
+
if (textureCache.has(url)) {
|
|
81
|
+
const cachedTex = textureCache.get(url);
|
|
82
|
+
// Reuse cached texture directly (textures can be shared in Three.js)
|
|
83
|
+
resolve({texture: cachedTex, url});
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Load new texture if not in cache
|
|
88
|
+
loader.load(url, tex => {
|
|
89
|
+
const configuredTex = configureTexture(tex);
|
|
90
|
+
// Store in cache for future use
|
|
91
|
+
textureCache.set(url, configuredTex);
|
|
92
|
+
resolve({texture: configuredTex, url});
|
|
93
|
+
});
|
|
94
|
+
}),
|
|
95
|
+
),
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
// Return textures and URLs separately
|
|
99
|
+
return {
|
|
100
|
+
textures: loadedTextures.map(item => item.texture),
|
|
101
|
+
urls: loadedTextures.map(item => item.url),
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// Load all textures
|
|
106
|
+
const initialTextures = await loadTextures(content);
|
|
107
|
+
textures = initialTextures.textures;
|
|
108
|
+
textureUrls = initialTextures.urls;
|
|
109
|
+
|
|
110
|
+
// Create meshes in Fibonacci sphere pattern
|
|
111
|
+
for (let i = 0; i < COUNT; i++) {
|
|
112
|
+
const v = (i + 0.5) / COUNT;
|
|
113
|
+
const th = phi * i;
|
|
114
|
+
const z = 1 - 2 * v;
|
|
115
|
+
const r0 = Math.sqrt(1 - z * z);
|
|
116
|
+
const fx = Math.cos(th) * r0 * R;
|
|
117
|
+
const fy = z * R;
|
|
118
|
+
const fz = Math.sin(th) * r0 * R;
|
|
119
|
+
|
|
120
|
+
const contentIndex = i % content.length;
|
|
121
|
+
const mat = new THREE.MeshBasicMaterial({
|
|
122
|
+
map: textures[contentIndex],
|
|
123
|
+
opacity: 0, // Start invisible for animation
|
|
124
|
+
transparent: true,
|
|
125
|
+
side: THREE.DoubleSide,
|
|
126
|
+
depthWrite: false,
|
|
127
|
+
});
|
|
128
|
+
const mesh = new THREE.Mesh(sharedGeometry, mat);
|
|
129
|
+
mesh.position.set(fx, fy, fz);
|
|
130
|
+
mesh.lookAt(new THREE.Vector3(fx * 2, fy * 2, fz * 2));
|
|
131
|
+
|
|
132
|
+
// Set initial scale to 0 for animation
|
|
133
|
+
mesh.scale.set(0, 0, 0);
|
|
134
|
+
|
|
135
|
+
// Store content data on mesh for easy access
|
|
136
|
+
mesh.userData.contentIndex = contentIndex;
|
|
137
|
+
mesh.userData.content = content[contentIndex];
|
|
138
|
+
|
|
139
|
+
group.add(mesh);
|
|
140
|
+
meshes.push(mesh); // Store mesh reference
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Expose entrance animation so callers can start it later
|
|
144
|
+
// (e.g. only after the page transition has fully completed).
|
|
145
|
+
let entrancePlayed = false;
|
|
146
|
+
group.userData.playEntranceAnimation = () => {
|
|
147
|
+
if (entrancePlayed) return;
|
|
148
|
+
entrancePlayed = true;
|
|
149
|
+
|
|
150
|
+
meshes.forEach((mesh, i) => {
|
|
151
|
+
gsap.to(mesh.scale, {
|
|
152
|
+
x: 1,
|
|
153
|
+
y: 1,
|
|
154
|
+
z: 1,
|
|
155
|
+
duration: 1.2,
|
|
156
|
+
delay: i * 0.01,
|
|
157
|
+
ease: 'power2.out',
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
gsap.to(mesh.material, {
|
|
161
|
+
opacity: 1,
|
|
162
|
+
duration: 0.8,
|
|
163
|
+
delay: i * 0.01,
|
|
164
|
+
ease: 'power2.inOut',
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// Expose exit animation (reverse of entrance)
|
|
170
|
+
group.userData.playExitAnimation = () => {
|
|
171
|
+
return new Promise(resolve => {
|
|
172
|
+
// Reset entrance played flag so it can be replayed
|
|
173
|
+
entrancePlayed = false;
|
|
174
|
+
|
|
175
|
+
// Animate in reverse order for visual effect
|
|
176
|
+
meshes.forEach((mesh, i) => {
|
|
177
|
+
gsap.to(mesh.scale, {
|
|
178
|
+
x: 0,
|
|
179
|
+
y: 0,
|
|
180
|
+
z: 0,
|
|
181
|
+
duration: 0.8,
|
|
182
|
+
delay: i * 0.005,
|
|
183
|
+
ease: 'power2.in',
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
gsap.to(mesh.material, {
|
|
187
|
+
opacity: 0,
|
|
188
|
+
duration: 0.6,
|
|
189
|
+
delay: i * 0.005,
|
|
190
|
+
ease: 'power2.inOut',
|
|
191
|
+
onComplete: i === meshes.length - 1 ? resolve : undefined,
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
// Set up raycaster and click handler if renderer and scene are provided
|
|
198
|
+
if (renderer && scene) {
|
|
199
|
+
const raycaster = new THREE.Raycaster();
|
|
200
|
+
const mouse = new THREE.Vector2();
|
|
201
|
+
|
|
202
|
+
// Store original camera position and rotation
|
|
203
|
+
const originalCameraPosition = camera.position.clone();
|
|
204
|
+
const originalCameraQuaternion = camera.quaternion.clone();
|
|
205
|
+
let isAtOriginalPosition = true;
|
|
206
|
+
let currentAnimation = null;
|
|
207
|
+
let selectedMesh = null; // Track currently selected mesh
|
|
208
|
+
let opacityAnimation = null; // Track opacity animation
|
|
209
|
+
|
|
210
|
+
// Animation objects for GSAP to animate
|
|
211
|
+
const cameraPos = {
|
|
212
|
+
x: camera.position.x,
|
|
213
|
+
y: camera.position.y,
|
|
214
|
+
z: camera.position.z,
|
|
215
|
+
};
|
|
216
|
+
const cameraProgress = {t: 0}; // Progress for quaternion slerp
|
|
217
|
+
const controlsTarget = controls
|
|
218
|
+
? {x: controls.target.x, y: controls.target.y, z: controls.target.z}
|
|
219
|
+
: null;
|
|
220
|
+
|
|
221
|
+
// Store quaternions for slerp
|
|
222
|
+
let startQuaternion = new THREE.Quaternion();
|
|
223
|
+
let endQuaternion = new THREE.Quaternion();
|
|
224
|
+
|
|
225
|
+
// Animation function using GSAP
|
|
226
|
+
function animateCamera(
|
|
227
|
+
targetPosition,
|
|
228
|
+
targetLookAt,
|
|
229
|
+
duration = 1,
|
|
230
|
+
applyOffset = true,
|
|
231
|
+
normalVector = null,
|
|
232
|
+
ease = 'power3.inOut',
|
|
233
|
+
) {
|
|
234
|
+
// Kill any existing animation
|
|
235
|
+
if (currentAnimation) {
|
|
236
|
+
currentAnimation.kill();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
let finalPosition;
|
|
240
|
+
let finalQuaternion;
|
|
241
|
+
|
|
242
|
+
if (applyOffset && normalVector) {
|
|
243
|
+
// Position camera along the normal vector, offset from the mesh
|
|
244
|
+
const zOffset = isTablet() ? 0.4 : 0.5;
|
|
245
|
+
finalPosition = targetPosition
|
|
246
|
+
.clone()
|
|
247
|
+
.add(normalVector.clone().multiplyScalar(zOffset));
|
|
248
|
+
|
|
249
|
+
// Calculate quaternion to make camera look at the mesh
|
|
250
|
+
const up = new THREE.Vector3(0, 1, 0);
|
|
251
|
+
const lookAtMatrix = new THREE.Matrix4();
|
|
252
|
+
lookAtMatrix.lookAt(finalPosition, targetPosition, up);
|
|
253
|
+
finalQuaternion = new THREE.Quaternion().setFromRotationMatrix(
|
|
254
|
+
lookAtMatrix,
|
|
255
|
+
);
|
|
256
|
+
} else {
|
|
257
|
+
// Return to original position
|
|
258
|
+
finalPosition = targetPosition;
|
|
259
|
+
finalQuaternion = originalCameraQuaternion.clone();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Store quaternions for slerp
|
|
263
|
+
startQuaternion.copy(camera.quaternion);
|
|
264
|
+
endQuaternion.copy(finalQuaternion);
|
|
265
|
+
|
|
266
|
+
// Update animation objects with current values
|
|
267
|
+
cameraPos.x = camera.position.x;
|
|
268
|
+
cameraPos.y = camera.position.y;
|
|
269
|
+
cameraPos.z = camera.position.z;
|
|
270
|
+
cameraProgress.t = 0;
|
|
271
|
+
|
|
272
|
+
if (controlsTarget) {
|
|
273
|
+
controlsTarget.x = controls.target.x;
|
|
274
|
+
controlsTarget.y = controls.target.y;
|
|
275
|
+
controlsTarget.z = controls.target.z;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Create GSAP timeline
|
|
279
|
+
const tl = gsap.timeline({
|
|
280
|
+
onUpdate: () => {
|
|
281
|
+
// Update camera position
|
|
282
|
+
camera.position.set(cameraPos.x, cameraPos.y, cameraPos.z);
|
|
283
|
+
|
|
284
|
+
// Update camera quaternion using slerp
|
|
285
|
+
camera.quaternion.slerpQuaternions(
|
|
286
|
+
startQuaternion,
|
|
287
|
+
endQuaternion,
|
|
288
|
+
cameraProgress.t,
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
// Update controls target
|
|
292
|
+
if (controls && controlsTarget) {
|
|
293
|
+
controls.target.set(
|
|
294
|
+
controlsTarget.x,
|
|
295
|
+
controlsTarget.y,
|
|
296
|
+
controlsTarget.z,
|
|
297
|
+
);
|
|
298
|
+
controls.update();
|
|
299
|
+
}
|
|
300
|
+
},
|
|
301
|
+
onComplete: () => {
|
|
302
|
+
currentAnimation = null;
|
|
303
|
+
isAtOriginalPosition =
|
|
304
|
+
finalPosition.distanceTo(originalCameraPosition) < 0.1;
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// Animate camera position
|
|
309
|
+
tl.to(
|
|
310
|
+
cameraPos,
|
|
311
|
+
{
|
|
312
|
+
x: finalPosition.x,
|
|
313
|
+
y: finalPosition.y,
|
|
314
|
+
z: finalPosition.z,
|
|
315
|
+
duration,
|
|
316
|
+
ease: ease,
|
|
317
|
+
},
|
|
318
|
+
0,
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
// Animate quaternion progress (0 to 1)
|
|
322
|
+
tl.to(
|
|
323
|
+
cameraProgress,
|
|
324
|
+
{
|
|
325
|
+
t: 1,
|
|
326
|
+
duration,
|
|
327
|
+
ease: ease,
|
|
328
|
+
},
|
|
329
|
+
0,
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
// Animate controls target
|
|
333
|
+
if (controls && controlsTarget) {
|
|
334
|
+
tl.to(
|
|
335
|
+
controlsTarget,
|
|
336
|
+
{
|
|
337
|
+
x: targetLookAt.x,
|
|
338
|
+
y: targetLookAt.y,
|
|
339
|
+
z: targetLookAt.z,
|
|
340
|
+
duration,
|
|
341
|
+
ease: ease,
|
|
342
|
+
},
|
|
343
|
+
0,
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
currentAnimation = tl;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Function to animate mesh opacity
|
|
351
|
+
function animateMeshOpacity(targetMesh, opacity, duration = 0.5) {
|
|
352
|
+
// Kill any existing opacity animation
|
|
353
|
+
if (opacityAnimation) {
|
|
354
|
+
opacityAnimation.kill();
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Create opacity animation objects for GSAP
|
|
358
|
+
const opacityObjects = meshes.map(mesh => ({
|
|
359
|
+
opacity: mesh.material.opacity,
|
|
360
|
+
}));
|
|
361
|
+
|
|
362
|
+
// Create timeline for opacity animation
|
|
363
|
+
const tl = gsap.timeline({
|
|
364
|
+
onUpdate: () => {
|
|
365
|
+
meshes.forEach((mesh, index) => {
|
|
366
|
+
mesh.material.opacity = opacityObjects[index].opacity;
|
|
367
|
+
});
|
|
368
|
+
},
|
|
369
|
+
onComplete: () => {
|
|
370
|
+
opacityAnimation = null;
|
|
371
|
+
},
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
// Animate each mesh's opacity
|
|
375
|
+
meshes.forEach((mesh, index) => {
|
|
376
|
+
const targetOpacity = mesh === targetMesh ? 1 : opacity;
|
|
377
|
+
tl.to(
|
|
378
|
+
opacityObjects[index],
|
|
379
|
+
{
|
|
380
|
+
opacity: targetOpacity,
|
|
381
|
+
duration,
|
|
382
|
+
ease: 'power3.inOut',
|
|
383
|
+
},
|
|
384
|
+
0,
|
|
385
|
+
);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
opacityAnimation = tl;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Function to reset all mesh opacities
|
|
392
|
+
function resetMeshOpacities(duration = 0.5) {
|
|
393
|
+
// Kill any existing opacity animation
|
|
394
|
+
if (opacityAnimation) {
|
|
395
|
+
opacityAnimation.kill();
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Create opacity animation objects for GSAP
|
|
399
|
+
const opacityObjects = meshes.map(mesh => ({
|
|
400
|
+
opacity: mesh.material.opacity,
|
|
401
|
+
}));
|
|
402
|
+
|
|
403
|
+
// Create timeline for opacity animation
|
|
404
|
+
const tl = gsap.timeline({
|
|
405
|
+
onUpdate: () => {
|
|
406
|
+
meshes.forEach((mesh, index) => {
|
|
407
|
+
mesh.material.opacity = opacityObjects[index].opacity;
|
|
408
|
+
});
|
|
409
|
+
},
|
|
410
|
+
onComplete: () => {
|
|
411
|
+
opacityAnimation = null;
|
|
412
|
+
},
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
// Animate all meshes to full opacity
|
|
416
|
+
meshes.forEach((mesh, index) => {
|
|
417
|
+
tl.to(
|
|
418
|
+
opacityObjects[index],
|
|
419
|
+
{
|
|
420
|
+
opacity: 1,
|
|
421
|
+
duration,
|
|
422
|
+
ease: 'power2.inOut',
|
|
423
|
+
},
|
|
424
|
+
0,
|
|
425
|
+
);
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
opacityAnimation = tl;
|
|
429
|
+
selectedMesh = null;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Function to return to original position
|
|
433
|
+
function returnToOriginalPosition() {
|
|
434
|
+
if (controls) {
|
|
435
|
+
controls.autoRotate = true;
|
|
436
|
+
}
|
|
437
|
+
const originalLookAt = new THREE.Vector3(0, 0, 0);
|
|
438
|
+
animateCamera(originalCameraPosition, originalLookAt, 1.3, false);
|
|
439
|
+
isAtOriginalPosition = true;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Click handler
|
|
443
|
+
function onMouseClick(event) {
|
|
444
|
+
if (currentAnimation && currentAnimation.isActive()) return;
|
|
445
|
+
|
|
446
|
+
// Calculate mouse position in normalized device coordinates (-1 to +1)
|
|
447
|
+
const rect = renderer.domElement.getBoundingClientRect();
|
|
448
|
+
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
|
|
449
|
+
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
|
|
450
|
+
|
|
451
|
+
// Update raycaster with camera and mouse position
|
|
452
|
+
raycaster.setFromCamera(mouse, camera);
|
|
453
|
+
|
|
454
|
+
// Check for intersections with meshes
|
|
455
|
+
const intersects = raycaster.intersectObjects(meshes);
|
|
456
|
+
|
|
457
|
+
if (intersects.length > 0) {
|
|
458
|
+
const clickedMesh = intersects[0].object;
|
|
459
|
+
|
|
460
|
+
// If clicking the same mesh, reset everything
|
|
461
|
+
if (selectedMesh === clickedMesh) {
|
|
462
|
+
resetMeshOpacities();
|
|
463
|
+
returnToOriginalPosition();
|
|
464
|
+
// Notify callback with null to reset content
|
|
465
|
+
if (onMeshClick) {
|
|
466
|
+
onMeshClick(null);
|
|
467
|
+
}
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const targetPosition = clickedMesh.position.clone();
|
|
472
|
+
|
|
473
|
+
// Get the normal vector of the plane (direction the plane is facing)
|
|
474
|
+
const meshNormal = new THREE.Vector3(0, 0, 1);
|
|
475
|
+
meshNormal.applyQuaternion(clickedMesh.quaternion);
|
|
476
|
+
meshNormal.normalize();
|
|
477
|
+
|
|
478
|
+
// Fallback: if normal is invalid, use direction from origin to mesh
|
|
479
|
+
if (meshNormal.length() < 0.1 || !meshNormal.length()) {
|
|
480
|
+
meshNormal.copy(targetPosition).normalize();
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Disable auto rotate when clicking a mesh
|
|
484
|
+
if (controls) {
|
|
485
|
+
controls.autoRotate = false;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Animate camera to clicked image position, oriented parallel to the plane
|
|
489
|
+
animateCamera(targetPosition, targetPosition, 1.3, true, meshNormal);
|
|
490
|
+
isAtOriginalPosition = false;
|
|
491
|
+
|
|
492
|
+
// Animate opacity: clicked mesh stays at 1, others go to low opacity
|
|
493
|
+
selectedMesh = clickedMesh;
|
|
494
|
+
animateMeshOpacity(clickedMesh, 0.1, 1.3);
|
|
495
|
+
|
|
496
|
+
// Notify callback with content data
|
|
497
|
+
if (onMeshClick && clickedMesh.userData.content) {
|
|
498
|
+
onMeshClick(clickedMesh.userData.content);
|
|
499
|
+
}
|
|
500
|
+
} else if (!isAtOriginalPosition) {
|
|
501
|
+
// Clicked on empty space - return to original position and reset opacities
|
|
502
|
+
resetMeshOpacities();
|
|
503
|
+
returnToOriginalPosition();
|
|
504
|
+
// Notify callback with null to reset content
|
|
505
|
+
if (onMeshClick) {
|
|
506
|
+
onMeshClick(null);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Add click event listener
|
|
512
|
+
renderer.domElement.addEventListener('click', onMouseClick);
|
|
513
|
+
|
|
514
|
+
// Store cleanup function on the group (includes renderer-specific cleanup)
|
|
515
|
+
group.userData.cleanup = () => {
|
|
516
|
+
// Remove event listeners
|
|
517
|
+
renderer.domElement.removeEventListener('click', onMouseClick);
|
|
518
|
+
|
|
519
|
+
// Kill all animations
|
|
520
|
+
if (currentAnimation) {
|
|
521
|
+
currentAnimation.kill();
|
|
522
|
+
currentAnimation = null;
|
|
523
|
+
}
|
|
524
|
+
if (opacityAnimation) {
|
|
525
|
+
opacityAnimation.kill();
|
|
526
|
+
opacityAnimation = null;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Call common cleanup
|
|
530
|
+
performCommonCleanup();
|
|
531
|
+
};
|
|
532
|
+
} else {
|
|
533
|
+
// Store cleanup function on the group (common cleanup only)
|
|
534
|
+
group.userData.cleanup = () => {
|
|
535
|
+
performCommonCleanup();
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Common cleanup function for textures, materials, geometry, and deviceDetector
|
|
540
|
+
function performCommonCleanup() {
|
|
541
|
+
// Dispose deviceDetector
|
|
542
|
+
disposeDeviceDetector();
|
|
543
|
+
|
|
544
|
+
// Dispose textures
|
|
545
|
+
textures.forEach(texture => {
|
|
546
|
+
if (texture) {
|
|
547
|
+
texture.dispose();
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
// Dispose materials
|
|
552
|
+
meshes.forEach(mesh => {
|
|
553
|
+
if (mesh.material) {
|
|
554
|
+
mesh.material.dispose();
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
// Dispose geometry (shared geometry)
|
|
559
|
+
if (sharedGeometry) {
|
|
560
|
+
sharedGeometry.dispose();
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Method to update content without recreating the scene
|
|
565
|
+
group.userData.updateContent = async newContent => {
|
|
566
|
+
if (!newContent || newContent.length === 0) return;
|
|
567
|
+
|
|
568
|
+
currentContent = newContent;
|
|
569
|
+
|
|
570
|
+
// Store old textures and URLs for disposal after new ones are loaded
|
|
571
|
+
const oldTextures = [...textures];
|
|
572
|
+
const oldUrls = [...textureUrls];
|
|
573
|
+
|
|
574
|
+
// Load new textures (will use cache if available)
|
|
575
|
+
const loadedData = await loadTextures(newContent);
|
|
576
|
+
textures = loadedData.textures;
|
|
577
|
+
textureUrls = loadedData.urls;
|
|
578
|
+
|
|
579
|
+
// Update meshes with new textures and content
|
|
580
|
+
meshes.forEach((mesh, i) => {
|
|
581
|
+
const contentIndex = i % newContent.length;
|
|
582
|
+
|
|
583
|
+
// Update texture
|
|
584
|
+
mesh.material.map = textures[contentIndex];
|
|
585
|
+
mesh.material.needsUpdate = true;
|
|
586
|
+
|
|
587
|
+
// Update content data
|
|
588
|
+
mesh.userData.contentIndex = contentIndex;
|
|
589
|
+
mesh.userData.content = newContent[contentIndex];
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
// Dispose old textures only if they're not in the cache
|
|
593
|
+
// Cached textures are reused, so we never dispose them
|
|
594
|
+
oldTextures.forEach((texture, index) => {
|
|
595
|
+
if (texture && !textures.includes(texture)) {
|
|
596
|
+
const oldUrl = oldUrls[index];
|
|
597
|
+
// Only dispose if texture is not in cache (not being reused)
|
|
598
|
+
if (!textureCache.has(oldUrl) || textureCache.get(oldUrl) !== texture) {
|
|
599
|
+
texture.dispose();
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
};
|
|
604
|
+
|
|
605
|
+
return group;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
export {createGlobeSphere};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import {BoxGeometry, MathUtils, Mesh, MeshStandardMaterial} from 'three';
|
|
2
|
+
|
|
3
|
+
function createCube() {
|
|
4
|
+
const geometry = new BoxGeometry(1.5, 1.5, 1.5);
|
|
5
|
+
const material = new MeshStandardMaterial({
|
|
6
|
+
color: '#ffd93d',
|
|
7
|
+
roughness: 0.4,
|
|
8
|
+
metalness: 0.6,
|
|
9
|
+
});
|
|
10
|
+
const cube = new Mesh(geometry, material);
|
|
11
|
+
|
|
12
|
+
cube.position.set(0, 0, 0);
|
|
13
|
+
cube.rotation.set(-0.5, -0.1, 0.8);
|
|
14
|
+
|
|
15
|
+
const radiansPerSecond = MathUtils.degToRad(30);
|
|
16
|
+
|
|
17
|
+
cube.tick = delta => {
|
|
18
|
+
// increase the cube's rotation each frame
|
|
19
|
+
cube.rotation.z += radiansPerSecond * delta;
|
|
20
|
+
cube.rotation.x += radiansPerSecond * delta;
|
|
21
|
+
cube.rotation.y += radiansPerSecond * delta;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
return cube;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export {createCube};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import {DirectionalLight, HemisphereLight} from 'three';
|
|
2
|
+
|
|
3
|
+
function createLights() {
|
|
4
|
+
const ambientLight = new HemisphereLight(
|
|
5
|
+
'white', // bright sky color
|
|
6
|
+
'darkslategrey', // dim ground color
|
|
7
|
+
5, // intensity
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
const mainLight = new DirectionalLight('white', 8);
|
|
11
|
+
mainLight.position.set(10, 10, 10);
|
|
12
|
+
|
|
13
|
+
return {ambientLight, mainLight};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export {createLights};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import {SphereGeometry, MathUtils, Mesh, MeshStandardMaterial} from 'three';
|
|
2
|
+
|
|
3
|
+
function createSphere() {
|
|
4
|
+
const geometry = new SphereGeometry(1, 32, 32);
|
|
5
|
+
const material = new MeshStandardMaterial({
|
|
6
|
+
color: '#4ecdc4',
|
|
7
|
+
roughness: 0.2,
|
|
8
|
+
metalness: 0.5,
|
|
9
|
+
});
|
|
10
|
+
const sphere = new Mesh(geometry, material);
|
|
11
|
+
|
|
12
|
+
sphere.position.set(3, 0, 0);
|
|
13
|
+
|
|
14
|
+
const radiansPerSecond = MathUtils.degToRad(15);
|
|
15
|
+
let time = 0;
|
|
16
|
+
|
|
17
|
+
sphere.tick = delta => {
|
|
18
|
+
time += delta;
|
|
19
|
+
sphere.position.y = Math.sin(time * 2) * 0.5;
|
|
20
|
+
sphere.rotation.y += radiansPerSecond * delta;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
return sphere;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export {createSphere};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import {TorusGeometry, MathUtils, Mesh, MeshStandardMaterial} from 'three';
|
|
2
|
+
|
|
3
|
+
function createTorus() {
|
|
4
|
+
const geometry = new TorusGeometry(1, 0.4, 16, 100);
|
|
5
|
+
const material = new MeshStandardMaterial({
|
|
6
|
+
color: '#ff6b9d',
|
|
7
|
+
roughness: 0.3,
|
|
8
|
+
metalness: 0.8,
|
|
9
|
+
});
|
|
10
|
+
const torus = new Mesh(geometry, material);
|
|
11
|
+
|
|
12
|
+
torus.position.set(-3, 0, 0);
|
|
13
|
+
torus.rotation.set(0.5, 0, 0);
|
|
14
|
+
|
|
15
|
+
const radiansPerSecond = MathUtils.degToRad(20);
|
|
16
|
+
|
|
17
|
+
torus.tick = delta => {
|
|
18
|
+
torus.rotation.x += radiansPerSecond * delta;
|
|
19
|
+
torus.rotation.y += radiansPerSecond * delta * 0.5;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
return torus;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export {createTorus};
|