package.src.graphic.Displayable.ts Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of zrender Show documentation
Show all versions of zrender Show documentation
A lightweight graphic library providing 2d draw for Apache ECharts
The newest version!
/**
* 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
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 = {
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
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
export type DisplayableStatePropNames = ElementStatePropNames | 'style' | 'z' | 'z2' | 'invisible';
export type DisplayableState = Pick & 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 {
animate(key?: '', loop?: boolean): Animator
animate(key: 'style', loop?: boolean): Animator
getState(stateName: string): DisplayableState
ensureState(stateName: string): DisplayableState
states: Dictionary
stateProxy: (stateName: string) => DisplayableState
}
class Displayable extends Element {
/**
* 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
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(
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);
}
else {
this.setStyle(value as Dictionary);
}
}
}
setStyle(obj: Props['style']): this
setStyle(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(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);
}
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, 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;
© 2015 - 2025 Weber Informatics LLC | Privacy Policy