123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624 |
- /**
- * Base class of all displayable graphic objects
- */
- import Element, {ElementProps, ElementStatePropNames, ElementAnimateConfig, ElementCommonState} from '../Element';
- import BoundingRect from '../core/BoundingRect';
- import { PropType, Dictionary, MapToType } from '../core/types';
- import Path from './Path';
- import { keys, extend, createObject } from '../core/util';
- import Animator from '../animation/Animator';
- import { REDRAW_BIT, STYLE_CHANGED_BIT } from './constants';
- // type CalculateTextPositionResult = ReturnType<typeof calculateTextPosition>
- const STYLE_MAGIC_KEY = '__zr_style_' + Math.round((Math.random() * 10));
- export interface CommonStyleProps {
- shadowBlur?: number
- shadowOffsetX?: number
- shadowOffsetY?: number
- shadowColor?: string
- opacity?: number
- /**
- * https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation
- */
- blend?: string
- }
- export const DEFAULT_COMMON_STYLE: CommonStyleProps = {
- shadowBlur: 0,
- shadowOffsetX: 0,
- shadowOffsetY: 0,
- shadowColor: '#000',
- opacity: 1,
- blend: 'source-over'
- };
- export const DEFAULT_COMMON_ANIMATION_PROPS: MapToType<DisplayableProps, boolean> = {
- style: {
- shadowBlur: true,
- shadowOffsetX: true,
- shadowOffsetY: true,
- shadowColor: true,
- opacity: true
- }
- };
- (DEFAULT_COMMON_STYLE as any)[STYLE_MAGIC_KEY] = true;
- export interface DisplayableProps extends ElementProps {
- style?: Dictionary<any>
- zlevel?: number
- z?: number
- z2?: number
- culling?: boolean
- // TODO list all cursors
- cursor?: string
- rectHover?: boolean
- progressive?: boolean
- incremental?: boolean
- ignoreCoarsePointer?: boolean
- batch?: boolean
- invisible?: boolean
- }
- type DisplayableKey = keyof DisplayableProps
- type DisplayablePropertyType = PropType<DisplayableProps, DisplayableKey>
- export type DisplayableStatePropNames = ElementStatePropNames | 'style' | 'z' | 'z2' | 'invisible';
- export type DisplayableState = Pick<DisplayableProps, DisplayableStatePropNames> & ElementCommonState;
- const PRIMARY_STATES_KEYS = ['z', 'z2', 'invisible'] as const;
- const PRIMARY_STATES_KEYS_IN_HOVER_LAYER = ['invisible'] as const;
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- interface Displayable<Props extends DisplayableProps = DisplayableProps> {
- animate(key?: '', loop?: boolean): Animator<this>
- animate(key: 'style', loop?: boolean): Animator<this['style']>
- getState(stateName: string): DisplayableState
- ensureState(stateName: string): DisplayableState
- states: Dictionary<DisplayableState>
- stateProxy: (stateName: string) => DisplayableState
- }
- class Displayable<Props extends DisplayableProps = DisplayableProps> extends Element<Props> {
- /**
- * Whether the displayable object is visible. when it is true, the displayable object
- * is not drawn, but the mouse event can still trigger the object.
- */
- invisible: boolean
- z: number
- z2: number
- /**
- * The z level determines the displayable object can be drawn in which layer canvas.
- */
- zlevel: number
- /**
- * If enable culling
- */
- culling: boolean
- /**
- * Mouse cursor when hovered
- */
- cursor: string
- /**
- * If hover area is bounding rect
- */
- rectHover: boolean
- /**
- * For increamental rendering
- */
- incremental: boolean
- /**
- * Never increase to target size
- */
- ignoreCoarsePointer?: boolean
- style: Dictionary<any>
- protected _normalState: DisplayableState
- protected _rect: BoundingRect
- protected _paintRect: BoundingRect
- protected _prevPaintRect: BoundingRect
- dirtyRectTolerance: number
- /************* Properties will be inejected in other modules. *******************/
- // @deprecated.
- useHoverLayer?: boolean
- __hoverStyle?: CommonStyleProps
- // TODO use WeakMap?
- // Shapes for cascade clipping.
- // Can only be `null`/`undefined` or an non-empty array, MUST NOT be an empty array.
- // because it is easy to only using null to check whether clipPaths changed.
- __clipPaths?: Path[]
- // FOR CANVAS PAINTER
- __canvasFillGradient: CanvasGradient
- __canvasStrokeGradient: CanvasGradient
- __canvasFillPattern: CanvasPattern
- __canvasStrokePattern: CanvasPattern
- // FOR SVG PAINTER
- __svgEl: SVGElement
- constructor(props?: Props) {
- super(props);
- }
- protected _init(props?: Props) {
- // Init default properties
- const keysArr = keys(props);
- for (let i = 0; i < keysArr.length; i++) {
- const key = keysArr[i];
- if (key === 'style') {
- this.useStyle(props[key] as Props['style']);
- }
- else {
- super.attrKV(key as any, props[key]);
- }
- }
- // Give a empty style
- if (!this.style) {
- this.useStyle({});
- }
- }
- // Hook provided to developers.
- beforeBrush() {}
- afterBrush() {}
- // Hook provided to inherited classes.
- // Executed between beforeBrush / afterBrush
- innerBeforeBrush() {}
- innerAfterBrush() {}
- shouldBePainted(
- viewWidth: number,
- viewHeight: number,
- considerClipPath: boolean,
- considerAncestors: boolean
- ) {
- const m = this.transform;
- if (
- this.ignore
- // Ignore invisible element
- || this.invisible
- // Ignore transparent element
- || this.style.opacity === 0
- // Ignore culled element
- || (this.culling
- && isDisplayableCulled(this, viewWidth, viewHeight)
- )
- // Ignore scale 0 element, in some environment like node-canvas
- // Draw a scale 0 element can cause all following draw wrong
- // And setTransform with scale 0 will cause set back transform failed.
- || (m && !m[0] && !m[3])
- ) {
- return false;
- }
- if (considerClipPath && this.__clipPaths) {
- for (let i = 0; i < this.__clipPaths.length; ++i) {
- if (this.__clipPaths[i].isZeroArea()) {
- return false;
- }
- }
- }
- if (considerAncestors && this.parent) {
- let parent = this.parent;
- while (parent) {
- if (parent.ignore) {
- return false;
- }
- parent = parent.parent;
- }
- }
- return true;
- }
- /**
- * If displayable element contain coord x, y
- */
- contain(x: number, y: number) {
- return this.rectContain(x, y);
- }
- traverse<Context>(
- cb: (this: Context, el: this) => void,
- context?: Context
- ) {
- cb.call(context, this);
- }
- /**
- * If bounding rect of element contain coord x, y
- */
- rectContain(x: number, y: number) {
- const coord = this.transformCoordToLocal(x, y);
- const rect = this.getBoundingRect();
- return rect.contain(coord[0], coord[1]);
- }
- getPaintRect(): BoundingRect {
- let rect = this._paintRect;
- if (!this._paintRect || this.__dirty) {
- const transform = this.transform;
- const elRect = this.getBoundingRect();
- const style = this.style;
- const shadowSize = style.shadowBlur || 0;
- const shadowOffsetX = style.shadowOffsetX || 0;
- const shadowOffsetY = style.shadowOffsetY || 0;
- rect = this._paintRect || (this._paintRect = new BoundingRect(0, 0, 0, 0));
- if (transform) {
- BoundingRect.applyTransform(rect, elRect, transform);
- }
- else {
- rect.copy(elRect);
- }
- if (shadowSize || shadowOffsetX || shadowOffsetY) {
- rect.width += shadowSize * 2 + Math.abs(shadowOffsetX);
- rect.height += shadowSize * 2 + Math.abs(shadowOffsetY);
- rect.x = Math.min(rect.x, rect.x + shadowOffsetX - shadowSize);
- rect.y = Math.min(rect.y, rect.y + shadowOffsetY - shadowSize);
- }
- // For the accuracy tolerance of text height or line joint point
- const tolerance = this.dirtyRectTolerance;
- if (!rect.isZero()) {
- rect.x = Math.floor(rect.x - tolerance);
- rect.y = Math.floor(rect.y - tolerance);
- rect.width = Math.ceil(rect.width + 1 + tolerance * 2);
- rect.height = Math.ceil(rect.height + 1 + tolerance * 2);
- }
- }
- return rect;
- }
- setPrevPaintRect(paintRect: BoundingRect) {
- if (paintRect) {
- this._prevPaintRect = this._prevPaintRect || new BoundingRect(0, 0, 0, 0);
- this._prevPaintRect.copy(paintRect);
- }
- else {
- this._prevPaintRect = null;
- }
- }
- getPrevPaintRect(): BoundingRect {
- return this._prevPaintRect;
- }
- /**
- * Alias for animate('style')
- * @param loop
- */
- animateStyle(loop: boolean) {
- return this.animate('style', loop);
- }
- // Override updateDuringAnimation
- updateDuringAnimation(targetKey: string) {
- if (targetKey === 'style') {
- this.dirtyStyle();
- }
- else {
- this.markRedraw();
- }
- }
- attrKV(key: DisplayableKey, value: DisplayablePropertyType) {
- if (key !== 'style') {
- super.attrKV(key as keyof DisplayableProps, value);
- }
- else {
- if (!this.style) {
- this.useStyle(value as Dictionary<any>);
- }
- else {
- this.setStyle(value as Dictionary<any>);
- }
- }
- }
- setStyle(obj: Props['style']): this
- setStyle<T extends keyof Props['style']>(obj: T, value: Props['style'][T]): this
- setStyle(keyOrObj: keyof Props['style'] | Props['style'], value?: unknown): this {
- if (typeof keyOrObj === 'string') {
- this.style[keyOrObj] = value;
- }
- else {
- extend(this.style, keyOrObj as Props['style']);
- }
- this.dirtyStyle();
- return this;
- }
- // getDefaultStyleValue<T extends keyof Props['style']>(key: T): Props['style'][T] {
- // // Default value is on the prototype.
- // return this.style.prototype[key];
- // }
- dirtyStyle(notRedraw?: boolean) {
- if (!notRedraw) {
- this.markRedraw();
- }
- this.__dirty |= STYLE_CHANGED_BIT;
- // Clear bounding rect.
- if (this._rect) {
- this._rect = null;
- }
- }
- dirty() {
- this.dirtyStyle();
- }
- /**
- * Is style changed. Used with dirtyStyle.
- */
- styleChanged() {
- return !!(this.__dirty & STYLE_CHANGED_BIT);
- }
- /**
- * Mark style updated. Only useful when style is used for caching. Like in the text.
- */
- styleUpdated() {
- this.__dirty &= ~STYLE_CHANGED_BIT;
- }
- /**
- * Create a style object with default values in it's prototype.
- */
- createStyle(obj?: Props['style']) {
- return createObject(DEFAULT_COMMON_STYLE, obj);
- }
- /**
- * Replace style property.
- * It will create a new style if given obj is not a valid style object.
- */
- // PENDING should not createStyle if it's an style object.
- useStyle(obj: Props['style']) {
- if (!obj[STYLE_MAGIC_KEY]) {
- obj = this.createStyle(obj);
- }
- if (this.__inHover) {
- this.__hoverStyle = obj; // Not affect exists style.
- }
- else {
- this.style = obj;
- }
- this.dirtyStyle();
- }
- /**
- * Determine if an object is a valid style object.
- * Which means it is created by `createStyle.`
- *
- * A valid style object will have all default values in it's prototype.
- * To avoid get null/undefined values.
- */
- isStyleObject(obj: Props['style']) {
- return obj[STYLE_MAGIC_KEY];
- }
- protected _innerSaveToNormal(toState: DisplayableState) {
- super._innerSaveToNormal(toState);
- const normalState = this._normalState;
- if (toState.style && !normalState.style) {
- // Clone style object.
- // TODO: Only save changed style.
- normalState.style = this._mergeStyle(this.createStyle(), this.style);
- }
- this._savePrimaryToNormal(toState, normalState, PRIMARY_STATES_KEYS);
- }
- protected _applyStateObj(
- stateName: string,
- state: DisplayableState,
- normalState: DisplayableState,
- keepCurrentStates: boolean,
- transition: boolean,
- animationCfg: ElementAnimateConfig
- ) {
- super._applyStateObj(stateName, state, normalState, keepCurrentStates, transition, animationCfg);
- const needsRestoreToNormal = !(state && keepCurrentStates);
- let targetStyle: Props['style'];
- if (state && state.style) {
- // Only animate changed properties.
- if (transition) {
- if (keepCurrentStates) {
- targetStyle = state.style;
- }
- else {
- targetStyle = this._mergeStyle(this.createStyle(), normalState.style);
- this._mergeStyle(targetStyle, state.style);
- }
- }
- else {
- targetStyle = this._mergeStyle(
- this.createStyle(),
- keepCurrentStates ? this.style : normalState.style
- );
- this._mergeStyle(targetStyle, state.style);
- }
- }
- else if (needsRestoreToNormal) {
- targetStyle = normalState.style;
- }
- if (targetStyle) {
- if (transition) {
- // Clone a new style. Not affect the original one.
- const sourceStyle = this.style;
- this.style = this.createStyle(needsRestoreToNormal ? {} : sourceStyle);
- // const sourceStyle = this.style = this.createStyle(this.style);
- if (needsRestoreToNormal) {
- const changedKeys = keys(sourceStyle);
- for (let i = 0; i < changedKeys.length; i++) {
- const key = changedKeys[i];
- if (key in targetStyle) { // Not use `key == null` because == null may means no stroke/fill.
- // Pick out from prototype. Or the property won't be animated.
- (targetStyle as any)[key] = targetStyle[key];
- // Omit the property has no default value.
- (this.style as any)[key] = sourceStyle[key];
- }
- }
- }
- // If states is switched twice in ONE FRAME, for example:
- // one property(for example shadowBlur) changed from default value to a specifed value,
- // then switched back in immediately. this.style may don't set this property yet when switching back.
- // It won't treat it as an changed property when switching back. And it won't be animated.
- // So here we make sure the properties will be animated from default value to a specifed value are set.
- const targetKeys = keys(targetStyle);
- for (let i = 0; i < targetKeys.length; i++) {
- const key = targetKeys[i];
- this.style[key] = this.style[key];
- }
- this._transitionState(stateName, {
- style: targetStyle
- } as Props, animationCfg, this.getAnimationStyleProps() as MapToType<Props, boolean>);
- }
- else {
- this.useStyle(targetStyle);
- }
- }
- // Don't change z, z2 for element moved into hover layer.
- // It's not necessary and will cause paint list order changed.
- const statesKeys = this.__inHover ? PRIMARY_STATES_KEYS_IN_HOVER_LAYER : PRIMARY_STATES_KEYS;
- for (let i = 0; i < statesKeys.length; i++) {
- let key = statesKeys[i];
- if (state && state[key] != null) {
- // Replace if it exist in target state
- (this as any)[key] = state[key];
- }
- else if (needsRestoreToNormal) {
- // Restore to normal state
- if (normalState[key] != null) {
- (this as any)[key] = normalState[key];
- }
- }
- }
- }
- protected _mergeStates(states: DisplayableState[]) {
- const mergedState = super._mergeStates(states) as DisplayableState;
- let mergedStyle: Props['style'];
- for (let i = 0; i < states.length; i++) {
- const state = states[i];
- if (state.style) {
- mergedStyle = mergedStyle || {};
- this._mergeStyle(mergedStyle, state.style);
- }
- }
- if (mergedStyle) {
- mergedState.style = mergedStyle;
- }
- return mergedState;
- }
- protected _mergeStyle(
- targetStyle: CommonStyleProps,
- sourceStyle: CommonStyleProps
- ) {
- extend(targetStyle, sourceStyle);
- return targetStyle;
- }
- getAnimationStyleProps() {
- return DEFAULT_COMMON_ANIMATION_PROPS;
- }
- /**
- * The string value of `textPosition` needs to be calculated to a real postion.
- * For example, `'inside'` is calculated to `[rect.width/2, rect.height/2]`
- * by default. See `contain/text.js#calculateTextPosition` for more details.
- * But some coutom shapes like "pin", "flag" have center that is not exactly
- * `[width/2, height/2]`. So we provide this hook to customize the calculation
- * for those shapes. It will be called if the `style.textPosition` is a string.
- * @param out Prepared out object. If not provided, this method should
- * be responsible for creating one.
- * @param style
- * @param rect {x, y, width, height}
- * @return out The same as the input out.
- * {
- * x: number. mandatory.
- * y: number. mandatory.
- * textAlign: string. optional. use style.textAlign by default.
- * textVerticalAlign: string. optional. use style.textVerticalAlign by default.
- * }
- */
- // calculateTextPosition: (out: CalculateTextPositionResult, style: Dictionary<any>, rect: RectLike) => CalculateTextPositionResult
- protected static initDefaultProps = (function () {
- const dispProto = Displayable.prototype;
- dispProto.type = 'displayable';
- dispProto.invisible = false;
- dispProto.z = 0;
- dispProto.z2 = 0;
- dispProto.zlevel = 0;
- dispProto.culling = false;
- dispProto.cursor = 'pointer';
- dispProto.rectHover = false;
- dispProto.incremental = false;
- dispProto._rect = null;
- dispProto.dirtyRectTolerance = 0;
- dispProto.__dirty = REDRAW_BIT | STYLE_CHANGED_BIT;
- })()
- }
- const tmpRect = new BoundingRect(0, 0, 0, 0);
- const viewRect = new BoundingRect(0, 0, 0, 0);
- function isDisplayableCulled(el: Displayable, width: number, height: number) {
- tmpRect.copy(el.getBoundingRect());
- if (el.transform) {
- tmpRect.applyTransform(el.transform);
- }
- viewRect.width = width;
- viewRect.height = height;
- return !tmpRect.intersect(viewRect);
- }
- export default Displayable;
|