-
Notifications
You must be signed in to change notification settings - Fork 1.6k
/
Copy pathDisplayable.ts
635 lines (543 loc) · 19.6 KB
/
Displayable.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
/**
* 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
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
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,
considerHostTarget: 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;
}
}
// consider host target
if (
considerAncestors
&& considerHostTarget
&& this.parent
&& this.parent.__hostTarget
) {
let hostTarget = this.parent.__hostTarget;
let parent = hostTarget;
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;