mirror of
https://github.com/konvajs/konva.git
synced 2025-06-28 15:23:44 +08:00
635 lines
21 KiB
JavaScript
635 lines
21 KiB
JavaScript
Kinetic.Shape = (function() {
|
|
/**
|
|
* 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]
|
|
*/
|
|
var Shape = function(config) {
|
|
this._initShape(config);
|
|
};
|
|
|
|
Shape.prototype = {
|
|
_initShape: function(config) {
|
|
this.nodeType = 'Shape';
|
|
this.appliedShadow = false;
|
|
|
|
// set colorKey
|
|
var shapes = Kinetic.Global.shapes;
|
|
var key;
|
|
|
|
while(true) {
|
|
key = Kinetic.Type._getRandomColorKey();
|
|
if(key && !( key in shapes)) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
this.colorKey = key;
|
|
shapes[key] = this;
|
|
|
|
// call super constructor
|
|
Kinetic.Node.call(this, 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
|
|
* @name stroke
|
|
* @methodOf Kinetic.Shape.prototype
|
|
*/
|
|
stroke: function(context) {
|
|
if(context.type === 'scene') {
|
|
this._strokeScene(context);
|
|
}
|
|
else if(context.type === 'hit') {
|
|
this._strokeHit(context);
|
|
}
|
|
},
|
|
_strokeScene: function(context) {
|
|
var strokeWidth = this.getStrokeWidth(), stroke = this.getStroke();
|
|
if(stroke || strokeWidth) {
|
|
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);
|
|
}
|
|
}
|
|
},
|
|
_strokeHit: function(context) {
|
|
var strokeWidth = this.getStrokeWidth(), stroke = this.colorKey;
|
|
if(stroke || strokeWidth) {
|
|
context.save();
|
|
context.lineWidth = strokeWidth || 2;
|
|
context.strokeStyle = stroke || 'black';
|
|
context.stroke(context);
|
|
context.restore();
|
|
}
|
|
},
|
|
_getFillType: function(fill) {
|
|
var type = Kinetic.Type;
|
|
if(!fill) {
|
|
return undefined;
|
|
}
|
|
else if(type._isString(fill)) {
|
|
return 'COLOR';
|
|
}
|
|
else if(fill.image) {
|
|
return 'PATTERN';
|
|
}
|
|
else if(fill.start && fill.end && !fill.start.radius && !fill.end.radius) {
|
|
return 'LINEAR_GRADIENT';
|
|
}
|
|
else if(fill.start && fill.end && type._isNumber(fill.start.radius) && type._isNumber(fill.end.radius)) {
|
|
return 'RADIAL_GRADIENT';
|
|
}
|
|
else {
|
|
return 'UNKNOWN';
|
|
}
|
|
},
|
|
/**
|
|
* helper method to fill the shape
|
|
* @name fill
|
|
* @methodOf Kinetic.Shape.prototype
|
|
* */
|
|
fill: function(context) {
|
|
if(context.type === 'scene') {
|
|
this._fillScene(context);
|
|
}
|
|
else if(context.type === 'hit') {
|
|
this._fillHit(context);
|
|
}
|
|
},
|
|
_fillScene: function(context) {
|
|
var appliedShadow = false, fill = this.getFill(), fillType = this._getFillType(fill);
|
|
if(fill) {
|
|
context.save();
|
|
if(this.attrs.shadow && !this.appliedShadow) {
|
|
appliedShadow = this._applyShadow(context);
|
|
}
|
|
|
|
var s = fill.start;
|
|
var e = fill.end;
|
|
|
|
// color fill
|
|
switch(fillType) {
|
|
case 'COLOR':
|
|
context.fillStyle = fill;
|
|
context.fill(context);
|
|
break;
|
|
case 'PATTERN':
|
|
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);
|
|
break;
|
|
case 'LINEAR_GRADIENT':
|
|
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);
|
|
|
|
break;
|
|
case 'RADIAL_GRADIENT':
|
|
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);
|
|
break;
|
|
default:
|
|
context.fillStyle = 'black';
|
|
context.fill(context);
|
|
break;
|
|
}
|
|
context.restore();
|
|
}
|
|
|
|
if(appliedShadow) {
|
|
this.fill(context);
|
|
}
|
|
},
|
|
_fillHit: function(context) {
|
|
context.save();
|
|
context.fillStyle = this.colorKey;
|
|
context.fill(context);
|
|
context.restore();
|
|
},
|
|
/**
|
|
* helper method to draw an image and apply
|
|
* a shadow if needed
|
|
* @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);
|
|
}
|
|
},
|
|
applyOpacity: function(context) {
|
|
var absOpacity = this.getAbsoluteOpacity();
|
|
if(absOpacity !== 1) {
|
|
context.globalAlpha = absOpacity;
|
|
}
|
|
},
|
|
/**
|
|
* helper method to set the line join of a shape
|
|
* based on the applyLineJoin property
|
|
* @name lineJoin
|
|
* @methodOf Kinetic.Shape.prototype
|
|
*/
|
|
applyLineJoin: function(context) {
|
|
var lineJoin = this.attrs.lineJoin;
|
|
if(lineJoin) {
|
|
context.lineJoin = lineJoin;
|
|
}
|
|
},
|
|
/**
|
|
* helper method to set the line cap of a path
|
|
* based on the lineCap property
|
|
* @name applyLineCap
|
|
* @methodOf Kinetic.Shape.prototype
|
|
*/
|
|
applyLineCap: function(context) {
|
|
var lineCap = this.attrs.lineCap;
|
|
if(lineCap) {
|
|
context.lineCap = lineCap;
|
|
}
|
|
},
|
|
/**
|
|
* set shadow object
|
|
* @name setShadow
|
|
* @methodOf Kinetic.Shape.prototype
|
|
* @param {Object} config
|
|
* @param {String} config.color
|
|
* @param {Number} config.blur
|
|
* @param {Array|Object|Number} config.offset
|
|
* @param {Number} config.opacity
|
|
*/
|
|
setShadow: function(config) {
|
|
var type = Kinetic.Type;
|
|
if(config.offset !== undefined) {
|
|
config.offset = type._getXY(config.offset);
|
|
}
|
|
this.setAttr('shadow', type._merge(config, this.getShadow()));
|
|
},
|
|
/**
|
|
* 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
|
|
*/
|
|
setFill: function(fill) {
|
|
var type = Kinetic.Type;
|
|
var oldFill = this.getFill();
|
|
var fillType = this._getFillType(fill);
|
|
var oldFillType = this._getFillType(oldFill);
|
|
var newOrOldFillIsColor = fillType === 'COLOR' || oldFillType === 'COLOR';
|
|
var changedFillType = fillType === oldFillType || fillType === 'UNKNOWN';
|
|
|
|
// if fill.offset is defined, normalize the xy value
|
|
if(fill.offset !== undefined) {
|
|
fill.offset = type._getXY(fill.offset);
|
|
}
|
|
|
|
/*
|
|
* merge fill objects if neither the new or old fill
|
|
* is type is COLOR, and if if the fill type has not changed. Otherwise,
|
|
* overwrite the fill entirely
|
|
*/
|
|
if(!newOrOldFillIsColor && changedFillType) {
|
|
fill = type._merge(fill, oldFill);
|
|
}
|
|
|
|
this.setAttr('fill', fill);
|
|
},
|
|
/**
|
|
* set width and height
|
|
* @name setSize
|
|
* @methodOf Kinetic.Shape.prototype
|
|
*/
|
|
setSize: function() {
|
|
var size = Kinetic.Type._getSize(Array.prototype.slice.call(arguments));
|
|
this.setWidth(size.width);
|
|
this.setHeight(size.height);
|
|
},
|
|
/**
|
|
* return shape size
|
|
* @name getSize
|
|
* @methodOf Kinetic.Shape.prototype
|
|
*/
|
|
getSize: function() {
|
|
return {
|
|
width: this.getWidth(),
|
|
height: this.getHeight()
|
|
};
|
|
},
|
|
_get: function(selector) {
|
|
return this.nodeType === selector || this.shapeType === selector ? [this] : [];
|
|
},
|
|
/**
|
|
* 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 hitCanvas = stage.hitCanvas;
|
|
hitCanvas.clear();
|
|
this.drawBuffer(hitCanvas);
|
|
var p = hitCanvas.context.getImageData(Math.round(pos.x), Math.round(pos.y), 1, 1).data;
|
|
return p[3] > 0;
|
|
},
|
|
remove: function() {
|
|
Kinetic.Node.prototype.remove.call(this);
|
|
delete Kinetic.Global.shapes[this.colorKey];
|
|
},
|
|
drawBuffer: function(canvas) {
|
|
var attrs = this.attrs, drawFunc = attrs.drawFunc, context = canvas.getContext();
|
|
|
|
if(drawFunc && this.isVisible()) {
|
|
var stage = this.getStage(), family = [], parent = this.parent;
|
|
|
|
family.unshift(this);
|
|
while(parent) {
|
|
family.unshift(parent);
|
|
parent = parent.parent;
|
|
}
|
|
|
|
context.save();
|
|
var len = family.length;
|
|
for(var n = 0; n < len; n++) {
|
|
var node = family[n], t = node.getTransform(), m = t.getMatrix();
|
|
context.transform(m[0], m[1], m[2], m[3], m[4], m[5]);
|
|
}
|
|
|
|
this.applyOpacity(context);
|
|
this.applyLineJoin(context);
|
|
this.applyLineCap(context);
|
|
this.appliedShadow = false;
|
|
|
|
drawFunc.call(this, context);
|
|
context.restore();
|
|
}
|
|
},
|
|
drawScene: function() {
|
|
var attrs = this.attrs, drawFunc = attrs.drawFunc, context = this.getLayer().getCanvas().getContext();
|
|
|
|
if(drawFunc && this.isVisible()) {
|
|
var stage = this.getStage(), family = [], parent = this.parent;
|
|
|
|
family.unshift(this);
|
|
while(parent) {
|
|
family.unshift(parent);
|
|
parent = parent.parent;
|
|
}
|
|
|
|
context.save();
|
|
var len = family.length;
|
|
for(var n = 0; n < len; n++) {
|
|
var node = family[n], t = node.getTransform(), m = t.getMatrix();
|
|
context.transform(m[0], m[1], m[2], m[3], m[4], m[5]);
|
|
}
|
|
|
|
this.applyOpacity(context);
|
|
this.applyLineJoin(context);
|
|
this.applyLineCap(context);
|
|
this.appliedShadow = false;
|
|
|
|
drawFunc.call(this, context);
|
|
context.restore();
|
|
}
|
|
},
|
|
drawHit: function() {
|
|
var attrs = this.attrs, drawFunc = attrs.drawHitFunc || attrs.drawFunc, context = this.getLayer().hitCanvas.getContext();
|
|
|
|
if(drawFunc && this.isVisible() && this.isListening()) {
|
|
var stage = this.getStage(), family = [], parent = this.parent;
|
|
|
|
family.unshift(this);
|
|
while(parent) {
|
|
family.unshift(parent);
|
|
parent = parent.parent;
|
|
}
|
|
|
|
context.save();
|
|
var len = family.length;
|
|
for(var n = 0; n < len; n++) {
|
|
var node = family[n], t = node.getTransform(), m = t.getMatrix();
|
|
context.transform(m[0], m[1], m[2], m[3], m[4], m[5]);
|
|
}
|
|
|
|
// don't draw shadows on hit context
|
|
this.applyLineJoin(context);
|
|
this.applyLineCap(context);
|
|
|
|
drawFunc.call(this, context);
|
|
context.restore();
|
|
}
|
|
},
|
|
_setDrawFuncs: function() {
|
|
if(!this.attrs.drawFunc && this.drawFunc) {
|
|
this.setDrawFunc(this.drawFunc);
|
|
}
|
|
if(!this.attrs.drawHitFunc && this.drawHitFunc) {
|
|
this.setDrawHitFunc(this.drawHitFunc);
|
|
}
|
|
}
|
|
};
|
|
Kinetic.Global.extend(Shape, Kinetic.Node);
|
|
|
|
// add getters and setters
|
|
Kinetic.Node.addGettersSetters(Shape, ['stroke', 'lineJoin', 'strokeWidth', 'drawFunc', 'drawHitFunc', 'cornerRadius']);
|
|
Kinetic.Node.addGetters(Shape, ['shadow', '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 draw function
|
|
* @name setDrawFunc
|
|
* @methodOf Kinetic.Shape.prototype
|
|
* @param {Function} drawFunc drawing function
|
|
*/
|
|
|
|
/**
|
|
* set draw hit function used for hit detection
|
|
* @name setDrawHitFunc
|
|
* @methodOf Kinetic.Shape.prototype
|
|
* @param {Function} drawHitFunc drawing function used for hit detection
|
|
*/
|
|
|
|
/**
|
|
* set corner radius
|
|
* @name setCornerRadius
|
|
* @methodOf Kinetic.Shape.prototype
|
|
* @param {Number} corner radius
|
|
*/
|
|
|
|
/**
|
|
* set line cap. Can be butt, round, or square
|
|
* @name setLineCap
|
|
* @methodOf Kinetic.Shape.prototype
|
|
* @param {String} lineCap
|
|
*/
|
|
|
|
/**
|
|
* 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 corner radius
|
|
* @name getCornerRadius
|
|
* @methodOf Kinetic.Shape.prototype
|
|
*/
|
|
|
|
/**
|
|
* get draw function
|
|
* @name getDrawFunc
|
|
* @methodOf Kinetic.Shape.prototype
|
|
*/
|
|
|
|
/**
|
|
* get draw hit function
|
|
* @name getDrawHitFunc
|
|
* @methodOf Kinetic.Shape.prototype
|
|
*/
|
|
|
|
/**
|
|
* get shadow object
|
|
* @name getShadow
|
|
* @methodOf Kinetic.Shape.prototype
|
|
*/
|
|
|
|
/**
|
|
* get fill
|
|
* @name getFill
|
|
* @methodOf Kinetic.Shape.prototype
|
|
*/
|
|
|
|
/**
|
|
* get line cap
|
|
* @name getLineCap
|
|
* @methodOf Kinetic.Shape.prototype
|
|
*/
|
|
|
|
return Shape;
|
|
})();
|