Code Monkey home page Code Monkey logo

rn-sprite-sheet's Introduction

rn-sprite-sheet's People

Contributors

cihadturhan avatar elisechant avatar endel avatar genesy avatar gorvinsky avatar jaret-kiser avatar kgpasta avatar logan-lim avatar mileung avatar shukerullah avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

rn-sprite-sheet's Issues

Different sources work incorrect

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
    };
  };
}

Can I make smooth animations ?

Will be animation smooth ? Without defect. Or use webview + some webgl engine for animation ? And can i animate spine animation ?

React Compatibility

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 .

[Android] Sometimes the animation is shaky

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.

rough re-write of the component in typescript and with hooks

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";
`;

Document spritesheet specification requirements

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:

Screen Shot 2020-04-08 at 7 01 13 am

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.

Image is not scalable

I have uploaded a png, but the quality of image is not ok.
Seems like: when the number of rows and columns grows - quality get worse.
Example:
image
The real part of image:
image
If you know a way to fix it, please help :)

[Feature] Autoplay

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'}
/>

select first image dynamically

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:
cupsprite

  • Each image that I uploaded has dimensions of (W)96 x 144((H) and 72 dpi vertical and horizontal resolution
  • The generated 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>
    )

Offer to continue this project

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)

With 10x10 Sprite Sheet, Animation Looks Crisp On iOS But Looks Very Very Pixelated On Android

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
/>

Calculation for animation doesn't seem to work correctly

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:

kapture 2018-03-01 at 14 23 21

@mileung: is there anything that I can do to get it working?

SpriteSheet with only 1 row and 94 columns doesnt seems to work

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

Change sprite sheets on the fly?

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.

uri does work as a source by providing its dimensions as well

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.

Deprecated usage of iOS UIWebView

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?

Image downsampling on Android by fresco

facebook/fresco#2397

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?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.