mileung / rn-sprite-sheet Goto Github PK
View Code? Open in Web Editor NEWA sprite sheet animation library for React Native
Home Page: https://www.npmjs.com/package/rn-sprite-sheet
A sprite sheet animation library for React Native
Home Page: https://www.npmjs.com/package/rn-sprite-sheet
Hi, great component! Thank you for it very much!
But if I have few different sources pasted in spritesheet component it works incorrect because it load and calculate data only in constructor. So I need to update and calc new params when source is changed. frameWidth and frameHeight pasted into getFrameCoords because it is don't updated in state before it is called.
Maybe my changes will help someone or you can implement it into library.
export default class SpriteSheet extends React.Component {
static propTypes = {
source: PropTypes.number.isRequired, // source must be required; { uri } will not work
columns: PropTypes.number.isRequired,
rows: PropTypes.number.isRequired,
animations: PropTypes.object.isRequired, // see example
viewStyle: stylePropType, // styles for the sprite sheet container
imageStyle: stylePropType, // styles for the sprite sheet
height: PropTypes.number, // set either height, width, or neither
width: PropTypes.number, // do not set both height and width
onLoad: PropTypes.func
};
static defaultPropTypes = {
columns: 1,
rows: 1,
animations: {}
};
constructor(props) {
super(props);
this.state = {
imageHeight: 0,
imageWidth: 0,
source: null,
defaultFrameHeight: 0,
defaultFrameWidth: 0,
topInputRange: [0, 1],
topOutputRange: [0, 1],
leftInputRange: [0, 1],
leftOutputRange: [0, 1]
};
this.time = new Animated.Value(0);
this.interpolationRanges = {};
let { source, height, width, rows, columns } = this.props;
let image = resolveAssetSource(source);
let ratio = 1;
let imageHeight = image.height;
let imageWidth = image.width;
let frameHeight = image.height / rows;
let frameWidth = image.width / columns;
if (width) {
ratio = (width * columns) / image.width;
imageHeight = image.height * ratio;
imageWidth = width * columns;
frameHeight = (image.height / rows) * ratio;
frameWidth = width;
} else if (height) {
ratio = (height * rows) / image.height;
imageHeight = height * rows;
imageWidth = image.width * ratio;
frameHeight = height;
frameWidth = (image.width / columns) * ratio;
}
Object.assign(this.state, {
imageHeight,
imageWidth,
frameHeight,
frameWidth,
source
});
this.generateInterpolationRanges(frameWidth, frameHeight);
}
///update calculating with new source
updateData(){
const { source, height, width, rows, columns } = this.props;
const image = resolveAssetSource(source);
let ratio = 1;
let imageHeight = image.height;
let imageWidth = image.width;
let frameHeight = image.height / rows;
let frameWidth = image.width / columns;
if (width) {
ratio = (width * columns) / image.width;
frameHeight = Math.floor((image.height / rows) * ratio);
frameWidth = width;
imageHeight = frameHeight*rows//Math.floor(image.height * ratio);
imageWidth = frameWidth*columns//Math.floor(width * columns);
} else if (height) {
ratio = (height * rows) / image.height;
imageHeight = height * rows;
imageWidth = image.width * ratio;
frameHeight = height;
frameWidth = (image.width / columns) * ratio;
}
this.setState({
imageHeight,
imageWidth,
frameHeight,
frameWidth,
source
});
this.generateInterpolationRanges(frameWidth, frameHeight);
}
componentDidUpdate(){
if (this.state.source !== this.props.source) {
this.updateData()
}
}
render() {
const { imageHeight, imageWidth, frameHeight, frameWidth, animationType } = this.state;
const { viewStyle, imageStyle, rows, columns, height, width, source, onLoad} = this.props;
console.log(this.state)
console.log(this.props)
const { top = { in: [0, 0], out: [0, 0] }, left = { in: [0, 0], out: [0, 0] } } =
this.interpolationRanges[animationType] || {};
return (
<View
style={[
viewStyle,
{
height: frameHeight,
width: frameWidth,
overflow: 'hidden'
}
]}
>
<Animated.Image
source={source}
onLoad={onLoad}
style={[
imageStyle,
{
height: imageHeight,
width: imageWidth,
top: this.time.interpolate({
inputRange: top.in,
outputRange: top.out
}),
left: this.time.interpolate({
inputRange: left.in,
outputRange: left.out
})
}
]}
/>
</View>
);
}
generateInterpolationRanges = (frameWidth, frameHeight) => {
let { animations } = this.props;
for (let key in animations) {
const { length } = animations[key];
const input = [].concat(...Array.from({ length }, (_, i) => [i, i + 0.99999999999]));
this.interpolationRanges[key] = {
top: {
in: input,
out: [].concat(
...animations[key].map(i => {
let { y } = this.getFrameCoords(i, frameWidth, frameHeight);
return [y, y];
})
)
},
left: {
in: input,
out: [].concat(
...animations[key].map(i => {
let { x } = this.getFrameCoords(i, frameWidth, frameHeight);
return [x, x];
})
)
}
};
}
};
stop = cb => {
this.time.stopAnimation(cb);
};
play = ({ type, fps, loop, resetAfterFinish, onFinish = () => {} }) => {
let { animations } = this.props;
let { length } = animations[type];
this.setState({ animationType: type }, () => {
let animation = Animated.timing(this.time, {
toValue: length,
duration: (length / fps) * 1000,
easing: Easing.linear
});
this.time.setValue(0);
if (loop) {
Animated.loop(animation).start();
} else {
animation.start(() => {
if (resetAfterFinish) {
this.time.setValue(0);
}
onFinish();
});
}
});
};
getFrameCoords = (i, frameWidth, frameHeight) => {
const { rows, columns } = this.props;
const successionWidth = i * frameWidth;
return {
x: -successionWidth % (columns * frameWidth),
y: -Math.floor(successionWidth / (columns * frameWidth)) * frameHeight
};
};
}
When using "resetAfterFinish: false" the animation resets to first frame instead of staying on last.
Ex:
spriteRef.current.play({
fps: 8,
type: 'death',
resetAfterFinish: false,
loop: false
})
The result is the animation playing backwards
I'm trying to scale up the sprite i'm using but reactnative's default image scaling blurs the lines.
Will be animation smooth ? Without defect. Or use webview + some webgl engine for animation ? And can i animate spine animation ?
This package seems to be dependent on 16.6.1 while 18.2.0 appears to be the version supported react-native making this package incompatible .
To illustrate this problem, I used a 5 x 5 sprite sheet where all 25 cells are having the same image. With this, the end result should be just a still image without any noticeable movement.
However, if you observe carefully, you'll notice that the resulted animation shakes for a short moment from time to time. You can check the result from this video.
You may also grab the sprite sheet that I used from here.
This only occur on Android and not every sprite sheet will produce this shaky effect. I have seen a few that can animated smoothly without any shaky effect. I have yet to determine what kind of image will produce it as I have not tested with a variety of images.
I believe it's the effect of translating from one cell to another quickly.
Edit 1:
It seems that it's related to the number of cells. The maximum number of cells where it won't shake is 20. 21 and the shaking effect appears.
did this for personal use and figured i would share, this is NOT very well tested but it should be either working or close to working for most use cases.
import {
Animated,
Easing,
Image,
Image as NativeImage,
ImageLoadEventData,
ImageSourcePropType,
ImageStyle,
NativeSyntheticEvent,
Platform,
View,
ViewStyle,
} from "react-native";
import React, {
forwardRef,
useEffect,
useImperativeHandle,
useRef,
useState,
} from "react";
import styled from "@emotion/native";
function resolveAssetSource(source, callback) {
if (Platform.OS === "web") {
Image.getSize(source, (width, height) => {
callback({ width, height });
});
} else {
let _source = Image.resolveAssetSource(source);
const width = _source.width;
const height = _source.height;
callback({ width, height });
}
}
type SpriteSheetPropTypes = {
source: ImageSourcePropType; // source must be required
columns: number;
rows: number;
animations?: object; // see example
viewStyle?: ViewStyle; // styles for the sprite sheet container
imageStyle?: ImageStyle; // styles for the sprite sheet
height?: number; // set either height, width, or neither
width?: number; // do not set both height and width
onLoad?: (event: NativeSyntheticEvent<ImageLoadEventData>) => void;
frameWidth?: number;
frameHeight?: number;
offsetY?: number;
offsetX?: number;
};
type AnimationStateType = {
fps: number;
loop: boolean;
resetAfterFinish: boolean;
onFinish: Function;
length: number;
animationType: string;
};
export const SpriteSheet = forwardRef(
(
{
source,
height,
width,
rows = 1,
columns = 1,
frameHeight: _frameHeight,
frameWidth: _frameWidth,
animations = {},
offsetY: offsetYProp = 0,
offsetX: offsetXProp = 0,
viewStyle,
imageStyle,
onLoad,
}: SpriteSheetPropTypes,
ref
) => {
let time = useRef<Animated.Value>(new Animated.Value(0));
// let [interpolationRanges, setInterpolationRanges] = useState({});
const interpolationRanges = useRef<any>({});
const [imageHeight, setImageHeight] = useState(0);
const [imageWidth, setImageWidth] = useState(0);
const [offsetX, setOffsetX] = useState(0);
const [offsetY, setOffsetY] = useState(0);
const [frameHeight, setFrameHeight] = useState(0);
const [frameWidth, setFrameWidth] = useState(0);
const [animationState, setAnimationState] = useState<AnimationStateType>({
fps: 24,
loop: false,
resetAfterFinish: false,
onFinish: () => {},
length: 0,
animationType: "",
});
useEffect(() => {
resolveAssetSource(source, ({ width: $width, height: $height }) => {
let ratio = 1;
let _imageHeight = $height;
let _imageWidth = $width;
let _offsetX = -offsetXProp;
let _offsetY = -offsetYProp;
let $frameHeight = $height / rows;
let $frameWidth = $width / columns;
if (width) {
ratio = (width * columns) / $width;
_imageHeight = $height * ratio;
_imageWidth = width * columns;
$frameHeight = ($height / rows) * ratio;
$frameWidth = width;
} else if (height) {
ratio = (height * rows) / $height;
_imageHeight = height * rows;
_imageWidth = $width * ratio;
$frameHeight = height;
$frameWidth = ($width / columns) * ratio;
}
setImageHeight(_imageHeight);
setImageWidth(_imageWidth);
setOffsetX(_offsetX);
setOffsetY(_offsetY);
setFrameHeight($frameHeight);
setFrameWidth($frameWidth);
});
}, [source]);
useEffect(() => {
const { fps, loop, resetAfterFinish, onFinish, length } = animationState;
let animation = Animated.sequence([
Animated.timing(time.current, {
toValue: 0,
duration: 0,
delay: (length / fps) * 1000,
easing: Easing.linear,
useNativeDriver: false,
}),
Animated.timing(time.current, {
toValue: length,
duration: 0,
easing: Easing.linear,
useNativeDriver: false,
delay: (length / fps) * 1000,
}),
]);
time.current.setValue(0);
if (loop) {
Animated.loop(animation).start();
} else {
animation.start(() => {
if (resetAfterFinish) {
time.current.setValue(0);
}
onFinish();
});
}
}, [animationState.animationType]);
const getFrameCoords = (i) => {
let currentColumn = i % columns;
let xAdjust = -currentColumn * frameWidth;
xAdjust -= offsetX;
let yAdjust = -((i - currentColumn) / columns) * frameHeight;
yAdjust -= offsetY;
return {
x: xAdjust,
y: Math.floor(yAdjust),
};
};
// GENERATE INTERPOLATION RANGES
useEffect(() => {
const ranges = {};
for (let key in animations) {
let { length } = animations[key];
let input = [].concat(...Array.from({ length }, (_, i) => [i, i + 1]));
ranges[key] = {
translateY: {
in: input,
out: [].concat(
...animations[key].map((i) => {
let { y } = getFrameCoords(i);
return [y, y];
})
),
},
translateX: {
in: input,
out: [].concat(
...animations[key].map((i) => {
let { x } = getFrameCoords(i);
return [x, x];
})
),
},
};
}
interpolationRanges.current = ranges;
// setInterpolationRanges({ ...interpolationRanges, ...ranges });
}, [animations, frameWidth, frameHeight, offsetX, offsetY]);
useImperativeHandle(
ref,
() => {
return {
stop(cb) {
time.current.stopAnimation(cb);
},
reset(cb) {
time.current.stopAnimation(cb);
time.current.setValue(0);
},
play({
type,
fps = 24,
loop = false,
resetAfterFinish = false,
onFinish = () => {},
}) {
let { length } = animations[type];
setAnimationState({
animationType: type,
fps,
loop,
resetAfterFinish,
onFinish,
length,
});
},
};
},
[]
);
let {
translateY = { in: [0, 0], out: [offsetY, offsetY] },
translateX = { in: [0, 0], out: [offsetX, offsetX] },
} = interpolationRanges.current[animationState.animationType] || {};
const transformStyle = [
{
translateX: time.current.interpolate({
inputRange: translateX.in,
outputRange: translateX.out,
}),
},
{
translateY: time.current.interpolate({
inputRange: translateY.in,
outputRange: translateY.out,
}),
},
];
return (
<View
style={[
viewStyle,
{
height: frameHeight,
width: frameWidth,
overflow: "hidden",
},
]}
>
<StyledAnimatedImage
source={source}
onLoad={onLoad}
style={[
imageStyle,
{
height: imageHeight,
width: imageWidth,
// Transform properties are GPU accelerated and supported by Native Driver
transform: animationState.animationType ? transformStyle : [],
},
]}
/>
</View>
);
}
);
const StyledAnimatedImage = styled(Animated.Image)`
image-rendering: "pixelated";
`;
It would be great if you could document information about spritesheet characteristics and specifications that this library requires.
I am looking at the mummy.png file and comparing it to my own spritesheet to find clues. But I am struggling to understand scaling and positioning of the spritesheet itself.
There are a number of configurations options for spritesheet generation that we have available to us, for example in Adobe Animate:
Currently I think it would be more intuitive if the input API was refactored to consume a spritesheet .png in addition to a coordinate mapping file which is usually .json instead of the rows
, columns
, width
props.
In the interim, some basic documentation would be welcomed.
I want to play different sprite sheet when first sprite animation finishes. I have tried setting source as a state but it doesn't work. Is there any other way to do this.
Autoplay
will be useful as below.
Sometimes I just want to play animation without control the SpriteSheet
by ref.
<SpriteSheet
{...props}
animations={{
basic: [0, 1, 2, 3],
}},
autoplay={'basic'}
/>
Hi @mileung,
I've generated a sprite sheet using https://www.piskelapp.com/ but my sprite does not seem to work.
and it looks like that:
sprite.png
file has dimensions of (W)576 x 720(H)How can I dynamically select the 1st image the sprite will be visible?
The cup can be - disabled, full or empty.
Can it be done dynamically or should I use the sprite only when I tap on a full cup?
My code looks like:
play = (type) => {
this.cup.play({
type,
fps: FRAMES_PER_SECOND,
loop: false,
resetAfterFinish: false,
onFinish: () => console.log('hi'),
});
}
stop = () => {
this.cup.stop(() => console.log('stopped'));
}
renderAnimatedImages = () => (
<TouchableOpacity onPress={this.onCupPress} >
<SpriteSheet
ref={(ref) => {
this.cup = ref;
}}
height={70}
source={require('../../../../assets/animatedCup/cupSprite.png')}
columns={6}
rows={5}
animations={{
drink: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29],
full: [1],
empty: [29],
disable: [30],
}}
/>
</TouchableOpacity>
)
Hi Michael, I wasn't able to find an easier way to contact you so I'll do it here.
I'm currently in the process of preparing a small number of useful React Native Packages and while I originally planned to use rn-sprite-sheet in my own projects, it turns out that to achieve my goals and full compatibility with current versions of React Native and React Web at the same time (for example using EXPO) some adjustments are needed.
I would offer to keep working on the project, keep it downwards compatible and also adding new features in a "non breaking change" way so people that previously used rn-sprite-sheet could switch over to my version and use their existing implementations without any changes needed to ease/help their migration.
If you are interested in this and would like to discuss details, feel free to contact me on twitter (https://twitter.com/AllBitsEqual) or discord (AllBitsEqual#2341)
Hey there. Animated.loop is not working when I start the animation in web app. the problem is in the useNativeDriver: true
Seems need to add a condition there to be like:
useNativeDriver: Platform.OS !== 'web',
https://github.com/MiLeung/rn-sprite-sheet/blob/master/src/SpriteSheet.js#L213
Has anyone else experienced this issue? I'm trying to understand what can be causing this huge drop in resolution. Everything else on the screen surrounding the sprite (text, images, & buttons) looks good. Just the sprite is pixelated.
Here's my code for SpriteSheet
.
<SpriteSheet
ref={ref => (this.animation = ref)}
source={require('./animation.png')}
columns={10}
rows={10}
animations={{
open: Array.from({ length: 100 }, (map, value) => value),
}}
viewStyle={{
marginTop: '25%'
alignSelf: 'center',
marginBottom: '16.3%',
}}
width={'150%'} // setting this to 100% doesn't change the pixelation issue
/>
I'm trying to use a sprite with donuts, it has 8 columns, 3 rows and 23 donuts, each sized 200x200:
<RNSpriteSheet
ref={ref => this.spriteSheet = ref}
source={require('../../../assets/images/spritesheets/pinkDonut.png')}
columns={8}
rows={3}
width={200}
animations={{
show: [...new Array(23)].map((i, key) => key),
}}
/>
And I'm triggering play
like this:
componentDidMount() {
this.spriteSheet.play({
type: 'show',
fps: 14,
loop: true,
resetAfterFinish: true,
});
}
But the transition between them isn't working as expected:
@mileung: is there anything that I can do to get it working?
Hello :)
thats not working for me, did things like that :
<SpriteSheet
ref={ref => this.alarmLogo = ref}
source={require('./assets/spriteAlarm.png')}
columns={94}
rows={1}
width={300}
animations={{
walk: [20, 22, 23, 24, 25, 80,81,82,83,84,85,86],
appear: Array.from({ length: 15 }, (v, i) => i + 18),
die: Array.from({ length: 21 }, (v, i) => i + 33)
}}
/>
With sprite sheet 300 x 300 for each img, got the view with the good height and width but not displaying the spritesheet
Would it be possible to be able to change sprite sheets on the fly?
I realize that I could generate one master sprite sheet with all the sprites and map my animations accordingly, but it would be easier to just assign a new image and update the fps and sprites I want to use.
In the documentations, it is stated that { uri } will not work as a source. However, it's not entirely true. It will work if you provide its dimentions as well. Refer below for more info.
This will not work:
source = { uri: 'someurl' };
This will work:
source = { uri: 'someurl', width: 100, height: 100 };
Do correct me if I'm wrong.
Submitting my app to Apple results in a warning that usage of the deprecated UIWebView will result in apps being rejected soon. I'm not entirely certain but a search with grep -r UIWebView node_modules/*
seems to point to rn-sprite-sheet (latest version) still referencing this deprecated API.
Does rn-sprite-sheete use UIWebView? If so is there a plan to refactor it out so apps won't get rejected?
Images are downsampled when loading large bundled images on Android. And normally sprite sheet are using large images.
And I am using facebook/fresco bundle I patched to avoid downsampling. But I do not want to patch fesco for some reasons.
Is there anybody to solve this issue?
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.