@octanejs/drei 0.0.1

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 (152) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +11 -0
  3. package/UPSTREAM.md +92 -0
  4. package/package.json +96 -0
  5. package/src/config.ts +14 -0
  6. package/src/core/AccumulativeShadows.three.tsrx +313 -0
  7. package/src/core/AdaptivePerformance.three.tsrx +49 -0
  8. package/src/core/AsciiRenderer.three.tsrx +61 -0
  9. package/src/core/Bounds.three.tsrx +323 -0
  10. package/src/core/Bvh.three.tsrx +116 -0
  11. package/src/core/CameraControls.three.tsrx +181 -0
  12. package/src/core/CameraShake.three.tsrx +104 -0
  13. package/src/core/Caustics.three.tsrx +107 -0
  14. package/src/core/Center.three.tsrx +145 -0
  15. package/src/core/Clone.three.tsrx +167 -0
  16. package/src/core/Cloud.three.tsrx +314 -0
  17. package/src/core/ComputedAttribute.three.tsrx +27 -0
  18. package/src/core/ContactShadows.three.tsrx +106 -0
  19. package/src/core/CubeTexture.three.tsrx +32 -0
  20. package/src/core/CurveModifier.three.tsrx +37 -0
  21. package/src/core/Decal.three.tsrx +132 -0
  22. package/src/core/Detailed.three.tsrx +28 -0
  23. package/src/core/DetectGPU.three.tsrx +22 -0
  24. package/src/core/Effects.three.tsrx +123 -0
  25. package/src/core/Environment.three.tsrx +220 -0
  26. package/src/core/Example.three.tsrx +56 -0
  27. package/src/core/Fbo.three.tsrx +70 -0
  28. package/src/core/Fbx.three.tsrx +25 -0
  29. package/src/core/Fisheye.three.tsrx +82 -0
  30. package/src/core/GizmoHelper.three.tsrx +115 -0
  31. package/src/core/GizmoViewcube.three.tsrx +82 -0
  32. package/src/core/GizmoViewport.three.tsrx +122 -0
  33. package/src/core/Gltf.three.tsrx +62 -0
  34. package/src/core/GradientTexture.three.tsrx +66 -0
  35. package/src/core/Grid.three.tsrx +170 -0
  36. package/src/core/Helper.three.tsrx +56 -0
  37. package/src/core/Hud.three.tsrx +52 -0
  38. package/src/core/Image.three.tsrx +146 -0
  39. package/src/core/Instances.three.tsrx +332 -0
  40. package/src/core/Ktx2.three.tsrx +59 -0
  41. package/src/core/Lightformer.three.tsrx +75 -0
  42. package/src/core/MarchingCubes.three.tsrx +125 -0
  43. package/src/core/MeshPortalMaterial.three.tsrx +103 -0
  44. package/src/core/MeshReflectorMaterial.three.tsrx +67 -0
  45. package/src/core/MeshRefractionMaterial.three.tsrx +70 -0
  46. package/src/core/MeshTransmissionMaterial.three.tsrx +71 -0
  47. package/src/core/MotionPathControls.three.tsrx +139 -0
  48. package/src/core/MultiMaterial.three.tsrx +32 -0
  49. package/src/core/Outlines.three.tsrx +162 -0
  50. package/src/core/PerformanceMonitor.three.tsrx +144 -0
  51. package/src/core/PointerLockControls.three.tsrx +84 -0
  52. package/src/core/Points.three.tsrx +242 -0
  53. package/src/core/PositionalAudio.three.tsrx +76 -0
  54. package/src/core/Preload.three.tsrx +40 -0
  55. package/src/core/Progress.three.tsrx +47 -0
  56. package/src/core/RenderCubeTexture.three.tsrx +111 -0
  57. package/src/core/RenderTexture.three.tsrx +102 -0
  58. package/src/core/Resize.three.tsrx +59 -0
  59. package/src/core/RoundedBox.three.tsrx +106 -0
  60. package/src/core/Sampler.three.tsrx +116 -0
  61. package/src/core/ScreenSizer.three.tsrx +43 -0
  62. package/src/core/ScreenSpace.three.tsrx +21 -0
  63. package/src/core/Segments.three.tsrx +119 -0
  64. package/src/core/Sky.three.tsrx +63 -0
  65. package/src/core/Sparkles.three.tsrx +145 -0
  66. package/src/core/Splat.three.tsrx +52 -0
  67. package/src/core/SpotLight.three.tsrx +350 -0
  68. package/src/core/SpriteAnimator.three.tsrx +338 -0
  69. package/src/core/Stage.three.tsrx +112 -0
  70. package/src/core/Stars.three.tsrx +103 -0
  71. package/src/core/Stats.three.tsrx +35 -0
  72. package/src/core/StatsGl.three.tsrx +60 -0
  73. package/src/core/Svg.three.tsrx +82 -0
  74. package/src/core/Text.three.tsrx +96 -0
  75. package/src/core/Text3D.three.tsrx +90 -0
  76. package/src/core/Texture.three.tsrx +72 -0
  77. package/src/core/Trail.three.tsrx +167 -0
  78. package/src/core/TrailTexture.three.tsrx +171 -0
  79. package/src/core/TransformControls.three.tsrx +155 -0
  80. package/src/core/VideoTexture.three.tsrx +116 -0
  81. package/src/core/Wireframe.three.tsrx +92 -0
  82. package/src/core/basic-controls.three.tsrx +353 -0
  83. package/src/core/calculateScaleFactor.ts +42 -0
  84. package/src/core/cameras.three.tsrx +230 -0
  85. package/src/core/catalog-resource.ts +12 -0
  86. package/src/core/catalog-textures.three.tsrx +105 -0
  87. package/src/core/compiler-slot.ts +9 -0
  88. package/src/core/effects-utils.ts +8 -0
  89. package/src/core/environment-loader.ts +87 -0
  90. package/src/core/font-resource.ts +31 -0
  91. package/src/core/gltf-loader.ts +33 -0
  92. package/src/core/gradient-types.ts +4 -0
  93. package/src/core/index.ts +295 -0
  94. package/src/core/lines.three.tsrx +329 -0
  95. package/src/core/meshBounds.ts +26 -0
  96. package/src/core/motion-materials.three.tsrx +93 -0
  97. package/src/core/octane-children.ts +18 -0
  98. package/src/core/octane-element.ts +16 -0
  99. package/src/core/resource.ts +29 -0
  100. package/src/core/scene-motion.three.tsrx +183 -0
  101. package/src/core/shaderMaterial.ts +50 -0
  102. package/src/core/shadows.three.tsrx +164 -0
  103. package/src/core/shapes.three.tsrx +170 -0
  104. package/src/core/simple-materials.three.tsrx +124 -0
  105. package/src/core/softShadows.three.tsrx +151 -0
  106. package/src/core/splat-resource.ts +598 -0
  107. package/src/core/useAnimations.three.tsrx +57 -0
  108. package/src/core/useAspect.three.tsrx +10 -0
  109. package/src/core/useBoxProjectedEnv.three.tsrx +113 -0
  110. package/src/core/useCamera.three.tsrx +26 -0
  111. package/src/core/useContextBridge.three.tsrx +25 -0
  112. package/src/core/useDepthBuffer.three.tsrx +35 -0
  113. package/src/core/useEnvironment.three.tsrx +81 -0
  114. package/src/core/useFont.three.tsrx +12 -0
  115. package/src/core/useIntersect.three.tsrx +38 -0
  116. package/src/core/useSpriteLoader.three.tsrx +249 -0
  117. package/src/core/video-texture-resource.ts +14 -0
  118. package/src/index.ts +2 -0
  119. package/src/materials/BlurPass.ts +94 -0
  120. package/src/materials/CausticsMaterials.ts +197 -0
  121. package/src/materials/ConvolutionMaterial.ts +65 -0
  122. package/src/materials/MeshReflectorMaterial.ts +180 -0
  123. package/src/materials/MeshRefractionMaterial.ts +131 -0
  124. package/src/materials/MeshTransmissionMaterial.ts +182 -0
  125. package/src/materials/PortalMaterial.ts +38 -0
  126. package/src/materials/WireframeMaterial.ts +241 -0
  127. package/src/materials/makeSDFGenerator.ts +155 -0
  128. package/src/troika-three-text.d.ts +19 -0
  129. package/src/web/CycleRaycast.three.tsrx +81 -0
  130. package/src/web/DragControls.three.tsrx +82 -0
  131. package/src/web/FaceControls.three.tsrx +178 -0
  132. package/src/web/FaceLandmarker.three.tsrx +48 -0
  133. package/src/web/Facemesh.three.tsrx +464 -0
  134. package/src/web/Html.three.tsrx +390 -0
  135. package/src/web/KeyboardControls.three.tsrx +98 -0
  136. package/src/web/Loader.tsrx +61 -0
  137. package/src/web/PresentationControls.three.tsrx +63 -0
  138. package/src/web/ScrollControls.three.tsrx +120 -0
  139. package/src/web/Select.three.tsrx +155 -0
  140. package/src/web/View.three.tsrx +46 -0
  141. package/src/web/face-landmarker-resource.ts +18 -0
  142. package/src/web/facemesh-data.ts +54 -0
  143. package/src/web/index.ts +41 -0
  144. package/src/web/keyboard-controls-types.ts +33 -0
  145. package/src/web/media-textures.three.tsrx +50 -0
  146. package/src/web/pivotControls/annotation.tsrx +17 -0
  147. package/src/web/pivotControls/context.ts +38 -0
  148. package/src/web/pivotControls/index.three.tsrx +877 -0
  149. package/src/web/scroll-html-root.tsrx +27 -0
  150. package/src/web/useCursor.three.tsrx +19 -0
  151. package/src/web/useGesture.ts +27 -0
  152. package/status.json +14 -0
