blob: bd98470a2a8881877505e5a41f93b462d523d241 [file] [log] [blame]
import Displayable from './Displayable';
import * as zrUtil from '../core/util';
import PathProxy from '../core/PathProxy';
import * as pathContain from '../contain/path';
import Pattern from './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);
export default Path;