UNPKG

@react-three/drei

Version:

useful add-ons for react-three-fiber

414 lines (397 loc) 14.9 kB
import _extends from '@babel/runtime/helpers/esm/extends'; import * as React from 'react'; import { useFrame } from '@react-three/fiber'; import * as THREE from 'three'; import { Instances, Instance } from './Instances.js'; import { Billboard } from './Billboard.js'; import { useSpriteLoader, getFirstFrame } from './useSpriteLoader.js'; // Frame-related types const context = /*#__PURE__*/React.createContext(null); function useSpriteAnimator() { return React.useContext(context); } // Type guard for SpriteData function isSpriteData(data) { return data !== null && 'meta' in data && 'frames' in data; } const geometry = /* @__PURE__ */new THREE.PlaneGeometry(1, 1); const SpriteAnimator = /* @__PURE__ */React.forwardRef(({ startFrame = 0, endFrame, fps = 30, frameName = '', textureDataURL, textureImageURL, loop = false, numberOfFrames = 1, autoPlay = true, animationNames, onStart, onEnd, onLoopEnd, onFrame, play, pause = false, flipX = false, alphaTest = 0.0, children, asSprite = false, offset, playBackwards = false, resetOnEnd = false, maxItems = 1, instanceItems = [[0, 0, 0]], spriteDataset, canvasRenderingContext2DSettings, roundFramePosition = false, meshProps = {}, ...props }, fref) => { const ref = React.useRef(new THREE.Group()); const spriteData = React.useRef(null); const matRef = React.useRef(null); const spriteRef = React.useRef(null); const timerOffset = React.useRef(window.performance.now()); const currentFrame = React.useRef(startFrame); const currentFrameName = React.useRef(frameName); const fpsInterval = fps > 0 ? 1000 / fps : 0; const [spriteTexture, setSpriteTexture] = React.useState(new THREE.Texture()); const totalFrames = React.useRef(0); const [aspect, setAspect] = React.useState(new THREE.Vector3(1, 1, 1)); const flipOffset = flipX ? -1 : 1; const pauseRef = React.useRef(pause); const pos = React.useRef(offset); const softEnd = React.useRef(false); const { spriteObj, loadJsonAndTexture } = useSpriteLoader(null, null, animationNames, numberOfFrames, undefined, canvasRenderingContext2DSettings); const frameNameRef = React.useRef(frameName); // lite version for pre-loaded assets const parseSpriteDataLite = React.useCallback((textureData, data) => { if (data === null) { if (numberOfFrames) { //get size from texture totalFrames.current = numberOfFrames; if (playBackwards) { currentFrame.current = numberOfFrames - 1; } spriteData.current = data; } } else { var _spriteData$current$f, _spriteData$current; spriteData.current = data; if (spriteData.current && Array.isArray(spriteData.current.frames)) { totalFrames.current = spriteData.current.frames.length; } else if (spriteData.current && typeof spriteData.current === 'object' && frameNameRef.current) { totalFrames.current = spriteData.current.frames[frameNameRef.current].length; } else { totalFrames.current = 0; } if (playBackwards) { currentFrame.current = totalFrames.current - 1; } const { w, h } = getFirstFrame((_spriteData$current$f = (_spriteData$current = spriteData.current) == null ? void 0 : _spriteData$current.frames) !== null && _spriteData$current$f !== void 0 ? _spriteData$current$f : [], frameNameRef.current).sourceSize; const aspect = calculateAspectRatio(w, h); setAspect(aspect); if (matRef.current) { matRef.current.map = textureData; } } setSpriteTexture(textureData); }, [numberOfFrames, playBackwards]); // modify the sprite material after json is parsed and state updated const modifySpritePosition = React.useCallback(() => { if (!spriteData.current) return; const { meta: { size: metaInfo }, frames } = spriteData.current; const { w: frameW, h: frameH } = Array.isArray(frames) ? frames[0].sourceSize : frameName ? frames[frameName] ? frames[frameName][0].sourceSize : { w: 0, h: 0 } : { w: 0, h: 0 }; if (matRef.current && matRef.current.map) { matRef.current.map.wrapS = matRef.current.map.wrapT = THREE.RepeatWrapping; matRef.current.map.center.set(0, 0); matRef.current.map.repeat.set(1 * flipOffset / (metaInfo.w / frameW), 1 / (metaInfo.h / frameH)); } //const framesH = (metaInfo.w - 1) / frameW const framesV = (metaInfo.h - 1) / frameH; const frameOffsetY = 1 / framesV; if (matRef.current && matRef.current.map) { matRef.current.map.offset.x = 0.0; //-matRef.current.map.repeat.x matRef.current.map.offset.y = 1 - frameOffsetY; } if (onStart) { onStart({ currentFrameName: frameName !== null && frameName !== void 0 ? frameName : '', currentFrame: currentFrame.current }); } }, [flipOffset, frameName, onStart]); const state = React.useMemo(() => ({ current: pos.current, offset: pos.current, imageUrl: textureImageURL, hasEnded: false, ref: fref }), [textureImageURL, fref]); React.useImperativeHandle(fref, () => ref.current, []); React.useLayoutEffect(() => { pos.current = offset; }, [offset]); const calculateAspectRatio = (width, height) => { var _spriteRef$current; const ret = new THREE.Vector3(); const aspectRatio = height / width; ret.set(1, aspectRatio, 1); (_spriteRef$current = spriteRef.current) == null || _spriteRef$current.scale.copy(ret); return ret; }; // initial loads React.useEffect(() => { if (spriteDataset) { var _spriteDataset$sprite; parseSpriteDataLite(spriteDataset == null || (_spriteDataset$sprite = spriteDataset.spriteTexture) == null ? void 0 : _spriteDataset$sprite.clone(), spriteDataset.spriteData); } else { if (textureImageURL && textureDataURL) { loadJsonAndTexture(textureImageURL, textureDataURL); } } }, [loadJsonAndTexture, spriteDataset, textureDataURL, textureImageURL, parseSpriteDataLite]); React.useEffect(() => { if (spriteObj) { var _spriteObj$spriteText; parseSpriteDataLite(spriteObj == null || (_spriteObj$spriteText = spriteObj.spriteTexture) == null ? void 0 : _spriteObj$spriteText.clone(), spriteObj == null ? void 0 : spriteObj.spriteData); } }, [spriteObj, parseSpriteDataLite]); // support backwards play React.useEffect(() => { state.hasEnded = false; if (spriteData.current && playBackwards === true) { var _ref; currentFrame.current = ((_ref = spriteData.current.frames.length) !== null && _ref !== void 0 ? _ref : 0) - 1; } else { currentFrame.current = 0; } }, [playBackwards, state]); React.useLayoutEffect(() => { modifySpritePosition(); }, [spriteTexture, flipX, modifySpritePosition]); React.useEffect(() => { if (autoPlay) { pauseRef.current = false; } }, [autoPlay]); React.useLayoutEffect(() => { if (currentFrameName.current !== frameName && frameName) { currentFrame.current = 0; currentFrameName.current = frameName; state.hasEnded = false; if (fpsInterval <= 0) { currentFrame.current = endFrame || startFrame || 0; } // modifySpritePosition() if (spriteData.current) { const { w, h } = getFirstFrame(spriteData.current.frames, frameName).sourceSize; const _aspect = calculateAspectRatio(w, h); setAspect(_aspect); } } }, [frameName, fpsInterval, state, endFrame, startFrame]); // run the animation on each frame const runAnimation = () => { if (!isSpriteData(spriteData.current)) return; const { meta: { size: metaInfo }, frames } = spriteData.current; const { w: frameW, h: frameH } = getFirstFrame(frames, frameName).sourceSize; const spriteFrames = Array.isArray(frames) ? frames : frameName ? frames[frameName] : []; const _endFrame = endFrame || spriteFrames.length - 1; var _offset = offset === undefined ? state.current : offset; if (fpsInterval <= 0) { currentFrame.current = endFrame || startFrame || 0; calculateFinalPosition(frameW, frameH, metaInfo, spriteFrames); return; } const now = window.performance.now(); const diff = now - timerOffset.current; if (diff <= fpsInterval) return; // 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 !== null && startFrame !== void 0 ? startFrame : 0 : 0; if (playBackwards) { currentFrame.current = _endFrame; } if (loop) { onLoopEnd == null || onLoopEnd({ currentFrameName: frameName !== null && frameName !== void 0 ? frameName : '', currentFrame: currentFrame.current }); } else { onEnd == null || onEnd({ currentFrameName: frameName !== null && frameName !== void 0 ? frameName : '', currentFrame: currentFrame.current }); state.hasEnded = !resetOnEnd; if (resetOnEnd) { pauseRef.current = true; //calculateFinalPosition(frameW, frameH, metaInfo, spriteFrames) } } if (!loop) return; } else if (onStartCondition) { onStart == null || onStart({ currentFrameName: frameName !== null && frameName !== void 0 ? frameName : '', currentFrame: currentFrame.current }); } // for manual update if (_offset !== undefined && manualProgressEndCondition) { if (softEnd.current === false) { onEnd == null || onEnd({ currentFrameName: frameName !== null && frameName !== void 0 ? 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); }; const calculateFinalPosition = (frameW, frameH, metaInfo, spriteFrames) => { // 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 = roundFramePosition ? Math.round((metaInfo.w - 1) / frameW) : (metaInfo.w - 1) / frameW; const framesV = roundFramePosition ? Math.round((metaInfo.h - 1) / frameH) : (metaInfo.h - 1) / frameH; if (!spriteFrames[targetFrame]) { return; } const { frame: { x: frameX, y: frameY }, sourceSize: { w: originalSizeX, h: originalSizeY } } = spriteFrames[targetFrame]; const frameOffsetX = 1 / framesH; const frameOffsetY = 1 / framesV; if (matRef.current && matRef.current.map) { finalValX = flipOffset > 0 ? frameOffsetX * (frameX / originalSizeX) : frameOffsetX * (frameX / originalSizeX) - matRef.current.map.repeat.x; finalValY = Math.abs(1 - frameOffsetY) - frameOffsetY * (frameY / originalSizeY); 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)) { frameIndex = 0; //fallback } currentFrame.current = frameIndex; } else { // auto update if (playBackwards) { currentFrame.current -= 1; } else { currentFrame.current += 1; } } }; // *** Warning! It runs on every frame! *** useFrame((_state, _delta) => { var _spriteData$current2, _matRef$current; if (!((_spriteData$current2 = spriteData.current) != null && _spriteData$current2.frames) || !((_matRef$current = matRef.current) != null && _matRef$current.map)) { return; } if (pauseRef.current) { return; } if (!state.hasEnded && (autoPlay || play)) { runAnimation(); onFrame == null || onFrame({ currentFrameName: currentFrameName.current, currentFrame: currentFrame.current }); } }); function multiplyScale(initialScale = new THREE.Vector3(1, 1, 1), newScale = 1) { if (typeof newScale === 'number') return initialScale.multiplyScalar(newScale); if (Array.isArray(newScale)) return initialScale.multiply(new THREE.Vector3(...newScale)); if (newScale instanceof THREE.Vector3) return initialScale.multiply(newScale); } return /*#__PURE__*/React.createElement("group", _extends({}, props, { ref: ref, scale: multiplyScale(aspect, props.scale) }), /*#__PURE__*/React.createElement(context.Provider, { value: state }, asSprite && /*#__PURE__*/React.createElement(Billboard, null, /*#__PURE__*/React.createElement("mesh", _extends({ ref: spriteRef, scale: 1.0, geometry: geometry }, meshProps), /*#__PURE__*/React.createElement("meshBasicMaterial", { premultipliedAlpha: false, toneMapped: false, side: THREE.DoubleSide, ref: matRef, map: spriteTexture, transparent: true, alphaTest: alphaTest !== null && alphaTest !== void 0 ? alphaTest : 0.0 }))), !asSprite && /*#__PURE__*/React.createElement(Instances, _extends({ geometry: geometry, limit: maxItems !== null && maxItems !== void 0 ? maxItems : 1 }, meshProps), /*#__PURE__*/React.createElement("meshBasicMaterial", { premultipliedAlpha: false, toneMapped: false, side: THREE.DoubleSide, ref: matRef, map: spriteTexture, transparent: true, alphaTest: alphaTest !== null && alphaTest !== void 0 ? alphaTest : 0.0 }), (instanceItems !== null && instanceItems !== void 0 ? instanceItems : [0]).map((item, index) => /*#__PURE__*/React.createElement(Instance, _extends({ key: index, ref: (instanceItems == null ? void 0 : instanceItems.length) === 1 ? spriteRef : null, position: item, scale: 1.0 }, meshProps)))), children)); }); export { SpriteAnimator, useSpriteAnimator };