@react-three/drei 9.68.5 → 9.69.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/README.md CHANGED
@@ -105,6 +105,7 @@ The `native` route of the library **does not** export `Html` or `Loader`. The de
105
105
  <li><a href="#html">Html</a></li>
106
106
  <li><a href="#cycleraycast">CycleRaycast</a></li>
107
107
  <li><a href="#select">Select</a></li>
108
+ <li><a href="#spriteanimator">Sprite Animator</a></li>
108
109
  <li><a href="#stats">Stats</a></li>
109
110
  <li><a href="#wireframe">Wireframe</a></li>
110
111
  <li><a href="#usedepthbuffer">useDepthBuffer</a></li>
@@ -2077,6 +2078,69 @@ function Foo() {
2077
2078
  const selected = useSelect()
2078
2079
  ```
2079
2080
 
2081
+ #### Sprite Animator
2082
+
2083
+ ![](https://img.shields.io/badge/-Dom only-red)
2084
+
2085
+ <p>
2086
+ <a href="https://codesandbox.io/s/r3f-sprite-animator-s12ijv"><img width="20%" src="https://codesandbox.io/api/v1/sandboxes/s12ijv/screenshot.png" alt="Demo"/></a>
2087
+ </p>
2088
+
2089
+ ```tsx
2090
+ type Props = {
2091
+ /** The start frame of the animation */
2092
+ startFrame?: number
2093
+ /** The end frame of the animation */
2094
+ endFrame?: number
2095
+ /** The desired frames per second of the animaiton */
2096
+ fps?: number
2097
+ /** The frame identifier to use, has to be one of animationNames */
2098
+ frameName?: string
2099
+ /** The scale factor of the aspect ratio */
2100
+ scaleFactor?: number
2101
+ /** The URL of the texture JSON (if using JSON-Array or JSON-Hash) */
2102
+ textureDataURL?: string
2103
+ /** The URL of the texture image */
2104
+ textureImageURL?: string
2105
+ /** Whether or not the animation should loop */
2106
+ loop?: boolean
2107
+ /** The number of frames of the animation (required if using plain spritesheet without JSON) */
2108
+ numberOfFrames?: number
2109
+ /** Whether or not the animation should auto-start when all assets are loaded */
2110
+ autoPlay?: boolean
2111
+ /** The animation names of the spritesheet (if the spritesheet -with JSON- contains more animation sequences) */
2112
+ animationNames?: Array<string>
2113
+ /** Event callback when the animation starts */
2114
+ onStart?: Function
2115
+ /** Event callback when the animation ends */
2116
+ onEnd?: Function
2117
+ /** Event callback when the animation loops */
2118
+ onLoopEnd?: Function
2119
+ /** Event callback when each frame changes */
2120
+ onFrame?: Function
2121
+ /** Control when the animation runs */
2122
+ play?: boolean
2123
+ /** Control when the animation pauses */
2124
+ pause?: boolean
2125
+ }
2126
+ ```
2127
+
2128
+ The SpriteAnimator component provided by drei is a powerful tool for animating sprites in a simple and efficient manner. It allows you to create sprite animations by cycling through a sequence of frames from a sprite sheet image or JSON data.
2129
+ Notes:
2130
+ The SpriteAnimator component internally uses the useFrame hook from react-three-fiber (r3f) for efficient frame updates and rendering.
2131
+
2132
+ ```jsx
2133
+ <SpriteAnimator
2134
+ position={[-3.5, -2.0, 2.5]}
2135
+ startFrame={0}
2136
+ scaleFactor={0.125}
2137
+ autoPlay={true}
2138
+ loop={true}
2139
+ numberOfFrames={16}
2140
+ textureImageURL={'./alien.png'}
2141
+ />
2142
+ ```
2143
+
2080
2144
  #### Stats
2081
2145
 
2082
2146
  [![](https://img.shields.io/badge/-storybook-%23ff69b4)](https://drei.vercel.app/?path=/story/misc-stats--default-story)
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0});var e=require("react"),r=require("@react-three/fiber"),t=require("three");function n(e){if(e&&e.__esModule)return e;var r=Object.create(null);return e&&Object.keys(e).forEach((function(t){if("default"!==t){var n=Object.getOwnPropertyDescriptor(e,t);Object.defineProperty(r,t,n.get?n:{enumerable:!0,get:function(){return e[t]}})}})),r.default=e,Object.freeze(r)}var a=n(e),u=n(t);exports.SpriteAnimator=({startFrame:e,endFrame:t,fps:n,frameName:c,scaleFactor:s,maxScale:o,textureDataURL:f,textureImageURL:i,loop:m,numberOfFrames:l,autoPlay:p,animationNames:h,onStart:w,onEnd:y,onLoopEnd:d,onFrame:S,play:g,pause:x,flipX:b,alphaTest:z,children:F,...A},E)=>{const O=r.useThree((e=>e.viewport)),R=a.useRef(null),[j,v]=a.useState(!1),L=a.useRef(),P=a.useRef(),T=a.useRef(window.performance.now()),M=a.useRef(),N=a.useRef(e||0),k=a.useRef(c||""),_=1e3/(n||30),[q,C]=a.useState(new u.Texture),D=a.useRef(0),[I,U]=a.useState([1,1,1]),W=s||.1,X=b?-1:1;const B=(e,r,t)=>{const n=r*(O.aspect>e/r?O.width/e:O.height/r),a=e*(O.aspect>e/r?O.width/e:O.height/r)*t,u=n*t,c=null!=o?o:1;let s=Math.min(c,a),f=Math.min(c,u);return a>c&&(s=c,f=u/a*c),P.current.scale.set(s,f,1),[s,f,1]};a.useEffect((()=>{if(f&&i)!function(e,r,t){const n=new u.TextureLoader,a=fetch(e).then((e=>e.json())),c=new Promise((e=>{n.load(r,e)}));Promise.all([a,c]).then((e=>{t(e[0],e[1])}))}(f,i,G);else if(i){const e=new u.TextureLoader;new Promise((r=>{e.load(i,r)})).then((e=>{G(null,e)}))}}),[]),a.useLayoutEffect((()=>{J()}),[q]),a.useEffect((()=>{}),[x]),a.useEffect((()=>{k.current!==c&&c&&(N.current=0,k.current=c)}),[c]);const G=(e,r)=>{if(null===e){if(r&&l){const e=r.image.width,t=r.image.height,n=e/l,a=t;if(M.current=r,D.current=l,R.current={frames:[],meta:{version:"1.0",size:{w:e,h:t},scale:"1"}},parseInt(n.toString(),10)===n)for(let e=0;e<l;e++)R.current.frames.push({frame:{x:e*n,y:0,w:n,h:a},rotated:!1,trimmed:!1,spriteSourceSize:{x:0,y:0,w:n,h:a},sourceSize:{w:n,h:t}})}}else if(r){R.current=e,R.current.frames=Array.isArray(e.frames)?e.frames:H(),D.current=Array.isArray(e.frames)?e.frames.length:Object.keys(e.frames).length,M.current=r;const{w:t,h:n}=K(e.frames).sourceSize,a=B(t,n,W);U(a),L.current&&(L.current.map=r)}r.premultiplyAlpha=!1,C(r)},H=()=>{const e={},r=R.current,t=h;if(t)for(let n=0;n<t.length;n++){e[t[n]]=[];for(let a in r.frames){const u=r.frames[a],c=u.frame,s=c.x,o=c.y,f=c.w,i=c.h,m=u.sourceSize.w,l=u.sourceSize.h;"string"==typeof a&&-1!==a.toLowerCase().indexOf(t[n].toLowerCase())&&e[t[n]].push({x:s,y:o,w:f,h:i,frame:c,sourceSize:{w:m,h:l}})}}return e},J=()=>{if(!R.current)return;const{meta:{size:e},frames:r}=R.current,{w:t,h:n}=Array.isArray(r)?r[0].sourceSize:c&&r[c]?r[c][0].sourceSize:{w:0,h:0};L.current.map.wrapS=L.current.map.wrapT=u.RepeatWrapping,L.current.map.center.set(0,0),L.current.map.repeat.set(1*X/(e.w/t),1/(e.h/n));const a=1/((e.h-1)/n);L.current.map.offset.x=0,L.current.map.offset.y=1-a,v(!0),w&&w({currentFrameName:c,currentFrame:N.current})};r.useFrame(((r,n)=>{var a,u;null!=(a=R.current)&&a.frames&&null!=(u=L.current)&&u.map&&(x||(p||g)&&((()=>{const r=window.performance.now(),n=r-T.current,{meta:{size:a},frames:u}=R.current,{w:s,h:o}=K(u).sourceSize,f=Array.isArray(u)?u:c?u[c]:[];let i=0,l=0;const p=t||f.length-1;if(N.current>p&&(N.current=m&&null!=e?e:0,m?null==d||d({currentFrameName:c,currentFrame:N.current}):null==y||y({currentFrameName:c,currentFrame:N.current}),!m))return;if(n<=_)return;T.current=r-n%_,B(s,o,W);const h=(a.w-1)/s,w=(a.h-1)/o,{frame:{x:S,y:g},sourceSize:{w:x,h:b}}=f[N.current],z=1/h,F=1/w;i=X>0?z*(S/x):z*(S/x)-L.current.map.repeat.x,l=Math.abs(1-F)-F*(g/b),L.current.map.offset.x=i,L.current.map.offset.y=l,N.current+=1})(),S&&S({currentFrameName:k.current,currentFrame:N.current})))}));const K=e=>{if(Array.isArray(e))return e[0];if("object"==typeof e&&null!==e){return e[Object.keys(e)[0]][0]}return{w:0,h:0}};return a.createElement("group",A,a.createElement(a.Suspense,{fallback:null},a.createElement("sprite",{ref:P,scale:I},a.createElement("spriteMaterial",{ref:L,map:q,premultipliedAlpha:!1,transparent:!0,alphaTest:null!=z?z:0}))),F)};
@@ -0,0 +1,25 @@
1
+ import * as React from 'react';
2
+ export declare type SpriteAnimatorProps = {
3
+ startFrame?: number;
4
+ endFrame?: number;
5
+ fps?: number;
6
+ frameName?: string;
7
+ scaleFactor?: number;
8
+ maxScale?: number;
9
+ textureDataURL?: string;
10
+ textureImageURL: string;
11
+ loop?: boolean;
12
+ numberOfFrames?: number;
13
+ autoPlay?: boolean;
14
+ animationNames?: Array<string>;
15
+ onStart?: Function;
16
+ onEnd?: Function;
17
+ onLoopEnd?: Function;
18
+ onFrame?: Function;
19
+ play?: boolean;
20
+ pause?: boolean;
21
+ flipX?: boolean;
22
+ position?: Array<number>;
23
+ alphaTest?: number;
24
+ } & JSX.IntrinsicElements['group'];
25
+ export declare const SpriteAnimator: React.FC<SpriteAnimatorProps>;
@@ -0,0 +1,358 @@
1
+ import * as React from 'react';
2
+ import { useThree, useFrame } from '@react-three/fiber';
3
+ import * as THREE from 'three';
4
+
5
+ const SpriteAnimator = ({
6
+ startFrame,
7
+ endFrame,
8
+ fps,
9
+ frameName,
10
+ scaleFactor,
11
+ maxScale,
12
+ textureDataURL,
13
+ textureImageURL,
14
+ loop,
15
+ numberOfFrames,
16
+ autoPlay,
17
+ animationNames,
18
+ onStart,
19
+ onEnd,
20
+ onLoopEnd,
21
+ onFrame,
22
+ play,
23
+ pause,
24
+ flipX,
25
+ alphaTest,
26
+ children,
27
+ ...props
28
+ }, fref) => {
29
+ const v = useThree(state => state.viewport);
30
+ const spriteData = React.useRef(null);
31
+ const [isJsonReady, setJsonReady] = React.useState(false);
32
+ const matRef = React.useRef();
33
+ const spriteRef = React.useRef();
34
+ const timerOffset = React.useRef(window.performance.now());
35
+ const textureData = React.useRef();
36
+ const currentFrame = React.useRef(startFrame || 0);
37
+ const currentFrameName = React.useRef(frameName || '');
38
+ const fpsInterval = 1000 / (fps || 30);
39
+ const [spriteTexture, setSpriteTexture] = React.useState(new THREE.Texture());
40
+ const totalFrames = React.useRef(0);
41
+ const [aspect, setAspect] = React.useState([1, 1, 1]);
42
+ const aspectFactor = scaleFactor || 0.1;
43
+ const flipOffset = flipX ? -1 : 1;
44
+
45
+ function loadJsonAndTextureAndExecuteCallback(jsonUrl, textureUrl, callback) {
46
+ const textureLoader = new THREE.TextureLoader();
47
+ const jsonPromise = fetch(jsonUrl).then(response => response.json());
48
+ const texturePromise = new Promise(resolve => {
49
+ textureLoader.load(textureUrl, resolve);
50
+ });
51
+ Promise.all([jsonPromise, texturePromise]).then(response => {
52
+ callback(response[0], response[1]);
53
+ });
54
+ }
55
+
56
+ const calculateAspectRatio = (width, height, factor) => {
57
+ const adaptedHeight = height * (v.aspect > width / height ? v.width / width : v.height / height);
58
+ const adaptedWidth = width * (v.aspect > width / height ? v.width / width : v.height / height);
59
+ const scaleX = adaptedWidth * factor;
60
+ const scaleY = adaptedHeight * factor;
61
+ const currentMaxScale = maxScale !== null && maxScale !== void 0 ? maxScale : 1; // Calculate the maximum scale based on the aspect ratio and max scale limit
62
+
63
+ let finalMaxScaleW = Math.min(currentMaxScale, scaleX);
64
+ let finalMaxScaleH = Math.min(currentMaxScale, scaleY); // Ensure that scaleX and scaleY do not exceed the max scale while maintaining aspect ratio
65
+
66
+ if (scaleX > currentMaxScale) {
67
+ finalMaxScaleW = currentMaxScale;
68
+ finalMaxScaleH = scaleY / scaleX * currentMaxScale;
69
+ }
70
+
71
+ spriteRef.current.scale.set(finalMaxScaleW, finalMaxScaleH, 1);
72
+ return [finalMaxScaleW, finalMaxScaleH, 1];
73
+ }; // initial loads
74
+
75
+
76
+ React.useEffect(() => {
77
+ if (textureDataURL && textureImageURL) {
78
+ loadJsonAndTextureAndExecuteCallback(textureDataURL, textureImageURL, parseSpriteData);
79
+ } else if (textureImageURL) {
80
+ // only load the texture, this is an image sprite only
81
+ const textureLoader = new THREE.TextureLoader();
82
+ new Promise(resolve => {
83
+ textureLoader.load(textureImageURL, resolve);
84
+ }).then(texture => {
85
+ parseSpriteData(null, texture);
86
+ });
87
+ }
88
+ }, []);
89
+ React.useLayoutEffect(() => {
90
+ modifySpritePosition();
91
+ }, [spriteTexture]);
92
+ React.useEffect(() => {
93
+ }, [pause]);
94
+ React.useEffect(() => {
95
+ if (currentFrameName.current !== frameName && frameName) {
96
+ currentFrame.current = 0;
97
+ currentFrameName.current = frameName;
98
+ }
99
+ }, [frameName]);
100
+
101
+ const parseSpriteData = (json, _spriteTexture) => {
102
+ // sprite only case
103
+ if (json === null) {
104
+ if (_spriteTexture && numberOfFrames) {
105
+ //get size from texture
106
+ const width = _spriteTexture.image.width;
107
+ const height = _spriteTexture.image.height;
108
+ const frameWidth = width / numberOfFrames;
109
+ const frameHeight = height;
110
+ textureData.current = _spriteTexture;
111
+ totalFrames.current = numberOfFrames;
112
+ spriteData.current = {
113
+ frames: [],
114
+ meta: {
115
+ version: '1.0',
116
+ size: {
117
+ w: width,
118
+ h: height
119
+ },
120
+ scale: '1'
121
+ }
122
+ };
123
+
124
+ if (parseInt(frameWidth.toString(), 10) === frameWidth) {
125
+ // if it fits
126
+ for (let i = 0; i < numberOfFrames; i++) {
127
+ spriteData.current.frames.push({
128
+ frame: {
129
+ x: i * frameWidth,
130
+ y: 0,
131
+ w: frameWidth,
132
+ h: frameHeight
133
+ },
134
+ rotated: false,
135
+ trimmed: false,
136
+ spriteSourceSize: {
137
+ x: 0,
138
+ y: 0,
139
+ w: frameWidth,
140
+ h: frameHeight
141
+ },
142
+ sourceSize: {
143
+ w: frameWidth,
144
+ h: height
145
+ }
146
+ });
147
+ }
148
+ }
149
+ }
150
+ } else if (_spriteTexture) {
151
+ spriteData.current = json;
152
+ spriteData.current.frames = Array.isArray(json.frames) ? json.frames : parseFrames();
153
+ totalFrames.current = Array.isArray(json.frames) ? json.frames.length : Object.keys(json.frames).length;
154
+ textureData.current = _spriteTexture;
155
+ const {
156
+ w,
157
+ h
158
+ } = getFirstItem(json.frames).sourceSize;
159
+ const aspect = calculateAspectRatio(w, h, aspectFactor);
160
+ setAspect(aspect);
161
+
162
+ if (matRef.current) {
163
+ matRef.current.map = _spriteTexture;
164
+ }
165
+ }
166
+
167
+ _spriteTexture.premultiplyAlpha = false;
168
+ setSpriteTexture(_spriteTexture);
169
+ }; // for frame based JSON Hash sprite data
170
+
171
+
172
+ const parseFrames = () => {
173
+ const sprites = {};
174
+ const data = spriteData.current;
175
+ const delimiters = animationNames;
176
+
177
+ if (delimiters) {
178
+ for (let i = 0; i < delimiters.length; i++) {
179
+ sprites[delimiters[i]] = [];
180
+
181
+ for (let innerKey in data['frames']) {
182
+ const value = data['frames'][innerKey];
183
+ const frameData = value['frame'];
184
+ const x = frameData['x'];
185
+ const y = frameData['y'];
186
+ const width = frameData['w'];
187
+ const height = frameData['h'];
188
+ const sourceWidth = value['sourceSize']['w'];
189
+ const sourceHeight = value['sourceSize']['h'];
190
+
191
+ if (typeof innerKey === 'string' && innerKey.toLowerCase().indexOf(delimiters[i].toLowerCase()) !== -1) {
192
+ sprites[delimiters[i]].push({
193
+ x: x,
194
+ y: y,
195
+ w: width,
196
+ h: height,
197
+ frame: frameData,
198
+ sourceSize: {
199
+ w: sourceWidth,
200
+ h: sourceHeight
201
+ }
202
+ });
203
+ }
204
+ }
205
+ }
206
+ }
207
+
208
+ return sprites;
209
+ }; // modify the sprite material after json is parsed and state updated
210
+
211
+
212
+ const modifySpritePosition = () => {
213
+ if (!spriteData.current) return;
214
+ const {
215
+ meta: {
216
+ size: metaInfo
217
+ },
218
+ frames
219
+ } = spriteData.current;
220
+ const {
221
+ w: frameW,
222
+ h: frameH
223
+ } = Array.isArray(frames) ? frames[0].sourceSize : frameName ? frames[frameName] ? frames[frameName][0].sourceSize : {
224
+ w: 0,
225
+ h: 0
226
+ } : {
227
+ w: 0,
228
+ h: 0
229
+ };
230
+ matRef.current.map.wrapS = matRef.current.map.wrapT = THREE.RepeatWrapping;
231
+ matRef.current.map.center.set(0, 0);
232
+ matRef.current.map.repeat.set(1 * flipOffset / (metaInfo.w / frameW), 1 / (metaInfo.h / frameH)); //const framesH = (metaInfo.w - 1) / frameW
233
+
234
+ const framesV = (metaInfo.h - 1) / frameH;
235
+ const frameOffsetY = 1 / framesV;
236
+ matRef.current.map.offset.x = 0.0; //-matRef.current.map.repeat.x
237
+
238
+ matRef.current.map.offset.y = 1 - frameOffsetY;
239
+ setJsonReady(true);
240
+ if (onStart) onStart({
241
+ currentFrameName: frameName,
242
+ currentFrame: currentFrame.current
243
+ });
244
+ }; // run the animation on each frame
245
+
246
+
247
+ const runAnimation = () => {
248
+ //if (!frameName) return
249
+ const now = window.performance.now();
250
+ const diff = now - timerOffset.current;
251
+ const {
252
+ meta: {
253
+ size: metaInfo
254
+ },
255
+ frames
256
+ } = spriteData.current;
257
+ const {
258
+ w: frameW,
259
+ h: frameH
260
+ } = getFirstItem(frames).sourceSize;
261
+ const spriteFrames = Array.isArray(frames) ? frames : frameName ? frames[frameName] : [];
262
+ let finalValX = 0;
263
+ let finalValY = 0;
264
+
265
+ const _endFrame = endFrame || spriteFrames.length - 1;
266
+
267
+ if (currentFrame.current > _endFrame) {
268
+ currentFrame.current = loop ? startFrame !== null && startFrame !== void 0 ? startFrame : 0 : 0;
269
+
270
+ if (loop) {
271
+ onLoopEnd == null ? void 0 : onLoopEnd({
272
+ currentFrameName: frameName,
273
+ currentFrame: currentFrame.current
274
+ });
275
+ } else {
276
+ onEnd == null ? void 0 : onEnd({
277
+ currentFrameName: frameName,
278
+ currentFrame: currentFrame.current
279
+ });
280
+ }
281
+
282
+ if (!loop) return;
283
+ }
284
+
285
+ if (diff <= fpsInterval) return;
286
+ timerOffset.current = now - diff % fpsInterval;
287
+ calculateAspectRatio(frameW, frameH, aspectFactor);
288
+ const framesH = (metaInfo.w - 1) / frameW;
289
+ const framesV = (metaInfo.h - 1) / frameH;
290
+ const {
291
+ frame: {
292
+ x: frameX,
293
+ y: frameY
294
+ },
295
+ sourceSize: {
296
+ w: originalSizeX,
297
+ h: originalSizeY
298
+ }
299
+ } = spriteFrames[currentFrame.current];
300
+ const frameOffsetX = 1 / framesH;
301
+ const frameOffsetY = 1 / framesV;
302
+ finalValX = flipOffset > 0 ? frameOffsetX * (frameX / originalSizeX) : frameOffsetX * (frameX / originalSizeX) - matRef.current.map.repeat.x;
303
+ finalValY = Math.abs(1 - frameOffsetY) - frameOffsetY * (frameY / originalSizeY);
304
+ matRef.current.map.offset.x = finalValX;
305
+ matRef.current.map.offset.y = finalValY;
306
+ currentFrame.current += 1;
307
+ }; // *** Warning! It runs on every frame! ***
308
+
309
+
310
+ useFrame((state, delta) => {
311
+ var _spriteData$current, _matRef$current;
312
+
313
+ if (!((_spriteData$current = spriteData.current) != null && _spriteData$current.frames) || !((_matRef$current = matRef.current) != null && _matRef$current.map)) {
314
+ return;
315
+ }
316
+
317
+ if (pause) {
318
+ return;
319
+ }
320
+
321
+ if (autoPlay || play) {
322
+ runAnimation();
323
+ onFrame && onFrame({
324
+ currentFrameName: currentFrameName.current,
325
+ currentFrame: currentFrame.current
326
+ });
327
+ }
328
+ }); // utils
329
+
330
+ const getFirstItem = param => {
331
+ if (Array.isArray(param)) {
332
+ return param[0];
333
+ } else if (typeof param === 'object' && param !== null) {
334
+ const keys = Object.keys(param);
335
+ return param[keys[0]][0];
336
+ } else {
337
+ return {
338
+ w: 0,
339
+ h: 0
340
+ };
341
+ }
342
+ };
343
+
344
+ return /*#__PURE__*/React.createElement("group", props, /*#__PURE__*/React.createElement(React.Suspense, {
345
+ fallback: null
346
+ }, /*#__PURE__*/React.createElement("sprite", {
347
+ ref: spriteRef,
348
+ scale: aspect
349
+ }, /*#__PURE__*/React.createElement("spriteMaterial", {
350
+ ref: matRef,
351
+ map: spriteTexture,
352
+ premultipliedAlpha: false,
353
+ transparent: true,
354
+ alphaTest: alphaTest !== null && alphaTest !== void 0 ? alphaTest : 0.0
355
+ }))), children);
356
+ };
357
+
358
+ export { SpriteAnimator };