UNPKG

rn-sprite-sheet-18

Version:

A sprite sheet animation library for React Native

246 lines (220 loc) 6.34 kB
import { Animated, Easing, Image as NativeImage, Platform, View } from 'react-native'; import PropTypes from 'prop-types'; import React from 'react'; const stylePropType = PropTypes.oneOfType([PropTypes.number, PropTypes.object, PropTypes.array]); const sourcePropType = PropTypes.oneOfType([PropTypes.number, PropTypes.object]); function resolveAssetSource(source) { if (Platform.OS === 'web') { // eslint-disable-next-line no-undef const img = new Image(); img.src = source; return { width: img.width, height: img.height, }; } return NativeImage.resolveAssetSource(source); } export default class SpriteSheet extends React.PureComponent { static propTypes = { source: sourcePropType.isRequired, // source must be required 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, frameWidth: PropTypes.number, frameHeight: PropTypes.number, }; static defaultProps = { columns: 1, rows: 1, animations: {}, offsetY: 0, offsetX: 0, }; constructor(props) { super(props); this.state = { imageHeight: 0, imageWidth: 0, defaultFrameHeight: 0, defaultFrameWidth: 0, translateYInputRange: [0, 1], translateYOutputRange: [0, 1], translateXInputRange: [0, 1], translateXOutputRange: [0, 1], }; this.time = new Animated.Value(0); this.interpolationRanges = {}; let { source, height, width, rows, columns, frameHeight, frameWidth, offsetY, offsetX, } = this.props; let image = resolveAssetSource(source); let ratio = 1; let imageHeight = image.height; let imageWidth = image.width; offsetX = -offsetX; offsetY = -offsetY; frameHeight = frameHeight || image.height / rows; frameWidth = 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, offsetX, offsetY, }); this.generateInterpolationRanges(); } render() { let { imageHeight, imageWidth, frameHeight, frameWidth, animationType, offsetX, offsetY, } = this.state; let { viewStyle, imageStyle, source, onLoad } = this.props; let { translateY = { in: [0, 0], out: [offsetY, offsetY] }, translateX = { in: [0, 0], out: [offsetX, offsetX] }, } = 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, // Transform properties are GPU accelerated and supported by Native Driver transform: [ { translateX: this.time.interpolate({ inputRange: translateX.in, outputRange: translateX.out, }), }, { translateY: this.time.interpolate({ inputRange: translateY.in, outputRange: translateY.out, }), }, ], }, ]} /> </View> ); } generateInterpolationRanges = () => { let { animations } = this.props; for (let key in animations) { let { length } = animations[key]; let input = [].concat(...Array.from({ length }, (_, i) => [i, i + 1])); this.interpolationRanges[key] = { translateY: { in: input, out: [].concat( ...animations[key].map(i => { let { y } = this.getFrameCoords(i); return [y, y]; }), ), }, translateX: { in: input, out: [].concat( ...animations[key].map(i => { let { x } = this.getFrameCoords(i); return [x, x]; }), ), }, }; } }; stop = cb => { this.time.stopAnimation(cb); }; reset = cb => { this.time.stopAnimation(cb); this.time.setValue(0); }; play = ({ type, fps = 24, loop = false, resetAfterFinish = false, 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, useNativeDriver: true, // Using native animation driver instead of JS }); this.time.setValue(0); if (loop) { Animated.loop(animation).start(); } else { animation.start(() => { if (resetAfterFinish) { this.time.setValue(0); } onFinish(); }); } }); }; getFrameCoords = i => { let { columns, offsetX, offsetY } = this.props; let { frameHeight, frameWidth } = this.state; let currentColumn = i % columns; let xAdjust = -currentColumn * frameWidth; xAdjust -= offsetX; let yAdjust = -((i - currentColumn) / columns) * frameHeight; yAdjust -= offsetY; return { x: xAdjust, y: yAdjust, }; }; }