123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677 |
- import Displayable, { DisplayableProps,
- CommonStyleProps,
- DEFAULT_COMMON_STYLE,
- DisplayableStatePropNames,
- DEFAULT_COMMON_ANIMATION_PROPS
- } from './Displayable';
- import Element, { ElementAnimateConfig } from '../Element';
- import PathProxy from '../core/PathProxy';
- import * as pathContain from '../contain/path';
- import { PatternObject } from './Pattern';
- import { Dictionary, PropType, MapToType } from '../core/types';
- import BoundingRect from '../core/BoundingRect';
- import { LinearGradientObject } from './LinearGradient';
- import { RadialGradientObject } from './RadialGradient';
- import { defaults, keys, extend, clone, isString, createObject } from '../core/util';
- import Animator from '../animation/Animator';
- import { lum } from '../tool/color';
- import { DARK_LABEL_COLOR, LIGHT_LABEL_COLOR, DARK_MODE_THRESHOLD, LIGHTER_LABEL_COLOR } from '../config';
- import { REDRAW_BIT, SHAPE_CHANGED_BIT, STYLE_CHANGED_BIT } from './constants';
- import { TRANSFORMABLE_PROPS } from '../core/Transformable';
- export interface PathStyleProps extends CommonStyleProps {
- fill?: string | PatternObject | LinearGradientObject | RadialGradientObject
- stroke?: string | PatternObject | LinearGradientObject | RadialGradientObject
- decal?: PatternObject
- /**
- * Still experimental, not works weel on arc with edge cases(large angle).
- */
- strokePercent?: number
- strokeNoScale?: boolean
- fillOpacity?: number
- strokeOpacity?: number
- /**
- * `true` is not supported.
- * `false`/`null`/`undefined` are the same.
- * `false` is used to remove lineDash in some
- * case that `null`/`undefined` can not be set.
- * (e.g., emphasis.lineStyle in echarts)
- */
- lineDash?: false | number[] | 'solid' | 'dashed' | 'dotted'
- lineDashOffset?: number
- lineWidth?: number
- lineCap?: CanvasLineCap
- lineJoin?: CanvasLineJoin
- miterLimit?: number
- /**
- * Paint order, if do stroke first. Similar to SVG paint-order
- * https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/paint-order
- */
- strokeFirst?: boolean
- }
- export const DEFAULT_PATH_STYLE: PathStyleProps = defaults({
- fill: '#000',
- stroke: null,
- strokePercent: 1,
- fillOpacity: 1,
- strokeOpacity: 1,
- lineDashOffset: 0,
- lineWidth: 1,
- lineCap: 'butt',
- miterLimit: 10,
- strokeNoScale: false,
- strokeFirst: false
- } as PathStyleProps, DEFAULT_COMMON_STYLE);
- export const DEFAULT_PATH_ANIMATION_PROPS: MapToType<PathProps, boolean> = {
- style: defaults<MapToType<PathStyleProps, boolean>, MapToType<PathStyleProps, boolean>>({
- fill: true,
- stroke: true,
- strokePercent: true,
- fillOpacity: true,
- strokeOpacity: true,
- lineDashOffset: true,
- lineWidth: true,
- miterLimit: true
- } as MapToType<PathStyleProps, boolean>, DEFAULT_COMMON_ANIMATION_PROPS.style)
- };
- export interface PathProps extends DisplayableProps {
- strokeContainThreshold?: number
- segmentIgnoreThreshold?: number
- subPixelOptimize?: boolean
- style?: PathStyleProps
- shape?: Dictionary<any>
- autoBatch?: boolean
- __value?: (string | number)[] | (string | number)
- buildPath?: (
- ctx: PathProxy | CanvasRenderingContext2D,
- shapeCfg: Dictionary<any>,
- inBatch?: boolean
- ) => void
- }
- type PathKey = keyof PathProps
- type PathPropertyType = PropType<PathProps, PathKey>
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- interface Path<Props extends PathProps = PathProps> {
- animate(key?: '', loop?: boolean): Animator<this>
- animate(key: 'style', loop?: boolean): Animator<this['style']>
- animate(key: 'shape', loop?: boolean): Animator<this['shape']>
- getState(stateName: string): PathState
- ensureState(stateName: string): PathState
- states: Dictionary<PathState>
- stateProxy: (stateName: string) => PathState
- }
- export type PathStatePropNames = DisplayableStatePropNames | 'shape';
- export type PathState = Pick<PathProps, PathStatePropNames> & {
- hoverLayer?: boolean
- }
- const pathCopyParams = (TRANSFORMABLE_PROPS as readonly string[]).concat(['invisible',
- 'culling', 'z', 'z2', 'zlevel', 'parent'
- ]) as (keyof Path)[];
- class Path<Props extends PathProps = PathProps> extends Displayable<Props> {
- path: PathProxy
- strokeContainThreshold: number
- // 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: number
- subPixelOptimize: boolean
- style: PathStyleProps
- /**
- * If element can be batched automatically
- */
- autoBatch: boolean
- private _rectStroke: BoundingRect
- protected _normalState: PathState
- protected _decalEl: Path
- // Must have an initial value on shape.
- // It will be assigned by default value.
- shape: Dictionary<any>
- constructor(opts?: Props) {
- super(opts);
- }
- update() {
- super.update();
- const style = this.style;
- if (style.decal) {
- const decalEl: Path = this._decalEl = this._decalEl || new Path();
- if (decalEl.buildPath === Path.prototype.buildPath) {
- decalEl.buildPath = ctx => {
- this.buildPath(ctx, this.shape);
- };
- }
- decalEl.silent = true;
- const decalElStyle = decalEl.style;
- for (let key in style) {
- if ((decalElStyle as any)[key] !== (style as any)[key]) {
- (decalElStyle as any)[key] = (style as any)[key];
- }
- }
- decalElStyle.fill = style.fill ? style.decal : null;
- decalElStyle.decal = null;
- decalElStyle.shadowColor = null;
- style.strokeFirst && (decalElStyle.stroke = null);
- for (let i = 0; i < pathCopyParams.length; ++i) {
- (decalEl as any)[pathCopyParams[i]] = this[pathCopyParams[i]];
- }
- decalEl.__dirty |= REDRAW_BIT;
- }
- else if (this._decalEl) {
- this._decalEl = null;
- }
- }
- getDecalElement() {
- return this._decalEl;
- }
- protected _init(props?: Props) {
- // Init default properties
- const keysArr = keys(props);
- this.shape = this.getDefaultShape();
- const defaultStyle = this.getDefaultStyle();
- if (defaultStyle) {
- this.useStyle(defaultStyle);
- }
- for (let i = 0; i < keysArr.length; i++) {
- const key = keysArr[i];
- const value = props[key];
- if (key === 'style') {
- if (!this.style) {
- // PENDING Reuse style object if possible?
- this.useStyle(value as Props['style']);
- }
- else {
- extend(this.style, value as Props['style']);
- }
- }
- else if (key === 'shape') {
- // this.shape = value;
- extend(this.shape, value as Props['shape']);
- }
- else {
- super.attrKV(key as any, value);
- }
- }
- // Create an empty one if no style object exists.
- if (!this.style) {
- this.useStyle({});
- }
- // const defaultShape = this.getDefaultShape();
- // if (!this.shape) {
- // this.shape = defaultShape;
- // }
- // else {
- // defaults(this.shape, defaultShape);
- // }
- }
- protected getDefaultStyle(): Props['style'] {
- return null;
- }
- // Needs to override
- protected getDefaultShape() {
- return {};
- }
- protected canBeInsideText() {
- return this.hasFill();
- }
- protected getInsideTextFill() {
- const pathFill = this.style.fill;
- if (pathFill !== 'none') {
- if (isString(pathFill)) {
- const fillLum = lum(pathFill, 0);
- // Determin text color based on the lum of path fill.
- // TODO use (1 - DARK_MODE_THRESHOLD)?
- if (fillLum > 0.5) { // TODO Consider background lum?
- return DARK_LABEL_COLOR;
- }
- else if (fillLum > 0.2) {
- return LIGHTER_LABEL_COLOR;
- }
- return LIGHT_LABEL_COLOR;
- }
- else if (pathFill) {
- return LIGHT_LABEL_COLOR;
- }
- }
- return DARK_LABEL_COLOR;
- }
- protected getInsideTextStroke(textFill?: string) {
- const pathFill = this.style.fill;
- // Not stroke on none fill object or gradient object
- if (isString(pathFill)) {
- const zr = this.__zr;
- const isDarkMode = !!(zr && zr.isDarkMode());
- const isDarkLabel = lum(textFill, 0) < DARK_MODE_THRESHOLD;
- // All dark or all light.
- if (isDarkMode === isDarkLabel) {
- return pathFill;
- }
- }
- }
- // When bundling path, some shape may decide if use moveTo to begin a new subpath or closePath
- // Like in circle
- buildPath(
- ctx: PathProxy | CanvasRenderingContext2D,
- shapeCfg: Dictionary<any>,
- inBatch?: boolean
- ) {}
- pathUpdated() {
- this.__dirty &= ~SHAPE_CHANGED_BIT;
- }
- getUpdatedPathProxy(inBatch?: boolean) {
- // Update path proxy data to latest.
- !this.path && this.createPathProxy();
- this.path.beginPath();
- this.buildPath(this.path, this.shape, inBatch);
- return this.path;
- }
- createPathProxy() {
- this.path = new PathProxy(false);
- }
- hasStroke() {
- const style = this.style;
- const stroke = style.stroke;
- return !(stroke == null || stroke === 'none' || !(style.lineWidth > 0));
- }
- hasFill() {
- const style = this.style;
- const fill = style.fill;
- return fill != null && fill !== 'none';
- }
- getBoundingRect(): BoundingRect {
- let rect = this._rect;
- const style = this.style;
- const needsUpdateRect = !rect;
- if (needsUpdateRect) {
- let firstInvoke = false;
- if (!this.path) {
- firstInvoke = true;
- // Create path on demand.
- this.createPathProxy();
- }
- let path = this.path;
- if (firstInvoke || (this.__dirty & SHAPE_CHANGED_BIT)) {
- path.beginPath();
- this.buildPath(path, this.shape, false);
- this.pathUpdated();
- }
- rect = path.getBoundingRect();
- }
- this._rect = rect;
- if (this.hasStroke() && this.path && this.path.len() > 0) {
- // Needs update rect with stroke lineWidth when
- // 1. Element changes scale or lineWidth
- // 2. Shape is changed
- const rectStroke = this._rectStroke || (this._rectStroke = rect.clone());
- if (this.__dirty || needsUpdateRect) {
- rectStroke.copy(rect);
- // PENDING, Min line width is needed when line is horizontal or vertical
- const lineScale = style.strokeNoScale ? this.getLineScale() : 1;
- // FIXME Must after updateTransform
- let w = style.lineWidth;
- // Only add extra hover lineWidth when there are no fill
- if (!this.hasFill()) {
- const strokeContainThreshold = this.strokeContainThreshold;
- w = Math.max(w, strokeContainThreshold == null ? 4 : strokeContainThreshold);
- }
- // Consider line width
- // Line scale can't be 0;
- if (lineScale > 1e-10) {
- rectStroke.width += w / lineScale;
- rectStroke.height += w / lineScale;
- rectStroke.x -= w / lineScale / 2;
- rectStroke.y -= w / lineScale / 2;
- }
- }
- // Return rect with stroke
- return rectStroke;
- }
- return rect;
- }
- contain(x: number, y: number): boolean {
- const localPos = this.transformCoordToLocal(x, y);
- const rect = this.getBoundingRect();
- const style = this.style;
- x = localPos[0];
- y = localPos[1];
- if (rect.contain(x, y)) {
- const pathProxy = this.path;
- if (this.hasStroke()) {
- let lineWidth = style.lineWidth;
- let 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 (!this.hasFill()) {
- lineWidth = Math.max(lineWidth, this.strokeContainThreshold);
- }
- if (pathContain.containStroke(
- pathProxy, lineWidth / lineScale, x, y
- )) {
- return true;
- }
- }
- }
- if (this.hasFill()) {
- return pathContain.contain(pathProxy, x, y);
- }
- }
- return false;
- }
- /**
- * Shape changed
- */
- dirtyShape() {
- this.__dirty |= SHAPE_CHANGED_BIT;
- if (this._rect) {
- this._rect = null;
- }
- if (this._decalEl) {
- this._decalEl.dirtyShape();
- }
- this.markRedraw();
- }
- dirty() {
- this.dirtyStyle();
- this.dirtyShape();
- }
- /**
- * Alias for animate('shape')
- * @param {boolean} loop
- */
- animateShape(loop: boolean) {
- return this.animate('shape', loop);
- }
- // Override updateDuringAnimation
- updateDuringAnimation(targetKey: string) {
- if (targetKey === 'style') {
- this.dirtyStyle();
- }
- else if (targetKey === 'shape') {
- this.dirtyShape();
- }
- else {
- this.markRedraw();
- }
- }
- // Overwrite attrKV
- attrKV(key: PathKey, value: PathPropertyType) {
- // FIXME
- if (key === 'shape') {
- this.setShape(value as Props['shape']);
- }
- else {
- super.attrKV(key as keyof DisplayableProps, value);
- }
- }
- setShape(obj: Props['shape']): this
- setShape<T extends keyof Props['shape']>(obj: T, value: Props['shape'][T]): this
- setShape(keyOrObj: keyof Props['shape'] | Props['shape'], value?: unknown): this {
- let shape = this.shape;
- if (!shape) {
- shape = this.shape = {};
- }
- // Path from string may not have shape
- if (typeof keyOrObj === 'string') {
- shape[keyOrObj] = value;
- }
- else {
- extend(shape, keyOrObj as Props['shape']);
- }
- this.dirtyShape();
- return this;
- }
- /**
- * If shape changed. used with dirtyShape
- */
- shapeChanged() {
- return !!(this.__dirty & SHAPE_CHANGED_BIT);
- }
- /**
- * Create a path style object with default values in it's prototype.
- * @override
- */
- createStyle(obj?: Props['style']) {
- return createObject(DEFAULT_PATH_STYLE, obj);
- }
- protected _innerSaveToNormal(toState: PathState) {
- super._innerSaveToNormal(toState);
- const normalState = this._normalState;
- // Clone a new one. DON'T share object reference between states and current using.
- // TODO: Clone array in shape?.
- // TODO: Only save changed shape.
- if (toState.shape && !normalState.shape) {
- normalState.shape = extend({}, this.shape);
- }
- }
- protected _applyStateObj(
- stateName: string,
- state: PathState,
- normalState: PathState,
- keepCurrentStates: boolean,
- transition: boolean,
- animationCfg: ElementAnimateConfig
- ) {
- super._applyStateObj(stateName, state, normalState, keepCurrentStates, transition, animationCfg);
- const needsRestoreToNormal = !(state && keepCurrentStates);
- let targetShape: Props['shape'];
- if (state && state.shape) {
- // Only animate changed properties.
- if (transition) {
- if (keepCurrentStates) {
- targetShape = state.shape;
- }
- else {
- // Inherits from normal state.
- targetShape = extend({}, normalState.shape);
- extend(targetShape, state.shape);
- }
- }
- else {
- // Because the shape will be replaced. So inherits from current shape.
- targetShape = extend({}, keepCurrentStates ? this.shape : normalState.shape);
- extend(targetShape, state.shape);
- }
- }
- else if (needsRestoreToNormal) {
- targetShape = normalState.shape;
- }
- if (targetShape) {
- if (transition) {
- // Clone a new shape.
- this.shape = extend({}, this.shape);
- // Only supports transition on primary props. Because shape is not deep cloned.
- const targetShapePrimaryProps: Props['shape'] = {};
- const shapeKeys = keys(targetShape);
- for (let i = 0; i < shapeKeys.length; i++) {
- const key = shapeKeys[i];
- if (typeof targetShape[key] === 'object') {
- (this.shape as Props['shape'])[key] = targetShape[key];
- }
- else {
- targetShapePrimaryProps[key] = targetShape[key];
- }
- }
- this._transitionState(stateName, {
- shape: targetShapePrimaryProps
- } as Props, animationCfg);
- }
- else {
- this.shape = targetShape;
- this.dirtyShape();
- }
- }
- }
- protected _mergeStates(states: PathState[]) {
- const mergedState = super._mergeStates(states) as PathState;
- let mergedShape: Props['shape'];
- for (let i = 0; i < states.length; i++) {
- const state = states[i];
- if (state.shape) {
- mergedShape = mergedShape || {};
- this._mergeStyle(mergedShape, state.shape);
- }
- }
- if (mergedShape) {
- mergedState.shape = mergedShape;
- }
- return mergedState;
- }
- getAnimationStyleProps() {
- return DEFAULT_PATH_ANIMATION_PROPS;
- }
- /**
- * If path shape is zero area
- */
- isZeroArea(): boolean {
- return false;
- }
- /**
- * 扩展一个 Path element, 比如星形,圆等。
- * Extend a path element
- * @DEPRECATED Use class extends
- * @param props
- * @param props.type Path type
- * @param props.init Initialize
- * @param props.buildPath Overwrite buildPath method
- * @param props.style Extended default style config
- * @param props.shape Extended default shape config
- */
- static extend<Shape extends Dictionary<any>>(defaultProps: {
- type?: string
- shape?: Shape
- style?: PathStyleProps
- beforeBrush?: Displayable['beforeBrush']
- afterBrush?: Displayable['afterBrush']
- getBoundingRect?: Displayable['getBoundingRect']
- calculateTextPosition?: Element['calculateTextPosition']
- buildPath(this: Path, ctx: CanvasRenderingContext2D | PathProxy, shape: Shape, inBatch?: boolean): void
- init?(this: Path, opts: PathProps): void // TODO Should be SubPathOption
- }): {
- new(opts?: PathProps & {shape: Shape}): Path
- } {
- interface SubPathOption extends PathProps {
- shape: Shape
- }
- class Sub extends Path {
- shape: Shape
- getDefaultStyle() {
- return clone(defaultProps.style);
- }
- getDefaultShape() {
- return clone(defaultProps.shape);
- }
- constructor(opts?: SubPathOption) {
- super(opts);
- defaultProps.init && defaultProps.init.call(this as any, opts);
- }
- }
- // TODO Legacy usage. Extend functions
- for (let key in defaultProps) {
- if (typeof (defaultProps as any)[key] === 'function') {
- (Sub.prototype as any)[key] = (defaultProps as any)[key];
- }
- }
- // Sub.prototype.buildPath = defaultProps.buildPath;
- // Sub.prototype.beforeBrush = defaultProps.beforeBrush;
- // Sub.prototype.afterBrush = defaultProps.afterBrush;
- return Sub as any;
- }
- protected static initDefaultProps = (function () {
- const pathProto = Path.prototype;
- pathProto.type = 'path';
- pathProto.strokeContainThreshold = 5;
- pathProto.segmentIgnoreThreshold = 0;
- pathProto.subPixelOptimize = false;
- pathProto.autoBatch = false;
- pathProto.__dirty = REDRAW_BIT | STYLE_CHANGED_BIT | SHAPE_CHANGED_BIT;
- })()
- }
- export default Path;
|