Impact

This forum is read only and just serves as an archive. If you have any questions, please post them on github.com/phoboslab/impact

1 decade ago by Bradley

Scale is 1. I wouldn't think an image would need to be loaded if I am removing a layer. This is where I first noticed the frame rate drop.

1 decade ago by monkeyArms

Quote from Bradley
I've just noticed a significant drop in frame rate the instant layers are being added or removed. It is noticeable in Safari Mobile, and equally noticeable in Safari desktop version when the activity window is open.

I'm not dealing with very large sprite sheets.


Yeah, currently that is just how it works - when you add or remove a layer the composite image has to be regenerated, which is a bit processing intensive. When I wrote this I wasn't really worried or thinking about trying to add or remove layers smoothly.

I suppose I could try adding "states" to the plugin - i.e. you could call saveState() at any point after adding removing layers, and then revert to one of those states. The problem is all that image data would have to be stored in memory (I don't know how big of a deal that is), but then the composite images should be able to be switched fairly instantaneously.

1 decade ago by Bradley

Yup, I was looking at your code, thinking you may need a way to cache the states. I like your idea.

Rendering part of a giant animation sheet causes lag, but simply having them in memory shouldn't hurt, since browsers are made to keep enormous assets in memory. Besides, this plugin drastically reduces redundancy in animation sheets anyways. Games should be smaller on download AND in memory, overall.

I think it's worth a shot. It might be useful to have a way to specify specific states to cache before game play begins so the user doesn't experience lag during game play.

1 decade ago by Bradley

I could totally take a crack at this if you'd like. I realize you're probably a busy programmer. I've built caching and pre-rendering schemes for much more complex things.

1 decade ago by Arantor

Having them in memory is going to hurt because browsers are not designed to cope with images that are huge, especially not in a canvas.

Let's see, though, let's do the leaps of logic.

It's all drawn to an off-screen canvas, through its context and manipulating the data. Now, memory-mapping might be cheap, but throwing around a JavaScript array of width x height x 4 elements probably isn't cheap. (I don't know, I've never thrown around much more than a couple of million elements at a time in JavaScript, but it isn't wonderfully fast)

Then it's converted to a PNG, and stored as a string. Even assuming great compression, you're still going to end up with something that's a non trivial amount of stuff being converted to a PNG and then stored in a base-64 encoded string.

It isn't fast, and it certainly isn't efficient in memory - since you're not using a direct 1:1 relationship with array elements - those array elements are going to take up a VERY large space in memory. Most JS engines internally use floating point numbers, which means you're talking 64 bits per number... for every single element.

A 1000px square sprite sheet is going to create a 4 million element array (1000 x 1000 x 4 components) and that's going to eat up to 32MB of memory to handle it, depending on the JS engine.

The really sad part is, I don't think there's a much better way to do it >_<

1 decade ago by Bradley

It may not be the most optimal thing for performance, but it should probably still be worth trying. Of course there's a trade-off, but the benefits may outweigh them for certain projects.

1 decade ago by Bradley

I just noticed, in the latest version, if a layer is removed from one entity, it is removed from all similar entities.

1 decade ago by monkeyArms

Hey guys,

Sorry I haven't responded in awhile - I landed a freelance gig that has been sucking up all my spare time.

I finally did some refactoring on this plugin tonight. It now works with ig.system.scale, and also seems to run a lot faster. It also allows you to save "states", then revert back to them.

e.g.

this.animSheet = new ig.AnimationSheet( 'media/some-character.png', 16, 16 );
this.animSheet.saveState( 'naked' );
// now you may call this.animSheet.revertToState( 'naked' ) to display the base sprite sheet only
this.animSheet.addLayer( 'media/some-character-HELMET.png' );
this.animSheet.saveState( 'wearing helmet' );
// call this.animSheet.revertToState( 'wearing helmet' ) to use the composited sprite sheet at this point.

I have modified the original post with the updated code. Please let me know if you have any issues with it.

1 decade ago by Xander

Awesome update. Now that scale is working, I will take a crack at implementing this. Can't wait to try this out. :)

Edit: Worked like a charm, of course! Well done. :)

1 decade ago by Datamosh

probably im doing something wrong...
I have to change the state when the player picks up a weapon.
(Pistol is default weapon)

Init:
this.animSheet.saveState('none');
this.animSheet.addLayer('media/weapon_pistol.png');

this.animSheet.revertToState('none');
this.animSheet.addLayer('media/weapon_shotgun.png');
this.animSheet.saveState('shotgun');

this.animSheet.revertToState('none');
this.animSheet.addLayer('media/weapon_doublepistol.png');
this.animSheet.saveState('doublepistol');

When pick up weapon:
this.animSheet.revertToState('shotgun');

I see all the layers at the same time!

What is the correct way to add all the layers as different states at init and then just call them?

1 decade ago by alexandre

bump on this. Similar problem for me:

my entity can be selected, and it can be idle or busy:
selection has 3 states: unselected, highlighted, selected
activity has 2 states: idle, working

I want to represent these 2 properties visually with (layered) animation sheets. Sprite sheets are created, but not sure how to use LayeredAnimation to show these combined properties and states. For example:

1. idle and selected
2. working and unselected
etc.

Thanks in advance

1 decade ago by monkeyArms

guys, I'm sorry.


I will take a look at this as soon as I can. I recently lost my full time job, and am doing full-time freelance web development now...basically I don't have the time I had when originally writing this for my purposes.

I want to make this work for everyone, so please be patient :)

