diff --git a/README.md b/README.md
index 66b437249..e546740e6 100644
--- a/README.md
+++ b/README.md
@@ -2545,8 +2545,16 @@ type Props = {
alphaTest?: number
/** Displays the texture on a SpriteGeometry always facing the camera, if set to false, it renders on a PlaneGeometry */
asSprite?: boolean
+ /** Allows for manual update of the sprite animation e.g: via ScrollControls */
+ offset?: number
+ /** Allows the sprite animation to start from the end towards the start */
+ playBackwards: boolean
/** Allows the animation to be paused after it ended so it can be restarted on demand via auto */
resetOnEnd?: boolean
+ /** An array of items to create instances from */
+ instanceItems?: any[]
+ /** The max number of items to instance (optional) */
+ maxItems?: number
}
```
@@ -2570,6 +2578,37 @@ Notes:
/>
```
+ScrollControls example
+
+```jsx
+;
+
+
+
+
+
+function FireScroll() {
+ const sprite = useSpriteAnimator()
+ const scroll = useScroll()
+ const ref = React.useRef()
+ useFrame(() => {
+ if (sprite && scroll) {
+ sprite.current = scroll.offset
+ }
+ })
+
+ return null
+}
+```
+
#### Stats
[![](https://img.shields.io/badge/-storybook-%23ff69b4)](https://drei.vercel.app/?path=/story/misc-stats--default-story)
diff --git a/src/core/SpriteAnimator.tsx b/src/core/SpriteAnimator.tsx
index d56aaae46..7b31f075c 100644
--- a/src/core/SpriteAnimator.tsx
+++ b/src/core/SpriteAnimator.tsx
@@ -1,6 +1,7 @@
import * as React from 'react'
import { useFrame, Vector3 } from '@react-three/fiber'
import * as THREE from 'three'
+import { Instances, Instance } from './Instances'
export type SpriteAnimatorProps = {
startFrame?: number
@@ -23,9 +24,28 @@ export type SpriteAnimatorProps = {
position?: Array
alphaTest?: number
asSprite?: boolean
+ offset?: number
+ playBackwards?: boolean
resetOnEnd?: boolean
+ maxItems?: number
+ instanceItems?: any[]
} & JSX.IntrinsicElements['group']
+type SpriteAnimatorState = {
+ /** The user-defined, mutable, current goal position along the curve, it may be >1 or <0 */
+ current: number | undefined
+ /** The 0-1 normalised and damped current goal position along curve */
+ offset: number | undefined
+ hasEnded: boolean | undefined
+ ref: React.MutableRefObject | undefined | null | ((instance: any) => void)
+}
+
+const context = React.createContext(null!)
+
+export function useSpriteAnimator() {
+ return React.useContext(context) as SpriteAnimatorState
+}
+
export const SpriteAnimator: React.FC = /* @__PURE__ */ React.forwardRef(
(
{
@@ -49,14 +69,18 @@ export const SpriteAnimator: React.FC = /* @__PURE__ */ Rea
alphaTest,
children,
asSprite,
+ offset,
+ playBackwards,
resetOnEnd,
+ maxItems,
+ instanceItems,
...props
},
fref
) => {
+ const ref = React.useRef()
const spriteData = React.useRef(null)
- const [isJsonReady, setJsonReady] = React.useState(false)
- const hasEnded = React.useRef(false)
+ //const hasEnded = React.useRef(false)
const matRef = React.useRef()
const spriteRef = React.useRef()
const timerOffset = React.useRef(window.performance.now())
@@ -70,6 +94,30 @@ export const SpriteAnimator: React.FC = /* @__PURE__ */ Rea
const flipOffset = flipX ? -1 : 1
const [displayAsSprite, setDisplayAsSprite] = React.useState(asSprite ?? true)
const pauseRef = React.useRef(pause)
+ const pos = React.useRef(offset)
+ const softEnd = React.useRef(false)
+ const frameBuffer = React.useRef([])
+ //
+
+ function reset() {}
+
+ const state = React.useMemo(
+ () => ({
+ current: pos.current,
+ offset: pos.current,
+ imageUrl: textureImageURL,
+ reset: reset,
+ hasEnded: false,
+ ref: fref,
+ }),
+ [textureImageURL]
+ )
+
+ React.useImperativeHandle(fref, () => ref.current, [])
+
+ React.useLayoutEffect(() => {
+ pos.current = offset
+ }, [offset])
function loadJsonAndTextureAndExecuteCallback(
jsonUrl: string,
@@ -114,6 +162,16 @@ export const SpriteAnimator: React.FC = /* @__PURE__ */ Rea
setDisplayAsSprite(asSprite ?? true)
}, [asSprite])
+ // support backwards play
+ React.useEffect(() => {
+ state.hasEnded = false
+ if (spriteData.current && playBackwards === true) {
+ currentFrame.current = spriteData.current.frames.length - 1
+ } else {
+ currentFrame.current = 0
+ }
+ }, [playBackwards])
+
React.useLayoutEffect(() => {
modifySpritePosition()
}, [spriteTexture, flipX])
@@ -128,7 +186,7 @@ export const SpriteAnimator: React.FC = /* @__PURE__ */ Rea
if (currentFrameName.current !== frameName && frameName) {
currentFrame.current = 0
currentFrameName.current = frameName
- hasEnded.current = false
+ state.hasEnded = false
modifySpritePosition()
if (spriteData.current) {
const { w, h } = getFirstItem(spriteData.current.frames).sourceSize
@@ -138,6 +196,7 @@ export const SpriteAnimator: React.FC = /* @__PURE__ */ Rea
}
}, [frameName])
+ // parse sprite-data from JSON file (jsonHash or jsonArray)
const parseSpriteData = (json: any, _spriteTexture: THREE.Texture): void => {
// sprite only case
if (json === null) {
@@ -149,6 +208,11 @@ export const SpriteAnimator: React.FC = /* @__PURE__ */ Rea
const frameHeight = height
textureData.current = _spriteTexture
totalFrames.current = numberOfFrames
+
+ if (playBackwards) {
+ currentFrame.current = numberOfFrames - 1
+ }
+
spriteData.current = {
frames: [],
meta: {
@@ -177,6 +241,10 @@ export const SpriteAnimator: React.FC = /* @__PURE__ */ Rea
totalFrames.current = Array.isArray(json.frames) ? json.frames.length : Object.keys(json.frames).length
textureData.current = _spriteTexture
+ if (playBackwards) {
+ currentFrame.current = totalFrames.current - 1
+ }
+
const { w, h } = getFirstItem(json.frames).sourceSize
const aspect = calculateAspectRatio(w, h)
@@ -186,6 +254,21 @@ export const SpriteAnimator: React.FC = /* @__PURE__ */ Rea
}
}
+ // buffer for instanced
+ if (instanceItems) {
+ for (var i = 0; i < instanceItems.length; i++) {
+ const keys = Object.keys(spriteData.current.frames)
+ const randomKey = keys[Math.floor(Math.random() * keys.length)]
+
+ frameBuffer.current.push({
+ key: i,
+ frames: spriteData.current.frames,
+ selectedFrame: randomKey,
+ offset: { x: 0, y: 0 },
+ })
+ }
+ }
+
_spriteTexture.premultiplyAlpha = false
setSpriteTexture(_spriteTexture)
@@ -259,7 +342,6 @@ export const SpriteAnimator: React.FC = /* @__PURE__ */ Rea
matRef.current.map.offset.x = 0.0 //-matRef.current.map.repeat.x
matRef.current.map.offset.y = 1 - frameOffsetY
- setJsonReady(true)
if (onStart) onStart({ currentFrameName: frameName, currentFrame: currentFrame.current })
}
@@ -274,11 +356,22 @@ export const SpriteAnimator: React.FC = /* @__PURE__ */ Rea
} = spriteData.current
const { w: frameW, h: frameH } = getFirstItem(frames).sourceSize
const spriteFrames = Array.isArray(frames) ? frames : frameName ? frames[frameName] : []
-
const _endFrame = endFrame || spriteFrames.length - 1
- if (currentFrame.current > _endFrame) {
+ var _offset = offset === undefined ? state.current : offset
+
+ // conditionals to support backwards play
+ var endCondition = playBackwards ? currentFrame.current < 0 : currentFrame.current > _endFrame
+ var onStartCondition = playBackwards ? currentFrame.current === _endFrame : currentFrame.current === 0
+ var manualProgressEndCondition = playBackwards ? currentFrame.current < 0 : currentFrame.current >= _endFrame
+
+ if (endCondition) {
currentFrame.current = loop ? startFrame ?? 0 : 0
+
+ if (playBackwards) {
+ currentFrame.current = _endFrame
+ }
+
if (loop) {
onLoopEnd?.({
currentFrameName: frameName,
@@ -289,7 +382,12 @@ export const SpriteAnimator: React.FC = /* @__PURE__ */ Rea
currentFrameName: frameName,
currentFrame: currentFrame.current,
})
- hasEnded.current = resetOnEnd ? false : true
+
+ if (!_offset) {
+ console.log('will end')
+ }
+
+ state.hasEnded = resetOnEnd ? false : true
if (resetOnEnd) {
pauseRef.current = true
//calculateFinalPosition(frameW, frameH, metaInfo, spriteFrames)
@@ -297,14 +395,32 @@ export const SpriteAnimator: React.FC = /* @__PURE__ */ Rea
}
if (!loop) return
+ } else if (onStartCondition) {
+ onStart?.({
+ currentFrameName: frameName,
+ currentFrame: currentFrame.current,
+ })
+ }
+
+ // for manual update
+ if (_offset !== undefined && manualProgressEndCondition) {
+ if (softEnd.current === false) {
+ onEnd?.({
+ currentFrameName: frameName,
+ currentFrame: currentFrame.current,
+ })
+ softEnd.current = true
+ }
+ } else {
+ // same for start?
+ softEnd.current = false
}
+ // clock to limit fps
if (diff <= fpsInterval) return
timerOffset.current = now - (diff % fpsInterval)
calculateFinalPosition(frameW, frameH, metaInfo, spriteFrames)
-
- currentFrame.current += 1
}
const calculateFinalPosition = (
@@ -313,15 +429,23 @@ export const SpriteAnimator: React.FC = /* @__PURE__ */ Rea
metaInfo: { w: number; h: number },
spriteFrames: { frame: { x: any; y: any }; sourceSize: { w: any; h: any } }[]
) => {
+ // get the manual update offset to find the next frame
+ var _offset = offset === undefined ? state.current : offset
+ const targetFrame = currentFrame.current
let finalValX = 0
let finalValY = 0
calculateAspectRatio(frameW, frameH)
const framesH = (metaInfo.w - 1) / frameW
const framesV = (metaInfo.h - 1) / frameH
+ if (!spriteFrames[targetFrame]) {
+ return
+ }
+
const {
frame: { x: frameX, y: frameY },
sourceSize: { w: originalSizeX, h: originalSizeY },
- } = spriteFrames[currentFrame.current]
+ } = spriteFrames[targetFrame]
+
const frameOffsetX = 1 / framesH
const frameOffsetY = 1 / framesV
finalValX =
@@ -332,6 +456,28 @@ export const SpriteAnimator: React.FC = /* @__PURE__ */ Rea
matRef.current.map.offset.x = finalValX
matRef.current.map.offset.y = finalValY
+
+ // if manual update is active
+ if (_offset !== undefined && _offset !== null) {
+ // Calculate the frame index, based on offset given from the provider
+ let frameIndex = Math.floor(_offset * spriteFrames.length)
+
+ // Ensure the frame index is within the valid range
+ frameIndex = Math.max(0, Math.min(frameIndex, spriteFrames.length - 1))
+
+ if (isNaN(frameIndex)) {
+ console.log('nan frame detected')
+ frameIndex = 0 //fallback
+ }
+ currentFrame.current = frameIndex
+ } else {
+ // auto update
+ if (playBackwards) {
+ currentFrame.current -= 1
+ } else {
+ currentFrame.current += 1
+ }
+ }
}
// *** Warning! It runs on every frame! ***
@@ -344,7 +490,7 @@ export const SpriteAnimator: React.FC = /* @__PURE__ */ Rea
return
}
- if (!hasEnded.current && (autoPlay || play)) {
+ if (!state.hasEnded && (autoPlay || play)) {
runAnimation()
onFrame && onFrame({ currentFrameName: currentFrameName.current, currentFrame: currentFrame.current })
}
@@ -363,34 +509,57 @@ export const SpriteAnimator: React.FC = /* @__PURE__ */ Rea
}
return (
-
-
- {displayAsSprite && (
-
-
-
- )}
- {!displayAsSprite && (
-
-
-
-
- )}
-
- {children}
+
+
+
+ {displayAsSprite && (
+
+
+
+ )}
+ {!displayAsSprite && (
+
+
+
+
+ {(instanceItems ?? [0]).map((item, index) => {
+ const texture = spriteTexture.clone()
+ if (matRef.current && frameBuffer.current[index]) {
+ texture.offset.set(frameBuffer.current[index].offset.x, frameBuffer.current[index].offset.y) // Set the offset for this item
+ }
+
+ return (
+
+
+
+ )
+ })}
+
+ )}
+
+ {children}
+
)
}