4
\$\begingroup\$

I am building a library that uses animations in a canvas.

I need to apply effects to some objects in the canvas and I came up with an effect stack that contains each effect to be executed on a object. Each object has it's own effect stack, because each effect has some random values to them.

As each effect needs to be executed on a set of frames, I need those to get back in the effect stack to be continued in the next frame.

Here is what I want:

enter image description here

Note that each effect in the stack is consumed once. If the effect is not completed, it is reinserted on the stack.


I came up with a couple of solutions:

  1. I pop() the effect, consume it and unshift() it in the stack. Reading only the top of the stack n times (n -> nb of effects).
  2. The opposite is also possible: shift() -> consume() -> push()

    The problem with these 2, is the shift() and the unshift() as it copies all the values in memory to another location. I'd prefer to use pop() or push() only.

  3. I use 2 stacks. One for the current effects to be consumed and the other for the effects that are consumed. And each frame, I copy the values from #2 to #1, then reset the #2.

But now, I have to create 2 stacks for each object and add a function to process those, which complicates all the code that needs access to these effects.


A basic implementation for the second solution is:

var EffectStack = function() {
    this.stack = [];
    this.queued = [];
}

EffectStack.prototype.length = function() {
    return this.stack.length;
};

EffectStack.prototype.push = function( fx ) {
    this.stack.push( fx );
};

EffectStack.prototype.pop = function( fx ) {
    return this.stack.pop( fx );
};

EffectStack.prototype.update = function() {
    this.stack = this.queued.slice(0);
    this.queued = [];
};

EffectStack.prototype.continue = function( fx ) {
    this.queued.push( fx );
};

And for the animated object:

/**
 * Applies each effect and updates the Effect Stack by queuing
 * the effects that weren't complete.
 */
AnimObject.fn.consumeEffects = function() {
    var fx
    while( this.eStack.length() > 0 ) {
        // fx = {
        //     func: function(){ ...core of the effect... },
        //     data: { ... }
        // }
        fx = this.eStack.pop();
        fx.func( this, fx ); 
    }

    this.eStack.update();
}

Effect Example

/**
 * This is an Effect (fx).
 */
changeOpacity: function( value, step ){ 
    return {

        // Basic information.
        data: {
            step: step ? step : Math.random()*0.06 + 0.04,
            targetValue: value ? value : Math.random()*0.7 + 0.2
        },

        // The core of the effect.
        func: function( particle, fx ){
            if(!FX.opacity_FUNC( animObj, fx.data )) {
                // **IMPORTANT**
                // Add to stack if the effect isn't over.
                animObj.eStack.continue({ 
                    data: fx.data, 
                    func: fx.func 
                });
            }
        }
    }
}


/**
 * -- THIS IS A SUB_EFFECT. IT CONTAINS THE CORE LOGIC --
 * -- SUB_EFFECTS CAN BE MIXED TOGETHER. --
 * Changes the opacity of a AnimObject that is floating.
 *
 * @param animObj   : Affected AnimObject.
 * @param data      : Transformation data.
 */
opacity_FUNC: function( animObj, data ) {

    // Distance to targetValue.
    var _delta = (animObj.opacity - data.targetValue);

    // If the <animObj> still validates the conditions,
    // continue the effect till it's done.
    if ((animObj.isFloating || animObj.isMoving) && Math.abs(_delta) > 0.1) 
    {
        animObj.opacity += data.step * (_delta > 0 ? -1 : 1);
        return false;   
    } 

    // Effect is over.
    else 
    {
        animObj.opacity = data.targetValue;
        return true;
    }
},

Here is a little jsfiddle of what my sandbox looks like.

But, I still need to copy the values. Any better ideas?

\$\endgroup\$
  • \$\begingroup\$ I refer you to collectionsjs.com/deque \$\endgroup\$ – J_H Aug 31 '17 at 0:20
  • \$\begingroup\$ i'm not real clear on why you need to do all this array manipulation. if you're concerned about memory,why not just have a single array for your animations and and refer to them from that array (only storing and returning the relevant indexes).. you say in the comments you needto remove it from the stack but it sounds like you're just putting it back on later.. so why remove it at all? this is how i've done it \$\endgroup\$ – I wrestled a bear once. Aug 31 '17 at 1:10
  • \$\begingroup\$ Good question! As you can see, my fx object contains a function and it's data. The data contains all the important numbers, ex : targetValues, steps, speed, etc. The func, contains the logic. I did it that way to separate the effect's code from the basic animations. That way I can also mix effects by calling their functions. The thing is, it is the func that knows if the effect is over (or if it is interrupted). If the effect isn't over, it is reinserted on the stack. So it is really flexible. An effect can call another bunch of effects. \$\endgroup\$ – Bird Aug 31 '17 at 3:07
2
\$\begingroup\$

You want a data structure that supports efficiently manipulating head and tail. Usually we call that a deque, a double-ended queue, sometimes implemented with linked lists.

As I look at your diagram, the containers on top and bottom in some sense hold the same R,B,Y sequence of items, just in cyclic permutation. So why don't you consider using a simple array of size N, with head and tail indices that you increment mod N ? Then you only incur expensive memory copies when an effect finally completes.

\$\endgroup\$
  • \$\begingroup\$ @JH, thanks for your answer. The problem with this approach is that when an effect is done, I need it to be removed from the stack. So in other words, I have to splice my stack and put it back together. Well, I'm going to give it a try. Might be a good solution. \$\endgroup\$ – Bird Aug 30 '17 at 18:09
  • \$\begingroup\$ Hey, I checked the source code of collectionjs and operations that shifts or unshift (like in a deque) still copies all the values each time a value is queued (which is normal I guess). In my implementation, it copies the values just once: at the end of the frame. Or maybe I missed something when I read the source code? \$\endgroup\$ – Bird Aug 31 '17 at 3:41
  • \$\begingroup\$ No. Each shift() or unshift() of a scalar value (argsLength==1) completes in O(1) time, independent of deque size. I'm looking e.g. at unshift github.com/montagejs/collections/blob/master/… (shift is simpler and also fast). Even for a usage pattern that would (unlike yours) keep expanding the number of stored items, each ensureCapacity call has amortized cost of log N. \$\endgroup\$ – J_H Aug 31 '17 at 15:31
1
\$\begingroup\$

In the update() method of EffectStack, the queued property is re-assigned to an empty array:

this.queued = [];

You could consider truncating the array by setting the length property to zero:

this.queued.length = 0;

There are various responses to Is it an antipattern to set an array length in JavaScript? that may prove valuable in this regard.


In opacity_FUNC() there is an else block, but the else and brackets can be removed, since the block above contains a return statement.

// Effect is over.
else 
{
    animObj.opacity = data.targetValue;
    return true;
}

can be simplified to the following:

// Effect is over.
animObj.opacity = data.targetValue;
return true;
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.