Jump to content
Search Community

Using spritesheets with timeline animation

chrism test
Moderator Tag

Recommended Posts

Hi,

 

I have seen that using SteppedEase seems to be the recommended way to cycle through bitmaps for an animation.

 

Unfortunately in my case they have to be split over a few separate files because of limitations of PNG image dimensions.

 

TexturePacker does a good job of creating these PNG files and generates an accompanying JSON file with the relevant x, y, width and heights to piece the animation back together.

 

The output looks something like this...

 

[
"image004.png":
{
	"frame": {"x":366,"y":0,"w":367,"h":241},
	"rotated": false,
	"trimmed": true,
	"spriteSourceSize": {"x":2,"y":129,"w":367,"h":241},
	"sourceSize": {"w":782,"h":414}
},
"image005.png":
{
	"frame": {"x":733,"y":0,"w":367,"h":241},
	"rotated": false,
	"trimmed": true,
	"spriteSourceSize": {"x":2,"y":129,"w":367,"h":241},
	"sourceSize": {"w":782,"h":414}
}
]

My question is there a preferred way to use these spritesheets and information to recreate the animation using Greensock?

 

I guess that this is too complex for SteppedEase, which only seems to be able to alter a fixed value a certain number of iterations—but for something more complex like this what would be a sensible approach?

 

I’m quite new to Greensock so looking for some advice really.

Thanks!

Link to comment
Share on other sites

@chrism could you share a small set of images and the associated JSON file for the sprite sheet? I'm curious about something and it'd really help to have that. 

 

I wouldn't recommend using a background-image that shifts around for this because as soon as you need to switch to a different image, you risk jank because the browser would have to load and decode a new image so it'd be key to have all of the images fully loaded and decoded and then maybe just stack them and toggle their visibility accordingly. This is definitely not a simplistic "just point it at a certain ease and it handles everything for me" kind of thing. You'd need to create a proxy object that manages things for you, and then just use a tween to animate a "progress" getter/setter or something. 

  • Like 1
  • Thanks 1
Link to comment
Share on other sites

Hi Jack,

 

Sure—and thanks very much for the reply.

 

This is a few of the PNGs and JSON files from a very basic test example animation but will hopefully give you some idea at least.

 

It is the raw output from TexturePacker using no trimming but allowing for multi-packs. I’m using a 2048x2048px limit because that is recommended for supporting mobile browsers. I’d like to trim and pack them more efficiently once I have things generally working OK.

 

There will be lots of individual animations like this so my ultimate goal is to build a nice efficient workflow requiring the minimum of additional effort for adding more animations.

From the JSON files I’m able to extract the important data and my instinct was to do something along the lines of...

  • load the JSON files and push each image image information into an array
  • preload the sprite sheets and stack them
  • loop through the entire array of images adding each one to the timeline somehow

I found this piece of code from this question posted here before
 

var frames = document.querySelector(".frames");

var spriteSheet = {
  width: 165,
  height: 292,
  total: 64,
  cols: 12,
  rows: 6,
  duration: 2
};

TweenLite.set(frames, { force3D: true });

var tl = new TimelineMax({ repeat: -1 });

for (var i = 0; i < spriteSheet.total; i++) {  
  tl.set(frames, {
    x: (i % spriteSheet.cols) * -spriteSheet.width,
    y: Math.floor(i / spriteSheet.cols) * -spriteSheet.height
  }, i / (spriteSheet.total - 1) * spriteSheet.duration);
}

Which I felt might be the closest match to help get me started.

 

But yeh, the fact it is over multiple PNG files and how to handle that is a head scratcher!

 

Any help or advice would be very much appreciated :)

Thanks.

01-Bulldozer-5.png

01-Bulldozer-0.png

01-Bulldozer-1.png

01-Bulldozer-2.png

01-Bulldozer-3.png

01-Bulldozer-4.png

01-Bulldozer-0.json 01-Bulldozer-1.json 01-Bulldozer-2.json 01-Bulldozer-3.json 01-Bulldozer-4.json 01-Bulldozer-5.json

Link to comment
Share on other sites

This isn't making much sense to me - your JSON file data doesn't match the images at all. For example, the images are 708x750...but your JSON says things like:

"Bulldozer000.png":
{
	"frame": {"x":0,"y":0,"w":782,"h":414},
	"rotated": false,
	"trimmed": false,
	"spriteSourceSize": {"x":0,"y":0,"w":782,"h":414},
	"sourceSize": {"w":782,"h":414}
},

None of the dimensions match. I expected that you'd have one image that has a bunch of "frames" laid out, and the JSON data would describe the coordinates (x, y, width, hight) of each "frame" embedded in the image but i saw nothing like that. Can you please explain? 

  • Like 1
  • Thanks 1
Link to comment
Share on other sites

I think that the dimensions you see for the image (708 × 750) are the size of the thumbnail preview image, not the uploaded full size image?

 

For example, if you look at this link full size version of the image the dimensions I think should be 1564 × 1656.

 

