worked through some synchronous / asynchronous issues with toDataURL and toImage(). stage toDataURL() is now asynchronous, and all other node toDataURLs is still synchronous. toImage() is now asynchronous. Kinetic.Image once again only accepts image objects, and can no longer be instantiated with a data URL or image data due to asynchronous issues. It's much cleaner for the developer to load an image however they like, and then instantiate a Kinetic.Image shape synchronously

This commit is contained in:
Eric Rowell
2012-07-15 20:12:18 -07:00
parent acc6b6e14e
commit 384a686740
11 changed files with 233 additions and 267 deletions

139
dist/kinetic-core.js vendored
View File

@@ -277,21 +277,28 @@ Kinetic.Type = {
/*
* arg can be an image object or image data
*/
_getImage: function(arg) {
// if arg is already an image object, just return it
if(this._isElement(arg)) {
return arg;
_getImage: function(arg, callback) {
// if arg is null or undefined
if(!arg) {
callback(null);
}
// if arg is already an image object
else if(this._isElement(arg)) {
callback(arg);
}
// if arg is a string, then it's a data url
if(this._isString(arg)) {
else if(this._isString(arg)) {
var imageObj = new Image();
imageObj.onload = function() {
callback(imageObj);
}
imageObj.src = arg;
return imageObj;
}
//if arg is an object that contains the data property, it's an image object
if(arg.data) {
else if(arg.data) {
var canvas = document.createElement('canvas');
canvas.width = arg.width;
canvas.height = arg.height;
@@ -299,12 +306,15 @@ Kinetic.Type = {
context.putImageData(arg, 0, 0);
var dataUrl = canvas.toDataURL();
var imageObj = new Image();
imageObj.onload = function() {
callback(imageObj);
}
imageObj.src = dataUrl;
return imageObj;
}
// default
return null;
else {
callback(null);
}
}
};
@@ -717,10 +727,6 @@ Kinetic.Node = Kinetic.Class.extend({
that._setAttr(obj[key], 'width', size.width);
that._setAttr(obj[key], 'height', size.height);
break;
case 'image':
var img = type._getImage(val);
that._setAttr(obj, key, img);
break;
default:
that._setAttr(obj, key, val);
break;
@@ -1320,10 +1326,15 @@ Kinetic.Node = Kinetic.Class.extend({
}
},
/**
* to image
* converts node into an image. Since the toImage
* method is asynchronous, a callback is required
* @name toImage
* @methodOf Kinetic.Stage.prototype
*/
toImage: function() {
return Kinetic.Type._getImage(this.toDataURL());
toImage: function(callback) {
Kinetic.Type._getImage(this.toDataURL(), function(img) {
callback(img);
});
},
_setImageData: function(imageData) {
if(imageData && imageData.data) {
@@ -2190,25 +2201,37 @@ Kinetic.Stage = Kinetic.Container.extend({
return this.content;
},
/**
* Creates a composite data URL. If MIME type is not
* specified, then "image/png" will result. For "image/jpeg", specify a quality
* level as quality (range 0.0 - 1.0). Note that this method works
* differently from toDataURL() for other nodes because it generates an absolute dataURL
* based on what's draw onto the canvases for each layer, rather than drawing
* the current state of each node
* Creates a composite data URL and requires a callback because the stage
* toDataURL method is asynchronous. If MIME type is not
* specified, then "image/png" will result. For "image/jpeg", specify a quality
* level as quality (range 0.0 - 1.0). Note that this method works
* differently from toDataURL() for other nodes because it generates an absolute dataURL
* based on what's draw onto the canvases for each layer, rather than drawing
* the current state of each node
* @name toDataURL
* @methodOf Kinetic.Stage.prototype
* @param {Function} callback
* @param {String} [mimeType]
* @param {Number} [quality]
*/
toDataURL: function(mimeType, quality) {
var bufferLayer = this.bufferLayer;
var bufferCanvas = bufferLayer.getCanvas();
var bufferContext = bufferLayer.getContext();
var layers = this.children;
bufferLayer.clear();
toDataURL: function(callback, mimeType, quality) {
/*
* we need to create a temp layer rather than using
* the bufferLayer because the stage toDataURL method
* is asynchronous, which means that other parts of the
* code base could be updating or clearing the bufferLayer
* while the stage toDataURL method is processing
*/
var tempLayer = new Kinetic.Layer();
tempLayer.getCanvas().width = this.attrs.width;
tempLayer.getCanvas().height = this.attrs.height;
tempLayer.parent = this;
var tempCanvas = tempLayer.getCanvas();
var tempContext = tempLayer.getContext();
for(var n = 0; n < layers.length; n++) {
var layers = this.children;
function drawLayer(n) {
var layer = layers[n];
var layerUrl;
try {
@@ -2221,18 +2244,40 @@ Kinetic.Stage = Kinetic.Container.extend({
}
var imageObj = new Image();
imageObj.src = layerUrl;
bufferContext.drawImage(imageObj, 0, 0);
}
imageObj.onload = function() {
tempContext.drawImage(imageObj, 0, 0);
try {
// If this call fails (due to browser bug, like in Firefox 3.6),
// then revert to previous no-parameter image/png behavior
return bufferLayer.getCanvas().toDataURL(mimeType, quality);
}
catch(e) {
return bufferLayer.getCanvas().toDataURL();
if(n < layers.length - 1) {
drawLayer(n + 1);
}
else {
try {
// If this call fails (due to browser bug, like in Firefox 3.6),
// then revert to previous no-parameter image/png behavior
callback(tempLayer.getCanvas().toDataURL(mimeType, quality));
}
catch(e) {
callback(tempLayer.getCanvas().toDataURL());
}
}
};
imageObj.src = layerUrl;
}
drawLayer(0);
},
/**
* converts stage into an image. Since the stage toImage() method
* is asynchronous, a callback function is required
* @name toImage
* @methodOf Kinetic.Stage.prototype
* @param {Function} callback
*/
toImage: function(callback) {
this.toDataURL(function(dataUrl) {
Kinetic.Type._getImage(dataUrl, function(img) {
callback(img);
});
});
},
_resizeDOM: function() {
var width = this.attrs.width;
@@ -3864,20 +3909,6 @@ Kinetic.Image = Kinetic.Shape.extend({
};
// call super constructor
this._super(config);
/*
* if image property is ever changed, check and see
* if it was set to image data, and if it was, go ahead
* and save it
*/
this.on('beforeImageChange.kinetic', function(evt) {
this._setImageData(evt.newVal);
});
/*
* if image property was set with image data,
* go ahead and save it
*/
this._setImageData(config.image);
},
/**
* set width and height

File diff suppressed because one or more lines are too long

View File

@@ -251,10 +251,6 @@ Kinetic.Node = Kinetic.Class.extend({
that._setAttr(obj[key], 'width', size.width);
that._setAttr(obj[key], 'height', size.height);
break;
case 'image':
var img = type._getImage(val);
that._setAttr(obj, key, img);
break;
default:
that._setAttr(obj, key, val);
break;
@@ -854,10 +850,15 @@ Kinetic.Node = Kinetic.Class.extend({
}
},
/**
* to image
* converts node into an image. Since the toImage
* method is asynchronous, a callback is required
* @name toImage
* @methodOf Kinetic.Stage.prototype
*/
toImage: function() {
return Kinetic.Type._getImage(this.toDataURL());
toImage: function(callback) {
Kinetic.Type._getImage(this.toDataURL(), function(img) {
callback(img);
});
},
_setImageData: function(imageData) {
if(imageData && imageData.data) {

View File

@@ -274,25 +274,37 @@ Kinetic.Stage = Kinetic.Container.extend({
return this.content;
},
/**
* Creates a composite data URL. If MIME type is not
* specified, then "image/png" will result. For "image/jpeg", specify a quality
* level as quality (range 0.0 - 1.0). Note that this method works
* differently from toDataURL() for other nodes because it generates an absolute dataURL
* based on what's draw onto the canvases for each layer, rather than drawing
* the current state of each node
* Creates a composite data URL and requires a callback because the stage
* toDataURL method is asynchronous. If MIME type is not
* specified, then "image/png" will result. For "image/jpeg", specify a quality
* level as quality (range 0.0 - 1.0). Note that this method works
* differently from toDataURL() for other nodes because it generates an absolute dataURL
* based on what's draw onto the canvases for each layer, rather than drawing
* the current state of each node
* @name toDataURL
* @methodOf Kinetic.Stage.prototype
* @param {Function} callback
* @param {String} [mimeType]
* @param {Number} [quality]
*/
toDataURL: function(mimeType, quality) {
var bufferLayer = this.bufferLayer;
var bufferCanvas = bufferLayer.getCanvas();
var bufferContext = bufferLayer.getContext();
var layers = this.children;
bufferLayer.clear();
toDataURL: function(callback, mimeType, quality) {
/*
* we need to create a temp layer rather than using
* the bufferLayer because the stage toDataURL method
* is asynchronous, which means that other parts of the
* code base could be updating or clearing the bufferLayer
* while the stage toDataURL method is processing
*/
var tempLayer = new Kinetic.Layer();
tempLayer.getCanvas().width = this.attrs.width;
tempLayer.getCanvas().height = this.attrs.height;
tempLayer.parent = this;
var tempCanvas = tempLayer.getCanvas();
var tempContext = tempLayer.getContext();
for(var n = 0; n < layers.length; n++) {
var layers = this.children;
function drawLayer(n) {
var layer = layers[n];
var layerUrl;
try {
@@ -305,18 +317,40 @@ Kinetic.Stage = Kinetic.Container.extend({
}
var imageObj = new Image();
imageObj.src = layerUrl;
bufferContext.drawImage(imageObj, 0, 0);
}
imageObj.onload = function() {
tempContext.drawImage(imageObj, 0, 0);
try {
// If this call fails (due to browser bug, like in Firefox 3.6),
// then revert to previous no-parameter image/png behavior
return bufferLayer.getCanvas().toDataURL(mimeType, quality);
}
catch(e) {
return bufferLayer.getCanvas().toDataURL();
if(n < layers.length - 1) {
drawLayer(n + 1);
}
else {
try {
// If this call fails (due to browser bug, like in Firefox 3.6),
// then revert to previous no-parameter image/png behavior
callback(tempLayer.getCanvas().toDataURL(mimeType, quality));
}
catch(e) {
callback(tempLayer.getCanvas().toDataURL());
}
}
};
imageObj.src = layerUrl;
}
drawLayer(0);
},
/**
* converts stage into an image. Since the stage toImage() method
* is asynchronous, a callback function is required
* @name toImage
* @methodOf Kinetic.Stage.prototype
* @param {Function} callback
*/
toImage: function(callback) {
this.toDataURL(function(dataUrl) {
Kinetic.Type._getImage(dataUrl, function(img) {
callback(img);
});
});
},
_resizeDOM: function() {
var width = this.attrs.width;

View File

@@ -43,20 +43,6 @@ Kinetic.Image = Kinetic.Shape.extend({
};
// call super constructor
this._super(config);
/*
* if image property is ever changed, check and see
* if it was set to image data, and if it was, go ahead
* and save it
*/
this.on('beforeImageChange.kinetic', function(evt) {
this._setImageData(evt.newVal);
});
/*
* if image property was set with image data,
* go ahead and save it
*/
this._setImageData(config.image);
},
/**
* set width and height

View File

@@ -212,21 +212,28 @@ Kinetic.Type = {
/*
* arg can be an image object or image data
*/
_getImage: function(arg) {
// if arg is already an image object, just return it
if(this._isElement(arg)) {
return arg;
_getImage: function(arg, callback) {
// if arg is null or undefined
if(!arg) {
callback(null);
}
// if arg is already an image object
else if(this._isElement(arg)) {
callback(arg);
}
// if arg is a string, then it's a data url
if(this._isString(arg)) {
else if(this._isString(arg)) {
var imageObj = new Image();
imageObj.onload = function() {
callback(imageObj);
}
imageObj.src = arg;
return imageObj;
}
//if arg is an object that contains the data property, it's an image object
if(arg.data) {
else if(arg.data) {
var canvas = document.createElement('canvas');
canvas.width = arg.width;
canvas.height = arg.height;
@@ -234,11 +241,14 @@ Kinetic.Type = {
context.putImageData(arg, 0, 0);
var dataUrl = canvas.toDataURL();
var imageObj = new Image();
imageObj.onload = function() {
callback(imageObj);
}
imageObj.src = dataUrl;
return imageObj;
}
// default
return null;
else {
callback(null);
}
}
};

File diff suppressed because one or more lines are too long

View File

@@ -38,7 +38,7 @@ Test.prototype.tests = {
circle.on('dragend', function() {
dragEnd = true;
});
startDataUrl = stage.toDataURL();
startDataUrl = layer.toDataURL();
warn(urls[0] === startDataUrl, 'start data url is incorrect');
/*
* simulate drag and drop
@@ -70,7 +70,7 @@ Test.prototype.tests = {
test(dragMove, 'dragmove event was not triggered');
test(dragEnd, 'dragend event was not triggered');
var endDataUrl = stage.toDataURL();
var endDataUrl = layer.toDataURL();
warn(urls[1] === endDataUrl, 'end data url is incorrect');
},
'DRAG AND DROP - cancel drag and drop by setting draggable to false': function(containerId) {
@@ -176,7 +176,7 @@ Test.prototype.tests = {
stage.add(layer);
var startDataUrl = stage.toDataURL();
var startDataUrl = layer.toDataURL();
warn(urls[0] === startDataUrl, 'start data url is incorrect');
/*
@@ -197,7 +197,7 @@ Test.prototype.tests = {
clientY: 109
});
var endDataUrl = stage.toDataURL()
var endDataUrl = layer.toDataURL()
warn(urls[1] === endDataUrl, 'end data url is incorrect');
},
@@ -240,14 +240,15 @@ Test.prototype.tests = {
layer.add(circle);
stage.add(layer);
warn(stage.toDataURL() === urls[0], 'start data url is incorrect');
warn(layer.toDataURL() === urls[0], 'start data url is incorrect');
stage._mousemove({
clientX: 377,
clientY: 101
});
warn(stage.toDataURL() === urls[1], 'mid data url is incorrect');
console.log(layer.toDataURL())
warn(layer.toDataURL() === urls[1], 'mid data url is incorrect');
// move mouse back out of circle
stage._mousemove({
@@ -259,7 +260,7 @@ Test.prototype.tests = {
clientY: 138
});
warn(stage.toDataURL() === urls[0], 'end data url is incorrect');
warn(layer.toDataURL() === urls[0], 'end data url is incorrect');
},
'EVENTS - path detection mousedown mouseup mouseover mouseout mousemove click dblclick / touchstart touchend touchmove tap dbltap': function(containerId) {
var stage = new Kinetic.Stage({

View File

@@ -1468,7 +1468,7 @@ Test.prototype.tests = {
});
Ellipse.on('click', function() {
window.open(stage.toDataURL());
window.open(layer.toDataURL());
});
layer.add(Ellipse);
@@ -1492,7 +1492,7 @@ Test.prototype.tests = {
});
Ellipse.on('click', function() {
window.open(stage.toDataURL('image/jpeg', 0));
window.open(layer.toDataURL('image/jpeg', 0));
});
layer.add(Ellipse);
@@ -1516,7 +1516,7 @@ Test.prototype.tests = {
});
Ellipse.on('click', function() {
window.open(stage.toDataURL('image/jpeg', 1));
window.open(layer.toDataURL('image/jpeg', 1));
});
layer.add(Ellipse);

View File

@@ -1,5 +1,5 @@
Test.prototype.tests = {
'PERFORMANCE - draw rect vs image from image data': function(containerId) {
'DRAWING - draw rect vs image from image data': function(containerId) {
var stage = new Kinetic.Stage({
container: containerId,
width: 578,
@@ -34,7 +34,7 @@ Test.prototype.tests = {
endTimer('draw 10,000 images with image object from image data');
},
'PERFORMANCE - draw rect vs image from data url': function(containerId) {
'DRAWING - draw rect vs image from data url': function(containerId) {
var stage = new Kinetic.Stage({
container: containerId,
width: 578,
@@ -57,7 +57,7 @@ Test.prototype.tests = {
endTimer('draw 10,000 rects with canvas API');
startTimer();
var url = canvas.toDataURL();
var url = layer.toDataURL();
endTimer('create data url');
var imageObj = new Image();
@@ -71,7 +71,7 @@ Test.prototype.tests = {
}
endTimer('draw 10,000 images with image object from data url');
},
'*ANIMATION - draw 1,000 stars': function(containerId) {
'DRAWING - draw 1,000 stars': function(containerId) {
var stage = new Kinetic.Stage({
container: containerId,
width: 578,
@@ -99,7 +99,7 @@ Test.prototype.tests = {
endTimer('draw 1,000 stars');
},
'*ANIMATION - draw 1,000 cached stars': function(containerId) {
'DRAWING - draw 1,000 cached stars': function(containerId) {
var stage = new Kinetic.Stage({
container: containerId,
width: 578,
@@ -107,8 +107,6 @@ Test.prototype.tests = {
});
var layer = new Kinetic.Layer();
startTimer();
var star = new Kinetic.Star({
innerRadius: 20,
outerRadius: 50,
@@ -123,21 +121,23 @@ Test.prototype.tests = {
layer.add(star);
stage.add(layer);
var img = star.toImage();
console.log('call toImage')
star.toImage(function(img) {
startTimer();
for(var n = 0; n < 1000; n++) {
var image = new Kinetic.Image({
image: img,
x: Math.random() * stage.getWidth(),
y: Math.random() * stage.getHeight(),
offset: 70
});
for(var n = 0; n < 1000; n++) {
var image = new Kinetic.Image({
image: img,
x: Math.random() * stage.getWidth(),
y: Math.random() * stage.getHeight(),
offset: 70
});
layer.add(image);
}
layer.add(image);
}
layer.draw();
layer.draw();
endTimer('draw 1,000 stars');
endTimer('draw 1,000 cached stars');
});
}
};

View File

@@ -362,7 +362,7 @@ Test.prototype.tests = {
layer.add(group);
stage.add(layer);
var startDataUrl = stage.toDataURL();
var startDataUrl = layer.toDataURL();
warn(startDataUrl === urls[0], 'start data url is incorrect');
test(triangle.getId() === 'myTriangle', 'triangle id should be myTriangle');
@@ -377,7 +377,7 @@ Test.prototype.tests = {
*/
layer.draw();
var endDataUrl = stage.toDataURL();
var endDataUrl = layer.toDataURL();
warn(endDataUrl === urls[0], 'end data url is incorrect');
},
@@ -1042,7 +1042,9 @@ Test.prototype.tests = {
layer.draw();
}
warn(urls[0] === stage.toDataURL(), 'stage data url is incorrect');
stage.toDataURL(function(dataUrl) {
warn(urls[0] === dataUrl, 'stage data url is incorrect');
})
warn(urls[0] === layer.toDataURL(), 'layer data url is incorrect');
warn(urls[1] === group.toDataURL(), 'group data url is incorrect');
warn(urls[1] === circle.toDataURL(), 'shape data url is incorrect');
@@ -1989,96 +1991,6 @@ Test.prototype.tests = {
};
imageObj.src = '../darth-vader.jpg';
},
'SHAPE - add image with image data': function(containerId) {
var stage = new Kinetic.Stage({
container: containerId,
width: 578,
height: 200
});
var layer = new Kinetic.Layer();
var group = new Kinetic.Group();
var circle = new Kinetic.Ellipse({
x: 100,
y: stage.getHeight() / 2,
radius: 70,
fill: 'green',
stroke: 'black',
strokeWidth: 4
});
var circle2 = new Kinetic.Ellipse({
x: 150,
y: stage.getHeight() / 2,
radius: 70,
fill: 'yellow',
stroke: 'black',
strokeWidth: 4
});
group.add(circle);
group.add(circle2);
layer.add(group);
stage.add(layer);
group.saveImageData();
var image = new Kinetic.Image({
image: group.getImageData(),
x: 200,
y: 0,
draggable: true
});
layer.add(image);
layer.draw();
test(Kinetic.Type._isElement(image.getImage()), 'image property should have been converted to an image element');
},
'SHAPE - add image with data url': function(containerId) {
var stage = new Kinetic.Stage({
container: containerId,
width: 578,
height: 200
});
var layer = new Kinetic.Layer();
var group = new Kinetic.Group();
var circle = new Kinetic.Ellipse({
x: 100,
y: stage.getHeight() / 2,
radius: 70,
fill: 'green',
stroke: 'black',
strokeWidth: 4
});
var circle2 = new Kinetic.Ellipse({
x: 150,
y: stage.getHeight() / 2,
radius: 70,
fill: 'yellow',
stroke: 'black',
strokeWidth: 4
});
group.add(circle);
group.add(circle2);
layer.add(group);
stage.add(layer);
var image = new Kinetic.Image({
image: layer.toDataURL(),
x: 200,
y: 0,
draggable: true
});
layer.add(image);
layer.draw();
test(Kinetic.Type._isElement(image.getImage()), 'image property should have been converted to an image element');
},
'SHAPE - set image fill to color then image': function(containerId) {
var imageObj = new Image();
imageObj.onload = function() {
@@ -2272,10 +2184,18 @@ Test.prototype.tests = {
layer.add(group);
stage.add(layer);
test(Kinetic.Type._isElement(poly.toImage()), 'shape toImage() should be an image object');
test(Kinetic.Type._isElement(group.toImage()), 'group toImage() should be an image object');
test(Kinetic.Type._isElement(layer.toImage()), 'layer toImage() should be an image object');
test(Kinetic.Type._isElement(stage.toImage()), 'stage toImage() should be an image object');
poly.toImage(function(imageObj) {
test(Kinetic.Type._isElement(imageObj), 'shape toImage() should be an image object');
});
group.toImage(function(imageObj) {
test(Kinetic.Type._isElement(imageObj), 'group toImage() should be an image object');
});
layer.toImage(function(imageObj) {
test(Kinetic.Type._isElement(imageObj), 'layer toImage() should be an image object');
});
stage.toImage(function(imageObj) {
test(Kinetic.Type._isElement(imageObj), 'stage toImage() should be an image object');
});
},
'SHAPE - add polygon': function(containerId) {
var stage = new Kinetic.Stage({