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.
Files changed (50) hide show
  1. package/dist/add-three-app.d.ts +6 -0
  2. package/dist/add-three-app.js +111 -0
  3. package/dist/app.d.ts +11 -0
  4. package/dist/app.js +143 -0
  5. package/dist/cli.d.ts +2 -0
  6. package/dist/cli.js +71 -0
  7. package/dist/components/FeatureSelector.d.ts +21 -0
  8. package/dist/components/FeatureSelector.js +175 -0
  9. package/dist/components/ProgressIndicator.d.ts +7 -0
  10. package/dist/components/ProgressIndicator.js +46 -0
  11. package/dist/components/Summary.d.ts +8 -0
  12. package/dist/components/Summary.js +51 -0
  13. package/dist/components/WelcomeHeader.d.ts +2 -0
  14. package/dist/components/WelcomeHeader.js +8 -0
  15. package/dist/template_code/three/README.md +146 -0
  16. package/dist/template_code/three/World.js +133 -0
  17. package/dist/template_code/three/camera.js +30 -0
  18. package/dist/template_code/three/components/GlobeSphere.js +608 -0
  19. package/dist/template_code/three/components/cube.js +27 -0
  20. package/dist/template_code/three/components/lights.js +16 -0
  21. package/dist/template_code/three/components/sphere.js +26 -0
  22. package/dist/template_code/three/components/torus.js +25 -0
  23. package/dist/template_code/three/scene.js +28 -0
  24. package/dist/template_code/three/systems/Loop.js +43 -0
  25. package/dist/template_code/three/systems/Resizer.js +26 -0
  26. package/dist/template_code/three/systems/controls.js +19 -0
  27. package/dist/template_code/three/systems/post-processing.js +50 -0
  28. package/dist/template_code/three/systems/renderer.js +17 -0
  29. package/dist/template_code/three/utils/deviceDetector.js +141 -0
  30. package/dist/template_code/three/utils/gltfLoader.js +14 -0
  31. package/dist/template_code/three/utils/loadKTX2Texture.js +42 -0
  32. package/dist/template_code/three/utils/textureLoader.js +21 -0
  33. package/dist/utils/add-three.d.ts +7 -0
  34. package/dist/utils/add-three.js +288 -0
  35. package/dist/utils/app-creation.d.ts +4 -0
  36. package/dist/utils/app-creation.js +35 -0
  37. package/dist/utils/config-generator.d.ts +6 -0
  38. package/dist/utils/config-generator.js +508 -0
  39. package/dist/utils/css-variables.d.ts +4 -0
  40. package/dist/utils/css-variables.js +316 -0
  41. package/dist/utils/dependencies.d.ts +11 -0
  42. package/dist/utils/dependencies.js +68 -0
  43. package/dist/utils/package-manager.d.ts +4 -0
  44. package/dist/utils/package-manager.js +56 -0
  45. package/dist/utils/project-detection.d.ts +2 -0
  46. package/dist/utils/project-detection.js +60 -0
  47. package/dist/utils/shadcn-setup.d.ts +2 -0
  48. package/dist/utils/shadcn-setup.js +46 -0
  49. package/package.json +81 -0
  50. 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};