This should make more sense because it is the image dimension (782 × 414) multiplied by 2 × 4 grid.

 

Quote

I expected that you'd have one image that has a bunch of "frames" laid out, and the JSON data would describe the coordinates (x, y, width, hight) of each "frame" embedded in the image

 

So this is where those 2 files, the PNG image and the matching JSON file should now match up with what you expected and make sense.

 

So, for example, 01-Bulldozer-0.json describes the 8 frames in the image 01-Bulldozer-0.png.

 

The reason why there is more than just one image and more than just one json file is that there is a limit to the size of the PNG dimensions considered usable, especially on mobile devices.

 

My understanding is that a maximum dimension size of 2048 × 2048 is best practice for mobile devices.

 

Thats why the software does this extra work of splitting the animation up into several separate image (and accompanying json) files. This is called multi-packing apparently.

 

I’ve written the code which loops through all those json files and stitches the frames together to result in something like you expected, it looks like this.

 

  async getAnimationData(animationName, sheets) {
    let textureFilenames = A();
    let allFrames = {};

    for (let index = 0; index < sheets; index++) {
      let filename = `${animationName}-${index}`;

      textureFilenames.push(filename);
      let path = this.assetMap.resolve(`animation-json/not-trimmed-multipack/${filename}.json`);
      let req = await fetch(path);
      let json = await req.json();

      let { frames } = json;

      Object.entries(frames).forEach(([key, value]) => {
        value.texturePath = `${filename}.png`
      });

      assign(allFrames, frames);
    }

    return { sheets, animationName, frames: allFrames, textureFilenames } ;
  }

And the resulting output looks something like you’d expect with image filename, x position and y position...

Bulldozer000.png
- 01-Bulldozer-0.png
- 0
- 0
Bulldozer001.png
- 01-Bulldozer-0.png
- 782
- 0
Bulldozer002.png
- 01-Bulldozer-0.png
- 0
- 414
Bulldozer003.png
- 01-Bulldozer-0.png
- 782
- 414
Bulldozer004.png
- 01-Bulldozer-0.png
- 0
- 828
Bulldozer005.png
- 01-Bulldozer-0.png
- 782
- 828
Bulldozer006.png
- 01-Bulldozer-0.png
- 0
- 1242
Bulldozer007.png
- 01-Bulldozer-0.png
- 782
- 1242
Bulldozer008.png
- 01-Bulldozer-1.png
- 0
- 0
Bulldozer009.png
- 01-Bulldozer-1.png
- 782
- 0
Bulldozer010.png
- 01-Bulldozer-1.png
- 0
- 414
Bulldozer011.png
- 01-Bulldozer-1.png
- 782
- 414

So now I’m starting to work with Greensock to see if I can use these values for the animation.

 

Sorry if I have not been precise enough, I hope this makes more sense to you now and why the assets are the way they are?

 

Any advice still very much appreciated.

 

Thanks.

Link to comment
Share on other sites

Here's a SpriteSheet helper function I whipped together to accommodate more complex situations like this. You'll need to load/format the data to match what I did (or edit my function), but hopefully it at least gets you moving in the right direction (I slapped some labels on the frames of the first two sheets): 

See the Pen 4299f8616fdf0f87df155c9624bd2604?editors=0010 by GreenSock (@GreenSock) on CodePen

 

function SpriteSheet(container, data, onLoad) {
	container = gsap.utils.toArray(container)[0];
	let progress = 0,
		frames = 0,
		lookup = [],
		loading = [],
		loadingQueue = e => loading.splice(loading.indexOf(e.target), 1) && !loading.length && onLoad && onLoad(),
		curSheet, curIndex, img;
	gsap.set(container, {overflow: "hidden", position: gsap.getProperty(container, "position") === "absolute" ? "absolute" : "relative"});
	data.forEach((sheet, index) => {
		img = document.createElement("img");
		loading.push(img);
		img.addEventListener("load", loadingQueue);
		img.setAttribute("src", sheet.file);
		gsap.set(img, {position: "absolute", top: 0, left: 0, visibility: index ? "hidden" : "inherit"});
		container.appendChild(img);
		sheet.img = img;
		sheet.framesBefore = frames;
		let i = sheet.frames.length;
		frames += i;
		while (i--) {
			lookup.push(index);
		}
	});
	frames--;
	this.progress = function(value) {
		if (arguments.length) {
			let lookupIndex = ~~(value * frames + 0.5),
				sheet, frame;
			if (lookupIndex !== curIndex) {
				curIndex = lookupIndex;
				sheet = data[lookup[curIndex]];
				frame = sheet.frames[curIndex - sheet.framesBefore];
				if (sheet !== curSheet) {
					curSheet && (curSheet.img.style.visibility = "hidden");
					sheet.img.style.visibility = "inherit";
					container.style.width = frame.w + "px";
					container.style.height = frame.h + "px";
					curSheet = sheet;
				}
				curSheet.img.style.transform = "translate(" + -frame.x + "px, " + -frame.y + "px)";
			}
			progress = value;
		}
		return progress;
	};
	this.frame = function(value) {
		arguments.length && this.progress(--value / frames);
		return ~~(progress * frames + 0.5);
	};
	this.progress(0);
}

