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

View File

@@ -4,8 +4,7 @@
* animations * animations
* @constructor * @constructor
* @param {Function} func function executed on each animation frame * @param {Function} func function executed on each animation frame
* @param {Kinetic.Node} [node] node to be redrawn.&nbsp; Specifying a node will improve * @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.
* draw performance.&nbsp; This can be a shape, a group, a layer, or the stage.
*/ */
Kinetic.Animation = function(func, node) { Kinetic.Animation = function(func, node) {
this.func = func; this.func = func;
@@ -21,6 +20,20 @@
* Animation methods * Animation methods
*/ */
Kinetic.Animation.prototype = { 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 * start animation
* @name start * @name start

View File

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

View File

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

View File

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

View File

@@ -6,10 +6,11 @@
* @constructor * @constructor
*/ */
Kinetic.Transition = function(node, config) { Kinetic.Transition = function(node, config) {
var that = this, obj = {};
this.node = node; this.node = node;
this.config = config; this.config = config;
this.tweens = []; this.tweens = [];
var that = this;
// add tween for each property // add tween for each property
function addTween(c, attrs, obj, rootObj) { function addTween(c, attrs, obj, rootObj) {
@@ -26,19 +27,38 @@
} }
} }
} }
var obj = {};
addTween(config, node.attrs, obj, obj); addTween(config, node.attrs, obj, obj);
var finishedTweens = 0; // map first tween event to transition event
for(var n = 0; n < this.tweens.length; n++) { this.tweens[0].onStarted = function() {
var tween = this.tweens[n];
tween.onFinished = function() { };
finishedTweens++; this.tweens[0].onStopped = function() {
if(finishedTweens >= that.tweens.length) { node.transAnim.stop();
that.onFinished(); };
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 * Transition methods
@@ -116,34 +136,20 @@
* transition completes * transition completes
*/ */
Kinetic.Node.prototype.transitionTo = function(config) { Kinetic.Node.prototype.transitionTo = function(config) {
var that = this, trans = new Kinetic.Transition(this, config);
if(!this.transAnim) { if(!this.transAnim) {
this.transAnim = new Kinetic.Animation(); 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() { this.transAnim.func = function() {
trans._onEnterFrame(); 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 // auto start
trans.start(); trans.start();
this.transAnim.start(); this.transAnim.start();
this.trans = trans;
return 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 = { Test.Modules.DD = {
'remove shape from onclick': function(containerId) { 'remove shape with onclick': function(containerId) {
var stage = new Kinetic.Stage({ var stage = new Kinetic.Stage({
container: containerId, container: containerId,
width: 578, width: 578,
@@ -38,6 +38,8 @@ Test.Modules.DD = {
clientY: 112 + top clientY: 112 + top
}); });
warn(layer.toDataURL() === dataUrls['cleared'], 'canvas should be cleared after removing shape onclick');
}, },
'test dragstart, dragmove, dragend': function(containerId) { 'test dragstart, dragmove, dragend': function(containerId) {
var stage = new Kinetic.Stage({ var stage = new Kinetic.Stage({

View File

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

View File

@@ -1936,7 +1936,7 @@ Test.Modules.NODE = {
stage.add(layer); stage.add(layer);
test(stage.getAbsoluteZIndex() === 0, 'stage abs zindex should be 0'); 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(layer.getAbsoluteZIndex() === 1, 'layer abs zindex should be 1');
test(group1.getAbsoluteZIndex() === 2, 'group1 abs zindex should be 2'); test(group1.getAbsoluteZIndex() === 2, 'group1 abs zindex should be 2');
test(group2.getAbsoluteZIndex() === 3, 'group2 abs zindex should be 3'); test(group2.getAbsoluteZIndex() === 3, 'group2 abs zindex should be 3');
@@ -2506,5 +2506,47 @@ Test.Modules.NODE = {
test(go.names.myRect2 === undefined, 'rect still in hash'); 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[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'); 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);
} }
}; };