@@ -0,0 +1,350 @@
1
+ /** @jsxImportSource @octanejs/three/intrinsics */
2
+ import { type ThreeElements, type ThreeRef, useFrame, useThree } from '@octanejs/three';
3
+ import { useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from '@octanejs/three/renderer';
4
+ import { createContext, type OctaneNode, useContext } from 'octane';
5
+ import * as THREE from 'three';
6
+ import { FullScreenQuad } from 'three-stdlib';
7
+
8
+ export type SpotLightProps = Omit<ThreeElements['spotLight'], 'ref' | 'children'> & {
9
+ depthBuffer?: THREE.DepthTexture;
10
+ attenuation?: number;
11
+ anglePower?: number;
12
+ radiusTop?: number;
13
+ radiusBottom?: number;
14
+ opacity?: number;
15
+ color?: string | number;
16
+ volumetric?: boolean;
17
+ debug?: boolean;
18
+ children?: OctaneNode;
19
+ };
20
+
21
+ class SpotLightMaterial extends THREE.ShaderMaterial {
22
+ constructor() {
23
+ super({
24
+ uniforms: {
25
+ depth: { value: null },
26
+ opacity: { value: 1 },
27
+ attenuation: { value: 2.5 },
28
+ anglePower: { value: 12 },
29
+ spotPosition: { value: new THREE.Vector3(0, 0, 0) },
30
+ lightColor: { value: new THREE.Color('white') },
31
+ cameraNear: { value: 0 },
32
+ cameraFar: { value: 1 },
33
+ resolution: { value: new THREE.Vector2(0, 0) },
34
+ },
35
+ transparent: true,
36
+ depthWrite: false,
37
+ vertexShader: /* glsl */ `
38
+ varying vec3 vNormal;
39
+ varying float vViewZ;
40
+ varying float vIntensity;
41
+ uniform vec3 spotPosition;
42
+ uniform float attenuation;
43
+
44
+ #include <common>
45
+ #include <logdepthbuf_pars_vertex>
46
+
47
+ void main() {
48
+ vNormal = normalize(normalMatrix * normal);
49
+ vec4 worldPosition = modelMatrix * vec4(position, 1);
50
+ vec4 viewPosition = viewMatrix * worldPosition;
51
+ vViewZ = viewPosition.z;
52
+ vIntensity = 1.0 - saturate(distance(worldPosition.xyz, spotPosition) / attenuation);
53
+ gl_Position = projectionMatrix * viewPosition;
54
+ #include <logdepthbuf_vertex>
55
+ }
56
+ `,
57
+ fragmentShader: /* glsl */ `
58
+ varying vec3 vNormal;
59
+ varying float vViewZ;
60
+ varying float vIntensity;
61
+ uniform vec3 lightColor;
62
+ uniform float anglePower;
63
+ uniform sampler2D depth;
64
+ uniform vec2 resolution;
65
+ uniform float cameraNear;
66
+ uniform float cameraFar;
67
+ uniform float opacity;
68
+
69
+ #include <packing>
70
+ #include <logdepthbuf_pars_fragment>
71
+
72
+ float readDepth(sampler2D depthSampler, vec2 uv) {
73
+ float fragCoordZ = texture(depthSampler, uv).r;
74
+ #ifdef USE_LOGDEPTHBUF
75
+ float viewZ = 1.0 - exp2(fragCoordZ * log(cameraFar + 1.0) / log(2.0));
76
+ #else
77
+ float viewZ = perspectiveDepthToViewZ(fragCoordZ, cameraNear, cameraFar);
78
+ #endif
79
+ return viewZ;
80
+ }
81
+
82
+ void main() {
83
+ #include <logdepthbuf_fragment>
84
+ vec3 normal = vec3(vNormal.x, vNormal.y, abs(vNormal.z));
85
+ float angleIntensity = pow(dot(normal, vec3(0, 0, 1)), anglePower);
86
+ float intensity = vIntensity * angleIntensity;
87
+ bool isSoft = resolution[0] > 0.0 && resolution[1] > 0.0;
88
+ if (isSoft) {
89
+ vec2 uv = gl_FragCoord.xy / resolution;
90
+ intensity *= smoothstep(0.0, 1.0, vViewZ - readDepth(depth, uv));
91
+ }
92
+ gl_FragColor = vec4(lightColor, intensity * opacity);
93
+ #include <tonemapping_fragment>
94
+ #include <colorspace_fragment>
95
+ }
96
+ `,
97
+ });
98
+ }
99
+ }
100
+
101
+ const isSpotLight = (child: THREE.Object3D | null): child is THREE.SpotLight =>
102
+ Boolean((child as THREE.SpotLight | null)?.isSpotLight);
103
+
104
+ function VolumetricMesh({
105
+ opacity = 1,
106
+ radiusTop,
107
+ radiusBottom,
108
+ depthBuffer,
109
+ color = 'white',
110
+ distance = 5,
111
+ angle = 0.15,
112
+ attenuation = 5,
113
+ anglePower = 5,
114
+ }: Omit<SpotLightProps, 'volumetric'>) @{
115
+ const mesh = useRef<THREE.Mesh | null>(null);
116
+ const size = useThree((state) => state.size);
117
+ const camera = useThree((state) => state.camera);
118
+ const dpr = useThree((state) => state.viewport.dpr);
119
+ const [material] = useState(() => new SpotLightMaterial());
120
+ const [vector] = useState(() => new THREE.Vector3());
121
+ const top = radiusTop === undefined ? 0.1 : radiusTop;
122
+ const bottom = radiusBottom === undefined ? angle * 7 : radiusBottom;
123
+ useFrame(() => {
124
+ if (!mesh.current) return;
125
+ material.uniforms.spotPosition.value.copy(mesh.current.getWorldPosition(vector));
126
+ mesh.current.lookAt((mesh.current.parent as THREE.SpotLight).target.getWorldPosition(vector));
127
+ });
128
+ const geom = useMemo(() => {
129
+ const value = new THREE.CylinderGeometry(top, bottom, distance, 128, 64, true);
130
+ value.applyMatrix4(new THREE.Matrix4().makeTranslation(0, -distance / 2, 0));
131
+ value.applyMatrix4(new THREE.Matrix4().makeRotationX(-Math.PI / 2));
132
+ return value;
133
+ }, [distance, top, bottom]);
134
+ <mesh ref={mesh} {...({ geometry: geom, raycast: () => null } as any)}>
135
+ <primitive
136
+ object={material}
137
+ attach="material"
138
+ uniforms-opacity-value={opacity}
139
+ uniforms-lightColor-value={color}
140
+ uniforms-attenuation-value={attenuation}
141
+ uniforms-anglePower-value={anglePower}
142
+ uniforms-depth-value={depthBuffer}
143
+ uniforms-cameraNear-value={camera.near}
144
+ uniforms-cameraFar-value={camera.far}
145
+ uniforms-resolution-value={depthBuffer ? [size.width * dpr, size.height * dpr] : [0, 0]}
146
+ />
147
+ </mesh>
148
+ }
149
+
150
+ function useCommon(
151
+ spotlight: { current: THREE.SpotLight | null },
152
+ mesh: { current: THREE.Mesh | null },
153
+ width: number,
154
+ height: number,
155
+ distance: number,
156
+ ): void {
157
+ const [[position, direction]] = useState(() => [new THREE.Vector3(), new THREE.Vector3()]);
158
+ useLayoutEffect(() => {
159
+ if (isSpotLight(spotlight.current)) {
160
+ spotlight.current.shadow.mapSize.set(width, height);
161
+ spotlight.current.shadow.needsUpdate = true;
162
+ } else throw new Error('SpotlightShadow must be a child of a SpotLight');
163
+ }, [spotlight, width, height]);
164
+ useFrame(() => {
165
+ if (!spotlight.current || !mesh.current) return;
166
+ const source = spotlight.current.position;
167
+ const target = spotlight.current.target.position;
168
+ direction.copy(target).sub(source);
169
+ const length = direction.length();
170
+ direction.normalize().multiplyScalar(length * distance);
171
+ position.copy(source).add(direction);
172
+ mesh.current.position.copy(position);
173
+ mesh.current.lookAt(spotlight.current.target.position);
174
+ });
175
+ }
176
+
177
+ interface ShadowMeshProps {
178
+ distance?: number;
179
+ alphaTest?: number;
180
+ scale?: number;
181
+ map?: THREE.Texture;
182
+ shader?: string;
183
+ width?: number;
184
+ height?: number;
185
+ children?: OctaneNode;
186
+ spotlightRef?: { current: THREE.SpotLight | null };
187
+ debug?: boolean;
188
+ }
189
+
190
+ const SpotLightShadowContext = createContext<{
191
+ spotlightRef: { current: THREE.SpotLight | null };
192
+ debug: boolean;
193
+ } | null>(null);
194
+
195
+ const defaultSpotlightShadowShader = /* glsl */ `
196
+ varying vec2 vUv;
197
+
198
+ uniform sampler2D uShadowMap;
199
+ uniform float uTime;
200
+
201
+ void main() {
202
+ vec3 color = texture2D(uShadowMap, vUv).xyz;
203
+ gl_FragColor = vec4(color, 1.);
204
+ }
205
+ `;
206
+
207
+ function SpotlightShadowWithShader({
208
+ distance = 0.4,
209
+ alphaTest = 0.5,
210
+ map,
211
+ shader = defaultSpotlightShadowShader,
212
+ width = 512,
213
+ height = 512,
214
+ scale = 1,
215
+ children,
216
+ spotlightRef,
217
+ debug,
218
+ }: ShadowMeshProps) @{
219
+ const mesh = useRef<THREE.Mesh | null>(null);
220
+ const parent = useContext(SpotLightShadowContext);
221
+ const activeSpotlight = spotlightRef ?? parent?.spotlightRef;
222
+ const activeDebug = debug ?? parent?.debug;
223
+ useCommon(activeSpotlight!, mesh, width, height, distance);
224
+ const renderTarget = useMemo(() => new THREE.WebGLRenderTarget(width, height, {
225
+ format: THREE.RGBAFormat,
226
+ stencilBuffer: false,
227
+ }), [width, height]);
228
+ const uniforms = useRef({ uShadowMap: { value: map }, uTime: { value: 0 } });
229
+ useEffect(() => { uniforms.current.uShadowMap.value = map; }, [map]);
230
+ const fsQuad = useMemo(() => new FullScreenQuad(new THREE.ShaderMaterial({
231
+ uniforms: uniforms.current,
232
+ vertexShader: /* glsl */ `
233
+ varying vec2 vUv;
234
+ void main() {
235
+ vUv = uv;
236
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
237
+ }
238
+ `,
239
+ fragmentShader: shader,
240
+ }) as any), [shader]);
241
+ useEffect(() => () => {
242
+ fsQuad.material.dispose();
243
+ fsQuad.dispose();
244
+ }, [fsQuad]);
245
+ useEffect(() => () => renderTarget.dispose(), [renderTarget]);
246
+ useFrame(({ gl }, delta) => {
247
+ uniforms.current.uTime.value += delta;
248
+ gl.setRenderTarget(renderTarget);
249
+ fsQuad.render(gl as any);
250
+ gl.setRenderTarget(null);
251
+ });
252
+ <mesh ref={mesh} scale={scale} castShadow>
253
+ <planeGeometry />
254
+ <meshBasicMaterial
255
+ transparent
256
+ side={THREE.DoubleSide}
257
+ alphaTest={alphaTest}
258
+ alphaMap={renderTarget.texture}
259
+ alphaMap-wrapS={THREE.RepeatWrapping}
260
+ alphaMap-wrapT={THREE.RepeatWrapping}
261
+ opacity={activeDebug ? 1 : 0}
262
+ >
263
+ {children}
264
+ </meshBasicMaterial>
265
+ </mesh>
266
+ }
267
+
268
+ function SpotlightShadowWithoutShader({
269
+ distance = 0.4,
270
+ alphaTest = 0.5,
271
+ map,
272
+ width = 512,
273
+ height = 512,
274
+ scale,
275
+ children,
276
+ spotlightRef,
277
+ debug,
278
+ }: ShadowMeshProps) @{
279
+ const mesh = useRef<THREE.Mesh | null>(null);
280
+ const parent = useContext(SpotLightShadowContext);
281
+ const activeSpotlight = spotlightRef ?? parent?.spotlightRef;
282
+ const activeDebug = debug ?? parent?.debug;
283
+ useCommon(activeSpotlight!, mesh, width, height, distance);
284
+ <mesh ref={mesh} scale={scale} castShadow>
285
+ <planeGeometry />
286
+ <meshBasicMaterial
287
+ transparent
288
+ side={THREE.DoubleSide}
289
+ alphaTest={alphaTest}
290
+ alphaMap={map}
291
+ alphaMap-wrapS={THREE.RepeatWrapping}
292
+ alphaMap-wrapT={THREE.RepeatWrapping}
293
+ opacity={activeDebug ? 1 : 0}
294
+ >
295
+ {children}
296
+ </meshBasicMaterial>
297
+ </mesh>
298
+ }
299
+
300
+ export function SpotLightShadow(props: ShadowMeshProps) @{
301
+ @if (props.shader) {
302
+ <SpotlightShadowWithShader {...props} />
303
+ } @else {
304
+ <SpotlightShadowWithoutShader {...props} />
305
+ }
306
+ }
307
+
308
+ export function SpotLight({
309
+ opacity = 1,
310
+ radiusTop,
311
+ radiusBottom,
312
+ depthBuffer,
313
+ color = 'white',
314
+ distance = 5,
315
+ angle = 0.15,
316
+ attenuation = 5,
317
+ anglePower = 5,
318
+ volumetric = true,
319
+ debug = false,
320
+ children,
321
+ ref: forwardedRef,
322
+ ...props
323
+ }: SpotLightProps & { ref?: ThreeRef<THREE.SpotLight> }) @{
324
+ const spotlight = useRef<THREE.SpotLight | null>(null);
325
+ useImperativeHandle((forwardedRef ?? null) as any, () => spotlight.current!, []);
326
+ <group>
327
+ @if (debug && spotlight.current) {
328
+ <spotLightHelper args={[spotlight.current]} />
329
+ }
330
+ <spotLight ref={spotlight} angle={angle} color={color} distance={distance} castShadow {...props as any}>
331
+ @if (volumetric) {
332
+ <VolumetricMesh
333
+ debug={debug}
334
+ opacity={opacity}
335
+ radiusTop={radiusTop}
336
+ radiusBottom={radiusBottom}
337
+ depthBuffer={depthBuffer}
338
+ color={color}
339
+ distance={distance}
340
+ angle={angle}
341
+ attenuation={attenuation}
342
+ anglePower={anglePower}
343
+ />
344
+ }
345
+ </spotLight>
346
+ <SpotLightShadowContext.Provider value={{ spotlightRef: spotlight, debug }}>
347
+ {children}
348
+ </SpotLightShadowContext.Provider>
349
+ </group>
350
+ }
@@ -0,0 +1,338 @@
1
+ /** @jsxImportSource @octanejs/three/intrinsics */
2
+ import { type ThreeElements, type ThreeRef, type Vector3, useFrame } from '@octanejs/three';
3
+ import {
4
+ useCallback,
5
+ useEffect,
6
+ useImperativeHandle,
7
+ useLayoutEffect,
8
+ useMemo,
9
+ useRef,
10
+ useState,
11
+ } from '@octanejs/three/renderer';
12
+ import { createContext, type OctaneNode, useContext } from 'octane';
13
+ import * as THREE from 'three';
14
+ import { Instance, Instances, type InstanceProps, type InstancesProps } from './Instances.three.tsrx';
15
+ import { Billboard } from './scene-motion.three.tsrx';
16
+ import {
17
+ getFirstFrame,
18
+ type FrameData,
19
+ type Size,
20
+ type SpriteData,
21
+ useSpriteLoader,
22
+ } from './useSpriteLoader.three.tsrx';
23
+
24
+ type AnimationEventData = { currentFrameName: string; currentFrame: number };
25
+ type CommonProps<T, U, V> = Pick<T & U & V, keyof T & keyof U & keyof V>;
26
+ type CommonMeshProps = CommonProps<ThreeElements['mesh'], InstanceProps, InstancesProps>;
27
+
28
+ export type SpriteAnimatorProps = {
29
+ startFrame?: number;
30
+ endFrame?: number;
31
+ fps?: number;
32
+ frameName?: string;
33
+ textureDataURL?: string;
34
+ textureImageURL?: string;
35
+ loop?: boolean;
36
+ numberOfFrames?: number;
37
+ autoPlay?: boolean;
38
+ animationNames?: string[];
39
+ onStart?: (data: AnimationEventData) => void;
40
+ onEnd?: (data: AnimationEventData) => void;
41
+ onLoopEnd?: (data: AnimationEventData) => void;
42
+ onFrame?: (data: AnimationEventData) => void;
43
+ /** @deprecated Use pause={false} instead. */
44
+ play?: boolean;
45
+ pause?: boolean;
46
+ flipX?: boolean;
47
+ alphaTest?: number;
48
+ asSprite?: boolean;
49
+ offset?: number;
50
+ playBackwards?: boolean;
51
+ resetOnEnd?: boolean;
52
+ instanceItems?: Vector3[];
53
+ maxItems?: number;
54
+ spriteDataset?: {
55
+ spriteTexture: THREE.Texture;
56
+ spriteData: SpriteData | null;
57
+ aspect: Vector3;
58
+ } | null;
59
+ canvasRenderingContext2DSettings?: CanvasRenderingContext2DSettings;
60
+ roundFramePosition?: boolean;
61
+ meshProps?: CommonMeshProps;
62
+ children?: OctaneNode;
63
+ } & Omit<ThreeElements['group'], 'ref' | 'children'>;
64
+
65
+ type SpriteAnimatorState = {
66
+ current?: number;
67
+ offset?: number;
68
+ imageUrl?: string;
69
+ hasEnded: boolean;
70
+ ref: ThreeRef<THREE.Group> | undefined;
71
+ };
72
+
73
+ type Scale = Vector3;
74
+
75
+ const context = createContext<SpriteAnimatorState | null>(null);
76
+
77
+ export function useSpriteAnimator(): SpriteAnimatorState | null {
78
+ return useContext(context);
79
+ }
80
+
81
+ function isSpriteData(data: SpriteData | null): data is SpriteData {
82
+ return data !== null && 'meta' in data && 'frames' in data;
83
+ }
84
+
85
+ const geometry = /* @__PURE__ */ new THREE.PlaneGeometry(1, 1);
86
+
87
+ export function SpriteAnimator({
88
+ startFrame = 0,
89
+ endFrame,
90
+ fps = 30,
91
+ frameName = '',
92
+ textureDataURL,
93
+ textureImageURL,
94
+ loop = false,
95
+ numberOfFrames = 1,
96
+ autoPlay = true,
97
+ animationNames,
98
+ onStart,
99
+ onEnd,
100
+ onLoopEnd,
101
+ onFrame,
102
+ play,
103
+ pause = false,
104
+ flipX = false,
105
+ alphaTest = 0,
106
+ children,
107
+ asSprite = false,
108
+ offset,
109
+ playBackwards = false,
110
+ resetOnEnd = false,
111
+ maxItems = 1,
112
+ instanceItems = [[0, 0, 0]],
113
+ spriteDataset,
114
+ canvasRenderingContext2DSettings,
115
+ roundFramePosition = false,
116
+ meshProps = {},
117
+ ref: forwardedRef,
118
+ ...props
119
+ }: SpriteAnimatorProps & { ref?: ThreeRef<THREE.Group> }) @{
120
+ const ref = useRef<THREE.Group | null>(new THREE.Group());
121
+ const spriteData = useRef<SpriteData | null>(null);
122
+ const matRef = useRef<THREE.MeshBasicMaterial | null>(null);
123
+ const spriteRef = useRef<THREE.Mesh | THREE.InstancedMesh | null>(null);
124
+ const timerOffset = useRef(window.performance.now());
125
+ const currentFrame = useRef(startFrame);
126
+ const currentFrameName = useRef(frameName);
127
+ const fpsInterval = fps > 0 ? 1000 / fps : 0;
128
+ const [spriteTexture, setSpriteTexture] = useState(new THREE.Texture());
129
+ const totalFrames = useRef(0);
130
+ const [aspect, setAspect] = useState(new THREE.Vector3(1, 1, 1));
131
+ const flipOffset = flipX ? -1 : 1;
132
+ const pauseRef = useRef(pause);
133
+ const pos = useRef(offset);
134
+ const softEnd = useRef(false);
135
+ const { spriteObj, loadJsonAndTexture } = useSpriteLoader(
136
+ null,
137
+ null,
138
+ animationNames,
139
+ numberOfFrames,
140
+ undefined,
141
+ canvasRenderingContext2DSettings,
142
+ );
143
+ const frameNameRef = useRef(frameName);
144
+
145
+ const calculateAspectRatio = (width: number, height: number) => {
146
+ const result = new THREE.Vector3();
147
+ result.set(1, height / width, 1);
148
+ spriteRef.current?.scale.copy(result);
149
+ return result;
150
+ };
151
+
152
+ const parseSpriteDataLite = useCallback((textureData: THREE.Texture, data: SpriteData | null) => {
153
+ if (data === null) {
154
+ if (numberOfFrames) {
155
+ totalFrames.current = numberOfFrames;
156
+ if (playBackwards) currentFrame.current = numberOfFrames - 1;
157
+ spriteData.current = data;
158
+ }
159
+ } else {
160
+ spriteData.current = data;
161
+ if (Array.isArray(spriteData.current.frames)) totalFrames.current = spriteData.current.frames.length;
162
+ else if (typeof spriteData.current === 'object' && frameNameRef.current) {
163
+ totalFrames.current = spriteData.current.frames[frameNameRef.current].length;
164
+ } else totalFrames.current = 0;
165
+ if (playBackwards) currentFrame.current = totalFrames.current - 1;
166
+ const { w, h } = getFirstFrame(spriteData.current?.frames ?? [], frameNameRef.current).sourceSize;
167
+ setAspect(calculateAspectRatio(w, h));
168
+ if (matRef.current) matRef.current.map = textureData;
169
+ }
170
+ setSpriteTexture(textureData);
171
+ }, [numberOfFrames, playBackwards]);
172
+
173
+ const modifySpritePosition = useCallback((): void => {
174
+ if (!spriteData.current) return;
175
+ const { meta: { size: metaInfo }, frames } = spriteData.current;
176
+ const { w: frameW, h: frameH } = Array.isArray(frames)
177
+ ? frames[0].sourceSize
178
+ : frameName
179
+ ? frames[frameName]
180
+ ? frames[frameName][0].sourceSize
181
+ : { w: 0, h: 0 }
182
+ : { w: 0, h: 0 };
183
+ if (matRef.current?.map) {
184
+ matRef.current.map.wrapS = matRef.current.map.wrapT = THREE.RepeatWrapping;
185
+ matRef.current.map.center.set(0, 0);
186
+ matRef.current.map.repeat.set(flipOffset / (metaInfo.w / frameW), 1 / (metaInfo.h / frameH));
187
+ }
188
+ const framesV = (metaInfo.h - 1) / frameH;
189
+ const frameOffsetY = 1 / framesV;
190
+ if (matRef.current?.map) {
191
+ matRef.current.map.offset.x = 0;
192
+ matRef.current.map.offset.y = 1 - frameOffsetY;
193
+ }
194
+ onStart?.({ currentFrameName: frameName ?? '', currentFrame: currentFrame.current });
195
+ }, [flipOffset, frameName, onStart]);
196
+
197
+ const state = useMemo<SpriteAnimatorState>(() => ({
198
+ current: pos.current,
199
+ offset: pos.current,
200
+ imageUrl: textureImageURL,
201
+ hasEnded: false,
202
+ ref: forwardedRef,
203
+ }), [textureImageURL, forwardedRef]);
204
+
205
+ useImperativeHandle((forwardedRef ?? null) as any, () => ref.current!, []);
206
+ useLayoutEffect(() => { pos.current = offset; }, [offset]);
207
+
208
+ useEffect(() => {
209
+ if (spriteDataset) parseSpriteDataLite(spriteDataset.spriteTexture.clone(), spriteDataset.spriteData);
210
+ else if (textureImageURL && textureDataURL) loadJsonAndTexture(textureImageURL, textureDataURL);
211
+ }, [loadJsonAndTexture, spriteDataset, textureDataURL, textureImageURL, parseSpriteDataLite]);
212
+
213
+ useEffect(() => {
214
+ if (spriteObj) parseSpriteDataLite(spriteObj.spriteTexture.clone(), spriteObj.spriteData);
215
+ }, [spriteObj, parseSpriteDataLite]);
216
+
217
+ useEffect(() => {
218
+ state.hasEnded = false;
219
+ if (spriteData.current && playBackwards === true) {
220
+ currentFrame.current = ((spriteData.current.frames.length as number) ?? 0) - 1;
221
+ } else currentFrame.current = 0;
222
+ }, [playBackwards, state]);
223
+
224
+ useLayoutEffect(() => { modifySpritePosition(); }, [spriteTexture, flipX, modifySpritePosition]);
225
+ useEffect(() => { if (autoPlay) pauseRef.current = false; }, [autoPlay]);
226
+
227
+ useLayoutEffect(() => {
228
+ if (currentFrameName.current !== frameName && frameName) {
229
+ currentFrame.current = 0;
230
+ currentFrameName.current = frameName;
231
+ state.hasEnded = false;
232
+ if (fpsInterval <= 0) currentFrame.current = endFrame || startFrame || 0;
233
+ if (spriteData.current) {
234
+ const { w, h } = getFirstFrame(spriteData.current.frames, frameName).sourceSize;
235
+ setAspect(calculateAspectRatio(w, h));
236
+ }
237
+ }
238
+ }, [frameName, fpsInterval, state, endFrame, startFrame]);
239
+
240
+ const calculateFinalPosition = (frameW: number, frameH: number, metaInfo: Size, spriteFrames: FrameData[]) => {
241
+ const activeOffset = offset === undefined ? state.current : offset;
242
+ const targetFrame = currentFrame.current;
243
+ calculateAspectRatio(frameW, frameH);
244
+ const framesH = roundFramePosition ? Math.round((metaInfo.w - 1) / frameW) : (metaInfo.w - 1) / frameW;
245
+ const framesV = roundFramePosition ? Math.round((metaInfo.h - 1) / frameH) : (metaInfo.h - 1) / frameH;
246
+ if (!spriteFrames[targetFrame]) return;
247
+ const { frame: { x: frameX, y: frameY }, sourceSize: { w: originalSizeX, h: originalSizeY } } = spriteFrames[targetFrame];
248
+ const frameOffsetX = 1 / framesH;
249
+ const frameOffsetY = 1 / framesV;
250
+ if (matRef.current?.map) {
251
+ matRef.current.map.offset.x = flipOffset > 0
252
+ ? frameOffsetX * (frameX / originalSizeX)
253
+ : frameOffsetX * (frameX / originalSizeX) - matRef.current.map.repeat.x;
254
+ matRef.current.map.offset.y = Math.abs(1 - frameOffsetY) - frameOffsetY * (frameY / originalSizeY);
255
+ }
256
+ if (activeOffset !== undefined && activeOffset !== null) {
257
+ let frameIndex = Math.floor(activeOffset * spriteFrames.length);
258
+ frameIndex = Math.max(0, Math.min(frameIndex, spriteFrames.length - 1));
259
+ if (isNaN(frameIndex)) frameIndex = 0;
260
+ currentFrame.current = frameIndex;
261
+ } else if (playBackwards) currentFrame.current -= 1;
262
+ else currentFrame.current += 1;
263
+ };
264
+
265
+ const runAnimation = (): void => {
266
+ if (!isSpriteData(spriteData.current)) return;
267
+ const { meta: { size: metaInfo }, frames } = spriteData.current;
268
+ const { w: frameW, h: frameH } = getFirstFrame(frames, frameName).sourceSize;
269
+ const spriteFrames = Array.isArray(frames) ? frames : frameName ? frames[frameName] : [];
270
+ const activeEndFrame = endFrame || spriteFrames.length - 1;
271
+ const activeOffset = offset === undefined ? state.current : offset;
272
+ if (fpsInterval <= 0) {
273
+ currentFrame.current = endFrame || startFrame || 0;
274
+ calculateFinalPosition(frameW, frameH, metaInfo, spriteFrames);
275
+ return;
276
+ }
277
+ const now = window.performance.now();
278
+ const difference = now - timerOffset.current;
279
+ if (difference <= fpsInterval) return;
280
+ const endCondition = playBackwards ? currentFrame.current < 0 : currentFrame.current > activeEndFrame;
281
+ const onStartCondition = playBackwards ? currentFrame.current === activeEndFrame : currentFrame.current === 0;
282
+ const manualProgressEndCondition = playBackwards ? currentFrame.current < 0 : currentFrame.current >= activeEndFrame;
283
+ if (endCondition) {
284
+ currentFrame.current = loop ? (startFrame ?? 0) : 0;
285
+ if (playBackwards) currentFrame.current = activeEndFrame;
286
+ if (loop) onLoopEnd?.({ currentFrameName: frameName ?? '', currentFrame: currentFrame.current });
287
+ else {
288
+ onEnd?.({ currentFrameName: frameName ?? '', currentFrame: currentFrame.current });
289
+ state.hasEnded = !resetOnEnd;
290
+ if (resetOnEnd) pauseRef.current = true;
291
+ }
292
+ if (!loop) return;
293
+ } else if (onStartCondition) onStart?.({ currentFrameName: frameName ?? '', currentFrame: currentFrame.current });
294
+ if (activeOffset !== undefined && manualProgressEndCondition) {
295
+ if (softEnd.current === false) {
296
+ onEnd?.({ currentFrameName: frameName ?? '', currentFrame: currentFrame.current });
297
+ softEnd.current = true;
298
+ }
299
+ } else softEnd.current = false;
300
+ if (difference <= fpsInterval) return;
301
+ timerOffset.current = now - (difference % fpsInterval);
302
+ calculateFinalPosition(frameW, frameH, metaInfo, spriteFrames);
303
+ };
304
+
305
+ useFrame(() => {
306
+ if (!spriteData.current?.frames || !matRef.current?.map || pauseRef.current) return;
307
+ if (!state.hasEnded && (autoPlay || play)) {
308
+ runAnimation();
309
+ onFrame?.({ currentFrameName: currentFrameName.current, currentFrame: currentFrame.current });
310
+ }
311
+ });
312
+
313
+ function multiplyScale(initialScale = new THREE.Vector3(1, 1, 1), newScale: Scale = 1) {
314
+ if (typeof newScale === 'number') return initialScale.multiplyScalar(newScale);
315
+ if (Array.isArray(newScale)) return initialScale.multiply(new THREE.Vector3(...newScale));
316
+ if (newScale instanceof THREE.Vector3) return initialScale.multiply(newScale);
317
+ }
318
+
319
+ <group {...props as any} ref={ref} scale={multiplyScale(aspect, props.scale as Scale | undefined)}>
320
+ <context.Provider value={state}>
321
+ @if (asSprite) {
322
+ <Billboard>
323
+ <mesh ref={spriteRef as any} scale={1} geometry={geometry} {...meshProps as any}>
324
+ <meshBasicMaterial premultipliedAlpha={false} toneMapped={false} side={THREE.DoubleSide} ref={matRef} map={spriteTexture} transparent={true} alphaTest={alphaTest ?? 0} />
325
+ </mesh>
326
+ </Billboard>
327
+ } @else {
328
+ <Instances geometry={geometry} limit={maxItems ?? 1} {...meshProps as any}>
329
+ <meshBasicMaterial premultipliedAlpha={false} toneMapped={false} side={THREE.DoubleSide} ref={matRef} map={spriteTexture} transparent={true} alphaTest={alphaTest ?? 0} />
330
+ {(instanceItems ?? [0]).map((item, index) => (
331
+ <Instance key={index} ref={(instanceItems?.length === 1 ? spriteRef : null) as any} position={item as Vector3} scale={1} {...meshProps as any} />
332
+ ))}
333
+ </Instances>
334
+ }
335
+ {children}
336
+ </context.Provider>
337
+ </group>
338
+ }