378 lines
10 KiB
JavaScript
378 lines
10 KiB
JavaScript
var Displayable = require("./Displayable");
|
|
|
|
var zrUtil = require("../core/util");
|
|
|
|
var PathProxy = require("../core/PathProxy");
|
|
|
|
var pathContain = require("../contain/path");
|
|
|
|
var Pattern = require("./Pattern");
|
|
|
|
var getCanvasPattern = Pattern.prototype.getCanvasPattern;
|
|
var abs = Math.abs;
|
|
var pathProxyForDraw = new PathProxy(true);
|
|
/**
|
|
* @alias module:zrender/graphic/Path
|
|
* @extends module:zrender/graphic/Displayable
|
|
* @constructor
|
|
* @param {Object} opts
|
|
*/
|
|
|
|
function Path(opts) {
|
|
Displayable.call(this, opts);
|
|
/**
|
|
* @type {module:zrender/core/PathProxy}
|
|
* @readOnly
|
|
*/
|
|
|
|
this.path = null;
|
|
}
|
|
|
|
Path.prototype = {
|
|
constructor: Path,
|
|
type: 'path',
|
|
__dirtyPath: true,
|
|
strokeContainThreshold: 5,
|
|
// This item default to be false. But in map series in echarts,
|
|
// in order to improve performance, it should be set to true,
|
|
// so the shorty segment won't draw.
|
|
segmentIgnoreThreshold: 0,
|
|
|
|
/**
|
|
* See `module:zrender/src/graphic/helper/subPixelOptimize`.
|
|
* @type {boolean}
|
|
*/
|
|
subPixelOptimize: false,
|
|
brush: function (ctx, prevEl) {
|
|
var style = this.style;
|
|
var path = this.path || pathProxyForDraw;
|
|
var hasStroke = style.hasStroke();
|
|
var hasFill = style.hasFill();
|
|
var fill = style.fill;
|
|
var stroke = style.stroke;
|
|
var hasFillGradient = hasFill && !!fill.colorStops;
|
|
var hasStrokeGradient = hasStroke && !!stroke.colorStops;
|
|
var hasFillPattern = hasFill && !!fill.image;
|
|
var hasStrokePattern = hasStroke && !!stroke.image;
|
|
style.bind(ctx, this, prevEl);
|
|
this.setTransform(ctx);
|
|
|
|
if (this.__dirty) {
|
|
var rect; // Update gradient because bounding rect may changed
|
|
|
|
if (hasFillGradient) {
|
|
rect = rect || this.getBoundingRect();
|
|
this._fillGradient = style.getGradient(ctx, fill, rect);
|
|
}
|
|
|
|
if (hasStrokeGradient) {
|
|
rect = rect || this.getBoundingRect();
|
|
this._strokeGradient = style.getGradient(ctx, stroke, rect);
|
|
}
|
|
} // Use the gradient or pattern
|
|
|
|
|
|
if (hasFillGradient) {
|
|
// PENDING If may have affect the state
|
|
ctx.fillStyle = this._fillGradient;
|
|
} else if (hasFillPattern) {
|
|
ctx.fillStyle = getCanvasPattern.call(fill, ctx);
|
|
}
|
|
|
|
if (hasStrokeGradient) {
|
|
ctx.strokeStyle = this._strokeGradient;
|
|
} else if (hasStrokePattern) {
|
|
ctx.strokeStyle = getCanvasPattern.call(stroke, ctx);
|
|
}
|
|
|
|
var lineDash = style.lineDash;
|
|
var lineDashOffset = style.lineDashOffset;
|
|
var ctxLineDash = !!ctx.setLineDash; // Update path sx, sy
|
|
|
|
var scale = this.getGlobalScale();
|
|
path.setScale(scale[0], scale[1], this.segmentIgnoreThreshold); // Proxy context
|
|
// Rebuild path in following 2 cases
|
|
// 1. Path is dirty
|
|
// 2. Path needs javascript implemented lineDash stroking.
|
|
// In this case, lineDash information will not be saved in PathProxy
|
|
|
|
if (this.__dirtyPath || lineDash && !ctxLineDash && hasStroke) {
|
|
path.beginPath(ctx); // Setting line dash before build path
|
|
|
|
if (lineDash && !ctxLineDash) {
|
|
path.setLineDash(lineDash);
|
|
path.setLineDashOffset(lineDashOffset);
|
|
}
|
|
|
|
this.buildPath(path, this.shape, false); // Clear path dirty flag
|
|
|
|
if (this.path) {
|
|
this.__dirtyPath = false;
|
|
}
|
|
} else {
|
|
// Replay path building
|
|
ctx.beginPath();
|
|
this.path.rebuildPath(ctx);
|
|
}
|
|
|
|
if (hasFill) {
|
|
if (style.fillOpacity != null) {
|
|
var originalGlobalAlpha = ctx.globalAlpha;
|
|
ctx.globalAlpha = style.fillOpacity * style.opacity;
|
|
path.fill(ctx);
|
|
ctx.globalAlpha = originalGlobalAlpha;
|
|
} else {
|
|
path.fill(ctx);
|
|
}
|
|
}
|
|
|
|
if (lineDash && ctxLineDash) {
|
|
ctx.setLineDash(lineDash);
|
|
ctx.lineDashOffset = lineDashOffset;
|
|
}
|
|
|
|
if (hasStroke) {
|
|
if (style.strokeOpacity != null) {
|
|
var originalGlobalAlpha = ctx.globalAlpha;
|
|
ctx.globalAlpha = style.strokeOpacity * style.opacity;
|
|
path.stroke(ctx);
|
|
ctx.globalAlpha = originalGlobalAlpha;
|
|
} else {
|
|
path.stroke(ctx);
|
|
}
|
|
}
|
|
|
|
if (lineDash && ctxLineDash) {
|
|
// PENDING
|
|
// Remove lineDash
|
|
ctx.setLineDash([]);
|
|
} // Draw rect text
|
|
|
|
|
|
if (style.text != null) {
|
|
// Only restore transform when needs draw text.
|
|
this.restoreTransform(ctx);
|
|
this.drawRectText(ctx, this.getBoundingRect());
|
|
}
|
|
},
|
|
// When bundling path, some shape may decide if use moveTo to begin a new subpath or closePath
|
|
// Like in circle
|
|
buildPath: function (ctx, shapeCfg, inBundle) {},
|
|
createPathProxy: function () {
|
|
this.path = new PathProxy();
|
|
},
|
|
getBoundingRect: function () {
|
|
var rect = this._rect;
|
|
var style = this.style;
|
|
var needsUpdateRect = !rect;
|
|
|
|
if (needsUpdateRect) {
|
|
var path = this.path;
|
|
|
|
if (!path) {
|
|
// Create path on demand.
|
|
path = this.path = new PathProxy();
|
|
}
|
|
|
|
if (this.__dirtyPath) {
|
|
path.beginPath();
|
|
this.buildPath(path, this.shape, false);
|
|
}
|
|
|
|
rect = path.getBoundingRect();
|
|
}
|
|
|
|
this._rect = rect;
|
|
|
|
if (style.hasStroke()) {
|
|
// Needs update rect with stroke lineWidth when
|
|
// 1. Element changes scale or lineWidth
|
|
// 2. Shape is changed
|
|
var rectWithStroke = this._rectWithStroke || (this._rectWithStroke = rect.clone());
|
|
|
|
if (this.__dirty || needsUpdateRect) {
|
|
rectWithStroke.copy(rect); // FIXME Must after updateTransform
|
|
|
|
var w = style.lineWidth; // PENDING, Min line width is needed when line is horizontal or vertical
|
|
|
|
var lineScale = style.strokeNoScale ? this.getLineScale() : 1; // Only add extra hover lineWidth when there are no fill
|
|
|
|
if (!style.hasFill()) {
|
|
w = Math.max(w, this.strokeContainThreshold || 4);
|
|
} // Consider line width
|
|
// Line scale can't be 0;
|
|
|
|
|
|
if (lineScale > 1e-10) {
|
|
rectWithStroke.width += w / lineScale;
|
|
rectWithStroke.height += w / lineScale;
|
|
rectWithStroke.x -= w / lineScale / 2;
|
|
rectWithStroke.y -= w / lineScale / 2;
|
|
}
|
|
} // Return rect with stroke
|
|
|
|
|
|
return rectWithStroke;
|
|
}
|
|
|
|
return rect;
|
|
},
|
|
contain: function (x, y) {
|
|
var localPos = this.transformCoordToLocal(x, y);
|
|
var rect = this.getBoundingRect();
|
|
var style = this.style;
|
|
x = localPos[0];
|
|
y = localPos[1];
|
|
|
|
if (rect.contain(x, y)) {
|
|
var pathData = this.path.data;
|
|
|
|
if (style.hasStroke()) {
|
|
var lineWidth = style.lineWidth;
|
|
var lineScale = style.strokeNoScale ? this.getLineScale() : 1; // Line scale can't be 0;
|
|
|
|
if (lineScale > 1e-10) {
|
|
// Only add extra hover lineWidth when there are no fill
|
|
if (!style.hasFill()) {
|
|
lineWidth = Math.max(lineWidth, this.strokeContainThreshold);
|
|
}
|
|
|
|
if (pathContain.containStroke(pathData, lineWidth / lineScale, x, y)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (style.hasFill()) {
|
|
return pathContain.contain(pathData, x, y);
|
|
}
|
|
}
|
|
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* @param {boolean} dirtyPath
|
|
*/
|
|
dirty: function (dirtyPath) {
|
|
if (dirtyPath == null) {
|
|
dirtyPath = true;
|
|
} // Only mark dirty, not mark clean
|
|
|
|
|
|
if (dirtyPath) {
|
|
this.__dirtyPath = dirtyPath;
|
|
this._rect = null;
|
|
}
|
|
|
|
this.__dirty = this.__dirtyText = true;
|
|
this.__zr && this.__zr.refresh(); // Used as a clipping path
|
|
|
|
if (this.__clipTarget) {
|
|
this.__clipTarget.dirty();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Alias for animate('shape')
|
|
* @param {boolean} loop
|
|
*/
|
|
animateShape: function (loop) {
|
|
return this.animate('shape', loop);
|
|
},
|
|
// Overwrite attrKV
|
|
attrKV: function (key, value) {
|
|
// FIXME
|
|
if (key === 'shape') {
|
|
this.setShape(value);
|
|
this.__dirtyPath = true;
|
|
this._rect = null;
|
|
} else {
|
|
Displayable.prototype.attrKV.call(this, key, value);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @param {Object|string} key
|
|
* @param {*} value
|
|
*/
|
|
setShape: function (key, value) {
|
|
var shape = this.shape; // Path from string may not have shape
|
|
|
|
if (shape) {
|
|
if (zrUtil.isObject(key)) {
|
|
for (var name in key) {
|
|
if (key.hasOwnProperty(name)) {
|
|
shape[name] = key[name];
|
|
}
|
|
}
|
|
} else {
|
|
shape[key] = value;
|
|
}
|
|
|
|
this.dirty(true);
|
|
}
|
|
|
|
return this;
|
|
},
|
|
getLineScale: function () {
|
|
var m = this.transform; // Get the line scale.
|
|
// Determinant of `m` means how much the area is enlarged by the
|
|
// transformation. So its square root can be used as a scale factor
|
|
// for width.
|
|
|
|
return m && abs(m[0] - 1) > 1e-10 && abs(m[3] - 1) > 1e-10 ? Math.sqrt(abs(m[0] * m[3] - m[2] * m[1])) : 1;
|
|
}
|
|
};
|
|
/**
|
|
* 扩展一个 Path element, 比如星形,圆等。
|
|
* Extend a path element
|
|
* @param {Object} props
|
|
* @param {string} props.type Path type
|
|
* @param {Function} props.init Initialize
|
|
* @param {Function} props.buildPath Overwrite buildPath method
|
|
* @param {Object} [props.style] Extended default style config
|
|
* @param {Object} [props.shape] Extended default shape config
|
|
*/
|
|
|
|
Path.extend = function (defaults) {
|
|
var Sub = function (opts) {
|
|
Path.call(this, opts);
|
|
|
|
if (defaults.style) {
|
|
// Extend default style
|
|
this.style.extendFrom(defaults.style, false);
|
|
} // Extend default shape
|
|
|
|
|
|
var defaultShape = defaults.shape;
|
|
|
|
if (defaultShape) {
|
|
this.shape = this.shape || {};
|
|
var thisShape = this.shape;
|
|
|
|
for (var name in defaultShape) {
|
|
if (!thisShape.hasOwnProperty(name) && defaultShape.hasOwnProperty(name)) {
|
|
thisShape[name] = defaultShape[name];
|
|
}
|
|
}
|
|
}
|
|
|
|
defaults.init && defaults.init.call(this, opts);
|
|
};
|
|
|
|
zrUtil.inherits(Sub, Path); // FIXME 不能 extend position, rotation 等引用对象
|
|
|
|
for (var name in defaults) {
|
|
// Extending prototype values and methods
|
|
if (name !== 'style' && name !== 'shape') {
|
|
Sub.prototype[name] = defaults[name];
|
|
}
|
|
}
|
|
|
|
return Sub;
|
|
};
|
|
|
|
zrUtil.inherits(Path, Displayable);
|
|
var _default = Path;
|
|
module.exports = _default; |