aaron

1 decade ago by clump

First, thanks aaron for developing this. I've been finding it useful, and a nice example to learn from as well. However, I've been puzzling over some odd behaviour that I've encountered sometimes, and after some investigation I think I see what's going on, and it accounts for datamosh's problem too.

To begin, there's a relatively minor bug, probably not the cause anything interesting, but one I noticed in debugging. In addLayer, the variable "this.numActiveLayers" can be updated improperly---if you add a layer that is already active and in the list it will (re-)mark it active and increment that count. Thus it's possible to have more active layers than layers. (Also, addLayer should declare the variable i so it keeps a local scope.)

The bigger issue is in revertToState, where the array of layers is restored. In doing this the array is not actually copied (nor are the activeLayers and numLayers counts updated). So the restored array is actually still shared with its stored copy, and so after restoring when you add new layers you are adding to the saved array of layers as well as the current one. Thus, each subsequent revertToState is not properly reverting to the original, saved state, and so updates accumulate.

One other thing I'm a bit confused about/unsure of. When saving, only the layer list is stored, but not the current image data. The stored data field is set to null. So, from that I'd expect that after each revertToState you'd need to regenerate the composite image. That's not what happens though---in fact, in the update function if you've added/removed any layer it will go through all the saved states and regenerate a new composite for each. I suspect this design choice was forced by the fact that the composite is not created until the update function is called, and so the current state may not have a composite available to save when saveState is called. Would it not be better though to move the composite creation into the AnimationSheet object so it can create one eagerly during saving? Or are there other reasons why construction of the composite needs to be done lazily, in update?

1 decade ago by clump

Ok, and without any desire to fork/hijack aaron's code, here's an updated, simplified version (as a drop-in replacement for layeredAnimation.js) that addresses the above issues. Well, at least it works in my small tests. :)

I find in my layered images that the overlays tend to be much smaller than the base image, and in some cases are just the same image repeated across each frame of the animation. So I also added the ability to specify an optional initial offset and (horizontal) periodicity when adding a new layer. That way my overlays can be smaller, and also reused in different animations even when the frame size is a bit different.

Note that while this should ensure that saved overlays don't get modified by new updates, it still assumes that individual layers themselves are read-only once added/created. Also, you have remove a layer exactly as you specify it (ie same image name, offset, periodicity).

Only thing I'm not real sure of is how it will work if loading of images is delayed. If the images haven't been preloaded and are not available when it creates the composite (ie during state save or restore) then it may end up recreating the composite too often (every update). That doesn't happen in my little examples, but let me know if you try it out and find that happens (I have a fix in mind, but don't want to add more complexity for a non-problem if it doesn't happen in practice).