So your data could look like this (just the first two): 

let data = [{
			file: "https://assets.codepen.io/16327/01-Bulldozer-0.png",
			frames: [
				{"x":0,"y":0,"w":782,"h":414},
				{"x":782,"y":0,"w":782,"h":414},
				{"x":0,"y":414,"w":782,"h":414},
				{"x":782,"y":414,"w":782,"h":414},
				{"x":0,"y":828,"w":782,"h":414},
				{"x":782,"y":828,"w":782,"h":414},
				{"x":0,"y":1242,"w":782,"h":414},
				{"x":782,"y":1242,"w":782,"h":414}
			]
		}, {
			file: "https://assets.codepen.io/16327/01-Bulldozer-1.png",
			frames: [
				{"x":0,"y":0,"w":782,"h":414},
				{"x":782,"y":0,"w":782,"h":414},
				{"x":0,"y":414,"w":782,"h":414},
				{"x":782,"y":414,"w":782,"h":414},
				{"x":0,"y":828,"w":782,"h":414},
				{"x":782,"y":828,"w":782,"h":414},
				{"x":0,"y":1242,"w":782,"h":414},
				{"x":782,"y":1242,"w":782,"h":414}
			]
		}];

Usage: 

let sheet = new SpriteSheet(".spritesheet", data, () => {
  gsap.to(sheet, {progress: 0.8, duration: 2, ease: "none"});
});

So you give it a container element and the data, and it'll load an <img> for each spritesheet file, and fire the onLoad when they're all loaded. Then you can simply animate the "progress" of that SpriteSheet object or the "frame" value (whatever you find more convenient) and it'll handle shifting around the images and showing/hiding them. 

 

Is that what you're looking for? 

  • Like 3
  • Thanks 1
Link to comment
Share on other sites

Wow, thank you for spending the time to do this.

 

This approach looks really interesting and I am going to definitely investigate the way you are using images and hiding/showing them.

 

From my side I have made some good progress I think and am happy with the results so far.

 

You can see where I have got to so far here if you are interested.

 

I customised the exporter from TexturePacker to write slightly more useful JSON to use and have gone a step further and also trimmed the transparency to reduce the number of sheets needed.

 

Besides the preparation of the data from the JSON the main code to animate is this

 

this.tl = gsap.timeline({ repeat: -1, repeatDelay: animation.delay });

for (var i = 0; i < numFrames; i++) {

  let currentFrame = animation.frames[i];
  let x = currentFrame.cornerOffset.x;
  let y = currentFrame.cornerOffset.y;
  let width = currentFrame.frame.w;
  let height = currentFrame.frame.h;
  let backgroundPosition = `-${currentFrame.frame.x}px -${currentFrame.frame.y}px`;
  let backgroundPath = this.assetMap.resolve(`assets/animation-png/${currentFrame.texturePath}`);
  let backgroundImage = `url(${backgroundPath})`;

  this.tl.set(this.element, {
    x,
    y,
    width,
    height,
    backgroundImage,
    backgroundPosition
  }, i / (numFrames - 1) * duration);
}

I was hoping that maybe by preloading those images there would not be notable issues when switching between them.

 

Also, these animations are 24fps so it maybe does not need to be as performant as if I was needed 60fps, for example.

 

Anyway, I’m really pleased to have got a basic working version already with this method.

 

Maybe I should update this to something closer to your approach to improve the performance now?

 

Thanks so much for your advice.

 

Also, I was wondering—once I’ve got this optimised with best practices maybe this could become a useful tutorial for others trying to use TexturePacker assets with Greensock because I didn’t find many (any) resources for this workflow and have learned some stuff during this process which might be useful to others?

  • Like 1
Link to comment
Share on other sites

19 hours ago, chrism said:

once I’ve got this optimised with best practices maybe this could become a useful tutorial for others trying to use TexturePacker assets with Greensock because I didn’t find many (any) resources for this workflow and have learned some stuff during this process which might be useful to others?

Absolutely! We love it when people in the community help each other and spread knowledge about how to accomplish cool animation tasks. If you want to write up a tutorial or something we'd gladly consider having it posted either in these forums or if it's super good, maybe a guest blog post. 

 

Frankly I'm not 100% sure which solution would perform better - you'd need to do some tests to know for sure, but the solution I offered leverages transforms which I believe may be more performant than altering the background-position. And I leveraged a technique for caching and super-fast lookups on the frames. Users may never notice any real-world performance difference though. 

 

Good luck!

  • Like 2
  • Thanks 1
Link to comment
Share on other sites

Thanks again for your help.

 

I’m going to follow your advice and do some tests on performance comparing approaches.

 

Once I’ve settled on a method and workflow I’m happy with from TexturePacker to Greensock I’ll write it up and drop you a line.

 

Cheers!

  • Like 1
  • Thanks 1
Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...