konva/src/Stage.js

603 lines
20 KiB
JavaScript
Raw Normal View History

(function() {
// CONSTANTS
2013-03-24 14:50:51 +08:00
var STAGE = 'Stage',
STRING = 'string',
PX = 'px',
MOUSEOUT = 'mouseout',
MOUSELEAVE = 'mouseleave',
MOUSEOUT = 'mouseout',
MOUSEOVER = 'mouseover',
MOUSEENTER = 'mouseenter',
MOUSEMOVE = 'mousemove',
MOUSEDOWN = 'mousedown',
MOUSEUP = 'mouseup',
CLICK = 'click',
DBL_CLICK = 'dblclick',
TOUCHSTART = 'touchstart'
TOUCHEND = 'touchend'
TAP = 'tap',
DBL_TAP = 'dbltap',
TOUCHMOVE = 'touchmove',
DIV = 'div',
RELATIVE = 'relative',
INLINE_BLOCK = 'inline-block',
KINETICJS_CONTENT = 'kineticjs-content',
SPACE = ' ',
CONTAINER = 'container',
EVENTS = [MOUSEDOWN, MOUSEMOVE, MOUSEUP, MOUSEOUT, TOUCHSTART, TOUCHMOVE, TOUCHEND],
// cached variables
eventsLength = EVENTS.length;
2013-04-08 16:02:08 +08:00
function addEvent(ctx, eventName) {
ctx.content.addEventListener(eventName, function(evt) {
ctx['_' + eventName](evt);
}, false);
}
Kinetic.Util.addMethods(Kinetic.Stage, {
_initStage: function(config) {
this.createAttrs();
// call super constructor
Kinetic.Container.call(this, config);
2013-03-24 14:50:51 +08:00
this.nodeType = STAGE;
this.dblClickWindow = 400;
this._id = Kinetic.Global.idCounter++;
this._buildDOM();
this._bindContentEvents();
Kinetic.Global.stages.push(this);
},
2012-12-17 12:52:07 +08:00
/**
* set container dom element which contains the stage wrapper div element
* @name setContainer
* @methodOf Kinetic.Stage.prototype
* @param {DomElement} container can pass in a dom element or id string
*/
setContainer: function(container) {
2013-03-24 14:50:51 +08:00
if( typeof container === STRING) {
container = document.getElementById(container);
}
this._setAttr(CONTAINER, container);
},
draw: function() {
// clear children layers
var children = this.getChildren(),
len = children.length,
n, layer;
for(n = 0; n < len; n++) {
layer = children[n];
if (layer.getClearBeforeDraw()) {
layer.getCanvas().clear();
layer.getHitCanvas().clear();
}
}
Kinetic.Node.prototype.draw.call(this);
},
/**
2012-12-17 12:52:07 +08:00
* draw layer scene graphs
* @name draw
* @methodOf Kinetic.Stage.prototype
*/
/**
2012-12-17 12:52:07 +08:00
* draw layer hit graphs
* @name drawHit
* @methodOf Kinetic.Stage.prototype
*/
/**
* set height
* @name setHeight
* @methodOf Kinetic.Stage.prototype
* @param {Number} height
*/
setHeight: function(height) {
Kinetic.Node.prototype.setHeight.call(this, height);
this._resizeDOM();
},
/**
* set width
* @name setWidth
* @methodOf Kinetic.Stage.prototype
* @param {Number} width
*/
setWidth: function(width) {
Kinetic.Node.prototype.setWidth.call(this, width);
this._resizeDOM();
},
/**
* clear all layers
* @name clear
* @methodOf Kinetic.Stage.prototype
*/
clear: function() {
2013-03-24 14:50:51 +08:00
var layers = this.children,
2013-04-04 03:43:32 +08:00
len = layers.length,
2013-03-24 14:50:51 +08:00
n;
for(n = 0; n < len; n++) {
layers[n].clear();
}
},
/**
* remove stage
*/
remove: function() {
var content = this.content;
Kinetic.Node.prototype.remove.call(this);
if(content && Kinetic.Util._isInDocument(content)) {
2013-03-24 14:50:51 +08:00
this.getContainer().removeChild(content);
}
},
/**
* get mouse position for desktop apps
* @name getMousePosition
* @methodOf Kinetic.Stage.prototype
*/
2012-12-17 12:52:07 +08:00
getMousePosition: function() {
return this.mousePos;
},
/**
* get touch position for mobile apps
* @name getTouchPosition
* @methodOf Kinetic.Stage.prototype
*/
2012-12-17 12:52:07 +08:00
getTouchPosition: function() {
return this.touchPos;
},
/**
* get pointer position which can be a touc position or mouse position
* @name getPointerPosition
* @methodOf Kinetic.Stage.prototype
*/
getPointerPosition: function() {
return this.getTouchPosition() || this.getMousePosition();
},
getStage: function() {
return this;
},
/**
* get stage content div element which has the
* the class name "kineticjs-content"
* @name getContent
* @methodOf Kinetic.Stage.prototype
*/
getContent: function() {
return this.content;
},
/**
2012-12-17 12:52:07 +08:00
* Creates a composite data URL and requires a callback because the composite is generated asynchronously.
* @name toDataURL
* @methodOf Kinetic.Stage.prototype
* @param {Object} config
2012-12-17 12:52:07 +08:00
* @param {Function} config.callback function executed when the composite has completed
* @param {String} [config.mimeType] can be "image/png" or "image/jpeg".
* "image/png" is the default
2012-12-17 12:52:07 +08:00
* @param {Number} [config.x] x position of canvas section
* @param {Number} [config.y] y position of canvas section
* @param {Number} [config.width] width of canvas section
* @param {Number} [config.height] height of canvas section
* @param {Number} [config.quality] jpeg quality. If using an "image/jpeg" mimeType,
* you can specify the quality from 0 to 1, where 0 is very poor quality and 1
* is very high quality
*/
toDataURL: function(config) {
2013-03-24 14:50:51 +08:00
var config = config || {},
mimeType = config.mimeType || null,
quality = config.quality || null,
x = config.x || 0,
y = config.y || 0,
canvas = new Kinetic.SceneCanvas({
width: config.width || this.getWidth(),
height: config.height || this.getHeight(),
pixelRatio: 1
}),
context = canvas.getContext(),
layers = this.children;
if(x || y) {
context.translate(-1 * x, -1 * y);
}
function drawLayer(n) {
2013-03-24 14:50:51 +08:00
var layer = layers[n],
layerUrl = layer.toDataURL(),
imageObj = new Image();
imageObj.onload = function() {
context.drawImage(imageObj, 0, 0);
if(n < layers.length - 1) {
drawLayer(n + 1);
}
else {
config.callback(canvas.toDataURL(mimeType, quality));
}
};
imageObj.src = layerUrl;
}
drawLayer(0);
},
/**
* converts stage into an image.
* @name toImage
* @methodOf Kinetic.Stage.prototype
* @param {Object} config
2012-12-17 12:52:07 +08:00
* @param {Function} config.callback function executed when the composite has completed
* @param {String} [config.mimeType] can be "image/png" or "image/jpeg".
* "image/png" is the default
2012-12-17 12:52:07 +08:00
* @param {Number} [config.x] x position of canvas section
* @param {Number} [config.y] y position of canvas section
* @param {Number} [config.width] width of canvas section
* @param {Number} [config.height] height of canvas section
* @param {Number} [config.quality] jpeg quality. If using an "image/jpeg" mimeType,
* you can specify the quality from 0 to 1, where 0 is very poor quality and 1
* is very high quality
*/
toImage: function(config) {
var cb = config.callback;
config.callback = function(dataUrl) {
Kinetic.Util._getImage(dataUrl, function(img) {
cb(img);
});
};
this.toDataURL(config);
},
/**
* get intersection object that contains shape and pixel data
* @name getIntersection
* @methodOf Kinetic.Stage.prototype
* @param {Object} pos point object
*/
getIntersection: function() {
var pos = Kinetic.Util._getXY(Array.prototype.slice.call(arguments)),
layers = this.getChildren(),
2013-03-24 14:50:51 +08:00
len = layers.length,
end = len - 1,
n, obj;
2013-03-24 14:50:51 +08:00
for(n = end; n >= 0; n--) {
obj = layers[n].getIntersection(pos);
if (obj) {
return obj;
}
}
return null;
},
_resizeDOM: function() {
if(this.content) {
2013-03-24 14:50:51 +08:00
var width = this.getWidth(),
height = this.getHeight(),
layers = this.getChildren(),
len = layers.length,
n;
// set content dimensions
2013-03-24 14:50:51 +08:00
this.content.style.width = width + PX;
this.content.style.height = height + PX;
this.bufferCanvas.setSize(width, height, 1);
this.hitCanvas.setSize(width, height);
2013-03-24 14:50:51 +08:00
// set pointer defined layer dimensions
2013-03-24 14:50:51 +08:00
for(n = 0; n < len; n++) {
layer = layers[n];
layer.getCanvas().setSize(width, height);
layer.hitCanvas.setSize(width, height);
layer.draw();
}
}
},
/**
* add layer to stage
2012-12-17 12:52:07 +08:00
* @param {Kinetic.Layer} layer
*/
add: function(layer) {
Kinetic.Container.prototype.add.call(this, layer);
layer.canvas.setSize(this.attrs.width, this.attrs.height);
layer.hitCanvas.setSize(this.attrs.width, this.attrs.height);
// draw layer and append canvas to container
layer.draw();
this.content.appendChild(layer.canvas.element);
// chainable
return this;
},
getParent: function() {
return null;
},
getLayer: function() {
return null;
},
/**
* get layers
* @name getLayers
* @methodOf Kinetic.Stage.prototype
*/
getLayers: function() {
return this.getChildren();
},
_setPointerPosition: function(evt) {
if(!evt) {
evt = window.event;
}
this._setMousePosition(evt);
this._setTouchPosition(evt);
},
/**
* begin listening for events by adding event handlers
* to the container
*/
_bindContentEvents: function() {
var that = this,
2013-04-08 16:02:08 +08:00
n;
2013-03-24 14:50:51 +08:00
for (n = 0; n < eventsLength; n++) {
2013-04-08 16:02:08 +08:00
addEvent(this, EVENTS[n]);
}
},
_mouseout: function(evt) {
this._setPointerPosition(evt);
2013-03-24 14:50:51 +08:00
var go = Kinetic.Global,
targetShape = this.targetShape;
if(targetShape && !go.isDragging()) {
targetShape._fireAndBubble(MOUSEOUT, evt);
targetShape._fireAndBubble(MOUSELEAVE, evt);
this.targetShape = null;
}
this.mousePos = undefined;
},
_mousemove: function(evt) {
this._setPointerPosition(evt);
var go = Kinetic.Global,
dd = Kinetic.DD,
2013-03-24 14:50:51 +08:00
obj = this.getIntersection(this.getPointerPosition()),
shape;
if(obj) {
2013-03-24 14:50:51 +08:00
shape = obj.shape;
if(shape) {
if(!go.isDragging() && obj.pixel[3] === 255 && (!this.targetShape || this.targetShape._id !== shape._id)) {
if(this.targetShape) {
this.targetShape._fireAndBubble(MOUSEOUT, evt, shape);
this.targetShape._fireAndBubble(MOUSELEAVE, evt, shape);
}
shape._fireAndBubble(MOUSEOVER, evt, this.targetShape);
shape._fireAndBubble(MOUSEENTER, evt, this.targetShape);
this.targetShape = shape;
}
else {
shape._fireAndBubble(MOUSEMOVE, evt);
}
}
}
/*
* if no shape was detected, clear target shape and try
* to run mouseout from previous target shape
*/
else if(this.targetShape && !go.isDragging()) {
this.targetShape._fireAndBubble(MOUSEOUT, evt);
this.targetShape._fireAndBubble(MOUSELEAVE, evt);
this.targetShape = null;
}
if(dd) {
dd._drag(evt);
}
},
_mousedown: function(evt) {
this._setPointerPosition(evt);
2013-04-03 13:29:56 +08:00
var go = Kinetic.Global,
obj = this.getIntersection(this.getPointerPosition()),
shape;
2013-03-24 14:50:51 +08:00
if(obj && obj.shape) {
2013-03-24 14:50:51 +08:00
shape = obj.shape;
this.clickStart = true;
this.clickStartShape = shape;
shape._fireAndBubble(MOUSEDOWN, evt);
}
//init stage drag and drop
2013-04-03 13:29:56 +08:00
if(this.isDraggable() && !go.isDragReady()) {
this.startDrag(evt);
}
},
_mouseup: function(evt) {
this._setPointerPosition(evt);
var that = this,
go = Kinetic.Global,
2013-03-24 14:50:51 +08:00
obj = this.getIntersection(this.getPointerPosition()),
shape;
if(obj && obj.shape) {
2013-03-24 14:50:51 +08:00
shape = obj.shape;
shape._fireAndBubble(MOUSEUP, evt);
// detect if click or double click occurred
if(this.clickStart) {
/*
* if dragging and dropping, or if click doesn't map to
* the correct shape, don't fire click or dbl click event
*/
if(!go.isDragging() && shape._id === this.clickStartShape._id) {
shape._fireAndBubble(CLICK, evt);
if(this.inDoubleClickWindow) {
shape._fireAndBubble(DBL_CLICK, evt);
}
this.inDoubleClickWindow = true;
setTimeout(function() {
that.inDoubleClickWindow = false;
}, this.dblClickWindow);
}
}
}
this.clickStart = false;
},
_touchstart: function(evt) {
2013-03-24 14:50:51 +08:00
this._setPointerPosition(evt);
var go = Kinetic.Global,
2013-03-24 14:50:51 +08:00
obj = this.getIntersection(this.getPointerPosition()),
shape;
evt.preventDefault();
if(obj && obj.shape) {
2013-03-24 14:50:51 +08:00
shape = obj.shape;
this.tapStart = true;
2013-03-24 14:50:51 +08:00
this.tapStartShape = shape;
shape._fireAndBubble(TOUCHSTART, evt);
}
//init stage drag and drop
if(this.isDraggable() && !go.isDragReady()) {
this.startDrag(evt);
}
},
_touchend: function(evt) {
this._setPointerPosition(evt);
2013-03-24 14:50:51 +08:00
var that = this,
go = Kinetic.Global,
obj = this.getIntersection(this.getPointerPosition()),
shape;
if(obj && obj.shape) {
2013-03-24 14:50:51 +08:00
shape = obj.shape;
shape._fireAndBubble(TOUCHEND, evt);
// detect if tap or double tap occurred
if(this.tapStart) {
/*
* if dragging and dropping, don't fire tap or dbltap
* event
*/
2013-03-24 14:50:51 +08:00
if(!go.isDragging() && shape._id === this.tapStartShape._id) {
shape._fireAndBubble(TAP, evt);
if(this.inDoubleClickWindow) {
shape._fireAndBubble(DBL_TAP, evt);
}
this.inDoubleClickWindow = true;
setTimeout(function() {
that.inDoubleClickWindow = false;
}, this.dblClickWindow);
}
}
}
this.tapStart = false;
},
_touchmove: function(evt) {
this._setPointerPosition(evt);
2013-03-24 14:50:51 +08:00
var dd = Kinetic.DD,
obj = this.getIntersection(this.getPointerPosition()),
shape;
evt.preventDefault();
2013-03-24 14:50:51 +08:00
if(obj && obj.shape) {
2013-03-24 14:50:51 +08:00
shape = obj.shape;
shape._fireAndBubble(TOUCHMOVE, evt);
}
// start drag and drop
if(dd) {
dd._drag(evt);
}
},
/**
* set mouse positon for desktop apps
* @param {Event} evt
*/
_setMousePosition: function(evt) {
2013-03-24 14:50:51 +08:00
var mouseX = evt.clientX - this._getContentPosition().left,
mouseY = evt.clientY - this._getContentPosition().top;
this.mousePos = {
x: mouseX,
y: mouseY
};
},
/**
* set touch position for mobile apps
* @param {Event} evt
*/
_setTouchPosition: function(evt) {
2013-03-24 14:50:51 +08:00
var touch, touchX, touchY;
if(evt.touches !== undefined && evt.touches.length === 1) {
// one finger
2013-03-24 14:50:51 +08:00
touch = evt.touches[0];
// get the information for finger #1
touchX = touch.clientX - this._getContentPosition().left;
touchY = touch.clientY - this._getContentPosition().top;
this.touchPos = {
x: touchX,
y: touchY
};
}
},
/**
* get container position
*/
_getContentPosition: function() {
var rect = this.content.getBoundingClientRect();
return {
top: rect.top,
left: rect.left
};
},
/**
* build dom
*/
_buildDOM: function() {
// content
2013-03-24 14:50:51 +08:00
this.content = document.createElement(DIV);
this.content.style.position = RELATIVE;
this.content.style.display = INLINE_BLOCK;
this.content.className = KINETICJS_CONTENT;
this.attrs.container.appendChild(this.content);
this.bufferCanvas = new Kinetic.SceneCanvas();
this.hitCanvas = new Kinetic.HitCanvas();
this._resizeDOM();
},
/**
* bind event listener to container DOM element
* @param {String} typesStr
* @param {function} handler
*/
_onContent: function(typesStr, handler) {
2013-03-24 14:50:51 +08:00
var types = typesStr.split(SPACE),
len = types.length,
n, baseEvent;
for(n = 0; n < len; n++) {
baseEvent = types[n];
this.content.addEventListener(baseEvent, handler, false);
}
}
});
Kinetic.Util.extend(Kinetic.Stage, Kinetic.Container);
// add getters and setters
Kinetic.Node.addGetter(Kinetic.Stage, 'container');
/**
* get container DOM element
* @name getContainer
* @methodOf Kinetic.Stage.prototype
*/
})();