Skip to content

Commit

Permalink
Move sort functionality into rekapi.Rekapi
Browse files Browse the repository at this point in the history
  • Loading branch information
jeremyckahn committed Sep 1, 2017
1 parent 5e76bc9 commit b96ff2e
Show file tree
Hide file tree
Showing 4 changed files with 74 additions and 194 deletions.
55 changes: 49 additions & 6 deletions src/rekapi.js
Original file line number Diff line number Diff line change
Expand Up @@ -219,11 +219,31 @@ export class Rekapi {
/**
* @member {(Object|CanvasRenderingContext2D|HTMLElement)}
* rekapi.Rekapi#context The rendering context for an animation.
* @default {}
*/
this.context = context;
this._actors = [];
this._playState = STOPPED;

/**
* @member {(rekapi.actorSortFunction|null)} rekapi.Rekapi#sort Optional
* function for sorting the render order of {@link rekapi.Actor}s. If set,
* this is called each frame before the {@link rekapi.Actor}s are rendered.
* If not set, {@link rekapi.Actor}s will render in the order they were
* added via {@link rekapi.Rekapi#addActor}.
*
* The following example assumes that all {@link rekapi.Actor}s are circles
* that have a `radius` {@link rekapi.KeyframeProperty}. The circles will
* be rendered in order of the value of their `radius`, from smallest to
* largest. This has the effect of layering larger circles on top of
* smaller circles, thus giving a sense of perspective.
*
* const rekapi = new Rekapi();
* rekapi.sort = actor => actor.get().radius;
* @default null
*/
this.sort = null;

this._events = {
animationComplete: [],
playStateChange: [],
Expand Down Expand Up @@ -547,17 +567,17 @@ export class Rekapi {
millisecond = this._lastUpdatedMillisecond,
doResetLaterFnKeyframes = false
) {
const skipRender = this.renderers.some(
renderer => renderer._batchRendering
);

fireEvent(this, 'beforeUpdate');

const renderOrder = this.sort ?
_.sortBy(this._actors, this.sort) :
this._actors;

// Update and render each of the actors
this._actors.forEach(actor => {
renderOrder.forEach(actor => {
actor._updateState(millisecond, doResetLaterFnKeyframes);

if (!skipRender && actor.wasActive) {
if (actor.wasActive) {
actor.render(actor.context, actor.get());
}
});
Expand Down Expand Up @@ -787,4 +807,27 @@ export class Rekapi {
renderer instanceof rendererConstructor
)[0];
}

/**
* Move a {@link rekapi.Actor} around within the internal render order list.
* By default, a {@link rekapi.Actor} is rendered in the order it was added
* with {@link rekapi.Rekapi#addActor}.
*
* This method has no effect if {@link rekapi.Rekapi#sort} is set.
*
* @method rekapi.Rekapi#moveActorToPosition
* @param {rekapi.Actor} actor
* @param {number} layer This should be within `0` and the total number of
* {@link rekapi.Actor}s in the animation. That number can be found with
* {@link rekapi.Rekapi#getActorCount}.
* @return {rekapi.Rekapi}
*/
moveActorToPosition (actor, position) {
if (position < this._actors.length && position > -1) {
this._actors = _.without(this._actors, actor);
this._actors.splice(position, 0, actor);
}

return this;
}
}
154 changes: 5 additions & 149 deletions src/renderers/canvas.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import _ from 'lodash';
import Rekapi, {
rendererBootstrappers,
fireEvent
rendererBootstrappers
} from '../rekapi';

// PRIVATE UTILITY FUNCTIONS
Expand All @@ -24,63 +23,6 @@ const dimension = (canvas, heightOrWidth, newSize = undefined) => {
return canvas[heightOrWidth];
};

/*!
* Takes care of some pre-rendering tasks for canvas animations.
* @param {CanvasRenderer} canvasRenderer
*/
const beforeRender = canvasRenderer => canvasRenderer.clear();

/*!
* Render all the `Actor`s at whatever position they are currently in.
* @param {Rekapi}
* @param {CanvasRenderer} canvasRenderer
* @return {Rekapi}
*/
const render = (rekapi, canvasRenderer) => {
fireEvent(rekapi, 'beforeRender');
const { _renderOrderSorter } = canvasRenderer;

const renderOrder = _renderOrderSorter ?
_.pluck(
_.sortBy(canvasRenderer._canvasActors, _renderOrderSorter),
'id'
) :
canvasRenderer._renderOrder;

const { _canvasActors } = canvasRenderer;

renderOrder.forEach(id => {
const actor = _canvasActors[id];

if (actor.wasActive) {
actor.render(actor.context, actor.get());
}
});

fireEvent(rekapi, 'afterRender');

return rekapi;
};

/*!
* @param {Actor} actor
* @param {CanvasRenderer} canvasRenderer
*/
const addActor = (actor, canvasRenderer) => {
const { id } = actor;
canvasRenderer._renderOrder.push(id);
canvasRenderer._canvasActors[id] = actor;
};

/*!
* @param {Actor} actor
* @param {CanvasRenderer} canvasRenderer
*/
const removeActor = (actor, canvasRenderer) => {
canvasRenderer._renderOrder = _.without(canvasRenderer._renderOrder, actor.id);
delete canvasRenderer._canvasActors[actor.id];
};

// CANVAS RENDERER OBJECT
//

Expand All @@ -97,15 +39,6 @@ const removeActor = (actor, canvasRenderer) => {
*
* const canvasRenderer = rekapi.getRendererInstance(CanvasRenderer);
*
* `CanvasRenderer` adds some canvas-specific events you can bind to
* with {@link rekapi.Rekapi#on} (and unbind from
* with {@link rekapi.Rekapi#off}:
*
* - __beforeRender__: Fires just before a {@link rekapi.Actor} is rendered to
* the canvas.
* - __afterRender__: Fires just after a {@link rekapi.Actor} is rendered to
* the canvas.
*
* __Note__: {@link rekapi.CanvasRenderer} is added to {@link
* rekapi.Rekapi#renderers} automatically, there is no reason to call the
* constructor yourself in most cases.
Expand All @@ -120,22 +53,10 @@ export class CanvasRenderer {
constructor (rekapi, context = undefined) {
Object.assign(this, {
rekapi,
canvasContext: context || rekapi.context,
_renderOrder: [],
_renderOrderSorter: null,
_canvasActors: {},
_batchRendering: true
});

_.extend(rekapi._events, {
beforeRender: [],
afterRender: []
canvasContext: context || rekapi.context
});

rekapi.on('afterUpdate', () => render(rekapi, this));
rekapi.on('addActor', (rekapi, actor) => addActor(actor, this));
rekapi.on('removeActor', (rekapi, actor) => removeActor(actor, this));
rekapi.on('beforeRender', () => beforeRender(this));
rekapi.on('beforeUpdate', () => this.clear());
}

/**
Expand All @@ -161,77 +82,12 @@ export class CanvasRenderer {
/**
* Erase the `<canvas>`.
* @method rekapi.CanvasRenderer#clear
* @return {rekapi.Rekapi}
* @return {rekapi.CanvasRenderer}
*/
clear () {
this.canvasContext.clearRect(0, 0, this.width(), this.height());

return this.rekapi;
}

/**
* Move a {@link rekapi.Actor} around within the render order list. Each
* {@link rekapi.Actor} is rendered in order of its layer (layers and {@link
* rekapi.Actor}s have a 1:1 relationship). The later a {@link rekapi.Actor}
* is added to an animation (with {@link rekapi.Rekapi#addActor}), the higher
* its layer. Lower layers (starting with 0) are rendered earlier.
*
* This method has no effect if an order function is set with {@link
* rekapi.CanvasRenderer#setOrderFunction}.
*
* @method rekapi.CanvasRenderer#moveActorToLayer
* @param {rekapi.Actor} actor
* @param {number} layer This should be within `0` and the total number of
* {@link rekapi.Actor}s in the animation. That number can be found with
* {@link rekapi.Rekapi#getActorCount}.
* @return {rekapi.Actor}
*/
moveActorToLayer (actor, layer) {
if (layer < this._renderOrder.length && layer > -1) {
this._renderOrder = _.without(this._renderOrder, actor.id);
this._renderOrder.splice(layer, 0, actor.id);
}

return actor;
}

/**
* Set a function that defines the render order of the actors. This is
* called each frame before the actors are rendered.
*
* The following example assumes that all actors are circles that have a
* `radius` {@link rekapi.KeyframeProperty}. The circles will be rendered
* in order of the value of their `radius`, from smallest to largest. This
* has the effect of layering larger circles on top of smaller circles, thus
* giving a sense of perspective.
*
* If a render order function is specified, {@link
* rekapi.CanvasRenderer#moveActorToLayer} will have no effect.
*
* rekapi.getRendererInstance(CanvasRenderer).setOrderFunction(
* actor => actor.get().radius
* );
* @method rekapi.CanvasRenderer#setOrderFunction
* @param {rekapi.actorSortFunction} sortFunction
* @return {rekapi.Rekapi}
*/
setOrderFunction (sortFunction) {
this._renderOrderSorter = sortFunction;
return this.rekapi;
}

/**
* Remove the order function set by {@link
* rekapi.CanvasRenderer#setOrderFunction}. The render order defaults back
* to the order in which the {@link rekapi.Actor}s were added to the
* animation.
*
* @method rekapi.CanvasRenderer#unsetOrderFunction
* @return {rekapi.Rekapi}
*/
unsetOrderFunction () {
this._renderOrderSorter = null;
return this.rekapi;
return this;
}
}

Expand Down
39 changes: 0 additions & 39 deletions test/canvas.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,43 +36,4 @@ describe('Canvas renderer', () => {
assert.equal(typeof CanvasRenderer, 'function');
});
});

describe('renderOrder array', () => {
it('is populated as actors are added', () => {
assert.equal(rekapi.getRendererInstance(CanvasRenderer)._renderOrder[0], actor.id);
});

it('is emptied as actors are removed', () => {
rekapi.removeActor(actor);
assert.equal(rekapi.getRendererInstance(CanvasRenderer)._renderOrder.length, 0);
});

describe('compatibility with Rekapi#removeActor', () => {
it('is updated when Rekapi#removeActor is called', () => {
rekapi.removeActor(actor);

assert.equal(rekapi.getRendererInstance(CanvasRenderer)._renderOrder.indexOf(actor.id), -1);
});
});
});

describe('#moveActorToLayer', () => {
it('can move actors to the beginning of the list', () => {
actor2 = setupTestActor(rekapi);
rekapi.getRendererInstance(CanvasRenderer).moveActorToLayer(actor2, 0);

assert.equal(rekapi.getRendererInstance(CanvasRenderer)._renderOrder[0], actor2.id);
assert.equal(rekapi.getRendererInstance(CanvasRenderer)._renderOrder[1], actor.id);
assert.equal(rekapi.getActorCount(), 2);
});

it('can move actors to the end of the list', () => {
actor2 = setupTestActor(rekapi);
rekapi.getRendererInstance(CanvasRenderer).moveActorToLayer(actor, 1);

assert.equal(rekapi.getRendererInstance(CanvasRenderer)._renderOrder[0], actor2.id);
assert.equal(rekapi.getRendererInstance(CanvasRenderer)._renderOrder[1], actor.id);
assert.equal(rekapi.getActorCount(), 2);
});
});
});
20 changes: 20 additions & 0 deletions test/rekapi.js
Original file line number Diff line number Diff line change
Expand Up @@ -727,4 +727,24 @@ describe('Rekapi', () => {
assert.equal(testActor2.get().x, 100);
});
});

describe('#moveActorToPosition', () => {
it('can move actors to the beginning of the list', () => {
actor2 = setupTestActor(rekapi);
rekapi.moveActorToPosition(actor2, 0);

assert.equal(rekapi._actors[0], actor2);
assert.equal(rekapi._actors[1], actor);
assert.equal(rekapi.getActorCount(), 2);
});

it('can move actors to the end of the list', () => {
actor2 = setupTestActor(rekapi);
rekapi.moveActorToPosition(actor, 1);

assert.equal(rekapi._actors[0], actor2);
assert.equal(rekapi._actors[1], actor);
assert.equal(rekapi.getActorCount(), 2);
});
});
});

0 comments on commit b96ff2e

Please sign in to comment.