konva/src/Shape.js

489 lines
15 KiB
JavaScript

///////////////////////////////////////////////////////////////////////
// Shape
///////////////////////////////////////////////////////////////////////
/**
* Shape constructor. Shapes are primitive objects such as rectangles,
* circles, text, lines, etc.
* @constructor
* @augments Kinetic.Node
* @param {Object} config
* @config {String|Object} [config.fill] can be a string color, a linear gradient object, a radial
* gradient object, or a pattern object.
* @config {Image} [config.fill.image] image object if filling the shape with a pattern
* @config {Object} [config.fill.offset] pattern offset if filling the shape with a pattern
* @config {Number} [config.fill.offset.x]
* @config {Number} [config.fill.offset.y]
* @config {Object} [config.fill.start] start point if using a linear gradient or
* radial gradient fill
* @config {Number} [config.fill.start.x]
* @config {Number} [config.fill.start.y]
* @config {Number} [config.fill.start.radius] start radius if using a radial gradient fill
* @config {Object} [config.fill.end] end point if using a linear gradient or
* radial gradient fill
* @config {Number} [config.fill.end.x]
* @config {Number} [config.fill.end.y]
* @config {Number} [config.fill.end.radius] end radius if using a radial gradient fill
* @config {String} [config.stroke] stroke color
* @config {Number} [config.strokeWidth] stroke width
* @config {String} [config.lineJoin] line join can be miter, round, or bevel. The default
* is miter
* @config {Object} [config.shadow] shadow object
* @config {String} [config.shadow.color]
* @config {Number} [config.shadow.blur]
* @config {Obect} [config.shadow.blur.offset]
* @config {Number} [config.shadow.blur.offset.x]
* @config {Number} [config.shadow.blur.offset.y]
* @config {Number} [config.shadow.opacity] shadow opacity. Can be any real number
* between 0 and 1
* @param {Number} [config.x]
* @param {Number} [config.y]
* @param {Boolean} [config.visible]
* @param {Boolean} [config.listening] whether or not the node is listening for events
* @param {String} [config.id] unique id
* @param {String} [config.name] non-unique name
* @param {Number} [config.opacity] determines node opacity. Can be any number between 0 and 1
* @param {Object} [config.scale]
* @param {Number} [config.scale.x]
* @param {Number} [config.scale.y]
* @param {Number} [config.rotation] rotation in radians
* @param {Number} [config.rotationDeg] rotation in degrees
* @param {Object} [config.offset] offsets default position point and rotation point
* @param {Number} [config.offset.x]
* @param {Number} [config.offset.y]
* @param {Boolean} [config.draggable]
* @param {String} [config.dragConstraint] can be vertical, horizontal, or none. The default
* is none
* @param {Object} [config.dragBounds]
* @param {Number} [config.dragBounds.top]
* @param {Number} [config.dragBounds.right]
* @param {Number} [config.dragBounds.bottom]
* @param {Number} [config.dragBounds.left]
*/
Kinetic.Shape = Kinetic.Node.extend({
init: function(config) {
this.nodeType = 'Shape';
this.appliedShadow = false;
// call super constructor
this._super(config);
},
/**
* get canvas context tied to the layer
* @name getContext
* @methodOf Kinetic.Shape.prototype
*/
getContext: function() {
return this.getLayer().getContext();
},
/**
* get canvas tied to the layer
* @name getCanvas
* @methodOf Kinetic.Shape.prototype
*/
getCanvas: function() {
return this.getLayer().getCanvas();
},
/**
* helper method to stroke the shape and apply
* shadows if needed
* @name stroke
* @methodOf Kinetic.Shape.prototype
*/
stroke: function(context) {
var strokeWidth = this.getStrokeWidth();
var stroke = this.getStroke();
if(stroke || strokeWidth) {
var go = Kinetic.Global;
var appliedShadow = false;
context.save();
if(this.attrs.shadow && !this.appliedShadow) {
appliedShadow = this._applyShadow(context);
}
context.lineWidth = strokeWidth || 2;
context.strokeStyle = stroke || 'black';
context.stroke(context);
context.restore();
if(appliedShadow) {
this.stroke(context);
}
}
},
/**
* helper method to fill the shape with a color, linear gradient,
* radial gradient, or pattern, and also apply shadows if needed
* @name fill
* @methodOf Kinetic.Shape.prototype
* */
fill: function(context) {
var appliedShadow = false;
var fill = this.attrs.fill;
if(fill) {
context.save();
if(this.attrs.shadow && !this.appliedShadow) {
appliedShadow = this._applyShadow(context);
}
var s = fill.start;
var e = fill.end;
var f = null;
// color fill
if(Kinetic.Type._isString(fill)) {
context.fillStyle = fill;
context.fill(context);
}
// pattern
else if(fill.image) {
var repeat = !fill.repeat ? 'repeat' : fill.repeat;
if(fill.scale) {
context.scale(fill.scale.x, fill.scale.y);
}
if(fill.offset) {
context.translate(fill.offset.x, fill.offset.y);
}
context.fillStyle = context.createPattern(fill.image, repeat);
context.fill(context);
}
// linear gradient
else if(!s.radius && !e.radius) {
var grd = context.createLinearGradient(s.x, s.y, e.x, e.y);
var colorStops = fill.colorStops;
// build color stops
for(var n = 0; n < colorStops.length; n += 2) {
grd.addColorStop(colorStops[n], colorStops[n + 1]);
}
context.fillStyle = grd;
context.fill(context);
}
// radial gradient
else if((s.radius || s.radius === 0) && (e.radius || e.radius === 0)) {
var grd = context.createRadialGradient(s.x, s.y, s.radius, e.x, e.y, e.radius);
var colorStops = fill.colorStops;
// build color stops
for(var n = 0; n < colorStops.length; n += 2) {
grd.addColorStop(colorStops[n], colorStops[n + 1]);
}
context.fillStyle = grd;
context.fill(context);
}
else {
context.fillStyle = 'black';
context.fill(context);
}
context.restore();
}
if(appliedShadow) {
this.fill(context);
}
},
/**
* helper method to fill text and appy shadows if needed
* @param {String} text
* @name fillText
* @methodOf Kinetic.Shape.prototype
*/
fillText: function(context, text) {
var appliedShadow = false;
if(this.attrs.textFill) {
context.save();
if(this.attrs.shadow && !this.appliedShadow) {
appliedShadow = this._applyShadow(context);
}
context.fillStyle = this.attrs.textFill;
context.fillText(text, 0, 0);
context.restore();
}
if(appliedShadow) {
this.fillText(context, text, 0, 0);
}
},
/**
* helper method to stroke text and apply shadows
* if needed
* @name strokeText
* @methodOf Kinetic.Shape.prototype
* @param {String} text
*/
strokeText: function(context, text) {
var appliedShadow = false;
if(this.attrs.textStroke || this.attrs.textStrokeWidth) {
context.save();
if(this.attrs.shadow && !this.appliedShadow) {
appliedShadow = this._applyShadow(context);
}
// defaults
var textStroke = this.attrs.textStroke ? this.attrs.textStroke : 'black';
var textStrokeWidth = this.attrs.textStrokeWidth ? this.attrs.textStrokeWidth : 2;
context.lineWidth = textStrokeWidth;
context.strokeStyle = textStroke;
context.strokeText(text, 0, 0);
context.restore();
}
if(appliedShadow) {
this.strokeText(context, text, 0, 0);
}
},
/**
* helper method to draw an image and apply
* a shadow if neede
* @name drawImage
* @methodOf Kinetic.Shape.prototype
*/
drawImage: function() {
var appliedShadow = false;
var context = arguments[0];
context.save();
var a = Array.prototype.slice.call(arguments);
if(a.length === 6 || a.length === 10) {
if(this.attrs.shadow && !this.appliedShadow) {
appliedShadow = this._applyShadow(context);
}
if(a.length === 6) {
context.drawImage(a[1], a[2], a[3], a[4], a[5]);
}
else {
context.drawImage(a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8], a[9]);
}
}
context.restore();
if(appliedShadow) {
this.drawImage.apply(this, a);
}
},
/**
* helper method to set the line join of a shape
* based on the lineJoin property
* @name applyLineJoin
* @methodOf Kinetic.Shape.prototype
*/
applyLineJoin: function(context) {
if(this.attrs.lineJoin) {
context.lineJoin = this.attrs.lineJoin;
}
},
/**
* apply shadow. return true if shadow was applied
* and false if it was not
*/
_applyShadow: function(context) {
var s = this.attrs.shadow;
if(s) {
var aa = this.getAbsoluteOpacity();
// defaults
var color = s.color ? s.color : 'black';
var blur = s.blur ? s.blur : 5;
var offset = s.offset ? s.offset : {
x: 0,
y: 0
};
if(s.opacity) {
context.globalAlpha = s.opacity * aa;
}
context.shadowColor = color;
context.shadowBlur = blur;
context.shadowOffsetX = offset.x;
context.shadowOffsetY = offset.y;
this.appliedShadow = true;
return true;
}
return false;
},
/**
* determines if point is in the shape
* @param {Object|Array} point point can be an object containing
* an x and y property, or it can be an array with two elements
* in which the first element is the x component and the second
* element is the y component
*/
intersects: function() {
var pos = Kinetic.Type._getXY(Array.prototype.slice.call(arguments));
var stage = this.getStage();
var bufferCanvas = stage.bufferCanvas;
bufferCanvas.clear();
this._draw(bufferCanvas);
var obj = stage.getIntersection(pos);
return !!(obj && obj.pixel[3] > 0);
},
_draw: function(canvas) {
if(this.attrs.drawFunc) {
var stage = this.getStage();
var context = canvas.getContext();
var family = [];
var parent = this.parent;
family.unshift(this);
while(parent) {
family.unshift(parent);
parent = parent.parent;
}
context.save();
for(var n = 0; n < family.length; n++) {
var node = family[n];
var t = node.getTransform();
var m = t.getMatrix();
context.transform(m[0], m[1], m[2], m[3], m[4], m[5]);
}
/*
* pre styles include opacity, linejoin
*/
var absOpacity = this.getAbsoluteOpacity();
if(absOpacity !== 1) {
context.globalAlpha = absOpacity;
}
this.applyLineJoin(context);
// draw the shape
this.appliedShadow = false;
var wl = Kinetic.Global.BUFFER_WHITELIST;
var bl = Kinetic.Global.BUFFER_BLACKLIST;
var attrs = {};
if(canvas.name === 'buffer') {
for(var n = 0; n < wl.length; n++) {
var key = wl[n];
attrs[key] = this.attrs[key];
if(this.attrs[key]) {
this.attrs[key] = '#' + this.colorKey;
}
}
for(var n = 0; n < bl.length; n++) {
var key = bl[n];
attrs[key] = this.attrs[key];
this.attrs[key] = '';
}
// image is a special case
if('image' in this.attrs) {
attrs.image = this.attrs.image;
if(this.bufferImage) {
this.attrs.image = this.bufferImage;
}
else {
this.attrs.image = null;
this.attrs.fill = '#' + this.colorKey;
}
}
context.globalAlpha = 1;
}
this.attrs.drawFunc.call(this, canvas.getContext());
if(canvas.name === 'buffer') {
var bothLists = wl.concat(bl);
for(var n = 0; n < bothLists.length; n++) {
var key = bothLists[n];
this.attrs[key] = attrs[key];
}
// image is a special case
this.attrs.image = attrs.image;
}
context.restore();
}
}
});
// add getters and setters
Kinetic.Node.addGettersSetters(Kinetic.Shape, ['fill', 'stroke', 'lineJoin', 'strokeWidth', 'shadow', 'drawFunc', 'filter']);
/**
* set fill which can be a color, linear gradient object,
* radial gradient object, or pattern object
* @name setFill
* @methodOf Kinetic.Shape.prototype
* @param {String|Object} fill
*/
/**
* set stroke color
* @name setStroke
* @methodOf Kinetic.Shape.prototype
* @param {String} stroke
*/
/**
* set line join
* @name setLineJoin
* @methodOf Kinetic.Shape.prototype
* @param {String} lineJoin. Can be miter, round, or bevel. The
* default is miter
*/
/**
* set stroke width
* @name setStrokeWidth
* @methodOf Kinetic.Shape.prototype
* @param {Number} strokeWidth
*/
/**
* set shadow object
* @name setShadow
* @methodOf Kinetic.Shape.prototype
* @param {Object} config
*/
/**
* set draw function
* @name setDrawFunc
* @methodOf Kinetic.Shape.prototype
* @param {Function} drawFunc drawing function
*/
/**
* get fill
* @name getFill
* @methodOf Kinetic.Shape.prototype
*/
/**
* get stroke color
* @name getStroke
* @methodOf Kinetic.Shape.prototype
*/
/**
* get line join
* @name getLineJoin
* @methodOf Kinetic.Shape.prototype
*/
/**
* get stroke width
* @name getStrokeWidth
* @methodOf Kinetic.Shape.prototype
*/
/**
* get shadow object
* @name getShadow
* @methodOf Kinetic.Shape.prototype
*/
/**
* get draw function
* @name getDrawFunc
* @methodOf Kinetic.Shape.prototype
*/