fixed several memory issues with transition logic. Heavily refactored Transition module. New Animation isRunning method. destroy() method now correctly stops currently running transitions. added several transition and destroy related unit tests

This commit is contained in:
Eric Rowell 2013-01-13 19:59:35 -08:00
parent b6ba1a503c
commit 8ed84f474a
12 changed files with 189 additions and 86 deletions

View File

@ -12,6 +12,7 @@ class Build < Thor
]
UNIT_TESTS = [
"tests/js/unit/animationTests.js",
"tests/js/unit/globalTests.js",
"tests/js/unit/nodeTests.js",
"tests/js/unit/stageTests.js",
@ -19,7 +20,6 @@ class Build < Thor
"tests/js/unit/layerTests.js",
"tests/js/unit/shapeTests.js",
"tests/js/unit/ddTests.js",
"tests/js/unit/animationTests.js",
"tests/js/unit/transitionTests.js",
"tests/js/unit/shapes/rectTests.js",
"tests/js/unit/shapes/circleTests.js",

View File

@ -4,8 +4,7 @@
* animations
* @constructor
* @param {Function} func function executed on each animation frame
* @param {Kinetic.Node} [node] node to be redrawn.&nbsp; Specifying a node will improve
* draw performance.&nbsp; This can be a shape, a group, a layer, or the stage.
* @param {Kinetic.Node} [node] node to be redrawn.&nbsp; Can be a layer or the stage. Not specifying a node will result in no redraw.
*/
Kinetic.Animation = function(func, node) {
this.func = func;
@ -21,6 +20,20 @@
* Animation methods
*/
Kinetic.Animation.prototype = {
/**
* determine if animation is running. returns true or false
* @name isRunning
* @methodOf Kinetic.Aniamtion.prototype
*/
isRunning: function() {
var a = Kinetic.Animation, animations = a.animations;
for(var n = 0; n < animations.length; n++) {
if(animations[n].id === this.id) {
return true;
}
}
return false;
},
/**
* start animation
* @name start

View File

@ -26,7 +26,7 @@
// Group or Shape node types
else {
if(this.getDragOnTop()) {
this._buildDragLayer();
dd._buildDragLayer(this);
dd.anim.node = stage.dragLayer;
dd.prevParent = this.getParent();
// WARNING: it's important to delay the moveTo operation,
@ -34,10 +34,12 @@
// has completed or else there will be a flicker on mobile devices
// due to the time it takes to append the dd canvas to the DOM
setTimeout(function() {
that.moveTo(stage.dragLayer);
dd.prevParent.getLayer().draw();
stage.dragLayer.draw();
dd.anim.start();
if(dd.node) {
that.moveTo(stage.dragLayer);
dd.prevParent.getLayer().draw();
stage.dragLayer.draw();
dd.anim.start();
}
}, 0);
}
else {
@ -47,11 +49,11 @@
}
}
};
Kinetic.Node.prototype._buildDragLayer = function() {
var dd = Kinetic.DD, stage = this.getStage(), nodeType = this.nodeType, lastContainer, group;
Kinetic.DD._buildDragLayer = function(no) {
var dd = Kinetic.DD, stage = no.getStage(), nodeType = no.nodeType, lastContainer, group;
// re-construct node tree
this._eachAncestorReverse(function(node) {
no._eachAncestorReverse(function(node) {
if(node.nodeType === 'Layer') {
stage.dragLayer.setAttrs({
x: node.getX(),
@ -112,9 +114,9 @@
}
};
Kinetic.DD._endDrag = function(evt) {
var dd = Kinetic.DD, node = dd.node;
if(node) {
var nodeType = node.nodeType, stage = node.getStage();
var dd = Kinetic.DD, node = dd.node, nodeType, stage;
if(node) { nodeType = node.nodeType, stage = node.getStage();
node.setListening(true);
if(nodeType === 'Stage') {
node.draw();
@ -122,7 +124,7 @@
else {
if((nodeType === 'Group' || nodeType === 'Shape') && node.getDragOnTop() && dd.prevParent) {
node.moveTo(dd.prevParent);
stage.dragLayer.remove();
node.getStage().dragLayer.remove();
dd.prevParent = null;
}
@ -137,7 +139,7 @@
node._handleEvent('dragend', evt);
}
}
dd.node = null;
delete dd.node;
dd.anim.stop();
};
/**

View File

@ -152,8 +152,6 @@
destroy: function() {
var parent = this.getParent(), stage = this.getStage(), dd = Kinetic.DD, go = Kinetic.Global;
this.remove();
// destroy children
while(this.children && this.children.length > 0) {
this.children[0].destroy();
@ -163,10 +161,17 @@
go._removeId(this.getId());
go._removeName(this.getName(), this._id);
// remove from DD
// stop DD
if(dd && dd.node && dd.node._id === this._id) {
delete Kinetic.DD.node;
node._endDrag();
}
// stop transition
if(this.trans) {
this.trans.stop();
}
this.remove();
},
/**
* get attrs

View File

@ -158,11 +158,6 @@
getUserPosition: function() {
return this.getTouchPosition() || this.getMousePosition();
},
/**
* get stage
* @name getStage
* @methodOf Kinetic.Stage.prototype
*/
getStage: function() {
return this;
},
@ -416,9 +411,13 @@
},
_mouseup: function(evt) {
this._setUserPosition(evt);
var dd = Kinetic.DD;
var obj = this.getIntersection(this.getUserPosition());
var that = this;
var that = this, dd = Kinetic.DD, obj = this.getIntersection(this.getUserPosition());
// end drag and drop
if(dd) {
dd._endDrag(evt);
}
if(obj && obj.shape) {
var shape = obj.shape;
shape._handleEvent('mouseup', evt);
@ -443,11 +442,6 @@
}
}
this.clickStart = false;
// end drag and drop
if(dd) {
dd._endDrag(evt);
}
},
_touchstart: function(evt) {
this._setUserPosition(evt);
@ -469,9 +463,13 @@
},
_touchend: function(evt) {
this._setUserPosition(evt);
var dd = Kinetic.DD;
var obj = this.getIntersection(this.getUserPosition());
var that = this;
var that = this, dd = Kinetic.DD, obj = this.getIntersection(this.getUserPosition());
// end drag and drop
if(dd) {
dd._endDrag(evt);
}
if(obj && obj.shape) {
var shape = obj.shape;
shape._handleEvent('touchend', evt);
@ -497,11 +495,6 @@
}
this.tapStart = false;
// end drag and drop
if(dd) {
dd._endDrag(evt);
}
},
_touchmove: function(evt) {
this._setUserPosition(evt);

View File

@ -6,10 +6,11 @@
* @constructor
*/
Kinetic.Transition = function(node, config) {
var that = this, obj = {};
this.node = node;
this.config = config;
this.tweens = [];
var that = this;
// add tween for each property
function addTween(c, attrs, obj, rootObj) {
@ -26,19 +27,38 @@
}
}
}
var obj = {};
addTween(config, node.attrs, obj, obj);
var finishedTweens = 0;
for(var n = 0; n < this.tweens.length; n++) {
var tween = this.tweens[n];
tween.onFinished = function() {
finishedTweens++;
if(finishedTweens >= that.tweens.length) {
that.onFinished();
// map first tween event to transition event
this.tweens[0].onStarted = function() {
};
this.tweens[0].onStopped = function() {
node.transAnim.stop();
};
this.tweens[0].onResumed = function() {
node.transAnim.start();
};
this.tweens[0].onLooped = function() {
};
this.tweens[0].onChanged = function() {
};
this.tweens[0].onFinished = function() {
var newAttrs = {};
// create new attr obj
for(var key in config) {
if(key !== 'duration' && key !== 'easing' && key !== 'callback') {
newAttrs[key] = config[key];
}
};
}
}
node.transAnim.stop();
node.setAttrs(newAttrs);
if(config.callback) {
config.callback();
}
};
};
/*
* Transition methods
@ -116,34 +136,20 @@
* transition completes
*/
Kinetic.Node.prototype.transitionTo = function(config) {
var that = this, trans = new Kinetic.Transition(this, config);
if(!this.transAnim) {
this.transAnim = new Kinetic.Animation();
}
/*
* create new transition
*/
var node = this.nodeType === 'Stage' ? this : this.getLayer();
var that = this;
var trans = new Kinetic.Transition(this, config);
this.transAnim.func = function() {
trans._onEnterFrame();
};
this.transAnim.node = node;
this.transAnim.node = this.nodeType === 'Stage' ? this : this.getLayer();
// subscribe to onFinished for first tween
trans.onFinished = function() {
// remove animation
that.transAnim.stop();
// callback
if(config.callback) {
config.callback();
}
};
// auto start
trans.start();
this.transAnim.start();
this.trans = trans;
return trans;
};
})();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,5 @@
Test.Modules.DD = {
'remove shape from onclick': function(containerId) {
'remove shape with onclick': function(containerId) {
var stage = new Kinetic.Stage({
container: containerId,
width: 578,
@ -37,6 +37,8 @@ Test.Modules.DD = {
clientX: 291,
clientY: 112 + top
});
warn(layer.toDataURL() === dataUrls['cleared'], 'canvas should be cleared after removing shape onclick');
},
'test dragstart, dragmove, dragend': function(containerId) {

View File

@ -83,14 +83,9 @@ Test.Modules.MANUAL = {
fill: 'green',
stroke: 'black',
strokeWidth: 4,
shadow: {
color: 'black',
offset: {
x: 10,
y: 10
},
alpha: 0.5
}
shadowColor: 'black',
shadowOffset: 10,
shadowOpacity: 0.5
});
layer.add(rect);
@ -98,16 +93,15 @@ Test.Modules.MANUAL = {
rect.transitionTo({
duration: 2,
shadow: {
offset: {
x: 80
}
shadowOffset: {
x: 80
},
x: 400,
y: 30,
rotation: Math.PI * 2,
easing: 'bounce-ease-out'
});
},
'TRANSITION - all transition types': function(containerId) {
document.getElementById(containerId).style.height = '300px';

View File

@ -1936,7 +1936,7 @@ Test.Modules.NODE = {
stage.add(layer);
test(stage.getAbsoluteZIndex() === 0, 'stage abs zindex should be 0');
console.log(layer.getAbsoluteZIndex());
//console.log(layer.getAbsoluteZIndex());
test(layer.getAbsoluteZIndex() === 1, 'layer abs zindex should be 1');
test(group1.getAbsoluteZIndex() === 2, 'group1 abs zindex should be 2');
test(group2.getAbsoluteZIndex() === 3, 'group2 abs zindex should be 3');
@ -2282,7 +2282,7 @@ Test.Modules.NODE = {
layer.add(circle);
stage.add(layer);
test(stage.children.length === 1, 'stage should have 1 children');
test(stage.get('.myLayer')[0] !== undefined, 'layer should exist');
test(stage.get('.myCircle')[0] !== undefined, 'circle should exist');
@ -2506,5 +2506,47 @@ Test.Modules.NODE = {
test(go.names.myRect2 === undefined, 'rect still in hash');
test(Kinetic.Global.shapes[circleColorKey] === undefined, 'circle color key should not be in shapes hash');
test(Kinetic.Global.shapes[rectColorKey] === undefined, 'rect color key should not be in shapes hash');
},
'destroy node mid transition': function(containerId) {
var stage = new Kinetic.Stage({
container: containerId,
width: 578,
height: 200
});
var layer = new Kinetic.Layer();
var rect = new Kinetic.Rect({
x: 100,
y: 100,
width: 100,
height: 50,
fill: 'green',
stroke: 'black',
strokeWidth: 4,
shadowColor: 'black',
shadowOffset: 10,
shadowOpacity: 0.5
});
layer.add(rect);
stage.add(layer);
rect.transitionTo({
duration: 2,
shadowOffset: {
x: 80
},
x: 400,
y: 30,
rotation: Math.PI * 2,
easing: 'bounce-ease-out'
});
setTimeout(function() {
test(rect.transAnim.isRunning(), 'rect trans should be running before destroying it');
rect.destroy();
test(!rect.transAnim.isRunning(), 'rect trans should not be running after destroying it');
layer.draw();
warn(layer.toDataURL() === dataUrls['cleared'], 'transitioning rectangle should have been destroyed and removed from the screen');
}, 1000);
}
};

View File

@ -156,5 +156,50 @@ Test.Modules.TRANSITION = {
*/
}
});
},
'stop transition': function(containerId) {
var stage = new Kinetic.Stage({
container: containerId,
width: 578,
height: 200
});
var layer = new Kinetic.Layer();
var rect = new Kinetic.Rect({
x: 100,
y: 100,
width: 100,
height: 50,
fill: 'green',
stroke: 'black',
strokeWidth: 4,
shadowColor: 'black',
shadowOffset: 10,
shadowOpacity: 0.5
});
layer.add(rect);
stage.add(layer);
var trans = rect.transitionTo({
duration: 2,
shadowOffset: {
x: 80
},
x: 400,
y: 30,
rotation: Math.PI * 2,
easing: 'bounce-ease-out'
});
setTimeout(function() {
test(rect.transAnim.isRunning(), 'rect trans should be running');
trans.stop();
test(!rect.transAnim.isRunning(), 'rect trans should not be running');
}, 1000);
setTimeout(function() {
trans.resume();
test(rect.transAnim.isRunning(), 'rect trans should be running after resume');
}, 1500);
}
};