ig.module(
    'plugins.layeredAnimation'
)
.requires(
    'impact.animation'
)
.defines( function()
{
    ig.AnimationSheet.inject({

        init: function( path, width, height )
        {
            this.parent( path, width, height );
            
            this.copyImage = new ig.Image( path+'?nocache=true' );
            this.layers = [];
            this.states = [];
            this.numLayers = 0;
            this.numActiveLayers = 0;
            this.composited = false;
            this.data = null;
        },
        
        addLayer: function( path, dx, dy, px )
        {
            var found = false;
            if (!dx) dx = 0;
            if (!px) px = 0;
            if (!dy) dy = 0;
            
            for(var i=0; i < this.numLayers; i++) {
                if (this.layers[i].img.path == path && this.layers[i].dx==dx && this.layers[i].px==px && this.layers[i].dy==dy) {
                    if (!this.layers[i].active) {
                        this.layers[i].active = true;
                        this.numActiveLayers++;
                    }
                    found = true;
                }
            }
            if (!found) {
                this.layers.push({
                    img: new ig.Image( path ),
                    dx: dx,
                    dy: dy,
                    px: px,
                    active: true
                });
                this.numLayers++;
                this.numActiveLayers++;
            }
            this.composited = false;
            this.data = null;
        },
        
        removeLayer: function( path, dx, dy, px )
        {
            var found = false,
                i;

            if (!dx) dx = 0;
            if (!px) px = 0;
            if (!dy) dy = 0;
            
            for(i=0; i < this.numLayers; i++) {
                if (this.layers[i].img.path == path && this.layers[i].dx==dx && this.layers[i].px==px  && this.layers[i].dy==dy) {
                    this.layers[i].active = false;
                    this.numActiveLayers--;
                    this.composited = false;
                    this.data = null;
                    found = true;
                }
            }
            
            return found;
        },
        
        saveState: function( name )
        {
            var i,
                state = {
                name: name,
                data: null,
                layers: []
            };
            
            for(i=0; i < this.numLayers; i++) {
                if (this.layers[i].active) {
                    state.layers.push( this.layers[i] );
                }
            }
            state.data = this.createComposite(state.layers);
            this.states.push( state );
        },
        
        revertToState: function( name )
        {
            var i, len;
            
            for(i=0, len = this.states.length; i < len; i++) {
                if (this.states[i].name == name) {
                    this.data = this.states[i].data;
                    this.layers = this.states[i].layers.slice();
                    if (!this.data) {
                        // we didn't store a composite, so create one now
                        this.data = this.createComposite(this.layers);
                        // and also try and store it so we don't have to create another later
                        this.states[i].data = this.data;
                    }
                    this.numLayers = this.states[i].layers.length;
                    this.numActiveLayers = this.numLayers; // all layers active
                    this.composited = (this.data) ? true : false;
                    
                    return true;    
                }
            }
            
            return false;
        },

        createComposite: function( layers )
        {
            var bufferCanvas = ig.$new( 'canvas' ),
                buffer = bufferCanvas.getContext( '2d' ),
                i,
                len;

            bufferCanvas.width = this.image.data.width;
            bufferCanvas.height = this.image.data.height;

            buffer.drawImage( this.copyImage.data, 0, 0 );
            
            for(i=0, len=layers.length; i < len; i++) {
                if (layers[i].active) {
                    var dx = layers[i].dx;
                    buffer.drawImage( layers[i].img.data, dx, layers[i].dy );
                    if (layers[i].px>0) {
                        dx += layers[i].px;
                        while (dx<this.image.data.width) {
                            buffer.drawImage( layers[i].img.data, dx, layers[i].dy );
                            dx += layers[i].px;
                        }
                    } 
                }
            }

            return bufferCanvas;
        }
    });

    ig.Animation.inject({    
        
        update: function()
        {
            var layersLoaded = true,
                i;
            
            if (!this.sheet.composited) {

                if (!this.sheet.image.loaded || !this.sheet.copyImage || !this.sheet.copyImage.loaded) {
                    layersLoaded = false;
                } else {
                    for(i=0; i < this.sheet.numLayers; i++) {
                        if (this.sheet.layers[i].active && !this.sheet.layers[i].img.loaded) {
                            layersLoaded = false;
                            break;
                        }
                    }
                }

                if (layersLoaded) {

                    if (!this.sheet.composited) {
                        this.sheet.data = this.sheet.createComposite( this.sheet.layers );
                    } 
                    if (this.sheet.data) {
                        this.sheet.composited = true;    
                        this.sheet.image.data = this.sheet.data;
                    }
                }
            } else {
                this.sheet.image.data = this.sheet.data;
            }

            this.parent();
        }
    });
});

1 decade ago by mimik

Hey guys,
I like this plugin!

It works fine on Desktop
But when i try this on iosImpact i got a big bad white box instead of the png.

Is this working for iOS Impact at all?
I think there is something wrong with the catching or something?


i have this:
this.animSheet.addLayer( 'media/awesome_helmet.png' );

1 decade ago by mimik

I have a problem using this with iOsImpact when i draw a new layer i just get a whitescreen, in place of the enity.
Is there away around this?

1 decade ago by Jerczu

Big white box is there because either your sheet can't be loaded into memory or your sheet has off pixels make sure you check that... As far as I am aware your sheet can't exceed 2000x2000 px

1 decade ago by Jerczu

There is also mem limit on graphic I had this issue when releasing kitchen fury all it took was reducing size of the sprite sheets

1 decade ago by bjennings76

Great plug-in! I was staring down the barrel of a 6-layer animated character with each layer possibly being swapped out based on costume choices. I was about to create 6 entities until I came across this. It's a one-stop shop solution for me and worked on the first run. Amazballs!

Thanks!!

10 years ago by Seeders

I know this is an old thread, but I'm trying to implement this and running in to a really confusing problem.

I have a 2d array of tiles for my map. I want to blend tiles, so that a sand tile next to a grass tile has a nice transition. I wanted to use this layered technique, and add corner/edge grass transitions to my sand tiles when appropriate.

The problem I am having is that it seems like the composite image is somehow cached and every sand tile paints with the same composite. Does every entity of the same type use the same instance of AnimationSheet?

Can I not add layers to a single instance?

10 years ago by Joncom

If you define the animation sheet as an entity-property, then yes, all instances of that entity will share the same animation sheet. In order for each entity to have his own animation sheet, you'd have to create it "live", perhaps in the init or update call.
Page 2 of 2
« first ‹ previous next › last »