Initial project import
This commit is contained in:
402
node_modules/@react-three/drei/core/useSpriteLoader.js
generated
vendored
Normal file
402
node_modules/@react-three/drei/core/useSpriteLoader.js
generated
vendored
Normal file
@@ -0,0 +1,402 @@
|
||||
import * as THREE from 'three';
|
||||
import { TextureLoader } from 'three';
|
||||
import { useThree, useLoader } from '@react-three/fiber';
|
||||
import * as React from 'react';
|
||||
import { useState } from 'react';
|
||||
|
||||
/* eslint react-hooks/exhaustive-deps: 1 */
|
||||
// utils
|
||||
const getFirstFrame = (frames, frameName) => {
|
||||
if (Array.isArray(frames)) {
|
||||
return frames[0];
|
||||
} else {
|
||||
const k = frameName !== null && frameName !== void 0 ? frameName : Object.keys(frames)[0];
|
||||
return frames[k][0];
|
||||
}
|
||||
};
|
||||
const checkIfFrameIsEmpty = frameData => {
|
||||
for (let i = 3; i < frameData.length; i += 4) {
|
||||
if (frameData[i] !== 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
function useSpriteLoader(/** The URL of the sprite sheet. */
|
||||
input, /** The JSON data of the sprite sheet. */
|
||||
json, /** The names of the animations in the sprite sheet. */
|
||||
animationNames, /** The number of frames in the sprite sheet. */
|
||||
numberOfFrames, /** A callback that is called when the sprite sheet is loaded. */
|
||||
onLoad, /** The settings to use when creating the 2D context. */
|
||||
canvasRenderingContext2DSettings) {
|
||||
const viewportRef = React.useRef(useThree(state => state.viewport));
|
||||
const spriteDataRef = React.useRef(null);
|
||||
const totalFrames = React.useRef(0);
|
||||
const aspectFactor = 0.1;
|
||||
const inputRef = React.useRef(input);
|
||||
const jsonRef = React.useRef(json);
|
||||
const animationFramesRef = React.useRef(animationNames);
|
||||
const [spriteData, setSpriteData] = useState(null);
|
||||
const [spriteTexture, setSpriteTexture] = React.useState(new THREE.Texture());
|
||||
const textureLoader = React.useMemo(() => new THREE.TextureLoader(), []);
|
||||
const [spriteObj, setSpriteObj] = useState(null);
|
||||
const calculateAspectRatio = React.useCallback((width, height, factor) => {
|
||||
const adaptedHeight = height * (viewportRef.current.aspect > width / height ? viewportRef.current.width / width : viewportRef.current.height / height);
|
||||
const adaptedWidth = width * (viewportRef.current.aspect > width / height ? viewportRef.current.width / width : viewportRef.current.height / height);
|
||||
const scaleX = adaptedWidth * factor;
|
||||
const scaleY = adaptedHeight * factor;
|
||||
const currentMaxScale = 1;
|
||||
// Calculate the maximum scale based on the aspect ratio and max scale limit
|
||||
let finalMaxScaleW = Math.min(currentMaxScale, scaleX);
|
||||
let finalMaxScaleH = Math.min(currentMaxScale, scaleY);
|
||||
|
||||
// Ensure that scaleX and scaleY do not exceed the max scale while maintaining aspect ratio
|
||||
if (scaleX > currentMaxScale) {
|
||||
finalMaxScaleW = currentMaxScale;
|
||||
finalMaxScaleH = scaleY / scaleX * currentMaxScale;
|
||||
}
|
||||
return new THREE.Vector3(finalMaxScaleW, finalMaxScaleH, 1);
|
||||
}, []);
|
||||
const getRowsAndColumns = React.useCallback((texture, totalFrames) => {
|
||||
if (texture.image) {
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d', canvasRenderingContext2DSettings);
|
||||
if (!ctx) {
|
||||
throw new Error('Failed to get 2d context');
|
||||
}
|
||||
canvas.width = texture.image.width;
|
||||
canvas.height = texture.image.height;
|
||||
ctx.drawImage(texture.image, 0, 0);
|
||||
const width = texture.image.width;
|
||||
const height = texture.image.height;
|
||||
|
||||
// Calculate rows and columns based on the number of frames and image dimensions
|
||||
const cols = Math.round(Math.sqrt(totalFrames * (width / height)));
|
||||
const rows = Math.round(totalFrames / cols);
|
||||
const frameWidth = width / cols;
|
||||
const frameHeight = height / rows;
|
||||
const emptyFrames = [];
|
||||
for (let row = 0; row < rows; row++) {
|
||||
for (let col = 0; col < cols; col++) {
|
||||
const frameIndex = row * cols + col;
|
||||
if (frameIndex >= totalFrames) {
|
||||
emptyFrames.push({
|
||||
row,
|
||||
col
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const frameData = ctx.getImageData(col * frameWidth, row * frameHeight, frameWidth, frameHeight).data;
|
||||
const isEmpty = checkIfFrameIsEmpty(frameData);
|
||||
if (isEmpty) {
|
||||
emptyFrames.push({
|
||||
row,
|
||||
col
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
rows,
|
||||
columns: cols,
|
||||
frameWidth,
|
||||
frameHeight,
|
||||
emptyFrames
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
rows: 0,
|
||||
columns: 0,
|
||||
frameWidth: 0,
|
||||
frameHeight: 0,
|
||||
emptyFrames: []
|
||||
};
|
||||
}
|
||||
}, [canvasRenderingContext2DSettings]);
|
||||
|
||||
// calculate scale ratio for the frames
|
||||
const calculateScaleRatio = React.useCallback(frames => {
|
||||
// Helper function to calculate scale ratio for an array of frames
|
||||
const processFrameArray = frameArray => {
|
||||
// Find the largest frame
|
||||
let largestFrame = null;
|
||||
for (const frame of frameArray) {
|
||||
const {
|
||||
w,
|
||||
h
|
||||
} = frame.frame;
|
||||
const area = w * h;
|
||||
if (!largestFrame || area > largestFrame.area) {
|
||||
largestFrame = {
|
||||
w,
|
||||
h,
|
||||
area
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Set scaleRatio property on each frame
|
||||
const frameArr = frameArray.map(frame => {
|
||||
const {
|
||||
w,
|
||||
h
|
||||
} = frame.frame;
|
||||
const area = w * h;
|
||||
const scaleRatio = largestFrame ? area === largestFrame.area ? 1 : Math.sqrt(area / largestFrame.area) : 1;
|
||||
return {
|
||||
...frame,
|
||||
scaleRatio
|
||||
};
|
||||
});
|
||||
return frameArr;
|
||||
};
|
||||
|
||||
// Handle both array and record cases
|
||||
if (Array.isArray(frames)) {
|
||||
return processFrameArray(frames);
|
||||
} else {
|
||||
const result = {};
|
||||
for (const key in frames) {
|
||||
result[key] = processFrameArray(frames[key]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}, []);
|
||||
|
||||
// for frame based JSON Hash sprite data
|
||||
const parseFrames = React.useCallback(() => {
|
||||
const sprites = {};
|
||||
const data = spriteDataRef.current;
|
||||
const delimiters = animationFramesRef.current;
|
||||
if (data) {
|
||||
if (delimiters && Array.isArray(data['frames'])) {
|
||||
for (let i = 0; i < delimiters.length; i++) {
|
||||
// we convert each named animation group into an array
|
||||
sprites[delimiters[i]] = [];
|
||||
for (const value of data['frames']) {
|
||||
const frameData = value['frame'];
|
||||
const sourceWidth = value['sourceSize']['w'];
|
||||
const sourceHeight = value['sourceSize']['h'];
|
||||
if (typeof value['filename'] === 'string' && value['filename'].toLowerCase().indexOf(delimiters[i].toLowerCase()) !== -1) {
|
||||
sprites[delimiters[i]].push({
|
||||
...value,
|
||||
frame: frameData,
|
||||
sourceSize: {
|
||||
w: sourceWidth,
|
||||
h: sourceHeight
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const frame in sprites) {
|
||||
const scaleRatioData = calculateScaleRatio(sprites[frame]);
|
||||
if (Array.isArray(scaleRatioData)) {
|
||||
sprites[frame] = scaleRatioData;
|
||||
}
|
||||
}
|
||||
return sprites;
|
||||
} else if (delimiters && typeof data['frames'] === 'object') {
|
||||
for (let i = 0; i < delimiters.length; i++) {
|
||||
// we convert each named animation group into an array
|
||||
sprites[delimiters[i]] = [];
|
||||
for (const innerKey in data['frames']) {
|
||||
const value = data['frames'][innerKey];
|
||||
const frameData = value['frame'];
|
||||
const sourceWidth = value['sourceSize']['w'];
|
||||
const sourceHeight = value['sourceSize']['h'];
|
||||
if (typeof innerKey === 'string' && innerKey.toLowerCase().indexOf(delimiters[i].toLowerCase()) !== -1) {
|
||||
sprites[delimiters[i]].push({
|
||||
...value,
|
||||
frame: frameData,
|
||||
sourceSize: {
|
||||
w: sourceWidth,
|
||||
h: sourceHeight
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const frame in sprites) {
|
||||
const scaleRatioData = calculateScaleRatio(sprites[frame]);
|
||||
if (Array.isArray(scaleRatioData)) {
|
||||
sprites[frame] = scaleRatioData;
|
||||
}
|
||||
}
|
||||
return sprites;
|
||||
} else {
|
||||
let spritesArr = [];
|
||||
if (data != null && data.frames) {
|
||||
if (Array.isArray(data.frames)) {
|
||||
spritesArr = data.frames.map(frame => ({
|
||||
...frame,
|
||||
x: frame.frame.x,
|
||||
y: frame.frame.y,
|
||||
w: frame.frame.w,
|
||||
h: frame.frame.h
|
||||
}));
|
||||
} else {
|
||||
spritesArr = Object.values(data.frames).flat().map(frame => ({
|
||||
...frame,
|
||||
x: frame.frame.x,
|
||||
y: frame.frame.y,
|
||||
w: frame.frame.w,
|
||||
h: frame.frame.h
|
||||
}));
|
||||
}
|
||||
}
|
||||
return calculateScaleRatio(spritesArr);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}, [calculateScaleRatio, spriteDataRef]);
|
||||
const parseSpriteData = React.useCallback((json, _spriteTexture) => {
|
||||
let aspect = new THREE.Vector3(1, 1, 1);
|
||||
// sprite only case
|
||||
if (json === null) {
|
||||
if (_spriteTexture && numberOfFrames) {
|
||||
//get size from texture
|
||||
const width = _spriteTexture.image.width;
|
||||
const height = _spriteTexture.image.height;
|
||||
totalFrames.current = numberOfFrames;
|
||||
const {
|
||||
rows,
|
||||
columns,
|
||||
frameWidth,
|
||||
frameHeight,
|
||||
emptyFrames
|
||||
} = getRowsAndColumns(_spriteTexture, numberOfFrames);
|
||||
const nonJsonFrames = {
|
||||
frames: [],
|
||||
meta: {
|
||||
version: '1.0',
|
||||
size: {
|
||||
w: width,
|
||||
h: height
|
||||
},
|
||||
rows,
|
||||
columns,
|
||||
frameWidth,
|
||||
frameHeight,
|
||||
scale: '1'
|
||||
}
|
||||
};
|
||||
for (let row = 0; row < rows; row++) {
|
||||
for (let col = 0; col < columns; col++) {
|
||||
const isExcluded = (emptyFrames !== null && emptyFrames !== void 0 ? emptyFrames : []).some(coord => coord.row === row && coord.col === col);
|
||||
if (isExcluded) {
|
||||
continue;
|
||||
}
|
||||
if (Array.isArray(nonJsonFrames.frames)) {
|
||||
nonJsonFrames.frames.push({
|
||||
frame: {
|
||||
x: col * frameWidth,
|
||||
y: row * frameHeight,
|
||||
w: frameWidth,
|
||||
h: frameHeight
|
||||
},
|
||||
scaleRatio: 1,
|
||||
rotated: false,
|
||||
trimmed: false,
|
||||
spriteSourceSize: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: frameWidth,
|
||||
h: frameHeight
|
||||
},
|
||||
sourceSize: {
|
||||
w: frameWidth,
|
||||
h: frameHeight
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
aspect = calculateAspectRatio(frameWidth, frameHeight, aspectFactor);
|
||||
spriteDataRef.current = nonJsonFrames;
|
||||
}
|
||||
|
||||
//scale ratio for standalone sprite
|
||||
if (spriteDataRef.current && spriteDataRef.current.frames) {
|
||||
spriteDataRef.current.frames = calculateScaleRatio(spriteDataRef.current.frames);
|
||||
}
|
||||
} else if (_spriteTexture) {
|
||||
spriteDataRef.current = json;
|
||||
spriteDataRef.current.frames = parseFrames();
|
||||
totalFrames.current = Array.isArray(json.frames) ? json.frames.length : Object.keys(json.frames).length;
|
||||
const {
|
||||
w,
|
||||
h
|
||||
} = getFirstFrame(json.frames).sourceSize;
|
||||
aspect = calculateAspectRatio(w, h, aspectFactor);
|
||||
}
|
||||
setSpriteData(spriteDataRef.current);
|
||||
if ('encoding' in _spriteTexture) {
|
||||
_spriteTexture.encoding = 3001; // sRGBEncoding
|
||||
} else if ('colorSpace' in _spriteTexture) {
|
||||
//@ts-ignore
|
||||
_spriteTexture.colorSpace = THREE.SRGBColorSpace;
|
||||
}
|
||||
setSpriteTexture(_spriteTexture);
|
||||
setSpriteObj({
|
||||
spriteTexture: _spriteTexture,
|
||||
spriteData: spriteDataRef.current,
|
||||
aspect: aspect
|
||||
});
|
||||
}, [getRowsAndColumns, numberOfFrames, parseFrames, calculateAspectRatio, calculateScaleRatio]);
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
const loadJsonAndTextureAndExecuteCallback = React.useCallback((jsonUrl, textureUrl, callback) => {
|
||||
const jsonPromise = fetch(jsonUrl).then(response => response.json());
|
||||
const texturePromise = new Promise(resolve => {
|
||||
textureLoader.load(textureUrl, resolve);
|
||||
});
|
||||
Promise.all([jsonPromise, texturePromise]).then(response => {
|
||||
callback(response[0], response[1]);
|
||||
});
|
||||
}, [textureLoader]);
|
||||
const loadStandaloneSprite = React.useCallback(textureUrl => {
|
||||
if (!textureUrl && !inputRef.current) {
|
||||
throw new Error('Either textureUrl or input must be provided');
|
||||
}
|
||||
const validUrl = textureUrl !== null && textureUrl !== void 0 ? textureUrl : inputRef.current;
|
||||
if (!validUrl) {
|
||||
throw new Error('A valid texture URL must be provided');
|
||||
}
|
||||
textureLoader.load(validUrl, texture => parseSpriteData(null, texture));
|
||||
}, [textureLoader, parseSpriteData]);
|
||||
const loadJsonAndTexture = React.useCallback((textureUrl, jsonUrl) => {
|
||||
if (jsonUrl && textureUrl) {
|
||||
loadJsonAndTextureAndExecuteCallback(jsonUrl, textureUrl, parseSpriteData);
|
||||
} else {
|
||||
loadStandaloneSprite(textureUrl);
|
||||
}
|
||||
}, [loadJsonAndTextureAndExecuteCallback, loadStandaloneSprite, parseSpriteData]);
|
||||
React.useLayoutEffect(() => {
|
||||
if (jsonRef.current && inputRef.current) {
|
||||
loadJsonAndTextureAndExecuteCallback(jsonRef.current, inputRef.current, parseSpriteData);
|
||||
} else if (inputRef.current) {
|
||||
// only load the texture, this is an image sprite only
|
||||
loadStandaloneSprite();
|
||||
}
|
||||
const _inputRef = inputRef.current;
|
||||
return () => {
|
||||
if (_inputRef) {
|
||||
useLoader.clear(TextureLoader, _inputRef);
|
||||
}
|
||||
};
|
||||
}, [loadJsonAndTextureAndExecuteCallback, loadStandaloneSprite, parseSpriteData]);
|
||||
React.useLayoutEffect(() => {
|
||||
onLoad == null || onLoad(spriteTexture, spriteData !== null && spriteData !== void 0 ? spriteData : null);
|
||||
}, [spriteTexture, spriteData, onLoad]);
|
||||
return {
|
||||
spriteObj,
|
||||
loadJsonAndTexture
|
||||
};
|
||||
}
|
||||
useSpriteLoader.preload = url => useLoader.preload(TextureLoader, url);
|
||||
useSpriteLoader.clear = input => useLoader.clear(TextureLoader, input);
|
||||
|
||||
export { checkIfFrameIsEmpty, getFirstFrame, useSpriteLoader };
|
||||
Reference in New Issue
Block a user