前端VUE重构

This commit is contained in:
lmk
2026-07-10 11:28:16 +08:00
parent 610b85e3a5
commit b1cc63f883
14549 changed files with 2349065 additions and 36103 deletions
+2030
View File
File diff suppressed because it is too large Load Diff
+547
View File
@@ -0,0 +1,547 @@
import * as util from './core/util';
import * as vec2 from './core/vector';
import Draggable from './mixin/Draggable';
import Eventful from './core/Eventful';
import * as eventTool from './core/event';
import {GestureMgr} from './core/GestureMgr';
import Displayable from './graphic/Displayable';
import {PainterBase} from './PainterBase';
import HandlerDomProxy, { HandlerProxyInterface } from './dom/HandlerProxy';
import { ZRRawEvent, ZRPinchEvent, ElementEventName, ElementEventNameWithOn, ZRRawTouchEvent } from './core/types';
import Storage from './Storage';
import Element, {ElementEvent} from './Element';
import CanvasPainter from './canvas/Painter';
import BoundingRect from './core/BoundingRect';
/**
* [The interface between `Handler` and `HandlerProxy`]:
*
* The default `HandlerProxy` only support the common standard web environment
* (e.g., standalone browser, headless browser, embed browser in mobild APP, ...).
* But `HandlerProxy` can be replaced to support more non-standard environment
* (e.g., mini app), or to support more feature that the default `HandlerProxy`
* not provided (like echarts-gl did).
* So the interface between `Handler` and `HandlerProxy` should be stable. Do not
* make break changes util inevitable. The interface include the public methods
* of `Handler` and the events listed in `handlerNames` below, by which `HandlerProxy`
* drives `Handler`.
*/
/**
* [DRAG_OUTSIDE]:
*
* That is, triggering `mousemove` and `mouseup` event when the pointer is out of the
* zrender area when dragging. That is important for the improvement of the user experience
* when dragging something near the boundary without being terminated unexpectedly.
*
* We originally consider to introduce new events like `pagemovemove` and `pagemouseup`
* to resolve this issue. But some drawbacks of it is described in
* https://github.com/ecomfe/zrender/pull/536#issuecomment-560286899
*
* Instead, we referenced the specifications:
* https://www.w3.org/TR/touch-events/#the-touchmove-event
* https://www.w3.org/TR/2014/WD-DOM-Level-3-Events-20140925/#event-type-mousemove
* where the the mousemove/touchmove can be continue to fire if the user began a drag
* operation and the pointer has left the boundary. (for the mouse event, browsers
* only do it on `document` and when the pointer has left the boundary of the browser.)
*
* So the default `HandlerProxy` supports this feature similarly: if it is in the dragging
* state (see `pointerCapture` in `HandlerProxy`), the `mousemove` and `mouseup` continue
* to fire until release the pointer. That is implemented by listen to those event on
* `document`.
* If we implement some other `HandlerProxy` only for touch device, that would be easier.
* The touch event support this feature by default.
* The term "pointer capture" is from the spec:
* https://www.w3.org/TR/pointerevents2/#idl-def-element-setpointercapture-pointerid
*
* Note:
* There might be some cases that the mouse event can not be received on `document`.
* For example,
* (A) When `useCapture` is not supported and some user defined event listeners on the ancestor
* of zr dom throw Error.
* (B) When `useCapture` is not supported and some user defined event listeners on the ancestor of
* zr dom call `stopPropagation`.
* In these cases, the `mousemove` event might be keep triggering event when the mouse is released.
* We try to reduce the side-effect in those cases, that is, use `isOutsideBoundary` to prevent
* it from do anything (especially, `findHover`).
* (`useCapture` mean, `addEvnetListener(listener, {capture: true})`, althought it may not be
* suppported in some environments.)
*
* Note:
* If `HandlerProxy` listens to `document` with `useCapture`, `HandlerProxy` needs to
* prevent user-registered-handler from calling `stopPropagation` and `preventDefault`
* when the `event.target` is not a zrender dom element. Otherwise the user-registered-handler
* may be able to prevent other elements (that not relevant to zrender) in the web page from receiving
* dom events.
*/
const SILENT = 'silent';
function makeEventPacket(eveType: ElementEventName, targetInfo: {
target?: Element
topTarget?: Element
}, event: ZRRawEvent): ElementEvent {
return {
type: eveType,
event: event,
// target can only be an element that is not silent.
target: targetInfo.target,
// topTarget can be a silent element.
topTarget: targetInfo.topTarget,
cancelBubble: false,
offsetX: event.zrX,
offsetY: event.zrY,
gestureEvent: (event as ZRPinchEvent).gestureEvent,
pinchX: (event as ZRPinchEvent).pinchX,
pinchY: (event as ZRPinchEvent).pinchY,
pinchScale: (event as ZRPinchEvent).pinchScale,
wheelDelta: event.zrDelta,
zrByTouch: event.zrByTouch,
which: event.which,
stop: stopEvent
};
}
function stopEvent(this: ElementEvent) {
eventTool.stop(this.event);
}
class EmptyProxy extends Eventful {
handler: Handler = null
dispose() {}
setCursor() {}
}
class HoveredResult {
x: number
y: number
target: Displayable
topTarget: Displayable
constructor(x?: number, y?: number) {
this.x = x;
this.y = y;
}
}
const handlerNames = [
'click', 'dblclick', 'mousewheel', 'mouseout',
'mouseup', 'mousedown', 'mousemove', 'contextmenu'
];
type HandlerName = 'click' |'dblclick' |'mousewheel' |'mouseout' |
'mouseup' |'mousedown' |'mousemove' |'contextmenu';
const tmpRect = new BoundingRect(0, 0, 0, 0);
// TODO draggable
class Handler extends Eventful {
storage: Storage
painter: PainterBase
painterRoot: HTMLElement
proxy: HandlerProxyInterface
private _hovered = new HoveredResult(0, 0)
private _gestureMgr: GestureMgr
private _draggingMgr: Draggable
private _pointerSize: number
_downEl: Element
_upEl: Element
_downPoint: [number, number]
constructor(
storage: Storage,
painter: PainterBase,
proxy: HandlerProxyInterface,
painterRoot: HTMLElement,
pointerSize: number
) {
super();
this.storage = storage;
this.painter = painter;
this.painterRoot = painterRoot;
this._pointerSize = pointerSize;
proxy = proxy || new EmptyProxy();
/**
* Proxy of event. can be Dom, WebGLSurface, etc.
*/
this.proxy = null;
this.setHandlerProxy(proxy);
this._draggingMgr = new Draggable(this);
}
setHandlerProxy(proxy: HandlerProxyInterface) {
if (this.proxy) {
this.proxy.dispose();
}
if (proxy) {
util.each(handlerNames, function (name) {
proxy.on && proxy.on(name, this[name as HandlerName], this);
}, this);
// Attach handler
proxy.handler = this;
}
this.proxy = proxy;
}
mousemove(event: ZRRawEvent) {
const x = event.zrX;
const y = event.zrY;
const isOutside = isOutsideBoundary(this, x, y);
let lastHovered = this._hovered;
let lastHoveredTarget = lastHovered.target;
// If lastHoveredTarget is removed from zr (detected by '__zr') by some API call
// (like 'setOption' or 'dispatchAction') in event handlers, we should find
// lastHovered again here. Otherwise 'mouseout' can not be triggered normally.
// See #6198.
if (lastHoveredTarget && !lastHoveredTarget.__zr) {
lastHovered = this.findHover(lastHovered.x, lastHovered.y);
lastHoveredTarget = lastHovered.target;
}
const hovered = this._hovered = isOutside ? new HoveredResult(x, y) : this.findHover(x, y);
const hoveredTarget = hovered.target;
const proxy = this.proxy;
proxy.setCursor && proxy.setCursor(hoveredTarget ? hoveredTarget.cursor : 'default');
// Mouse out on previous hovered element
if (lastHoveredTarget && hoveredTarget !== lastHoveredTarget) {
this.dispatchToElement(lastHovered, 'mouseout', event);
}
// Mouse moving on one element
this.dispatchToElement(hovered, 'mousemove', event);
// Mouse over on a new element
if (hoveredTarget && hoveredTarget !== lastHoveredTarget) {
this.dispatchToElement(hovered, 'mouseover', event);
}
}
mouseout(event: ZRRawEvent) {
const eventControl = event.zrEventControl;
if (eventControl !== 'only_globalout') {
this.dispatchToElement(this._hovered, 'mouseout', event);
}
if (eventControl !== 'no_globalout') {
// FIXME: if the pointer moving from the extra doms to realy "outside",
// the `globalout` should have been triggered. But currently not.
this.trigger('globalout', {type: 'globalout', event: event});
}
}
/**
* Resize
*/
resize() {
this._hovered = new HoveredResult(0, 0);
}
/**
* Dispatch event
*/
dispatch(eventName: HandlerName, eventArgs?: any) {
const handler = this[eventName];
handler && handler.call(this, eventArgs);
}
/**
* Dispose
*/
dispose() {
this.proxy.dispose();
this.storage = null;
this.proxy = null;
this.painter = null;
}
/**
* 设置默认的cursor style
* @param cursorStyle 例如 crosshair,默认为 'default'
*/
setCursorStyle(cursorStyle: string) {
const proxy = this.proxy;
proxy.setCursor && proxy.setCursor(cursorStyle);
}
/**
* 事件分发代理
*
* @private
* @param {Object} targetInfo {target, topTarget} 目标图形元素
* @param {string} eventName 事件名称
* @param {Object} event 事件对象
*/
dispatchToElement(targetInfo: {
target?: Element
topTarget?: Element
}, eventName: ElementEventName, event: ZRRawEvent) {
targetInfo = targetInfo || {};
let el = targetInfo.target as Element;
if (el && el.silent) {
return;
}
const eventKey = ('on' + eventName) as ElementEventNameWithOn;
const eventPacket = makeEventPacket(eventName, targetInfo, event);
while (el) {
el[eventKey]
&& (eventPacket.cancelBubble = !!el[eventKey].call(el, eventPacket));
el.trigger(eventName, eventPacket);
// Bubble to the host if on the textContent.
// PENDING
el = el.__hostTarget ? el.__hostTarget : el.parent;
if (eventPacket.cancelBubble) {
break;
}
}
if (!eventPacket.cancelBubble) {
// 冒泡到顶级 zrender 对象
this.trigger(eventName, eventPacket);
// 分发事件到用户自定义层
// 用户有可能在全局 click 事件中 dispose,所以需要判断下 painter 是否存在
if (this.painter && (this.painter as CanvasPainter).eachOtherLayer) {
(this.painter as CanvasPainter).eachOtherLayer(function (layer) {
if (typeof (layer[eventKey]) === 'function') {
layer[eventKey].call(layer, eventPacket);
}
if (layer.trigger) {
layer.trigger(eventName, eventPacket);
}
});
}
}
}
findHover(x: number, y: number, exclude?: Displayable): HoveredResult {
const list = this.storage.getDisplayList();
const out = new HoveredResult(x, y);
setHoverTarget(list, out, x, y, exclude);
if (this._pointerSize && !out.target) {
/**
* If no element at pointer position, check intersection with
* elements with pointer enlarged by target size.
*/
const candidates: Displayable[] = [];
const pointerSize = this._pointerSize;
const targetSizeHalf = pointerSize / 2;
const pointerRect = new BoundingRect(x - targetSizeHalf, y - targetSizeHalf, pointerSize, pointerSize);
for (let i = list.length - 1; i >= 0; i--) {
const el = list[i];
if (el !== exclude
&& !el.ignore
&& !el.ignoreCoarsePointer
// If an element ignores, its textContent should also ignore.
// TSpan's parent is not a Group but a ZRText.
// See Text.js _getOrCreateChild
&& (!el.parent || !(el.parent as any).ignoreCoarsePointer)
) {
tmpRect.copy(el.getBoundingRect());
if (el.transform) {
tmpRect.applyTransform(el.transform);
}
if (tmpRect.intersect(pointerRect)) {
candidates.push(el);
}
}
}
/**
* If there are elements whose bounding boxes are near the pointer,
* use the most top one that intersects with the enlarged pointer.
*/
if (candidates.length) {
const rStep = 4;
const thetaStep = Math.PI / 12;
const PI2 = Math.PI * 2;
for (let r = 0; r < targetSizeHalf; r += rStep) {
for (let theta = 0; theta < PI2; theta += thetaStep) {
const x1 = x + r * Math.cos(theta);
const y1 = y + r * Math.sin(theta);
setHoverTarget(candidates, out, x1, y1, exclude);
if (out.target) {
return out;
}
}
}
}
}
return out;
}
processGesture(event: ZRRawEvent, stage?: 'start' | 'end' | 'change') {
if (!this._gestureMgr) {
this._gestureMgr = new GestureMgr();
}
const gestureMgr = this._gestureMgr;
stage === 'start' && gestureMgr.clear();
const gestureInfo = gestureMgr.recognize(
event as ZRRawTouchEvent,
this.findHover(event.zrX, event.zrY, null).target,
(this.proxy as HandlerDomProxy).dom
);
stage === 'end' && gestureMgr.clear();
// Do not do any preventDefault here. Upper application do that if necessary.
if (gestureInfo) {
const type = gestureInfo.type;
(event as ZRPinchEvent).gestureEvent = type;
let res = new HoveredResult();
res.target = gestureInfo.target;
this.dispatchToElement(res, type as ElementEventName, gestureInfo.event as ZRRawEvent);
}
}
click: (event: ZRRawEvent) => void
mousedown: (event: ZRRawEvent) => void
mouseup: (event: ZRRawEvent) => void
mousewheel: (event: ZRRawEvent) => void
dblclick: (event: ZRRawEvent) => void
contextmenu: (event: ZRRawEvent) => void
}
// Common handlers
util.each(['click', 'mousedown', 'mouseup', 'mousewheel', 'dblclick', 'contextmenu'], function (name: HandlerName) {
Handler.prototype[name] = function (event) {
const x = event.zrX;
const y = event.zrY;
const isOutside = isOutsideBoundary(this, x, y);
let hovered;
let hoveredTarget;
if (name !== 'mouseup' || !isOutside) {
// Find hover again to avoid click event is dispatched manually. Or click is triggered without mouseover
hovered = this.findHover(x, y);
hoveredTarget = hovered.target;
}
if (name === 'mousedown') {
this._downEl = hoveredTarget;
this._downPoint = [event.zrX, event.zrY];
// In case click triggered before mouseup
this._upEl = hoveredTarget;
}
else if (name === 'mouseup') {
this._upEl = hoveredTarget;
}
else if (name === 'click') {
if (this._downEl !== this._upEl
// Original click event is triggered on the whole canvas element,
// including the case that `mousedown` - `mousemove` - `mouseup`,
// which should be filtered, otherwise it will bring trouble to
// pan and zoom.
|| !this._downPoint
// Arbitrary value
|| vec2.dist(this._downPoint, [event.zrX, event.zrY]) > 4
) {
return;
}
this._downPoint = null;
}
this.dispatchToElement(hovered, name, event);
};
});
function isHover(displayable: Displayable, x: number, y: number) {
if (displayable[displayable.rectHover ? 'rectContain' : 'contain'](x, y)) {
let el: Element = displayable;
let isSilent;
let ignoreClip = false;
while (el) {
// Ignore clip on any ancestors.
if (el.ignoreClip) {
ignoreClip = true;
}
if (!ignoreClip) {
let clipPath = el.getClipPath();
// If clipped by ancestor.
// FIXME: If clipPath has neither stroke nor fill,
// el.clipPath.contain(x, y) will always return false.
if (clipPath && !clipPath.contain(x, y)) {
return false;
}
}
if (el.silent) {
isSilent = true;
}
// Consider when el is textContent, also need to be silent
// if any of its host el and its ancestors is silent.
const hostEl = el.__hostTarget;
el = hostEl ? hostEl : el.parent;
}
return isSilent ? SILENT : true;
}
return false;
}
function setHoverTarget(
list: Displayable[],
out: HoveredResult,
x: number,
y: number,
exclude: Displayable
) {
for (let i = list.length - 1; i >= 0; i--) {
const el = list[i];
let hoverCheckResult;
if (el !== exclude
// getDisplayList may include ignored item in VML mode
&& !el.ignore
&& (hoverCheckResult = isHover(el, x, y))
) {
!out.topTarget && (out.topTarget = el);
if (hoverCheckResult !== SILENT) {
out.target = el;
break;
}
}
}
}
/**
* See [DRAG_OUTSIDE].
*/
function isOutsideBoundary(handlerInstance: Handler, x: number, y: number) {
const painter = handlerInstance.painter;
return x < 0 || x > painter.getWidth() || y < 0 || y > painter.getHeight();
}
export default Handler;
+42
View File
@@ -0,0 +1,42 @@
import { GradientObject } from './graphic/Gradient';
import { PatternObject } from './graphic/Pattern';
import { Dictionary } from './core/types';
// interface PainterOption {
// width?: number | string // Can be 10 / 10px / auto
// height?: number | string
// }
export interface PainterBase {
type: string
// root will be undefined if ssr is true
root?: HTMLElement
// If ssr only
ssrOnly?: boolean
// constructor(dom: HTMLElement, storage: Storage, opts: PainterOption, id: number): void
resize(width?: number | string, height?: number | string): void
refresh(): void
clear(): void
// must be given if ssr is true.
renderToString?(): string;
getType: () => string
getWidth(): number
getHeight(): number
dispose(): void
getViewportRoot: () => HTMLElement
getViewportRootOffset: () => {offsetLeft: number, offsetTop: number}
refreshHover(): void
configLayer(zlevel: number, config: Dictionary<any>): void
setBackgroundColor(backgroundColor: string | GradientObject | PatternObject): void
}
+243
View File
@@ -0,0 +1,243 @@
import * as util from './core/util';
import Group, { GroupLike } from './graphic/Group';
import Element from './Element';
// Use timsort because in most case elements are partially sorted
// https://jsfiddle.net/pissang/jr4x7mdm/8/
import timsort from './core/timsort';
import Displayable from './graphic/Displayable';
import Path from './graphic/Path';
import { REDRAW_BIT } from './graphic/constants';
let invalidZErrorLogged = false;
function logInvalidZError() {
if (invalidZErrorLogged) {
return;
}
invalidZErrorLogged = true;
console.warn('z / z2 / zlevel of displayable is invalid, which may cause unexpected errors');
}
function shapeCompareFunc(a: Displayable, b: Displayable) {
if (a.zlevel === b.zlevel) {
if (a.z === b.z) {
return a.z2 - b.z2;
}
return a.z - b.z;
}
return a.zlevel - b.zlevel;
}
export default class Storage {
private _roots: Element[] = []
private _displayList: Displayable[] = []
private _displayListLen = 0
traverse<T>(
cb: (this: T, el: Element) => void,
context?: T
) {
for (let i = 0; i < this._roots.length; i++) {
this._roots[i].traverse(cb, context);
}
}
/**
* get a list of elements to be rendered
*
* @param {boolean} update whether to update elements before return
* @param {DisplayParams} params options
* @return {Displayable[]} a list of elements
*/
getDisplayList(update?: boolean, includeIgnore?: boolean): Displayable[] {
includeIgnore = includeIgnore || false;
const displayList = this._displayList;
// If displaylist is not created yet. Update force
if (update || !displayList.length) {
this.updateDisplayList(includeIgnore);
}
return displayList;
}
/**
* 更新图形的绘制队列。
* 每次绘制前都会调用,该方法会先深度优先遍历整个树,更新所有Group和Shape的变换并且把所有可见的Shape保存到数组中,
* 最后根据绘制的优先级(zlevel > z > 插入顺序)排序得到绘制队列
*/
updateDisplayList(includeIgnore?: boolean) {
this._displayListLen = 0;
const roots = this._roots;
const displayList = this._displayList;
for (let i = 0, len = roots.length; i < len; i++) {
this._updateAndAddDisplayable(roots[i], null, includeIgnore);
}
displayList.length = this._displayListLen;
timsort(displayList, shapeCompareFunc);
}
private _updateAndAddDisplayable(
el: Element,
clipPaths: Path[],
includeIgnore?: boolean
) {
if (el.ignore && !includeIgnore) {
return;
}
el.beforeUpdate();
el.update();
el.afterUpdate();
const userSetClipPath = el.getClipPath();
if (el.ignoreClip) {
clipPaths = null;
}
else if (userSetClipPath) {
// FIXME 效率影响
if (clipPaths) {
clipPaths = clipPaths.slice();
}
else {
clipPaths = [];
}
let currentClipPath = userSetClipPath;
let parentClipPath = el;
// Recursively add clip path
while (currentClipPath) {
// clipPath 的变换是基于使用这个 clipPath 的元素
// TODO: parent should be group type.
currentClipPath.parent = parentClipPath as Group;
currentClipPath.updateTransform();
clipPaths.push(currentClipPath);
parentClipPath = currentClipPath;
currentClipPath = currentClipPath.getClipPath();
}
}
// ZRText and Group and combining morphing Path may use children
if ((el as GroupLike).childrenRef) {
const children = (el as GroupLike).childrenRef();
for (let i = 0; i < children.length; i++) {
const child = children[i];
// Force to mark as dirty if group is dirty
if (el.__dirty) {
child.__dirty |= REDRAW_BIT;
}
this._updateAndAddDisplayable(child, clipPaths, includeIgnore);
}
// Mark group clean here
el.__dirty = 0;
}
else {
const disp = el as Displayable;
// Element is displayable
if (clipPaths && clipPaths.length) {
disp.__clipPaths = clipPaths;
}
else if (disp.__clipPaths && disp.__clipPaths.length > 0) {
disp.__clipPaths = [];
}
// Avoid invalid z, z2, zlevel cause sorting error.
if (isNaN(disp.z)) {
logInvalidZError();
disp.z = 0;
}
if (isNaN(disp.z2)) {
logInvalidZError();
disp.z2 = 0;
}
if (isNaN(disp.zlevel)) {
logInvalidZError();
disp.zlevel = 0;
}
this._displayList[this._displayListLen++] = disp;
}
// Add decal
const decalEl = (el as Path).getDecalElement && (el as Path).getDecalElement();
if (decalEl) {
this._updateAndAddDisplayable(decalEl, clipPaths, includeIgnore);
}
// Add attached text element and guide line.
const textGuide = el.getTextGuideLine();
if (textGuide) {
this._updateAndAddDisplayable(textGuide, clipPaths, includeIgnore);
}
const textEl = el.getTextContent();
if (textEl) {
this._updateAndAddDisplayable(textEl, clipPaths, includeIgnore);
}
}
/**
* 添加图形(Displayable)或者组(Group)到根节点
*/
addRoot(el: Element) {
if (el.__zr && el.__zr.storage === this) {
return;
}
this._roots.push(el);
}
/**
* 删除指定的图形(Displayable)或者组(Group)
* @param el
*/
delRoot(el: Element | Element[]) {
if (el instanceof Array) {
for (let i = 0, l = el.length; i < l; i++) {
this.delRoot(el[i]);
}
return;
}
const idx = util.indexOf(this._roots, el);
if (idx >= 0) {
this._roots.splice(idx, 1);
}
}
delAllRoots() {
this._roots = [];
this._displayList = [];
this._displayListLen = 0;
return;
}
getRoots() {
return this._roots;
}
/**
* 清空并且释放Storage
*/
dispose() {
this._displayList = null;
this._roots = null;
}
displayableSortFunc = shapeCompareFunc
}
+8
View File
@@ -0,0 +1,8 @@
export * from './zrender';
export * from './export';
import {registerPainter} from './zrender';
import CanvasPainter from './canvas/Painter';
import SVGPainter from './svg/Painter';
registerPainter('canvas', CanvasPainter);
registerPainter('svg', SVGPainter);
+267
View File
@@ -0,0 +1,267 @@
/**
* Animation main class, dispatch and manage all animation controllers
*
*/
// TODO Additive animation
// http://iosoteric.com/additive-animations-animatewithduration-in-ios-8/
// https://developer.apple.com/videos/wwdc2014/#236
import Eventful from '../core/Eventful';
import requestAnimationFrame from './requestAnimationFrame';
import Animator from './Animator';
import Clip from './Clip';
export function getTime() {
return new Date().getTime();
}
interface Stage {
update?: () => void
}
interface AnimationOption {
stage?: Stage
}
/**
* @example
* const animation = new Animation();
* const obj = {
* x: 100,
* y: 100
* };
* animation.animate(node.position)
* .when(1000, {
* x: 500,
* y: 500
* })
* .when(2000, {
* x: 100,
* y: 100
* })
* .start();
*/
export default class Animation extends Eventful {
stage: Stage
// Use linked list to store clip
private _head: Clip
private _tail: Clip
private _running = false
private _time = 0
private _pausedTime = 0
private _pauseStart = 0
private _paused = false;
constructor(opts?: AnimationOption) {
super();
opts = opts || {};
this.stage = opts.stage || {};
}
/**
* Add clip
*/
addClip(clip: Clip) {
if (clip.animation) {
// Clip has been added
this.removeClip(clip);
}
if (!this._head) {
this._head = this._tail = clip;
}
else {
this._tail.next = clip;
clip.prev = this._tail;
clip.next = null;
this._tail = clip;
}
clip.animation = this;
}
/**
* Add animator
*/
addAnimator(animator: Animator<any>) {
animator.animation = this;
const clip = animator.getClip();
if (clip) {
this.addClip(clip);
}
}
/**
* Delete animation clip
*/
removeClip(clip: Clip) {
if (!clip.animation) {
return;
}
const prev = clip.prev;
const next = clip.next;
if (prev) {
prev.next = next;
}
else {
// Is head
this._head = next;
}
if (next) {
next.prev = prev;
}
else {
// Is tail
this._tail = prev;
}
clip.next = clip.prev = clip.animation = null;
}
/**
* Delete animation clip
*/
removeAnimator(animator: Animator<any>) {
const clip = animator.getClip();
if (clip) {
this.removeClip(clip);
}
animator.animation = null;
}
update(notTriggerFrameAndStageUpdate?: boolean) {
const time = getTime() - this._pausedTime;
const delta = time - this._time;
let clip = this._head;
while (clip) {
// Save the nextClip before step.
// So the loop will not been affected if the clip is removed in the callback
const nextClip = clip.next;
let finished = clip.step(time, delta);
if (finished) {
clip.ondestroy();
this.removeClip(clip);
clip = nextClip;
}
else {
clip = nextClip;
}
}
this._time = time;
if (!notTriggerFrameAndStageUpdate) {
// 'frame' should be triggered before stage, because upper application
// depends on the sequence (e.g., echarts-stream and finish
// event judge)
this.trigger('frame', delta);
this.stage.update && this.stage.update();
}
}
_startLoop() {
const self = this;
this._running = true;
function step() {
if (self._running) {
requestAnimationFrame(step);
!self._paused && self.update();
}
}
requestAnimationFrame(step);
}
/**
* Start animation.
*/
start() {
if (this._running) {
return;
}
this._time = getTime();
this._pausedTime = 0;
this._startLoop();
}
/**
* Stop animation.
*/
stop() {
this._running = false;
}
/**
* Pause animation.
*/
pause() {
if (!this._paused) {
this._pauseStart = getTime();
this._paused = true;
}
}
/**
* Resume animation.
*/
resume() {
if (this._paused) {
this._pausedTime += getTime() - this._pauseStart;
this._paused = false;
}
}
/**
* Clear animation.
*/
clear() {
let clip = this._head;
while (clip) {
let nextClip = clip.next;
clip.prev = clip.next = clip.animation = null;
clip = nextClip;
}
this._head = this._tail = null;
}
/**
* Whether animation finished.
*/
isFinished() {
return this._head == null;
}
/**
* Creat animator for a target, whose props can be animated.
*/
// TODO Gap
animate<T>(target: T, options: {
loop?: boolean // Whether loop animation
}) {
options = options || {};
// Start animation loop
this.start();
const animator = new Animator(
target,
options.loop
);
this.addAnimator(animator);
return animator;
}
}
File diff suppressed because it is too large Load Diff
+142
View File
@@ -0,0 +1,142 @@
/**
* 动画主控制器
* @config target 动画对象,可以是数组,如果是数组的话会批量分发onframe等事件
* @config life(1000) 动画时长
* @config delay(0) 动画延迟时间
* @config loop(true)
* @config onframe
* @config easing(optional)
* @config ondestroy(optional)
* @config onrestart(optional)
*
* TODO pause
*/
import easingFuncs, {AnimationEasing} from './easing';
import type Animation from './Animation';
import { isFunction, noop } from '../core/util';
import { createCubicEasingFunc } from './cubicEasing';
type OnframeCallback = (percent: number) => void;
type ondestroyCallback = () => void
type onrestartCallback = () => void
export type DeferredEventTypes = 'destroy' | 'restart'
// type DeferredEventKeys = 'ondestroy' | 'onrestart'
export interface ClipProps {
life?: number
delay?: number
loop?: boolean
easing?: AnimationEasing
onframe?: OnframeCallback
ondestroy?: ondestroyCallback
onrestart?: onrestartCallback
}
export default class Clip {
private _life: number
private _delay: number
private _inited: boolean = false
private _startTime = 0 // 开始时间单位毫秒
private _pausedTime = 0
private _paused = false
animation: Animation
loop: boolean
easing: AnimationEasing
easingFunc: (p: number) => number
// For linked list. Readonly
next: Clip
prev: Clip
onframe: OnframeCallback
ondestroy: ondestroyCallback
onrestart: onrestartCallback
constructor(opts: ClipProps) {
this._life = opts.life || 1000;
this._delay = opts.delay || 0;
this.loop = opts.loop || false;
this.onframe = opts.onframe || noop;
this.ondestroy = opts.ondestroy || noop;
this.onrestart = opts.onrestart || noop;
opts.easing && this.setEasing(opts.easing);
}
step(globalTime: number, deltaTime: number): boolean {
// Set startTime on first step, or _startTime may has milleseconds different between clips
// PENDING
if (!this._inited) {
this._startTime = globalTime + this._delay;
this._inited = true;
}
if (this._paused) {
this._pausedTime += deltaTime;
return;
}
const life = this._life;
let elapsedTime = globalTime - this._startTime - this._pausedTime;
let percent = elapsedTime / life;
// PENDING: Not begin yet. Still run the loop.
// In the case callback needs to be invoked.
// Or want to update to the begin state at next frame when `setToFinal` and `delay` are both used.
// To avoid the unexpected blink.
if (percent < 0) {
percent = 0;
}
percent = Math.min(percent, 1);
const easingFunc = this.easingFunc;
const schedule = easingFunc ? easingFunc(percent) : percent;
this.onframe(schedule);
// 结束
if (percent === 1) {
if (this.loop) {
// Restart
const remainder = elapsedTime % life;
this._startTime = globalTime - remainder;
this._pausedTime = 0;
this.onrestart();
}
else {
return true;
}
}
return false;
}
pause() {
this._paused = true;
}
resume() {
this._paused = false;
}
setEasing(easing: AnimationEasing) {
this.easing = easing;
this.easingFunc = isFunction(easing)
? easing
: easingFuncs[easing] || createCubicEasingFunc(easing);
}
}
+27
View File
@@ -0,0 +1,27 @@
import { cubicAt, cubicRootAt } from '../core/curve';
import { trim } from '../core/util';
const regexp = /cubic-bezier\(([0-9,\.e ]+)\)/;
export function createCubicEasingFunc(cubicEasingStr: string) {
const cubic = cubicEasingStr && regexp.exec(cubicEasingStr);
if (cubic) {
const points = cubic[1].split(',');
const a = +trim(points[0]);
const b = +trim(points[1]);
const c = +trim(points[2]);
const d = +trim(points[3]);
if (isNaN(a + b + c + d)) {
return;
}
const roots: number[] = [];
return (p: number) => {
return p <= 0
? 0 : p >= 1
? 1
: cubicRootAt(0, a, c, 1, p, roots) && cubicAt(0, b, d, 1, roots[0]);
};
}
}
+351
View File
@@ -0,0 +1,351 @@
/**
* 缓动代码来自 https://github.com/sole/tween.js/blob/master/src/Tween.js
* @see http://sole.github.io/tween.js/examples/03_graphs.html
* @exports zrender/animation/easing
*/
type easingFunc = (percent: number) => number;
export type AnimationEasing = keyof typeof easingFuncs | easingFunc;
const easingFuncs = {
/**
* @param {number} k
* @return {number}
*/
linear(k: number) {
return k;
},
/**
* @param {number} k
* @return {number}
*/
quadraticIn(k: number) {
return k * k;
},
/**
* @param {number} k
* @return {number}
*/
quadraticOut(k: number) {
return k * (2 - k);
},
/**
* @param {number} k
* @return {number}
*/
quadraticInOut(k: number) {
if ((k *= 2) < 1) {
return 0.5 * k * k;
}
return -0.5 * (--k * (k - 2) - 1);
},
// 三次方的缓动(t^3
/**
* @param {number} k
* @return {number}
*/
cubicIn(k: number) {
return k * k * k;
},
/**
* @param {number} k
* @return {number}
*/
cubicOut(k: number) {
return --k * k * k + 1;
},
/**
* @param {number} k
* @return {number}
*/
cubicInOut(k: number) {
if ((k *= 2) < 1) {
return 0.5 * k * k * k;
}
return 0.5 * ((k -= 2) * k * k + 2);
},
// 四次方的缓动(t^4
/**
* @param {number} k
* @return {number}
*/
quarticIn(k: number) {
return k * k * k * k;
},
/**
* @param {number} k
* @return {number}
*/
quarticOut(k: number) {
return 1 - (--k * k * k * k);
},
/**
* @param {number} k
* @return {number}
*/
quarticInOut(k: number) {
if ((k *= 2) < 1) {
return 0.5 * k * k * k * k;
}
return -0.5 * ((k -= 2) * k * k * k - 2);
},
// 五次方的缓动(t^5
/**
* @param {number} k
* @return {number}
*/
quinticIn(k: number) {
return k * k * k * k * k;
},
/**
* @param {number} k
* @return {number}
*/
quinticOut(k: number) {
return --k * k * k * k * k + 1;
},
/**
* @param {number} k
* @return {number}
*/
quinticInOut(k: number) {
if ((k *= 2) < 1) {
return 0.5 * k * k * k * k * k;
}
return 0.5 * ((k -= 2) * k * k * k * k + 2);
},
// 正弦曲线的缓动(sin(t)
/**
* @param {number} k
* @return {number}
*/
sinusoidalIn(k: number) {
return 1 - Math.cos(k * Math.PI / 2);
},
/**
* @param {number} k
* @return {number}
*/
sinusoidalOut(k: number) {
return Math.sin(k * Math.PI / 2);
},
/**
* @param {number} k
* @return {number}
*/
sinusoidalInOut(k: number) {
return 0.5 * (1 - Math.cos(Math.PI * k));
},
// 指数曲线的缓动(2^t
/**
* @param {number} k
* @return {number}
*/
exponentialIn(k: number) {
return k === 0 ? 0 : Math.pow(1024, k - 1);
},
/**
* @param {number} k
* @return {number}
*/
exponentialOut(k: number) {
return k === 1 ? 1 : 1 - Math.pow(2, -10 * k);
},
/**
* @param {number} k
* @return {number}
*/
exponentialInOut(k: number) {
if (k === 0) {
return 0;
}
if (k === 1) {
return 1;
}
if ((k *= 2) < 1) {
return 0.5 * Math.pow(1024, k - 1);
}
return 0.5 * (-Math.pow(2, -10 * (k - 1)) + 2);
},
// 圆形曲线的缓动(sqrt(1-t^2)
/**
* @param {number} k
* @return {number}
*/
circularIn(k: number) {
return 1 - Math.sqrt(1 - k * k);
},
/**
* @param {number} k
* @return {number}
*/
circularOut(k: number) {
return Math.sqrt(1 - (--k * k));
},
/**
* @param {number} k
* @return {number}
*/
circularInOut(k: number) {
if ((k *= 2) < 1) {
return -0.5 * (Math.sqrt(1 - k * k) - 1);
}
return 0.5 * (Math.sqrt(1 - (k -= 2) * k) + 1);
},
// 创建类似于弹簧在停止前来回振荡的动画
/**
* @param {number} k
* @return {number}
*/
elasticIn(k: number) {
let s;
let a = 0.1;
let p = 0.4;
if (k === 0) {
return 0;
}
if (k === 1) {
return 1;
}
if (!a || a < 1) {
a = 1;
s = p / 4;
}
else {
s = p * Math.asin(1 / a) / (2 * Math.PI);
}
return -(a * Math.pow(2, 10 * (k -= 1))
* Math.sin((k - s) * (2 * Math.PI) / p));
},
/**
* @param {number} k
* @return {number}
*/
elasticOut(k: number) {
let s;
let a = 0.1;
let p = 0.4;
if (k === 0) {
return 0;
}
if (k === 1) {
return 1;
}
if (!a || a < 1) {
a = 1;
s = p / 4;
}
else {
s = p * Math.asin(1 / a) / (2 * Math.PI);
}
return (a * Math.pow(2, -10 * k)
* Math.sin((k - s) * (2 * Math.PI) / p) + 1);
},
/**
* @param {number} k
* @return {number}
*/
elasticInOut(k: number) {
let s;
let a = 0.1;
let p = 0.4;
if (k === 0) {
return 0;
}
if (k === 1) {
return 1;
}
if (!a || a < 1) {
a = 1;
s = p / 4;
}
else {
s = p * Math.asin(1 / a) / (2 * Math.PI);
}
if ((k *= 2) < 1) {
return -0.5 * (a * Math.pow(2, 10 * (k -= 1))
* Math.sin((k - s) * (2 * Math.PI) / p));
}
return a * Math.pow(2, -10 * (k -= 1))
* Math.sin((k - s) * (2 * Math.PI) / p) * 0.5 + 1;
},
// 在某一动画开始沿指示的路径进行动画处理前稍稍收回该动画的移动
/**
* @param {number} k
* @return {number}
*/
backIn(k: number) {
let s = 1.70158;
return k * k * ((s + 1) * k - s);
},
/**
* @param {number} k
* @return {number}
*/
backOut(k: number) {
let s = 1.70158;
return --k * k * ((s + 1) * k + s) + 1;
},
/**
* @param {number} k
* @return {number}
*/
backInOut(k: number) {
let s = 1.70158 * 1.525;
if ((k *= 2) < 1) {
return 0.5 * (k * k * ((s + 1) * k - s));
}
return 0.5 * ((k -= 2) * k * ((s + 1) * k + s) + 2);
},
// 创建弹跳效果
/**
* @param {number} k
* @return {number}
*/
bounceIn(k: number) {
return 1 - easingFuncs.bounceOut(1 - k);
},
/**
* @param {number} k
* @return {number}
*/
bounceOut(k: number) {
if (k < (1 / 2.75)) {
return 7.5625 * k * k;
}
else if (k < (2 / 2.75)) {
return 7.5625 * (k -= (1.5 / 2.75)) * k + 0.75;
}
else if (k < (2.5 / 2.75)) {
return 7.5625 * (k -= (2.25 / 2.75)) * k + 0.9375;
}
else {
return 7.5625 * (k -= (2.625 / 2.75)) * k + 0.984375;
}
},
/**
* @param {number} k
* @return {number}
*/
bounceInOut(k: number) {
if (k < 0.5) {
return easingFuncs.bounceIn(k * 2) * 0.5;
}
return easingFuncs.bounceOut(k * 2 - 1) * 0.5 + 0.5;
}
};
export default easingFuncs;
+21
View File
@@ -0,0 +1,21 @@
import env from '../core/env';
type RequestAnimationFrameType = typeof window.requestAnimationFrame
let requestAnimationFrame: RequestAnimationFrameType;
requestAnimationFrame = (
env.hasGlobalWindow
&& (
(window.requestAnimationFrame && window.requestAnimationFrame.bind(window))
// https://github.com/ecomfe/zrender/issues/189#issuecomment-224919809
|| ((window as any).msRequestAnimationFrame && (window as any).msRequestAnimationFrame.bind(window))
|| (window as any).mozRequestAnimationFrame
// @ts-ignore
|| window.webkitRequestAnimationFrame
)
) || function (func: Parameters<RequestAnimationFrameType>[0]): number {
return setTimeout(func, 16) as any;
};
export default requestAnimationFrame;
+511
View File
@@ -0,0 +1,511 @@
import * as util from '../core/util';
import {devicePixelRatio} from '../config';
import { ImagePatternObject } from '../graphic/Pattern';
import CanvasPainter from './Painter';
import { GradientObject, InnerGradientObject } from '../graphic/Gradient';
import { ZRCanvasRenderingContext } from '../core/types';
import Eventful from '../core/Eventful';
import { ElementEventCallback } from '../Element';
import { getCanvasGradient } from './helper';
import { createCanvasPattern } from './graphic';
import Displayable from '../graphic/Displayable';
import BoundingRect from '../core/BoundingRect';
import { REDRAW_BIT } from '../graphic/constants';
import { platformApi } from '../core/platform';
function createDom(id: string, painter: CanvasPainter, dpr: number) {
const newDom = platformApi.createCanvas();
const width = painter.getWidth();
const height = painter.getHeight();
const newDomStyle = newDom.style;
if (newDomStyle) { // In node or some other non-browser environment
newDomStyle.position = 'absolute';
newDomStyle.left = '0';
newDomStyle.top = '0';
newDomStyle.width = width + 'px';
newDomStyle.height = height + 'px';
newDom.setAttribute('data-zr-dom-id', id);
}
newDom.width = width * dpr;
newDom.height = height * dpr;
return newDom;
}
export interface LayerConfig {
// 每次清空画布的颜色
clearColor?: string | GradientObject | ImagePatternObject
// 是否开启动态模糊
motionBlur?: boolean
// 在开启动态模糊的时候使用,与上一帧混合的alpha值,值越大尾迹越明显
lastFrameAlpha?: number
};
export default class Layer extends Eventful {
id: string
dom: HTMLCanvasElement
domBack: HTMLCanvasElement
ctx: CanvasRenderingContext2D
ctxBack: CanvasRenderingContext2D
painter: CanvasPainter
// Configs
/**
* 每次清空画布的颜色
*/
clearColor: string | GradientObject | ImagePatternObject
/**
* 是否开启动态模糊
*/
motionBlur = false
/**
* 在开启动态模糊的时候使用,与上一帧混合的alpha值,值越大尾迹越明显
*/
lastFrameAlpha = 0.7
/**
* Layer dpr
*/
dpr = 1
/**
* Virtual layer will not be inserted into dom.
*/
virtual = false
config = {}
incremental = false
zlevel = 0
maxRepaintRectCount = 5
private _paintRects: BoundingRect[]
__dirty = true
__firstTimePaint = true
__used = false
__drawIndex = 0
__startIndex = 0
__endIndex = 0
// indices in the previous frame
__prevStartIndex: number = null
__prevEndIndex: number = null
__builtin__: boolean
constructor(id: string | HTMLCanvasElement, painter: CanvasPainter, dpr?: number) {
super();
let dom;
dpr = dpr || devicePixelRatio;
if (typeof id === 'string') {
dom = createDom(id, painter, dpr);
}
// Not using isDom because in node it will return false
else if (util.isObject(id)) {
dom = id;
id = dom.id;
}
this.id = id as string;
this.dom = dom;
const domStyle = dom.style;
if (domStyle) { // Not in node
util.disableUserSelect(dom);
dom.onselectstart = () => false;
domStyle.padding = '0';
domStyle.margin = '0';
domStyle.borderWidth = '0';
}
this.painter = painter;
this.dpr = dpr;
}
getElementCount() {
return this.__endIndex - this.__startIndex;
}
afterBrush() {
this.__prevStartIndex = this.__startIndex;
this.__prevEndIndex = this.__endIndex;
}
initContext() {
this.ctx = this.dom.getContext('2d');
(this.ctx as ZRCanvasRenderingContext).dpr = this.dpr;
}
setUnpainted() {
this.__firstTimePaint = true;
}
createBackBuffer() {
const dpr = this.dpr;
this.domBack = createDom('back-' + this.id, this.painter, dpr);
this.ctxBack = this.domBack.getContext('2d');
if (dpr !== 1) {
this.ctxBack.scale(dpr, dpr);
}
}
/**
* Create repaint list when using dirty rect rendering.
*
* @param displayList current rendering list
* @param prevList last frame rendering list
* @return repaint rects. null for the first frame, [] for no element dirty
*/
createRepaintRects(
displayList: Displayable[],
prevList: Displayable[],
viewWidth: number,
viewHeight: number
) {
if (this.__firstTimePaint) {
this.__firstTimePaint = false;
return null;
}
const mergedRepaintRects: BoundingRect[] = [];
const maxRepaintRectCount = this.maxRepaintRectCount;
let full = false;
const pendingRect = new BoundingRect(0, 0, 0, 0);
function addRectToMergePool(rect: BoundingRect) {
if (!rect.isFinite() || rect.isZero()) {
return;
}
if (mergedRepaintRects.length === 0) {
// First rect, create new merged rect
const boundingRect = new BoundingRect(0, 0, 0, 0);
boundingRect.copy(rect);
mergedRepaintRects.push(boundingRect);
}
else {
let isMerged = false;
let minDeltaArea = Infinity;
let bestRectToMergeIdx = 0;
for (let i = 0; i < mergedRepaintRects.length; ++i) {
const mergedRect = mergedRepaintRects[i];
// Merge if has intersection
if (mergedRect.intersect(rect)) {
const pendingRect = new BoundingRect(0, 0, 0, 0);
pendingRect.copy(mergedRect);
pendingRect.union(rect);
mergedRepaintRects[i] = pendingRect;
isMerged = true;
break;
}
else if (full) {
// Merged to exists rectangles if full
pendingRect.copy(rect);
pendingRect.union(mergedRect);
const aArea = rect.width * rect.height;
const bArea = mergedRect.width * mergedRect.height;
const pendingArea = pendingRect.width * pendingRect.height;
const deltaArea = pendingArea - aArea - bArea;
if (deltaArea < minDeltaArea) {
minDeltaArea = deltaArea;
bestRectToMergeIdx = i;
}
}
}
if (full) {
mergedRepaintRects[bestRectToMergeIdx].union(rect);
isMerged = true;
}
if (!isMerged) {
// Create new merged rect if cannot merge with current
const boundingRect = new BoundingRect(0, 0, 0, 0);
boundingRect.copy(rect);
mergedRepaintRects.push(boundingRect);
}
if (!full) {
full = mergedRepaintRects.length >= maxRepaintRectCount;
}
}
}
/**
* Loop the paint list of this frame and get the dirty rects of elements
* in this frame.
*/
for (let i = this.__startIndex; i < this.__endIndex; ++i) {
const el = displayList[i];
if (el) {
/**
* `shouldPaint` is true only when the element is not ignored or
* invisible and all its ancestors are not ignored.
* `shouldPaint` being true means it will be brushed this frame.
*
* `__isRendered` being true means the element is currently on
* the canvas.
*
* `__dirty` being true means the element should be brushed this
* frame.
*
* We only need to repaint the element's previous painting rect
* if it's currently on the canvas and needs repaint this frame
* or not painted this frame.
*/
const shouldPaint = el.shouldBePainted(viewWidth, viewHeight, true, true);
const prevRect = el.__isRendered && ((el.__dirty & REDRAW_BIT) || !shouldPaint)
? el.getPrevPaintRect()
: null;
if (prevRect) {
addRectToMergePool(prevRect);
}
/**
* On the other hand, we only need to paint the current rect
* if the element should be brushed this frame and either being
* dirty or not rendered before.
*/
const curRect = shouldPaint && ((el.__dirty & REDRAW_BIT) || !el.__isRendered)
? el.getPaintRect()
: null;
if (curRect) {
addRectToMergePool(curRect);
}
}
}
/**
* The above loop calculates the dirty rects of elements that are in the
* paint list this frame, which does not include those elements removed
* in this frame. So we loop the `prevList` to get the removed elements.
*/
for (let i = this.__prevStartIndex; i < this.__prevEndIndex; ++i) {
const el = prevList[i];
/**
* Consider the elements whose ancestors are invisible, they should
* not be painted and their previous painting rects should be
* cleared if they are rendered on the canvas (`__isRendered` being
* true). `!shouldPaint` means the element is not brushed in this
* frame.
*
* `!el.__zr` means it's removed from the storage.
*
* In conclusion, an element needs to repaint the previous painting
* rect if and only if it's not painted this frame and was
* previously painted on the canvas.
*/
const shouldPaint = el && el.shouldBePainted(viewWidth, viewHeight, true, true);
if (el && (!shouldPaint || !el.__zr) && el.__isRendered) {
// el was removed
const prevRect = el.getPrevPaintRect();
if (prevRect) {
addRectToMergePool(prevRect);
}
}
}
// Merge intersected rects in the result
let hasIntersections;
do {
hasIntersections = false;
for (let i = 0; i < mergedRepaintRects.length;) {
if (mergedRepaintRects[i].isZero()) {
mergedRepaintRects.splice(i, 1);
continue;
}
for (let j = i + 1; j < mergedRepaintRects.length;) {
if (mergedRepaintRects[i].intersect(mergedRepaintRects[j])) {
hasIntersections = true;
mergedRepaintRects[i].union(mergedRepaintRects[j]);
mergedRepaintRects.splice(j, 1);
}
else {
j++;
}
}
i++;
}
} while (hasIntersections);
this._paintRects = mergedRepaintRects;
return mergedRepaintRects;
}
/**
* Get paint rects for debug usage.
*/
debugGetPaintRects() {
return (this._paintRects || []).slice();
}
resize(width: number, height: number) {
const dpr = this.dpr;
const dom = this.dom;
const domStyle = dom.style;
const domBack = this.domBack;
if (domStyle) {
domStyle.width = width + 'px';
domStyle.height = height + 'px';
}
dom.width = width * dpr;
dom.height = height * dpr;
if (domBack) {
domBack.width = width * dpr;
domBack.height = height * dpr;
if (dpr !== 1) {
this.ctxBack.scale(dpr, dpr);
}
}
}
/**
* 清空该层画布
*/
clear(
clearAll?: boolean,
clearColor?: string | GradientObject | ImagePatternObject,
repaintRects?: BoundingRect[]
) {
const dom = this.dom;
const ctx = this.ctx;
const width = dom.width;
const height = dom.height;
clearColor = clearColor || this.clearColor;
const haveMotionBLur = this.motionBlur && !clearAll;
const lastFrameAlpha = this.lastFrameAlpha;
const dpr = this.dpr;
const self = this;
if (haveMotionBLur) {
if (!this.domBack) {
this.createBackBuffer();
}
this.ctxBack.globalCompositeOperation = 'copy';
this.ctxBack.drawImage(
dom, 0, 0,
width / dpr,
height / dpr
);
}
const domBack = this.domBack;
function doClear(x: number, y: number, width: number, height: number) {
ctx.clearRect(x, y, width, height);
if (clearColor && clearColor !== 'transparent') {
let clearColorGradientOrPattern;
// Gradient
if (util.isGradientObject(clearColor)) {
// shouldn't cache when clearColor is not global and size changed
const shouldCache = clearColor.global || (
(clearColor as InnerGradientObject).__width === width
&& (clearColor as InnerGradientObject).__height === height
);
// Cache canvas gradient
clearColorGradientOrPattern = shouldCache
&& (clearColor as InnerGradientObject).__canvasGradient
|| getCanvasGradient(ctx, clearColor, {
x: 0,
y: 0,
width: width,
height: height
});
(clearColor as InnerGradientObject).__canvasGradient = clearColorGradientOrPattern;
(clearColor as InnerGradientObject).__width = width;
(clearColor as InnerGradientObject).__height = height;
}
// Pattern
else if (util.isImagePatternObject(clearColor)) {
// scale pattern by dpr
clearColor.scaleX = clearColor.scaleX || dpr;
clearColor.scaleY = clearColor.scaleY || dpr;
clearColorGradientOrPattern = createCanvasPattern(
ctx, clearColor, {
dirty() {
self.setUnpainted();
self.painter.refresh();
}
}
);
}
ctx.save();
ctx.fillStyle = clearColorGradientOrPattern || (clearColor as string);
ctx.fillRect(x, y, width, height);
ctx.restore();
}
if (haveMotionBLur) {
ctx.save();
ctx.globalAlpha = lastFrameAlpha;
ctx.drawImage(domBack, x, y, width, height);
ctx.restore();
}
};
if (!repaintRects || haveMotionBLur) {
// Clear the full canvas
doClear(0, 0, width, height);
}
else if (repaintRects.length) {
// Clear the repaint areas
util.each(repaintRects, rect => {
doClear(
rect.x * dpr,
rect.y * dpr,
rect.width * dpr,
rect.height * dpr
);
});
}
}
// Interface of refresh
refresh: (clearColor?: string | GradientObject | ImagePatternObject) => void
// Interface of renderToCanvas in getRenderedCanvas
renderToCanvas: (ctx: CanvasRenderingContext2D) => void
// Events
onclick: ElementEventCallback<unknown, this>
ondblclick: ElementEventCallback<unknown, this>
onmouseover: ElementEventCallback<unknown, this>
onmouseout: ElementEventCallback<unknown, this>
onmousemove: ElementEventCallback<unknown, this>
onmousewheel: ElementEventCallback<unknown, this>
onmousedown: ElementEventCallback<unknown, this>
onmouseup: ElementEventCallback<unknown, this>
oncontextmenu: ElementEventCallback<unknown, this>
ondrag: ElementEventCallback<unknown, this>
ondragstart: ElementEventCallback<unknown, this>
ondragend: ElementEventCallback<unknown, this>
ondragenter: ElementEventCallback<unknown, this>
ondragleave: ElementEventCallback<unknown, this>
ondragover: ElementEventCallback<unknown, this>
ondrop: ElementEventCallback<unknown, this>
}
+977
View File
@@ -0,0 +1,977 @@
import {devicePixelRatio} from '../config';
import * as util from '../core/util';
import Layer, { LayerConfig } from './Layer';
import requestAnimationFrame from '../animation/requestAnimationFrame';
import env from '../core/env';
import Displayable from '../graphic/Displayable';
import { WXCanvasRenderingContext } from '../core/types';
import { GradientObject } from '../graphic/Gradient';
import { ImagePatternObject } from '../graphic/Pattern';
import Storage from '../Storage';
import { brush, BrushScope, brushSingle } from './graphic';
import { PainterBase } from '../PainterBase';
import BoundingRect from '../core/BoundingRect';
import { REDRAW_BIT } from '../graphic/constants';
import { getSize } from './helper';
import type IncrementalDisplayable from '../graphic/IncrementalDisplayable';
const HOVER_LAYER_ZLEVEL = 1e5;
const CANVAS_ZLEVEL = 314159;
const EL_AFTER_INCREMENTAL_INC = 0.01;
const INCREMENTAL_INC = 0.001;
function isLayerValid(layer: Layer) {
if (!layer) {
return false;
}
if (layer.__builtin__) {
return true;
}
if (typeof (layer.resize) !== 'function'
|| typeof (layer.refresh) !== 'function'
) {
return false;
}
return true;
}
function createRoot(width: number, height: number) {
const domRoot = document.createElement('div');
// domRoot.onselectstart = returnFalse; // Avoid page selected
domRoot.style.cssText = [
'position:relative',
// IOS13 safari probably has a compositing bug (z order of the canvas and the consequent
// dom does not act as expected) when some of the parent dom has
// `-webkit-overflow-scrolling: touch;` and the webpage is longer than one screen and
// the canvas is not at the top part of the page.
// Check `https://bugs.webkit.org/show_bug.cgi?id=203681` for more details. We remove
// this `overflow:hidden` to avoid the bug.
// 'overflow:hidden',
'width:' + width + 'px',
'height:' + height + 'px',
'padding:0',
'margin:0',
'border-width:0'
].join(';') + ';';
return domRoot;
}
interface CanvasPainterOption {
devicePixelRatio?: number
width?: number | string // Can be 10 / 10px / auto
height?: number | string,
useDirtyRect?: boolean
}
export default class CanvasPainter implements PainterBase {
type = 'canvas'
root: HTMLElement
dpr: number
storage: Storage
private _singleCanvas: boolean
private _opts: CanvasPainterOption
private _zlevelList: number[] = []
private _prevDisplayList: Displayable[] = []
private _layers: {[key: number]: Layer} = {} // key is zlevel
private _layerConfig: {[key: number]: LayerConfig} = {} // key is zlevel
/**
* zrender will do compositing when root is a canvas and have multiple zlevels.
*/
private _needsManuallyCompositing = false
private _width: number
private _height: number
private _domRoot: HTMLElement
private _hoverlayer: Layer
private _redrawId: number
private _backgroundColor: string | GradientObject | ImagePatternObject
constructor(root: HTMLElement, storage: Storage, opts: CanvasPainterOption, id: number) {
this.type = 'canvas';
// In node environment using node-canvas
const singleCanvas = !root.nodeName // In node ?
|| root.nodeName.toUpperCase() === 'CANVAS';
this._opts = opts = util.extend({}, opts || {}) as CanvasPainterOption;
/**
* @type {number}
*/
this.dpr = opts.devicePixelRatio || devicePixelRatio;
/**
* @type {boolean}
* @private
*/
this._singleCanvas = singleCanvas;
/**
* 绘图容器
* @type {HTMLElement}
*/
this.root = root;
const rootStyle = root.style;
if (rootStyle) {
// @ts-ignore
util.disableUserSelect(root);
root.innerHTML = '';
}
/**
* @type {module:zrender/Storage}
*/
this.storage = storage;
const zlevelList: number[] = this._zlevelList;
this._prevDisplayList = [];
const layers = this._layers;
if (!singleCanvas) {
this._width = getSize(root, 0, opts);
this._height = getSize(root, 1, opts);
const domRoot = this._domRoot = createRoot(
this._width, this._height
);
root.appendChild(domRoot);
}
else {
const rootCanvas = root as HTMLCanvasElement;
let width = rootCanvas.width;
let height = rootCanvas.height;
if (opts.width != null) {
// TODO sting?
width = opts.width as number;
}
if (opts.height != null) {
// TODO sting?
height = opts.height as number;
}
this.dpr = opts.devicePixelRatio || 1;
// Use canvas width and height directly
rootCanvas.width = width * this.dpr;
rootCanvas.height = height * this.dpr;
this._width = width;
this._height = height;
// Create layer if only one given canvas
// Device can be specified to create a high dpi image.
const mainLayer = new Layer(rootCanvas, this, this.dpr);
mainLayer.__builtin__ = true;
mainLayer.initContext();
// FIXME Use canvas width and height
// mainLayer.resize(width, height);
layers[CANVAS_ZLEVEL] = mainLayer;
mainLayer.zlevel = CANVAS_ZLEVEL;
// Not use common zlevel.
zlevelList.push(CANVAS_ZLEVEL);
this._domRoot = root;
}
}
getType() {
return 'canvas';
}
/**
* If painter use a single canvas
*/
isSingleCanvas() {
return this._singleCanvas;
}
getViewportRoot() {
return this._domRoot;
}
getViewportRootOffset() {
const viewportRoot = this.getViewportRoot();
if (viewportRoot) {
return {
offsetLeft: viewportRoot.offsetLeft || 0,
offsetTop: viewportRoot.offsetTop || 0
};
}
}
/**
* 刷新
* @param paintAll 强制绘制所有displayable
*/
refresh(paintAll?: boolean) {
const list = this.storage.getDisplayList(true);
const prevList = this._prevDisplayList;
const zlevelList = this._zlevelList;
this._redrawId = Math.random();
this._paintList(list, prevList, paintAll, this._redrawId);
// Paint custum layers
for (let i = 0; i < zlevelList.length; i++) {
const z = zlevelList[i];
const layer = this._layers[z];
if (!layer.__builtin__ && layer.refresh) {
const clearColor = i === 0 ? this._backgroundColor : null;
layer.refresh(clearColor);
}
}
if (this._opts.useDirtyRect) {
this._prevDisplayList = list.slice();
}
return this;
}
refreshHover() {
this._paintHoverList(this.storage.getDisplayList(false));
}
private _paintHoverList(list: Displayable[]) {
let len = list.length;
let hoverLayer = this._hoverlayer;
hoverLayer && hoverLayer.clear();
if (!len) {
return;
}
const scope: BrushScope = {
inHover: true,
viewWidth: this._width,
viewHeight: this._height
};
let ctx;
for (let i = 0; i < len; i++) {
const el = list[i];
if (el.__inHover) {
// Use a extream large zlevel
// FIXME?
if (!hoverLayer) {
hoverLayer = this._hoverlayer = this.getLayer(HOVER_LAYER_ZLEVEL);
}
if (!ctx) {
ctx = hoverLayer.ctx;
ctx.save();
}
brush(ctx, el, scope, i === len - 1);
}
}
if (ctx) {
ctx.restore();
}
}
getHoverLayer() {
return this.getLayer(HOVER_LAYER_ZLEVEL);
}
paintOne(ctx: CanvasRenderingContext2D, el: Displayable) {
brushSingle(ctx, el);
}
private _paintList(list: Displayable[], prevList: Displayable[], paintAll: boolean, redrawId?: number) {
if (this._redrawId !== redrawId) {
return;
}
paintAll = paintAll || false;
this._updateLayerStatus(list);
const {finished, needsRefreshHover} = this._doPaintList(list, prevList, paintAll);
if (this._needsManuallyCompositing) {
this._compositeManually();
}
if (needsRefreshHover) {
this._paintHoverList(list);
}
if (!finished) {
const self = this;
requestAnimationFrame(function () {
self._paintList(list, prevList, paintAll, redrawId);
});
}
else {
this.eachLayer(layer => {
layer.afterBrush && layer.afterBrush();
});
}
}
private _compositeManually() {
const ctx = this.getLayer(CANVAS_ZLEVEL).ctx;
const width = (this._domRoot as HTMLCanvasElement).width;
const height = (this._domRoot as HTMLCanvasElement).height;
ctx.clearRect(0, 0, width, height);
// PENDING, If only builtin layer?
this.eachBuiltinLayer(function (layer) {
if (layer.virtual) {
ctx.drawImage(layer.dom, 0, 0, width, height);
}
});
}
private _doPaintList(
list: Displayable[],
prevList: Displayable[],
paintAll?: boolean
): {
finished: boolean
needsRefreshHover: boolean
} {
const layerList = [];
const useDirtyRect = this._opts.useDirtyRect;
for (let zi = 0; zi < this._zlevelList.length; zi++) {
const zlevel = this._zlevelList[zi];
const layer = this._layers[zlevel];
if (layer.__builtin__
&& layer !== this._hoverlayer
&& (layer.__dirty || paintAll)
// Layer with hover elements can't be redrawn.
// && !layer.__hasHoverLayerELement
) {
layerList.push(layer);
}
}
let finished = true;
let needsRefreshHover = false;
for (let k = 0; k < layerList.length; k++) {
const layer = layerList[k];
const ctx = layer.ctx;
const repaintRects = useDirtyRect
&& layer.createRepaintRects(list, prevList, this._width, this._height);
let start = paintAll ? layer.__startIndex : layer.__drawIndex;
const useTimer = !paintAll && layer.incremental && Date.now;
const startTime = useTimer && Date.now();
const clearColor = layer.zlevel === this._zlevelList[0]
? this._backgroundColor : null;
// All elements in this layer are removed.
if (layer.__startIndex === layer.__endIndex) {
layer.clear(false, clearColor, repaintRects);
}
else if (start === layer.__startIndex) {
const firstEl = list[start];
if (!firstEl.incremental || !(firstEl as IncrementalDisplayable).notClear || paintAll) {
layer.clear(false, clearColor, repaintRects);
}
}
if (start === -1) {
console.error('For some unknown reason. drawIndex is -1');
start = layer.__startIndex;
}
let i: number;
/* eslint-disable-next-line */
const repaint = (repaintRect?: BoundingRect) => {
const scope: BrushScope = {
inHover: false,
allClipped: false,
prevEl: null,
viewWidth: this._width,
viewHeight: this._height
};
for (i = start; i < layer.__endIndex; i++) {
const el = list[i];
if (el.__inHover) {
needsRefreshHover = true;
}
this._doPaintEl(el, layer, useDirtyRect, repaintRect, scope, i === layer.__endIndex - 1);
if (useTimer) {
// Date.now can be executed in 13,025,305 ops/second.
const dTime = Date.now() - startTime;
// Give 15 millisecond to draw.
// The rest elements will be drawn in the next frame.
if (dTime > 15) {
break;
}
}
}
if (scope.prevElClipPaths) {
// Needs restore the state. If last drawn element is in the clipping area.
ctx.restore();
}
};
if (repaintRects) {
if (repaintRects.length === 0) {
// Nothing to repaint, mark as finished
i = layer.__endIndex;
}
else {
const dpr = this.dpr;
// Set repaintRect as clipPath
for (var r = 0; r < repaintRects.length; ++r) {
const rect = repaintRects[r];
ctx.save();
ctx.beginPath();
ctx.rect(
rect.x * dpr,
rect.y * dpr,
rect.width * dpr,
rect.height * dpr
);
ctx.clip();
repaint(rect);
ctx.restore();
}
}
}
else {
// Paint all once
ctx.save();
repaint();
ctx.restore();
}
layer.__drawIndex = i;
if (layer.__drawIndex < layer.__endIndex) {
finished = false;
}
}
if (env.wxa) {
// Flush for weixin application
util.each(this._layers, function (layer) {
if (layer && layer.ctx && (layer.ctx as WXCanvasRenderingContext).draw) {
(layer.ctx as WXCanvasRenderingContext).draw();
}
});
}
return {
finished,
needsRefreshHover
};
}
private _doPaintEl(
el: Displayable,
currentLayer: Layer,
useDirtyRect: boolean,
repaintRect: BoundingRect,
scope: BrushScope,
isLast: boolean
) {
const ctx = currentLayer.ctx;
if (useDirtyRect) {
const paintRect = el.getPaintRect();
if (!repaintRect || paintRect && paintRect.intersect(repaintRect)) {
brush(ctx, el, scope, isLast);
el.setPrevPaintRect(paintRect);
}
}
else {
brush(ctx, el, scope, isLast);
}
}
/**
* 获取 zlevel 所在层,如果不存在则会创建一个新的层
* @param zlevel
* @param virtual Virtual layer will not be inserted into dom.
*/
getLayer(zlevel: number, virtual?: boolean) {
if (this._singleCanvas && !this._needsManuallyCompositing) {
zlevel = CANVAS_ZLEVEL;
}
let layer = this._layers[zlevel];
if (!layer) {
// Create a new layer
layer = new Layer('zr_' + zlevel, this, this.dpr);
layer.zlevel = zlevel;
layer.__builtin__ = true;
if (this._layerConfig[zlevel]) {
util.merge(layer, this._layerConfig[zlevel], true);
}
// TODO Remove EL_AFTER_INCREMENTAL_INC magic number
else if (this._layerConfig[zlevel - EL_AFTER_INCREMENTAL_INC]) {
util.merge(layer, this._layerConfig[zlevel - EL_AFTER_INCREMENTAL_INC], true);
}
if (virtual) {
layer.virtual = virtual;
}
this.insertLayer(zlevel, layer);
// Context is created after dom inserted to document
// Or excanvas will get 0px clientWidth and clientHeight
layer.initContext();
}
return layer;
}
insertLayer(zlevel: number, layer: Layer) {
const layersMap = this._layers;
const zlevelList = this._zlevelList;
const len = zlevelList.length;
const domRoot = this._domRoot;
let prevLayer = null;
let i = -1;
if (layersMap[zlevel]) {
if (process.env.NODE_ENV !== 'production') {
util.logError('ZLevel ' + zlevel + ' has been used already');
}
return;
}
// Check if is a valid layer
if (!isLayerValid(layer)) {
if (process.env.NODE_ENV !== 'production') {
util.logError('Layer of zlevel ' + zlevel + ' is not valid');
}
return;
}
if (len > 0 && zlevel > zlevelList[0]) {
for (i = 0; i < len - 1; i++) {
if (
zlevelList[i] < zlevel
&& zlevelList[i + 1] > zlevel
) {
break;
}
}
prevLayer = layersMap[zlevelList[i]];
}
zlevelList.splice(i + 1, 0, zlevel);
layersMap[zlevel] = layer;
// Virtual layer will not directly show on the screen.
// (It can be a WebGL layer and assigned to a ZRImage element)
// But it still under management of zrender.
if (!layer.virtual) {
if (prevLayer) {
const prevDom = prevLayer.dom;
if (prevDom.nextSibling) {
domRoot.insertBefore(
layer.dom,
prevDom.nextSibling
);
}
else {
domRoot.appendChild(layer.dom);
}
}
else {
if (domRoot.firstChild) {
domRoot.insertBefore(layer.dom, domRoot.firstChild);
}
else {
domRoot.appendChild(layer.dom);
}
}
}
layer.painter || (layer.painter = this);
}
// Iterate each layer
eachLayer<T>(cb: (this: T, layer: Layer, z: number) => void, context?: T) {
const zlevelList = this._zlevelList;
for (let i = 0; i < zlevelList.length; i++) {
const z = zlevelList[i];
cb.call(context, this._layers[z], z);
}
}
// Iterate each buildin layer
eachBuiltinLayer<T>(cb: (this: T, layer: Layer, z: number) => void, context?: T) {
const zlevelList = this._zlevelList;
for (let i = 0; i < zlevelList.length; i++) {
const z = zlevelList[i];
const layer = this._layers[z];
if (layer.__builtin__) {
cb.call(context, layer, z);
}
}
}
// Iterate each other layer except buildin layer
eachOtherLayer<T>(cb: (this: T, layer: Layer, z: number) => void, context?: T) {
const zlevelList = this._zlevelList;
for (let i = 0; i < zlevelList.length; i++) {
const z = zlevelList[i];
const layer = this._layers[z];
if (!layer.__builtin__) {
cb.call(context, layer, z);
}
}
}
/**
* 获取所有已创建的层
* @param prevLayer
*/
getLayers() {
return this._layers;
}
_updateLayerStatus(list: Displayable[]) {
this.eachBuiltinLayer(function (layer, z) {
layer.__dirty = layer.__used = false;
});
function updatePrevLayer(idx: number) {
if (prevLayer) {
if (prevLayer.__endIndex !== idx) {
prevLayer.__dirty = true;
}
prevLayer.__endIndex = idx;
}
}
if (this._singleCanvas) {
for (let i = 1; i < list.length; i++) {
const el = list[i];
if (el.zlevel !== list[i - 1].zlevel || el.incremental) {
this._needsManuallyCompositing = true;
break;
}
}
}
let prevLayer: Layer = null;
let incrementalLayerCount = 0;
let prevZlevel;
let i;
for (i = 0; i < list.length; i++) {
const el = list[i];
const zlevel = el.zlevel;
let layer;
if (prevZlevel !== zlevel) {
prevZlevel = zlevel;
incrementalLayerCount = 0;
}
// TODO Not use magic number on zlevel.
// Each layer with increment element can be separated to 3 layers.
// (Other Element drawn after incremental element)
// -----------------zlevel + EL_AFTER_INCREMENTAL_INC--------------------
// (Incremental element)
// ----------------------zlevel + INCREMENTAL_INC------------------------
// (Element drawn before incremental element)
// --------------------------------zlevel--------------------------------
if (el.incremental) {
layer = this.getLayer(zlevel + INCREMENTAL_INC, this._needsManuallyCompositing);
layer.incremental = true;
incrementalLayerCount = 1;
}
else {
layer = this.getLayer(
zlevel + (incrementalLayerCount > 0 ? EL_AFTER_INCREMENTAL_INC : 0),
this._needsManuallyCompositing
);
}
if (!layer.__builtin__) {
util.logError('ZLevel ' + zlevel + ' has been used by unkown layer ' + layer.id);
}
if (layer !== prevLayer) {
layer.__used = true;
if (layer.__startIndex !== i) {
layer.__dirty = true;
}
layer.__startIndex = i;
if (!layer.incremental) {
layer.__drawIndex = i;
}
else {
// Mark layer draw index needs to update.
layer.__drawIndex = -1;
}
updatePrevLayer(i);
prevLayer = layer;
}
if ((el.__dirty & REDRAW_BIT) && !el.__inHover) { // Ignore dirty elements in hover layer.
layer.__dirty = true;
if (layer.incremental && layer.__drawIndex < 0) {
// Start draw from the first dirty element.
layer.__drawIndex = i;
}
}
}
updatePrevLayer(i);
this.eachBuiltinLayer(function (layer, z) {
// Used in last frame but not in this frame. Needs clear
if (!layer.__used && layer.getElementCount() > 0) {
layer.__dirty = true;
layer.__startIndex = layer.__endIndex = layer.__drawIndex = 0;
}
// For incremental layer. In case start index changed and no elements are dirty.
if (layer.__dirty && layer.__drawIndex < 0) {
layer.__drawIndex = layer.__startIndex;
}
});
}
/**
* 清除hover层外所有内容
*/
clear() {
this.eachBuiltinLayer(this._clearLayer);
return this;
}
_clearLayer(layer: Layer) {
layer.clear();
}
setBackgroundColor(backgroundColor: string | GradientObject | ImagePatternObject) {
this._backgroundColor = backgroundColor;
util.each(this._layers, layer => {
layer.setUnpainted();
});
}
/**
* 修改指定zlevel的绘制参数
*/
configLayer(zlevel: number, config: LayerConfig) {
if (config) {
const layerConfig = this._layerConfig;
if (!layerConfig[zlevel]) {
layerConfig[zlevel] = config;
}
else {
util.merge(layerConfig[zlevel], config, true);
}
for (let i = 0; i < this._zlevelList.length; i++) {
const _zlevel = this._zlevelList[i];
// TODO Remove EL_AFTER_INCREMENTAL_INC magic number
if (_zlevel === zlevel || _zlevel === zlevel + EL_AFTER_INCREMENTAL_INC) {
const layer = this._layers[_zlevel];
util.merge(layer, layerConfig[zlevel], true);
}
}
}
}
/**
* 删除指定层
* @param zlevel 层所在的zlevel
*/
delLayer(zlevel: number) {
const layers = this._layers;
const zlevelList = this._zlevelList;
const layer = layers[zlevel];
if (!layer) {
return;
}
layer.dom.parentNode.removeChild(layer.dom);
delete layers[zlevel];
zlevelList.splice(util.indexOf(zlevelList, zlevel), 1);
}
/**
* 区域大小变化后重绘
*/
resize(
width?: number | string,
height?: number | string
) {
if (!this._domRoot.style) { // Maybe in node or worker
if (width == null || height == null) {
return;
}
// TODO width / height may be string
this._width = width as number;
this._height = height as number;
this.getLayer(CANVAS_ZLEVEL).resize(width as number, height as number);
}
else {
const domRoot = this._domRoot;
// FIXME Why ?
domRoot.style.display = 'none';
// Save input w/h
const opts = this._opts;
const root = this.root;
width != null && (opts.width = width);
height != null && (opts.height = height);
width = getSize(root, 0, opts);
height = getSize(root, 1, opts);
domRoot.style.display = '';
// 优化没有实际改变的resize
if (this._width !== width || height !== this._height) {
domRoot.style.width = width + 'px';
domRoot.style.height = height + 'px';
for (let id in this._layers) {
if (this._layers.hasOwnProperty(id)) {
this._layers[id].resize(width, height);
}
}
this.refresh(true);
}
this._width = width;
this._height = height;
}
return this;
}
/**
* 清除单独的一个层
* @param {number} zlevel
*/
clearLayer(zlevel: number) {
const layer = this._layers[zlevel];
if (layer) {
layer.clear();
}
}
/**
* 释放
*/
dispose() {
this.root.innerHTML = '';
this.root =
this.storage =
this._domRoot =
this._layers = null;
}
/**
* Get canvas which has all thing rendered
*/
getRenderedCanvas(opts?: {
backgroundColor?: string | GradientObject | ImagePatternObject
pixelRatio?: number
}) {
opts = opts || {};
if (this._singleCanvas && !this._compositeManually) {
return this._layers[CANVAS_ZLEVEL].dom;
}
const imageLayer = new Layer('image', this, opts.pixelRatio || this.dpr);
imageLayer.initContext();
imageLayer.clear(false, opts.backgroundColor || this._backgroundColor);
const ctx = imageLayer.ctx;
if (opts.pixelRatio <= this.dpr) {
this.refresh();
const width = imageLayer.dom.width;
const height = imageLayer.dom.height;
this.eachLayer(function (layer) {
if (layer.__builtin__) {
ctx.drawImage(layer.dom, 0, 0, width, height);
}
else if (layer.renderToCanvas) {
ctx.save();
layer.renderToCanvas(ctx);
ctx.restore();
}
});
}
else {
// PENDING, echarts-gl and incremental rendering.
const scope = {
inHover: false,
viewWidth: this._width,
viewHeight: this._height
};
const displayList = this.storage.getDisplayList(true);
for (let i = 0, len = displayList.length; i < len; i++) {
const el = displayList[i];
brush(ctx, el, scope, i === len - 1);
}
}
return imageLayer.dom;
}
/**
* 获取绘图区域宽度
*/
getWidth() {
return this._width;
}
/**
* 获取绘图区域高度
*/
getHeight() {
return this._height;
}
};
+4
View File
@@ -0,0 +1,4 @@
import {registerPainter} from '../zrender';
import Painter from './Painter';
registerPainter('canvas', Painter);
+32
View File
@@ -0,0 +1,32 @@
import { isArray, isNumber, map } from '../core/util';
import Path from '../graphic/Path';
import TSpan from '../graphic/TSpan';
export function normalizeLineDash(lineType: any, lineWidth?: number): number[] | false {
if (!lineType || lineType === 'solid' || !(lineWidth > 0)) {
return null;
}
return lineType === 'dashed'
? [4 * lineWidth, 2 * lineWidth]
: lineType === 'dotted'
? [lineWidth]
: isNumber(lineType)
? [lineType] : isArray(lineType) ? lineType : null;
}
export function getLineDash(el: Path | TSpan): [number[] | false, number] {
const style = el.style;
let lineDash = style.lineDash && style.lineWidth > 0 && normalizeLineDash(style.lineDash, style.lineWidth);
let lineDashOffset = style.lineDashOffset;
if (lineDash) {
const lineScale = (style.strokeNoScale && el.getLineScale) ? el.getLineScale() : 1;
if (lineScale && lineScale !== 1) {
lineDash = map(lineDash, function (rawVal) {
return rawVal / lineScale;
});
lineDashOffset /= lineScale;
}
}
return [lineDash, lineDashOffset];
}
+816
View File
@@ -0,0 +1,816 @@
import Displayable, { DEFAULT_COMMON_STYLE } from '../graphic/Displayable';
import PathProxy from '../core/PathProxy';
import { GradientObject } from '../graphic/Gradient';
import { ImagePatternObject, InnerImagePatternObject } from '../graphic/Pattern';
import { LinearGradientObject } from '../graphic/LinearGradient';
import { RadialGradientObject } from '../graphic/RadialGradient';
import { ZRCanvasRenderingContext } from '../core/types';
import { createOrUpdateImage, isImageReady } from '../graphic/helper/image';
import { getCanvasGradient, isClipPathChanged } from './helper';
import Path, { PathStyleProps } from '../graphic/Path';
import ZRImage, { ImageStyleProps } from '../graphic/Image';
import TSpan, {TSpanStyleProps} from '../graphic/TSpan';
import { MatrixArray } from '../core/matrix';
import { RADIAN_TO_DEGREE } from '../core/util';
import { getLineDash } from './dashStyle';
import { REDRAW_BIT, SHAPE_CHANGED_BIT } from '../graphic/constants';
import type IncrementalDisplayable from '../graphic/IncrementalDisplayable';
import { DEFAULT_FONT } from '../core/platform';
const pathProxyForDraw = new PathProxy(true);
// Not use el#hasStroke because style may be different.
function styleHasStroke(style: PathStyleProps) {
const stroke = style.stroke;
return !(stroke == null || stroke === 'none' || !(style.lineWidth > 0));
}
// ignore lineWidth and must be string
// Expected color but found '[' when color is gradient
function isValidStrokeFillStyle(
strokeOrFill: PathStyleProps['stroke'] | PathStyleProps['fill']
): strokeOrFill is string {
return typeof strokeOrFill === 'string' && strokeOrFill !== 'none';
}
function styleHasFill(style: PathStyleProps) {
const fill = style.fill;
return fill != null && fill !== 'none';
}
function doFillPath(ctx: CanvasRenderingContext2D, style: PathStyleProps) {
if (style.fillOpacity != null && style.fillOpacity !== 1) {
const originalGlobalAlpha = ctx.globalAlpha;
ctx.globalAlpha = style.fillOpacity * style.opacity;
ctx.fill();
// Set back globalAlpha
ctx.globalAlpha = originalGlobalAlpha;
}
else {
ctx.fill();
}
}
function doStrokePath(ctx: CanvasRenderingContext2D, style: PathStyleProps) {
if (style.strokeOpacity != null && style.strokeOpacity !== 1) {
const originalGlobalAlpha = ctx.globalAlpha;
ctx.globalAlpha = style.strokeOpacity * style.opacity;
ctx.stroke();
// Set back globalAlpha
ctx.globalAlpha = originalGlobalAlpha;
}
else {
ctx.stroke();
}
}
export function createCanvasPattern(
this: void,
ctx: CanvasRenderingContext2D,
pattern: ImagePatternObject,
el: {dirty: () => void}
): CanvasPattern {
const image = createOrUpdateImage(pattern.image, (pattern as InnerImagePatternObject).__image, el);
if (isImageReady(image)) {
const canvasPattern = ctx.createPattern(image, pattern.repeat || 'repeat');
if (
typeof DOMMatrix === 'function'
&& canvasPattern // image may be not ready
&& canvasPattern.setTransform // setTransform may not be supported in some old devices.
) {
const matrix = new DOMMatrix();
matrix.translateSelf((pattern.x || 0), (pattern.y || 0));
matrix.rotateSelf(0, 0, (pattern.rotation || 0) * RADIAN_TO_DEGREE);
matrix.scaleSelf((pattern.scaleX || 1), (pattern.scaleY || 1));
canvasPattern.setTransform(matrix);
}
return canvasPattern;
}
}
// Draw Path Elements
function brushPath(ctx: CanvasRenderingContext2D, el: Path, style: PathStyleProps, inBatch: boolean) {
let hasStroke = styleHasStroke(style);
let hasFill = styleHasFill(style);
const strokePercent = style.strokePercent;
const strokePart = strokePercent < 1;
// TODO Reduce path memory cost.
const firstDraw = !el.path;
// Create path for each element when:
// 1. Element has interactions.
// 2. Element draw part of the line.
if ((!el.silent || strokePart) && firstDraw) {
el.createPathProxy();
}
const path = el.path || pathProxyForDraw;
const dirtyFlag = el.__dirty;
if (!inBatch) {
const fill = style.fill;
const stroke = style.stroke;
const hasFillGradient = hasFill && !!(fill as GradientObject).colorStops;
const hasStrokeGradient = hasStroke && !!(stroke as GradientObject).colorStops;
const hasFillPattern = hasFill && !!(fill as ImagePatternObject).image;
const hasStrokePattern = hasStroke && !!(stroke as ImagePatternObject).image;
let fillGradient;
let strokeGradient;
let fillPattern;
let strokePattern;
let rect;
if (hasFillGradient || hasStrokeGradient) {
rect = el.getBoundingRect();
}
// Update gradient because bounding rect may changed
if (hasFillGradient) {
fillGradient = dirtyFlag
? getCanvasGradient(ctx, fill as (LinearGradientObject | RadialGradientObject), rect)
: el.__canvasFillGradient;
// No need to clear cache when fill is not gradient.
// It will always been updated when fill changed back to gradient.
el.__canvasFillGradient = fillGradient;
}
if (hasStrokeGradient) {
strokeGradient = dirtyFlag
? getCanvasGradient(ctx, stroke as (LinearGradientObject | RadialGradientObject), rect)
: el.__canvasStrokeGradient;
el.__canvasStrokeGradient = strokeGradient;
}
if (hasFillPattern) {
// Pattern might be null if image not ready (even created from dataURI)
fillPattern = (dirtyFlag || !el.__canvasFillPattern)
? createCanvasPattern(ctx, fill as ImagePatternObject, el)
: el.__canvasFillPattern;
el.__canvasFillPattern = fillPattern;
}
if (hasStrokePattern) {
// Pattern might be null if image not ready (even created from dataURI)
strokePattern = (dirtyFlag || !el.__canvasStrokePattern)
? createCanvasPattern(ctx, stroke as ImagePatternObject, el)
: el.__canvasStrokePattern;
el.__canvasStrokePattern = fillPattern;
}
// Use the gradient or pattern
if (hasFillGradient) {
// PENDING If may have affect the state
ctx.fillStyle = fillGradient;
}
else if (hasFillPattern) {
if (fillPattern) { // createCanvasPattern may return false if image is not ready.
ctx.fillStyle = fillPattern;
}
else {
// Don't fill if image is not ready
hasFill = false;
}
}
if (hasStrokeGradient) {
ctx.strokeStyle = strokeGradient;
}
else if (hasStrokePattern) {
if (strokePattern) {
ctx.strokeStyle = strokePattern;
}
else {
// Don't stroke if image is not ready
hasStroke = false;
}
}
}
// Update path sx, sy
const scale = el.getGlobalScale();
path.setScale(scale[0], scale[1], el.segmentIgnoreThreshold);
let lineDash;
let lineDashOffset;
if (ctx.setLineDash && style.lineDash) {
[lineDash, lineDashOffset] = getLineDash(el);
}
let needsRebuild = true;
if (firstDraw || (dirtyFlag & SHAPE_CHANGED_BIT)) {
path.setDPR((ctx as any).dpr);
if (strokePart) {
// Use rebuildPath for percent stroke, so no context.
path.setContext(null);
}
else {
path.setContext(ctx);
needsRebuild = false;
}
path.reset();
el.buildPath(path, el.shape, inBatch);
path.toStatic();
// Clear path dirty flag
el.pathUpdated();
}
// Not support separate fill and stroke. For the compatibility of SVG
if (needsRebuild) {
path.rebuildPath(ctx, strokePart ? strokePercent : 1);
}
if (lineDash) {
ctx.setLineDash(lineDash);
ctx.lineDashOffset = lineDashOffset;
}
if (!inBatch) {
if (style.strokeFirst) {
if (hasStroke) {
doStrokePath(ctx, style);
}
if (hasFill) {
doFillPath(ctx, style);
}
}
else {
if (hasFill) {
doFillPath(ctx, style);
}
if (hasStroke) {
doStrokePath(ctx, style);
}
}
}
if (lineDash) {
// PENDING
// Remove lineDash
ctx.setLineDash([]);
}
}
// Draw Image Elements
function brushImage(ctx: CanvasRenderingContext2D, el: ZRImage, style: ImageStyleProps) {
const image = el.__image = createOrUpdateImage(
style.image,
el.__image,
el,
el.onload
);
if (!image || !isImageReady(image)) {
return;
}
const x = style.x || 0;
const y = style.y || 0;
let width = el.getWidth();
let height = el.getHeight();
const aspect = image.width / image.height;
if (width == null && height != null) {
// Keep image/height ratio
width = height * aspect;
}
else if (height == null && width != null) {
height = width / aspect;
}
else if (width == null && height == null) {
width = image.width;
height = image.height;
}
if (style.sWidth && style.sHeight) {
const sx = style.sx || 0;
const sy = style.sy || 0;
ctx.drawImage(
image,
sx, sy, style.sWidth, style.sHeight,
x, y, width, height
);
}
else if (style.sx && style.sy) {
const sx = style.sx;
const sy = style.sy;
const sWidth = width - sx;
const sHeight = height - sy;
ctx.drawImage(
image,
sx, sy, sWidth, sHeight,
x, y, width, height
);
}
else {
ctx.drawImage(image, x, y, width, height);
}
}
// Draw Text Elements
function brushText(ctx: CanvasRenderingContext2D, el: TSpan, style: TSpanStyleProps) {
let text = style.text;
// Convert to string
text != null && (text += '');
if (text) {
ctx.font = style.font || DEFAULT_FONT;
ctx.textAlign = style.textAlign;
ctx.textBaseline = style.textBaseline;
let lineDash;
let lineDashOffset;
if (ctx.setLineDash && style.lineDash) {
[lineDash, lineDashOffset] = getLineDash(el);
}
if (lineDash) {
ctx.setLineDash(lineDash);
ctx.lineDashOffset = lineDashOffset;
}
if (style.strokeFirst) {
if (styleHasStroke(style)) {
ctx.strokeText(text, style.x, style.y);
}
if (styleHasFill(style)) {
ctx.fillText(text, style.x, style.y);
}
}
else {
if (styleHasFill(style)) {
ctx.fillText(text, style.x, style.y);
}
if (styleHasStroke(style)) {
ctx.strokeText(text, style.x, style.y);
}
}
if (lineDash) {
// Remove lineDash
ctx.setLineDash([]);
}
}
}
const SHADOW_NUMBER_PROPS = ['shadowBlur', 'shadowOffsetX', 'shadowOffsetY'] as const;
const STROKE_PROPS = [
['lineCap', 'butt'], ['lineJoin', 'miter'], ['miterLimit', 10]
] as const;
type AllStyleOption = PathStyleProps | TSpanStyleProps | ImageStyleProps;
// type ShadowPropNames = typeof SHADOW_PROPS[number][0];
// type StrokePropNames = typeof STROKE_PROPS[number][0];
// type DrawPropNames = typeof DRAW_PROPS[number][0];
function bindCommonProps(
ctx: CanvasRenderingContext2D,
style: AllStyleOption,
prevStyle: AllStyleOption,
forceSetAll: boolean,
scope: BrushScope
): boolean {
let styleChanged = false;
if (!forceSetAll) {
prevStyle = prevStyle || {};
// Shared same style.
if (style === prevStyle) {
return false;
}
}
if (forceSetAll || style.opacity !== prevStyle.opacity) {
flushPathDrawn(ctx, scope);
styleChanged = true;
// Ensure opacity is between 0 ~ 1. Invalid opacity will lead to a failure set and use the leaked opacity from the previous.
const opacity = Math.max(Math.min(style.opacity, 1), 0);
ctx.globalAlpha = isNaN(opacity) ? DEFAULT_COMMON_STYLE.opacity : opacity;
}
if (forceSetAll || style.blend !== prevStyle.blend) {
if (!styleChanged) {
flushPathDrawn(ctx, scope);
styleChanged = true;
}
ctx.globalCompositeOperation = style.blend || DEFAULT_COMMON_STYLE.blend;
}
for (let i = 0; i < SHADOW_NUMBER_PROPS.length; i++) {
const propName = SHADOW_NUMBER_PROPS[i];
if (forceSetAll || style[propName] !== prevStyle[propName]) {
if (!styleChanged) {
flushPathDrawn(ctx, scope);
styleChanged = true;
}
// FIXME Invalid property value will cause style leak from previous element.
ctx[propName] = (ctx as ZRCanvasRenderingContext).dpr * (style[propName] || 0);
}
}
if (forceSetAll || style.shadowColor !== prevStyle.shadowColor) {
if (!styleChanged) {
flushPathDrawn(ctx, scope);
styleChanged = true;
}
ctx.shadowColor = style.shadowColor || DEFAULT_COMMON_STYLE.shadowColor;
}
return styleChanged;
}
function bindPathAndTextCommonStyle(
ctx: CanvasRenderingContext2D,
el: TSpan | Path,
prevEl: TSpan | Path,
forceSetAll: boolean,
scope: BrushScope
) {
const style = getStyle(el, scope.inHover);
const prevStyle = forceSetAll
? null
: (prevEl && getStyle(prevEl, scope.inHover) || {});
// Shared same style. prevStyle will be null if forceSetAll.
if (style === prevStyle) {
return false;
}
let styleChanged = bindCommonProps(ctx, style, prevStyle, forceSetAll, scope);
if (forceSetAll || style.fill !== prevStyle.fill) {
if (!styleChanged) {
// Flush before set
flushPathDrawn(ctx, scope);
styleChanged = true;
}
isValidStrokeFillStyle(style.fill) && (ctx.fillStyle = style.fill);
}
if (forceSetAll || style.stroke !== prevStyle.stroke) {
if (!styleChanged) {
flushPathDrawn(ctx, scope);
styleChanged = true;
}
isValidStrokeFillStyle(style.stroke) && (ctx.strokeStyle = style.stroke);
}
if (forceSetAll || style.opacity !== prevStyle.opacity) {
if (!styleChanged) {
flushPathDrawn(ctx, scope);
styleChanged = true;
}
ctx.globalAlpha = style.opacity == null ? 1 : style.opacity;
}
if (el.hasStroke()) {
const lineWidth = style.lineWidth;
const newLineWidth = lineWidth / (
(style.strokeNoScale && el.getLineScale) ? el.getLineScale() : 1
);
if (ctx.lineWidth !== newLineWidth) {
if (!styleChanged) {
flushPathDrawn(ctx, scope);
styleChanged = true;
}
ctx.lineWidth = newLineWidth;
}
}
for (let i = 0; i < STROKE_PROPS.length; i++) {
const prop = STROKE_PROPS[i];
const propName = prop[0];
if (forceSetAll || style[propName] !== prevStyle[propName]) {
if (!styleChanged) {
flushPathDrawn(ctx, scope);
styleChanged = true;
}
// FIXME Invalid property value will cause style leak from previous element.
(ctx as any)[propName] = style[propName] || prop[1];
}
}
return styleChanged;
}
function bindImageStyle(
ctx: CanvasRenderingContext2D,
el: ZRImage,
prevEl: ZRImage,
// forceSetAll must be true if prevEl is null
forceSetAll: boolean,
scope: BrushScope
) {
return bindCommonProps(
ctx,
getStyle(el, scope.inHover),
prevEl && getStyle(prevEl, scope.inHover),
forceSetAll,
scope
);
}
function setContextTransform(ctx: CanvasRenderingContext2D, el: Displayable) {
const m = el.transform;
const dpr = (ctx as ZRCanvasRenderingContext).dpr || 1;
if (m) {
ctx.setTransform(dpr * m[0], dpr * m[1], dpr * m[2], dpr * m[3], dpr * m[4], dpr * m[5]);
}
else {
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
}
function updateClipStatus(clipPaths: Path[], ctx: CanvasRenderingContext2D, scope: BrushScope) {
let allClipped = false;
for (let i = 0; i < clipPaths.length; i++) {
const clipPath = clipPaths[i];
// Ignore draw following elements if clipPath has zero area.
allClipped = allClipped || clipPath.isZeroArea();
setContextTransform(ctx, clipPath);
ctx.beginPath();
clipPath.buildPath(ctx, clipPath.shape);
ctx.clip();
}
scope.allClipped = allClipped;
}
function isTransformChanged(m0: MatrixArray, m1: MatrixArray): boolean {
if (m0 && m1) {
return m0[0] !== m1[0]
|| m0[1] !== m1[1]
|| m0[2] !== m1[2]
|| m0[3] !== m1[3]
|| m0[4] !== m1[4]
|| m0[5] !== m1[5];
}
else if (!m0 && !m1) { // All identity matrix.
return false;
}
return true;
}
const DRAW_TYPE_PATH = 1;
const DRAW_TYPE_IMAGE = 2;
const DRAW_TYPE_TEXT = 3;
const DRAW_TYPE_INCREMENTAL = 4;
export type BrushScope = {
inHover: boolean
// width / height of viewport
viewWidth: number
viewHeight: number
// Status for clipping
prevElClipPaths?: Path[]
prevEl?: Displayable
allClipped?: boolean // If the whole element can be clipped
// Status for batching
batchFill?: string
batchStroke?: string
lastDrawType?: number
}
// If path can be batched
function canPathBatch(style: PathStyleProps) {
const hasFill = styleHasFill(style);
const hasStroke = styleHasStroke(style);
return !(
// Line dash is dynamically set in brush function.
style.lineDash
// Can't batch if element is both set fill and stroke. Or both not set
|| !(+hasFill ^ +hasStroke)
// Can't batch if element is drawn with gradient or pattern.
|| (hasFill && typeof style.fill !== 'string')
|| (hasStroke && typeof style.stroke !== 'string')
// Can't batch if element only stroke part of line.
|| style.strokePercent < 1
// Has stroke or fill opacity
|| style.strokeOpacity < 1
|| style.fillOpacity < 1
);
}
function flushPathDrawn(ctx: CanvasRenderingContext2D, scope: BrushScope) {
// Force flush all after drawn last element
scope.batchFill && ctx.fill();
scope.batchStroke && ctx.stroke();
scope.batchFill = '';
scope.batchStroke = '';
}
function getStyle(el: Displayable, inHover?: boolean) {
return inHover ? (el.__hoverStyle || el.style) : el.style;
}
export function brushSingle(ctx: CanvasRenderingContext2D, el: Displayable) {
brush(ctx, el, { inHover: false, viewWidth: 0, viewHeight: 0 }, true);
}
// Brush different type of elements.
export function brush(
ctx: CanvasRenderingContext2D,
el: Displayable,
scope: BrushScope,
isLast: boolean
) {
const m = el.transform;
if (!el.shouldBePainted(scope.viewWidth, scope.viewHeight, false, false)) {
// Needs to mark el rendered.
// Or this element will always been rendered in progressive rendering.
// But other dirty bit should not be cleared, otherwise it cause the shape
// can not be updated in this case.
el.__dirty &= ~REDRAW_BIT;
el.__isRendered = false;
return;
}
// HANDLE CLIPPING
const clipPaths = el.__clipPaths;
const prevElClipPaths = scope.prevElClipPaths;
let forceSetTransform = false;
let forceSetStyle = false;
// Optimize when clipping on group with several elements
if (!prevElClipPaths || isClipPathChanged(clipPaths, prevElClipPaths)) {
// If has previous clipping state, restore from it
if (prevElClipPaths && prevElClipPaths.length) {
// Flush restore
flushPathDrawn(ctx, scope);
ctx.restore();
// Must set all style and transform because context changed by restore
forceSetStyle = forceSetTransform = true;
scope.prevElClipPaths = null;
scope.allClipped = false;
// Reset prevEl since context has been restored
scope.prevEl = null;
}
// New clipping state
if (clipPaths && clipPaths.length) {
// Flush before clip
flushPathDrawn(ctx, scope);
ctx.save();
updateClipStatus(clipPaths, ctx, scope);
// Must set transform because it's changed when clip.
forceSetTransform = true;
}
scope.prevElClipPaths = clipPaths;
}
// Not rendering elements if it's clipped by a zero area path.
// Or it may cause bug on some version of IE11 (like 11.0.9600.178**),
// where exception "unexpected call to method or property access"
// might be thrown when calling ctx.fill or ctx.stroke after a path
// whose area size is zero is drawn and ctx.clip() is called and
// shadowBlur is set. See #4572, #3112, #5777.
// (e.g.,
// ctx.moveTo(10, 10);
// ctx.lineTo(20, 10);
// ctx.closePath();
// ctx.clip();
// ctx.shadowBlur = 10;
// ...
// ctx.fill();
// )
if (scope.allClipped) {
el.__isRendered = false;
return;
}
// START BRUSH
el.beforeBrush && el.beforeBrush();
el.innerBeforeBrush();
const prevEl = scope.prevEl;
// TODO el type changed.
if (!prevEl) {
forceSetStyle = forceSetTransform = true;
}
let canBatchPath = el instanceof Path // Only path supports batch
&& el.autoBatch
&& canPathBatch(el.style);
if (forceSetTransform || isTransformChanged(m, prevEl.transform)) {
// Flush
flushPathDrawn(ctx, scope);
setContextTransform(ctx, el);
}
else if (!canBatchPath) {
// Flush
flushPathDrawn(ctx, scope);
}
const style = getStyle(el, scope.inHover);
if (el instanceof Path) {
// PENDING do we need to rebind all style if displayable type changed?
if (scope.lastDrawType !== DRAW_TYPE_PATH) {
forceSetStyle = true;
scope.lastDrawType = DRAW_TYPE_PATH;
}
bindPathAndTextCommonStyle(ctx, el as Path, prevEl as Path, forceSetStyle, scope);
// Begin path at start
if (!canBatchPath || (!scope.batchFill && !scope.batchStroke)) {
ctx.beginPath();
}
brushPath(ctx, el as Path, style, canBatchPath);
if (canBatchPath) {
scope.batchFill = style.fill as string || '';
scope.batchStroke = style.stroke as string || '';
}
}
else {
if (el instanceof TSpan) {
if (scope.lastDrawType !== DRAW_TYPE_TEXT) {
forceSetStyle = true;
scope.lastDrawType = DRAW_TYPE_TEXT;
}
bindPathAndTextCommonStyle(ctx, el as TSpan, prevEl as TSpan, forceSetStyle, scope);
brushText(ctx, el as TSpan, style);
}
else if (el instanceof ZRImage) {
if (scope.lastDrawType !== DRAW_TYPE_IMAGE) {
forceSetStyle = true;
scope.lastDrawType = DRAW_TYPE_IMAGE;
}
bindImageStyle(ctx, el as ZRImage, prevEl as ZRImage, forceSetStyle, scope);
brushImage(ctx, el as ZRImage, style);
}
// Assume it's a IncrementalDisplayable
else if ((el as IncrementalDisplayable).getTemporalDisplayables) {
if (scope.lastDrawType !== DRAW_TYPE_INCREMENTAL) {
forceSetStyle = true;
scope.lastDrawType = DRAW_TYPE_INCREMENTAL;
}
brushIncremental(ctx, el as IncrementalDisplayable, scope);
}
}
if (canBatchPath && isLast) {
flushPathDrawn(ctx, scope);
}
el.innerAfterBrush();
el.afterBrush && el.afterBrush();
scope.prevEl = el;
// Mark as painted.
el.__dirty = 0;
el.__isRendered = true;
}
function brushIncremental(
ctx: CanvasRenderingContext2D,
el: IncrementalDisplayable,
scope: BrushScope
) {
let displayables = el.getDisplayables();
let temporalDisplayables = el.getTemporalDisplayables();
// Provide an inner scope.
// Save current context and restore after brushed.
ctx.save();
let innerScope: BrushScope = {
prevElClipPaths: null,
prevEl: null,
allClipped: false,
viewWidth: scope.viewWidth,
viewHeight: scope.viewHeight,
inHover: scope.inHover
};
let i;
let len;
// Render persistant displayables.
for (i = el.getCursor(), len = displayables.length; i < len; i++) {
const displayable = displayables[i];
displayable.beforeBrush && displayable.beforeBrush();
displayable.innerBeforeBrush();
brush(ctx, displayable, innerScope, i === len - 1);
displayable.innerAfterBrush();
displayable.afterBrush && displayable.afterBrush();
innerScope.prevEl = displayable;
}
// Render temporary displayables.
for (let i = 0, len = temporalDisplayables.length; i < len; i++) {
const displayable = temporalDisplayables[i];
displayable.beforeBrush && displayable.beforeBrush();
displayable.innerBeforeBrush();
brush(ctx, displayable, innerScope, i === len - 1);
displayable.innerAfterBrush();
displayable.afterBrush && displayable.afterBrush();
innerScope.prevEl = displayable;
}
el.clearTemporalDisplayables();
el.notClear = true;
ctx.restore();
}
+127
View File
@@ -0,0 +1,127 @@
import { LinearGradientObject } from '../graphic/LinearGradient';
import { RadialGradientObject } from '../graphic/RadialGradient';
import { GradientObject } from '../graphic/Gradient';
import { RectLike } from '../core/BoundingRect';
import Path from '../graphic/Path';
function isSafeNum(num: number) {
// NaN、Infinity、undefined、'xx'
return isFinite(num);
}
export function createLinearGradient(
this: void,
ctx: CanvasRenderingContext2D,
obj: LinearGradientObject,
rect: RectLike
) {
let x = obj.x == null ? 0 : obj.x;
let x2 = obj.x2 == null ? 1 : obj.x2;
let y = obj.y == null ? 0 : obj.y;
let y2 = obj.y2 == null ? 0 : obj.y2;
if (!obj.global) {
x = x * rect.width + rect.x;
x2 = x2 * rect.width + rect.x;
y = y * rect.height + rect.y;
y2 = y2 * rect.height + rect.y;
}
// Fix NaN when rect is Infinity
x = isSafeNum(x) ? x : 0;
x2 = isSafeNum(x2) ? x2 : 1;
y = isSafeNum(y) ? y : 0;
y2 = isSafeNum(y2) ? y2 : 0;
const canvasGradient = ctx.createLinearGradient(x, y, x2, y2);
return canvasGradient;
}
export function createRadialGradient(
this: void,
ctx: CanvasRenderingContext2D,
obj: RadialGradientObject,
rect: RectLike
) {
const width = rect.width;
const height = rect.height;
const min = Math.min(width, height);
let x = obj.x == null ? 0.5 : obj.x;
let y = obj.y == null ? 0.5 : obj.y;
let r = obj.r == null ? 0.5 : obj.r;
if (!obj.global) {
x = x * width + rect.x;
y = y * height + rect.y;
r = r * min;
}
x = isSafeNum(x) ? x : 0.5;
y = isSafeNum(y) ? y : 0.5;
r = r >= 0 && isSafeNum(r) ? r : 0.5;
const canvasGradient = ctx.createRadialGradient(x, y, 0, x, y, r);
return canvasGradient;
}
export function getCanvasGradient(this: void, ctx: CanvasRenderingContext2D, obj: GradientObject, rect: RectLike) {
// TODO Cache?
const canvasGradient = obj.type === 'radial'
? createRadialGradient(ctx, obj as RadialGradientObject, rect)
: createLinearGradient(ctx, obj as LinearGradientObject, rect);
const colorStops = obj.colorStops;
for (let i = 0; i < colorStops.length; i++) {
canvasGradient.addColorStop(
colorStops[i].offset, colorStops[i].color
);
}
return canvasGradient;
}
export function isClipPathChanged(clipPaths: Path[], prevClipPaths: Path[]): boolean {
// displayable.__clipPaths can only be `null`/`undefined` or an non-empty array.
if (clipPaths === prevClipPaths || (!clipPaths && !prevClipPaths)) {
return false;
}
if (!clipPaths || !prevClipPaths || (clipPaths.length !== prevClipPaths.length)) {
return true;
}
for (let i = 0; i < clipPaths.length; i++) {
if (clipPaths[i] !== prevClipPaths[i]) {
return true;
}
}
return false;
}
function parseInt10(val: string) {
return parseInt(val, 10);
}
export function getSize(
root: HTMLElement,
whIdx: number,
opts: { width?: number | string, height?: number | string}
) {
const wh = ['width', 'height'][whIdx] as 'width' | 'height';
const cwh = ['clientWidth', 'clientHeight'][whIdx] as 'clientWidth' | 'clientHeight';
const plt = ['paddingLeft', 'paddingTop'][whIdx] as 'paddingLeft' | 'paddingTop';
const prb = ['paddingRight', 'paddingBottom'][whIdx] as 'paddingRight' | 'paddingBottom';
if (opts[wh] != null && opts[wh] !== 'auto') {
return parseFloat(opts[wh] as string);
}
// IE8 does not support getComputedStyle, but it use VML.
const stl = document.defaultView.getComputedStyle(root);
return (
(root[cwh] || parseInt10(stl[wh]) || parseInt10(root.style[wh]))
- (parseInt10(stl[plt]) || 0)
- (parseInt10(stl[prb]) || 0)
) | 0;
}
+43
View File
@@ -0,0 +1,43 @@
import env from './core/env';
let dpr = 1;
// If in browser environment
if (env.hasGlobalWindow) {
dpr = Math.max(
window.devicePixelRatio
|| (window.screen && (window.screen as any).deviceXDPI / (window.screen as any).logicalXDPI)
|| 1, 1
);
}
/**
* Debug log mode:
* 0: Do nothing, for release.
* 1: console.error, for debug.
*/
export const debugMode = 0;
// retina 屏幕优化
export const devicePixelRatio = dpr;
/**
* Determine when to turn on dark mode based on the luminance of backgroundColor
*/
export const DARK_MODE_THRESHOLD = 0.4;
/**
* Color of default dark label.
*/
export const DARK_LABEL_COLOR = '#333';
/**
* Color of default light label.
*/
export const LIGHT_LABEL_COLOR = '#ccc';
/**
* Color of default light label.
*/
export const LIGHTER_LABEL_COLOR = '#eee';
+51
View File
@@ -0,0 +1,51 @@
import {normalizeRadian} from './util';
const PI2 = Math.PI * 2;
/**
* 圆弧描边包含判断
*/
export function containStroke(
cx: number, cy: number, r: number, startAngle: number, endAngle: number,
anticlockwise: boolean,
lineWidth: number, x: number, y: number
): boolean {
if (lineWidth === 0) {
return false;
}
const _l = lineWidth;
x -= cx;
y -= cy;
const d = Math.sqrt(x * x + y * y);
if ((d - _l > r) || (d + _l < r)) {
return false;
}
// TODO
if (Math.abs(startAngle - endAngle) % PI2 < 1e-4) {
// Is a circle
return true;
}
if (anticlockwise) {
const tmp = startAngle;
startAngle = normalizeRadian(endAngle);
endAngle = normalizeRadian(tmp);
}
else {
startAngle = normalizeRadian(startAngle);
endAngle = normalizeRadian(endAngle);
}
if (startAngle > endAngle) {
endAngle += PI2;
}
let angle = Math.atan2(y, x);
if (angle < 0) {
angle += PI2;
}
return (angle >= startAngle && angle <= endAngle)
|| (angle + PI2 >= startAngle && angle + PI2 <= endAngle);
}
+30
View File
@@ -0,0 +1,30 @@
import * as curve from '../core/curve';
/**
* 三次贝塞尔曲线描边包含判断
*/
export function containStroke(
x0: number, y0: number, x1: number, y1: number,
x2: number, y2: number, x3: number, y3: number,
lineWidth: number, x: number, y: number
): boolean {
if (lineWidth === 0) {
return false;
}
const _l = lineWidth;
// Quick reject
if (
(y > y0 + _l && y > y1 + _l && y > y2 + _l && y > y3 + _l)
|| (y < y0 - _l && y < y1 - _l && y < y2 - _l && y < y3 - _l)
|| (x > x0 + _l && x > x1 + _l && x > x2 + _l && x > x3 + _l)
|| (x < x0 - _l && x < x1 - _l && x < x2 - _l && x < x3 - _l)
) {
return false;
}
const d = curve.cubicProjectPoint(
x0, y0, x1, y1, x2, y2, x3, y3,
x, y, null
);
return d <= _l / 2;
}
+43
View File
@@ -0,0 +1,43 @@
/**
* 线段包含判断
* @param {number} x0
* @param {number} y0
* @param {number} x1
* @param {number} y1
* @param {number} lineWidth
* @param {number} x
* @param {number} y
* @return {boolean}
*/
export function containStroke(
x0: number, y0: number, x1: number, y1: number,
lineWidth: number, x: number, y: number
): boolean {
if (lineWidth === 0) {
return false;
}
const _l = lineWidth;
let _a = 0;
let _b = x0;
// Quick reject
if (
(y > y0 + _l && y > y1 + _l)
|| (y < y0 - _l && y < y1 - _l)
|| (x > x0 + _l && x > x1 + _l)
|| (x < x0 - _l && x < x1 - _l)
) {
return false;
}
if (x0 !== x1) {
_a = (y0 - y1) / (x0 - x1);
_b = (x0 * y1 - x1 * y0) / (x0 - x1);
}
else {
return Math.abs(x - x0) <= _l / 2;
}
const tmp = _a * x - y + _b;
const _s = tmp * tmp / (_a * _a + 1);
return _s <= _l / 2 * _l / 2;
}
+409
View File
@@ -0,0 +1,409 @@
import PathProxy from '../core/PathProxy';
import * as line from './line';
import * as cubic from './cubic';
import * as quadratic from './quadratic';
import * as arc from './arc';
import * as curve from '../core/curve';
import windingLine from './windingLine';
const CMD = PathProxy.CMD;
const PI2 = Math.PI * 2;
const EPSILON = 1e-4;
function isAroundEqual(a: number, b: number) {
return Math.abs(a - b) < EPSILON;
}
// 临时数组
const roots = [-1, -1, -1];
const extrema = [-1, -1];
function swapExtrema() {
const tmp = extrema[0];
extrema[0] = extrema[1];
extrema[1] = tmp;
}
function windingCubic(
x0: number, y0: number, x1: number, y1: number, x2: number, y2: number, x3: number, y3: number,
x: number, y: number
): number {
// Quick reject
if (
(y > y0 && y > y1 && y > y2 && y > y3)
|| (y < y0 && y < y1 && y < y2 && y < y3)
) {
return 0;
}
const nRoots = curve.cubicRootAt(y0, y1, y2, y3, y, roots);
if (nRoots === 0) {
return 0;
}
else {
let w = 0;
let nExtrema = -1;
let y0_;
let y1_;
for (let i = 0; i < nRoots; i++) {
let t = roots[i];
// Avoid winding error when intersection point is the connect point of two line of polygon
let unit = (t === 0 || t === 1) ? 0.5 : 1;
let x_ = curve.cubicAt(x0, x1, x2, x3, t);
if (x_ < x) { // Quick reject
continue;
}
if (nExtrema < 0) {
nExtrema = curve.cubicExtrema(y0, y1, y2, y3, extrema);
if (extrema[1] < extrema[0] && nExtrema > 1) {
swapExtrema();
}
y0_ = curve.cubicAt(y0, y1, y2, y3, extrema[0]);
if (nExtrema > 1) {
y1_ = curve.cubicAt(y0, y1, y2, y3, extrema[1]);
}
}
if (nExtrema === 2) {
// 分成三段单调函数
if (t < extrema[0]) {
w += y0_ < y0 ? unit : -unit;
}
else if (t < extrema[1]) {
w += y1_ < y0_ ? unit : -unit;
}
else {
w += y3 < y1_ ? unit : -unit;
}
}
else {
// 分成两段单调函数
if (t < extrema[0]) {
w += y0_ < y0 ? unit : -unit;
}
else {
w += y3 < y0_ ? unit : -unit;
}
}
}
return w;
}
}
function windingQuadratic(
x0: number, y0: number, x1: number, y1: number, x2: number, y2: number,
x: number, y: number
): number {
// Quick reject
if (
(y > y0 && y > y1 && y > y2)
|| (y < y0 && y < y1 && y < y2)
) {
return 0;
}
const nRoots = curve.quadraticRootAt(y0, y1, y2, y, roots);
if (nRoots === 0) {
return 0;
}
else {
const t = curve.quadraticExtremum(y0, y1, y2);
if (t >= 0 && t <= 1) {
let w = 0;
let y_ = curve.quadraticAt(y0, y1, y2, t);
for (let i = 0; i < nRoots; i++) {
// Remove one endpoint.
let unit = (roots[i] === 0 || roots[i] === 1) ? 0.5 : 1;
let x_ = curve.quadraticAt(x0, x1, x2, roots[i]);
if (x_ < x) { // Quick reject
continue;
}
if (roots[i] < t) {
w += y_ < y0 ? unit : -unit;
}
else {
w += y2 < y_ ? unit : -unit;
}
}
return w;
}
else {
// Remove one endpoint.
const unit = (roots[0] === 0 || roots[0] === 1) ? 0.5 : 1;
const x_ = curve.quadraticAt(x0, x1, x2, roots[0]);
if (x_ < x) { // Quick reject
return 0;
}
return y2 < y0 ? unit : -unit;
}
}
}
// TODO
// Arc 旋转
// startAngle, endAngle has been normalized by normalizeArcAngles
function windingArc(
cx: number, cy: number, r: number, startAngle: number, endAngle: number, anticlockwise: boolean,
x: number, y: number
) {
y -= cy;
if (y > r || y < -r) {
return 0;
}
const tmp = Math.sqrt(r * r - y * y);
roots[0] = -tmp;
roots[1] = tmp;
const dTheta = Math.abs(startAngle - endAngle);
if (dTheta < 1e-4) {
return 0;
}
if (dTheta >= PI2 - 1e-4) {
// Is a circle
startAngle = 0;
endAngle = PI2;
const dir = anticlockwise ? 1 : -1;
if (x >= roots[0] + cx && x <= roots[1] + cx) {
return dir;
}
else {
return 0;
}
}
if (startAngle > endAngle) {
// Swap, make sure startAngle is smaller than endAngle.
const tmp = startAngle;
startAngle = endAngle;
endAngle = tmp;
}
// endAngle - startAngle is normalized to 0 - 2*PI.
// So following will normalize them to 0 - 4*PI
if (startAngle < 0) {
startAngle += PI2;
endAngle += PI2;
}
let w = 0;
for (let i = 0; i < 2; i++) {
const x_ = roots[i];
if (x_ + cx > x) {
let angle = Math.atan2(y, x_);
let dir = anticlockwise ? 1 : -1;
if (angle < 0) {
angle = PI2 + angle;
}
if (
(angle >= startAngle && angle <= endAngle)
|| (angle + PI2 >= startAngle && angle + PI2 <= endAngle)
) {
if (angle > Math.PI / 2 && angle < Math.PI * 1.5) {
dir = -dir;
}
w += dir;
}
}
}
return w;
}
function containPath(
path: PathProxy, lineWidth: number, isStroke: boolean, x: number, y: number
): boolean {
const data = path.data;
const len = path.len();
let w = 0;
let xi = 0;
let yi = 0;
let x0 = 0;
let y0 = 0;
let x1;
let y1;
for (let i = 0; i < len;) {
const cmd = data[i++];
const isFirst = i === 1;
// Begin a new subpath
if (cmd === CMD.M && i > 1) {
// Close previous subpath
if (!isStroke) {
w += windingLine(xi, yi, x0, y0, x, y);
}
// 如果被任何一个 subpath 包含
// if (w !== 0) {
// return true;
// }
}
if (isFirst) {
// 如果第一个命令是 L, C, Q
// 则 previous point 同绘制命令的第一个 point
//
// 第一个命令为 Arc 的情况下会在后面特殊处理
xi = data[i];
yi = data[i + 1];
x0 = xi;
y0 = yi;
}
switch (cmd) {
case CMD.M:
// moveTo 命令重新创建一个新的 subpath, 并且更新新的起点
// 在 closePath 的时候使用
x0 = data[i++];
y0 = data[i++];
xi = x0;
yi = y0;
break;
case CMD.L:
if (isStroke) {
if (line.containStroke(xi, yi, data[i], data[i + 1], lineWidth, x, y)) {
return true;
}
}
else {
// NOTE 在第一个命令为 L, C, Q 的时候会计算出 NaN
w += windingLine(xi, yi, data[i], data[i + 1], x, y) || 0;
}
xi = data[i++];
yi = data[i++];
break;
case CMD.C:
if (isStroke) {
if (cubic.containStroke(xi, yi,
data[i++], data[i++], data[i++], data[i++], data[i], data[i + 1],
lineWidth, x, y
)) {
return true;
}
}
else {
w += windingCubic(
xi, yi,
data[i++], data[i++], data[i++], data[i++], data[i], data[i + 1],
x, y
) || 0;
}
xi = data[i++];
yi = data[i++];
break;
case CMD.Q:
if (isStroke) {
if (quadratic.containStroke(xi, yi,
data[i++], data[i++], data[i], data[i + 1],
lineWidth, x, y
)) {
return true;
}
}
else {
w += windingQuadratic(
xi, yi,
data[i++], data[i++], data[i], data[i + 1],
x, y
) || 0;
}
xi = data[i++];
yi = data[i++];
break;
case CMD.A:
// TODO Arc 判断的开销比较大
const cx = data[i++];
const cy = data[i++];
const rx = data[i++];
const ry = data[i++];
const theta = data[i++];
const dTheta = data[i++];
// TODO Arc 旋转
i += 1;
const anticlockwise = !!(1 - data[i++]);
x1 = Math.cos(theta) * rx + cx;
y1 = Math.sin(theta) * ry + cy;
// 不是直接使用 arc 命令
if (!isFirst) {
w += windingLine(xi, yi, x1, y1, x, y);
}
else {
// 第一个命令起点还未定义
x0 = x1;
y0 = y1;
}
// zr 使用scale来模拟椭圆, 这里也对x做一定的缩放
const _x = (x - cx) * ry / rx + cx;
if (isStroke) {
if (arc.containStroke(
cx, cy, ry, theta, theta + dTheta, anticlockwise,
lineWidth, _x, y
)) {
return true;
}
}
else {
w += windingArc(
cx, cy, ry, theta, theta + dTheta, anticlockwise,
_x, y
);
}
xi = Math.cos(theta + dTheta) * rx + cx;
yi = Math.sin(theta + dTheta) * ry + cy;
break;
case CMD.R:
x0 = xi = data[i++];
y0 = yi = data[i++];
const width = data[i++];
const height = data[i++];
x1 = x0 + width;
y1 = y0 + height;
if (isStroke) {
if (line.containStroke(x0, y0, x1, y0, lineWidth, x, y)
|| line.containStroke(x1, y0, x1, y1, lineWidth, x, y)
|| line.containStroke(x1, y1, x0, y1, lineWidth, x, y)
|| line.containStroke(x0, y1, x0, y0, lineWidth, x, y)
) {
return true;
}
}
else {
// FIXME Clockwise ?
w += windingLine(x1, y0, x1, y1, x, y);
w += windingLine(x0, y1, x0, y0, x, y);
}
break;
case CMD.Z:
if (isStroke) {
if (line.containStroke(
xi, yi, x0, y0, lineWidth, x, y
)) {
return true;
}
}
else {
// Close a subpath
w += windingLine(xi, yi, x0, y0, x, y);
// 如果被任何一个 subpath 包含
// FIXME subpaths may overlap
// if (w !== 0) {
// return true;
// }
}
xi = x0;
yi = y0;
break;
}
}
if (!isStroke && !isAroundEqual(yi, y0)) {
w += windingLine(xi, yi, x0, y0, x, y) || 0;
}
return w !== 0;
}
export function contain(pathProxy: PathProxy, x: number, y: number): boolean {
return containPath(pathProxy, 0, false, x, y);
}
export function containStroke(pathProxy: PathProxy, lineWidth: number, x: number, y: number): boolean {
return containPath(pathProxy, lineWidth, true, x, y);
}
+31
View File
@@ -0,0 +1,31 @@
import windingLine from './windingLine';
import { VectorArray } from '../core/vector';
const EPSILON = 1e-8;
function isAroundEqual(a: number, b: number): boolean {
return Math.abs(a - b) < EPSILON;
}
export function contain(points: VectorArray[], x: number, y: number) {
let w = 0;
let p = points[0];
if (!p) {
return false;
}
for (let i = 1; i < points.length; i++) {
const p2 = points[i];
w += windingLine(p[0], p[1], p2[0], p2[1], x, y);
p = p2;
}
// Close polygon
const p0 = points[0];
if (!isAroundEqual(p[0], p0[0]) || !isAroundEqual(p[1], p0[1])) {
w += windingLine(p[0], p[1], p0[0], p0[1], x, y);
}
return w !== 0;
}
+28
View File
@@ -0,0 +1,28 @@
import {quadraticProjectPoint} from '../core/curve';
/**
* 二次贝塞尔曲线描边包含判断
*/
export function containStroke(
x0: number, y0: number, x1: number, y1: number, x2: number, y2: number,
lineWidth: number, x: number, y: number
): boolean {
if (lineWidth === 0) {
return false;
}
const _l = lineWidth;
// Quick reject
if (
(y > y0 + _l && y > y1 + _l && y > y2 + _l)
|| (y < y0 - _l && y < y1 - _l && y < y2 - _l)
|| (x > x0 + _l && x > x1 + _l && x > x2 + _l)
|| (x < x0 - _l && x < x1 - _l && x < x2 - _l)
) {
return false;
}
const d = quadraticProjectPoint(
x0, y0, x1, y1, x2, y2,
x, y, null
);
return d <= _l / 2;
}
+240
View File
@@ -0,0 +1,240 @@
import BoundingRect, { RectLike } from '../core/BoundingRect';
import { Dictionary, TextAlign, TextVerticalAlign, BuiltinTextPosition } from '../core/types';
import LRU from '../core/LRU';
import { DEFAULT_FONT, platformApi } from '../core/platform';
let textWidthCache: Dictionary<LRU<number>> = {};
export function getWidth(text: string, font: string): number {
font = font || DEFAULT_FONT;
let cacheOfFont = textWidthCache[font];
if (!cacheOfFont) {
cacheOfFont = textWidthCache[font] = new LRU(500);
}
let width = cacheOfFont.get(text);
if (width == null) {
width = platformApi.measureText(text, font).width;
cacheOfFont.put(text, width);
}
return width;
}
/**
*
* Get bounding rect for inner usage(TSpan)
* Which not include text newline.
*/
export function innerGetBoundingRect(
text: string,
font: string,
textAlign?: TextAlign,
textBaseline?: TextVerticalAlign
): BoundingRect {
const width = getWidth(text, font);
const height = getLineHeight(font);
const x = adjustTextX(0, width, textAlign);
const y = adjustTextY(0, height, textBaseline);
const rect = new BoundingRect(x, y, width, height);
return rect;
}
/**
*
* Get bounding rect for outer usage. Compatitable with old implementation
* Which includes text newline.
*/
export function getBoundingRect(
text: string,
font: string,
textAlign?: TextAlign,
textBaseline?: TextVerticalAlign
) {
const textLines = ((text || '') + '').split('\n');
const len = textLines.length;
if (len === 1) {
return innerGetBoundingRect(textLines[0], font, textAlign, textBaseline);
}
else {
const uniondRect = new BoundingRect(0, 0, 0, 0);
for (let i = 0; i < textLines.length; i++) {
const rect = innerGetBoundingRect(textLines[i], font, textAlign, textBaseline);
i === 0 ? uniondRect.copy(rect) : uniondRect.union(rect);
}
return uniondRect;
}
}
export function adjustTextX(x: number, width: number, textAlign: TextAlign): number {
// TODO Right to left language
if (textAlign === 'right') {
x -= width;
}
else if (textAlign === 'center') {
x -= width / 2;
}
return x;
}
export function adjustTextY(y: number, height: number, verticalAlign: TextVerticalAlign): number {
if (verticalAlign === 'middle') {
y -= height / 2;
}
else if (verticalAlign === 'bottom') {
y -= height;
}
return y;
}
export function getLineHeight(font?: string): number {
// FIXME A rough approach.
return getWidth('国', font);
}
export function measureText(text: string, font?: string): {
width: number
} {
return platformApi.measureText(text, font);
}
export function parsePercent(value: number | string, maxValue: number): number {
if (typeof value === 'string') {
if (value.lastIndexOf('%') >= 0) {
return parseFloat(value) / 100 * maxValue;
}
return parseFloat(value);
}
return value;
}
export interface TextPositionCalculationResult {
x: number
y: number
align: TextAlign
verticalAlign: TextVerticalAlign
}
/**
* Follow same interface to `Displayable.prototype.calculateTextPosition`.
* @public
* @param out Prepared out object. If not input, auto created in the method.
* @param style where `textPosition` and `textDistance` are visited.
* @param rect {x, y, width, height} Rect of the host elment, according to which the text positioned.
* @return The input `out`. Set: {x, y, textAlign, textVerticalAlign}
*/
export function calculateTextPosition(
out: TextPositionCalculationResult,
opts: {
position?: BuiltinTextPosition | (number | string)[]
distance?: number // Default 5
global?: boolean
},
rect: RectLike
): TextPositionCalculationResult {
const textPosition = opts.position || 'inside';
const distance = opts.distance != null ? opts.distance : 5;
const height = rect.height;
const width = rect.width;
const halfHeight = height / 2;
let x = rect.x;
let y = rect.y;
let textAlign: TextAlign = 'left';
let textVerticalAlign: TextVerticalAlign = 'top';
if (textPosition instanceof Array) {
x += parsePercent(textPosition[0], rect.width);
y += parsePercent(textPosition[1], rect.height);
// Not use textAlign / textVerticalAlign
textAlign = null;
textVerticalAlign = null;
}
else {
switch (textPosition) {
case 'left':
x -= distance;
y += halfHeight;
textAlign = 'right';
textVerticalAlign = 'middle';
break;
case 'right':
x += distance + width;
y += halfHeight;
textVerticalAlign = 'middle';
break;
case 'top':
x += width / 2;
y -= distance;
textAlign = 'center';
textVerticalAlign = 'bottom';
break;
case 'bottom':
x += width / 2;
y += height + distance;
textAlign = 'center';
break;
case 'inside':
x += width / 2;
y += halfHeight;
textAlign = 'center';
textVerticalAlign = 'middle';
break;
case 'insideLeft':
x += distance;
y += halfHeight;
textVerticalAlign = 'middle';
break;
case 'insideRight':
x += width - distance;
y += halfHeight;
textAlign = 'right';
textVerticalAlign = 'middle';
break;
case 'insideTop':
x += width / 2;
y += distance;
textAlign = 'center';
break;
case 'insideBottom':
x += width / 2;
y += height - distance;
textAlign = 'center';
textVerticalAlign = 'bottom';
break;
case 'insideTopLeft':
x += distance;
y += distance;
break;
case 'insideTopRight':
x += width - distance;
y += distance;
textAlign = 'right';
break;
case 'insideBottomLeft':
x += distance;
y += height - distance;
textVerticalAlign = 'bottom';
break;
case 'insideBottomRight':
x += width - distance;
y += height - distance;
textAlign = 'right';
textVerticalAlign = 'bottom';
break;
}
}
out = out || {} as TextPositionCalculationResult;
out.x = x;
out.y = y;
out.align = textAlign;
out.verticalAlign = textVerticalAlign;
return out;
}
+10
View File
@@ -0,0 +1,10 @@
const PI2 = Math.PI * 2;
export function normalizeRadian(angle: number): number {
angle %= PI2;
if (angle < 0) {
angle += PI2;
}
return angle;
}
+24
View File
@@ -0,0 +1,24 @@
export default function windingLine(
x0: number, y0: number, x1: number, y1: number, x: number, y: number
): number {
if ((y > y0 && y > y1) || (y < y0 && y < y1)) {
return 0;
}
// Ignore horizontal line
if (y1 === y0) {
return 0;
}
const t = (y - y0) / (y1 - y0);
let dir = y1 < y0 ? 1 : -1;
// Avoid winding error when intersection point is the connect point of two line of polygon
if (t === 1 || t === 0) {
dir = y1 < y0 ? 0.5 : -0.5;
}
const x_ = t * (x1 - x0) + x0;
// If (x, y) on the line, considered as "contain".
return x_ === x ? Infinity : x_ > x ? dir : 0;
}
+289
View File
@@ -0,0 +1,289 @@
/**
* @module echarts/core/BoundingRect
*/
import * as matrix from './matrix';
import Point, { PointLike } from './Point';
const mathMin = Math.min;
const mathMax = Math.max;
const lt = new Point();
const rb = new Point();
const lb = new Point();
const rt = new Point();
const minTv = new Point();
const maxTv = new Point();
class BoundingRect {
x: number
y: number
width: number
height: number
constructor(x: number, y: number, width: number, height: number) {
if (width < 0) {
x = x + width;
width = -width;
}
if (height < 0) {
y = y + height;
height = -height;
}
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
union(other: BoundingRect) {
const x = mathMin(other.x, this.x);
const y = mathMin(other.y, this.y);
// If x is -Infinity and width is Infinity (like in the case of
// IncrementalDisplayble), x + width would be NaN
if (isFinite(this.x) && isFinite(this.width)) {
this.width = mathMax(
other.x + other.width,
this.x + this.width
) - x;
}
else {
this.width = other.width;
}
if (isFinite(this.y) && isFinite(this.height)) {
this.height = mathMax(
other.y + other.height,
this.y + this.height
) - y;
}
else {
this.height = other.height;
}
this.x = x;
this.y = y;
}
applyTransform(m: matrix.MatrixArray) {
BoundingRect.applyTransform(this, this, m);
}
calculateTransform(b: RectLike): matrix.MatrixArray {
const a = this;
const sx = b.width / a.width;
const sy = b.height / a.height;
const m = matrix.create();
// 矩阵右乘
matrix.translate(m, m, [-a.x, -a.y]);
matrix.scale(m, m, [sx, sy]);
matrix.translate(m, m, [b.x, b.y]);
return m;
}
intersect(b: RectLike, mtv?: PointLike): boolean {
if (!b) {
return false;
}
if (!(b instanceof BoundingRect)) {
// Normalize negative width/height.
b = BoundingRect.create(b);
}
const a = this;
const ax0 = a.x;
const ax1 = a.x + a.width;
const ay0 = a.y;
const ay1 = a.y + a.height;
const bx0 = b.x;
const bx1 = b.x + b.width;
const by0 = b.y;
const by1 = b.y + b.height;
let overlap = !(ax1 < bx0 || bx1 < ax0 || ay1 < by0 || by1 < ay0);
if (mtv) {
let dMin = Infinity;
let dMax = 0;
const d0 = Math.abs(ax1 - bx0);
const d1 = Math.abs(bx1 - ax0);
const d2 = Math.abs(ay1 - by0);
const d3 = Math.abs(by1 - ay0);
const dx = Math.min(d0, d1);
const dy = Math.min(d2, d3);
// On x axis
if (ax1 < bx0 || bx1 < ax0) {
if (dx > dMax) {
dMax = dx;
if (d0 < d1) {
Point.set(maxTv, -d0, 0); // b is on the right
}
else {
Point.set(maxTv, d1, 0); // b is on the left
}
}
}
else {
if (dx < dMin) {
dMin = dx;
if (d0 < d1) {
Point.set(minTv, d0, 0); // b is on the right
}
else {
Point.set(minTv, -d1, 0); // b is on the left
}
}
}
// On y axis
if (ay1 < by0 || by1 < ay0) {
if (dy > dMax) {
dMax = dy;
if (d2 < d3) {
Point.set(maxTv, 0, -d2); // b is on the bottom(larger y)
}
else {
Point.set(maxTv, 0, d3); // b is on the top(smaller y)
}
}
}
else {
if (dx < dMin) {
dMin = dx;
if (d2 < d3) {
Point.set(minTv, 0, d2); // b is on the bottom
}
else {
Point.set(minTv, 0, -d3); // b is on the top
}
}
}
}
if (mtv) {
Point.copy(mtv, overlap ? minTv : maxTv);
}
return overlap;
}
contain(x: number, y: number): boolean {
const rect = this;
return x >= rect.x
&& x <= (rect.x + rect.width)
&& y >= rect.y
&& y <= (rect.y + rect.height);
}
clone() {
return new BoundingRect(this.x, this.y, this.width, this.height);
}
/**
* Copy from another rect
*/
copy(other: RectLike) {
BoundingRect.copy(this, other);
}
plain(): RectLike {
return {
x: this.x,
y: this.y,
width: this.width,
height: this.height
};
}
/**
* If not having NaN or Infinity with attributes
*/
isFinite(): boolean {
return isFinite(this.x)
&& isFinite(this.y)
&& isFinite(this.width)
&& isFinite(this.height);
}
isZero(): boolean {
return this.width === 0 || this.height === 0;
}
static create(rect: RectLike): BoundingRect {
return new BoundingRect(rect.x, rect.y, rect.width, rect.height);
}
static copy(target: RectLike, source: RectLike) {
target.x = source.x;
target.y = source.y;
target.width = source.width;
target.height = source.height;
}
static applyTransform(target: RectLike, source: RectLike, m: matrix.MatrixArray) {
// In case usage like this
// el.getBoundingRect().applyTransform(el.transform)
// And element has no transform
if (!m) {
if (target !== source) {
BoundingRect.copy(target, source);
}
return;
}
// Fast path when there is no rotation in matrix.
if (m[1] < 1e-5 && m[1] > -1e-5 && m[2] < 1e-5 && m[2] > -1e-5) {
const sx = m[0];
const sy = m[3];
const tx = m[4];
const ty = m[5];
target.x = source.x * sx + tx;
target.y = source.y * sy + ty;
target.width = source.width * sx;
target.height = source.height * sy;
if (target.width < 0) {
target.x += target.width;
target.width = -target.width;
}
if (target.height < 0) {
target.y += target.height;
target.height = -target.height;
}
return;
}
// source and target can be same instance.
lt.x = lb.x = source.x;
lt.y = rt.y = source.y;
rb.x = rt.x = source.x + source.width;
rb.y = lb.y = source.y + source.height;
lt.transform(m);
rt.transform(m);
rb.transform(m);
lb.transform(m);
target.x = mathMin(lt.x, rb.x, lb.x, rt.x);
target.y = mathMin(lt.y, rb.y, lb.y, rt.y);
const maxX = mathMax(lt.x, rb.x, lb.x, rt.x);
const maxY = mathMax(lt.y, rb.y, lb.y, rt.y);
target.width = maxX - target.x;
target.height = maxY - target.y;
}
}
export type RectLike = {
x: number
y: number
width: number
height: number
}
export default BoundingRect;
+311
View File
@@ -0,0 +1,311 @@
import { Dictionary, WithThisType } from './types';
// Return true to cancel bubble
export type EventCallbackSingleParam<EvtParam = any> = EvtParam extends any
? (params: EvtParam) => boolean | void
: never
export type EventCallback<EvtParams = any[]> = EvtParams extends any[]
? (...args: EvtParams) => boolean | void
: never
export type EventQuery = string | Object
type CbThis<Ctx, Impl> = unknown extends Ctx ? Impl : Ctx;
type EventHandler<Ctx, Impl, EvtParams> = {
h: EventCallback<EvtParams>
ctx: CbThis<Ctx, Impl>
query: EventQuery
callAtLast: boolean
}
type DefaultEventDefinition = Dictionary<EventCallback<any[]>>;
export interface EventProcessor<EvtDef = DefaultEventDefinition> {
normalizeQuery?: (query: EventQuery) => EventQuery
filter?: (eventType: keyof EvtDef, query: EventQuery) => boolean
afterTrigger?: (eventType: keyof EvtDef) => void
}
/**
* Event dispatcher.
*
* Event can be defined in EvtDef to enable type check. For example:
* ```ts
* interface FooEvents {
* // key: event name, value: the first event param in `trigger` and `callback`.
* myevent: {
* aa: string;
* bb: number;
* };
* }
* class Foo extends Eventful<FooEvents> {
* fn() {
* // Type check of event name and the first event param is enabled here.
* this.trigger('myevent', {aa: 'xx', bb: 3});
* }
* }
* let foo = new Foo();
* // Type check of event name and the first event param is enabled here.
* foo.on('myevent', (eventParam) => { ... });
* ```
*
* @param eventProcessor The object eventProcessor is the scope when
* `eventProcessor.xxx` called.
* @param eventProcessor.normalizeQuery
* param: {string|Object} Raw query.
* return: {string|Object} Normalized query.
* @param eventProcessor.filter Event will be dispatched only
* if it returns `true`.
* param: {string} eventType
* param: {string|Object} query
* return: {boolean}
* @param eventProcessor.afterTrigger Called after all handlers called.
* param: {string} eventType
*/
export default class Eventful<EvtDef extends DefaultEventDefinition = DefaultEventDefinition> {
private _$handlers: Dictionary<EventHandler<any, any, any[]>[]>
protected _$eventProcessor: EventProcessor<EvtDef>
constructor(eventProcessors?: EventProcessor<EvtDef>) {
if (eventProcessors) {
this._$eventProcessor = eventProcessors;
}
}
on<Ctx, EvtNm extends keyof EvtDef>(
event: EvtNm,
handler: WithThisType<EvtDef[EvtNm], CbThis<Ctx, this>>,
context?: Ctx
): this
on<Ctx, EvtNm extends keyof EvtDef>(
event: EvtNm,
query: EventQuery,
handler: WithThisType<EvtDef[EvtNm], CbThis<Ctx, this>>,
context?: Ctx
): this
/**
* Bind a handler.
*
* @param event The event name.
* @param Condition used on event filter.
* @param handler The event handler.
* @param context
*/
on<Ctx, EvtNm extends keyof EvtDef>(
event: EvtNm,
query: EventQuery | WithThisType<EventCallback<EvtDef[EvtNm]>, CbThis<Ctx, this>>,
handler?: WithThisType<EventCallback<EvtDef[EvtNm]>, CbThis<Ctx, this>> | Ctx,
context?: Ctx
): this {
if (!this._$handlers) {
this._$handlers = {};
}
const _h = this._$handlers;
if (typeof query === 'function') {
context = handler as Ctx;
handler = query as (...args: any) => any;
query = null;
}
if (!handler || !event) {
return this;
}
const eventProcessor = this._$eventProcessor;
if (query != null && eventProcessor && eventProcessor.normalizeQuery) {
query = eventProcessor.normalizeQuery(query);
}
if (!_h[event as string]) {
_h[event as string] = [];
}
for (let i = 0; i < _h[event as string].length; i++) {
if (_h[event as string][i].h === handler) {
return this;
}
}
const wrap: EventHandler<Ctx, this, unknown[]> = {
h: handler as EventCallback<unknown[]>,
query: query,
ctx: (context || this) as CbThis<Ctx, this>,
// FIXME
// Do not publish this feature util it is proved that it makes sense.
callAtLast: (handler as any).zrEventfulCallAtLast
};
const lastIndex = _h[event as string].length - 1;
const lastWrap = _h[event as string][lastIndex];
(lastWrap && lastWrap.callAtLast)
? _h[event as string].splice(lastIndex, 0, wrap)
: _h[event as string].push(wrap);
return this;
}
/**
* Whether any handler has bound.
*/
isSilent(eventName: keyof EvtDef): boolean {
const _h = this._$handlers;
return !_h || !_h[eventName as string] || !_h[eventName as string].length;
}
/**
* Unbind a event.
*
* @param eventType The event name.
* If no `event` input, "off" all listeners.
* @param handler The event handler.
* If no `handler` input, "off" all listeners of the `event`.
*/
off(eventType?: keyof EvtDef, handler?: Function): this {
const _h = this._$handlers;
if (!_h) {
return this;
}
if (!eventType) {
this._$handlers = {};
return this;
}
if (handler) {
if (_h[eventType as string]) {
const newList = [];
for (let i = 0, l = _h[eventType as string].length; i < l; i++) {
if (_h[eventType as string][i].h !== handler) {
newList.push(_h[eventType as string][i]);
}
}
_h[eventType as string] = newList;
}
if (_h[eventType as string] && _h[eventType as string].length === 0) {
delete _h[eventType as string];
}
}
else {
delete _h[eventType as string];
}
return this;
}
/**
* Dispatch a event.
*
* @param {string} eventType The event name.
*/
trigger<EvtNm extends keyof EvtDef>(
eventType: EvtNm,
...args: Parameters<EvtDef[EvtNm]>
): this {
if (!this._$handlers) {
return this;
}
const _h = this._$handlers[eventType as string];
const eventProcessor = this._$eventProcessor;
if (_h) {
const argLen = args.length;
const len = _h.length;
for (let i = 0; i < len; i++) {
const hItem = _h[i];
if (eventProcessor
&& eventProcessor.filter
&& hItem.query != null
&& !eventProcessor.filter(eventType, hItem.query)
) {
continue;
}
// Optimize advise from backbone
switch (argLen) {
case 0:
hItem.h.call(hItem.ctx);
break;
case 1:
hItem.h.call(hItem.ctx, args[0]);
break;
case 2:
hItem.h.call(hItem.ctx, args[0], args[1]);
break;
default:
// have more than 2 given arguments
hItem.h.apply(hItem.ctx, args);
break;
}
}
}
eventProcessor && eventProcessor.afterTrigger
&& eventProcessor.afterTrigger(eventType);
return this;
}
/**
* Dispatch a event with context, which is specified at the last parameter.
*
* @param {string} type The event name.
*/
triggerWithContext(type: keyof EvtDef, ...args: any[]): this {
if (!this._$handlers) {
return this;
}
const _h = this._$handlers[type as string];
const eventProcessor = this._$eventProcessor;
if (_h) {
const argLen = args.length;
const ctx = args[argLen - 1];
const len = _h.length;
for (let i = 0; i < len; i++) {
const hItem = _h[i];
if (eventProcessor
&& eventProcessor.filter
&& hItem.query != null
&& !eventProcessor.filter(type, hItem.query)
) {
continue;
}
// Optimize advise from backbone
switch (argLen) {
case 0:
hItem.h.call(ctx);
break;
case 1:
hItem.h.call(ctx, args[0]);
break;
case 2:
hItem.h.call(ctx, args[0], args[1]);
break;
default:
// have more than 2 given arguments
hItem.h.apply(ctx, args.slice(1, argLen - 1));
break;
}
}
}
eventProcessor && eventProcessor.afterTrigger
&& eventProcessor.afterTrigger(type);
return this;
}
}
+123
View File
@@ -0,0 +1,123 @@
/**
* Only implements needed gestures for mobile.
*/
import * as eventUtil from './event';
import { ZRRawTouchEvent, ZRPinchEvent, Dictionary } from './types';
import Displayable from '../graphic/Displayable';
interface TrackItem {
points: number[][]
touches: Touch[]
target: Displayable,
event: ZRRawTouchEvent
}
export class GestureMgr {
private _track: TrackItem[] = []
constructor() {}
recognize(event: ZRRawTouchEvent, target: Displayable, root: HTMLElement) {
this._doTrack(event, target, root);
return this._recognize(event);
}
clear() {
this._track.length = 0;
return this;
}
_doTrack(event: ZRRawTouchEvent, target: Displayable, root: HTMLElement) {
const touches = event.touches;
if (!touches) {
return;
}
const trackItem: TrackItem = {
points: [],
touches: [],
target: target,
event: event
};
for (let i = 0, len = touches.length; i < len; i++) {
const touch = touches[i];
const pos = eventUtil.clientToLocal(root, touch, {});
trackItem.points.push([pos.zrX, pos.zrY]);
trackItem.touches.push(touch);
}
this._track.push(trackItem);
}
_recognize(event: ZRRawTouchEvent) {
for (let eventName in recognizers) {
if (recognizers.hasOwnProperty(eventName)) {
const gestureInfo = recognizers[eventName](this._track, event);
if (gestureInfo) {
return gestureInfo;
}
}
}
}
}
function dist(pointPair: number[][]): number {
const dx = pointPair[1][0] - pointPair[0][0];
const dy = pointPair[1][1] - pointPair[0][1];
return Math.sqrt(dx * dx + dy * dy);
}
function center(pointPair: number[][]): number[] {
return [
(pointPair[0][0] + pointPair[1][0]) / 2,
(pointPair[0][1] + pointPair[1][1]) / 2
];
}
type Recognizer = (tracks: TrackItem[], event: ZRRawTouchEvent) => {
type: string
target: Displayable
event: ZRRawTouchEvent
}
const recognizers: Dictionary<Recognizer> = {
pinch: function (tracks: TrackItem[], event: ZRRawTouchEvent) {
const trackLen = tracks.length;
if (!trackLen) {
return;
}
const pinchEnd = (tracks[trackLen - 1] || {}).points;
const pinchPre = (tracks[trackLen - 2] || {}).points || pinchEnd;
if (pinchPre
&& pinchPre.length > 1
&& pinchEnd
&& pinchEnd.length > 1
) {
let pinchScale = dist(pinchEnd) / dist(pinchPre);
!isFinite(pinchScale) && (pinchScale = 1);
(event as ZRPinchEvent).pinchScale = pinchScale;
const pinchCenter = center(pinchEnd);
(event as ZRPinchEvent).pinchX = pinchCenter[0];
(event as ZRPinchEvent).pinchY = pinchCenter[1];
return {
type: 'pinch',
target: tracks[0].target,
event: event
};
}
}
// Only pinch currently.
};
+175
View File
@@ -0,0 +1,175 @@
import { Dictionary } from './types';
// Simple LRU cache use doubly linked list
// @module zrender/core/LRU
export class Entry<T> {
value: T
key: string | number
next: Entry<T>
prev: Entry<T>
constructor(val: T) {
this.value = val;
}
}
/**
* Simple double linked list. Compared with array, it has O(1) remove operation.
* @constructor
*/
export class LinkedList<T> {
head: Entry<T>
tail: Entry<T>
private _len = 0
/**
* Insert a new value at the tail
*/
insert(val: T): Entry<T> {
const entry = new Entry(val);
this.insertEntry(entry);
return entry;
}
/**
* Insert an entry at the tail
*/
insertEntry(entry: Entry<T>) {
if (!this.head) {
this.head = this.tail = entry;
}
else {
this.tail.next = entry;
entry.prev = this.tail;
entry.next = null;
this.tail = entry;
}
this._len++;
}
/**
* Remove entry.
*/
remove(entry: Entry<T>) {
const prev = entry.prev;
const next = entry.next;
if (prev) {
prev.next = next;
}
else {
// Is head
this.head = next;
}
if (next) {
next.prev = prev;
}
else {
// Is tail
this.tail = prev;
}
entry.next = entry.prev = null;
this._len--;
}
/**
* Get length
*/
len(): number {
return this._len;
}
/**
* Clear list
*/
clear() {
this.head = this.tail = null;
this._len = 0;
}
}
/**
* LRU Cache
*/
export default class LRU<T> {
private _list = new LinkedList<T>()
private _maxSize = 10
private _lastRemovedEntry: Entry<T>
private _map: Dictionary<Entry<T>> = {}
constructor(maxSize: number) {
this._maxSize = maxSize;
}
/**
* @return Removed value
*/
put(key: string | number, value: T): T {
const list = this._list;
const map = this._map;
let removed = null;
if (map[key] == null) {
const len = list.len();
// Reuse last removed entry
let entry = this._lastRemovedEntry;
if (len >= this._maxSize && len > 0) {
// Remove the least recently used
const leastUsedEntry = list.head;
list.remove(leastUsedEntry);
delete map[leastUsedEntry.key];
removed = leastUsedEntry.value;
this._lastRemovedEntry = leastUsedEntry;
}
if (entry) {
entry.value = value;
}
else {
entry = new Entry(value);
}
entry.key = key;
list.insertEntry(entry);
map[key] = entry;
}
return removed;
}
get(key: string | number): T {
const entry = this._map[key];
const list = this._list;
if (entry != null) {
// Put the latest used entry in the tail
if (entry !== list.tail) {
list.remove(entry);
list.insertEntry(entry);
}
return entry.value;
}
}
/**
* Clear the cache
*/
clear() {
this._list.clear();
this._map = {};
}
len() {
return this._list.len();
}
}
+187
View File
@@ -0,0 +1,187 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import Point, { PointLike } from './Point';
import BoundingRect from './BoundingRect';
import { MatrixArray } from './matrix';
const extent = [0, 0];
const extent2 = [0, 0];
const minTv = new Point();
const maxTv = new Point();
class OrientedBoundingRect {
// lt, rt, rb, lb
private _corners: Point[] = [];
private _axes: Point[] = [];
private _origin: number[] = [0, 0];
constructor(rect?: BoundingRect, transform?: MatrixArray) {
for (let i = 0; i < 4; i++) {
this._corners[i] = new Point();
}
for (let i = 0; i < 2; i++) {
this._axes[i] = new Point();
}
if (rect) {
this.fromBoundingRect(rect, transform);
}
}
fromBoundingRect(rect: BoundingRect, transform?: MatrixArray) {
const corners = this._corners;
const axes = this._axes;
const x = rect.x;
const y = rect.y;
const x2 = x + rect.width;
const y2 = y + rect.height;
corners[0].set(x, y);
corners[1].set(x2, y);
corners[2].set(x2, y2);
corners[3].set(x, y2);
if (transform) {
for (let i = 0; i < 4; i++) {
corners[i].transform(transform);
}
}
// Calculate axes
Point.sub(axes[0], corners[1], corners[0]);
Point.sub(axes[1], corners[3], corners[0]);
axes[0].normalize();
axes[1].normalize();
// Calculate projected origin
for (let i = 0; i < 2; i++) {
this._origin[i] = axes[i].dot(corners[0]);
}
}
/**
* If intersect with another OBB
* @param other Bounding rect to be intersected with
* @param mtv Calculated .
* If it's not overlapped. it means needs to move given rect with Maximum Translation Vector to be overlapped.
* Else it means needs to move given rect with Minimum Translation Vector to be not overlapped.
*/
intersect(other: OrientedBoundingRect, mtv?: PointLike): boolean {
// OBB collision with SAT method
let overlapped = true;
const noMtv = !mtv;
minTv.set(Infinity, Infinity);
maxTv.set(0, 0);
// Check two axes for both two obb.
if (!this._intersectCheckOneSide(this, other, minTv, maxTv, noMtv, 1)) {
overlapped = false;
if (noMtv) {
// Early return if no need to calculate mtv
return overlapped;
}
}
if (!this._intersectCheckOneSide(other, this, minTv, maxTv, noMtv, -1)) {
overlapped = false;
if (noMtv) {
return overlapped;
}
}
if (!noMtv) {
Point.copy(mtv, overlapped ? minTv : maxTv);
}
return overlapped;
}
private _intersectCheckOneSide(
self: OrientedBoundingRect,
other: OrientedBoundingRect,
minTv: Point,
maxTv: Point,
noMtv: boolean,
inverse: 1 | -1
): boolean {
let overlapped = true;
for (let i = 0; i < 2; i++) {
const axis = this._axes[i];
this._getProjMinMaxOnAxis(i, self._corners, extent);
this._getProjMinMaxOnAxis(i, other._corners, extent2);
// Not overlap on the any axis.
if (extent[1] < extent2[0] || extent[0] > extent2[1]) {
overlapped = false;
if (noMtv) {
return overlapped;
}
const dist0 = Math.abs(extent2[0] - extent[1]);
const dist1 = Math.abs(extent[0] - extent2[1]);
// Find longest distance of all axes.
if (Math.min(dist0, dist1) > maxTv.len()) {
if (dist0 < dist1) {
Point.scale(maxTv, axis, -dist0 * inverse);
}
else {
Point.scale(maxTv, axis, dist1 * inverse);
}
}
}
else if (minTv) {
const dist0 = Math.abs(extent2[0] - extent[1]);
const dist1 = Math.abs(extent[0] - extent2[1]);
if (Math.min(dist0, dist1) < minTv.len()) {
if (dist0 < dist1) {
Point.scale(minTv, axis, dist0 * inverse);
}
else {
Point.scale(minTv, axis, -dist1 * inverse);
}
}
}
}
return overlapped;
}
private _getProjMinMaxOnAxis(dim: number, corners: Point[], out: number[]) {
const axis = this._axes[dim];
const origin = this._origin;
const proj = corners[0].dot(axis) + origin[dim];
let min = proj;
let max = proj;
for (let i = 1; i < corners.length; i++) {
const proj = corners[i].dot(axis) + origin[dim];
min = Math.min(proj, min);
max = Math.max(proj, max);
}
out[0] = min;
out[1] = max;
}
}
export default OrientedBoundingRect;
+994
View File
@@ -0,0 +1,994 @@
/**
* Path 代理,可以在`buildPath`中用于替代`ctx`, 会保存每个path操作的命令到pathCommands属性中
* 可以用于 isInsidePath 判断以及获取boundingRect
*/
// TODO getTotalLength, getPointAtLength, arcTo
/* global Float32Array */
import * as vec2 from './vector';
import BoundingRect from './BoundingRect';
import {devicePixelRatio as dpr} from '../config';
import { fromLine, fromCubic, fromQuadratic, fromArc } from './bbox';
import { cubicLength, cubicSubdivide, quadraticLength, quadraticSubdivide } from './curve';
const CMD = {
M: 1,
L: 2,
C: 3,
Q: 4,
A: 5,
Z: 6,
// Rect
R: 7
};
// const CMD_MEM_SIZE = {
// M: 3,
// L: 3,
// C: 7,
// Q: 5,
// A: 9,
// R: 5,
// Z: 1
// };
interface ExtendedCanvasRenderingContext2D extends CanvasRenderingContext2D {
dpr?: number
}
const tmpOutX: number[] = [];
const tmpOutY: number[] = [];
const min: number[] = [];
const max: number[] = [];
const min2: number[] = [];
const max2: number[] = [];
const mathMin = Math.min;
const mathMax = Math.max;
const mathCos = Math.cos;
const mathSin = Math.sin;
const mathAbs = Math.abs;
const PI = Math.PI;
const PI2 = PI * 2;
const hasTypedArray = typeof Float32Array !== 'undefined';
const tmpAngles: number[] = [];
function modPI2(radian: number) {
// It's much more stable to mod N instedof PI
const n = Math.round(radian / PI * 1e8) / 1e8;
return (n % 2) * PI;
}
/**
* Normalize start and end angles.
* startAngle will be normalized to 0 ~ PI*2
* sweepAngle(endAngle - startAngle) will be normalized to 0 ~ PI*2 if clockwise.
* -PI*2 ~ 0 if anticlockwise.
*/
export function normalizeArcAngles(angles: number[], anticlockwise: boolean): void {
let newStartAngle = modPI2(angles[0]);
if (newStartAngle < 0) {
// Normlize to 0 - PI2
newStartAngle += PI2;
}
let delta = newStartAngle - angles[0];
let newEndAngle = angles[1];
newEndAngle += delta;
// https://github.com/chromium/chromium/blob/c20d681c9c067c4e15bb1408f17114b9e8cba294/third_party/blink/renderer/modules/canvas/canvas2d/canvas_path.cc#L184
// Is circle
if (!anticlockwise && newEndAngle - newStartAngle >= PI2) {
newEndAngle = newStartAngle + PI2;
}
else if (anticlockwise && newStartAngle - newEndAngle >= PI2) {
newEndAngle = newStartAngle - PI2;
}
// Make startAngle < endAngle when clockwise, otherwise endAngle < startAngle.
// The sweep angle can never been larger than P2.
else if (!anticlockwise && newStartAngle > newEndAngle) {
newEndAngle = newStartAngle + (PI2 - modPI2(newStartAngle - newEndAngle));
}
else if (anticlockwise && newStartAngle < newEndAngle) {
newEndAngle = newStartAngle - (PI2 - modPI2(newEndAngle - newStartAngle));
}
angles[0] = newStartAngle;
angles[1] = newEndAngle;
}
export default class PathProxy {
dpr = 1
data: number[] | Float32Array
/**
* Version is for tracking if the path has been changed.
*/
private _version: number
/**
* If save path data.
*/
private _saveData: boolean
/**
* If the line segment is too small to draw. It will be added to the pending pt.
* It will be added if the subpath needs to be finished before stroke, fill, or starting a new subpath.
*/
private _pendingPtX: number;
private _pendingPtY: number;
// Distance of pending pt to previous point.
// 0 if there is no pending point.
// Only update the pending pt when distance is larger.
private _pendingPtDist: number;
private _ctx: ExtendedCanvasRenderingContext2D
private _xi = 0
private _yi = 0
private _x0 = 0
private _y0 = 0
private _len = 0
// Calculating path len and seg len.
private _pathSegLen: number[]
private _pathLen: number
// Unit x, Unit y. Provide for avoiding drawing that too short line segment
private _ux: number
private _uy: number
static CMD = CMD
constructor(notSaveData?: boolean) {
if (notSaveData) {
this._saveData = false;
}
if (this._saveData) {
this.data = [];
}
}
increaseVersion() {
this._version++;
}
/**
* Version can be used outside for compare if the path is changed.
* For example to determine if need to update svg d str in svg renderer.
*/
getVersion() {
return this._version;
}
/**
* @readOnly
*/
setScale(sx: number, sy: number, segmentIgnoreThreshold?: number) {
// Compat. Previously there is no segmentIgnoreThreshold.
segmentIgnoreThreshold = segmentIgnoreThreshold || 0;
if (segmentIgnoreThreshold > 0) {
this._ux = mathAbs(segmentIgnoreThreshold / dpr / sx) || 0;
this._uy = mathAbs(segmentIgnoreThreshold / dpr / sy) || 0;
}
}
setDPR(dpr: number) {
this.dpr = dpr;
}
setContext(ctx: ExtendedCanvasRenderingContext2D) {
this._ctx = ctx;
}
getContext(): ExtendedCanvasRenderingContext2D {
return this._ctx;
}
beginPath() {
this._ctx && this._ctx.beginPath();
this.reset();
return this;
}
/**
* Reset path data.
*/
reset() {
// Reset
if (this._saveData) {
this._len = 0;
}
if (this._pathSegLen) {
this._pathSegLen = null;
this._pathLen = 0;
}
// Update version
this._version++;
}
moveTo(x: number, y: number) {
// Add pending point for previous path.
this._drawPendingPt();
this.addData(CMD.M, x, y);
this._ctx && this._ctx.moveTo(x, y);
// x0, y0, xi, yi 是记录在 _dashedXXXXTo 方法中使用
// xi, yi 记录当前点, x0, y0 在 closePath 的时候回到起始点。
// 有可能在 beginPath 之后直接调用 lineTo,这时候 x0, y0 需要
// 在 lineTo 方法中记录,这里先不考虑这种情况,dashed line 也只在 IE10- 中不支持
this._x0 = x;
this._y0 = y;
this._xi = x;
this._yi = y;
return this;
}
lineTo(x: number, y: number) {
const dx = mathAbs(x - this._xi);
const dy = mathAbs(y - this._yi);
const exceedUnit = dx > this._ux || dy > this._uy;
this.addData(CMD.L, x, y);
if (this._ctx && exceedUnit) {
this._ctx.lineTo(x, y);
}
if (exceedUnit) {
this._xi = x;
this._yi = y;
this._pendingPtDist = 0;
}
else {
const d2 = dx * dx + dy * dy;
// Only use the farthest pending point.
if (d2 > this._pendingPtDist) {
this._pendingPtX = x;
this._pendingPtY = y;
this._pendingPtDist = d2;
}
}
return this;
}
bezierCurveTo(x1: number, y1: number, x2: number, y2: number, x3: number, y3: number) {
this._drawPendingPt();
this.addData(CMD.C, x1, y1, x2, y2, x3, y3);
if (this._ctx) {
this._ctx.bezierCurveTo(x1, y1, x2, y2, x3, y3);
}
this._xi = x3;
this._yi = y3;
return this;
}
quadraticCurveTo(x1: number, y1: number, x2: number, y2: number) {
this._drawPendingPt();
this.addData(CMD.Q, x1, y1, x2, y2);
if (this._ctx) {
this._ctx.quadraticCurveTo(x1, y1, x2, y2);
}
this._xi = x2;
this._yi = y2;
return this;
}
arc(cx: number, cy: number, r: number, startAngle: number, endAngle: number, anticlockwise?: boolean) {
this._drawPendingPt();
tmpAngles[0] = startAngle;
tmpAngles[1] = endAngle;
normalizeArcAngles(tmpAngles, anticlockwise);
startAngle = tmpAngles[0];
endAngle = tmpAngles[1];
let delta = endAngle - startAngle;
this.addData(
CMD.A, cx, cy, r, r, startAngle, delta, 0, anticlockwise ? 0 : 1
);
this._ctx && this._ctx.arc(cx, cy, r, startAngle, endAngle, anticlockwise);
this._xi = mathCos(endAngle) * r + cx;
this._yi = mathSin(endAngle) * r + cy;
return this;
}
// TODO
arcTo(x1: number, y1: number, x2: number, y2: number, radius: number) {
this._drawPendingPt();
if (this._ctx) {
this._ctx.arcTo(x1, y1, x2, y2, radius);
}
return this;
}
// TODO
rect(x: number, y: number, w: number, h: number) {
this._drawPendingPt();
this._ctx && this._ctx.rect(x, y, w, h);
this.addData(CMD.R, x, y, w, h);
return this;
}
closePath() {
// Add pending point for previous path.
this._drawPendingPt();
this.addData(CMD.Z);
const ctx = this._ctx;
const x0 = this._x0;
const y0 = this._y0;
if (ctx) {
ctx.closePath();
}
this._xi = x0;
this._yi = y0;
return this;
}
fill(ctx: CanvasRenderingContext2D) {
ctx && ctx.fill();
this.toStatic();
}
stroke(ctx: CanvasRenderingContext2D) {
ctx && ctx.stroke();
this.toStatic();
}
len() {
return this._len;
}
setData(data: Float32Array | number[]) {
const len = data.length;
if (!(this.data && this.data.length === len) && hasTypedArray) {
this.data = new Float32Array(len);
}
for (let i = 0; i < len; i++) {
this.data[i] = data[i];
}
this._len = len;
}
appendPath(path: PathProxy | PathProxy[]) {
if (!(path instanceof Array)) {
path = [path];
}
const len = path.length;
let appendSize = 0;
let offset = this._len;
for (let i = 0; i < len; i++) {
appendSize += path[i].len();
}
if (hasTypedArray && (this.data instanceof Float32Array)) {
this.data = new Float32Array(offset + appendSize);
}
for (let i = 0; i < len; i++) {
const appendPathData = path[i].data;
for (let k = 0; k < appendPathData.length; k++) {
this.data[offset++] = appendPathData[k];
}
}
this._len = offset;
}
/**
* 填充 Path 数据。
* 尽量复用而不申明新的数组。大部分图形重绘的指令数据长度都是不变的。
*/
addData(
cmd: number,
a?: number,
b?: number,
c?: number,
d?: number,
e?: number,
f?: number,
g?: number,
h?: number
) {
if (!this._saveData) {
return;
}
let data = this.data;
if (this._len + arguments.length > data.length) {
// 因为之前的数组已经转换成静态的 Float32Array
// 所以不够用时需要扩展一个新的动态数组
this._expandData();
data = this.data;
}
for (let i = 0; i < arguments.length; i++) {
data[this._len++] = arguments[i];
}
}
private _drawPendingPt() {
if (this._pendingPtDist > 0) {
this._ctx && this._ctx.lineTo(this._pendingPtX, this._pendingPtY);
this._pendingPtDist = 0;
}
}
private _expandData() {
// Only if data is Float32Array
if (!(this.data instanceof Array)) {
const newData = [];
for (let i = 0; i < this._len; i++) {
newData[i] = this.data[i];
}
this.data = newData;
}
}
/**
* Convert dynamic array to static Float32Array
*
* It will still use a normal array if command buffer length is less than 10
* Because Float32Array itself may take more memory than a normal array.
*
* 10 length will make sure at least one M command and one A(arc) command.
*/
toStatic() {
if (!this._saveData) {
return;
}
this._drawPendingPt();
const data = this.data;
if (data instanceof Array) {
data.length = this._len;
if (hasTypedArray && this._len > 11) {
this.data = new Float32Array(data);
}
}
}
getBoundingRect() {
min[0] = min[1] = min2[0] = min2[1] = Number.MAX_VALUE;
max[0] = max[1] = max2[0] = max2[1] = -Number.MAX_VALUE;
const data = this.data;
let xi = 0;
let yi = 0;
let x0 = 0;
let y0 = 0;
let i;
for (i = 0; i < this._len;) {
const cmd = data[i++] as number;
const isFirst = i === 1;
if (isFirst) {
// 如果第一个命令是 L, C, Q
// 则 previous point 同绘制命令的第一个 point
// 第一个命令为 Arc 的情况下会在后面特殊处理
xi = data[i];
yi = data[i + 1];
x0 = xi;
y0 = yi;
}
switch (cmd) {
case CMD.M:
// moveTo 命令重新创建一个新的 subpath, 并且更新新的起点
// 在 closePath 的时候使用
xi = x0 = data[i++];
yi = y0 = data[i++];
min2[0] = x0;
min2[1] = y0;
max2[0] = x0;
max2[1] = y0;
break;
case CMD.L:
fromLine(xi, yi, data[i], data[i + 1], min2, max2);
xi = data[i++];
yi = data[i++];
break;
case CMD.C:
fromCubic(
xi, yi, data[i++], data[i++], data[i++], data[i++], data[i], data[i + 1],
min2, max2
);
xi = data[i++];
yi = data[i++];
break;
case CMD.Q:
fromQuadratic(
xi, yi, data[i++], data[i++], data[i], data[i + 1],
min2, max2
);
xi = data[i++];
yi = data[i++];
break;
case CMD.A:
const cx = data[i++];
const cy = data[i++];
const rx = data[i++];
const ry = data[i++];
const startAngle = data[i++];
const endAngle = data[i++] + startAngle;
// TODO Arc 旋转
i += 1;
const anticlockwise = !data[i++];
if (isFirst) {
// 直接使用 arc 命令
// 第一个命令起点还未定义
x0 = mathCos(startAngle) * rx + cx;
y0 = mathSin(startAngle) * ry + cy;
}
fromArc(
cx, cy, rx, ry, startAngle, endAngle,
anticlockwise, min2, max2
);
xi = mathCos(endAngle) * rx + cx;
yi = mathSin(endAngle) * ry + cy;
break;
case CMD.R:
x0 = xi = data[i++];
y0 = yi = data[i++];
const width = data[i++];
const height = data[i++];
// Use fromLine
fromLine(x0, y0, x0 + width, y0 + height, min2, max2);
break;
case CMD.Z:
xi = x0;
yi = y0;
break;
}
// Union
vec2.min(min, min, min2);
vec2.max(max, max, max2);
}
// No data
if (i === 0) {
min[0] = min[1] = max[0] = max[1] = 0;
}
return new BoundingRect(
min[0], min[1], max[0] - min[0], max[1] - min[1]
);
}
private _calculateLength(): number {
const data = this.data;
const len = this._len;
const ux = this._ux;
const uy = this._uy;
let xi = 0;
let yi = 0;
let x0 = 0;
let y0 = 0;
if (!this._pathSegLen) {
this._pathSegLen = [];
}
const pathSegLen = this._pathSegLen;
let pathTotalLen = 0;
let segCount = 0;
for (let i = 0; i < len;) {
const cmd = data[i++] as number;
const isFirst = i === 1;
if (isFirst) {
// 如果第一个命令是 L, C, Q
// 则 previous point 同绘制命令的第一个 point
// 第一个命令为 Arc 的情况下会在后面特殊处理
xi = data[i];
yi = data[i + 1];
x0 = xi;
y0 = yi;
}
let l = -1;
switch (cmd) {
case CMD.M:
// moveTo 命令重新创建一个新的 subpath, 并且更新新的起点
// 在 closePath 的时候使用
xi = x0 = data[i++];
yi = y0 = data[i++];
break;
case CMD.L: {
const x2 = data[i++];
const y2 = data[i++];
const dx = x2 - xi;
const dy = y2 - yi;
if (mathAbs(dx) > ux || mathAbs(dy) > uy || i === len - 1) {
l = Math.sqrt(dx * dx + dy * dy);
xi = x2;
yi = y2;
}
break;
}
case CMD.C: {
const x1 = data[i++];
const y1 = data[i++];
const x2 = data[i++];
const y2 = data[i++];
const x3 = data[i++];
const y3 = data[i++];
// TODO adaptive iteration
l = cubicLength(xi, yi, x1, y1, x2, y2, x3, y3, 10);
xi = x3;
yi = y3;
break;
}
case CMD.Q: {
const x1 = data[i++];
const y1 = data[i++];
const x2 = data[i++];
const y2 = data[i++];
l = quadraticLength(xi, yi, x1, y1, x2, y2, 10);
xi = x2;
yi = y2;
break;
}
case CMD.A:
// TODO Arc 判断的开销比较大
const cx = data[i++];
const cy = data[i++];
const rx = data[i++];
const ry = data[i++];
const startAngle = data[i++];
let delta = data[i++];
const endAngle = delta + startAngle;
// TODO Arc 旋转
i += 1;
if (isFirst) {
// 直接使用 arc 命令
// 第一个命令起点还未定义
x0 = mathCos(startAngle) * rx + cx;
y0 = mathSin(startAngle) * ry + cy;
}
// TODO Ellipse
l = mathMax(rx, ry) * mathMin(PI2, Math.abs(delta));
xi = mathCos(endAngle) * rx + cx;
yi = mathSin(endAngle) * ry + cy;
break;
case CMD.R: {
x0 = xi = data[i++];
y0 = yi = data[i++];
const width = data[i++];
const height = data[i++];
l = width * 2 + height * 2;
break;
}
case CMD.Z: {
const dx = x0 - xi;
const dy = y0 - yi;
l = Math.sqrt(dx * dx + dy * dy);
xi = x0;
yi = y0;
break;
}
}
if (l >= 0) {
pathSegLen[segCount++] = l;
pathTotalLen += l;
}
}
// TODO Optimize memory cost.
this._pathLen = pathTotalLen;
return pathTotalLen;
}
/**
* Rebuild path from current data
* Rebuild path will not consider javascript implemented line dash.
* @param {CanvasRenderingContext2D} ctx
*/
rebuildPath(ctx: PathRebuilder, percent: number) {
const d = this.data;
const ux = this._ux;
const uy = this._uy;
const len = this._len;
let x0;
let y0;
let xi;
let yi;
let x;
let y;
const drawPart = percent < 1;
let pathSegLen;
let pathTotalLen;
let accumLength = 0;
let segCount = 0;
let displayedLength;
let pendingPtDist = 0;
let pendingPtX: number;
let pendingPtY: number;
if (drawPart) {
if (!this._pathSegLen) {
this._calculateLength();
}
pathSegLen = this._pathSegLen;
pathTotalLen = this._pathLen;
displayedLength = percent * pathTotalLen;
if (!displayedLength) {
return;
}
}
lo: for (let i = 0; i < len;) {
const cmd = d[i++];
const isFirst = i === 1;
if (isFirst) {
// 如果第一个命令是 L, C, Q
// 则 previous point 同绘制命令的第一个 point
// 第一个命令为 Arc 的情况下会在后面特殊处理
xi = d[i];
yi = d[i + 1];
x0 = xi;
y0 = yi;
}
// Only lineTo support ignoring small segments.
// Otherwise if the pending point should always been flushed.
if (cmd !== CMD.L && pendingPtDist > 0) {
ctx.lineTo(pendingPtX, pendingPtY);
pendingPtDist = 0;
}
switch (cmd) {
case CMD.M:
x0 = xi = d[i++];
y0 = yi = d[i++];
ctx.moveTo(xi, yi);
break;
case CMD.L: {
x = d[i++];
y = d[i++];
const dx = mathAbs(x - xi);
const dy = mathAbs(y - yi);
// Not draw too small seg between
if (dx > ux || dy > uy) {
if (drawPart) {
const l = pathSegLen[segCount++];
if (accumLength + l > displayedLength) {
const t = (displayedLength - accumLength) / l;
ctx.lineTo(xi * (1 - t) + x * t, yi * (1 - t) + y * t);
break lo;
}
accumLength += l;
}
ctx.lineTo(x, y);
xi = x;
yi = y;
pendingPtDist = 0;
}
else {
const d2 = dx * dx + dy * dy;
// Only use the farthest pending point.
if (d2 > pendingPtDist) {
pendingPtX = x;
pendingPtY = y;
pendingPtDist = d2;
}
}
break;
}
case CMD.C: {
const x1 = d[i++];
const y1 = d[i++];
const x2 = d[i++];
const y2 = d[i++];
const x3 = d[i++];
const y3 = d[i++];
if (drawPart) {
const l = pathSegLen[segCount++];
if (accumLength + l > displayedLength) {
const t = (displayedLength - accumLength) / l;
cubicSubdivide(xi, x1, x2, x3, t, tmpOutX);
cubicSubdivide(yi, y1, y2, y3, t, tmpOutY);
ctx.bezierCurveTo(tmpOutX[1], tmpOutY[1], tmpOutX[2], tmpOutY[2], tmpOutX[3], tmpOutY[3]);
break lo;
}
accumLength += l;
}
ctx.bezierCurveTo(x1, y1, x2, y2, x3, y3);
xi = x3;
yi = y3;
break;
}
case CMD.Q: {
const x1 = d[i++];
const y1 = d[i++];
const x2 = d[i++];
const y2 = d[i++];
if (drawPart) {
const l = pathSegLen[segCount++];
if (accumLength + l > displayedLength) {
const t = (displayedLength - accumLength) / l;
quadraticSubdivide(xi, x1, x2, t, tmpOutX);
quadraticSubdivide(yi, y1, y2, t, tmpOutY);
ctx.quadraticCurveTo(tmpOutX[1], tmpOutY[1], tmpOutX[2], tmpOutY[2]);
break lo;
}
accumLength += l;
}
ctx.quadraticCurveTo(x1, y1, x2, y2);
xi = x2;
yi = y2;
break;
}
case CMD.A:
const cx = d[i++];
const cy = d[i++];
const rx = d[i++];
const ry = d[i++];
let startAngle = d[i++];
let delta = d[i++];
const psi = d[i++];
const anticlockwise = !d[i++];
const r = (rx > ry) ? rx : ry;
// const scaleX = (rx > ry) ? 1 : rx / ry;
// const scaleY = (rx > ry) ? ry / rx : 1;
const isEllipse = mathAbs(rx - ry) > 1e-3;
let endAngle = startAngle + delta;
let breakBuild = false;
if (drawPart) {
const l = pathSegLen[segCount++];
if (accumLength + l > displayedLength) {
endAngle = startAngle + delta * (displayedLength - accumLength) / l;
breakBuild = true;
}
accumLength += l;
}
if (isEllipse && ctx.ellipse) {
ctx.ellipse(cx, cy, rx, ry, psi, startAngle, endAngle, anticlockwise);
}
else {
ctx.arc(cx, cy, r, startAngle, endAngle, anticlockwise);
}
if (breakBuild) {
break lo;
}
if (isFirst) {
// 直接使用 arc 命令
// 第一个命令起点还未定义
x0 = mathCos(startAngle) * rx + cx;
y0 = mathSin(startAngle) * ry + cy;
}
xi = mathCos(endAngle) * rx + cx;
yi = mathSin(endAngle) * ry + cy;
break;
case CMD.R:
x0 = xi = d[i];
y0 = yi = d[i + 1];
x = d[i++];
y = d[i++];
const width = d[i++];
const height = d[i++];
if (drawPart) {
const l = pathSegLen[segCount++];
if (accumLength + l > displayedLength) {
let d = displayedLength - accumLength;
ctx.moveTo(x, y);
ctx.lineTo(x + mathMin(d, width), y);
d -= width;
if (d > 0) {
ctx.lineTo(x + width, y + mathMin(d, height));
}
d -= height;
if (d > 0) {
ctx.lineTo(x + mathMax(width - d, 0), y + height);
}
d -= width;
if (d > 0) {
ctx.lineTo(x, y + mathMax(height - d, 0));
}
break lo;
}
accumLength += l;
}
ctx.rect(x, y, width, height);
break;
case CMD.Z:
if (drawPart) {
const l = pathSegLen[segCount++];
if (accumLength + l > displayedLength) {
const t = (displayedLength - accumLength) / l;
ctx.lineTo(xi * (1 - t) + x0 * t, yi * (1 - t) + y0 * t);
break lo;
}
accumLength += l;
}
ctx.closePath();
xi = x0;
yi = y0;
}
}
}
clone() {
const newProxy = new PathProxy();
const data = this.data;
newProxy.data = data.slice ? data.slice()
: Array.prototype.slice.call(data);
newProxy._len = this._len;
return newProxy;
}
private static initDefaultProps = (function () {
const proto = PathProxy.prototype;
proto._saveData = true;
proto._ux = 0;
proto._uy = 0;
proto._pendingPtDist = 0;
proto._version = 0;
})()
}
export interface PathRebuilder {
moveTo(x: number, y: number): void
lineTo(x: number, y: number): void
bezierCurveTo(x: number, y: number, x2: number, y2: number, x3: number, y3: number): void
quadraticCurveTo(x: number, y: number, x2: number, y2: number): void
arc(cx: number, cy: number, r: number, startAngle: number, endAngle: number, anticlockwise: boolean): void
// eslint-disable-next-line max-len
ellipse(cx: number, cy: number, radiusX: number, radiusY: number, rotation: number, startAngle: number, endAngle: number, anticlockwise: boolean): void
rect(x: number, y: number, width: number, height: number): void
closePath(): void
}
+208
View File
@@ -0,0 +1,208 @@
import { MatrixArray } from './matrix';
export interface PointLike {
x: number
y: number
}
export default class Point {
x: number
y: number
constructor(x?: number, y?: number) {
this.x = x || 0;
this.y = y || 0;
}
/**
* Copy from another point
*/
copy(other: PointLike) {
this.x = other.x;
this.y = other.y;
return this;
}
/**
* Clone a point
*/
clone() {
return new Point(this.x, this.y);
}
/**
* Set x and y
*/
set(x: number, y: number) {
this.x = x;
this.y = y;
return this;
}
/**
* If equal to another point
*/
equal(other: PointLike) {
return other.x === this.x && other.y === this.y;
}
/**
* Add another point
*/
add(other: PointLike) {
this.x += other.x;
this.y += other.y;
return this;
}
scale(scalar: number) {
this.x *= scalar;
this.y *= scalar;
}
scaleAndAdd(other: PointLike, scalar: number) {
this.x += other.x * scalar;
this.y += other.y * scalar;
}
/**
* Sub another point
*/
sub(other: PointLike) {
this.x -= other.x;
this.y -= other.y;
return this;
}
/**
* Dot product with other point
*/
dot(other: PointLike) {
return this.x * other.x + this.y * other.y;
}
/**
* Get length of point
*/
len() {
return Math.sqrt(this.x * this.x + this.y * this.y);
}
/**
* Get squared length
*/
lenSquare() {
return this.x * this.x + this.y * this.y;
}
/**
* Normalize
*/
normalize() {
const len = this.len();
this.x /= len;
this.y /= len;
return this;
}
/**
* Distance to another point
*/
distance(other: PointLike) {
const dx = this.x - other.x;
const dy = this.y - other.y;
return Math.sqrt(dx * dx + dy * dy);
}
/**
* Square distance to another point
*/
distanceSquare(other: Point) {
const dx = this.x - other.x;
const dy = this.y - other.y;
return dx * dx + dy * dy;
}
/**
* Negate
*/
negate() {
this.x = -this.x;
this.y = -this.y;
return this;
}
/**
* Apply a transform matrix array.
*/
transform(m: MatrixArray) {
if (!m) {
return;
}
const x = this.x;
const y = this.y;
this.x = m[0] * x + m[2] * y + m[4];
this.y = m[1] * x + m[3] * y + m[5];
return this;
}
toArray(out: number[]) {
out[0] = this.x;
out[1] = this.y;
return out;
}
fromArray(input: number[]) {
this.x = input[0];
this.y = input[1];
}
static set(p: PointLike, x: number, y: number) {
p.x = x;
p.y = y;
}
static copy(p: PointLike, p2: PointLike) {
p.x = p2.x;
p.y = p2.y;
}
static len(p: PointLike) {
return Math.sqrt(p.x * p.x + p.y * p.y);
}
static lenSquare(p: PointLike) {
return p.x * p.x + p.y * p.y;
}
static dot(p0: PointLike, p1: PointLike) {
return p0.x * p1.x + p0.y * p1.y;
}
static add(out: PointLike, p0: PointLike, p1: PointLike) {
out.x = p0.x + p1.x;
out.y = p0.y + p1.y;
}
static sub(out: PointLike, p0: PointLike, p1: PointLike) {
out.x = p0.x - p1.x;
out.y = p0.y - p1.y;
}
static scale(out: PointLike, p0: PointLike, scalar: number) {
out.x = p0.x * scalar;
out.y = p0.y * scalar;
}
static scaleAndAdd(out: PointLike, p0: PointLike, p1: PointLike, scalar: number) {
out.x = p0.x + p1.x * scalar;
out.y = p0.y + p1.y * scalar;
}
static lerp(out: PointLike, p0: PointLike, p1: PointLike, t: number) {
const onet = 1 - t;
out.x = onet * p0.x + t * p1.x;
out.y = onet * p0.y + t * p1.y;
}
}
+378
View File
@@ -0,0 +1,378 @@
import * as matrix from './matrix';
import * as vector from './vector';
const mIdentity = matrix.identity;
const EPSILON = 5e-5;
function isNotAroundZero(val: number) {
return val > EPSILON || val < -EPSILON;
}
const scaleTmp: vector.VectorArray = [];
const tmpTransform: matrix.MatrixArray = [];
const originTransform = matrix.create();
const abs = Math.abs;
class Transformable {
parent: Transformable
x: number
y: number
scaleX: number
scaleY: number
skewX: number
skewY: number
rotation: number
/**
* Will translated the element to the anchor position before applying other transforms.
*/
anchorX: number
anchorY: number
/**
* Origin of scale, rotation, skew
*/
originX: number
originY: number
/**
* Scale ratio
*/
globalScaleRatio: number
transform: matrix.MatrixArray
invTransform: matrix.MatrixArray
/**
* Get computed local transform
*/
getLocalTransform(m?: matrix.MatrixArray) {
return Transformable.getLocalTransform(this, m);
}
/**
* Set position from array
*/
setPosition(arr: number[]) {
this.x = arr[0];
this.y = arr[1];
}
/**
* Set scale from array
*/
setScale(arr: number[]) {
this.scaleX = arr[0];
this.scaleY = arr[1];
}
/**
* Set skew from array
*/
setSkew(arr: number[]) {
this.skewX = arr[0];
this.skewY = arr[1];
}
/**
* Set origin from array
*/
setOrigin(arr: number[]) {
this.originX = arr[0];
this.originY = arr[1];
}
/**
* If needs to compute transform
*/
needLocalTransform(): boolean {
return isNotAroundZero(this.rotation)
|| isNotAroundZero(this.x)
|| isNotAroundZero(this.y)
|| isNotAroundZero(this.scaleX - 1)
|| isNotAroundZero(this.scaleY - 1)
|| isNotAroundZero(this.skewX)
|| isNotAroundZero(this.skewY);
}
/**
* Update global transform
*/
updateTransform() {
const parentTransform = this.parent && this.parent.transform;
const needLocalTransform = this.needLocalTransform();
let m = this.transform;
if (!(needLocalTransform || parentTransform)) {
if (m) {
mIdentity(m);
// reset invTransform
this.invTransform = null;
}
return;
}
m = m || matrix.create();
if (needLocalTransform) {
this.getLocalTransform(m);
}
else {
mIdentity(m);
}
// 应用父节点变换
if (parentTransform) {
if (needLocalTransform) {
matrix.mul(m, parentTransform, m);
}
else {
matrix.copy(m, parentTransform);
}
}
// 保存这个变换矩阵
this.transform = m;
this._resolveGlobalScaleRatio(m);
}
private _resolveGlobalScaleRatio(m: matrix.MatrixArray) {
const globalScaleRatio = this.globalScaleRatio;
if (globalScaleRatio != null && globalScaleRatio !== 1) {
this.getGlobalScale(scaleTmp);
const relX = scaleTmp[0] < 0 ? -1 : 1;
const relY = scaleTmp[1] < 0 ? -1 : 1;
const sx = ((scaleTmp[0] - relX) * globalScaleRatio + relX) / scaleTmp[0] || 0;
const sy = ((scaleTmp[1] - relY) * globalScaleRatio + relY) / scaleTmp[1] || 0;
m[0] *= sx;
m[1] *= sx;
m[2] *= sy;
m[3] *= sy;
}
this.invTransform = this.invTransform || matrix.create();
matrix.invert(this.invTransform, m);
}
/**
* Get computed global transform
* NOTE: this method will force update transform on all ancestors.
* Please be aware of the potential performance cost.
*/
getComputedTransform() {
let transformNode: Transformable = this;
const ancestors: Transformable[] = [];
while (transformNode) {
ancestors.push(transformNode);
transformNode = transformNode.parent;
}
// Update from topdown.
while (transformNode = ancestors.pop()) {
transformNode.updateTransform();
}
return this.transform;
}
setLocalTransform(m: vector.VectorArray) {
if (!m) {
// TODO return or set identity?
return;
}
let sx = m[0] * m[0] + m[1] * m[1];
let sy = m[2] * m[2] + m[3] * m[3];
const rotation = Math.atan2(m[1], m[0]);
const shearX = Math.PI / 2 + rotation - Math.atan2(m[3], m[2]);
sy = Math.sqrt(sy) * Math.cos(shearX);
sx = Math.sqrt(sx);
this.skewX = shearX;
this.skewY = 0;
this.rotation = -rotation;
this.x = +m[4];
this.y = +m[5];
this.scaleX = sx;
this.scaleY = sy;
this.originX = 0;
this.originY = 0;
}
/**
* 分解`transform`矩阵到`position`, `rotation`, `scale`
*/
decomposeTransform() {
if (!this.transform) {
return;
}
const parent = this.parent;
let m = this.transform;
if (parent && parent.transform) {
// Get local transform and decompose them to position, scale, rotation
parent.invTransform = parent.invTransform || matrix.create();
matrix.mul(tmpTransform, parent.invTransform, m);
m = tmpTransform;
}
const ox = this.originX;
const oy = this.originY;
if (ox || oy) {
originTransform[4] = ox;
originTransform[5] = oy;
matrix.mul(tmpTransform, m, originTransform);
tmpTransform[4] -= ox;
tmpTransform[5] -= oy;
m = tmpTransform;
}
this.setLocalTransform(m);
}
/**
* Get global scale
*/
getGlobalScale(out?: vector.VectorArray): vector.VectorArray {
const m = this.transform;
out = out || [];
if (!m) {
out[0] = 1;
out[1] = 1;
return out;
}
out[0] = Math.sqrt(m[0] * m[0] + m[1] * m[1]);
out[1] = Math.sqrt(m[2] * m[2] + m[3] * m[3]);
if (m[0] < 0) {
out[0] = -out[0];
}
if (m[3] < 0) {
out[1] = -out[1];
}
return out;
}
/**
* 变换坐标位置到 shape 的局部坐标空间
*/
transformCoordToLocal(x: number, y: number): number[] {
const v2 = [x, y];
const invTransform = this.invTransform;
if (invTransform) {
vector.applyTransform(v2, v2, invTransform);
}
return v2;
}
/**
* 变换局部坐标位置到全局坐标空间
*/
transformCoordToGlobal(x: number, y: number): number[] {
const v2 = [x, y];
const transform = this.transform;
if (transform) {
vector.applyTransform(v2, v2, transform);
}
return v2;
}
getLineScale() {
const 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;
}
copyTransform(source: Transformable) {
copyTransform(this, source);
}
static getLocalTransform(target: Transformable, m?: matrix.MatrixArray): matrix.MatrixArray {
m = m || [];
const ox = target.originX || 0;
const oy = target.originY || 0;
const sx = target.scaleX;
const sy = target.scaleY;
const ax = target.anchorX;
const ay = target.anchorY;
const rotation = target.rotation || 0;
const x = target.x;
const y = target.y;
const skewX = target.skewX ? Math.tan(target.skewX) : 0;
// TODO: zrender use different hand in coordinate system and y axis is inversed.
const skewY = target.skewY ? Math.tan(-target.skewY) : 0;
// The order of transform (-anchor * -origin * scale * skew * rotate * origin * translate).
// We merge (-origin * scale * skew) into one. Also did identity in these operations.
// origin
if (ox || oy || ax || ay) {
const dx = ox + ax;
const dy = oy + ay;
m[4] = -dx * sx - skewX * dy * sy;
m[5] = -dy * sy - skewY * dx * sx;
}
else {
m[4] = m[5] = 0;
}
// scale
m[0] = sx;
m[3] = sy;
// skew
m[1] = skewY * sx;
m[2] = skewX * sy;
// Apply rotation
rotation && matrix.rotate(m, m, rotation);
// Translate back from origin and apply translation
m[4] += ox + x;
m[5] += oy + y;
return m;
}
private static initDefaultProps = (function () {
const proto = Transformable.prototype;
proto.scaleX =
proto.scaleY =
proto.globalScaleRatio = 1;
proto.x =
proto.y =
proto.originX =
proto.originY =
proto.skewX =
proto.skewY =
proto.rotation =
proto.anchorX =
proto.anchorY = 0;
})()
};
export const TRANSFORMABLE_PROPS = [
'x', 'y', 'originX', 'originY', 'anchorX', 'anchorY', 'rotation', 'scaleX', 'scaleY', 'skewX', 'skewY'
] as const;
export type TransformProp = (typeof TRANSFORMABLE_PROPS)[number]
export function copyTransform(
target: Partial<Pick<Transformable, TransformProp>>,
source: Pick<Transformable, TransformProp>
) {
for (let i = 0; i < TRANSFORMABLE_PROPS.length; i++) {
const propName = TRANSFORMABLE_PROPS[i];
target[propName] = source[propName];
}
}
export default Transformable;
+50
View File
@@ -0,0 +1,50 @@
let wmUniqueIndex = Math.round(Math.random() * 9);
const supportDefineProperty = typeof Object.defineProperty === 'function';
export default class WeakMap<K extends object, V> {
protected _id: string;
constructor() {
this._id = '__ec_inner_' + wmUniqueIndex++;
}
get(key: K): V {
return (this._guard(key) as any)[this._id];
}
set(key: K, value: V): WeakMap<K, V> {
const target = this._guard(key) as any;
if (supportDefineProperty) {
Object.defineProperty(target, this._id, {
value: value,
enumerable: false,
configurable: true
});
}
else {
target[this._id] = value;
}
return this;
}
delete(key: K): boolean {
if (this.has(key)) {
delete (this._guard(key) as any)[this._id];
return true;
}
return false;
}
has(key: K): boolean {
return !!(this._guard(key) as any)[this._id];
}
protected _guard(key: K): K {
if (key !== Object(key)) {
throw TypeError('Value of WeakMap is not a non-null object.');
}
return key;
}
}
+196
View File
@@ -0,0 +1,196 @@
// Myers' Diff Algorithm
// Modified from https://github.com/kpdecker/jsdiff/blob/master/src/diff/base.js
type EqualFunc<T> = (a: T, b: T) => boolean;
type DiffComponent = {
count: number
added: boolean
removed: boolean,
indices: number[]
}
type DiffPath = {
components: DiffComponent[],
newPos: number
}
// Using O(ND) algorithm
// TODO: Optimize when diff is large.
function diff<T>(oldArr: T[], newArr: T[], equals: EqualFunc<T>): DiffComponent[] {
if (!equals) {
equals = function (a, b) {
return a === b;
};
}
oldArr = oldArr.slice();
newArr = newArr.slice();
// Allow subclasses to massage the input prior to running
var newLen = newArr.length;
var oldLen = oldArr.length;
var editLength = 1;
var maxEditLength = newLen + oldLen;
var bestPath: DiffPath[] = [{ newPos: -1, components: [] }];
// Seed editLength = 0, i.e. the content starts with the same values
var oldPos = extractCommon<T>(bestPath[0], newArr, oldArr, 0, equals);
if (!oldLen // All new created
|| !newLen // Clear
|| (bestPath[0].newPos + 1 >= newLen && oldPos + 1 >= oldLen)) {
var indices = [];
var allCleared = !newLen && oldLen > 0;
var allCreated = !oldLen && newLen > 0;
for (let i = 0; i < (allCleared ? oldArr : newArr).length; i++) {
indices.push(i);
}
// Identity per the equality and tokenizer
return [{
indices: indices,
count: indices.length,
added: allCreated,
removed: allCleared
}];
}
// Main worker method. checks all permutations of a given edit length for acceptance.
function execEditLength() {
for (let diagonalPath = -1 * editLength; diagonalPath <= editLength; diagonalPath += 2) {
var basePath;
var addPath = bestPath[diagonalPath - 1];
var removePath = bestPath[diagonalPath + 1];
var oldPos = (removePath ? removePath.newPos : 0) - diagonalPath;
if (addPath) {
// No one else is going to attempt to use this value, clear it
bestPath[diagonalPath - 1] = undefined;
}
var canAdd = addPath && addPath.newPos + 1 < newLen;
var canRemove = removePath && 0 <= oldPos && oldPos < oldLen;
if (!canAdd && !canRemove) {
// If this path is a terminal then prune
bestPath[diagonalPath] = undefined;
continue;
}
// Select the diagonal that we want to branch from. We select the prior
// path whose position in the new string is the farthest from the origin
// and does not pass the bounds of the diff graph
if (!canAdd || (canRemove && addPath.newPos < removePath.newPos)) {
basePath = clonePath(removePath);
pushComponent(basePath.components, false, true);
}
else {
basePath = addPath; // No need to clone, we've pulled it from the list
basePath.newPos++;
pushComponent(basePath.components, true, false);
}
oldPos = extractCommon<T>(basePath, newArr, oldArr, diagonalPath, equals);
// If we have hit the end of both strings, then we are done
if (basePath.newPos + 1 >= newLen && oldPos + 1 >= oldLen) {
return buildValues(basePath.components);
}
else {
// Otherwise track this path as a potential candidate and continue.
bestPath[diagonalPath] = basePath;
}
}
editLength++;
}
while (editLength <= maxEditLength) {
var ret = execEditLength();
if (ret) {
return ret;
}
}
}
function extractCommon<T>(basePath: DiffPath, newArr: T[], oldArr: T[], diagonalPath: number, equals: EqualFunc<T>) {
var newLen = newArr.length;
var oldLen = oldArr.length;
var newPos = basePath.newPos;
var oldPos = newPos - diagonalPath;
var commonCount = 0;
while (newPos + 1 < newLen && oldPos + 1 < oldLen && equals(newArr[newPos + 1], oldArr[oldPos + 1])) {
newPos++;
oldPos++;
commonCount++;
}
if (commonCount) {
basePath.components.push({
count: commonCount,
added: false,
removed: false,
indices: []
});
}
basePath.newPos = newPos;
return oldPos;
}
function pushComponent(components: DiffComponent[], added: boolean, removed: boolean) {
var last = components[components.length - 1];
if (last && last.added === added && last.removed === removed) {
// We need to clone here as the component clone operation is just
// as shallow array clone
components[components.length - 1] = {
count: last.count + 1,
added,
removed,
indices: []
};
}
else {
components.push({
count: 1,
added,
removed,
indices: []
});
}
}
function buildValues(components: DiffComponent[]) {
var componentPos = 0;
var componentLen = components.length;
var newPos = 0;
var oldPos = 0;
for (; componentPos < componentLen; componentPos++) {
var component = components[componentPos];
if (!component.removed) {
var indices = [];
for (let i = newPos; i < newPos + component.count; i++) {
indices.push(i);
}
component.indices = indices;
newPos += component.count;
// Common case
if (!component.added) {
oldPos += component.count;
}
}
else {
for (let i = oldPos; i < oldPos + component.count; i++) {
component.indices.push(i);
}
oldPos += component.count;
}
}
return components;
}
function clonePath(path: DiffPath) {
return { newPos: path.newPos, components: path.components.slice(0) };
}
export default function arrayDiff<T>(oldArr: T[], newArr: T[], equal?: EqualFunc<T>): DiffComponent[] {
return diff(oldArr, newArr, equal);
}
+187
View File
@@ -0,0 +1,187 @@
/**
* @author Yi Shen(https://github.com/pissang)
*/
import * as vec2 from './vector';
import * as curve from './curve';
const mathMin = Math.min;
const mathMax = Math.max;
const mathSin = Math.sin;
const mathCos = Math.cos;
const PI2 = Math.PI * 2;
const start = vec2.create();
const end = vec2.create();
const extremity = vec2.create();
/**
* 从顶点数组中计算出最小包围盒,写入`min`和`max`中
*/
export function fromPoints(points: ArrayLike<number>[], min: vec2.VectorArray, max: vec2.VectorArray) {
if (points.length === 0) {
return;
}
let p = points[0];
let left = p[0];
let right = p[0];
let top = p[1];
let bottom = p[1];
for (let i = 1; i < points.length; i++) {
p = points[i];
left = mathMin(left, p[0]);
right = mathMax(right, p[0]);
top = mathMin(top, p[1]);
bottom = mathMax(bottom, p[1]);
}
min[0] = left;
min[1] = top;
max[0] = right;
max[1] = bottom;
}
export function fromLine(
x0: number, y0: number, x1: number, y1: number,
min: vec2.VectorArray, max: vec2.VectorArray
) {
min[0] = mathMin(x0, x1);
min[1] = mathMin(y0, y1);
max[0] = mathMax(x0, x1);
max[1] = mathMax(y0, y1);
}
const xDim: number[] = [];
const yDim: number[] = [];
/**
* 从三阶贝塞尔曲线(p0, p1, p2, p3)中计算出最小包围盒,写入`min`和`max`中
*/
export function fromCubic(
x0: number, y0: number, x1: number, y1: number, x2: number, y2: number, x3: number, y3: number,
min: vec2.VectorArray, max: vec2.VectorArray
) {
const cubicExtrema = curve.cubicExtrema;
const cubicAt = curve.cubicAt;
let n = cubicExtrema(x0, x1, x2, x3, xDim);
min[0] = Infinity;
min[1] = Infinity;
max[0] = -Infinity;
max[1] = -Infinity;
for (let i = 0; i < n; i++) {
const x = cubicAt(x0, x1, x2, x3, xDim[i]);
min[0] = mathMin(x, min[0]);
max[0] = mathMax(x, max[0]);
}
n = cubicExtrema(y0, y1, y2, y3, yDim);
for (let i = 0; i < n; i++) {
const y = cubicAt(y0, y1, y2, y3, yDim[i]);
min[1] = mathMin(y, min[1]);
max[1] = mathMax(y, max[1]);
}
min[0] = mathMin(x0, min[0]);
max[0] = mathMax(x0, max[0]);
min[0] = mathMin(x3, min[0]);
max[0] = mathMax(x3, max[0]);
min[1] = mathMin(y0, min[1]);
max[1] = mathMax(y0, max[1]);
min[1] = mathMin(y3, min[1]);
max[1] = mathMax(y3, max[1]);
}
/**
* 从二阶贝塞尔曲线(p0, p1, p2)中计算出最小包围盒,写入`min`和`max`中
*/
export function fromQuadratic(
x0: number, y0: number, x1: number, y1: number, x2: number, y2: number,
min: vec2.VectorArray, max: vec2.VectorArray
) {
const quadraticExtremum = curve.quadraticExtremum;
const quadraticAt = curve.quadraticAt;
// Find extremities, where derivative in x dim or y dim is zero
const tx =
mathMax(
mathMin(quadraticExtremum(x0, x1, x2), 1), 0
);
const ty =
mathMax(
mathMin(quadraticExtremum(y0, y1, y2), 1), 0
);
const x = quadraticAt(x0, x1, x2, tx);
const y = quadraticAt(y0, y1, y2, ty);
min[0] = mathMin(x0, x2, x);
min[1] = mathMin(y0, y2, y);
max[0] = mathMax(x0, x2, x);
max[1] = mathMax(y0, y2, y);
}
/**
* 从圆弧中计算出最小包围盒,写入`min`和`max`中
*/
export function fromArc(
x: number, y: number, rx: number, ry: number, startAngle: number, endAngle: number, anticlockwise: boolean,
min: vec2.VectorArray, max: vec2.VectorArray
) {
const vec2Min = vec2.min;
const vec2Max = vec2.max;
const diff = Math.abs(startAngle - endAngle);
if (diff % PI2 < 1e-4 && diff > 1e-4) {
// Is a circle
min[0] = x - rx;
min[1] = y - ry;
max[0] = x + rx;
max[1] = y + ry;
return;
}
start[0] = mathCos(startAngle) * rx + x;
start[1] = mathSin(startAngle) * ry + y;
end[0] = mathCos(endAngle) * rx + x;
end[1] = mathSin(endAngle) * ry + y;
vec2Min(min, start, end);
vec2Max(max, start, end);
// Thresh to [0, Math.PI * 2]
startAngle = startAngle % (PI2);
if (startAngle < 0) {
startAngle = startAngle + PI2;
}
endAngle = endAngle % (PI2);
if (endAngle < 0) {
endAngle = endAngle + PI2;
}
if (startAngle > endAngle && !anticlockwise) {
endAngle += PI2;
}
else if (startAngle < endAngle && anticlockwise) {
startAngle += PI2;
}
if (anticlockwise) {
const tmp = endAngle;
endAngle = startAngle;
startAngle = tmp;
}
// const number = 0;
// const step = (anticlockwise ? -Math.PI : Math.PI) / 2;
for (let angle = 0; angle < endAngle; angle += Math.PI / 2) {
if (angle > startAngle) {
extremity[0] = mathCos(angle) * rx + x;
extremity[1] = mathSin(angle) * ry + y;
vec2Min(min, extremity, min);
vec2Max(max, extremity, max);
}
}
}
+500
View File
@@ -0,0 +1,500 @@
/**
* 曲线辅助模块
*/
import {
create as v2Create,
distSquare as v2DistSquare,
VectorArray
} from './vector';
const mathPow = Math.pow;
const mathSqrt = Math.sqrt;
const EPSILON = 1e-8;
const EPSILON_NUMERIC = 1e-4;
const THREE_SQRT = mathSqrt(3);
const ONE_THIRD = 1 / 3;
// 临时变量
const _v0 = v2Create();
const _v1 = v2Create();
const _v2 = v2Create();
function isAroundZero(val: number) {
return val > -EPSILON && val < EPSILON;
}
function isNotAroundZero(val: number) {
return val > EPSILON || val < -EPSILON;
}
/**
* 计算三次贝塞尔值
*/
export function cubicAt(p0: number, p1: number, p2: number, p3: number, t: number): number {
const onet = 1 - t;
return onet * onet * (onet * p0 + 3 * t * p1)
+ t * t * (t * p3 + 3 * onet * p2);
}
/**
* 计算三次贝塞尔导数值
*/
export function cubicDerivativeAt(p0: number, p1: number, p2: number, p3: number, t: number): number {
const onet = 1 - t;
return 3 * (
((p1 - p0) * onet + 2 * (p2 - p1) * t) * onet
+ (p3 - p2) * t * t
);
}
/**
* 计算三次贝塞尔方程根,使用盛金公式
*/
export function cubicRootAt(p0: number, p1: number, p2: number, p3: number, val: number, roots: number[]): number {
// Evaluate roots of cubic functions
const a = p3 + 3 * (p1 - p2) - p0;
const b = 3 * (p2 - p1 * 2 + p0);
const c = 3 * (p1 - p0);
const d = p0 - val;
const A = b * b - 3 * a * c;
const B = b * c - 9 * a * d;
const C = c * c - 3 * b * d;
let n = 0;
if (isAroundZero(A) && isAroundZero(B)) {
if (isAroundZero(b)) {
roots[0] = 0;
}
else {
const t1 = -c / b; //t1, t2, t3, b is not zero
if (t1 >= 0 && t1 <= 1) {
roots[n++] = t1;
}
}
}
else {
const disc = B * B - 4 * A * C;
if (isAroundZero(disc)) {
const K = B / A;
const t1 = -b / a + K; // t1, a is not zero
const t2 = -K / 2; // t2, t3
if (t1 >= 0 && t1 <= 1) {
roots[n++] = t1;
}
if (t2 >= 0 && t2 <= 1) {
roots[n++] = t2;
}
}
else if (disc > 0) {
const discSqrt = mathSqrt(disc);
let Y1 = A * b + 1.5 * a * (-B + discSqrt);
let Y2 = A * b + 1.5 * a * (-B - discSqrt);
if (Y1 < 0) {
Y1 = -mathPow(-Y1, ONE_THIRD);
}
else {
Y1 = mathPow(Y1, ONE_THIRD);
}
if (Y2 < 0) {
Y2 = -mathPow(-Y2, ONE_THIRD);
}
else {
Y2 = mathPow(Y2, ONE_THIRD);
}
const t1 = (-b - (Y1 + Y2)) / (3 * a);
if (t1 >= 0 && t1 <= 1) {
roots[n++] = t1;
}
}
else {
const T = (2 * A * b - 3 * a * B) / (2 * mathSqrt(A * A * A));
const theta = Math.acos(T) / 3;
const ASqrt = mathSqrt(A);
const tmp = Math.cos(theta);
const t1 = (-b - 2 * ASqrt * tmp) / (3 * a);
const t2 = (-b + ASqrt * (tmp + THREE_SQRT * Math.sin(theta))) / (3 * a);
const t3 = (-b + ASqrt * (tmp - THREE_SQRT * Math.sin(theta))) / (3 * a);
if (t1 >= 0 && t1 <= 1) {
roots[n++] = t1;
}
if (t2 >= 0 && t2 <= 1) {
roots[n++] = t2;
}
if (t3 >= 0 && t3 <= 1) {
roots[n++] = t3;
}
}
}
return n;
}
/**
* 计算三次贝塞尔方程极限值的位置
* @return 有效数目
*/
export function cubicExtrema(p0: number, p1: number, p2: number, p3: number, extrema: number[]): number {
const b = 6 * p2 - 12 * p1 + 6 * p0;
const a = 9 * p1 + 3 * p3 - 3 * p0 - 9 * p2;
const c = 3 * p1 - 3 * p0;
let n = 0;
if (isAroundZero(a)) {
if (isNotAroundZero(b)) {
const t1 = -c / b;
if (t1 >= 0 && t1 <= 1) {
extrema[n++] = t1;
}
}
}
else {
const disc = b * b - 4 * a * c;
if (isAroundZero(disc)) {
extrema[0] = -b / (2 * a);
}
else if (disc > 0) {
const discSqrt = mathSqrt(disc);
const t1 = (-b + discSqrt) / (2 * a);
const t2 = (-b - discSqrt) / (2 * a);
if (t1 >= 0 && t1 <= 1) {
extrema[n++] = t1;
}
if (t2 >= 0 && t2 <= 1) {
extrema[n++] = t2;
}
}
}
return n;
}
/**
* 细分三次贝塞尔曲线
*/
export function cubicSubdivide(p0: number, p1: number, p2: number, p3: number, t: number, out: number[]) {
const p01 = (p1 - p0) * t + p0;
const p12 = (p2 - p1) * t + p1;
const p23 = (p3 - p2) * t + p2;
const p012 = (p12 - p01) * t + p01;
const p123 = (p23 - p12) * t + p12;
const p0123 = (p123 - p012) * t + p012;
// Seg0
out[0] = p0;
out[1] = p01;
out[2] = p012;
out[3] = p0123;
// Seg1
out[4] = p0123;
out[5] = p123;
out[6] = p23;
out[7] = p3;
}
/**
* 投射点到三次贝塞尔曲线上,返回投射距离。
* 投射点有可能会有一个或者多个,这里只返回其中距离最短的一个。
*/
export function cubicProjectPoint(
x0: number, y0: number, x1: number, y1: number, x2: number, y2: number, x3: number, y3: number,
x: number, y: number, out: VectorArray
): number {
// http://pomax.github.io/bezierinfo/#projections
let t;
let interval = 0.005;
let d = Infinity;
let prev;
let next;
let d1;
let d2;
_v0[0] = x;
_v0[1] = y;
// 先粗略估计一下可能的最小距离的 t 值
// PENDING
for (let _t = 0; _t < 1; _t += 0.05) {
_v1[0] = cubicAt(x0, x1, x2, x3, _t);
_v1[1] = cubicAt(y0, y1, y2, y3, _t);
d1 = v2DistSquare(_v0, _v1);
if (d1 < d) {
t = _t;
d = d1;
}
}
d = Infinity;
// At most 32 iteration
for (let i = 0; i < 32; i++) {
if (interval < EPSILON_NUMERIC) {
break;
}
prev = t - interval;
next = t + interval;
// t - interval
_v1[0] = cubicAt(x0, x1, x2, x3, prev);
_v1[1] = cubicAt(y0, y1, y2, y3, prev);
d1 = v2DistSquare(_v1, _v0);
if (prev >= 0 && d1 < d) {
t = prev;
d = d1;
}
else {
// t + interval
_v2[0] = cubicAt(x0, x1, x2, x3, next);
_v2[1] = cubicAt(y0, y1, y2, y3, next);
d2 = v2DistSquare(_v2, _v0);
if (next <= 1 && d2 < d) {
t = next;
d = d2;
}
else {
interval *= 0.5;
}
}
}
// t
if (out) {
out[0] = cubicAt(x0, x1, x2, x3, t);
out[1] = cubicAt(y0, y1, y2, y3, t);
}
// console.log(interval, i);
return mathSqrt(d);
}
/**
* 计算三次贝塞尔曲线长度
*/
export function cubicLength(
x0: number, y0: number, x1: number, y1: number, x2: number, y2: number, x3: number, y3: number,
iteration: number
) {
let px = x0;
let py = y0;
let d = 0;
const step = 1 / iteration;
for (let i = 1; i <= iteration; i++) {
let t = i * step;
const x = cubicAt(x0, x1, x2, x3, t);
const y = cubicAt(y0, y1, y2, y3, t);
const dx = x - px;
const dy = y - py;
d += Math.sqrt(dx * dx + dy * dy);
px = x;
py = y;
}
return d;
}
/**
* 计算二次方贝塞尔值
*/
export function quadraticAt(p0: number, p1: number, p2: number, t: number): number {
const onet = 1 - t;
return onet * (onet * p0 + 2 * t * p1) + t * t * p2;
}
/**
* 计算二次方贝塞尔导数值
*/
export function quadraticDerivativeAt(p0: number, p1: number, p2: number, t: number): number {
return 2 * ((1 - t) * (p1 - p0) + t * (p2 - p1));
}
/**
* 计算二次方贝塞尔方程根
* @return 有效根数目
*/
export function quadraticRootAt(p0: number, p1: number, p2: number, val: number, roots: number[]): number {
const a = p0 - 2 * p1 + p2;
const b = 2 * (p1 - p0);
const c = p0 - val;
let n = 0;
if (isAroundZero(a)) {
if (isNotAroundZero(b)) {
const t1 = -c / b;
if (t1 >= 0 && t1 <= 1) {
roots[n++] = t1;
}
}
}
else {
const disc = b * b - 4 * a * c;
if (isAroundZero(disc)) {
const t1 = -b / (2 * a);
if (t1 >= 0 && t1 <= 1) {
roots[n++] = t1;
}
}
else if (disc > 0) {
const discSqrt = mathSqrt(disc);
const t1 = (-b + discSqrt) / (2 * a);
const t2 = (-b - discSqrt) / (2 * a);
if (t1 >= 0 && t1 <= 1) {
roots[n++] = t1;
}
if (t2 >= 0 && t2 <= 1) {
roots[n++] = t2;
}
}
}
return n;
}
/**
* 计算二次贝塞尔方程极限值
*/
export function quadraticExtremum(p0: number, p1: number, p2: number): number {
const divider = p0 + p2 - 2 * p1;
if (divider === 0) {
// p1 is center of p0 and p2
return 0.5;
}
else {
return (p0 - p1) / divider;
}
}
/**
* 细分二次贝塞尔曲线
*/
export function quadraticSubdivide(p0: number, p1: number, p2: number, t: number, out: number[]) {
const p01 = (p1 - p0) * t + p0;
const p12 = (p2 - p1) * t + p1;
const p012 = (p12 - p01) * t + p01;
// Seg0
out[0] = p0;
out[1] = p01;
out[2] = p012;
// Seg1
out[3] = p012;
out[4] = p12;
out[5] = p2;
}
/**
* 投射点到二次贝塞尔曲线上,返回投射距离。
* 投射点有可能会有一个或者多个,这里只返回其中距离最短的一个。
* @param {number} x0
* @param {number} y0
* @param {number} x1
* @param {number} y1
* @param {number} x2
* @param {number} y2
* @param {number} x
* @param {number} y
* @param {Array.<number>} out 投射点
* @return {number}
*/
export function quadraticProjectPoint(
x0: number, y0: number, x1: number, y1: number, x2: number, y2: number,
x: number, y: number, out: VectorArray
): number {
// http://pomax.github.io/bezierinfo/#projections
let t: number;
let interval = 0.005;
let d = Infinity;
_v0[0] = x;
_v0[1] = y;
// 先粗略估计一下可能的最小距离的 t 值
// PENDING
for (let _t = 0; _t < 1; _t += 0.05) {
_v1[0] = quadraticAt(x0, x1, x2, _t);
_v1[1] = quadraticAt(y0, y1, y2, _t);
const d1 = v2DistSquare(_v0, _v1);
if (d1 < d) {
t = _t;
d = d1;
}
}
d = Infinity;
// At most 32 iteration
for (let i = 0; i < 32; i++) {
if (interval < EPSILON_NUMERIC) {
break;
}
const prev = t - interval;
const next = t + interval;
// t - interval
_v1[0] = quadraticAt(x0, x1, x2, prev);
_v1[1] = quadraticAt(y0, y1, y2, prev);
const d1 = v2DistSquare(_v1, _v0);
if (prev >= 0 && d1 < d) {
t = prev;
d = d1;
}
else {
// t + interval
_v2[0] = quadraticAt(x0, x1, x2, next);
_v2[1] = quadraticAt(y0, y1, y2, next);
const d2 = v2DistSquare(_v2, _v0);
if (next <= 1 && d2 < d) {
t = next;
d = d2;
}
else {
interval *= 0.5;
}
}
}
// t
if (out) {
out[0] = quadraticAt(x0, x1, x2, t);
out[1] = quadraticAt(y0, y1, y2, t);
}
// console.log(interval, i);
return mathSqrt(d);
}
/**
* 计算二次贝塞尔曲线长度
*/
export function quadraticLength(
x0: number, y0: number, x1: number, y1: number, x2: number, y2: number,
iteration: number
) {
let px = x0;
let py = y0;
let d = 0;
const step = 1 / iteration;
for (let i = 1; i <= iteration; i++) {
let t = i * step;
const x = quadraticAt(x0, x1, x2, t);
const y = quadraticAt(y0, y1, y2, t);
const dx = x - px;
const dy = y - py;
d += Math.sqrt(dx * dx + dy * dy);
px = x;
py = y;
}
return d;
}
+187
View File
@@ -0,0 +1,187 @@
import env from './env';
import {buildTransformer} from './fourPointsTransform';
import {Dictionary} from './types';
const EVENT_SAVED_PROP = '___zrEVENTSAVED';
const _calcOut: number[] = [];
type SavedInfo = {
markers?: HTMLDivElement[]
trans?: ReturnType<typeof buildTransformer>
invTrans?: ReturnType<typeof buildTransformer>
srcCoords?: number[]
}
/**
* Transform "local coord" from `elFrom` to `elTarget`.
* "local coord": the coord based on the input `el`. The origin point is at
* the position of "left: 0; top: 0;" in the `el`.
*
* Support when CSS transform is used.
*
* Having the `out` (that is, `[outX, outY]`), we can create an DOM element
* and set the CSS style as "left: outX; top: outY;" and append it to `elTarge`
* to locate the element.
*
* For example, this code below positions a child of `document.body` on the event
* point, no matter whether `body` has `margin`/`paddin`/`transfrom`/... :
* ```js
* transformLocalCoord(out, container, document.body, event.offsetX, event.offsetY);
* if (!eqNaN(out[0])) {
* // Then locate the tip element on the event point.
* var tipEl = document.createElement('div');
* tipEl.style.cssText = 'position: absolute; left:' + out[0] + ';top:' + out[1] + ';';
* document.body.appendChild(tipEl);
* }
* ```
*
* Notice: In some env this method is not supported. If called, `out` will be `[NaN, NaN]`.
*
* @param {Array.<number>} out [inX: number, inY: number] The output..
* If can not transform, `out` will not be modified but return `false`.
* @param {HTMLElement} elFrom The `[inX, inY]` is based on elFrom.
* @param {HTMLElement} elTarget The `out` is based on elTarget.
* @param {number} inX
* @param {number} inY
* @return {boolean} Whether transform successfully.
*/
export function transformLocalCoord(
out: number[],
elFrom: HTMLElement,
elTarget: HTMLElement,
inX: number,
inY: number
) {
return transformCoordWithViewport(_calcOut, elFrom, inX, inY, true)
&& transformCoordWithViewport(out, elTarget, _calcOut[0], _calcOut[1]);
}
/**
* Transform between a "viewport coord" and a "local coord".
* "viewport coord": the coord based on the left-top corner of the viewport
* of the browser.
* "local coord": the coord based on the input `el`. The origin point is at
* the position of "left: 0; top: 0;" in the `el`.
*
* Support the case when CSS transform is used on el.
*
* @param out [inX: number, inY: number] The output. If `inverse: false`,
* it represents "local coord", otherwise "vireport coord".
* If can not transform, `out` will not be modified but return `false`.
* @param el The "local coord" is based on the `el`, see comment above.
* @param inX If `inverse: false`,
* it represents "vireport coord", otherwise "local coord".
* @param inY If `inverse: false`,
* it represents "vireport coord", otherwise "local coord".
* @param inverse
* `true`: from "viewport coord" to "local coord".
* `false`: from "local coord" to "viewport coord".
* @return {boolean} Whether transform successfully.
*/
export function transformCoordWithViewport(
out: number[],
el: HTMLElement,
inX: number,
inY: number,
inverse?: boolean
) {
if (el.getBoundingClientRect && env.domSupported && !isCanvasEl(el)) {
const saved = (el as any)[EVENT_SAVED_PROP] || ((el as any)[EVENT_SAVED_PROP] = {});
const markers = prepareCoordMarkers(el, saved);
const transformer = preparePointerTransformer(markers, saved, inverse);
if (transformer) {
transformer(out, inX, inY);
return true;
}
}
return false;
}
function prepareCoordMarkers(el: HTMLElement, saved: SavedInfo) {
let markers = saved.markers;
if (markers) {
return markers;
}
markers = saved.markers = [];
const propLR = ['left', 'right'];
const propTB = ['top', 'bottom'];
for (let i = 0; i < 4; i++) {
const marker = document.createElement('div');
const stl = marker.style;
const idxLR = i % 2;
const idxTB = (i >> 1) % 2;
stl.cssText = [
'position: absolute',
'visibility: hidden',
'padding: 0',
'margin: 0',
'border-width: 0',
'user-select: none',
'width:0',
'height:0',
// 'width: 5px',
// 'height: 5px',
propLR[idxLR] + ':0',
propTB[idxTB] + ':0',
propLR[1 - idxLR] + ':auto',
propTB[1 - idxTB] + ':auto',
''
].join('!important;');
el.appendChild(marker);
markers.push(marker);
}
return markers;
}
function preparePointerTransformer(markers: HTMLDivElement[], saved: SavedInfo, inverse?: boolean) {
const transformerName: 'invTrans' | 'trans' = inverse ? 'invTrans' : 'trans';
const transformer = saved[transformerName];
const oldSrcCoords = saved.srcCoords;
const srcCoords = [];
const destCoords = [];
let oldCoordTheSame = true;
for (let i = 0; i < 4; i++) {
const rect = markers[i].getBoundingClientRect();
const ii = 2 * i;
const x = rect.left;
const y = rect.top;
srcCoords.push(x, y);
oldCoordTheSame = oldCoordTheSame && oldSrcCoords && x === oldSrcCoords[ii] && y === oldSrcCoords[ii + 1];
destCoords.push(markers[i].offsetLeft, markers[i].offsetTop);
}
// Cache to avoid time consuming of `buildTransformer`.
return (oldCoordTheSame && transformer)
? transformer
: (
saved.srcCoords = srcCoords,
saved[transformerName] = inverse
? buildTransformer(destCoords, srcCoords)
: buildTransformer(srcCoords, destCoords)
);
}
export function isCanvasEl(el: HTMLElement): el is HTMLCanvasElement {
return el.nodeName.toUpperCase() === 'CANVAS';
}
const replaceReg = /([&<>"'])/g;
const replaceMap: Dictionary<string> = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
'\'': '&#39;'
};
export function encodeHTML(source: string): string {
return source == null
? ''
: (source + '').replace(replaceReg, function (str, c) {
return replaceMap[c];
});
}
+115
View File
@@ -0,0 +1,115 @@
declare const wx: {
getSystemInfoSync: Function
};
class Browser {
firefox = false
ie = false
edge = false
newEdge = false
weChat = false
version: string | number
}
class Env {
browser = new Browser()
node = false
wxa = false
worker = false
svgSupported = false
touchEventsSupported = false
pointerEventsSupported = false
domSupported = false
transformSupported = false
transform3dSupported = false
hasGlobalWindow = typeof window !== 'undefined'
}
const env = new Env();
if (typeof wx === 'object' && typeof wx.getSystemInfoSync === 'function') {
env.wxa = true;
env.touchEventsSupported = true;
}
else if (typeof document === 'undefined' && typeof self !== 'undefined') {
// In worker
env.worker = true;
}
else if (!env.hasGlobalWindow || 'Deno' in window) {
// In node
env.node = true;
env.svgSupported = true;
}
else {
detect(navigator.userAgent, env);
}
// Zepto.js
// (c) 2010-2013 Thomas Fuchs
// Zepto.js may be freely distributed under the MIT license.
function detect(ua: string, env: Env) {
const browser = env.browser;
const firefox = ua.match(/Firefox\/([\d.]+)/);
const ie = ua.match(/MSIE\s([\d.]+)/)
// IE 11 Trident/7.0; rv:11.0
|| ua.match(/Trident\/.+?rv:(([\d.]+))/);
const edge = ua.match(/Edge?\/([\d.]+)/); // IE 12 and 12+
const weChat = (/micromessenger/i).test(ua);
if (firefox) {
browser.firefox = true;
browser.version = firefox[1];
}
if (ie) {
browser.ie = true;
browser.version = ie[1];
}
if (edge) {
browser.edge = true;
browser.version = edge[1];
browser.newEdge = +edge[1].split('.')[0] > 18;
}
// It is difficult to detect WeChat in Win Phone precisely, because ua can
// not be set on win phone. So we do not consider Win Phone.
if (weChat) {
browser.weChat = true;
}
env.svgSupported = typeof SVGRect !== 'undefined';
env.touchEventsSupported = 'ontouchstart' in window && !browser.ie && !browser.edge;
env.pointerEventsSupported = 'onpointerdown' in window
&& (browser.edge || (browser.ie && +browser.version >= 11));
env.domSupported = typeof document !== 'undefined';
const style = document.documentElement.style;
env.transform3dSupported = (
// IE9 only supports transform 2D
// transform 3D supported since IE10
// we detect it by whether 'transition' is in style
(browser.ie && 'transition' in style)
// edge
|| browser.edge
// webkit
|| (('WebKitCSSMatrix' in window) && ('m11' in new WebKitCSSMatrix()))
// gecko-based browsers
|| 'MozPerspective' in style
) // Opera supports CSS transforms after version 12
&& !('OTransition' in style);
// except IE 6-8 and very old firefox 2-3 & opera 10.1
// other browsers all support `transform`
env.transformSupported = env.transform3dSupported
// transform 2D is supported in IE9
|| (browser.ie && +browser.version >= 9);
}
export default env;
+313
View File
@@ -0,0 +1,313 @@
/**
* Utilities for mouse or touch events.
*/
import Eventful from './Eventful';
import env from './env';
import { ZRRawEvent } from './types';
import {isCanvasEl, transformCoordWithViewport} from './dom';
const MOUSE_EVENT_REG = /^(?:mouse|pointer|contextmenu|drag|drop)|click/;
const _calcOut: number[] = [];
const firefoxNotSupportOffsetXY = env.browser.firefox
// use offsetX/offsetY for Firefox >= 39
// PENDING: consider Firefox for Android and Firefox OS? >= 43
&& +(env.browser.version as string).split('.')[0] < 39;
type FirefoxMouseEvent = {
layerX: number
layerY: number
}
/**
* Get the `zrX` and `zrY`, which are relative to the top-left of
* the input `el`.
* CSS transform (2D & 3D) is supported.
*
* The strategy to fetch the coords:
* + If `calculate` is not set as `true`, users of this method should
* ensure that `el` is the same or the same size & location as `e.target`.
* Otherwise the result coords are probably not expected. Because we
* firstly try to get coords from e.offsetX/e.offsetY.
* + If `calculate` is set as `true`, the input `el` can be any element
* and we force to calculate the coords based on `el`.
* + The input `el` should be positionable (not position:static).
*
* The force `calculate` can be used in case like:
* When mousemove event triggered on ec tooltip, `e.target` is not `el`(zr painter.dom).
*
* @param el DOM element.
* @param e Mouse event or touch event.
* @param out Get `out.zrX` and `out.zrY` as the result.
* @param calculate Whether to force calculate
* the coordinates but not use ones provided by browser.
*/
export function clientToLocal(
el: HTMLElement,
e: ZRRawEvent | FirefoxMouseEvent | Touch,
out: {zrX?: number, zrY?: number},
calculate?: boolean
) {
out = out || {};
// According to the W3C Working Draft, offsetX and offsetY should be relative
// to the padding edge of the target element. The only browser using this convention
// is IE. Webkit uses the border edge, Opera uses the content edge, and FireFox does
// not support the properties.
// (see http://www.jacklmoore.com/notes/mouse-position/)
// In zr painter.dom, padding edge equals to border edge.
if (calculate) {
calculateZrXY(el, e as ZRRawEvent, out);
}
// Caution: In FireFox, layerX/layerY Mouse position relative to the closest positioned
// ancestor element, so we should make sure el is positioned (e.g., not position:static).
// BTW1, Webkit don't return the same results as FF in non-simple cases (like add
// zoom-factor, overflow / opacity layers, transforms ...)
// BTW2, (ev.offsetY || ev.pageY - $(ev.target).offset().top) is not correct in preserve-3d.
// <https://bugs.jquery.com/ticket/8523#comment:14>
// BTW3, In ff, offsetX/offsetY is always 0.
else if (firefoxNotSupportOffsetXY
&& (e as FirefoxMouseEvent).layerX != null
&& (e as FirefoxMouseEvent).layerX !== (e as MouseEvent).offsetX
) {
out.zrX = (e as FirefoxMouseEvent).layerX;
out.zrY = (e as FirefoxMouseEvent).layerY;
}
// For IE6+, chrome, safari, opera, firefox >= 39
else if ((e as MouseEvent).offsetX != null) {
out.zrX = (e as MouseEvent).offsetX;
out.zrY = (e as MouseEvent).offsetY;
}
// For some other device, e.g., IOS safari.
else {
calculateZrXY(el, e as ZRRawEvent, out);
}
return out;
}
function calculateZrXY(
el: HTMLElement,
e: ZRRawEvent,
out: {zrX?: number, zrY?: number}
) {
// BlackBerry 5, iOS 3 (original iPhone) don't have getBoundingRect.
if (env.domSupported && el.getBoundingClientRect) {
const ex = (e as MouseEvent).clientX;
const ey = (e as MouseEvent).clientY;
if (isCanvasEl(el)) {
// Original approach, which do not support CSS transform.
// marker can not be locationed in a canvas container
// (getBoundingClientRect is always 0). We do not support
// that input a pre-created canvas to zr while using css
// transform in iOS.
const box = el.getBoundingClientRect();
out.zrX = ex - box.left;
out.zrY = ey - box.top;
return;
}
else {
if (transformCoordWithViewport(_calcOut, el, ex, ey)) {
out.zrX = _calcOut[0];
out.zrY = _calcOut[1];
return;
}
}
}
out.zrX = out.zrY = 0;
}
/**
* Find native event compat for legency IE.
* Should be called at the begining of a native event listener.
*
* @param e Mouse event or touch event or pointer event.
* For lagency IE, we use `window.event` is used.
* @return The native event.
*/
export function getNativeEvent(e: ZRRawEvent): ZRRawEvent {
return e
|| (window.event as any); // For IE
}
/**
* Normalize the coordinates of the input event.
*
* Get the `e.zrX` and `e.zrY`, which are relative to the top-left of
* the input `el`.
* Get `e.zrDelta` if using mouse wheel.
* Get `e.which`, see the comment inside this function.
*
* Do not calculate repeatly if `zrX` and `zrY` already exist.
*
* Notice: see comments in `clientToLocal`. check the relationship
* between the result coords and the parameters `el` and `calculate`.
*
* @param el DOM element.
* @param e See `getNativeEvent`.
* @param calculate Whether to force calculate
* the coordinates but not use ones provided by browser.
* @return The normalized native UIEvent.
*/
export function normalizeEvent(
el: HTMLElement,
e: ZRRawEvent,
calculate?: boolean
) {
e = getNativeEvent(e);
if (e.zrX != null) {
return e;
}
const eventType = e.type;
const isTouch = eventType && eventType.indexOf('touch') >= 0;
if (!isTouch) {
clientToLocal(el, e, e, calculate);
const wheelDelta = getWheelDeltaMayPolyfill(e);
// FIXME: IE8- has "wheelDeta" in event "mousewheel" but hat different value (120 times)
// with Chrome and Safari. It's not correct for zrender event but we left it as it was.
e.zrDelta = wheelDelta ? wheelDelta / 120 : -(e.detail || 0) / 3;
}
else {
const touch = eventType !== 'touchend'
? (<TouchEvent>e).targetTouches[0]
: (<TouchEvent>e).changedTouches[0];
touch && clientToLocal(el, touch, e, calculate);
}
// Add which for click: 1 === left; 2 === middle; 3 === right; otherwise: 0;
// See jQuery: https://github.com/jquery/jquery/blob/master/src/event.js
// If e.which has been defined, it may be readonly,
// see: https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/which
const button = (<MouseEvent>e).button;
if (e.which == null && button !== undefined && MOUSE_EVENT_REG.test(e.type)) {
(e as any).which = (button & 1 ? 1 : (button & 2 ? 3 : (button & 4 ? 2 : 0)));
}
// [Caution]: `e.which` from browser is not always reliable. For example,
// when press left button and `mousemove (pointermove)` in Edge, the `e.which`
// is 65536 and the `e.button` is -1. But the `mouseup (pointerup)` and
// `mousedown (pointerdown)` is the same as Chrome does.
return e;
}
// TODO: also provide prop "deltaX" "deltaY" in zrender "mousewheel" event.
function getWheelDeltaMayPolyfill(e: ZRRawEvent): number {
// Although event "wheel" do not has the prop "wheelDelta" in spec,
// agent like Chrome and Safari still provide "wheelDelta" like
// event "mousewheel" did (perhaps for backward compat).
// Since zrender has been using "wheelDeta" in zrender event "mousewheel".
// we currently do not break it.
// But event "wheel" in firefox do not has "wheelDelta", so we calculate
// "wheelDeta" from "deltaX", "deltaY" (which is the props in spec).
const rawWheelDelta = (e as any).wheelDelta;
// Theroetically `e.wheelDelta` won't be 0 unless some day it has been deprecated
// by agent like Chrome or Safari. So we also calculate it if rawWheelDelta is 0.
if (rawWheelDelta) {
return rawWheelDelta;
}
const deltaX = (e as any).deltaX;
const deltaY = (e as any).deltaY;
if (deltaX == null || deltaY == null) {
return rawWheelDelta;
}
// Test in Chrome and Safari (MacOS):
// The sign is corrent.
// The abs value is 99% corrent (inconsist case only like 62~63, 125~126 ...)
const delta = deltaY !== 0 ? Math.abs(deltaY) : Math.abs(deltaX);
const sign = deltaY > 0 ? -1
: deltaY < 0 ? 1
: deltaX > 0 ? -1
: 1;
return 3 * delta * sign;
}
type AddEventListenerParams = Parameters<typeof HTMLElement.prototype.addEventListener>
type RemoveEventListenerParams = Parameters<typeof HTMLElement.prototype.removeEventListener>
/**
* @param el
* @param name
* @param handler
* @param opt If boolean, means `opt.capture`
* @param opt.capture
* @param opt.passive
*/
export function addEventListener(
el: HTMLElement | HTMLDocument,
name: AddEventListenerParams[0],
handler: AddEventListenerParams[1],
opt?: AddEventListenerParams[2]
) {
// Reproduct the console warning:
// [Violation] Added non-passive event listener to a scroll-blocking <some> event.
// Consider marking event handler as 'passive' to make the page more responsive.
// Just set console log level: verbose in chrome dev tool.
// then the warning log will be printed when addEventListener called.
// See https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md
// We have not yet found a neat way to using passive. Because in zrender the dom event
// listener delegate all of the upper events of element. Some of those events need
// to prevent default. For example, the feature `preventDefaultMouseMove` of echarts.
// Before passive can be adopted, these issues should be considered:
// (1) Whether and how a zrender user specifies an event listener passive. And by default,
// passive or not.
// (2) How to tread that some zrender event listener is passive, and some is not. If
// we use other way but not preventDefault of mousewheel and touchmove, browser
// compatibility should be handled.
// const opts = (env.passiveSupported && name === 'mousewheel')
// ? {passive: true}
// // By default, the third param of el.addEventListener is `capture: false`.
// : void 0;
// el.addEventListener(name, handler /* , opts */);
el.addEventListener(name, handler, opt);
}
/**
* Parameter are the same as `addEventListener`.
*
* Notice that if a listener is registered twice, one with capture and one without,
* remove each one separately. Removal of a capturing listener does not affect a
* non-capturing version of the same listener, and vice versa.
*/
export function removeEventListener(
el: HTMLElement | HTMLDocument,
name: RemoveEventListenerParams[0],
handler: RemoveEventListenerParams[1],
opt: RemoveEventListenerParams[2]
) {
el.removeEventListener(name, handler, opt);
}
/**
* preventDefault and stopPropagation.
* Notice: do not use this method in zrender. It can only be
* used by upper applications if necessary.
*
* @param {Event} e A mouse or touch event.
*/
export const stop = function (e: MouseEvent | TouchEvent | PointerEvent) {
e.preventDefault();
e.stopPropagation();
e.cancelBubble = true;
};
/**
* This method only works for mouseup and mousedown. The functionality is restricted
* for fault tolerance, See the `e.which` compatibility above.
*
* params can be MouseEvent or ElementEvent
*/
export function isMiddleOrRightButtonOnMouseUpDown(e: { which: number }) {
return e.which === 2 || e.which === 3;
}
// For backward compatibility
export {Eventful as Dispatcher};
+108
View File
@@ -0,0 +1,108 @@
/**
* The algoritm is learnt from
* https://franklinta.com/2014/09/08/computing-css-matrix3d-transforms/
* And we made some optimization for matrix inversion.
* Other similar approaches:
* "cv::getPerspectiveTransform", "Direct Linear Transformation".
*/
const LN2 = Math.log(2);
function determinant(
rows: number[][],
rank: number,
rowStart: number,
rowMask: number,
colMask: number,
detCache: {[key: string]: number}
) {
const cacheKey = rowMask + '-' + colMask;
const fullRank = rows.length;
if (detCache.hasOwnProperty(cacheKey)) {
return detCache[cacheKey];
}
if (rank === 1) {
// In this case the colMask must be like: `11101111`. We can find the place of `0`.
const colStart = Math.round(Math.log(((1 << fullRank) - 1) & ~colMask) / LN2);
return rows[rowStart][colStart];
}
const subRowMask = rowMask | (1 << rowStart);
let subRowStart = rowStart + 1;
while (rowMask & (1 << subRowStart)) {
subRowStart++;
}
let sum = 0;
for (let j = 0, colLocalIdx = 0; j < fullRank; j++) {
const colTag = 1 << j;
if (!(colTag & colMask)) {
sum += (colLocalIdx % 2 ? -1 : 1) * rows[rowStart][j]
// det(subMatrix(0, j))
* determinant(rows, rank - 1, subRowStart, subRowMask, colMask | colTag, detCache);
colLocalIdx++;
}
}
detCache[cacheKey] = sum;
return sum;
}
/**
* Usage:
* ```js
* const transformer = buildTransformer(
* [10, 44, 100, 44, 100, 300, 10, 300],
* [50, 54, 130, 14, 140, 330, 14, 220]
* );
* const out = [];
* transformer && transformer([11, 33], out);
* ```
*
* Notice: `buildTransformer` may take more than 10ms in some Android device.
*
* @param src source four points, [x0, y0, x1, y1, x2, y2, x3, y3]
* @param dest destination four points, [x0, y0, x1, y1, x2, y2, x3, y3]
* @return transformer If fail, return null/undefined.
*/
export function buildTransformer(src: number[], dest: number[]) {
const mA = [
[src[0], src[1], 1, 0, 0, 0, -dest[0] * src[0], -dest[0] * src[1]],
[0, 0, 0, src[0], src[1], 1, -dest[1] * src[0], -dest[1] * src[1]],
[src[2], src[3], 1, 0, 0, 0, -dest[2] * src[2], -dest[2] * src[3]],
[0, 0, 0, src[2], src[3], 1, -dest[3] * src[2], -dest[3] * src[3]],
[src[4], src[5], 1, 0, 0, 0, -dest[4] * src[4], -dest[4] * src[5]],
[0, 0, 0, src[4], src[5], 1, -dest[5] * src[4], -dest[5] * src[5]],
[src[6], src[7], 1, 0, 0, 0, -dest[6] * src[6], -dest[6] * src[7]],
[0, 0, 0, src[6], src[7], 1, -dest[7] * src[6], -dest[7] * src[7]]
];
const detCache = {};
const det = determinant(mA, 8, 0, 0, 0, detCache);
if (det === 0) {
// can not make transformer when and only when
// any three of the markers are collinear.
return;
}
// `invert(mA) * dest`, that is, `adj(mA) / det * dest`.
const vh: number[] = [];
for (let i = 0; i < 8; i++) {
for (let j = 0; j < 8; j++) {
vh[j] == null && (vh[j] = 0);
vh[j] += ((i + j) % 2 ? -1 : 1)
// det(subMatrix(i, j))
* determinant(mA, 7, i === 0 ? 1 : 0, 1 << i, 1 << j, detCache)
/ det * dest[i];
}
}
return function (out: number[], srcPointX: number, srcPointY: number) {
const pk = srcPointX * vh[6] + srcPointY * vh[7] + 1;
out[0] = (srcPointX * vh[0] + srcPointY * vh[1] + vh[2]) / pk;
out[1] = (srcPointX * vh[3] + srcPointY * vh[4] + vh[5]) / pk;
};
}
+155
View File
@@ -0,0 +1,155 @@
/**
* 3x2矩阵操作类
* @exports zrender/tool/matrix
*/
/* global Float32Array */
import {VectorArray} from './vector';
export type MatrixArray = number[]
/**
* Create a identity matrix.
*/
export function create(): MatrixArray {
return [1, 0, 0, 1, 0, 0];
}
/**
* 设置矩阵为单位矩阵
*/
export function identity(out: MatrixArray): MatrixArray {
out[0] = 1;
out[1] = 0;
out[2] = 0;
out[3] = 1;
out[4] = 0;
out[5] = 0;
return out;
}
/**
* 复制矩阵
*/
export function copy(out: MatrixArray, m: MatrixArray): MatrixArray {
out[0] = m[0];
out[1] = m[1];
out[2] = m[2];
out[3] = m[3];
out[4] = m[4];
out[5] = m[5];
return out;
}
/**
* 矩阵相乘
*/
export function mul(out: MatrixArray, m1: MatrixArray, m2: MatrixArray): MatrixArray {
// Consider matrix.mul(m, m2, m);
// where out is the same as m2.
// So use temp constiable to escape error.
const out0 = m1[0] * m2[0] + m1[2] * m2[1];
const out1 = m1[1] * m2[0] + m1[3] * m2[1];
const out2 = m1[0] * m2[2] + m1[2] * m2[3];
const out3 = m1[1] * m2[2] + m1[3] * m2[3];
const out4 = m1[0] * m2[4] + m1[2] * m2[5] + m1[4];
const out5 = m1[1] * m2[4] + m1[3] * m2[5] + m1[5];
out[0] = out0;
out[1] = out1;
out[2] = out2;
out[3] = out3;
out[4] = out4;
out[5] = out5;
return out;
}
/**
* 平移变换
*/
export function translate(out: MatrixArray, a: MatrixArray, v: VectorArray): MatrixArray {
out[0] = a[0];
out[1] = a[1];
out[2] = a[2];
out[3] = a[3];
out[4] = a[4] + v[0];
out[5] = a[5] + v[1];
return out;
}
/**
* 旋转变换
*/
export function rotate(
out: MatrixArray,
a: MatrixArray,
rad: number,
pivot: VectorArray = [0, 0]
): MatrixArray {
const aa = a[0];
const ac = a[2];
const atx = a[4];
const ab = a[1];
const ad = a[3];
const aty = a[5];
const st = Math.sin(rad);
const ct = Math.cos(rad);
out[0] = aa * ct + ab * st;
out[1] = -aa * st + ab * ct;
out[2] = ac * ct + ad * st;
out[3] = -ac * st + ct * ad;
out[4] = ct * (atx - pivot[0]) + st * (aty - pivot[1]) + pivot[0];
out[5] = ct * (aty - pivot[1]) - st * (atx - pivot[0]) + pivot[1];
return out;
}
/**
* 缩放变换
*/
export function scale(out: MatrixArray, a: MatrixArray, v: VectorArray): MatrixArray {
const vx = v[0];
const vy = v[1];
out[0] = a[0] * vx;
out[1] = a[1] * vy;
out[2] = a[2] * vx;
out[3] = a[3] * vy;
out[4] = a[4] * vx;
out[5] = a[5] * vy;
return out;
}
/**
* 求逆矩阵
*/
export function invert(out: MatrixArray, a: MatrixArray): MatrixArray | null {
const aa = a[0];
const ac = a[2];
const atx = a[4];
const ab = a[1];
const ad = a[3];
const aty = a[5];
let det = aa * ad - ab * ac;
if (!det) {
return null;
}
det = 1.0 / det;
out[0] = ad * det;
out[1] = -ab * det;
out[2] = -ac * det;
out[3] = aa * det;
out[4] = (ac * aty - ad * atx) * det;
out[5] = (ab * atx - aa * aty) * det;
return out;
}
/**
* Clone a new matrix.
*/
export function clone(a: MatrixArray): MatrixArray {
const b = create();
copy(b, a);
return b;
}
+111
View File
@@ -0,0 +1,111 @@
export const DEFAULT_FONT_SIZE = 12;
export const DEFAULT_FONT_FAMILY = 'sans-serif';
export const DEFAULT_FONT = `${DEFAULT_FONT_SIZE}px ${DEFAULT_FONT_FAMILY}`;
interface Platform {
// TODO CanvasLike?
createCanvas(): HTMLCanvasElement
measureText(text: string, font?: string): { width: number }
loadImage(
src: string,
onload: () => void | HTMLImageElement['onload'],
onerror: () => void | HTMLImageElement['onerror']
): HTMLImageElement
}
// Text width map used for environment there is no canvas
// Only common ascii is used for size concern.
// Generated from following code
//
// ctx.font = '12px sans-serif';
// const asciiRange = [32, 126];
// let mapStr = '';
// for (let i = asciiRange[0]; i <= asciiRange[1]; i++) {
// const char = String.fromCharCode(i);
// const width = ctx.measureText(char).width;
// const ratio = Math.round(width / 12 * 100);
// mapStr += String.fromCharCode(ratio + 20))
// }
// mapStr.replace(/\\/g, '\\\\');
const OFFSET = 20;
const SCALE = 100;
// TODO other basic fonts?
// eslint-disable-next-line
const defaultWidthMapStr = `007LLmW'55;N0500LLLLLLLLLL00NNNLzWW\\\\WQb\\0FWLg\\bWb\\WQ\\WrWWQ000CL5LLFLL0LL**F*gLLLL5F0LF\\FFF5.5N`;
function getTextWidthMap(mapStr: string): Record<string, number> {
const map: Record<string, number> = {};
if (typeof JSON === 'undefined') {
return map;
}
for (let i = 0; i < mapStr.length; i++) {
const char = String.fromCharCode(i + 32);
const size = (mapStr.charCodeAt(i) - OFFSET) / SCALE;
map[char] = size;
}
return map;
}
export const DEFAULT_TEXT_WIDTH_MAP = getTextWidthMap(defaultWidthMapStr);
export const platformApi: Platform = {
// Export methods
createCanvas() {
return typeof document !== 'undefined'
&& document.createElement('canvas');
},
measureText: (function () {
let _ctx: CanvasRenderingContext2D;
let _cachedFont: string;
return (text: string, font?: string) => {
if (!_ctx) {
const canvas = platformApi.createCanvas();
_ctx = canvas && canvas.getContext('2d');
}
if (_ctx) {
if (_cachedFont !== font) {
_cachedFont = _ctx.font = font || DEFAULT_FONT;
}
return _ctx.measureText(text);
}
else {
text = text || '';
font = font || DEFAULT_FONT;
// Use font size if there is no other method can be used.
const res = /((?:\d+)?\.?\d*)px/.exec(font);
const fontSize = res && +res[1] || DEFAULT_FONT_SIZE;
let width = 0;
if (font.indexOf('mono') >= 0) { // is monospace
width = fontSize * text.length;
}
else {
for (let i = 0; i < text.length; i++) {
const preCalcWidth = DEFAULT_TEXT_WIDTH_MAP[text[i]];
width += preCalcWidth == null ? fontSize : (preCalcWidth * fontSize);
}
}
return { width };
}
};
})(),
loadImage(src, onload, onerror) {
const image = new Image();
image.onload = onload;
image.onerror = onerror;
image.src = src;
return image;
}
};
export function setPlatformAPI(newPlatformApis: Partial<Platform>) {
for (let key in platformApi) {
// Don't assign unknown methods.
if ((newPlatformApis as any)[key]) {
(platformApi as any)[key] = (newPlatformApis as any)[key];
}
}
}
+671
View File
@@ -0,0 +1,671 @@
// https://github.com/mziccard/node-timsort
const DEFAULT_MIN_MERGE = 32;
const DEFAULT_MIN_GALLOPING = 7;
type CompareFunc<T> =(a: T, b: T) => number
function minRunLength(n: number): number {
var r = 0;
while (n >= DEFAULT_MIN_MERGE) {
r |= n & 1;
n >>= 1;
}
return n + r;
}
function makeAscendingRun<T>(array: T[], lo: number, hi: number, compare: CompareFunc<T>) {
var runHi = lo + 1;
if (runHi === hi) {
return 1;
}
if (compare(array[runHi++], array[lo]) < 0) {
while (runHi < hi && compare(array[runHi], array[runHi - 1]) < 0) {
runHi++;
}
reverseRun<T>(array, lo, runHi);
}
else {
while (runHi < hi && compare(array[runHi], array[runHi - 1]) >= 0) {
runHi++;
}
}
return runHi - lo;
}
function reverseRun<T>(array: T[], lo: number, hi: number) {
hi--;
while (lo < hi) {
var t = array[lo];
array[lo++] = array[hi];
array[hi--] = t;
}
}
function binaryInsertionSort<T>(array: T[], lo: number, hi: number, start: number, compare: CompareFunc<T>) {
if (start === lo) {
start++;
}
for (; start < hi; start++) {
var pivot = array[start];
var left = lo;
var right = start;
var mid;
while (left < right) {
mid = left + right >>> 1;
if (compare(pivot, array[mid]) < 0) {
right = mid;
}
else {
left = mid + 1;
}
}
var n = start - left;
switch (n) {
case 3:
array[left + 3] = array[left + 2];
case 2:
array[left + 2] = array[left + 1];
case 1:
array[left + 1] = array[left];
break;
default:
while (n > 0) {
array[left + n] = array[left + n - 1];
n--;
}
}
array[left] = pivot;
}
}
function gallopLeft<T>(value: T, array: T[], start: number, length: number, hint: number, compare: CompareFunc<T>) {
var lastOffset = 0;
var maxOffset = 0;
var offset = 1;
if (compare(value, array[start + hint]) > 0) {
maxOffset = length - hint;
while (offset < maxOffset && compare(value, array[start + hint + offset]) > 0) {
lastOffset = offset;
offset = (offset << 1) + 1;
if (offset <= 0) {
offset = maxOffset;
}
}
if (offset > maxOffset) {
offset = maxOffset;
}
lastOffset += hint;
offset += hint;
}
else {
maxOffset = hint + 1;
while (offset < maxOffset && compare(value, array[start + hint - offset]) <= 0) {
lastOffset = offset;
offset = (offset << 1) + 1;
if (offset <= 0) {
offset = maxOffset;
}
}
if (offset > maxOffset) {
offset = maxOffset;
}
var tmp = lastOffset;
lastOffset = hint - offset;
offset = hint - tmp;
}
lastOffset++;
while (lastOffset < offset) {
var m = lastOffset + (offset - lastOffset >>> 1);
if (compare(value, array[start + m]) > 0) {
lastOffset = m + 1;
}
else {
offset = m;
}
}
return offset;
}
function gallopRight<T>(value: T, array: T[], start: number, length: number, hint: number, compare: CompareFunc<T>) {
var lastOffset = 0;
var maxOffset = 0;
var offset = 1;
if (compare(value, array[start + hint]) < 0) {
maxOffset = hint + 1;
while (offset < maxOffset && compare(value, array[start + hint - offset]) < 0) {
lastOffset = offset;
offset = (offset << 1) + 1;
if (offset <= 0) {
offset = maxOffset;
}
}
if (offset > maxOffset) {
offset = maxOffset;
}
var tmp = lastOffset;
lastOffset = hint - offset;
offset = hint - tmp;
}
else {
maxOffset = length - hint;
while (offset < maxOffset && compare(value, array[start + hint + offset]) >= 0) {
lastOffset = offset;
offset = (offset << 1) + 1;
if (offset <= 0) {
offset = maxOffset;
}
}
if (offset > maxOffset) {
offset = maxOffset;
}
lastOffset += hint;
offset += hint;
}
lastOffset++;
while (lastOffset < offset) {
var m = lastOffset + (offset - lastOffset >>> 1);
if (compare(value, array[start + m]) < 0) {
offset = m;
}
else {
lastOffset = m + 1;
}
}
return offset;
}
function TimSort<T>(array: T[], compare: CompareFunc<T>) {
let minGallop = DEFAULT_MIN_GALLOPING;
let runStart: number[];
let runLength: number[];
let stackSize = 0;
var tmp: T[] = [];
runStart = [];
runLength = [];
function pushRun(_runStart: number, _runLength: number) {
runStart[stackSize] = _runStart;
runLength[stackSize] = _runLength;
stackSize += 1;
}
function mergeRuns() {
while (stackSize > 1) {
var n = stackSize - 2;
if (
(n >= 1 && runLength[n - 1] <= runLength[n] + runLength[n + 1])
|| (n >= 2 && runLength[n - 2] <= runLength[n] + runLength[n - 1])
) {
if (runLength[n - 1] < runLength[n + 1]) {
n--;
}
}
else if (runLength[n] > runLength[n + 1]) {
break;
}
mergeAt(n);
}
}
function forceMergeRuns() {
while (stackSize > 1) {
var n = stackSize - 2;
if (n > 0 && runLength[n - 1] < runLength[n + 1]) {
n--;
}
mergeAt(n);
}
}
function mergeAt(i: number) {
var start1 = runStart[i];
var length1 = runLength[i];
var start2 = runStart[i + 1];
var length2 = runLength[i + 1];
runLength[i] = length1 + length2;
if (i === stackSize - 3) {
runStart[i + 1] = runStart[i + 2];
runLength[i + 1] = runLength[i + 2];
}
stackSize--;
var k = gallopRight<T>(array[start2], array, start1, length1, 0, compare);
start1 += k;
length1 -= k;
if (length1 === 0) {
return;
}
length2 = gallopLeft<T>(array[start1 + length1 - 1], array, start2, length2, length2 - 1, compare);
if (length2 === 0) {
return;
}
if (length1 <= length2) {
mergeLow(start1, length1, start2, length2);
}
else {
mergeHigh(start1, length1, start2, length2);
}
}
function mergeLow(start1: number, length1: number, start2: number, length2: number) {
var i = 0;
for (i = 0; i < length1; i++) {
tmp[i] = array[start1 + i];
}
var cursor1 = 0;
var cursor2 = start2;
var dest = start1;
array[dest++] = array[cursor2++];
if (--length2 === 0) {
for (i = 0; i < length1; i++) {
array[dest + i] = tmp[cursor1 + i];
}
return;
}
if (length1 === 1) {
for (i = 0; i < length2; i++) {
array[dest + i] = array[cursor2 + i];
}
array[dest + length2] = tmp[cursor1];
return;
}
var _minGallop = minGallop;
var count1;
var count2;
var exit;
while (1) {
count1 = 0;
count2 = 0;
exit = false;
do {
if (compare(array[cursor2], tmp[cursor1]) < 0) {
array[dest++] = array[cursor2++];
count2++;
count1 = 0;
if (--length2 === 0) {
exit = true;
break;
}
}
else {
array[dest++] = tmp[cursor1++];
count1++;
count2 = 0;
if (--length1 === 1) {
exit = true;
break;
}
}
} while ((count1 | count2) < _minGallop);
if (exit) {
break;
}
do {
count1 = gallopRight<T>(array[cursor2], tmp, cursor1, length1, 0, compare);
if (count1 !== 0) {
for (i = 0; i < count1; i++) {
array[dest + i] = tmp[cursor1 + i];
}
dest += count1;
cursor1 += count1;
length1 -= count1;
if (length1 <= 1) {
exit = true;
break;
}
}
array[dest++] = array[cursor2++];
if (--length2 === 0) {
exit = true;
break;
}
count2 = gallopLeft<T>(tmp[cursor1], array, cursor2, length2, 0, compare);
if (count2 !== 0) {
for (i = 0; i < count2; i++) {
array[dest + i] = array[cursor2 + i];
}
dest += count2;
cursor2 += count2;
length2 -= count2;
if (length2 === 0) {
exit = true;
break;
}
}
array[dest++] = tmp[cursor1++];
if (--length1 === 1) {
exit = true;
break;
}
_minGallop--;
} while (count1 >= DEFAULT_MIN_GALLOPING || count2 >= DEFAULT_MIN_GALLOPING);
if (exit) {
break;
}
if (_minGallop < 0) {
_minGallop = 0;
}
_minGallop += 2;
}
minGallop = _minGallop;
minGallop < 1 && (minGallop = 1);
if (length1 === 1) {
for (i = 0; i < length2; i++) {
array[dest + i] = array[cursor2 + i];
}
array[dest + length2] = tmp[cursor1];
}
else if (length1 === 0) {
throw new Error();
}
else {
for (i = 0; i < length1; i++) {
array[dest + i] = tmp[cursor1 + i];
}
}
}
function mergeHigh(start1: number, length1: number, start2: number, length2: number) {
var i = 0;
for (i = 0; i < length2; i++) {
tmp[i] = array[start2 + i];
}
var cursor1 = start1 + length1 - 1;
var cursor2 = length2 - 1;
var dest = start2 + length2 - 1;
var customCursor = 0;
var customDest = 0;
array[dest--] = array[cursor1--];
if (--length1 === 0) {
customCursor = dest - (length2 - 1);
for (i = 0; i < length2; i++) {
array[customCursor + i] = tmp[i];
}
return;
}
if (length2 === 1) {
dest -= length1;
cursor1 -= length1;
customDest = dest + 1;
customCursor = cursor1 + 1;
for (i = length1 - 1; i >= 0; i--) {
array[customDest + i] = array[customCursor + i];
}
array[dest] = tmp[cursor2];
return;
}
var _minGallop = minGallop;
while (true) {
var count1 = 0;
var count2 = 0;
var exit = false;
do {
if (compare(tmp[cursor2], array[cursor1]) < 0) {
array[dest--] = array[cursor1--];
count1++;
count2 = 0;
if (--length1 === 0) {
exit = true;
break;
}
}
else {
array[dest--] = tmp[cursor2--];
count2++;
count1 = 0;
if (--length2 === 1) {
exit = true;
break;
}
}
} while ((count1 | count2) < _minGallop);
if (exit) {
break;
}
do {
count1 = length1 - gallopRight<T>(tmp[cursor2], array, start1, length1, length1 - 1, compare);
if (count1 !== 0) {
dest -= count1;
cursor1 -= count1;
length1 -= count1;
customDest = dest + 1;
customCursor = cursor1 + 1;
for (i = count1 - 1; i >= 0; i--) {
array[customDest + i] = array[customCursor + i];
}
if (length1 === 0) {
exit = true;
break;
}
}
array[dest--] = tmp[cursor2--];
if (--length2 === 1) {
exit = true;
break;
}
count2 = length2 - gallopLeft<T>(array[cursor1], tmp, 0, length2, length2 - 1, compare);
if (count2 !== 0) {
dest -= count2;
cursor2 -= count2;
length2 -= count2;
customDest = dest + 1;
customCursor = cursor2 + 1;
for (i = 0; i < count2; i++) {
array[customDest + i] = tmp[customCursor + i];
}
if (length2 <= 1) {
exit = true;
break;
}
}
array[dest--] = array[cursor1--];
if (--length1 === 0) {
exit = true;
break;
}
_minGallop--;
} while (count1 >= DEFAULT_MIN_GALLOPING || count2 >= DEFAULT_MIN_GALLOPING);
if (exit) {
break;
}
if (_minGallop < 0) {
_minGallop = 0;
}
_minGallop += 2;
}
minGallop = _minGallop;
if (minGallop < 1) {
minGallop = 1;
}
if (length2 === 1) {
dest -= length1;
cursor1 -= length1;
customDest = dest + 1;
customCursor = cursor1 + 1;
for (i = length1 - 1; i >= 0; i--) {
array[customDest + i] = array[customCursor + i];
}
array[dest] = tmp[cursor2];
}
else if (length2 === 0) {
throw new Error();
// throw new Error('mergeHigh preconditions were not respected');
}
else {
customCursor = dest - (length2 - 1);
for (i = 0; i < length2; i++) {
array[customCursor + i] = tmp[i];
}
}
}
return {
mergeRuns,
forceMergeRuns,
pushRun
};
}
export default function sort<T>(
array: T[],
compare: CompareFunc<T>,
lo?: number, hi?: number
) {
if (!lo) {
lo = 0;
}
if (!hi) {
hi = array.length;
}
var remaining = hi - lo;
if (remaining < 2) {
return;
}
var runLength = 0;
if (remaining < DEFAULT_MIN_MERGE) {
runLength = makeAscendingRun<T>(array, lo, hi, compare);
binaryInsertionSort<T>(array, lo, hi, lo + runLength, compare);
return;
}
var ts = TimSort<T>(array, compare);
var minRun = minRunLength(remaining);
do {
runLength = makeAscendingRun<T>(array, lo, hi, compare);
if (runLength < minRun) {
var force = remaining;
if (force > minRun) {
force = minRun;
}
binaryInsertionSort<T>(array, lo, lo + force, lo + runLength, compare);
runLength = force;
}
ts.pushRun(lo, runLength);
ts.mergeRuns();
remaining -= runLength;
lo += runLength;
} while (remaining !== 0);
ts.forceMergeRuns();
}
+98
View File
@@ -0,0 +1,98 @@
export type Dictionary<T> = {
[key: string]: T
}
/**
* Not readonly ArrayLike
* Include Array, TypedArray
*/
export type ArrayLike<T> = {
[key: number]: T
length: number
}
export type ImageLike = HTMLImageElement | HTMLCanvasElement | HTMLVideoElement
// subset of CanvasTextBaseline
export type TextVerticalAlign = 'top' | 'middle' | 'bottom'
// | 'center' // DEPRECATED
// TODO: Have not support 'start', 'end' yet.
// subset of CanvasTextAlign
export type TextAlign = 'left' | 'center' | 'right'
// | 'middle' // DEPRECATED
export type FontWeight = 'normal' | 'bold' | 'bolder' | 'lighter' | number;
export type FontStyle = 'normal' | 'italic' | 'oblique';
export type BuiltinTextPosition = 'left' | 'right' | 'top' | 'bottom' | 'inside'
| 'insideLeft' | 'insideRight' | 'insideTop' | 'insideBottom'
| 'insideTopLeft' | 'insideTopRight'| 'insideBottomLeft' | 'insideBottomRight';
export type WXCanvasRenderingContext = CanvasRenderingContext2D & {
draw: () => void
};
export type ZRCanvasRenderingContext = CanvasRenderingContext2D & {
dpr: number
__attrCachedBy: boolean | number
}
// Properties zrender will extended to the raw event
type ZREventProperties = {
zrX: number
zrY: number
zrDelta: number
// 'no_globalout' means: do not trigger "globalout" event to zr user.
// 'only_globalout" means: only trigger "globalout" event, but do not
// trigger other event to zr user.
zrEventControl: 'no_globalout' | 'only_globalout'
zrByTouch: boolean
}
export type ZRRawMouseEvent = MouseEvent & ZREventProperties
export type ZRRawTouchEvent = TouchEvent & ZREventProperties
export type ZRRawPointerEvent = TouchEvent & ZREventProperties
export type ZRRawEvent = ZRRawMouseEvent | ZRRawTouchEvent | ZRRawPointerEvent
export type ZRPinchEvent = ZRRawEvent & {
pinchScale: number
pinchX: number
pinchY: number
gestureEvent: string
}
export type ElementEventName = 'click' | 'dblclick' | 'mousewheel' | 'mouseout' |
'mouseover' | 'mouseup' | 'mousedown' | 'mousemove' | 'contextmenu' |
'drag' | 'dragstart' | 'dragend' | 'dragenter' | 'dragleave' | 'dragover' | 'drop' | 'globalout';
export type ElementEventNameWithOn = 'onclick' | 'ondblclick' | 'onmousewheel' | 'onmouseout' |
'onmouseup' | 'onmousedown' | 'onmousemove' | 'oncontextmenu' |
'ondrag' | 'ondragstart' | 'ondragend' | 'ondragenter' | 'ondragleave' | 'ondragover' | 'ondrop';
export type RenderedEvent = {
elapsedTime: number
};
// Useful type methods
export type PropType<TObj, TProp extends keyof TObj> = TObj[TProp];
export type AllPropTypes<T> = PropType<T, keyof T>
export type FunctionPropertyNames<T> = {[K in keyof T]: T[K] extends Function ? K : never}[keyof T];
export type MapToType<T extends Dictionary<any>, S> = {
[P in keyof T]: T[P] extends Dictionary<any> ? MapToType<T[P], S> : S
}
// See https://www.staging-typescript.org/docs/handbook/advanced-types.html#distributive-conditional-types
// For the case:
// `keyof A | B` does not equals to `Keyof A | Keyof B`
// KeyOfDistributive<A | B> equals to `KeyOfDistributive<A> | KeyOfDistributive<B>`
export type KeyOfDistributive<T> = T extends unknown ? keyof T : never;
export type WithThisType<Func extends (...args: any) => any, This> =
(this: This, ...args: Parameters<Func>) => ReturnType<Func>;
+822
View File
@@ -0,0 +1,822 @@
/* global: defineProperty */
import { Dictionary, ArrayLike, KeyOfDistributive } from './types';
import { GradientObject } from '../graphic/Gradient';
import { ImagePatternObject } from '../graphic/Pattern';
import { platformApi } from './platform';
// 用于处理merge时无法遍历Date等对象的问题
const BUILTIN_OBJECT: Record<string, boolean> = reduce([
'Function',
'RegExp',
'Date',
'Error',
'CanvasGradient',
'CanvasPattern',
// For node-canvas
'Image',
'Canvas'
], (obj, val) => {
obj['[object ' + val + ']'] = true;
return obj;
}, {} as Record<string, boolean>);
const TYPED_ARRAY: Record<string, boolean> = reduce([
'Int8',
'Uint8',
'Uint8Clamped',
'Int16',
'Uint16',
'Int32',
'Uint32',
'Float32',
'Float64'
], (obj, val) => {
obj['[object ' + val + 'Array]'] = true;
return obj;
}, {} as Record<string, boolean>);
const objToString = Object.prototype.toString;
const arrayProto = Array.prototype;
const nativeForEach = arrayProto.forEach;
const nativeFilter = arrayProto.filter;
const nativeSlice = arrayProto.slice;
const nativeMap = arrayProto.map;
// In case some env may redefine the global variable `Function`.
const ctorFunction = function () {}.constructor;
const protoFunction = ctorFunction ? ctorFunction.prototype : null;
const protoKey = '__proto__';
let idStart = 0x0907;
/**
* Generate unique id
*/
export function guid(): number {
return idStart++;
}
export function logError(...args: any[]) {
if (typeof console !== 'undefined') {
console.error.apply(console, args);
}
}
/**
* Those data types can be cloned:
* Plain object, Array, TypedArray, number, string, null, undefined.
* Those data types will be assigned using the original data:
* BUILTIN_OBJECT
* Instance of user defined class will be cloned to a plain object, without
* properties in prototype.
* Other data types is not supported (not sure what will happen).
*
* Caution: do not support clone Date, for performance consideration.
* (There might be a large number of date in `series.data`).
* So date should not be modified in and out of echarts.
*/
export function clone<T extends any>(source: T): T {
if (source == null || typeof source !== 'object') {
return source;
}
let result = source as any;
const typeStr = <string>objToString.call(source);
if (typeStr === '[object Array]') {
if (!isPrimitive(source)) {
result = [] as any;
for (let i = 0, len = (source as any[]).length; i < len; i++) {
result[i] = clone((source as any[])[i]);
}
}
}
else if (TYPED_ARRAY[typeStr]) {
if (!isPrimitive(source)) {
/* eslint-disable-next-line */
const Ctor = source.constructor as typeof Float32Array;
if (Ctor.from) {
result = Ctor.from(source as Float32Array);
}
else {
result = new Ctor((source as Float32Array).length);
for (let i = 0, len = (source as Float32Array).length; i < len; i++) {
result[i] = (source as Float32Array)[i];
}
}
}
}
else if (!BUILTIN_OBJECT[typeStr] && !isPrimitive(source) && !isDom(source)) {
result = {} as any;
for (let key in source) {
// Check if key is __proto__ to avoid prototype pollution
if (source.hasOwnProperty(key) && key !== protoKey) {
result[key] = clone(source[key]);
}
}
}
return result;
}
export function merge<
T extends Dictionary<any>,
S extends Dictionary<any>
>(target: T, source: S, overwrite?: boolean): T & S;
export function merge<
T extends any,
S extends any
>(target: T, source: S, overwrite?: boolean): T | S;
export function merge(target: any, source: any, overwrite?: boolean): any {
// We should escapse that source is string
// and enter for ... in ...
if (!isObject(source) || !isObject(target)) {
return overwrite ? clone(source) : target;
}
for (let key in source) {
// Check if key is __proto__ to avoid prototype pollution
if (source.hasOwnProperty(key) && key !== protoKey) {
const targetProp = target[key];
const sourceProp = source[key];
if (isObject(sourceProp)
&& isObject(targetProp)
&& !isArray(sourceProp)
&& !isArray(targetProp)
&& !isDom(sourceProp)
&& !isDom(targetProp)
&& !isBuiltInObject(sourceProp)
&& !isBuiltInObject(targetProp)
&& !isPrimitive(sourceProp)
&& !isPrimitive(targetProp)
) {
// 如果需要递归覆盖,就递归调用merge
merge(targetProp, sourceProp, overwrite);
}
else if (overwrite || !(key in target)) {
// 否则只处理overwrite为true,或者在目标对象中没有此属性的情况
// NOTE,在 target[key] 不存在的时候也是直接覆盖
target[key] = clone(source[key]);
}
}
}
return target;
}
/**
* @param targetAndSources The first item is target, and the rests are source.
* @param overwrite
* @return Merged result
*/
export function mergeAll(targetAndSources: any[], overwrite?: boolean): any {
let result = targetAndSources[0];
for (let i = 1, len = targetAndSources.length; i < len; i++) {
result = merge(result, targetAndSources[i], overwrite);
}
return result;
}
export function extend<
T extends Dictionary<any>,
S extends Dictionary<any>
>(target: T, source: S): T & S {
// @ts-ignore
if (Object.assign) {
// @ts-ignore
Object.assign(target, source);
}
else {
for (let key in source) {
// Check if key is __proto__ to avoid prototype pollution
if (source.hasOwnProperty(key) && key !== protoKey) {
(target as S & T)[key] = (source as T & S)[key];
}
}
}
return target as T & S;
}
export function defaults<
T extends Dictionary<any>,
S extends Dictionary<any>
>(target: T, source: S, overlay?: boolean): T & S {
const keysArr = keys(source);
for (let i = 0, len = keysArr.length; i < len; i++) {
let key = keysArr[i];
if ((overlay ? source[key] != null : (target as T & S)[key] == null)) {
(target as S & T)[key] = (source as T & S)[key];
}
}
return target as T & S;
}
// Expose createCanvas in util for compatibility
export const createCanvas = platformApi.createCanvas;
/**
* 查询数组中元素的index
*/
export function indexOf<T>(array: T[] | readonly T[] | ArrayLike<T>, value: T): number {
if (array) {
if ((array as T[]).indexOf) {
return (array as T[]).indexOf(value);
}
for (let i = 0, len = array.length; i < len; i++) {
if (array[i] === value) {
return i;
}
}
}
return -1;
}
/**
* 构造类继承关系
*
* @param clazz 源类
* @param baseClazz 基类
*/
export function inherits(clazz: Function, baseClazz: Function) {
const clazzPrototype = clazz.prototype;
function F() {}
F.prototype = baseClazz.prototype;
clazz.prototype = new (F as any)();
for (let prop in clazzPrototype) {
if (clazzPrototype.hasOwnProperty(prop)) {
clazz.prototype[prop] = clazzPrototype[prop];
}
}
clazz.prototype.constructor = clazz;
(clazz as any).superClass = baseClazz;
}
export function mixin<T, S>(target: T | Function, source: S | Function, override?: boolean) {
target = 'prototype' in target ? target.prototype : target;
source = 'prototype' in source ? source.prototype : source;
// If build target is ES6 class. prototype methods is not enumerable. Use getOwnPropertyNames instead
// TODO: Determine if source is ES6 class?
if (Object.getOwnPropertyNames) {
const keyList = Object.getOwnPropertyNames(source);
for (let i = 0; i < keyList.length; i++) {
const key = keyList[i];
if (key !== 'constructor') {
if ((override ? (source as any)[key] != null : (target as any)[key] == null)) {
(target as any)[key] = (source as any)[key];
}
}
}
}
else {
defaults(target, source, override);
}
}
/**
* Consider typed array.
* @param data
*/
export function isArrayLike(data: any): data is ArrayLike<any> {
if (!data) {
return false;
}
if (typeof data === 'string') {
return false;
}
return typeof data.length === 'number';
}
/**
* 数组或对象遍历
*/
export function each<I extends Dictionary<any> | any[] | readonly any[] | ArrayLike<any>, Context>(
arr: I,
cb: (
this: Context,
// Use unknown to avoid to infer to "any", which may disable typo check.
value: I extends (infer T)[] | readonly (infer T)[] | ArrayLike<infer T> ? T
// Use Dictionary<infer T> may cause infer fail when I is an interface.
// So here use a Record to infer type.
: I extends Dictionary<any> ? I extends Record<infer K, infer T> ? T : unknown : unknown,
index?: I extends any[] | readonly any[] | ArrayLike<any> ? number : keyof I & string, // keyof Dictionary will return number | string
arr?: I
) => void,
context?: Context
) {
if (!(arr && cb)) {
return;
}
if ((arr as any).forEach && (arr as any).forEach === nativeForEach) {
(arr as any).forEach(cb, context);
}
else if (arr.length === +arr.length) {
for (let i = 0, len = arr.length; i < len; i++) {
// FIXME: should the elided item be travelled? like `[33,,55]`.
cb.call(context, (arr as any[])[i], i as any, arr);
}
}
else {
for (let key in arr) {
if (arr.hasOwnProperty(key)) {
cb.call(context, (arr as Dictionary<any>)[key], key as any, arr);
}
}
}
}
/**
* Array mapping.
* @typeparam T Type in Array
* @typeparam R Type Returned
* @return Must be an array.
*/
export function map<T, R, Context>(
arr: readonly T[],
cb: (this: Context, val: T, index?: number, arr?: readonly T[]) => R,
context?: Context
): R[] {
// Take the same behavior with lodash when !arr and !cb,
// which might be some common sense.
if (!arr) {
return [];
}
if (!cb) {
return slice(arr) as unknown[] as R[];
}
if (arr.map && arr.map === nativeMap) {
return arr.map(cb, context);
}
else {
const result = [];
for (let i = 0, len = arr.length; i < len; i++) {
// FIXME: should the elided item be travelled, like `[33,,55]`.
result.push(cb.call(context, arr[i], i, arr));
}
return result;
}
}
export function reduce<T, S, Context>(
arr: readonly T[],
cb: (this: Context, previousValue: S, currentValue: T, currentIndex?: number, arr?: readonly T[]) => S,
memo?: S,
context?: Context
): S {
if (!(arr && cb)) {
return;
}
for (let i = 0, len = arr.length; i < len; i++) {
memo = cb.call(context, memo, arr[i], i, arr);
}
return memo;
}
/**
* Array filtering.
* @return Must be an array.
*/
export function filter<T, Context>(
arr: readonly T[],
cb: (this: Context, value: T, index: number, arr: readonly T[]) => boolean,
context?: Context
): T[] {
// Take the same behavior with lodash when !arr and !cb,
// which might be some common sense.
if (!arr) {
return [];
}
if (!cb) {
return slice(arr);
}
if (arr.filter && arr.filter === nativeFilter) {
return arr.filter(cb, context);
}
else {
const result = [];
for (let i = 0, len = arr.length; i < len; i++) {
// FIXME: should the elided items be travelled? like `[33,,55]`.
if (cb.call(context, arr[i], i, arr)) {
result.push(arr[i]);
}
}
return result;
}
}
/**
* 数组项查找
*/
export function find<T, Context>(
arr: readonly T[],
cb: (this: Context, value: T, index?: number, arr?: readonly T[]) => boolean,
context?: Context
): T {
if (!(arr && cb)) {
return;
}
for (let i = 0, len = arr.length; i < len; i++) {
if (cb.call(context, arr[i], i, arr)) {
return arr[i];
}
}
}
/**
* Get all object keys
*
* Will return an empty array if obj is null/undefined
*/
export function keys<T extends object>(obj: T): (KeyOfDistributive<T> & string)[] {
if (!obj) {
return [];
}
// Return type should be `keyof T` but exclude `number`, becuase
// `Object.keys` only return string rather than `number | string`.
type TKeys = KeyOfDistributive<T> & string;
if (Object.keys) {
return Object.keys(obj) as TKeys[];
}
let keyList: TKeys[] = [];
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
keyList.push(key as any);
}
}
return keyList;
}
// Remove this type in returned function. Or it will conflicts wicth callback with given context. Like Eventful.
// According to lib.es5.d.ts
/* eslint-disable max-len*/
export type Bind1<F, Ctx> = F extends (this: Ctx, ...args: infer A) => infer R ? (...args: A) => R : unknown;
export type Bind2<F, Ctx, T1> = F extends (this: Ctx, a: T1, ...args: infer A) => infer R ? (...args: A) => R : unknown;
export type Bind3<F, Ctx, T1, T2> = F extends (this: Ctx, a: T1, b: T2, ...args: infer A) => infer R ? (...args: A) => R : unknown;
export type Bind4<F, Ctx, T1, T2, T3> = F extends (this: Ctx, a: T1, b: T2, c: T3, ...args: infer A) => infer R ? (...args: A) => R : unknown;
export type Bind5<F, Ctx, T1, T2, T3, T4> = F extends (this: Ctx, a: T1, b: T2, c: T3, d: T4, ...args: infer A) => infer R ? (...args: A) => R : unknown;
type BindFunc<Ctx> = (this: Ctx, ...arg: any[]) => any
interface FunctionBind {
<F extends BindFunc<Ctx>, Ctx>(func: F, ctx: Ctx): Bind1<F, Ctx>
<F extends BindFunc<Ctx>, Ctx, T1 extends Parameters<F>[0]>(func: F, ctx: Ctx, a: T1): Bind2<F, Ctx, T1>
<F extends BindFunc<Ctx>, Ctx, T1 extends Parameters<F>[0], T2 extends Parameters<F>[1]>(func: F, ctx: Ctx, a: T1, b: T2): Bind3<F, Ctx, T1, T2>
<F extends BindFunc<Ctx>, Ctx, T1 extends Parameters<F>[0], T2 extends Parameters<F>[1], T3 extends Parameters<F>[2]>(func: F, ctx: Ctx, a: T1, b: T2, c: T3): Bind4<F, Ctx, T1, T2, T3>
<F extends BindFunc<Ctx>, Ctx, T1 extends Parameters<F>[0], T2 extends Parameters<F>[1], T3 extends Parameters<F>[2], T4 extends Parameters<F>[3]>(func: F, ctx: Ctx, a: T1, b: T2, c: T3, d: T4): Bind5<F, Ctx, T1, T2, T3, T4>
}
function bindPolyfill<Ctx, Fn extends(...args: any) => any>(
func: Fn, context: Ctx, ...args: any[]
): (...args: Parameters<Fn>) => ReturnType<Fn> {
return function (this: Ctx) {
return func.apply(context, args.concat(nativeSlice.call(arguments)));
};
}
export const bind: FunctionBind = (protoFunction && isFunction(protoFunction.bind))
? protoFunction.call.bind(protoFunction.bind)
: bindPolyfill;
export type Curry1<F, T1> = F extends (a: T1, ...args: infer A) => infer R ? (...args: A) => R : unknown;
export type Curry2<F, T1, T2> = F extends (a: T1, b: T2, ...args: infer A) => infer R ? (...args: A) => R : unknown;
export type Curry3<F, T1, T2, T3> = F extends (a: T1, b: T2, c: T3, ...args: infer A) => infer R ? (...args: A) => R : unknown;
export type Curry4<F, T1, T2, T3, T4> = F extends (a: T1, b: T2, c: T3, d: T4, ...args: infer A) => infer R ? (...args: A) => R : unknown;
type CurryFunc = (...arg: any[]) => any
function curry<F extends CurryFunc, T1 extends Parameters<F>[0]>(func: F, a: T1): Curry1<F, T1>
function curry<F extends CurryFunc, T1 extends Parameters<F>[0], T2 extends Parameters<F>[1]>(func: F, a: T1, b: T2): Curry2<F, T1, T2>
function curry<F extends CurryFunc, T1 extends Parameters<F>[0], T2 extends Parameters<F>[1], T3 extends Parameters<F>[2]>(func: F, a: T1, b: T2, c: T3): Curry3<F, T1, T2, T3>
function curry<F extends CurryFunc, T1 extends Parameters<F>[0], T2 extends Parameters<F>[1], T3 extends Parameters<F>[2], T4 extends Parameters<F>[3]>(func: F, a: T1, b: T2, c: T3, d: T4): Curry4<F, T1, T2, T3, T4>
function curry(func: Function, ...args: any[]): Function {
return function (this: any) {
return func.apply(this, args.concat(nativeSlice.call(arguments)));
};
}
export {curry};
/* eslint-enable max-len*/
export function isArray(value: any): value is any[] {
if (Array.isArray) {
return Array.isArray(value);
}
return objToString.call(value) === '[object Array]';
}
export function isFunction(value: any): value is Function {
return typeof value === 'function';
}
export function isString(value: any): value is string {
// Faster than `objToString.call` several times in chromium and webkit.
// And `new String()` is rarely used.
return typeof value === 'string';
}
export function isStringSafe(value: any): value is string {
return objToString.call(value) === '[object String]';
}
export function isNumber(value: any): value is number {
// Faster than `objToString.call` several times in chromium and webkit.
// And `new Number()` is rarely used.
return typeof value === 'number';
}
// Usage: `isObject(xxx)` or `isObject(SomeType)(xxx)`
// Generic T can be used to avoid "ts type gruards" casting the `value` from its original
// type `Object` implicitly so that loose its original type info in the subsequent code.
export function isObject<T = unknown>(value: T): value is (object & T) {
// Avoid a V8 JIT bug in Chrome 19-20.
// See https://code.google.com/p/v8/issues/detail?id=2291 for more details.
const type = typeof value;
return type === 'function' || (!!value && type === 'object');
}
export function isBuiltInObject(value: any): boolean {
return !!BUILTIN_OBJECT[objToString.call(value)];
}
export function isTypedArray(value: any): boolean {
return !!TYPED_ARRAY[objToString.call(value)];
}
export function isDom(value: any): value is HTMLElement {
return typeof value === 'object'
&& typeof value.nodeType === 'number'
&& typeof value.ownerDocument === 'object';
}
export function isGradientObject(value: any): value is GradientObject {
return (value as GradientObject).colorStops != null;
}
export function isImagePatternObject(value: any): value is ImagePatternObject {
return (value as ImagePatternObject).image != null;
}
export function isRegExp(value: unknown): value is RegExp {
return objToString.call(value) === '[object RegExp]';
}
/**
* Whether is exactly NaN. Notice isNaN('a') returns true.
*/
export function eqNaN(value: any): boolean {
/* eslint-disable-next-line no-self-compare */
return value !== value;
}
/**
* If value1 is not null, then return value1, otherwise judget rest of values.
* Low performance.
* @return Final value
*/
export function retrieve<T>(...args: T[]): T {
for (let i = 0, len = args.length; i < len; i++) {
if (args[i] != null) {
return args[i];
}
}
}
export function retrieve2<T, R>(value0: T, value1: R): T | R {
return value0 != null
? value0
: value1;
}
export function retrieve3<T, R, W>(value0: T, value1: R, value2: W): T | R | W {
return value0 != null
? value0
: value1 != null
? value1
: value2;
}
type SliceParams = Parameters<typeof nativeSlice>;
export function slice<T>(arr: ArrayLike<T>, ...args: SliceParams): T[] {
return nativeSlice.apply(arr, args as any[]);
}
/**
* Normalize css liked array configuration
* e.g.
* 3 => [3, 3, 3, 3]
* [4, 2] => [4, 2, 4, 2]
* [4, 3, 2] => [4, 3, 2, 3]
*/
export function normalizeCssArray(val: number | number[]) {
if (typeof (val) === 'number') {
return [val, val, val, val];
}
const len = val.length;
if (len === 2) {
// vertical | horizontal
return [val[0], val[1], val[0], val[1]];
}
else if (len === 3) {
// top | horizontal | bottom
return [val[0], val[1], val[2], val[1]];
}
return val;
}
export function assert(condition: any, message?: string) {
if (!condition) {
throw new Error(message);
}
}
/**
* @param str string to be trimmed
* @return trimmed string
*/
export function trim(str: string): string {
if (str == null) {
return null;
}
else if (typeof str.trim === 'function') {
return str.trim();
}
else {
return str.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '');
}
}
const primitiveKey = '__ec_primitive__';
/**
* Set an object as primitive to be ignored traversing children in clone or merge
*/
export function setAsPrimitive(obj: any) {
obj[primitiveKey] = true;
}
export function isPrimitive(obj: any): boolean {
return obj[primitiveKey];
}
interface MapInterface<T, KEY extends string | number = string | number> {
delete(key: KEY): boolean;
has(key: KEY): boolean;
get(key: KEY): T | undefined;
set(key: KEY, value: T): this;
keys(): KEY[];
forEach(callback: (value: T, key: KEY) => void): void;
}
class MapPolyfill<T, KEY extends string | number = string | number> implements MapInterface<T, KEY> {
private data: Record<KEY, T> = {} as Record<KEY, T>;
delete(key: KEY): boolean {
const existed = this.has(key);
if (existed) {
delete this.data[key];
}
return existed;
}
has(key: KEY): boolean {
return this.data.hasOwnProperty(key);
}
get(key: KEY): T | undefined {
return this.data[key];
}
set(key: KEY, value: T): this {
this.data[key] = value;
return this;
}
keys(): KEY[] {
return keys(this.data);
}
forEach(callback: (value: T, key: KEY) => void): void {
// This is a potential performance bottleneck, see details in
// https://github.com/ecomfe/zrender/issues/965, however it is now
// less likely to occur as we default to native maps when possible.
const data = this.data;
for (const key in data) {
if (data.hasOwnProperty(key)) {
callback(data[key], key);
}
}
}
}
// We want to use native Map if it is available, but we do not want to polyfill the global scope
// in case users ship their own polyfills or patch the native map object in any way.
const isNativeMapSupported = typeof Map === 'function';
function maybeNativeMap<T, KEY extends string | number = string | number>(): MapInterface<T, KEY> {
// Map may be a native class if we are running in an ES6 compatible environment.
// eslint-disable-next-line
return (isNativeMapSupported ? new Map<KEY, T>() : new MapPolyfill<T, KEY>()) as MapInterface<T, KEY>;
}
/**
* @constructor
* @param {Object} obj
*/
export class HashMap<T, KEY extends string | number = string | number> {
data: MapInterface<T, KEY>
constructor(obj?: HashMap<T, KEY> | { [key in KEY]?: T } | KEY[]) {
const isArr = isArray(obj);
// Key should not be set on this, otherwise
// methods get/set/... may be overridden.
this.data = maybeNativeMap<T, KEY>();
const thisMap = this;
(obj instanceof HashMap)
? obj.each(visit)
: (obj && each(obj, visit));
function visit(value: any, key: any) {
isArr ? thisMap.set(value, key) : thisMap.set(key, value);
}
}
// `hasKey` instead of `has` for potential misleading.
hasKey(key: KEY): boolean {
return this.data.has(key);
}
get(key: KEY): T {
return this.data.get(key);
}
set(key: KEY, value: T): T {
// Comparing with invocation chaining, `return value` is more commonly
// used in this case: `const someVal = map.set('a', genVal());`
this.data.set(key, value);
return value;
}
// Although util.each can be performed on this hashMap directly, user
// should not use the exposed keys, who are prefixed.
each<Context>(
cb: (this: Context, value?: T, key?: KEY) => void,
context?: Context
) {
this.data.forEach((value, key) => {
cb.call(context, value, key);
});
}
keys(): KEY[] {
const keys = this.data.keys();
return isNativeMapSupported
// Native map returns an iterator so we need to convert it to an array
? Array.from(keys)
: keys;
}
// Do not use this method if performance sensitive.
removeKey(key: KEY): void {
this.data.delete(key);
}
}
export function createHashMap<T, KEY extends string | number = string | number>(
obj?: HashMap<T, KEY> | { [key in KEY]?: T } | KEY[]
) {
return new HashMap<T, KEY>(obj);
}
export function concatArray<T, R>(a: ArrayLike<T>, b: ArrayLike<R>): ArrayLike<T | R> {
const newArray = new (a as any).constructor(a.length + b.length);
for (let i = 0; i < a.length; i++) {
newArray[i] = a[i];
}
const offset = a.length;
for (let i = 0; i < b.length; i++) {
newArray[i + offset] = b[i];
}
return newArray;
}
export function createObject<T>(proto?: object, properties?: T): T {
// Performance of Object.create
// https://jsperf.com/style-strategy-proto-or-others
let obj: T;
if (Object.create) {
obj = Object.create(proto);
}
else {
const StyleCtor = function () {};
StyleCtor.prototype = proto;
obj = new (StyleCtor as any)();
}
if (properties) {
extend(obj, properties);
}
return obj;
}
export function disableUserSelect(dom: HTMLElement) {
const domStyle = dom.style;
domStyle.webkitUserSelect = 'none';
domStyle.userSelect = 'none';
// @ts-ignore
domStyle.webkitTapHighlightColor = 'rgba(0,0,0,0)';
(domStyle as any)['-webkit-touch-callout'] = 'none';
}
export function hasOwn(own: object, prop: string): boolean {
return own.hasOwnProperty(prop);
}
export function noop() {}
export const RADIAN_TO_DEGREE = 180 / Math.PI;
+210
View File
@@ -0,0 +1,210 @@
/**
* @deprecated
* Use zrender.Point class instead
*/
import { MatrixArray } from './matrix';
/* global Float32Array */
// const ArrayCtor = typeof Float32Array === 'undefined'
// ? Array
// : Float32Array;
export type VectorArray = number[]
/**
* 创建一个向量
*/
export function create(x?: number, y?: number): VectorArray {
if (x == null) {
x = 0;
}
if (y == null) {
y = 0;
}
return [x, y];
}
/**
* 复制向量数据
*/
export function copy<T extends VectorArray>(out: T, v: VectorArray): T {
out[0] = v[0];
out[1] = v[1];
return out;
}
/**
* 克隆一个向量
*/
export function clone(v: VectorArray): VectorArray {
return [v[0], v[1]];
}
/**
* 设置向量的两个项
*/
export function set<T extends VectorArray>(out: T, a: number, b: number): T {
out[0] = a;
out[1] = b;
return out;
}
/**
* 向量相加
*/
export function add<T extends VectorArray>(out: T, v1: VectorArray, v2: VectorArray): T {
out[0] = v1[0] + v2[0];
out[1] = v1[1] + v2[1];
return out;
}
/**
* 向量缩放后相加
*/
export function scaleAndAdd<T extends VectorArray>(out: T, v1: VectorArray, v2: VectorArray, a: number): T {
out[0] = v1[0] + v2[0] * a;
out[1] = v1[1] + v2[1] * a;
return out;
}
/**
* 向量相减
*/
export function sub<T extends VectorArray>(out: T, v1: VectorArray, v2: VectorArray): T {
out[0] = v1[0] - v2[0];
out[1] = v1[1] - v2[1];
return out;
}
/**
* 向量长度
*/
export function len(v: VectorArray): number {
return Math.sqrt(lenSquare(v));
}
export const length = len;
/**
* 向量长度平方
*/
export function lenSquare(v: VectorArray): number {
return v[0] * v[0] + v[1] * v[1];
}
export const lengthSquare = lenSquare;
/**
* 向量乘法
*/
export function mul<T extends VectorArray>(out: T, v1: VectorArray, v2: VectorArray): T {
out[0] = v1[0] * v2[0];
out[1] = v1[1] * v2[1];
return out;
}
/**
* 向量除法
*/
export function div<T extends VectorArray>(out: T, v1: VectorArray, v2: VectorArray): T {
out[0] = v1[0] / v2[0];
out[1] = v1[1] / v2[1];
return out;
}
/**
* 向量点乘
*/
export function dot(v1: VectorArray, v2: VectorArray) {
return v1[0] * v2[0] + v1[1] * v2[1];
}
/**
* 向量缩放
*/
export function scale<T extends VectorArray>(out: T, v: VectorArray, s: number): T {
out[0] = v[0] * s;
out[1] = v[1] * s;
return out;
}
/**
* 向量归一化
*/
export function normalize<T extends VectorArray>(out: T, v: VectorArray): T {
const d = len(v);
if (d === 0) {
out[0] = 0;
out[1] = 0;
}
else {
out[0] = v[0] / d;
out[1] = v[1] / d;
}
return out;
}
/**
* 计算向量间距离
*/
export function distance(v1: VectorArray, v2: VectorArray): number {
return Math.sqrt(
(v1[0] - v2[0]) * (v1[0] - v2[0])
+ (v1[1] - v2[1]) * (v1[1] - v2[1])
);
}
export const dist = distance;
/**
* 向量距离平方
*/
export function distanceSquare(v1: VectorArray, v2: VectorArray): number {
return (v1[0] - v2[0]) * (v1[0] - v2[0])
+ (v1[1] - v2[1]) * (v1[1] - v2[1]);
}
export const distSquare = distanceSquare;
/**
* 求负向量
*/
export function negate<T extends VectorArray>(out: T, v: VectorArray): T {
out[0] = -v[0];
out[1] = -v[1];
return out;
}
/**
* 插值两个点
*/
export function lerp<T extends VectorArray>(out: T, v1: VectorArray, v2: VectorArray, t: number): T {
out[0] = v1[0] + t * (v2[0] - v1[0]);
out[1] = v1[1] + t * (v2[1] - v1[1]);
return out;
}
/**
* 矩阵左乘向量
*/
export function applyTransform<T extends VectorArray>(out: T, v: VectorArray, m: MatrixArray): T {
const x = v[0];
const y = v[1];
out[0] = m[0] * x + m[2] * y + m[4];
out[1] = m[1] * x + m[3] * y + m[5];
return out;
}
/**
* 求两个向量最小值
*/
export function min<T extends VectorArray>(out: T, v1: VectorArray, v2: VectorArray): T {
out[0] = Math.min(v1[0], v2[0]);
out[1] = Math.min(v1[1], v2[1]);
return out;
}
/**
* 求两个向量最大值
*/
export function max<T extends VectorArray>(out: T, v1: VectorArray, v2: VectorArray): T {
out[0] = Math.max(v1[0], v2[0]);
out[1] = Math.max(v1[1], v2[1]);
return out;
}
+123
View File
@@ -0,0 +1,123 @@
import type { ZRenderType } from '../zrender';
import type CanvasPainter from '../canvas/Painter';
import type BoundingRect from '../core/BoundingRect';
import { extend } from '../core/util';
class DebugRect {
dom: HTMLDivElement
private _hideTimeout: number
constructor(style: Opts['style']) {
const dom = this.dom = document.createElement('div');
dom.className = 'ec-debug-dirty-rect';
style = extend({}, style);
extend(style, {
backgroundColor: 'rgba(0, 0, 255, 0.2)',
border: '1px solid #00f'
});
dom.style.cssText = `
position: absolute;
opacity: 0;
transition: opacity 0.5s linear;
pointer-events: none;
`;
for (let key in style) {
if (style.hasOwnProperty(key)) {
(dom.style as any)[key] = (style as any)[key];
}
}
}
update(rect: BoundingRect) {
const domStyle = this.dom.style;
domStyle.width = rect.width + 'px';
domStyle.height = rect.height + 'px';
domStyle.left = rect.x + 'px';
domStyle.top = rect.y + 'px';
}
hide() {
this.dom.style.opacity = '0';
}
show(autoHideDelay?: number) {
clearTimeout(this._hideTimeout);
this.dom.style.opacity = '1';
// Auto hide after 2 second
this._hideTimeout = setTimeout(() => {
this.hide();
}, autoHideDelay || 1000) as unknown as number;
}
}
interface Opts {
style?: {
backgroundColor?: string
color?: string
}
autoHideDelay?: number
}
export default function showDebugDirtyRect(zr: ZRenderType, opts?: Opts) {
opts = opts || {};
const painter = zr.painter as CanvasPainter;
if (!painter.getLayers) {
throw new Error('Debug dirty rect can only been used on canvas renderer.');
}
if (painter.isSingleCanvas()) {
throw new Error('Debug dirty rect can only been used on zrender inited with container.');
}
const debugViewRoot = document.createElement('div');
debugViewRoot.style.cssText = `
position:absolute;
left:0;
top:0;
right:0;
bottom:0;
pointer-events:none;
`;
debugViewRoot.className = 'ec-debug-dirty-rect-container';
const debugRects: DebugRect[] = [];
const dom = zr.dom;
dom.appendChild(debugViewRoot);
const computedStyle = getComputedStyle(dom);
if (computedStyle.position === 'static') {
dom.style.position = 'relative';
}
zr.on('rendered', function () {
if (painter.getLayers) {
let idx = 0;
painter.eachBuiltinLayer((layer) => {
if (!layer.debugGetPaintRects) {
return;
}
const paintRects = layer.debugGetPaintRects();
for (let i = 0; i < paintRects.length; i++) {
if (!paintRects[i].width || !paintRects[i].height) {
continue;
}
if (!debugRects[idx]) {
debugRects[idx] = new DebugRect(opts.style);
debugViewRoot.appendChild(debugRects[idx].dom);
}
debugRects[idx].show(opts.autoHideDelay);
debugRects[idx].update(paintRects[i]);
idx++;
}
});
for (let i = idx; i < debugRects.length; i++) {
debugRects[i].hide();
}
}
});
}
+634
View File
@@ -0,0 +1,634 @@
/* global document */
import {
addEventListener,
removeEventListener,
normalizeEvent,
getNativeEvent
} from '../core/event';
import * as zrUtil from '../core/util';
import Eventful from '../core/Eventful';
import env from '../core/env';
import { Dictionary, ZRRawEvent, ZRRawMouseEvent } from '../core/types';
import { VectorArray } from '../core/vector';
import Handler from '../Handler';
type DomHandlersMap = Dictionary<(this: HandlerDomProxy, event: ZRRawEvent) => void>
type DomExtended = Node & {
domBelongToZr: boolean
}
const TOUCH_CLICK_DELAY = 300;
const globalEventSupported = env.domSupported;
const localNativeListenerNames = (function () {
const mouseHandlerNames = [
'click', 'dblclick', 'mousewheel', 'wheel', 'mouseout',
'mouseup', 'mousedown', 'mousemove', 'contextmenu'
];
const touchHandlerNames = [
'touchstart', 'touchend', 'touchmove'
];
const pointerEventNameMap = {
pointerdown: 1, pointerup: 1, pointermove: 1, pointerout: 1
};
const pointerHandlerNames = zrUtil.map(mouseHandlerNames, function (name) {
const nm = name.replace('mouse', 'pointer');
return pointerEventNameMap.hasOwnProperty(nm) ? nm : name;
});
return {
mouse: mouseHandlerNames,
touch: touchHandlerNames,
pointer: pointerHandlerNames
};
})();
const globalNativeListenerNames = {
mouse: ['mousemove', 'mouseup'],
pointer: ['pointermove', 'pointerup']
};
let wheelEventSupported = false;
// Although firfox has 'DOMMouseScroll' event and do not has 'mousewheel' event,
// the 'DOMMouseScroll' event do not performe the same behavior on touch pad device
// (like on Mac) ('DOMMouseScroll' will be triggered only if a big wheel delta).
// So we should not use it.
// function eventNameFix(name: string) {
// return (name === 'mousewheel' && env.browser.firefox) ? 'DOMMouseScroll' : name;
// }
function isPointerFromTouch(event: ZRRawEvent) {
const pointerType = (event as any).pointerType;
return pointerType === 'pen' || pointerType === 'touch';
}
// function useMSGuesture(handlerProxy, event) {
// return isPointerFromTouch(event) && !!handlerProxy._msGesture;
// }
// function onMSGestureChange(proxy, event) {
// if (event.translationX || event.translationY) {
// // mousemove is carried by MSGesture to reduce the sensitivity.
// proxy.handler.dispatchToElement(event.target, 'mousemove', event);
// }
// if (event.scale !== 1) {
// event.pinchX = event.offsetX;
// event.pinchY = event.offsetY;
// event.pinchScale = event.scale;
// proxy.handler.dispatchToElement(event.target, 'pinch', event);
// }
// }
/**
* Prevent mouse event from being dispatched after Touch Events action
* @see <https://github.com/deltakosh/handjs/blob/master/src/hand.base.js>
* 1. Mobile browsers dispatch mouse events 300ms after touchend.
* 2. Chrome for Android dispatch mousedown for long-touch about 650ms
* Result: Blocking Mouse Events for 700ms.
*
* @param {DOMHandlerScope} scope
*/
function setTouchTimer(scope: DOMHandlerScope) {
scope.touching = true;
if (scope.touchTimer != null) {
clearTimeout(scope.touchTimer);
scope.touchTimer = null;
}
scope.touchTimer = setTimeout(function () {
scope.touching = false;
scope.touchTimer = null;
}, 700);
}
// Mark touch, which is useful in distinguish touch and
// mouse event in upper applicatoin.
function markTouch(event: ZRRawEvent) {
event && (event.zrByTouch = true);
}
// function markTriggeredFromLocal(event) {
// event && (event.__zrIsFromLocal = true);
// }
// function isTriggeredFromLocal(instance, event) {
// return !!(event && event.__zrIsFromLocal);
// }
function normalizeGlobalEvent(instance: HandlerDomProxy, event: ZRRawEvent) {
// offsetX, offsetY still need to be calculated. They are necessary in the event
// handlers of the upper applications. Set `true` to force calculate them.
return normalizeEvent(
instance.dom,
// TODO ANY TYPE
new FakeGlobalEvent(instance, event) as any as ZRRawEvent,
true
);
}
/**
* Detect whether the given el is in `painterRoot`.
*/
function isLocalEl(instance: HandlerDomProxy, el: Node) {
let elTmp = el;
let isLocal = false;
while (elTmp && elTmp.nodeType !== 9
&& !(
isLocal = (elTmp as DomExtended).domBelongToZr
|| (elTmp !== el && elTmp === instance.painterRoot)
)
) {
elTmp = elTmp.parentNode;
}
return isLocal;
}
/**
* Make a fake event but not change the original event,
* because the global event probably be used by other
* listeners not belonging to zrender.
* @class
*/
class FakeGlobalEvent {
type: string
target: HTMLElement
currentTarget: HTMLElement
pointerType: string
clientX: number
clientY: number
constructor(instance: HandlerDomProxy, event: ZRRawEvent) {
this.type = event.type;
this.target = this.currentTarget = instance.dom;
this.pointerType = (event as any).pointerType;
// Necessray for the force calculation of zrX, zrY
this.clientX = (event as ZRRawMouseEvent).clientX;
this.clientY = (event as ZRRawMouseEvent).clientY;
// Because we do not mount global listeners to touch events,
// we do not copy `targetTouches` and `changedTouches` here.
}
// we make the default methods on the event do nothing,
// otherwise it is dangerous. See more details in
// [DRAG_OUTSIDE] in `Handler.js`.
stopPropagation = zrUtil.noop
stopImmediatePropagation = zrUtil.noop
preventDefault = zrUtil.noop
}
/**
* Local DOM Handlers
* @this {HandlerProxy}
*/
const localDOMHandlers: DomHandlersMap = {
mousedown(event: ZRRawEvent) {
event = normalizeEvent(this.dom, event);
this.__mayPointerCapture = [event.zrX, event.zrY];
this.trigger('mousedown', event);
},
mousemove(event: ZRRawEvent) {
event = normalizeEvent(this.dom, event);
const downPoint = this.__mayPointerCapture;
if (downPoint && (event.zrX !== downPoint[0] || event.zrY !== downPoint[1])) {
this.__togglePointerCapture(true);
}
this.trigger('mousemove', event);
},
mouseup(event: ZRRawEvent) {
event = normalizeEvent(this.dom, event);
this.__togglePointerCapture(false);
this.trigger('mouseup', event);
},
mouseout(event: ZRRawEvent) {
event = normalizeEvent(this.dom, event);
// There might be some doms created by upper layer application
// at the same level of painter.getViewportRoot() (e.g., tooltip
// dom created by echarts), where 'globalout' event should not
// be triggered when mouse enters these doms. (But 'mouseout'
// should be triggered at the original hovered element as usual).
const element = (event as any).toElement || (event as ZRRawMouseEvent).relatedTarget;
// For SVG rendering, there are SVG elements inside `this.dom`.
// (especially in decal case). Should not to handle those "mouseout"..
if (!isLocalEl(this, element)) {
// Similarly to the browser did on `document` and touch event,
// `globalout` will be delayed to final pointer cature release.
if (this.__pointerCapturing) {
event.zrEventControl = 'no_globalout';
}
this.trigger('mouseout', event);
}
},
wheel(event: ZRRawEvent) {
// Morden agent has supported event `wheel` instead of `mousewheel`.
// About the polyfill of the props "delta", see "arc/core/event.ts".
// Firefox only support `wheel` rather than `mousewheel`. Although firfox has been supporting
// event `DOMMouseScroll`, it do not act the same behavior as `wheel` on touch pad device
// like on Mac, where `DOMMouseScroll` will be triggered only if a big wheel delta occurs,
// and it results in no chance to "preventDefault". So we should not use `DOMMouseScroll`.
wheelEventSupported = true;
event = normalizeEvent(this.dom, event);
// Follow the definition of the previous version, the zrender event name is still 'mousewheel'.
this.trigger('mousewheel', event);
},
mousewheel(event: ZRRawEvent) {
// IE8- and some other lagacy agent do not support event `wheel`, so we still listen
// to the legacy event `mouseevent`.
// Typically if event `wheel` is supported and the handler has been mounted on a
// DOM element, the legacy `mousewheel` event will not be triggered (Chrome and Safari).
// But we still do this guard to avoid to duplicated handle.
if (wheelEventSupported) {
return;
}
event = normalizeEvent(this.dom, event);
this.trigger('mousewheel', event);
},
touchstart(event: ZRRawEvent) {
// Default mouse behaviour should not be disabled here.
// For example, page may needs to be slided.
event = normalizeEvent(this.dom, event);
markTouch(event);
this.__lastTouchMoment = new Date();
this.handler.processGesture(event, 'start');
// For consistent event listener for both touch device and mouse device,
// we simulate "mouseover-->mousedown" in touch device. So we trigger
// `mousemove` here (to trigger `mouseover` inside), and then trigger
// `mousedown`.
localDOMHandlers.mousemove.call(this, event);
localDOMHandlers.mousedown.call(this, event);
},
touchmove(event: ZRRawEvent) {
event = normalizeEvent(this.dom, event);
markTouch(event);
this.handler.processGesture(event, 'change');
// Mouse move should always be triggered no matter whether
// there is gestrue event, because mouse move and pinch may
// be used at the same time.
localDOMHandlers.mousemove.call(this, event);
},
touchend(event: ZRRawEvent) {
event = normalizeEvent(this.dom, event);
markTouch(event);
this.handler.processGesture(event, 'end');
localDOMHandlers.mouseup.call(this, event);
// Do not trigger `mouseout` here, in spite of `mousemove`(`mouseover`) is
// triggered in `touchstart`. This seems to be illogical, but by this mechanism,
// we can conveniently implement "hover style" in both PC and touch device just
// by listening to `mouseover` to add "hover style" and listening to `mouseout`
// to remove "hover style" on an element, without any additional code for
// compatibility. (`mouseout` will not be triggered in `touchend`, so "hover
// style" will remain for user view)
// click event should always be triggered no matter whether
// there is gestrue event. System click can not be prevented.
if (+new Date() - (+this.__lastTouchMoment) < TOUCH_CLICK_DELAY) {
localDOMHandlers.click.call(this, event);
}
},
pointerdown(event: ZRRawEvent) {
localDOMHandlers.mousedown.call(this, event);
// if (useMSGuesture(this, event)) {
// this._msGesture.addPointer(event.pointerId);
// }
},
pointermove(event: ZRRawEvent) {
// FIXME
// pointermove is so sensitive that it always triggered when
// tap(click) on touch screen, which affect some judgement in
// upper application. So, we don't support mousemove on MS touch
// device yet.
if (!isPointerFromTouch(event)) {
localDOMHandlers.mousemove.call(this, event);
}
},
pointerup(event: ZRRawEvent) {
localDOMHandlers.mouseup.call(this, event);
},
pointerout(event: ZRRawEvent) {
// pointerout will be triggered when tap on touch screen
// (IE11+/Edge on MS Surface) after click event triggered,
// which is inconsistent with the mousout behavior we defined
// in touchend. So we unify them.
// (check localDOMHandlers.touchend for detailed explanation)
if (!isPointerFromTouch(event)) {
localDOMHandlers.mouseout.call(this, event);
}
}
};
/**
* Othere DOM UI Event handlers for zr dom.
* @this {HandlerProxy}
*/
zrUtil.each(['click', 'dblclick', 'contextmenu'], function (name) {
localDOMHandlers[name] = function (event) {
event = normalizeEvent(this.dom, event);
this.trigger(name, event);
};
});
/**
* DOM UI Event handlers for global page.
*
* [Caution]:
* those handlers should both support in capture phase and bubble phase!
*/
const globalDOMHandlers: DomHandlersMap = {
pointermove: function (event: ZRRawEvent) {
// FIXME
// pointermove is so sensitive that it always triggered when
// tap(click) on touch screen, which affect some judgement in
// upper application. So, we don't support mousemove on MS touch
// device yet.
if (!isPointerFromTouch(event)) {
globalDOMHandlers.mousemove.call(this, event);
}
},
pointerup: function (event: ZRRawEvent) {
globalDOMHandlers.mouseup.call(this, event);
},
mousemove: function (event: ZRRawEvent) {
this.trigger('mousemove', event);
},
mouseup: function (event: ZRRawEvent) {
const pointerCaptureReleasing = this.__pointerCapturing;
this.__togglePointerCapture(false);
this.trigger('mouseup', event);
if (pointerCaptureReleasing) {
event.zrEventControl = 'only_globalout';
this.trigger('mouseout', event);
}
}
};
function mountLocalDOMEventListeners(instance: HandlerDomProxy, scope: DOMHandlerScope) {
const domHandlers = scope.domHandlers;
if (env.pointerEventsSupported) { // Only IE11+/Edge
// 1. On devices that both enable touch and mouse (e.g., MS Surface and lenovo X240),
// IE11+/Edge do not trigger touch event, but trigger pointer event and mouse event
// at the same time.
// 2. On MS Surface, it probablely only trigger mousedown but no mouseup when tap on
// screen, which do not occurs in pointer event.
// So we use pointer event to both detect touch gesture and mouse behavior.
zrUtil.each(localNativeListenerNames.pointer, function (nativeEventName) {
mountSingleDOMEventListener(scope, nativeEventName, function (event) {
// markTriggeredFromLocal(event);
domHandlers[nativeEventName].call(instance, event);
});
});
// FIXME
// Note: MS Gesture require CSS touch-action set. But touch-action is not reliable,
// which does not prevent defuault behavior occasionally (which may cause view port
// zoomed in but use can not zoom it back). And event.preventDefault() does not work.
// So we have to not to use MSGesture and not to support touchmove and pinch on MS
// touch screen. And we only support click behavior on MS touch screen now.
// MS Gesture Event is only supported on IE11+/Edge and on Windows 8+.
// We don't support touch on IE on win7.
// See <https://msdn.microsoft.com/en-us/library/dn433243(v=vs.85).aspx>
// if (typeof MSGesture === 'function') {
// (this._msGesture = new MSGesture()).target = dom; // jshint ignore:line
// dom.addEventListener('MSGestureChange', onMSGestureChange);
// }
}
else {
if (env.touchEventsSupported) {
zrUtil.each(localNativeListenerNames.touch, function (nativeEventName) {
mountSingleDOMEventListener(scope, nativeEventName, function (event) {
// markTriggeredFromLocal(event);
domHandlers[nativeEventName].call(instance, event);
setTouchTimer(scope);
});
});
// Handler of 'mouseout' event is needed in touch mode, which will be mounted below.
// addEventListener(root, 'mouseout', this._mouseoutHandler);
}
// 1. Considering some devices that both enable touch and mouse event (like on MS Surface
// and lenovo X240, @see #2350), we make mouse event be always listened, otherwise
// mouse event can not be handle in those devices.
// 2. On MS Surface, Chrome will trigger both touch event and mouse event. How to prevent
// mouseevent after touch event triggered, see `setTouchTimer`.
zrUtil.each(localNativeListenerNames.mouse, function (nativeEventName) {
mountSingleDOMEventListener(scope, nativeEventName, function (event: ZRRawEvent) {
event = getNativeEvent(event);
if (!scope.touching) {
// markTriggeredFromLocal(event);
domHandlers[nativeEventName].call(instance, event);
}
});
});
}
}
function mountGlobalDOMEventListeners(instance: HandlerDomProxy, scope: DOMHandlerScope) {
// Only IE11+/Edge. See the comment in `mountLocalDOMEventListeners`.
if (env.pointerEventsSupported) {
zrUtil.each(globalNativeListenerNames.pointer, mount);
}
// Touch event has implemented "drag outside" so we do not mount global listener for touch event.
// (see https://www.w3.org/TR/touch-events/#the-touchmove-event) (see also `DRAG_OUTSIDE`).
// We do not consider "both-support-touch-and-mouse device" for this feature (see the comment of
// `mountLocalDOMEventListeners`) to avoid bugs util some requirements come.
else if (!env.touchEventsSupported) {
zrUtil.each(globalNativeListenerNames.mouse, mount);
}
function mount(nativeEventName: string) {
function nativeEventListener(event: ZRRawEvent) {
event = getNativeEvent(event);
// See the reason in [DRAG_OUTSIDE] in `Handler.js`
// This checking supports both `useCapture` or not.
// PENDING: if there is performance issue in some devices,
// we probably can not use `useCapture` and change a easier
// to judes whether local (mark).
if (!isLocalEl(instance, event.target as Node)) {
event = normalizeGlobalEvent(instance, event);
scope.domHandlers[nativeEventName].call(instance, event);
}
}
mountSingleDOMEventListener(
scope, nativeEventName, nativeEventListener,
{capture: true} // See [DRAG_OUTSIDE] in `Handler.js`
);
}
}
function mountSingleDOMEventListener(
scope: DOMHandlerScope,
nativeEventName: string,
listener: EventListener,
opt?: boolean | AddEventListenerOptions
) {
scope.mounted[nativeEventName] = listener;
scope.listenerOpts[nativeEventName] = opt;
addEventListener(scope.domTarget, nativeEventName, listener, opt);
}
function unmountDOMEventListeners(scope: DOMHandlerScope) {
const mounted = scope.mounted;
for (let nativeEventName in mounted) {
if (mounted.hasOwnProperty(nativeEventName)) {
removeEventListener(
scope.domTarget, nativeEventName, mounted[nativeEventName],
scope.listenerOpts[nativeEventName]
);
}
}
scope.mounted = {};
}
class DOMHandlerScope {
domTarget: HTMLElement | HTMLDocument
domHandlers: DomHandlersMap
// Key: eventName, value: mounted handler functions.
// Used for unmount.
mounted: Dictionary<EventListener> = {};
listenerOpts: Dictionary<boolean | AddEventListenerOptions> = {};
touchTimer: ReturnType<typeof setTimeout>;
touching = false;
constructor(
domTarget: HTMLElement | HTMLDocument,
domHandlers: DomHandlersMap
) {
this.domTarget = domTarget;
this.domHandlers = domHandlers;
}
}
export default class HandlerDomProxy extends Eventful {
dom: HTMLElement
painterRoot: HTMLElement
handler: Handler
private _localHandlerScope: DOMHandlerScope
private _globalHandlerScope: DOMHandlerScope
__lastTouchMoment: Date
// See [DRAG_OUTSIDE] in `Handler.ts`.
__pointerCapturing = false
// [x, y]
__mayPointerCapture: VectorArray
constructor(dom: HTMLElement, painterRoot: HTMLElement) {
super();
this.dom = dom;
this.painterRoot = painterRoot;
this._localHandlerScope = new DOMHandlerScope(dom, localDOMHandlers);
if (globalEventSupported) {
this._globalHandlerScope = new DOMHandlerScope(document, globalDOMHandlers);
}
mountLocalDOMEventListeners(this, this._localHandlerScope);
}
dispose() {
unmountDOMEventListeners(this._localHandlerScope);
if (globalEventSupported) {
unmountDOMEventListeners(this._globalHandlerScope);
}
}
setCursor(cursorStyle: string) {
this.dom.style && (this.dom.style.cursor = cursorStyle || 'default');
}
/**
* See [DRAG_OUTSIDE] in `Handler.js`.
* @implement
* @param isPointerCapturing Should never be `null`/`undefined`.
* `true`: start to capture pointer if it is not capturing.
* `false`: end the capture if it is capturing.
*/
__togglePointerCapture(isPointerCapturing?: boolean) {
this.__mayPointerCapture = null;
if (globalEventSupported
&& ((+this.__pointerCapturing) ^ (+isPointerCapturing))
) {
this.__pointerCapturing = isPointerCapturing;
const globalHandlerScope = this._globalHandlerScope;
isPointerCapturing
? mountGlobalDOMEventListeners(this, globalHandlerScope)
: unmountDOMEventListeners(globalHandlerScope);
}
}
}
export interface HandlerProxyInterface extends Eventful {
handler: Handler
dispose: () => void
setCursor: (cursorStyle?: string) => void
}
+76
View File
@@ -0,0 +1,76 @@
/**
* Do not mount those modules on 'src/zrender' for better tree shaking.
*/
import * as zrUtil from './core/util';
import * as matrix from './core/matrix';
import * as vector from './core/vector';
import * as colorTool from './tool/color';
import * as pathTool from './tool/path';
import {parseSVG} from './tool/parseSVG';
import * as morphPathTool from './tool/morphPath';
export {default as Point, PointLike} from './core/Point';
export {
default as Element,
ElementAnimateConfig,
ElementTextConfig,
ElementTextGuideLineConfig,
ElementEvent,
ElementEventCallback,
ElementProps
} from './Element';
export {default as Displayable, DisplayableProps} from './graphic/Displayable';
export {default as Group, GroupProps} from './graphic/Group';
export {default as Path, PathStyleProps, PathProps, PathStatePropNames, PathState} from './graphic/Path';
export {default as Image, ImageStyleProps, ImageProps, ImageState} from './graphic/Image';
export {default as CompoundPath, CompoundPathShape} from './graphic/CompoundPath';
export {default as TSpan, TSpanStyleProps, TSpanProps, TSpanState} from './graphic/TSpan';
export {default as IncrementalDisplayable} from './graphic/IncrementalDisplayable';
export {default as Text, TextStylePropsPart, TextStyleProps, TextProps, TextState} from './graphic/Text';
export {default as Arc, ArcProps, ArcShape} from './graphic/shape/Arc';
export {default as BezierCurve, BezierCurveProps, BezierCurveShape} from './graphic/shape/BezierCurve';
export {default as Circle, CircleProps, CircleShape} from './graphic/shape/Circle';
export {default as Droplet, DropletProps, DropletShape} from './graphic/shape/Droplet';
export {default as Ellipse, EllipseProps, EllipseShape} from './graphic/shape/Ellipse';
export {default as Heart, HeartProps, HeartShape} from './graphic/shape/Heart';
export {default as Isogon, IsogonProps, IsogonShape} from './graphic/shape/Isogon';
export {default as Line, LineProps, LineShape} from './graphic/shape/Line';
export {default as Polygon, PolygonProps, PolygonShape} from './graphic/shape/Polygon';
export {default as Polyline, PolylineProps, PolylineShape} from './graphic/shape/Polyline';
export {default as Rect, RectProps, RectShape} from './graphic/shape/Rect';
export {default as Ring, RingProps, RingShape} from './graphic/shape/Ring';
export {default as Rose, RoseProps, RoseShape} from './graphic/shape/Rose';
export {default as Sector, SectorProps, SectorShape} from './graphic/shape/Sector';
export {default as Star, StarProps, StarShape} from './graphic/shape/Star';
export {default as Trochoid, TrochoidProps, TrochoidShape} from './graphic/shape/Trochoid';
export {default as LinearGradient, LinearGradientObject} from './graphic/LinearGradient';
export {default as RadialGradient, RadialGradientObject} from './graphic/RadialGradient';
export {
default as Pattern,
PatternObjectBase,
PatternObject,
ImagePatternObject,
SVGPatternObject
} from './graphic/Pattern';
export {default as BoundingRect, RectLike} from './core/BoundingRect';
export {default as OrientedBoundingRect} from './core/OrientedBoundingRect';
export {matrix};
export {vector};
export {colorTool as color};
export {pathTool as path};
export {zrUtil as util};
export {morphPathTool as morph};
export {parseSVG};
export {default as showDebugDirtyRect} from './debug/showDebugDirtyRect';
export {setPlatformAPI} from './core/platform';
+5
View File
@@ -0,0 +1,5 @@
declare namespace NodeJS {
interface ProcessEnv {
NODE_ENV: 'production' | 'development'
}
}
+59
View File
@@ -0,0 +1,59 @@
// CompoundPath to improve performance
import Path from './Path';
import PathProxy from '../core/PathProxy';
export interface CompoundPathShape {
paths: Path[]
}
export default class CompoundPath extends Path {
type = 'compound'
shape: CompoundPathShape
private _updatePathDirty() {
const paths = this.shape.paths;
let dirtyPath = this.shapeChanged();
for (let i = 0; i < paths.length; i++) {
// Mark as dirty if any subpath is dirty
dirtyPath = dirtyPath || paths[i].shapeChanged();
}
if (dirtyPath) {
this.dirtyShape();
}
}
beforeBrush() {
this._updatePathDirty();
const paths = this.shape.paths || [];
const scale = this.getGlobalScale();
// Update path scale
for (let i = 0; i < paths.length; i++) {
if (!paths[i].path) {
paths[i].createPathProxy();
}
paths[i].path.setScale(scale[0], scale[1], paths[i].segmentIgnoreThreshold);
}
}
buildPath(ctx: PathProxy | CanvasRenderingContext2D, shape: CompoundPathShape) {
const paths = shape.paths || [];
for (let i = 0; i < paths.length; i++) {
paths[i].buildPath(ctx, paths[i].shape, true);
}
}
afterBrush() {
const paths = this.shape.paths || [];
for (let i = 0; i < paths.length; i++) {
paths[i].pathUpdated();
}
}
getBoundingRect() {
this._updatePathDirty.call(this);
return Path.prototype.getBoundingRect.call(this);
}
}
+624
View File
@@ -0,0 +1,624 @@
/**
* 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;
+44
View File
@@ -0,0 +1,44 @@
// TODO Should GradientObject been LinearGradientObject | RadialGradientObject
export interface GradientObject {
id?: number
type: string
colorStops: GradientColorStop[]
global?: boolean
}
export interface InnerGradientObject extends GradientObject {
__canvasGradient: CanvasGradient
__width: number
__height: number
}
export interface GradientColorStop {
offset: number
color: string
}
export default class Gradient {
id?: number
type: string
colorStops: GradientColorStop[]
global: boolean
constructor(colorStops: GradientColorStop[]) {
this.colorStops = colorStops || [];
}
addColorStop(offset: number, color: string) {
this.colorStops.push({
offset,
color
});
}
}
+300
View File
@@ -0,0 +1,300 @@
/**
* Group是一个容器,可以插入子节点,Group的变换也会被应用到子节点上
* @module zrender/graphic/Group
* @example
* const Group = require('zrender/graphic/Group');
* const Circle = require('zrender/graphic/shape/Circle');
* const g = new Group();
* g.position[0] = 100;
* g.position[1] = 100;
* g.add(new Circle({
* style: {
* x: 100,
* y: 100,
* r: 20,
* }
* }));
* zr.add(g);
*/
import * as zrUtil from '../core/util';
import Element, { ElementProps } from '../Element';
import BoundingRect from '../core/BoundingRect';
import { MatrixArray } from '../core/matrix';
import Displayable from './Displayable';
import { ZRenderType } from '../zrender';
export interface GroupProps extends ElementProps {
}
class Group extends Element<GroupProps> {
readonly isGroup = true
private _children: Element[] = []
constructor(opts?: GroupProps) {
super();
this.attr(opts);
}
/**
* Get children reference.
*/
childrenRef() {
return this._children;
}
/**
* Get children copy.
*/
children() {
return this._children.slice();
}
/**
* 获取指定 index 的儿子节点
*/
childAt(idx: number): Element {
return this._children[idx];
}
/**
* 获取指定名字的儿子节点
*/
childOfName(name: string): Element {
const children = this._children;
for (let i = 0; i < children.length; i++) {
if (children[i].name === name) {
return children[i];
}
}
}
childCount(): number {
return this._children.length;
}
/**
* 添加子节点到最后
*/
add(child: Element): Group {
if (child) {
if (child !== this && child.parent !== this) {
this._children.push(child);
this._doAdd(child);
}
if (process.env.NODE_ENV !== 'production') {
if (child.__hostTarget) {
throw 'This elemenet has been used as an attachment';
}
}
}
return this;
}
/**
* 添加子节点在 nextSibling 之前
*/
addBefore(child: Element, nextSibling: Element) {
if (child && child !== this && child.parent !== this
&& nextSibling && nextSibling.parent === this) {
const children = this._children;
const idx = children.indexOf(nextSibling);
if (idx >= 0) {
children.splice(idx, 0, child);
this._doAdd(child);
}
}
return this;
}
replace(oldChild: Element, newChild: Element) {
const idx = zrUtil.indexOf(this._children, oldChild);
if (idx >= 0) {
this.replaceAt(newChild, idx);
}
return this;
}
replaceAt(child: Element, index: number) {
const children = this._children;
const old = children[index];
if (child && child !== this && child.parent !== this && child !== old) {
children[index] = child;
old.parent = null;
const zr = this.__zr;
if (zr) {
old.removeSelfFromZr(zr);
}
this._doAdd(child);
}
return this;
}
_doAdd(child: Element) {
if (child.parent) {
// Parent must be a group
(child.parent as Group).remove(child);
}
child.parent = this;
const zr = this.__zr;
if (zr && zr !== (child as Group).__zr) { // Only group has __storage
child.addSelfToZr(zr);
}
zr && zr.refresh();
}
/**
* Remove child
* @param child
*/
remove(child: Element) {
const zr = this.__zr;
const children = this._children;
const idx = zrUtil.indexOf(children, child);
if (idx < 0) {
return this;
}
children.splice(idx, 1);
child.parent = null;
if (zr) {
child.removeSelfFromZr(zr);
}
zr && zr.refresh();
return this;
}
/**
* Remove all children
*/
removeAll() {
const children = this._children;
const zr = this.__zr;
for (let i = 0; i < children.length; i++) {
const child = children[i];
if (zr) {
child.removeSelfFromZr(zr);
}
child.parent = null;
}
children.length = 0;
return this;
}
/**
* 遍历所有子节点
*/
eachChild<Context>(
cb: (this: Context, el: Element, index?: number) => void,
context?: Context
) {
const children = this._children;
for (let i = 0; i < children.length; i++) {
const child = children[i];
cb.call(context, child, i);
}
return this;
}
/**
* Visit all descendants.
* Return false in callback to stop visit descendants of current node
*/
// TODO Group itself should also invoke the callback.
traverse<T>(
cb: (this: T, el: Element) => boolean | void,
context?: T
) {
for (let i = 0; i < this._children.length; i++) {
const child = this._children[i];
const stopped = cb.call(context, child);
if (child.isGroup && !stopped) {
child.traverse(cb, context);
}
}
return this;
}
addSelfToZr(zr: ZRenderType) {
super.addSelfToZr(zr);
for (let i = 0; i < this._children.length; i++) {
const child = this._children[i];
child.addSelfToZr(zr);
}
}
removeSelfFromZr(zr: ZRenderType) {
super.removeSelfFromZr(zr);
for (let i = 0; i < this._children.length; i++) {
const child = this._children[i];
child.removeSelfFromZr(zr);
}
}
getBoundingRect(includeChildren?: Element[]): BoundingRect {
// TODO Caching
const tmpRect = new BoundingRect(0, 0, 0, 0);
const children = includeChildren || this._children;
const tmpMat: MatrixArray = [];
let rect = null;
for (let i = 0; i < children.length; i++) {
const child = children[i];
// TODO invisible?
if (child.ignore || (child as Displayable).invisible) {
continue;
}
const childRect = child.getBoundingRect();
const transform = child.getLocalTransform(tmpMat);
// TODO
// The boundingRect cacluated by transforming original
// rect may be bigger than the actual bundingRect when rotation
// is used. (Consider a circle rotated aginst its center, where
// the actual boundingRect should be the same as that not be
// rotated.) But we can not find better approach to calculate
// actual boundingRect yet, considering performance.
if (transform) {
BoundingRect.applyTransform(tmpRect, childRect, transform);
rect = rect || tmpRect.clone();
rect.union(tmpRect);
}
else {
rect = rect || childRect.clone();
rect.union(childRect);
}
}
return rect || tmpRect;
}
}
Group.prototype.type = 'group';
// Storage will use childrenRef to get children to render.
export interface GroupLike extends Element {
childrenRef(): Element[]
}
export default Group;
+126
View File
@@ -0,0 +1,126 @@
import Displayable, { DisplayableProps,
CommonStyleProps,
DEFAULT_COMMON_STYLE,
DisplayableStatePropNames,
DEFAULT_COMMON_ANIMATION_PROPS
} from './Displayable';
import BoundingRect from '../core/BoundingRect';
import { ImageLike, MapToType } from '../core/types';
import { defaults, createObject } from '../core/util';
import { ElementCommonState } from '../Element';
export interface ImageStyleProps extends CommonStyleProps {
image?: string | ImageLike
x?: number
y?: number
width?: number
height?: number
sx?: number
sy?: number
sWidth?: number
sHeight?: number
}
export const DEFAULT_IMAGE_STYLE: CommonStyleProps = defaults({
x: 0,
y: 0
}, DEFAULT_COMMON_STYLE);
export const DEFAULT_IMAGE_ANIMATION_PROPS: MapToType<ImageProps, boolean> = {
style: defaults<MapToType<ImageStyleProps, boolean>, MapToType<ImageStyleProps, boolean>>({
x: true,
y: true,
width: true,
height: true,
sx: true,
sy: true,
sWidth: true,
sHeight: true
}, DEFAULT_COMMON_ANIMATION_PROPS.style)
};
export interface ImageProps extends DisplayableProps {
style?: ImageStyleProps
onload?: (image: ImageLike) => void
}
export type ImageState = Pick<ImageProps, DisplayableStatePropNames> & ElementCommonState
function isImageLike(source: unknown): source is HTMLImageElement {
return !!(source
&& typeof source !== 'string'
// Image source is an image, canvas, video.
&& (source as HTMLImageElement).width && (source as HTMLImageElement).height);
}
class ZRImage extends Displayable<ImageProps> {
style: ImageStyleProps
// FOR CANVAS RENDERER
__image: ImageLike
// FOR SVG RENDERER
__imageSrc: string
onload: (image: ImageLike) => void
/**
* Create an image style object with default values in it's prototype.
* @override
*/
createStyle(obj?: ImageStyleProps) {
return createObject(DEFAULT_IMAGE_STYLE, obj);
}
private _getSize(dim: 'width' | 'height') {
const style = this.style;
let size = style[dim];
if (size != null) {
return size;
}
const imageSource = isImageLike(style.image)
? style.image : this.__image;
if (!imageSource) {
return 0;
}
const otherDim = dim === 'width' ? 'height' : 'width';
let otherDimSize = style[otherDim];
if (otherDimSize == null) {
return imageSource[dim];
}
else {
return imageSource[dim] / imageSource[otherDim] * otherDimSize;
}
}
getWidth(): number {
return this._getSize('width');
}
getHeight(): number {
return this._getSize('height');
}
getAnimationStyleProps() {
return DEFAULT_IMAGE_ANIMATION_PROPS;
}
getBoundingRect(): BoundingRect {
const style = this.style;
if (!this._rect) {
this._rect = new BoundingRect(
style.x || 0, style.y || 0, this.getWidth(), this.getHeight()
);
}
return this._rect;
}
}
ZRImage.prototype.type = 'image';
export default ZRImage;
+148
View File
@@ -0,0 +1,148 @@
/**
* Displayable for incremental rendering. It will be rendered in a separate layer
* IncrementalDisplay have two main methods. `clearDisplayables` and `addDisplayables`
* addDisplayables will render the added displayables incremetally.
*
* It use a notClear flag to tell the painter don't clear the layer if it's the first element.
*
* It's not available for SVG rendering.
*/
import Displayble from './Displayable';
import BoundingRect from '../core/BoundingRect';
import { MatrixArray } from '../core/matrix';
import Group from './Group';
const m: MatrixArray = [];
// TODO Style override ?
export default class IncrementalDisplayable extends Displayble {
notClear: boolean = true
incremental = true
private _displayables: Displayble[] = []
private _temporaryDisplayables: Displayble[] = []
private _cursor = 0
traverse<T>(
cb: (this: T, el: this) => void,
context: T
) {
cb.call(context, this);
}
useStyle() {
// Use an empty style
// PENDING
this.style = {};
}
// getCurrentCursor / updateCursorAfterBrush
// is used in graphic.ts. It's not provided for developers
getCursor() {
return this._cursor;
}
// Update cursor after brush.
innerAfterBrush() {
this._cursor = this._displayables.length;
}
clearDisplaybles() {
this._displayables = [];
this._temporaryDisplayables = [];
this._cursor = 0;
this.markRedraw();
this.notClear = false;
}
clearTemporalDisplayables() {
this._temporaryDisplayables = [];
}
addDisplayable(displayable: Displayble, notPersistent?: boolean) {
if (notPersistent) {
this._temporaryDisplayables.push(displayable);
}
else {
this._displayables.push(displayable);
}
this.markRedraw();
}
addDisplayables(displayables: Displayble[], notPersistent?: boolean) {
notPersistent = notPersistent || false;
for (let i = 0; i < displayables.length; i++) {
this.addDisplayable(displayables[i], notPersistent);
}
}
getDisplayables(): Displayble[] {
return this._displayables;
}
getTemporalDisplayables(): Displayble[] {
return this._temporaryDisplayables;
}
eachPendingDisplayable(cb: (displayable: Displayble) => void) {
for (let i = this._cursor; i < this._displayables.length; i++) {
cb && cb(this._displayables[i]);
}
for (let i = 0; i < this._temporaryDisplayables.length; i++) {
cb && cb(this._temporaryDisplayables[i]);
}
}
update() {
this.updateTransform();
for (let i = this._cursor; i < this._displayables.length; i++) {
const displayable = this._displayables[i];
// PENDING
displayable.parent = this as unknown as Group;
displayable.update();
displayable.parent = null;
}
for (let i = 0; i < this._temporaryDisplayables.length; i++) {
const displayable = this._temporaryDisplayables[i];
// PENDING
displayable.parent = this as unknown as Group;
displayable.update();
displayable.parent = null;
}
}
getBoundingRect() {
if (!this._rect) {
const rect = new BoundingRect(Infinity, Infinity, -Infinity, -Infinity);
for (let i = 0; i < this._displayables.length; i++) {
const displayable = this._displayables[i];
const childRect = displayable.getBoundingRect().clone();
if (displayable.needLocalTransform()) {
childRect.applyTransform(displayable.getLocalTransform(m));
}
rect.union(childRect);
}
this._rect = rect;
}
return this._rect;
}
contain(x: number, y: number): boolean {
const localPos = this.transformCoordToLocal(x, y);
const rect = this.getBoundingRect();
if (rect.contain(localPos[0], localPos[1])) {
for (let i = 0; i < this._displayables.length; i++) {
const displayable = this._displayables[i];
if (displayable.contain(x, y)) {
return true;
}
}
}
return false;
}
}
+49
View File
@@ -0,0 +1,49 @@
import Gradient, {GradientObject, GradientColorStop} from './Gradient';
export interface LinearGradientObject extends GradientObject {
type: 'linear'
x: number
y: number
x2: number
y2: number
}
/**
* x, y, x2, y2 are all percent from 0 to 1 when globalCoord is false
*/
export default class LinearGradient extends Gradient {
type: 'linear'
x: number
y: number
x2: number
y2: number
constructor(
x: number, y: number, x2: number, y2: number,
colorStops?: GradientColorStop[], globalCoord?: boolean
) {
super(colorStops);
// Should do nothing more in this constructor. Because gradient can be
// declard by `color: {type: 'linear', colorStops: ...}`, where
// this constructor will not be called.
this.x = x == null ? 0 : x;
this.y = y == null ? 0 : y;
this.x2 = x2 == null ? 1 : x2;
this.y2 = y2 == null ? 0 : y2;
// Can be cloned
this.type = 'linear';
// If use global coord
this.global = globalCoord || false;
}
};
+677
View File
@@ -0,0 +1,677 @@
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;
+83
View File
@@ -0,0 +1,83 @@
import { ImageLike } from '../core/types';
import { SVGVNode } from '../svg/core';
type ImagePatternRepeat = 'repeat' | 'repeat-x' | 'repeat-y' | 'no-repeat'
export interface PatternObjectBase {
id?: number
// type is now unused, so make it optional
type?: 'pattern'
x?: number
y?: number
rotation?: number
scaleX?: number
scaleY?: number
}
export interface ImagePatternObject extends PatternObjectBase {
image: ImageLike | string
repeat?: ImagePatternRepeat
/**
* Width and height of image.
* `imageWidth` and `imageHeight` are only used in svg-ssr renderer.
* Because we can't get the size of image in svg-ssr renderer.
* They need to be give explictly.
*/
imageWidth?: number
imageHeight?: number
}
export interface InnerImagePatternObject extends ImagePatternObject {
// Cached image. Which is created in the canvas painter.
__image?: ImageLike
}
export interface SVGPatternObject extends PatternObjectBase {
/**
* svg vnode can only be used in svg renderer currently.
* svgWidth, svgHeight defines width and height used for pattern.
*/
svgElement?: SVGVNode
svgWidth?: number
svgHeight?: number
}
export type PatternObject = ImagePatternObject | SVGPatternObject
class Pattern {
type: 'pattern'
image: ImageLike | string
/**
* svg element can only be used in svg renderer currently.
*
* Will be string if using SSR rendering.
*/
svgElement: SVGElement | string
repeat: ImagePatternRepeat
x: number
y: number
rotation: number
scaleX: number
scaleY: number
constructor(image: ImageLike | string, repeat: ImagePatternRepeat) {
// Should do nothing more in this constructor. Because gradient can be
// declard by `color: {image: ...}`, where this constructor will not be called.
this.image = image;
this.repeat = repeat;
this.x = 0;
this.y = 0;
this.rotation = 0;
this.scaleX = 1;
this.scaleY = 1;
}
}
export default Pattern;
+43
View File
@@ -0,0 +1,43 @@
import Gradient, {GradientColorStop, GradientObject} from './Gradient';
export interface RadialGradientObject extends GradientObject {
type: 'radial'
x: number
y: number
r: number
}
/**
* x, y, r are all percent from 0 to 1 when globalCoord is false
*/
class RadialGradient extends Gradient {
type: 'radial'
x: number
y: number
r: number
constructor(
x: number, y: number, r: number,
colorStops?: GradientColorStop[], globalCoord?: boolean
) {
super(colorStops);
// Should do nothing more in this constructor. Because gradient can be
// declard by `color: {type: 'radial', colorStops: ...}`, where
// this constructor will not be called.
this.x = x == null ? 0.5 : x;
this.y = y == null ? 0.5 : y;
this.r = r == null ? 0.5 : r;
// Can be cloned
this.type = 'radial';
// If use global coord
this.global = globalCoord || false;
}
}
export default RadialGradient;
+123
View File
@@ -0,0 +1,123 @@
import Displayable, { DisplayableProps, DisplayableStatePropNames } from './Displayable';
import { getBoundingRect } from '../contain/text';
import BoundingRect from '../core/BoundingRect';
import { PathStyleProps, DEFAULT_PATH_STYLE } from './Path';
import { createObject, defaults } from '../core/util';
import { FontStyle, FontWeight, TextAlign, TextVerticalAlign } from '../core/types';
import { DEFAULT_FONT } from '../core/platform';
export interface TSpanStyleProps extends PathStyleProps {
x?: number
y?: number
// TODO Text is assigned inside zrender
text?: string
// Final generated font string
// Used in canvas, and when developers specified it.
font?: string
// Value for each part of font
// Used in svg.
// NOTE: font should always been sync with these 4 properties.
fontSize?: number
fontWeight?: FontWeight
fontStyle?: FontStyle
fontFamily?: string
textAlign?: CanvasTextAlign
textBaseline?: CanvasTextBaseline
}
export const DEFAULT_TSPAN_STYLE: TSpanStyleProps = defaults({
strokeFirst: true,
font: DEFAULT_FONT,
x: 0,
y: 0,
textAlign: 'left',
textBaseline: 'top',
miterLimit: 2
} as TSpanStyleProps, DEFAULT_PATH_STYLE);
export interface TSpanProps extends DisplayableProps {
style?: TSpanStyleProps
}
export type TSpanState = Pick<TSpanProps, DisplayableStatePropNames>
class TSpan extends Displayable<TSpanProps> {
style: TSpanStyleProps
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';
}
/**
* Create an image style object with default values in it's prototype.
* @override
*/
createStyle(obj?: TSpanStyleProps) {
return createObject(DEFAULT_TSPAN_STYLE, obj);
}
/**
* Set bounding rect calculated from Text
* For reducing time of calculating bounding rect.
*/
setBoundingRect(rect: BoundingRect) {
this._rect = rect;
}
getBoundingRect(): BoundingRect {
const style = this.style;
if (!this._rect) {
let text = style.text;
text != null ? (text += '') : (text = '');
const rect = getBoundingRect(
text,
style.font,
style.textAlign as TextAlign,
style.textBaseline as TextVerticalAlign
);
rect.x += style.x || 0;
rect.y += style.y || 0;
if (this.hasStroke()) {
const w = style.lineWidth;
rect.x -= w / 2;
rect.y -= w / 2;
rect.width += w;
rect.height += w;
}
this._rect = rect;
}
return this._rect;
}
protected static initDefaultProps = (function () {
const tspanProto = TSpan.prototype;
// TODO Calculate tolerance smarter
tspanProto.dirtyRectTolerance = 10;
})()
}
TSpan.prototype.type = 'tspan';
export default TSpan;
+1049
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -0,0 +1,4 @@
// Bit masks to check which parts of element needs to be updated.
export const REDRAW_BIT = 1;
export const STYLE_CHANGED_BIT = 2;
export const SHAPE_CHANGED_BIT = 4;
+106
View File
@@ -0,0 +1,106 @@
import LRU from '../../core/LRU';
import { platformApi } from '../../core/platform';
import { ImageLike } from '../../core/types';
const globalImageCache = new LRU<CachedImageObj>(50);
type PendingWrap = {
hostEl: {dirty: () => void}
cb: (image: ImageLike, payload: any) => void
cbPayload: any
}
type CachedImageObj = {
image: ImageLike
pending: PendingWrap[]
}
export function findExistImage(newImageOrSrc: string | ImageLike): ImageLike {
if (typeof newImageOrSrc === 'string') {
const cachedImgObj = globalImageCache.get(newImageOrSrc);
return cachedImgObj && cachedImgObj.image;
}
else {
return newImageOrSrc;
}
}
/**
* Caution: User should cache loaded images, but not just count on LRU.
* Consider if required images more than LRU size, will dead loop occur?
*
* @param newImageOrSrc
* @param image Existent image.
* @param hostEl For calling `dirty`.
* @param onload params: (image, cbPayload)
* @param cbPayload Payload on cb calling.
* @return image
*/
export function createOrUpdateImage<T>(
newImageOrSrc: string | ImageLike,
image: ImageLike,
hostEl: { dirty: () => void },
onload?: (image: ImageLike, payload: T) => void,
cbPayload?: T
) {
if (!newImageOrSrc) {
return image;
}
else if (typeof newImageOrSrc === 'string') {
// Image should not be loaded repeatly.
if ((image && (image as any).__zrImageSrc === newImageOrSrc) || !hostEl) {
return image;
}
// Only when there is no existent image or existent image src
// is different, this method is responsible for load.
const cachedImgObj = globalImageCache.get(newImageOrSrc);
const pendingWrap = {hostEl: hostEl, cb: onload, cbPayload: cbPayload};
if (cachedImgObj) {
image = cachedImgObj.image;
!isImageReady(image) && cachedImgObj.pending.push(pendingWrap);
}
else {
image = platformApi.loadImage(
newImageOrSrc, imageOnLoad, imageOnLoad
);
(image as any).__zrImageSrc = newImageOrSrc;
globalImageCache.put(
newImageOrSrc,
(image as any).__cachedImgObj = {
image: image,
pending: [pendingWrap]
}
);
}
return image;
}
// newImageOrSrc is an HTMLImageElement or HTMLCanvasElement or Canvas
else {
return newImageOrSrc;
}
}
function imageOnLoad(this: any) {
const cachedImgObj = this.__cachedImgObj;
this.onload = this.onerror = this.__cachedImgObj = null;
for (let i = 0; i < cachedImgObj.pending.length; i++) {
const pendingWrap = cachedImgObj.pending[i];
const cb = pendingWrap.cb;
cb && cb(this, pendingWrap.cbPayload);
pendingWrap.hostEl.dirty();
}
cachedImgObj.pending.length = 0;
}
export function isImageReady(image: ImageLike) {
return image && image.width && image.height;
}
+814
View File
@@ -0,0 +1,814 @@
import * as imageHelper from '../helper/image';
import {
extend,
retrieve2,
retrieve3,
reduce
} from '../../core/util';
import { TextAlign, TextVerticalAlign, ImageLike, Dictionary } from '../../core/types';
import { TextStyleProps } from '../Text';
import { getLineHeight, getWidth, parsePercent } from '../../contain/text';
const STYLE_REG = /\{([a-zA-Z0-9_]+)\|([^}]*)\}/g;
interface InnerTruncateOption {
maxIteration?: number
// If truncate result are less than minChar, ellipsis will not show
// which is better for user hint in some cases
minChar?: number
// When all truncated, use the placeholder
placeholder?: string
maxIterations?: number
}
interface InnerPreparedTruncateOption extends Required<InnerTruncateOption> {
font: string
ellipsis: string
ellipsisWidth: number
contentWidth: number
containerWidth: number
cnCharWidth: number
ascCharWidth: number
}
/**
* Show ellipsis if overflow.
*/
export function truncateText(
text: string,
containerWidth: number,
font: string,
ellipsis?: string,
options?: InnerTruncateOption
): string {
const out = {} as Parameters<typeof truncateText2>[0];
truncateText2(out, text, containerWidth, font, ellipsis, options);
return out.text;
}
// PENDING: not sure whether `truncateText` is used outside zrender, since it has an `export`
// specifier. So keep it and perform the interface modification in `truncateText2`.
function truncateText2(
out: {text: string, isTruncated: boolean},
text: string,
containerWidth: number,
font: string,
ellipsis?: string,
options?: InnerTruncateOption
): void {
if (!containerWidth) {
out.text = '';
out.isTruncated = false;
return;
}
const textLines = (text + '').split('\n');
options = prepareTruncateOptions(containerWidth, font, ellipsis, options);
// FIXME
// It is not appropriate that every line has '...' when truncate multiple lines.
let isTruncated = false;
const truncateOut = {} as Parameters<typeof truncateSingleLine>[0];
for (let i = 0, len = textLines.length; i < len; i++) {
truncateSingleLine(truncateOut, textLines[i], options as InnerPreparedTruncateOption);
textLines[i] = truncateOut.textLine;
isTruncated = isTruncated || truncateOut.isTruncated;
}
out.text = textLines.join('\n');
out.isTruncated = isTruncated;
}
function prepareTruncateOptions(
containerWidth: number,
font: string,
ellipsis?: string,
options?: InnerTruncateOption
): InnerPreparedTruncateOption {
options = options || {};
let preparedOpts = extend({}, options) as InnerPreparedTruncateOption;
preparedOpts.font = font;
ellipsis = retrieve2(ellipsis, '...');
preparedOpts.maxIterations = retrieve2(options.maxIterations, 2);
const minChar = preparedOpts.minChar = retrieve2(options.minChar, 0);
// FIXME
// Other languages?
preparedOpts.cnCharWidth = getWidth('国', font);
// FIXME
// Consider proportional font?
const ascCharWidth = preparedOpts.ascCharWidth = getWidth('a', font);
preparedOpts.placeholder = retrieve2(options.placeholder, '');
// Example 1: minChar: 3, text: 'asdfzxcv', truncate result: 'asdf', but not: 'a...'.
// Example 2: minChar: 3, text: '维度', truncate result: '维', but not: '...'.
let contentWidth = containerWidth = Math.max(0, containerWidth - 1); // Reserve some gap.
for (let i = 0; i < minChar && contentWidth >= ascCharWidth; i++) {
contentWidth -= ascCharWidth;
}
let ellipsisWidth = getWidth(ellipsis, font);
if (ellipsisWidth > contentWidth) {
ellipsis = '';
ellipsisWidth = 0;
}
contentWidth = containerWidth - ellipsisWidth;
preparedOpts.ellipsis = ellipsis;
preparedOpts.ellipsisWidth = ellipsisWidth;
preparedOpts.contentWidth = contentWidth;
preparedOpts.containerWidth = containerWidth;
return preparedOpts;
}
function truncateSingleLine(
out: {textLine: string, isTruncated: boolean},
textLine: string,
options: InnerPreparedTruncateOption
): void {
const containerWidth = options.containerWidth;
const font = options.font;
const contentWidth = options.contentWidth;
if (!containerWidth) {
out.textLine = '';
out.isTruncated = false;
return;
}
let lineWidth = getWidth(textLine, font);
if (lineWidth <= containerWidth) {
out.textLine = textLine;
out.isTruncated = false;
return;
}
for (let j = 0; ; j++) {
if (lineWidth <= contentWidth || j >= options.maxIterations) {
textLine += options.ellipsis;
break;
}
const subLength = j === 0
? estimateLength(textLine, contentWidth, options.ascCharWidth, options.cnCharWidth)
: lineWidth > 0
? Math.floor(textLine.length * contentWidth / lineWidth)
: 0;
textLine = textLine.substr(0, subLength);
lineWidth = getWidth(textLine, font);
}
if (textLine === '') {
textLine = options.placeholder;
}
out.textLine = textLine;
out.isTruncated = true;
}
function estimateLength(
text: string, contentWidth: number, ascCharWidth: number, cnCharWidth: number
): number {
let width = 0;
let i = 0;
for (let len = text.length; i < len && width < contentWidth; i++) {
const charCode = text.charCodeAt(i);
width += (0 <= charCode && charCode <= 127) ? ascCharWidth : cnCharWidth;
}
return i;
}
export interface PlainTextContentBlock {
lineHeight: number
// Line height of actual content.
calculatedLineHeight: number
contentWidth: number
contentHeight: number
width: number
height: number
/**
* Real text width containing padding.
* It should be the same as `width` if background is rendered
* and `width` is set by user.
*/
outerWidth: number
outerHeight: number
lines: string[]
// Be `true` if and only if the result text is modified due to overflow, due to
// settings on either `overflow` or `lineOverflow`
isTruncated: boolean
}
export function parsePlainText(
text: string,
style?: TextStyleProps
): PlainTextContentBlock {
text != null && (text += '');
// textPadding has been normalized
const overflow = style.overflow;
const padding = style.padding as number[];
const font = style.font;
const truncate = overflow === 'truncate';
const calculatedLineHeight = getLineHeight(font);
const lineHeight = retrieve2(style.lineHeight, calculatedLineHeight);
const bgColorDrawn = !!(style.backgroundColor);
const truncateLineOverflow = style.lineOverflow === 'truncate';
let isTruncated = false;
let width = style.width;
let lines: string[];
if (width != null && (overflow === 'break' || overflow === 'breakAll')) {
lines = text ? wrapText(text, style.font, width, overflow === 'breakAll', 0).lines : [];
}
else {
lines = text ? text.split('\n') : [];
}
const contentHeight = lines.length * lineHeight;
const height = retrieve2(style.height, contentHeight);
// Truncate lines.
if (contentHeight > height && truncateLineOverflow) {
const lineCount = Math.floor(height / lineHeight);
isTruncated = isTruncated || (lines.length > lineCount);
lines = lines.slice(0, lineCount);
// TODO If show ellipse for line truncate
// if (style.ellipsis) {
// const options = prepareTruncateOptions(width, font, style.ellipsis, {
// minChar: style.truncateMinChar,
// placeholder: style.placeholder
// });
// lines[lineCount - 1] = truncateSingleLine(lastLine, options);
// }
}
if (text && truncate && width != null) {
const options = prepareTruncateOptions(width, font, style.ellipsis, {
minChar: style.truncateMinChar,
placeholder: style.placeholder
});
// Having every line has '...' when truncate multiple lines.
const singleOut = {} as Parameters<typeof truncateSingleLine>[0];
for (let i = 0; i < lines.length; i++) {
truncateSingleLine(singleOut, lines[i], options);
lines[i] = singleOut.textLine;
isTruncated = isTruncated || singleOut.isTruncated;
}
}
// Calculate real text width and height
let outerHeight = height;
let contentWidth = 0;
for (let i = 0; i < lines.length; i++) {
contentWidth = Math.max(getWidth(lines[i], font), contentWidth);
}
if (width == null) {
// When width is not explicitly set, use outerWidth as width.
width = contentWidth;
}
let outerWidth = contentWidth;
if (padding) {
outerHeight += padding[0] + padding[2];
outerWidth += padding[1] + padding[3];
width += padding[1] + padding[3];
}
if (bgColorDrawn) {
// When render background, outerWidth should be the same as width.
outerWidth = width;
}
return {
lines: lines,
height: height,
outerWidth: outerWidth,
outerHeight: outerHeight,
lineHeight: lineHeight,
calculatedLineHeight: calculatedLineHeight,
contentWidth: contentWidth,
contentHeight: contentHeight,
width: width,
isTruncated: isTruncated
};
}
class RichTextToken {
styleName: string
text: string
width: number
height: number
// Inner height exclude padding
innerHeight: number
// Width and height of actual text content.
contentHeight: number
contentWidth: number
lineHeight: number
font: string
align: TextAlign
verticalAlign: TextVerticalAlign
textPadding: number[]
percentWidth?: string
isLineHolder: boolean
}
class RichTextLine {
lineHeight: number
width: number
tokens: RichTextToken[] = []
constructor(tokens?: RichTextToken[]) {
if (tokens) {
this.tokens = tokens;
}
}
}
export class RichTextContentBlock {
// width/height of content
width: number = 0
height: number = 0
// Calculated text height
contentWidth: number = 0
contentHeight: number = 0
// outerWidth/outerHeight with padding
outerWidth: number = 0
outerHeight: number = 0
lines: RichTextLine[] = []
// Be `true` if and only if the result text is modified due to overflow, due to
// settings on either `overflow` or `lineOverflow`
isTruncated: boolean = false
}
type WrapInfo = {
width: number,
accumWidth: number,
breakAll: boolean
}
/**
* For example: 'some text {a|some text}other text{b|some text}xxx{c|}xxx'
* Also consider 'bbbb{a|xxx\nzzz}xxxx\naaaa'.
* If styleName is undefined, it is plain text.
*/
export function parseRichText(text: string, style: TextStyleProps): RichTextContentBlock {
const contentBlock = new RichTextContentBlock();
text != null && (text += '');
if (!text) {
return contentBlock;
}
const topWidth = style.width;
const topHeight = style.height;
const overflow = style.overflow;
let wrapInfo: WrapInfo = (overflow === 'break' || overflow === 'breakAll') && topWidth != null
? {width: topWidth, accumWidth: 0, breakAll: overflow === 'breakAll'}
: null;
let lastIndex = STYLE_REG.lastIndex = 0;
let result;
while ((result = STYLE_REG.exec(text)) != null) {
const matchedIndex = result.index;
if (matchedIndex > lastIndex) {
pushTokens(contentBlock, text.substring(lastIndex, matchedIndex), style, wrapInfo);
}
pushTokens(contentBlock, result[2], style, wrapInfo, result[1]);
lastIndex = STYLE_REG.lastIndex;
}
if (lastIndex < text.length) {
pushTokens(contentBlock, text.substring(lastIndex, text.length), style, wrapInfo);
}
// For `textWidth: xx%`
let pendingList = [];
let calculatedHeight = 0;
let calculatedWidth = 0;
const stlPadding = style.padding as number[];
const truncate = overflow === 'truncate';
const truncateLine = style.lineOverflow === 'truncate';
const tmpTruncateOut = {} as Parameters<typeof truncateText2>[0];
// let prevToken: RichTextToken;
function finishLine(line: RichTextLine, lineWidth: number, lineHeight: number) {
line.width = lineWidth;
line.lineHeight = lineHeight;
calculatedHeight += lineHeight;
calculatedWidth = Math.max(calculatedWidth, lineWidth);
}
// Calculate layout info of tokens.
outer: for (let i = 0; i < contentBlock.lines.length; i++) {
const line = contentBlock.lines[i];
let lineHeight = 0;
let lineWidth = 0;
for (let j = 0; j < line.tokens.length; j++) {
const token = line.tokens[j];
const tokenStyle = token.styleName && style.rich[token.styleName] || {};
// textPadding should not inherit from style.
const textPadding = token.textPadding = tokenStyle.padding as number[];
const paddingH = textPadding ? textPadding[1] + textPadding[3] : 0;
const font = token.font = tokenStyle.font || style.font;
token.contentHeight = getLineHeight(font);
// textHeight can be used when textVerticalAlign is specified in token.
let tokenHeight = retrieve2(
// textHeight should not be inherited, consider it can be specified
// as box height of the block.
tokenStyle.height, token.contentHeight
);
token.innerHeight = tokenHeight;
textPadding && (tokenHeight += textPadding[0] + textPadding[2]);
token.height = tokenHeight;
// Inlcude padding in lineHeight.
token.lineHeight = retrieve3(
tokenStyle.lineHeight, style.lineHeight, tokenHeight
);
token.align = tokenStyle && tokenStyle.align || style.align;
token.verticalAlign = tokenStyle && tokenStyle.verticalAlign || 'middle';
if (truncateLine && topHeight != null && calculatedHeight + token.lineHeight > topHeight) {
// TODO Add ellipsis on the previous token.
// prevToken.text =
const originalLength = contentBlock.lines.length;
if (j > 0) {
line.tokens = line.tokens.slice(0, j);
finishLine(line, lineWidth, lineHeight);
contentBlock.lines = contentBlock.lines.slice(0, i + 1);
}
else {
contentBlock.lines = contentBlock.lines.slice(0, i);
}
contentBlock.isTruncated = contentBlock.isTruncated || (contentBlock.lines.length < originalLength);
break outer;
}
let styleTokenWidth = tokenStyle.width;
let tokenWidthNotSpecified = styleTokenWidth == null || styleTokenWidth === 'auto';
// Percent width, can be `100%`, can be used in drawing separate
// line when box width is needed to be auto.
if (typeof styleTokenWidth === 'string' && styleTokenWidth.charAt(styleTokenWidth.length - 1) === '%') {
token.percentWidth = styleTokenWidth;
pendingList.push(token);
token.contentWidth = getWidth(token.text, font);
// Do not truncate in this case, because there is no user case
// and it is too complicated.
}
else {
if (tokenWidthNotSpecified) {
// FIXME: If image is not loaded and textWidth is not specified, calling
// `getBoundingRect()` will not get correct result.
const textBackgroundColor = tokenStyle.backgroundColor;
let bgImg = textBackgroundColor && (textBackgroundColor as { image: ImageLike }).image;
if (bgImg) {
bgImg = imageHelper.findExistImage(bgImg);
if (imageHelper.isImageReady(bgImg)) {
// Update token width from image size.
token.width = Math.max(token.width, bgImg.width * tokenHeight / bgImg.height);
}
}
}
const remainTruncWidth = truncate && topWidth != null
? topWidth - lineWidth : null;
if (remainTruncWidth != null && remainTruncWidth < token.width) {
if (!tokenWidthNotSpecified || remainTruncWidth < paddingH) {
token.text = '';
token.width = token.contentWidth = 0;
}
else {
truncateText2(
tmpTruncateOut,
token.text, remainTruncWidth - paddingH, font, style.ellipsis,
{minChar: style.truncateMinChar}
);
token.text = tmpTruncateOut.text;
contentBlock.isTruncated = contentBlock.isTruncated || tmpTruncateOut.isTruncated;
token.width = token.contentWidth = getWidth(token.text, font);
}
}
else {
token.contentWidth = getWidth(token.text, font);
}
}
token.width += paddingH;
lineWidth += token.width;
tokenStyle && (lineHeight = Math.max(lineHeight, token.lineHeight));
// prevToken = token;
}
finishLine(line, lineWidth, lineHeight);
}
contentBlock.outerWidth = contentBlock.width = retrieve2(topWidth, calculatedWidth);
contentBlock.outerHeight = contentBlock.height = retrieve2(topHeight, calculatedHeight);
contentBlock.contentHeight = calculatedHeight;
contentBlock.contentWidth = calculatedWidth;
if (stlPadding) {
contentBlock.outerWidth += stlPadding[1] + stlPadding[3];
contentBlock.outerHeight += stlPadding[0] + stlPadding[2];
}
for (let i = 0; i < pendingList.length; i++) {
const token = pendingList[i];
const percentWidth = token.percentWidth;
// Should not base on outerWidth, because token can not be placed out of padding.
token.width = parseInt(percentWidth, 10) / 100 * contentBlock.width;
}
return contentBlock;
}
type TokenStyle = TextStyleProps['rich'][string];
function pushTokens(
block: RichTextContentBlock,
str: string,
style: TextStyleProps,
wrapInfo: WrapInfo,
styleName?: string
) {
const isEmptyStr = str === '';
const tokenStyle: TokenStyle = styleName && style.rich[styleName] || {};
const lines = block.lines;
const font = tokenStyle.font || style.font;
let newLine = false;
let strLines;
let linesWidths;
if (wrapInfo) {
const tokenPadding = tokenStyle.padding as number[];
let tokenPaddingH = tokenPadding ? tokenPadding[1] + tokenPadding[3] : 0;
if (tokenStyle.width != null && tokenStyle.width !== 'auto') {
// Wrap the whole token if tokenWidth if fixed.
const outerWidth = parsePercent(tokenStyle.width, wrapInfo.width) + tokenPaddingH;
if (lines.length > 0) { // Not first line
if (outerWidth + wrapInfo.accumWidth > wrapInfo.width) {
// TODO Support wrap text in token.
strLines = str.split('\n');
newLine = true;
}
}
wrapInfo.accumWidth = outerWidth;
}
else {
const res = wrapText(str, font, wrapInfo.width, wrapInfo.breakAll, wrapInfo.accumWidth);
wrapInfo.accumWidth = res.accumWidth + tokenPaddingH;
linesWidths = res.linesWidths;
strLines = res.lines;
}
}
else {
strLines = str.split('\n');
}
for (let i = 0; i < strLines.length; i++) {
const text = strLines[i];
const token = new RichTextToken();
token.styleName = styleName;
token.text = text;
token.isLineHolder = !text && !isEmptyStr;
if (typeof tokenStyle.width === 'number') {
token.width = tokenStyle.width;
}
else {
token.width = linesWidths
? linesWidths[i] // Caculated width in the wrap
: getWidth(text, font);
}
// The first token should be appended to the last line if not new line.
if (!i && !newLine) {
const tokens = (lines[lines.length - 1] || (lines[0] = new RichTextLine())).tokens;
// Consider cases:
// (1) ''.split('\n') => ['', '\n', ''], the '' at the first item
// (which is a placeholder) should be replaced by new token.
// (2) A image backage, where token likes {a|}.
// (3) A redundant '' will affect textAlign in line.
// (4) tokens with the same tplName should not be merged, because
// they should be displayed in different box (with border and padding).
const tokensLen = tokens.length;
(tokensLen === 1 && tokens[0].isLineHolder)
? (tokens[0] = token)
// Consider text is '', only insert when it is the "lineHolder" or
// "emptyStr". Otherwise a redundant '' will affect textAlign in line.
: ((text || !tokensLen || isEmptyStr) && tokens.push(token));
}
// Other tokens always start a new line.
else {
// If there is '', insert it as a placeholder.
lines.push(new RichTextLine([token]));
}
}
}
function isAlphabeticLetter(ch: string) {
// Unicode Character Ranges
// https://jrgraphix.net/research/unicode_blocks.php
// The following ranges may not cover all letter ranges but only the more
// popular ones. Developers could make pull requests when they find those
// not covered.
let code = ch.charCodeAt(0);
return code >= 0x20 && code <= 0x24F // Latin
|| code >= 0x370 && code <= 0x10FF // Greek, Coptic, Cyrilic, and etc.
|| code >= 0x1200 && code <= 0x13FF // Ethiopic and Cherokee
|| code >= 0x1E00 && code <= 0x206F; // Latin and Greek extended
}
const breakCharMap = reduce(',&?/;] '.split(''), function (obj, ch) {
obj[ch] = true;
return obj;
}, {} as Dictionary<boolean>);
/**
* If break by word. For latin languages.
*/
function isWordBreakChar(ch: string) {
if (isAlphabeticLetter(ch)) {
if (breakCharMap[ch]) {
return true;
}
return false;
}
return true;
}
function wrapText(
text: string,
font: string,
lineWidth: number,
isBreakAll: boolean,
lastAccumWidth: number
) {
let lines: string[] = [];
let linesWidths: number[] = [];
let line = '';
let currentWord = '';
let currentWordWidth = 0;
let accumWidth = 0;
for (let i = 0; i < text.length; i++) {
const ch = text.charAt(i);
if (ch === '\n') {
if (currentWord) {
line += currentWord;
accumWidth += currentWordWidth;
}
lines.push(line);
linesWidths.push(accumWidth);
// Reset
line = '';
currentWord = '';
currentWordWidth = 0;
accumWidth = 0;
continue;
}
const chWidth = getWidth(ch, font);
const inWord = isBreakAll ? false : !isWordBreakChar(ch);
if (!lines.length
? lastAccumWidth + accumWidth + chWidth > lineWidth
: accumWidth + chWidth > lineWidth
) {
if (!accumWidth) { // If nothing appended yet.
if (inWord) {
// The word length is still too long for one line
// Force break the word
lines.push(currentWord);
linesWidths.push(currentWordWidth);
currentWord = ch;
currentWordWidth = chWidth;
}
else {
// lineWidth is too small for ch
lines.push(ch);
linesWidths.push(chWidth);
}
}
else if (line || currentWord) {
if (inWord) {
if (!line) {
// The one word is still too long for one line
// Force break the word
// TODO Keep the word?
line = currentWord;
currentWord = '';
currentWordWidth = 0;
accumWidth = currentWordWidth;
}
lines.push(line);
linesWidths.push(accumWidth - currentWordWidth);
// Break the whole word
currentWord += ch;
currentWordWidth += chWidth;
line = '';
accumWidth = currentWordWidth;
}
else {
// Append lastWord if have
if (currentWord) {
line += currentWord;
currentWord = '';
currentWordWidth = 0;
}
lines.push(line);
linesWidths.push(accumWidth);
line = ch;
accumWidth = chWidth;
}
}
continue;
}
accumWidth += chWidth;
if (inWord) {
currentWord += ch;
currentWordWidth += chWidth;
}
else {
// Append whole word
if (currentWord) {
line += currentWord;
// Reset
currentWord = '';
currentWordWidth = 0;
}
// Append character
line += ch;
}
}
if (!lines.length && !line) {
line = text;
currentWord = '';
currentWordWidth = 0;
}
// Append last line.
if (currentWord) {
line += currentWord;
}
if (line) {
lines.push(line);
linesWidths.push(accumWidth);
}
if (lines.length === 1) {
// No new line.
accumWidth += lastAccumWidth;
}
return {
// Accum width of last line
accumWidth,
lines: lines,
linesWidths
};
}
+43
View File
@@ -0,0 +1,43 @@
import smoothBezier from './smoothBezier';
import { VectorArray } from '../../core/vector';
import PathProxy from '../../core/PathProxy';
export function buildPath(
ctx: CanvasRenderingContext2D | PathProxy,
shape: {
points: VectorArray[],
smooth?: number
smoothConstraint?: VectorArray[]
},
closePath: boolean
) {
const smooth = shape.smooth;
let points = shape.points;
if (points && points.length >= 2) {
if (smooth) {
const controlPoints = smoothBezier(
points, smooth, closePath, shape.smoothConstraint
);
ctx.moveTo(points[0][0], points[0][1]);
const len = points.length;
for (let i = 0; i < (closePath ? len : len - 1); i++) {
const cp1 = controlPoints[i * 2];
const cp2 = controlPoints[i * 2 + 1];
const p = points[(i + 1) % len];
ctx.bezierCurveTo(
cp1[0], cp1[1], cp2[0], cp2[1], p[0], p[1]
);
}
}
else {
ctx.moveTo(points[0][0], points[0][1]);
for (let i = 1, l = points.length; i < l; i++) {
ctx.lineTo(points[i][0], points[i][1]);
}
}
closePath && ctx.closePath();
}
}
+87
View File
@@ -0,0 +1,87 @@
import PathProxy from '../../core/PathProxy';
export function buildPath(ctx: CanvasRenderingContext2D | PathProxy, shape: {
x: number
y: number
width: number
height: number
r?: number | number[]
}) {
let x = shape.x;
let y = shape.y;
let width = shape.width;
let height = shape.height;
let r = shape.r;
let r1;
let r2;
let r3;
let r4;
// Convert width and height to positive for better borderRadius
if (width < 0) {
x = x + width;
width = -width;
}
if (height < 0) {
y = y + height;
height = -height;
}
if (typeof r === 'number') {
r1 = r2 = r3 = r4 = r;
}
else if (r instanceof Array) {
if (r.length === 1) {
r1 = r2 = r3 = r4 = r[0];
}
else if (r.length === 2) {
r1 = r3 = r[0];
r2 = r4 = r[1];
}
else if (r.length === 3) {
r1 = r[0];
r2 = r4 = r[1];
r3 = r[2];
}
else {
r1 = r[0];
r2 = r[1];
r3 = r[2];
r4 = r[3];
}
}
else {
r1 = r2 = r3 = r4 = 0;
}
let total;
if (r1 + r2 > width) {
total = r1 + r2;
r1 *= width / total;
r2 *= width / total;
}
if (r3 + r4 > width) {
total = r3 + r4;
r3 *= width / total;
r4 *= width / total;
}
if (r2 + r3 > height) {
total = r2 + r3;
r2 *= height / total;
r3 *= height / total;
}
if (r1 + r4 > height) {
total = r1 + r4;
r1 *= height / total;
r4 *= height / total;
}
ctx.moveTo(x + r1, y);
ctx.lineTo(x + width - r2, y);
r2 !== 0 && ctx.arc(x + width - r2, y + r2, r2, -Math.PI / 2, 0);
ctx.lineTo(x + width, y + height - r3);
r3 !== 0 && ctx.arc(x + width - r3, y + height - r3, r3, 0, Math.PI / 2);
ctx.lineTo(x + r4, y + height);
r4 !== 0 && ctx.arc(x + r4, y + height - r4, r4, Math.PI / 2, Math.PI);
ctx.lineTo(x, y + r1);
r1 !== 0 && ctx.arc(x + r1, y + r1, r1, Math.PI, Math.PI * 1.5);
}
+321
View File
@@ -0,0 +1,321 @@
import PathProxy from '../../core/PathProxy';
import { isArray } from '../../core/util';
const PI = Math.PI;
const PI2 = PI * 2;
const mathSin = Math.sin;
const mathCos = Math.cos;
const mathACos = Math.acos;
const mathATan2 = Math.atan2;
const mathAbs = Math.abs;
const mathSqrt = Math.sqrt;
const mathMax = Math.max;
const mathMin = Math.min;
const e = 1e-4;
function intersect(
x0: number, y0: number,
x1: number, y1: number,
x2: number, y2: number,
x3: number, y3: number
): [number, number] {
const dx10 = x1 - x0;
const dy10 = y1 - y0;
const dx32 = x3 - x2;
const dy32 = y3 - y2;
let t = dy32 * dx10 - dx32 * dy10;
if (t * t < e) {
return;
}
t = (dx32 * (y0 - y2) - dy32 * (x0 - x2)) / t;
return [x0 + t * dx10, y0 + t * dy10];
}
// Compute perpendicular offset line of length rc.
function computeCornerTangents(
x0: number, y0: number,
x1: number, y1: number,
radius: number, cr: number,
clockwise: boolean
) {
const x01 = x0 - x1;
const y01 = y0 - y1;
const lo = (clockwise ? cr : -cr) / mathSqrt(x01 * x01 + y01 * y01);
const ox = lo * y01;
const oy = -lo * x01;
const x11 = x0 + ox;
const y11 = y0 + oy;
const x10 = x1 + ox;
const y10 = y1 + oy;
const x00 = (x11 + x10) / 2;
const y00 = (y11 + y10) / 2;
const dx = x10 - x11;
const dy = y10 - y11;
const d2 = dx * dx + dy * dy;
const r = radius - cr;
const s = x11 * y10 - x10 * y11;
const d = (dy < 0 ? -1 : 1) * mathSqrt(mathMax(0, r * r * d2 - s * s));
let cx0 = (s * dy - dx * d) / d2;
let cy0 = (-s * dx - dy * d) / d2;
const cx1 = (s * dy + dx * d) / d2;
const cy1 = (-s * dx + dy * d) / d2;
const dx0 = cx0 - x00;
const dy0 = cy0 - y00;
const dx1 = cx1 - x00;
const dy1 = cy1 - y00;
// Pick the closer of the two intersection points
// TODO: Is there a faster way to determine which intersection to use?
if (dx0 * dx0 + dy0 * dy0 > dx1 * dx1 + dy1 * dy1) {
cx0 = cx1;
cy0 = cy1;
}
return {
cx: cx0,
cy: cy0,
x0: -ox,
y0: -oy,
x1: cx0 * (radius / r - 1),
y1: cy0 * (radius / r - 1)
};
}
// For compatibility, don't use normalizeCssArray
// 5 represents [5, 5, 5, 5]
// [5] represents [5, 5, 0, 0]
// [5, 10] represents [5, 5, 10, 10]
// [5, 10, 15] represents [5, 10, 15, 15]
// [5, 10, 15, 20] represents [5, 10, 15, 20]
function normalizeCornerRadius(cr: number | number[]): number[] {
let arr: number[];
if (isArray(cr)) {
const len = cr.length;
if (!len) {
return cr as number[];
}
if (len === 1) {
arr = [cr[0], cr[0], 0, 0];
}
else if (len === 2) {
arr = [cr[0], cr[0], cr[1], cr[1]];
}
else if (len === 3) {
arr = cr.concat(cr[2]);
}
else {
arr = cr;
}
}
else {
arr = [cr, cr, cr, cr];
}
return arr;
}
export function buildPath(ctx: CanvasRenderingContext2D | PathProxy, shape: {
cx: number
cy: number
startAngle: number
endAngle: number
clockwise?: boolean,
r?: number,
r0?: number,
cornerRadius?: number | number[]
}) {
let radius = mathMax(shape.r, 0);
let innerRadius = mathMax(shape.r0 || 0, 0);
const hasRadius = radius > 0;
const hasInnerRadius = innerRadius > 0;
if (!hasRadius && !hasInnerRadius) {
return;
}
if (!hasRadius) {
// use innerRadius as radius if no radius
radius = innerRadius;
innerRadius = 0;
}
if (innerRadius > radius) {
// swap, ensure that radius is always larger than innerRadius
const tmp = radius;
radius = innerRadius;
innerRadius = tmp;
}
const { startAngle, endAngle } = shape;
if (isNaN(startAngle) || isNaN(endAngle)) {
return;
}
const { cx, cy } = shape;
const clockwise = !!shape.clockwise;
let arc = mathAbs(endAngle - startAngle);
const mod = arc > PI2 && arc % PI2;
mod > e && (arc = mod);
// is a point
if (!(radius > e)) {
ctx.moveTo(cx, cy);
}
// is a circle or annulus
else if (arc > PI2 - e) {
ctx.moveTo(
cx + radius * mathCos(startAngle),
cy + radius * mathSin(startAngle)
);
ctx.arc(cx, cy, radius, startAngle, endAngle, !clockwise);
if (innerRadius > e) {
ctx.moveTo(
cx + innerRadius * mathCos(endAngle),
cy + innerRadius * mathSin(endAngle)
);
ctx.arc(cx, cy, innerRadius, endAngle, startAngle, clockwise);
}
}
// is a circular or annular sector
else {
let icrStart;
let icrEnd;
let ocrStart;
let ocrEnd;
let ocrs;
let ocre;
let icrs;
let icre;
let ocrMax;
let icrMax;
let limitedOcrMax;
let limitedIcrMax;
let xre;
let yre;
let xirs;
let yirs;
const xrs = radius * mathCos(startAngle);
const yrs = radius * mathSin(startAngle);
const xire = innerRadius * mathCos(endAngle);
const yire = innerRadius * mathSin(endAngle);
const hasArc = arc > e;
if (hasArc) {
const cornerRadius = shape.cornerRadius;
if (cornerRadius) {
[icrStart, icrEnd, ocrStart, ocrEnd] = normalizeCornerRadius(cornerRadius);
}
const halfRd = mathAbs(radius - innerRadius) / 2;
ocrs = mathMin(halfRd, ocrStart);
ocre = mathMin(halfRd, ocrEnd);
icrs = mathMin(halfRd, icrStart);
icre = mathMin(halfRd, icrEnd);
limitedOcrMax = ocrMax = mathMax(ocrs, ocre);
limitedIcrMax = icrMax = mathMax(icrs, icre);
// draw corner radius
if (ocrMax > e || icrMax > e) {
xre = radius * mathCos(endAngle);
yre = radius * mathSin(endAngle);
xirs = innerRadius * mathCos(startAngle);
yirs = innerRadius * mathSin(startAngle);
// restrict the max value of corner radius
if (arc < PI) {
const it = intersect(xrs, yrs, xirs, yirs, xre, yre, xire, yire);
if (it) {
const x0 = xrs - it[0];
const y0 = yrs - it[1];
const x1 = xre - it[0];
const y1 = yre - it[1];
const a = 1 / mathSin(
// eslint-disable-next-line max-len
mathACos((x0 * x1 + y0 * y1) / (mathSqrt(x0 * x0 + y0 * y0) * mathSqrt(x1 * x1 + y1 * y1))) / 2
);
const b = mathSqrt(it[0] * it[0] + it[1] * it[1]);
limitedOcrMax = mathMin(ocrMax, (radius - b) / (a + 1));
limitedIcrMax = mathMin(icrMax, (innerRadius - b) / (a - 1));
}
}
}
}
// the sector is collapsed to a line
if (!hasArc) {
ctx.moveTo(cx + xrs, cy + yrs);
}
// the outer ring has corners
else if (limitedOcrMax > e) {
const crStart = mathMin(ocrStart, limitedOcrMax);
const crEnd = mathMin(ocrEnd, limitedOcrMax);
const ct0 = computeCornerTangents(xirs, yirs, xrs, yrs, radius, crStart, clockwise);
const ct1 = computeCornerTangents(xre, yre, xire, yire, radius, crEnd, clockwise);
ctx.moveTo(cx + ct0.cx + ct0.x0, cy + ct0.cy + ct0.y0);
// Have the corners merged?
if (limitedOcrMax < ocrMax && crStart === crEnd) {
// eslint-disable-next-line max-len
ctx.arc(cx + ct0.cx, cy + ct0.cy, limitedOcrMax, mathATan2(ct0.y0, ct0.x0), mathATan2(ct1.y0, ct1.x0), !clockwise);
}
else {
// draw the two corners and the ring
// eslint-disable-next-line max-len
crStart > 0 && ctx.arc(cx + ct0.cx, cy + ct0.cy, crStart, mathATan2(ct0.y0, ct0.x0), mathATan2(ct0.y1, ct0.x1), !clockwise);
// eslint-disable-next-line max-len
ctx.arc(cx, cy, radius, mathATan2(ct0.cy + ct0.y1, ct0.cx + ct0.x1), mathATan2(ct1.cy + ct1.y1, ct1.cx + ct1.x1), !clockwise);
// eslint-disable-next-line max-len
crEnd > 0 && ctx.arc(cx + ct1.cx, cy + ct1.cy, crEnd, mathATan2(ct1.y1, ct1.x1), mathATan2(ct1.y0, ct1.x0), !clockwise);
}
}
// the outer ring is a circular arc
else {
ctx.moveTo(cx + xrs, cy + yrs);
ctx.arc(cx, cy, radius, startAngle, endAngle, !clockwise);
}
// no inner ring, is a circular sector
if (!(innerRadius > e) || !hasArc) {
ctx.lineTo(cx + xire, cy + yire);
}
// the inner ring has corners
else if (limitedIcrMax > e) {
const crStart = mathMin(icrStart, limitedIcrMax);
const crEnd = mathMin(icrEnd, limitedIcrMax);
const ct0 = computeCornerTangents(xire, yire, xre, yre, innerRadius, -crEnd, clockwise);
const ct1 = computeCornerTangents(xrs, yrs, xirs, yirs, innerRadius, -crStart, clockwise);
ctx.lineTo(cx + ct0.cx + ct0.x0, cy + ct0.cy + ct0.y0);
// Have the corners merged?
if (limitedIcrMax < icrMax && crStart === crEnd) {
// eslint-disable-next-line max-len
ctx.arc(cx + ct0.cx, cy + ct0.cy, limitedIcrMax, mathATan2(ct0.y0, ct0.x0), mathATan2(ct1.y0, ct1.x0), !clockwise);
}
// draw the two corners and the ring
else {
// eslint-disable-next-line max-len
crEnd > 0 && ctx.arc(cx + ct0.cx, cy + ct0.cy, crEnd, mathATan2(ct0.y0, ct0.x0), mathATan2(ct0.y1, ct0.x1), !clockwise);
// eslint-disable-next-line max-len
ctx.arc(cx, cy, innerRadius, mathATan2(ct0.cy + ct0.y1, ct0.cx + ct0.x1), mathATan2(ct1.cy + ct1.y1, ct1.cx + ct1.x1), clockwise);
// eslint-disable-next-line max-len
crStart > 0 && ctx.arc(cx + ct1.cx, cy + ct1.cy, crStart, mathATan2(ct1.y1, ct1.x1), mathATan2(ct1.y0, ct1.x0), !clockwise);
}
}
// the inner ring is just a circular arc
else {
// FIXME: if no lineTo, svg renderer will perform an abnormal drawing behavior.
ctx.lineTo(cx + xire, cy + yire);
ctx.arc(cx, cy, innerRadius, endAngle, startAngle, clockwise);
}
}
ctx.closePath();
}
+104
View File
@@ -0,0 +1,104 @@
/**
* 贝塞尔平滑曲线
*/
import {
min as v2Min,
max as v2Max,
scale as v2Scale,
distance as v2Distance,
add as v2Add,
clone as v2Clone,
sub as v2Sub,
VectorArray
} from '../../core/vector';
/**
* 贝塞尔平滑曲线
* @param points 线段顶点数组
* @param smooth 平滑等级, 0-1
* @param isLoop
* @param constraint 将计算出来的控制点约束在一个包围盒内
* 比如 [[0, 0], [100, 100]], 这个包围盒会与
* 整个折线的包围盒做一个并集用来约束控制点。
* @param 计算出来的控制点数组
*/
export default function smoothBezier(
points: VectorArray[],
smooth?: number,
isLoop?: boolean,
constraint?: VectorArray[]
) {
const cps = [];
const v: VectorArray = [];
const v1: VectorArray = [];
const v2: VectorArray = [];
let prevPoint;
let nextPoint;
let min;
let max;
if (constraint) {
min = [Infinity, Infinity];
max = [-Infinity, -Infinity];
for (let i = 0, len = points.length; i < len; i++) {
v2Min(min, min, points[i]);
v2Max(max, max, points[i]);
}
// 与指定的包围盒做并集
v2Min(min, min, constraint[0]);
v2Max(max, max, constraint[1]);
}
for (let i = 0, len = points.length; i < len; i++) {
const point = points[i];
if (isLoop) {
prevPoint = points[i ? i - 1 : len - 1];
nextPoint = points[(i + 1) % len];
}
else {
if (i === 0 || i === len - 1) {
cps.push(v2Clone(points[i]));
continue;
}
else {
prevPoint = points[i - 1];
nextPoint = points[i + 1];
}
}
v2Sub(v, nextPoint, prevPoint);
// use degree to scale the handle length
v2Scale(v, v, smooth);
let d0 = v2Distance(point, prevPoint);
let d1 = v2Distance(point, nextPoint);
const sum = d0 + d1;
if (sum !== 0) {
d0 /= sum;
d1 /= sum;
}
v2Scale(v1, v, -d0);
v2Scale(v2, v, d1);
const cp0 = v2Add([], point, v1);
const cp1 = v2Add([], point, v2);
if (constraint) {
v2Max(cp0, cp0, min);
v2Min(cp0, cp0, max);
v2Max(cp1, cp1, min);
v2Min(cp1, cp1, max);
}
cps.push(cp0);
cps.push(cp1);
}
if (isLoop) {
cps.push(cps.shift());
}
return cps;
}
+58
View File
@@ -0,0 +1,58 @@
/**
* Catmull-Rom spline 插值折线
*/
import {distance as v2Distance, VectorArray} from '../../core/vector';
function interpolate(
p0: number, p1: number, p2: number, p3: number, t: number, t2: number, t3: number
) {
const v0 = (p2 - p0) * 0.5;
const v1 = (p3 - p1) * 0.5;
return (2 * (p1 - p2) + v0 + v1) * t3
+ (-3 * (p1 - p2) - 2 * v0 - v1) * t2
+ v0 * t + p1;
}
export default function smoothSpline(points: VectorArray[], isLoop?: boolean): VectorArray[] {
const len = points.length;
const ret = [];
let distance = 0;
for (let i = 1; i < len; i++) {
distance += v2Distance(points[i - 1], points[i]);
}
let segs = distance / 2;
segs = segs < len ? len : segs;
for (let i = 0; i < segs; i++) {
const pos = i / (segs - 1) * (isLoop ? len : len - 1);
const idx = Math.floor(pos);
const w = pos - idx;
let p0;
let p1 = points[idx % len];
let p2;
let p3;
if (!isLoop) {
p0 = points[idx === 0 ? idx : idx - 1];
p2 = points[idx > len - 2 ? len - 1 : idx + 1];
p3 = points[idx > len - 3 ? len - 1 : idx + 2];
}
else {
p0 = points[(idx - 1 + len) % len];
p2 = points[(idx + 1) % len];
p3 = points[(idx + 2) % len];
}
const w2 = w * w;
const w3 = w * w2;
ret.push([
interpolate(p0[0], p1[0], p2[0], p3[0], w, w2, w3),
interpolate(p0[1], p1[1], p2[1], p3[1], w, w2, w3)
]);
}
return ret;
}
+134
View File
@@ -0,0 +1,134 @@
import { PathStyleProps } from '../Path';
/**
* Sub-pixel optimize for canvas rendering, prevent from blur
* when rendering a thin vertical/horizontal line.
*/
const round = Math.round;
type LineShape = {
x1: number
y1: number
x2: number
y2: number
}
type RectShape = {
x: number
y: number
width: number
height: number
r?: number | number[]
}
/**
* Sub pixel optimize line for canvas
*
* @param outputShape The modification will be performed on `outputShape`.
* `outputShape` and `inputShape` can be the same object.
* `outputShape` object can be used repeatly, because all of
* the `x1`, `x2`, `y1`, `y2` will be assigned in this method.
*/
export function subPixelOptimizeLine(
outputShape: Partial<LineShape>,
inputShape: LineShape,
style: Pick<PathStyleProps, 'lineWidth'> // DO not optimize when lineWidth is 0
): LineShape {
if (!inputShape) {
return;
}
const x1 = inputShape.x1;
const x2 = inputShape.x2;
const y1 = inputShape.y1;
const y2 = inputShape.y2;
outputShape.x1 = x1;
outputShape.x2 = x2;
outputShape.y1 = y1;
outputShape.y2 = y2;
const lineWidth = style && style.lineWidth;
if (!lineWidth) {
return outputShape as LineShape;
}
if (round(x1 * 2) === round(x2 * 2)) {
outputShape.x1 = outputShape.x2 = subPixelOptimize(x1, lineWidth, true);
}
if (round(y1 * 2) === round(y2 * 2)) {
outputShape.y1 = outputShape.y2 = subPixelOptimize(y1, lineWidth, true);
}
return outputShape as LineShape;
}
/**
* Sub pixel optimize rect for canvas
*
* @param outputShape The modification will be performed on `outputShape`.
* `outputShape` and `inputShape` can be the same object.
* `outputShape` object can be used repeatly, because all of
* the `x`, `y`, `width`, `height` will be assigned in this method.
*/
export function subPixelOptimizeRect(
outputShape: Partial<RectShape>,
inputShape: RectShape,
style: Pick<PathStyleProps, 'lineWidth'> // DO not optimize when lineWidth is 0
): RectShape {
if (!inputShape) {
return;
}
const originX = inputShape.x;
const originY = inputShape.y;
const originWidth = inputShape.width;
const originHeight = inputShape.height;
outputShape.x = originX;
outputShape.y = originY;
outputShape.width = originWidth;
outputShape.height = originHeight;
const lineWidth = style && style.lineWidth;
if (!lineWidth) {
return outputShape as RectShape;
}
outputShape.x = subPixelOptimize(originX, lineWidth, true);
outputShape.y = subPixelOptimize(originY, lineWidth, true);
outputShape.width = Math.max(
subPixelOptimize(originX + originWidth, lineWidth, false) - outputShape.x,
originWidth === 0 ? 0 : 1
);
outputShape.height = Math.max(
subPixelOptimize(originY + originHeight, lineWidth, false) - outputShape.y,
originHeight === 0 ? 0 : 1
);
return outputShape as RectShape;
}
/**
* Sub pixel optimize for canvas
*
* @param position Coordinate, such as x, y
* @param lineWidth If `null`/`undefined`/`0`, do not optimize.
* @param positiveOrNegative Default false (negative).
* @return Optimized position.
*/
export function subPixelOptimize(
position: number,
lineWidth?: number,
positiveOrNegative?: boolean
) {
if (!lineWidth) {
return position;
}
// Assure that (position + lineWidth / 2) is near integer edge,
// otherwise line will be fuzzy in canvas.
const doubledPosition = round(position * 2);
return (doubledPosition + round(lineWidth)) % 2 === 0
? doubledPosition / 2
: (doubledPosition + (positiveOrNegative ? 1 : -1)) / 2;
}
+58
View File
@@ -0,0 +1,58 @@
/**
* 圆弧
*/
import Path, { PathProps } from '../Path';
export class ArcShape {
cx = 0;
cy = 0;
r = 0;
startAngle = 0;
endAngle = Math.PI * 2
clockwise? = true
}
export interface ArcProps extends PathProps {
shape?: Partial<ArcShape>
}
class Arc extends Path<ArcProps> {
shape: ArcShape
constructor(opts?: ArcProps) {
super(opts);
}
getDefaultStyle() {
return {
stroke: '#000',
fill: null as string
};
}
getDefaultShape() {
return new ArcShape();
}
buildPath(ctx: CanvasRenderingContext2D, shape: ArcShape) {
const x = shape.cx;
const y = shape.cy;
const r = Math.max(shape.r, 0);
const startAngle = shape.startAngle;
const endAngle = shape.endAngle;
const clockwise = shape.clockwise;
const unitX = Math.cos(startAngle);
const unitY = Math.sin(startAngle);
ctx.moveTo(unitX * r + x, unitY * r + y);
ctx.arc(x, y, r, startAngle, endAngle, !clockwise);
}
}
Arc.prototype.type = 'arc';
export default Arc;
+138
View File
@@ -0,0 +1,138 @@
/**
* 贝塞尔曲线
*/
import Path, { PathProps } from '../Path';
import * as vec2 from '../../core/vector';
import {
quadraticSubdivide,
cubicSubdivide,
quadraticAt,
cubicAt,
quadraticDerivativeAt,
cubicDerivativeAt
} from '../../core/curve';
const out: number[] = [];
export class BezierCurveShape {
x1 = 0
y1 = 0
x2 = 0
y2 = 0
cpx1 = 0
cpy1 = 0
cpx2?: number
cpy2?: number
// Curve show percent, for animating
percent = 1
}
function someVectorAt(shape: BezierCurveShape, t: number, isTangent: boolean) {
const cpx2 = shape.cpx2;
const cpy2 = shape.cpy2;
if (cpx2 != null || cpy2 != null) {
return [
(isTangent ? cubicDerivativeAt : cubicAt)(shape.x1, shape.cpx1, shape.cpx2, shape.x2, t),
(isTangent ? cubicDerivativeAt : cubicAt)(shape.y1, shape.cpy1, shape.cpy2, shape.y2, t)
];
}
else {
return [
(isTangent ? quadraticDerivativeAt : quadraticAt)(shape.x1, shape.cpx1, shape.x2, t),
(isTangent ? quadraticDerivativeAt : quadraticAt)(shape.y1, shape.cpy1, shape.y2, t)
];
}
}
export interface BezierCurveProps extends PathProps {
shape?: Partial<BezierCurveShape>
}
class BezierCurve extends Path<BezierCurveProps> {
shape: BezierCurveShape
constructor(opts?: BezierCurveProps) {
super(opts);
}
getDefaultStyle() {
return {
stroke: '#000',
fill: null as string
};
}
getDefaultShape() {
return new BezierCurveShape();
}
buildPath(ctx: CanvasRenderingContext2D, shape: BezierCurveShape) {
let x1 = shape.x1;
let y1 = shape.y1;
let x2 = shape.x2;
let y2 = shape.y2;
let cpx1 = shape.cpx1;
let cpy1 = shape.cpy1;
let cpx2 = shape.cpx2;
let cpy2 = shape.cpy2;
let percent = shape.percent;
if (percent === 0) {
return;
}
ctx.moveTo(x1, y1);
if (cpx2 == null || cpy2 == null) {
if (percent < 1) {
quadraticSubdivide(x1, cpx1, x2, percent, out);
cpx1 = out[1];
x2 = out[2];
quadraticSubdivide(y1, cpy1, y2, percent, out);
cpy1 = out[1];
y2 = out[2];
}
ctx.quadraticCurveTo(
cpx1, cpy1,
x2, y2
);
}
else {
if (percent < 1) {
cubicSubdivide(x1, cpx1, cpx2, x2, percent, out);
cpx1 = out[1];
cpx2 = out[2];
x2 = out[3];
cubicSubdivide(y1, cpy1, cpy2, y2, percent, out);
cpy1 = out[1];
cpy2 = out[2];
y2 = out[3];
}
ctx.bezierCurveTo(
cpx1, cpy1,
cpx2, cpy2,
x2, y2
);
}
}
/**
* Get point at percent
*/
pointAt(t: number) {
return someVectorAt(this.shape, t, false);
}
/**
* Get tangent at percent
*/
tangentAt(t: number) {
const p = someVectorAt(this.shape, t, true);
return vec2.normalize(p, p);
}
};
BezierCurve.prototype.type = 'bezier-curve';
export default BezierCurve;
+38
View File
@@ -0,0 +1,38 @@
/**
* 圆形
*/
import Path, { PathProps } from '../Path';
export class CircleShape {
cx = 0
cy = 0
r = 0
}
export interface CircleProps extends PathProps {
shape?: Partial<CircleShape>
}
class Circle extends Path<CircleProps> {
shape: CircleShape
constructor(opts?: CircleProps) {
super(opts);
}
getDefaultShape() {
return new CircleShape();
}
buildPath(ctx: CanvasRenderingContext2D, shape: CircleShape) {
// Use moveTo to start a new sub path.
// Or it will be connected to other subpaths when in CompoundPath
ctx.moveTo(shape.cx + shape.r, shape.cy);
ctx.arc(shape.cx, shape.cy, shape.r, 0, Math.PI * 2);
}
};
Circle.prototype.type = 'circle';
export default Circle;
+58
View File
@@ -0,0 +1,58 @@
/**
* 水滴形状
*/
import Path, { PathProps } from '../Path';
export class DropletShape {
cx = 0
cy = 0
width = 0
height = 0
}
export interface DropletProps extends PathProps {
shape?: Partial<DropletShape>
}
class Droplet extends Path<DropletProps> {
shape: DropletShape
constructor(opts?: DropletProps) {
super(opts);
}
getDefaultShape() {
return new DropletShape();
}
buildPath(ctx: CanvasRenderingContext2D, shape: DropletShape) {
const x = shape.cx;
const y = shape.cy;
const a = shape.width;
const b = shape.height;
ctx.moveTo(x, y + a);
ctx.bezierCurveTo(
x + a,
y + a,
x + a * 3 / 2,
y - a / 3,
x,
y - b
);
ctx.bezierCurveTo(
x - a * 3 / 2,
y - a / 3,
x - a,
y + a,
x,
y + a
);
ctx.closePath();
}
}
Droplet.prototype.type = 'droplet';
export default Droplet;
+49
View File
@@ -0,0 +1,49 @@
/**
* 椭圆形状
*/
import Path, { PathProps } from '../Path';
export class EllipseShape {
cx = 0
cy = 0
rx = 0
ry = 0
}
export interface EllipseProps extends PathProps {
shape?: Partial<EllipseShape>
}
class Ellipse extends Path<EllipseProps> {
shape: EllipseShape
constructor(opts?: EllipseProps) {
super(opts);
}
getDefaultShape() {
return new EllipseShape();
}
buildPath(ctx: CanvasRenderingContext2D, shape: EllipseShape) {
const k = 0.5522848;
const x = shape.cx;
const y = shape.cy;
const a = shape.rx;
const b = shape.ry;
const ox = a * k; // 水平控制点偏移量
const oy = b * k; // 垂直控制点偏移量
// 从椭圆的左端点开始顺时针绘制四条三次贝塞尔曲线
ctx.moveTo(x - a, y);
ctx.bezierCurveTo(x - a, y - oy, x - ox, y - b, x, y - b);
ctx.bezierCurveTo(x + ox, y - b, x + a, y - oy, x + a, y);
ctx.bezierCurveTo(x + a, y + oy, x + ox, y + b, x, y + b);
ctx.bezierCurveTo(x - ox, y + b, x - a, y + oy, x - a, y);
ctx.closePath();
}
}
Ellipse.prototype.type = 'ellipse';
export default Ellipse;
+51
View File
@@ -0,0 +1,51 @@
/**
* 心形
*/
import Path, { PathProps } from '../Path';
export class HeartShape {
cx = 0
cy = 0
width = 0
height = 0
}
export interface HeartProps extends PathProps {
shape?: Partial<HeartShape>
}
class Heart extends Path<HeartProps> {
shape: HeartShape
constructor(opts?: HeartProps) {
super(opts);
}
getDefaultShape() {
return new HeartShape();
}
buildPath(ctx: CanvasRenderingContext2D, shape: HeartShape) {
const x = shape.cx;
const y = shape.cy;
const a = shape.width;
const b = shape.height;
ctx.moveTo(x, y);
ctx.bezierCurveTo(
x + a / 2, y - b * 2 / 3,
x + a * 2, y + b / 3,
x, y + b
);
ctx.bezierCurveTo(
x - a * 2, y + b / 3,
x - a / 2, y - b * 2 / 3,
x, y
);
}
}
Heart.prototype.type = 'heart';
export default Heart;
+60
View File
@@ -0,0 +1,60 @@
/**
* 正多边形
*/
import Path, { PathProps } from '../Path';
const PI = Math.PI;
const sin = Math.sin;
const cos = Math.cos;
export class IsogonShape {
x = 0
y = 0
r = 0
n = 0
}
export interface IsogonProps extends PathProps {
shape?: Partial<IsogonShape>
}
class Isogon extends Path<IsogonProps> {
shape: IsogonShape
constructor(opts?: IsogonProps) {
super(opts);
}
getDefaultShape() {
return new IsogonShape();
}
buildPath(ctx: CanvasRenderingContext2D, shape: IsogonShape) {
const n = shape.n;
if (!n || n < 2) {
return;
}
const x = shape.x;
const y = shape.y;
const r = shape.r;
const dStep = 2 * PI / n;
let deg = -PI / 2;
ctx.moveTo(x + r * cos(deg), y + r * sin(deg));
for (let i = 0, end = n - 1; i < end; i++) {
deg += dStep;
ctx.lineTo(x + r * cos(deg), y + r * sin(deg));
}
ctx.closePath();
return;
}
}
Isogon.prototype.type = 'isogon';
export default Isogon;
+96
View File
@@ -0,0 +1,96 @@
/**
* 直线
* @module zrender/graphic/shape/Line
*/
import Path, { PathProps } from '../Path';
import {subPixelOptimizeLine} from '../helper/subPixelOptimize';
import { VectorArray } from '../../core/vector';
// Avoid create repeatly.
const subPixelOptimizeOutputShape = {};
export class LineShape {
// Start point
x1 = 0
y1 = 0
// End point
x2 = 0
y2 = 0
percent = 1
}
export interface LineProps extends PathProps {
shape?: Partial<LineShape>
}
class Line extends Path<LineProps> {
shape: LineShape
constructor(opts?: LineProps) {
super(opts);
}
getDefaultStyle() {
return {
stroke: '#000',
fill: null as string
};
}
getDefaultShape() {
return new LineShape();
}
buildPath(ctx: CanvasRenderingContext2D, shape: LineShape) {
let x1;
let y1;
let x2;
let y2;
if (this.subPixelOptimize) {
const optimizedShape = subPixelOptimizeLine(
subPixelOptimizeOutputShape, shape, this.style
);
x1 = optimizedShape.x1;
y1 = optimizedShape.y1;
x2 = optimizedShape.x2;
y2 = optimizedShape.y2;
}
else {
x1 = shape.x1;
y1 = shape.y1;
x2 = shape.x2;
y2 = shape.y2;
}
const percent = shape.percent;
if (percent === 0) {
return;
}
ctx.moveTo(x1, y1);
if (percent < 1) {
x2 = x1 * (1 - percent) + x2 * percent;
y2 = y1 * (1 - percent) + y2 * percent;
}
ctx.lineTo(x2, y2);
}
/**
* Get point at percent
*/
pointAt(p: number): VectorArray {
const shape = this.shape;
return [
shape.x1 * (1 - p) + shape.x2 * p,
shape.y1 * (1 - p) + shape.y2 * p
];
}
}
Line.prototype.type = 'line';
export default Line;
+38
View File
@@ -0,0 +1,38 @@
/**
* 多边形
* @module zrender/shape/Polygon
*/
import Path, { PathProps } from '../Path';
import * as polyHelper from '../helper/poly';
import { VectorArray } from '../../core/vector';
export class PolygonShape {
points: VectorArray[] = null
smooth?: number = 0
smoothConstraint?: VectorArray[] = null
}
export interface PolygonProps extends PathProps {
shape?: Partial<PolygonShape>
}
class Polygon extends Path<PolygonProps> {
shape: PolygonShape
constructor(opts?: PolygonProps) {
super(opts);
}
getDefaultShape() {
return new PolygonShape();
}
buildPath(ctx: CanvasRenderingContext2D, shape: PolygonShape) {
polyHelper.buildPath(ctx, shape, true);
}
};
Polygon.prototype.type = 'polygon';
export default Polygon;
+45
View File
@@ -0,0 +1,45 @@
/**
* @module zrender/graphic/shape/Polyline
*/
import Path, { PathProps } from '../Path';
import * as polyHelper from '../helper/poly';
import { VectorArray } from '../../core/vector';
export class PolylineShape {
points: VectorArray[] = null
// Percent of displayed polyline. For animating purpose
percent?: number = 1
smooth?: number = 0
smoothConstraint?: VectorArray[] = null
}
export interface PolylineProps extends PathProps {
shape?: Partial<PolylineShape>
}
class Polyline extends Path<PolylineProps> {
shape: PolylineShape
constructor(opts?: PolylineProps) {
super(opts);
}
getDefaultStyle() {
return {
stroke: '#000',
fill: null as string
};
}
getDefaultShape() {
return new PolylineShape();
}
buildPath(ctx: CanvasRenderingContext2D, shape: PolylineShape) {
polyHelper.buildPath(ctx, shape, false);
}
}
Polyline.prototype.type = 'polyline';
export default Polyline;
+79
View File
@@ -0,0 +1,79 @@
/**
* 矩形
* @module zrender/graphic/shape/Rect
*/
import Path, { PathProps } from '../Path';
import * as roundRectHelper from '../helper/roundRect';
import {subPixelOptimizeRect} from '../helper/subPixelOptimize';
export class RectShape {
// 左上、右上、右下、左下角的半径依次为r1、r2、r3、r4
// r缩写为1 相当于 [1, 1, 1, 1]
// r缩写为[1] 相当于 [1, 1, 1, 1]
// r缩写为[1, 2] 相当于 [1, 2, 1, 2]
// r缩写为[1, 2, 3] 相当于 [1, 2, 3, 2]
r?: number | number[]
x = 0
y = 0
width = 0
height = 0
}
export interface RectProps extends PathProps {
shape?: Partial<RectShape>
}
// Avoid create repeatly.
const subPixelOptimizeOutputShape = {};
class Rect extends Path<RectProps> {
shape: RectShape
constructor(opts?: RectProps) {
super(opts);
}
getDefaultShape() {
return new RectShape();
}
buildPath(ctx: CanvasRenderingContext2D, shape: RectShape) {
let x: number;
let y: number;
let width: number;
let height: number;
if (this.subPixelOptimize) {
const optimizedShape = subPixelOptimizeRect(subPixelOptimizeOutputShape, shape, this.style);
x = optimizedShape.x;
y = optimizedShape.y;
width = optimizedShape.width;
height = optimizedShape.height;
optimizedShape.r = shape.r;
shape = optimizedShape;
}
else {
x = shape.x;
y = shape.y;
width = shape.width;
height = shape.height;
}
if (!shape.r) {
ctx.rect(x, y, width, height);
}
else {
roundRectHelper.buildPath(ctx, shape);
}
}
isZeroArea() {
return !this.shape.width || !this.shape.height;
}
}
Rect.prototype.type = 'rect';
export default Rect;
+41
View File
@@ -0,0 +1,41 @@
/**
* 圆环
*/
import Path, { PathProps } from '../Path';
export class RingShape {
cx = 0
cy = 0
r = 0
r0 = 0
}
export interface RingProps extends PathProps {
shape?: Partial<RingShape>
}
class Ring extends Path<RingProps> {
shape: RingShape
constructor(opts?: RingProps) {
super(opts);
}
getDefaultShape() {
return new RingShape();
}
buildPath(ctx: CanvasRenderingContext2D, shape: RingShape) {
const x = shape.cx;
const y = shape.cy;
const PI2 = Math.PI * 2;
ctx.moveTo(x + shape.r, y);
ctx.arc(x, y, shape.r, 0, PI2, false);
ctx.moveTo(x + shape.r0, y);
ctx.arc(x, y, shape.r0, 0, PI2, true);
}
}
Ring.prototype.type = 'ring';
export default Ring;
+76
View File
@@ -0,0 +1,76 @@
/**
* 玫瑰线
* @module zrender/graphic/shape/Rose
*/
import Path, { PathProps } from '../Path';
const sin = Math.sin;
const cos = Math.cos;
const radian = Math.PI / 180;
export class RoseShape {
cx = 0
cy = 0
r: number[] = []
k = 0
n = 1
}
export interface RoseProps extends PathProps {
shape?: Partial<RoseShape>
}
class Rose extends Path<RoseProps> {
shape: RoseShape
constructor(opts?: RoseProps) {
super(opts);
}
getDefaultStyle() {
return {
stroke: '#000',
fill: null as string
};
}
getDefaultShape() {
return new RoseShape();
}
buildPath(ctx: CanvasRenderingContext2D, shape: RoseShape) {
const R = shape.r;
const k = shape.k;
const n = shape.n;
const x0 = shape.cx;
const y0 = shape.cy;
let x;
let y;
let r;
ctx.moveTo(x0, y0);
for (let i = 0, len = R.length; i < len; i++) {
r = R[i];
for (let j = 0; j <= 360 * n; j++) {
x = r
* sin(k / n * j % 360 * radian)
* cos(j * radian)
+ x0;
y = r
* sin(k / n * j % 360 * radian)
* sin(j * radian)
+ y0;
ctx.lineTo(x, y);
}
}
}
}
Rose.prototype.type = 'rose';
export default Rose;
+56
View File
@@ -0,0 +1,56 @@
import Path, { PathProps } from '../Path';
import * as roundSectorHelper from '../helper/roundSector';
export class SectorShape {
cx = 0
cy = 0
r0 = 0
r = 0
startAngle = 0
endAngle = Math.PI * 2
clockwise = true
/**
* Corner radius of sector
*
* clockwise, from inside to outside, four corners are
* inner start -> inner end
* outer start -> outer end
*
* 5 => [5, 5, 5, 5]
* [5] => [5, 5, 0, 0]
* [5, 10] => [5, 5, 10, 10]
* [5, 10, 15] => [5, 10, 15, 15]
* [5, 10, 15, 20] => [5, 10, 15, 20]
*/
cornerRadius: number | number[] = 0
}
export interface SectorProps extends PathProps {
shape?: Partial<SectorShape>
}
class Sector extends Path<SectorProps> {
shape: SectorShape
constructor(opts?: SectorProps) {
super(opts);
}
getDefaultShape() {
return new SectorShape();
}
buildPath(ctx: CanvasRenderingContext2D, shape: SectorShape) {
roundSectorHelper.buildPath(ctx, shape);
}
isZeroArea() {
return this.shape.startAngle === this.shape.endAngle
|| this.shape.r === this.shape.r0;
}
}
Sector.prototype.type = 'sector';
export default Sector;
+76
View File
@@ -0,0 +1,76 @@
/**
* n角星(n>3
* @module zrender/graphic/shape/Star
*/
import Path, { PathProps } from '../Path';
const PI = Math.PI;
const cos = Math.cos;
const sin = Math.sin;
export class StarShape {
cx = 0
cy = 0
n = 3
r0: number
r = 0
}
export interface StarProps extends PathProps {
shape?: Partial<StarShape>
}
class Star extends Path<StarProps> {
shape: StarShape
constructor(opts?: StarProps) {
super(opts);
}
getDefaultShape() {
return new StarShape();
}
buildPath(ctx: CanvasRenderingContext2D, shape: StarShape) {
const n = shape.n;
if (!n || n < 2) {
return;
}
const x = shape.cx;
const y = shape.cy;
const r = shape.r;
let r0 = shape.r0;
// 如果未指定内部顶点外接圆半径,则自动计算
if (r0 == null) {
r0 = n > 4
// 相隔的外部顶点的连线的交点,
// 被取为内部交点,以此计算r0
? r * cos(2 * PI / n) / cos(PI / n)
// 二三四角星的特殊处理
: r / 3;
}
const dStep = PI / n;
let deg = -PI / 2;
const xStart = x + r * cos(deg);
const yStart = y + r * sin(deg);
deg += dStep;
// 记录边界点,用于判断inside
ctx.moveTo(xStart, yStart);
for (let i = 0, end = n * 2 - 1, ri; i < end; i++) {
ri = i % 2 === 0 ? r0 : r;
ctx.lineTo(x + ri * cos(deg), y + ri * sin(deg));
deg += dStep;
}
ctx.closePath();
}
}
Star.prototype.type = 'star';
export default Star;
+92
View File
@@ -0,0 +1,92 @@
/**
* 内外旋轮曲线
* @module zrender/graphic/shape/Trochold
*/
import Path, { PathProps } from '../Path';
const cos = Math.cos;
const sin = Math.sin;
export class TrochoidShape {
cx = 0
cy = 0
r = 0
r0 = 0
d = 0
location = 'out'
}
export interface TrochoidProps extends PathProps {
shape?: Partial<TrochoidShape>
}
class Trochoid extends Path<TrochoidProps> {
shape: TrochoidShape
constructor(opts?: TrochoidProps) {
super(opts);
}
getDefaultStyle() {
return {
stroke: '#000',
fill: null as string
};
}
getDefaultShape() {
return new TrochoidShape();
}
buildPath(ctx: CanvasRenderingContext2D, shape: TrochoidShape) {
const R = shape.r;
const r = shape.r0;
const d = shape.d;
const offsetX = shape.cx;
const offsetY = shape.cy;
const delta = shape.location === 'out' ? 1 : -1;
let x1;
let y1;
let x2;
let y2;
if (shape.location && R <= r) {
return;
}
let num = 0;
let i = 1;
let theta;
x1 = (R + delta * r) * cos(0)
- delta * d * cos(0) + offsetX;
y1 = (R + delta * r) * sin(0)
- d * sin(0) + offsetY;
ctx.moveTo(x1, y1);
// 计算结束时的i
do {
num++;
}
while ((r * num) % (R + delta * r) !== 0);
do {
theta = Math.PI / 180 * i;
x2 = (R + delta * r) * cos(theta)
- delta * d * cos((R / r + delta) * theta)
+ offsetX;
y2 = (R + delta * r) * sin(theta)
- d * sin((R / r + delta) * theta)
+ offsetY;
ctx.lineTo(x2, y2);
i++;
}
while (i <= (r * num) / (R + delta * r) * 360);
}
}
Trochoid.prototype.type = 'trochoid';
export default Trochoid;
+118
View File
@@ -0,0 +1,118 @@
import Handler from '../Handler';
import Element, { ElementEvent } from '../Element';
import Displayable from '../graphic/Displayable';
class Param {
target: Element
topTarget: Element
constructor(target: Element, e?: ElementEvent) {
this.target = target;
this.topTarget = e && e.topTarget;
}
}
// FIXME Draggable on element which has parent rotation or scale
export default class Draggable {
handler: Handler
_draggingTarget: Element
_dropTarget: Element
_x: number
_y: number
constructor(handler: Handler) {
this.handler = handler;
handler.on('mousedown', this._dragStart, this);
handler.on('mousemove', this._drag, this);
handler.on('mouseup', this._dragEnd, this);
// `mosuemove` and `mouseup` can be continue to fire when dragging.
// See [DRAG_OUTSIDE] in `Handler.js`. So we do not need to trigger
// `_dragEnd` when globalout. That would brings better user experience.
// this.on('globalout', this._dragEnd, this);
// this._dropTarget = null;
// this._draggingTarget = null;
// this._x = 0;
// this._y = 0;
}
_dragStart(e: ElementEvent) {
let draggingTarget = e.target;
// Find if there is draggable in the ancestor
while (draggingTarget && !draggingTarget.draggable) {
draggingTarget = draggingTarget.parent || draggingTarget.__hostTarget;
}
if (draggingTarget) {
this._draggingTarget = draggingTarget;
draggingTarget.dragging = true;
this._x = e.offsetX;
this._y = e.offsetY;
this.handler.dispatchToElement(
new Param(draggingTarget, e), 'dragstart', e.event
);
}
}
_drag(e: ElementEvent) {
const draggingTarget = this._draggingTarget;
if (draggingTarget) {
const x = e.offsetX;
const y = e.offsetY;
const dx = x - this._x;
const dy = y - this._y;
this._x = x;
this._y = y;
draggingTarget.drift(dx, dy, e);
this.handler.dispatchToElement(
new Param(draggingTarget, e), 'drag', e.event
);
const dropTarget = this.handler.findHover(
x, y, draggingTarget as Displayable // PENDING
).target;
const lastDropTarget = this._dropTarget;
this._dropTarget = dropTarget;
if (draggingTarget !== dropTarget) {
if (lastDropTarget && dropTarget !== lastDropTarget) {
this.handler.dispatchToElement(
new Param(lastDropTarget, e), 'dragleave', e.event
);
}
if (dropTarget && dropTarget !== lastDropTarget) {
this.handler.dispatchToElement(
new Param(dropTarget, e), 'dragenter', e.event
);
}
}
}
}
_dragEnd(e: ElementEvent) {
const draggingTarget = this._draggingTarget;
if (draggingTarget) {
draggingTarget.dragging = false;
}
this.handler.dispatchToElement(new Param(draggingTarget, e), 'dragend', e.event);
if (this._dropTarget) {
this.handler.dispatchToElement(new Param(this._dropTarget, e), 'drop', e.event);
}
this._draggingTarget = null;
this._dropTarget = null;
}
}
+421
View File
@@ -0,0 +1,421 @@
/**
* SVG Painter
*/
import {createElement, SVGNS, XLINKNS, XMLNS} from '../svg/core';
import { normalizeColor } from '../svg/helper';
import * as util from '../core/util';
import Path from '../graphic/Path';
import ZRImage from '../graphic/Image';
import TSpan from '../graphic/TSpan';
import arrayDiff from '../core/arrayDiff';
import GradientManager from './helper/GradientManager';
import PatternManager from './helper/PatternManager';
import ClippathManager, {hasClipPath} from './helper/ClippathManager';
import ShadowManager from './helper/ShadowManager';
import {
path as svgPath,
image as svgImage,
text as svgText,
SVGProxy
} from './graphic';
import Displayable from '../graphic/Displayable';
import Storage from '../Storage';
import { PainterBase } from '../PainterBase';
import { getSize } from '../canvas/helper';
function getSvgProxy(el: Displayable) {
if (el instanceof Path) {
return svgPath;
}
else if (el instanceof ZRImage) {
return svgImage;
}
else if (el instanceof TSpan) {
return svgText;
}
else {
return svgPath;
}
}
function checkParentAvailable(parent: SVGElement, child: SVGElement) {
return child && parent && child.parentNode !== parent;
}
function insertAfter(parent: SVGElement, child: SVGElement, prevSibling: SVGElement) {
if (checkParentAvailable(parent, child) && prevSibling) {
const nextSibling = prevSibling.nextSibling;
nextSibling ? parent.insertBefore(child, nextSibling)
: parent.appendChild(child);
}
}
function prepend(parent: SVGElement, child: SVGElement) {
if (checkParentAvailable(parent, child)) {
const firstChild = parent.firstChild;
firstChild ? parent.insertBefore(child, firstChild)
: parent.appendChild(child);
}
}
function remove(parent: SVGElement, child: SVGElement) {
if (child && parent && child.parentNode === parent) {
parent.removeChild(child);
}
}
function removeFromMyParent(child: SVGElement) {
if (child && child.parentNode) {
child.parentNode.removeChild(child);
}
}
function getSvgElement(displayable: Displayable) {
return displayable.__svgEl;
}
interface SVGPainterOption {
width?: number | string
height?: number | string
}
class SVGPainter implements PainterBase {
type = 'svg'
root: HTMLElement
storage: Storage
private _opts: SVGPainterOption
private _svgDom: SVGElement
private _svgRoot: SVGGElement
private _backgroundRoot: SVGGElement
private _backgroundNode: SVGRectElement
private _gradientManager: GradientManager
private _patternManager: PatternManager
private _clipPathManager: ClippathManager
private _shadowManager: ShadowManager
private _viewport: HTMLDivElement
private _visibleList: Displayable[]
private _width: number
private _height: number
constructor(root: HTMLElement, storage: Storage, opts: SVGPainterOption, zrId: number) {
this.root = root;
this.storage = storage;
this._opts = opts = util.extend({}, opts || {});
const svgDom = createElement('svg');
svgDom.setAttributeNS(XMLNS, 'xmlns', SVGNS);
svgDom.setAttributeNS(XMLNS, 'xmlns:xlink', XLINKNS);
svgDom.setAttribute('version', '1.1');
svgDom.setAttribute('baseProfile', 'full');
svgDom.style.cssText = 'user-select:none;position:absolute;left:0;top:0;';
const bgRoot = createElement('g') as SVGGElement;
svgDom.appendChild(bgRoot);
const svgRoot = createElement('g') as SVGGElement;
svgDom.appendChild(svgRoot);
this._gradientManager = new GradientManager(zrId, svgRoot);
this._patternManager = new PatternManager(zrId, svgRoot);
this._clipPathManager = new ClippathManager(zrId, svgRoot);
this._shadowManager = new ShadowManager(zrId, svgRoot);
const viewport = document.createElement('div');
viewport.style.cssText = 'overflow:hidden;position:relative';
this._svgDom = svgDom;
this._svgRoot = svgRoot;
this._backgroundRoot = bgRoot;
this._viewport = viewport;
root.appendChild(viewport);
viewport.appendChild(svgDom);
this.resize(opts.width, opts.height);
this._visibleList = [];
}
getType() {
return 'svg';
}
getViewportRoot() {
return this._viewport;
}
getSvgDom() {
return this._svgDom;
}
getSvgRoot() {
return this._svgRoot;
}
getViewportRootOffset() {
const viewportRoot = this.getViewportRoot();
if (viewportRoot) {
return {
offsetLeft: viewportRoot.offsetLeft || 0,
offsetTop: viewportRoot.offsetTop || 0
};
}
}
refresh() {
const list = this.storage.getDisplayList(true);
this._paintList(list);
}
setBackgroundColor(backgroundColor: string) {
// TODO gradient
// Insert a bg rect instead of setting background to viewport.
// Otherwise, the exported SVG don't have background.
if (this._backgroundRoot && this._backgroundNode) {
this._backgroundRoot.removeChild(this._backgroundNode);
}
const bgNode = createElement('rect') as SVGRectElement;
bgNode.setAttribute('width', this.getWidth() as any);
bgNode.setAttribute('height', this.getHeight() as any);
bgNode.setAttribute('x', 0 as any);
bgNode.setAttribute('y', 0 as any);
bgNode.setAttribute('id', 0 as any);
const { color, opacity } = normalizeColor(backgroundColor);
bgNode.setAttribute('fill', color);
bgNode.setAttribute('fill-opacity', opacity as any);
this._backgroundRoot.appendChild(bgNode);
this._backgroundNode = bgNode;
}
createSVGElement(tag: string): SVGElement {
return createElement(tag);
}
paintOne(el: Displayable): SVGElement {
const svgProxy = getSvgProxy(el);
svgProxy && (svgProxy as SVGProxy<Displayable>).brush(el);
return getSvgElement(el);
}
_paintList(list: Displayable[]) {
const gradientManager = this._gradientManager;
const patternManager = this._patternManager;
const clipPathManager = this._clipPathManager;
const shadowManager = this._shadowManager;
gradientManager.markAllUnused();
patternManager.markAllUnused();
clipPathManager.markAllUnused();
shadowManager.markAllUnused();
const svgRoot = this._svgRoot;
const visibleList = this._visibleList;
const listLen = list.length;
const newVisibleList = [];
for (let i = 0; i < listLen; i++) {
const displayable = list[i];
const svgProxy = getSvgProxy(displayable);
let svgElement = getSvgElement(displayable);
if (!displayable.invisible) {
if (displayable.__dirty || !svgElement) {
svgProxy && (svgProxy as SVGProxy<Displayable>).brush(displayable);
svgElement = getSvgElement(displayable);
// Update gradient and shadow
if (svgElement && displayable.style) {
gradientManager.update(displayable.style.fill);
gradientManager.update(displayable.style.stroke);
patternManager.update(displayable.style.fill);
patternManager.update(displayable.style.stroke);
shadowManager.update(svgElement, displayable);
}
displayable.__dirty = 0;
}
// May have optimizations and ignore brush(like empty string in TSpan)
if (svgElement) {
newVisibleList.push(displayable);
}
}
}
const diff = arrayDiff(visibleList, newVisibleList);
let prevSvgElement;
let topPrevSvgElement;
// NOTE: First do remove, in case element moved to the head and do remove
// after add
for (let i = 0; i < diff.length; i++) {
const item = diff[i];
if (item.removed) {
for (let k = 0; k < item.count; k++) {
const displayable = visibleList[item.indices[k]];
const svgElement = getSvgElement(displayable);
hasClipPath(displayable) ? removeFromMyParent(svgElement)
: remove(svgRoot, svgElement);
}
}
}
let prevDisplayable;
let currentClipGroup;
for (let i = 0; i < diff.length; i++) {
const item = diff[i];
// const isAdd = item.added;
if (item.removed) {
continue;
}
for (let k = 0; k < item.count; k++) {
const displayable = newVisibleList[item.indices[k]];
// Update clipPath
const clipGroup = clipPathManager.update(displayable, prevDisplayable);
if (clipGroup !== currentClipGroup) {
// First pop to top level.
prevSvgElement = topPrevSvgElement;
if (clipGroup) {
// Enter second level of clipping group.
prevSvgElement ? insertAfter(svgRoot, clipGroup, prevSvgElement)
: prepend(svgRoot, clipGroup);
topPrevSvgElement = clipGroup;
// Reset prevSvgElement in second level.
prevSvgElement = null;
}
currentClipGroup = clipGroup;
}
const svgElement = getSvgElement(displayable);
// if (isAdd) {
prevSvgElement
? insertAfter(currentClipGroup || svgRoot, svgElement, prevSvgElement)
: prepend(currentClipGroup || svgRoot, svgElement);
// }
prevSvgElement = svgElement || prevSvgElement;
if (!currentClipGroup) {
topPrevSvgElement = prevSvgElement;
}
gradientManager.markUsed(displayable);
gradientManager.addWithoutUpdate(svgElement, displayable);
patternManager.markUsed(displayable);
patternManager.addWithoutUpdate(svgElement, displayable);
clipPathManager.markUsed(displayable);
prevDisplayable = displayable;
}
}
gradientManager.removeUnused();
patternManager.removeUnused();
clipPathManager.removeUnused();
shadowManager.removeUnused();
this._visibleList = newVisibleList;
}
resize(width: number | string, height: number | string) {
const viewport = this._viewport;
// FIXME Why ?
viewport.style.display = 'none';
// Save input w/h
const opts = this._opts;
width != null && (opts.width = width);
height != null && (opts.height = height);
width = getSize(this.root, 0, opts);
height = getSize(this.root, 1, opts);
viewport.style.display = '';
if (this._width !== width || this._height !== height) {
this._width = width;
this._height = height;
const viewportStyle = viewport.style;
viewportStyle.width = width + 'px';
viewportStyle.height = height + 'px';
const svgRoot = this._svgDom;
// Set width by 'svgRoot.width = width' is invalid
svgRoot.setAttribute('width', width + '');
svgRoot.setAttribute('height', height + '');
}
if (this._backgroundNode) {
this._backgroundNode.setAttribute('width', width as any);
this._backgroundNode.setAttribute('height', height as any);
}
}
/**
* 获取绘图区域宽度
*/
getWidth() {
return this._width;
}
/**
* 获取绘图区域高度
*/
getHeight() {
return this._height;
}
dispose() {
this.root.innerHTML = '';
this._svgRoot =
this._backgroundRoot =
this._svgDom =
this._backgroundNode =
this._viewport = this.storage = null;
}
clear() {
const viewportNode = this._viewport;
if (viewportNode && viewportNode.parentNode) {
viewportNode.parentNode.removeChild(viewportNode);
}
}
toDataURL() {
this.refresh();
const svgDom = this._svgDom;
const outerHTML = svgDom.outerHTML
// outerHTML of `svg` tag is not supported in IE, use `parentNode.innerHTML` instead
// PENDING: Or use `new XMLSerializer().serializeToString(svg)`?
|| (svgDom.parentNode && (svgDom.parentNode as HTMLElement).innerHTML);
const html = encodeURIComponent(outerHTML.replace(/></g, '>\n\r<'));
return 'data:image/svg+xml;charset=UTF-8,' + html;
}
refreshHover = createMethodNotSupport('refreshHover') as PainterBase['refreshHover'];
configLayer = createMethodNotSupport('configLayer') as PainterBase['configLayer'];
}
// Not supported methods
function createMethodNotSupport(method: string): any {
return function () {
if (process.env.NODE_ENV !== 'production') {
util.logError('In SVG mode painter not support method "' + method + '"');
}
};
}
export default SVGPainter;
+194
View File
@@ -0,0 +1,194 @@
// TODO
// 1. shadow
// 2. Image: sx, sy, sw, sh
import {createElement, XLINKNS } from '../svg/core';
import { getMatrixStr, TEXT_ALIGN_TO_ANCHOR, adjustTextY } from '../svg/helper';
import * as matrix from '../core/matrix';
import Path, { PathStyleProps } from '../graphic/Path';
import ZRImage, { ImageStyleProps } from '../graphic/Image';
import { getLineHeight } from '../contain/text';
import TSpan, { TSpanStyleProps } from '../graphic/TSpan';
import SVGPathRebuilder from '../svg/SVGPathRebuilder';
import mapStyleToAttrs from '../svg/mapStyleToAttrs';
import { DEFAULT_FONT } from '../core/platform';
export interface SVGProxy<T> {
brush(el: T): void
}
type AllStyleOption = PathStyleProps | TSpanStyleProps | ImageStyleProps;
function setTransform(svgEl: SVGElement, m: matrix.MatrixArray) {
if (m) {
attr(svgEl, 'transform', getMatrixStr(m));
}
}
function attr(el: SVGElement, key: string, val: string | number) {
if (!val || (val as any).type !== 'linear' && (val as any).type !== 'radial') {
// Don't set attribute for gradient, since it need new dom nodes
el.setAttribute(key, val as any);
}
}
function attrXLink(el: SVGElement, key: string, val: string) {
el.setAttributeNS(XLINKNS, key, val);
}
function attrXML(el: SVGElement, key: string, val: string) {
el.setAttributeNS('http://www.w3.org/XML/1998/namespace', key, val);
}
function bindStyle(svgEl: SVGElement, style: PathStyleProps, el?: Path): void
function bindStyle(svgEl: SVGElement, style: TSpanStyleProps, el?: TSpan): void
function bindStyle(svgEl: SVGElement, style: ImageStyleProps, el?: ZRImage): void
function bindStyle(svgEl: SVGElement, style: AllStyleOption, el?: Path | TSpan | ZRImage) {
mapStyleToAttrs((key, val) => attr(svgEl, key, val), style, el, true);
}
interface PathWithSVGBuildPath extends Path {
__svgPathVersion: number
__svgPathBuilder: SVGPathRebuilder
}
const svgPath: SVGProxy<Path> = {
brush(el: Path) {
const style = el.style;
let svgEl = el.__svgEl;
if (!svgEl) {
svgEl = createElement('path');
el.__svgEl = svgEl;
}
if (!el.path) {
el.createPathProxy();
}
const path = el.path;
if (el.shapeChanged()) {
path.beginPath();
el.buildPath(path, el.shape);
el.pathUpdated();
}
const pathVersion = path.getVersion();
const elExt = el as PathWithSVGBuildPath;
let svgPathBuilder = elExt.__svgPathBuilder;
if (elExt.__svgPathVersion !== pathVersion || !svgPathBuilder || el.style.strokePercent < 1) {
if (!svgPathBuilder) {
svgPathBuilder = elExt.__svgPathBuilder = new SVGPathRebuilder();
}
svgPathBuilder.reset();
path.rebuildPath(svgPathBuilder, el.style.strokePercent);
svgPathBuilder.generateStr();
elExt.__svgPathVersion = pathVersion;
}
attr(svgEl, 'd', svgPathBuilder.getStr());
bindStyle(svgEl, style, el);
setTransform(svgEl, el.transform);
}
};
export {svgPath as path};
/***************************************************
* IMAGE
**************************************************/
const svgImage: SVGProxy<ZRImage> = {
brush(el: ZRImage) {
const style = el.style;
let image = style.image;
if (image instanceof HTMLImageElement) {
image = image.src;
}
// heatmap layer in geo may be a canvas
else if (image instanceof HTMLCanvasElement) {
image = image.toDataURL();
}
if (!image) {
return;
}
const x = style.x || 0;
const y = style.y || 0;
const dw = style.width;
const dh = style.height;
let svgEl = el.__svgEl;
if (!svgEl) {
svgEl = createElement('image');
el.__svgEl = svgEl;
}
if (image !== el.__imageSrc) {
attrXLink(svgEl, 'href', image as string);
// Caching image src
el.__imageSrc = image as string;
}
attr(svgEl, 'width', dw + '');
attr(svgEl, 'height', dh + '');
attr(svgEl, 'x', x + '');
attr(svgEl, 'y', y + '');
bindStyle(svgEl, style, el);
setTransform(svgEl, el.transform);
}
};
export {svgImage as image};
/***************************************************
* TEXT
**************************************************/
const svgText: SVGProxy<TSpan> = {
brush(el: TSpan) {
const style = el.style;
let text = style.text;
// Convert to string
text != null && (text += '');
if (!text || isNaN(style.x) || isNaN(style.y)) {
return;
}
let textSvgEl = el.__svgEl as SVGTextElement;
if (!textSvgEl) {
textSvgEl = createElement('text') as SVGTextElement;
attrXML(textSvgEl, 'xml:space', 'preserve');
el.__svgEl = textSvgEl;
}
const font = style.font || DEFAULT_FONT;
// style.font has been normalized by `normalizeTextStyle`.
const textSvgElStyle = textSvgEl.style;
textSvgElStyle.font = font;
textSvgEl.textContent = text;
bindStyle(textSvgEl, style, el);
setTransform(textSvgEl, el.transform);
// Consider different font display differently in vertial align, we always
// set vertialAlign as 'middle', and use 'y' to locate text vertically.
const x = style.x || 0;
const y = adjustTextY(style.y || 0, getLineHeight(font), style.textBaseline);
const textAlign = TEXT_ALIGN_TO_ANCHOR[style.textAlign as keyof typeof TEXT_ALIGN_TO_ANCHOR]
|| style.textAlign;
attr(textSvgEl, 'dominant-baseline', 'central');
attr(textSvgEl, 'text-anchor', textAlign);
attr(textSvgEl, 'x', x + '');
attr(textSvgEl, 'y', y + '');
}
};
export {svgText as text};
+173
View File
@@ -0,0 +1,173 @@
/**
* @file Manages SVG clipPath elements.
* @author Zhang Wenli
*/
import Definable from './Definable';
import * as zrUtil from '../../core/util';
import Displayable from '../../graphic/Displayable';
import Path from '../../graphic/Path';
import {path} from '../graphic';
import { Dictionary } from '../../core/types';
import { isClipPathChanged } from '../../canvas/helper';
import { getClipPathsKey, getIdURL } from '../../svg/helper';
import { createElement } from '../../svg/core';
type PathExtended = Path & {
_dom: SVGElement
}
export function hasClipPath(displayable: Displayable) {
const clipPaths = displayable.__clipPaths;
return clipPaths && clipPaths.length > 0;
}
/**
* Manages SVG clipPath elements.
*/
export default class ClippathManager extends Definable {
private _refGroups: Dictionary<SVGElement> = {};
private _keyDuplicateCount: Dictionary<number> = {};
constructor(zrId: number, svgRoot: SVGElement) {
super(zrId, svgRoot, 'clipPath', '__clippath_in_use__');
}
markAllUnused() {
super.markAllUnused();
const refGroups = this._refGroups;
for (let key in refGroups) {
if (refGroups.hasOwnProperty(key)) {
this.markDomUnused(refGroups[key]);
}
}
this._keyDuplicateCount = {};
}
private _getClipPathGroup(displayable: Displayable, prevDisplayable: Displayable) {
if (!hasClipPath(displayable)) {
return;
}
const clipPaths = displayable.__clipPaths;
const keyDuplicateCount = this._keyDuplicateCount;
let clipPathKey = getClipPathsKey(clipPaths);
if (isClipPathChanged(clipPaths, prevDisplayable && prevDisplayable.__clipPaths)) {
keyDuplicateCount[clipPathKey] = keyDuplicateCount[clipPathKey] || 0;
keyDuplicateCount[clipPathKey] && (clipPathKey += '-' + keyDuplicateCount[clipPathKey]);
keyDuplicateCount[clipPathKey]++;
}
return this._refGroups[clipPathKey]
|| (this._refGroups[clipPathKey] = createElement('g'));
}
/**
* Update clipPath.
*
* @param displayable displayable element
*/
update(displayable: Displayable, prevDisplayable: Displayable) {
const clipGroup = this._getClipPathGroup(displayable, prevDisplayable);
if (clipGroup) {
this.markDomUsed(clipGroup);
this.updateDom(clipGroup, displayable.__clipPaths);
}
return clipGroup;
};
/**
* Create an SVGElement of displayable and create a <clipPath> of its
* clipPath
*/
updateDom(parentEl: SVGElement, clipPaths: Path[]) {
if (clipPaths && clipPaths.length > 0) {
// Has clipPath, create <clipPath> with the first clipPath
const defs = this.getDefs(true);
const clipPath = clipPaths[0] as PathExtended;
let clipPathEl;
let id;
if (clipPath._dom) {
// Use a dom that is already in <defs>
id = clipPath._dom.getAttribute('id');
clipPathEl = clipPath._dom;
// Use a dom that is already in <defs>
if (!defs.contains(clipPathEl)) {
// This happens when set old clipPath that has
// been previously removed
defs.appendChild(clipPathEl);
}
}
else {
// New <clipPath>
id = 'zr' + this._zrId + '-clip-' + this.nextId;
++this.nextId;
clipPathEl = createElement('clipPath');
clipPathEl.setAttribute('id', id);
defs.appendChild(clipPathEl);
clipPath._dom = clipPathEl;
}
// Build path and add to <clipPath>
path.brush(clipPath);
const pathEl = this.getSvgElement(clipPath);
clipPathEl.innerHTML = '';
clipPathEl.appendChild(pathEl);
parentEl.setAttribute('clip-path', getIdURL(id));
if (clipPaths.length > 1) {
// Make the other clipPaths recursively
this.updateDom(clipPathEl, clipPaths.slice(1));
}
}
else {
// No clipPath
if (parentEl) {
parentEl.setAttribute('clip-path', 'none');
}
}
};
/**
* Mark a single clipPath to be used
*
* @param displayable displayable element
*/
markUsed(displayable: Displayable) {
// displayable.__clipPaths can only be `null`/`undefined` or an non-empty array.
if (displayable.__clipPaths) {
zrUtil.each(displayable.__clipPaths, (clipPath: PathExtended) => {
if (clipPath._dom) {
super.markDomUsed(clipPath._dom);
}
});
}
};
removeUnused() {
super.removeUnused();
const newRefGroupsMap: Dictionary<SVGElement> = {};
const refGroups = this._refGroups;
for (let key in refGroups) {
if (refGroups.hasOwnProperty(key)) {
const group = refGroups[key];
if (!this.isDomUnused(group)) {
newRefGroupsMap[key] = group;
}
else if (group.parentNode) {
group.parentNode.removeChild(group);
}
}
}
this._refGroups = newRefGroupsMap;
}
}
+235
View File
@@ -0,0 +1,235 @@
/**
* @file Manages elements that can be defined in <defs> in SVG,
* e.g., gradients, clip path, etc.
* @author Zhang Wenli
*/
import {createElement} from '../../svg/core';
import * as zrUtil from '../../core/util';
import Displayable from '../../graphic/Displayable';
const MARK_UNUSED = '0';
const MARK_USED = '1';
/**
* Manages elements that can be defined in <defs> in SVG,
* e.g., gradients, clip path, etc.
*/
export default class Definable {
nextId = 0
protected _zrId: number
protected _svgRoot: SVGElement
protected _tagNames: string[]
protected _markLabel: string
protected _domName: string = '_dom'
constructor(
zrId: number, // zrender instance id
svgRoot: SVGElement, // root of SVG document
tagNames: string | string[], // possible tag names
markLabel: string, // label name to make if the element
domName?: string
) {
this._zrId = zrId;
this._svgRoot = svgRoot;
this._tagNames = typeof tagNames === 'string' ? [tagNames] : tagNames;
this._markLabel = markLabel;
if (domName) {
this._domName = domName;
}
}
/**
* Get the <defs> tag for svgRoot; optionally creates one if not exists.
*
* @param isForceCreating if need to create when not exists
* @return SVG <defs> element, null if it doesn't
* exist and isForceCreating is false
*/
getDefs(isForceCreating?: boolean): SVGDefsElement {
let svgRoot = this._svgRoot;
let defs = this._svgRoot.getElementsByTagName('defs');
if (defs.length === 0) {
// Not exist
if (isForceCreating) {
let defs = svgRoot.insertBefore(
createElement('defs'), // Create new tag
svgRoot.firstChild // Insert in the front of svg
) as SVGDefsElement;
if (!defs.contains) {
// IE doesn't support contains method
defs.contains = function (el) {
const children = defs.children;
if (!children) {
return false;
}
for (let i = children.length - 1; i >= 0; --i) {
if (children[i] === el) {
return true;
}
}
return false;
};
}
return defs;
}
else {
return null;
}
}
else {
return defs[0];
}
}
/**
* Update DOM element if necessary.
*
* @param element style element. e.g., for gradient,
* it may be '#ccc' or {type: 'linear', ...}
* @param onUpdate update callback
*/
doUpdate<T>(target: T, onUpdate?: (target: T) => void) {
if (!target) {
return;
}
const defs = this.getDefs(false);
if ((target as any)[this._domName] && defs.contains((target as any)[this._domName])) {
// Update DOM
if (typeof onUpdate === 'function') {
onUpdate(target);
}
}
else {
// No previous dom, create new
const dom = this.add(target);
if (dom) {
(target as any)[this._domName] = dom;
}
}
}
add(target: any): SVGElement {
return null;
}
/**
* Add gradient dom to defs
*
* @param dom DOM to be added to <defs>
*/
addDom(dom: SVGElement) {
const defs = this.getDefs(true);
if (dom.parentNode !== defs) {
defs.appendChild(dom);
}
}
/**
* Remove DOM of a given element.
*
* @param target Target where to attach the dom
*/
removeDom<T>(target: T) {
const defs = this.getDefs(false);
if (defs && (target as any)[this._domName]) {
defs.removeChild((target as any)[this._domName]);
(target as any)[this._domName] = null;
}
}
/**
* Get DOMs of this element.
*
* @return doms of this defineable elements in <defs>
*/
getDoms() {
const defs = this.getDefs(false);
if (!defs) {
// No dom when defs is not defined
return [];
}
let doms: SVGElement[] = [];
zrUtil.each(this._tagNames, function (tagName) {
const tags = defs.getElementsByTagName(tagName) as HTMLCollectionOf<SVGElement>;
// Note that tags is HTMLCollection, which is array-like
// rather than real array.
// So `doms.concat(tags)` add tags as one object.
for (let i = 0; i < tags.length; i++) {
doms.push(tags[i]);
}
});
return doms;
}
/**
* Mark DOMs to be unused before painting, and clear unused ones at the end
* of the painting.
*/
markAllUnused() {
const doms = this.getDoms();
const that = this;
zrUtil.each(doms, function (dom) {
(dom as any)[that._markLabel] = MARK_UNUSED;
});
}
/**
* Mark a single DOM to be used.
*
* @param dom DOM to mark
*/
markDomUsed(dom: SVGElement) {
dom && ((dom as any)[this._markLabel] = MARK_USED);
};
markDomUnused(dom: SVGElement) {
dom && ((dom as any)[this._markLabel] = MARK_UNUSED);
};
isDomUnused(dom: SVGElement) {
return dom && (dom as any)[this._markLabel] !== MARK_USED;
}
/**
* Remove unused DOMs defined in <defs>
*/
removeUnused() {
const defs = this.getDefs(false);
if (!defs) {
// Nothing to remove
return;
}
const doms = this.getDoms();
zrUtil.each(doms, (dom) => {
if (this.isDomUnused(dom)) {
// Remove gradient
defs.removeChild(dom);
}
});
}
/**
* Get SVG element.
*
* @param displayable displayable element
* @return SVG element
*/
getSvgElement(displayable: Displayable): SVGElement {
return displayable.__svgEl;
}
}
+225
View File
@@ -0,0 +1,225 @@
/**
* @file Manages SVG gradient elements.
* @author Zhang Wenli
*/
import Definable from './Definable';
import * as zrUtil from '../../core/util';
import Displayable from '../../graphic/Displayable';
import { GradientObject } from '../../graphic/Gradient';
import { getIdURL, isGradient, isLinearGradient, isRadialGradient, normalizeColor, round4 } from '../../svg/helper';
import { createElement } from '../../svg/core';
type GradientObjectExtended = GradientObject & {
__dom: SVGElement
}
/**
* Manages SVG gradient elements.
*
* @param zrId zrender instance id
* @param svgRoot root of SVG document
*/
export default class GradientManager extends Definable {
constructor(zrId: number, svgRoot: SVGElement) {
super(zrId, svgRoot, ['linearGradient', 'radialGradient'], '__gradient_in_use__');
}
/**
* Create new gradient DOM for fill or stroke if not exist,
* but will not update gradient if exists.
*
* @param svgElement SVG element to paint
* @param displayable zrender displayable element
*/
addWithoutUpdate(
svgElement: SVGElement,
displayable: Displayable
) {
if (displayable && displayable.style) {
const that = this;
zrUtil.each(['fill', 'stroke'], function (fillOrStroke: 'fill' | 'stroke') {
let value = displayable.style[fillOrStroke] as GradientObject;
if (isGradient(value)) {
const gradient = value as GradientObjectExtended;
const defs = that.getDefs(true);
// Create dom in <defs> if not exists
let dom;
if (gradient.__dom) {
// Gradient exists
dom = gradient.__dom;
if (!defs.contains(gradient.__dom)) {
// __dom is no longer in defs, recreate
that.addDom(dom);
}
}
else {
// New dom
dom = that.add(gradient);
}
that.markUsed(displayable);
svgElement.setAttribute(fillOrStroke, getIdURL(dom.getAttribute('id')));
}
});
}
}
/**
* Add a new gradient tag in <defs>
*
* @param gradient zr gradient instance
*/
add(gradient: GradientObject): SVGElement {
let dom;
if (isLinearGradient(gradient)) {
dom = createElement('linearGradient');
}
else if (isRadialGradient(gradient)) {
dom = createElement('radialGradient');
}
else {
if (process.env.NODE_ENV !== 'production') {
zrUtil.logError('Illegal gradient type.');
}
return null;
}
// Set dom id with gradient id, since each gradient instance
// will have no more than one dom element.
// id may exists before for those dirty elements, in which case
// id should remain the same, and other attributes should be
// updated.
gradient.id = gradient.id || this.nextId++;
dom.setAttribute('id', 'zr' + this._zrId
+ '-gradient-' + gradient.id);
this.updateDom(gradient, dom);
this.addDom(dom);
return dom;
}
/**
* Update gradient.
*
* @param gradient zr gradient instance or color string
*/
update(gradient: GradientObject | string) {
if (!isGradient(gradient)) {
return;
}
const that = this;
this.doUpdate(gradient, function () {
const dom = (gradient as GradientObjectExtended).__dom;
if (!dom) {
return;
}
const tagName = dom.tagName;
const type = gradient.type;
if (type === 'linear' && tagName === 'linearGradient'
|| type === 'radial' && tagName === 'radialGradient'
) {
// Gradient type is not changed, update gradient
that.updateDom(gradient, (gradient as GradientObjectExtended).__dom);
}
else {
// Remove and re-create if type is changed
that.removeDom(gradient);
that.add(gradient);
}
});
}
/**
* Update gradient dom
*
* @param gradient zr gradient instance
* @param dom DOM to update
*/
updateDom(gradient: GradientObject, dom: SVGElement) {
if (isLinearGradient(gradient)) {
dom.setAttribute('x1', gradient.x as any);
dom.setAttribute('y1', gradient.y as any);
dom.setAttribute('x2', gradient.x2 as any);
dom.setAttribute('y2', gradient.y2 as any);
}
else if (isRadialGradient(gradient)) {
dom.setAttribute('cx', gradient.x as any);
dom.setAttribute('cy', gradient.y as any);
dom.setAttribute('r', gradient.r as any);
}
else {
if (process.env.NODE_ENV !== 'production') {
zrUtil.logError('Illegal gradient type.');
}
return;
}
dom.setAttribute('gradientUnits',
gradient.global
? 'userSpaceOnUse' // x1, x2, y1, y2 in range of 0 to canvas width or height
: 'objectBoundingBox' // x1, x2, y1, y2 in range of 0 to 1
);
// Remove color stops if exists
dom.innerHTML = '';
// Add color stops
const colors = gradient.colorStops;
for (let i = 0, len = colors.length; i < len; ++i) {
const stop = createElement('stop');
stop.setAttribute('offset', round4(colors[i].offset) * 100 + '%');
const stopColor = colors[i].color;
// Fix Safari bug that stop-color not recognizing alpha #9014
const {color, opacity} = normalizeColor(stopColor);
// stop-color cannot be color, since:
// The opacity value used for the gradient calculation is the
// *product* of the value of stop-opacity and the opacity of the
// value of stop-color.
// See https://www.w3.org/TR/SVG2/pservers.html#StopOpacityProperty
stop.setAttribute('stop-color', color);
if (opacity < 1) {
stop.setAttribute('stop-opacity', opacity as any);
}
dom.appendChild(stop);
}
// Store dom element in gradient, to avoid creating multiple
// dom instances for the same gradient element
(gradient as GradientObject as GradientObjectExtended).__dom = dom;
}
/**
* Mark a single gradient to be used
*
* @param displayable displayable element
*/
markUsed(displayable: Displayable) {
if (displayable.style) {
let gradient = displayable.style.fill as GradientObject as GradientObjectExtended;
if (gradient && gradient.__dom) {
super.markDomUsed(gradient.__dom);
}
gradient = displayable.style.stroke as GradientObject as GradientObjectExtended;
if (gradient && gradient.__dom) {
super.markDomUsed(gradient.__dom);
}
}
}
}
+221
View File
@@ -0,0 +1,221 @@
/**
* @file Manages SVG pattern elements.
* @author Zhang Wenli
*/
import Definable from './Definable';
import * as zrUtil from '../../core/util';
import Displayable from '../../graphic/Displayable';
import {PatternObject} from '../../graphic/Pattern';
import {createOrUpdateImage} from '../../graphic/helper/image';
import WeakMap from '../../core/WeakMap';
import { getIdURL, isPattern, isSVGPattern } from '../../svg/helper';
import { createElement } from '../../svg/core';
const patternDomMap = new WeakMap<PatternObject, SVGElement>();
/**
* Manages SVG pattern elements.
*
* @param zrId zrender instance id
* @param svgRoot root of SVG document
*/
export default class PatternManager extends Definable {
constructor(zrId: number, svgRoot: SVGElement) {
super(zrId, svgRoot, ['pattern'], '__pattern_in_use__');
}
/**
* Create new pattern DOM for fill or stroke if not exist,
* but will not update pattern if exists.
*
* @param svgElement SVG element to paint
* @param displayable zrender displayable element
*/
addWithoutUpdate(
svgElement: SVGElement,
displayable: Displayable
) {
if (displayable && displayable.style) {
const that = this;
zrUtil.each(['fill', 'stroke'], function (fillOrStroke: 'fill' | 'stroke') {
const pattern = displayable.style[fillOrStroke] as PatternObject;
if (isPattern(pattern)) {
const defs = that.getDefs(true);
// Create dom in <defs> if not exists
let dom = patternDomMap.get(pattern);
if (dom) {
// Pattern exists
if (!defs.contains(dom)) {
// __dom is no longer in defs, recreate
that.addDom(dom);
}
}
else {
// New dom
dom = that.add(pattern);
}
that.markUsed(displayable);
svgElement.setAttribute(fillOrStroke, getIdURL(dom.getAttribute('id')));
}
});
}
}
/**
* Add a new pattern tag in <defs>
*
* @param pattern zr pattern instance
*/
add(pattern: PatternObject): SVGElement {
if (!isPattern(pattern)) {
return;
}
let dom = createElement('pattern');
pattern.id = pattern.id == null ? this.nextId++ : pattern.id;
dom.setAttribute('id', 'zr' + this._zrId
+ '-pattern-' + pattern.id);
dom.setAttribute('patternUnits', 'userSpaceOnUse');
this.updateDom(pattern, dom);
this.addDom(dom);
return dom;
}
/**
* Update pattern.
*
* @param pattern zr pattern instance or color string
*/
update(pattern: PatternObject | string) {
if (!isPattern(pattern)) {
return;
}
const that = this;
this.doUpdate(pattern, function () {
const dom = patternDomMap.get(pattern);
that.updateDom(pattern, dom);
});
}
/**
* Update pattern dom
*
* @param pattern zr pattern instance
* @param patternDom DOM to update
*/
updateDom(pattern: PatternObject, patternDom: SVGElement) {
if (isSVGPattern(pattern)) {
// New SVGPattern will not been supported in the legacy SVG renderer.
// svg-legacy will been removed soon.
// const svgElement = pattern.svgElement;
// const isStringSVG = typeof svgElement === 'string';
// if (isStringSVG || svgElement.parentNode !== patternDom) {
// if (isStringSVG) {
// patternDom.innerHTML = svgElement;
// }
// else {
// patternDom.innerHTML = '';
// patternDom.appendChild(svgElement);
// }
// patternDom.setAttribute('width', pattern.svgWidth as any);
// patternDom.setAttribute('height', pattern.svgHeight as any);
// }
}
else {
let img: SVGElement;
const prevImage = patternDom.getElementsByTagName('image');
if (prevImage.length) {
if (pattern.image) {
// Update
img = prevImage[0];
}
else {
// Remove
patternDom.removeChild(prevImage[0]);
return;
}
}
else if (pattern.image) {
// Create
img = createElement('image');
}
if (img) {
let imageSrc;
const patternImage = pattern.image;
if (typeof patternImage === 'string') {
imageSrc = patternImage;
}
else if (patternImage instanceof HTMLImageElement) {
imageSrc = patternImage.src;
}
else if (patternImage instanceof HTMLCanvasElement) {
imageSrc = patternImage.toDataURL();
}
if (imageSrc) {
img.setAttribute('href', imageSrc);
// No need to re-render so dirty is empty
const hostEl = {
dirty: () => {}
};
const updateSize = (img: HTMLImageElement) => {
patternDom.setAttribute('width', img.width as any);
patternDom.setAttribute('height', img.height as any);
};
const createdImage = createOrUpdateImage(imageSrc, img as any, hostEl, updateSize);
if (createdImage && createdImage.width && createdImage.height) {
// Loaded before
updateSize(createdImage as HTMLImageElement);
}
patternDom.appendChild(img);
}
}
}
const x = pattern.x || 0;
const y = pattern.y || 0;
const rotation = (pattern.rotation || 0) / Math.PI * 180;
const scaleX = pattern.scaleX || 1;
const scaleY = pattern.scaleY || 1;
const transform = `translate(${x}, ${y}) rotate(${rotation}) scale(${scaleX}, ${scaleY})`;
patternDom.setAttribute('patternTransform', transform);
patternDomMap.set(pattern, patternDom);
}
/**
* Mark a single pattern to be used
*
* @param displayable displayable element
*/
markUsed(displayable: Displayable) {
if (displayable.style) {
if (isPattern(displayable.style.fill)) {
super.markDomUsed(patternDomMap.get(displayable.style.fill));
}
if (isPattern(displayable.style.stroke)) {
super.markDomUsed(patternDomMap.get(displayable.style.stroke));
}
}
}
}
+149
View File
@@ -0,0 +1,149 @@
/**
* @file Manages SVG shadow elements.
* @author Zhang Wenli
*/
import Definable from './Definable';
import Displayable from '../../graphic/Displayable';
import { Dictionary } from '../../core/types';
import { getIdURL, getShadowKey, hasShadow, normalizeColor } from '../../svg/helper';
import { createElement } from '../../svg/core';
type DisplayableExtended = Displayable & {
_shadowDom: SVGElement
}
/**
* Manages SVG shadow elements.
*
*/
export default class ShadowManager extends Definable {
private _shadowDomMap: Dictionary<SVGFilterElement> = {}
private _shadowDomPool: SVGFilterElement[] = []
constructor(zrId: number, svgRoot: SVGElement) {
super(zrId, svgRoot, ['filter'], '__filter_in_use__', '_shadowDom');
}
/**
* Add a new shadow tag in <defs>
*
* @param displayable zrender displayable element
* @return created DOM
*/
private _getFromPool(): SVGFilterElement {
let shadowDom = this._shadowDomPool.pop(); // Try to get one from trash.
if (!shadowDom) {
shadowDom = createElement('filter') as SVGFilterElement;
shadowDom.setAttribute('id', 'zr' + this._zrId + '-shadow-' + this.nextId++);
const domChild = createElement('feDropShadow');
shadowDom.appendChild(domChild);
this.addDom(shadowDom);
}
return shadowDom;
}
/**
* Update shadow.
*/
update(svgElement: SVGElement, displayable: Displayable) {
const style = displayable.style;
if (hasShadow(style)) {
// Try getting shadow from cache.
const shadowKey = getShadowKey(displayable);
let shadowDom = (displayable as DisplayableExtended)._shadowDom = this._shadowDomMap[shadowKey];
if (!shadowDom) {
shadowDom = this._getFromPool();
this._shadowDomMap[shadowKey] = shadowDom;
}
this.updateDom(svgElement, displayable, shadowDom);
}
else {
// Remove shadow
this.remove(svgElement, displayable);
}
}
/**
* Remove DOM and clear parent filter
*/
remove(svgElement: SVGElement, displayable: Displayable) {
if ((displayable as DisplayableExtended)._shadowDom != null) {
(displayable as DisplayableExtended)._shadowDom = null;
svgElement.removeAttribute('filter');
}
}
/**
* Update shadow dom
*
* @param displayable zrender displayable element
* @param shadowDom DOM to update
*/
updateDom(svgElement: SVGElement, displayable: Displayable, shadowDom: SVGElement) {
let domChild = shadowDom.children[0];
const style = displayable.style;
const globalScale = displayable.getGlobalScale();
const scaleX = globalScale[0];
const scaleY = globalScale[1];
if (!scaleX || !scaleY) {
return;
}
// TODO: textBoxShadowBlur is not supported yet
const offsetX = style.shadowOffsetX || 0;
const offsetY = style.shadowOffsetY || 0;
const blur = style.shadowBlur;
const normalizedColor = normalizeColor(style.shadowColor);
domChild.setAttribute('dx', offsetX / scaleX + '');
domChild.setAttribute('dy', offsetY / scaleY + '');
domChild.setAttribute('flood-color', normalizedColor.color);
domChild.setAttribute('flood-opacity', normalizedColor.opacity + '');
// Divide by two here so that it looks the same as in canvas
// See: https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-shadowblur
const stdDx = blur / 2 / scaleX;
const stdDy = blur / 2 / scaleY;
const stdDeviation = stdDx + ' ' + stdDy;
domChild.setAttribute('stdDeviation', stdDeviation);
// Fix filter clipping problem
shadowDom.setAttribute('x', '-100%');
shadowDom.setAttribute('y', '-100%');
shadowDom.setAttribute('width', '300%');
shadowDom.setAttribute('height', '300%');
// Store dom element in shadow, to avoid creating multiple
// dom instances for the same shadow element
(displayable as DisplayableExtended)._shadowDom = shadowDom;
svgElement.setAttribute('filter', getIdURL(shadowDom.getAttribute('id')));
}
removeUnused() {
const defs = this.getDefs(false);
if (!defs) {
// Nothing to remove
return;
}
let shadowDomsPool = this._shadowDomPool;
// let currentUsedShadow = 0;
const shadowDomMap = this._shadowDomMap;
for (let key in shadowDomMap) {
if (shadowDomMap.hasOwnProperty(key)) {
shadowDomsPool.push(shadowDomMap[key]);
}
// currentUsedShadow++;
}
// Reset the map.
this._shadowDomMap = {};
}
}
+4
View File
@@ -0,0 +1,4 @@
import {registerPainter} from '../zrender';
import Painter from './Painter';
registerPainter('svg-legacy', Painter);
+410
View File
@@ -0,0 +1,410 @@
/**
* SVG Painter
*/
import {
brush,
setClipPath,
setGradient,
setPattern
} from './graphic';
import Displayable from '../graphic/Displayable';
import Storage from '../Storage';
import { PainterBase } from '../PainterBase';
import {
createElement,
createVNode,
vNodeToString,
SVGVNodeAttrs,
SVGVNode,
getCssString,
BrushScope,
createBrushScope,
createSVGVNode
} from './core';
import { normalizeColor, encodeBase64, isGradient, isPattern } from './helper';
import { extend, keys, logError, map, noop, retrieve2 } from '../core/util';
import Path from '../graphic/Path';
import patch, { updateAttrs } from './patch';
import { getSize } from '../canvas/helper';
import { GradientObject } from '../graphic/Gradient';
import { PatternObject } from '../graphic/Pattern';
let svgId = 0;
interface SVGPainterOption {
width?: number
height?: number
ssr?: boolean
}
type SVGPainterBackgroundColor = string | GradientObject | PatternObject;
class SVGPainter implements PainterBase {
type = 'svg'
storage: Storage
root: HTMLElement
private _svgDom: SVGElement
private _viewport: HTMLElement
private _opts: SVGPainterOption
private _oldVNode: SVGVNode
private _bgVNode: SVGVNode
private _mainVNode: SVGVNode
private _width: number
private _height: number
private _backgroundColor: SVGPainterBackgroundColor
private _id: string
constructor(root: HTMLElement, storage: Storage, opts: SVGPainterOption) {
this.storage = storage;
this._opts = opts = extend({}, opts);
this.root = root;
// A unique id for generating svg ids.
this._id = 'zr' + svgId++;
this._oldVNode = createSVGVNode(opts.width, opts.height);
if (root && !opts.ssr) {
const viewport = this._viewport = document.createElement('div');
viewport.style.cssText = 'position:relative;overflow:hidden';
const svgDom = this._svgDom = this._oldVNode.elm = createElement('svg');
updateAttrs(null, this._oldVNode);
viewport.appendChild(svgDom);
root.appendChild(viewport);
}
this.resize(opts.width, opts.height);
}
getType() {
return this.type;
}
getViewportRoot() {
return this._viewport;
}
getViewportRootOffset() {
const viewportRoot = this.getViewportRoot();
if (viewportRoot) {
return {
offsetLeft: viewportRoot.offsetLeft || 0,
offsetTop: viewportRoot.offsetTop || 0
};
}
}
getSvgDom() {
return this._svgDom;
}
refresh() {
if (this.root) {
const vnode = this.renderToVNode({
willUpdate: true
});
// Disable user selection.
vnode.attrs.style = 'position:absolute;left:0;top:0;user-select:none';
patch(this._oldVNode, vnode);
this._oldVNode = vnode;
}
}
renderOneToVNode(el: Displayable) {
return brush(el, createBrushScope(this._id));
}
renderToVNode(opts?: {
animation?: boolean,
willUpdate?: boolean,
compress?: boolean,
useViewBox?: boolean,
emphasis?: boolean
}) {
opts = opts || {};
const list = this.storage.getDisplayList(true);
const width = this._width;
const height = this._height;
const scope = createBrushScope(this._id);
scope.animation = opts.animation;
scope.willUpdate = opts.willUpdate;
scope.compress = opts.compress;
scope.emphasis = opts.emphasis;
scope.ssr = this._opts.ssr;
const children: SVGVNode[] = [];
const bgVNode = this._bgVNode = createBackgroundVNode(width, height, this._backgroundColor, scope);
bgVNode && children.push(bgVNode);
// Ignore the root g if wan't the output to be more tight.
const mainVNode = !opts.compress
? (this._mainVNode = createVNode('g', 'main', {}, [])) : null;
this._paintList(list, scope, mainVNode ? mainVNode.children : children);
mainVNode && children.push(mainVNode);
const defs = map(keys(scope.defs), (id) => scope.defs[id]);
if (defs.length) {
children.push(createVNode('defs', 'defs', {}, defs));
}
if (opts.animation) {
const animationCssStr = getCssString(scope.cssNodes, scope.cssAnims, { newline: true });
if (animationCssStr) {
const styleNode = createVNode('style', 'stl', {}, [], animationCssStr);
children.push(styleNode);
}
}
return createSVGVNode(width, height, children, opts.useViewBox);
}
renderToString(opts?: {
/**
* If add css animation.
* @default true
*/
cssAnimation?: boolean,
/**
* If add css emphasis.
* @default true
*/
cssEmphasis?: boolean,
/**
* If use viewBox
* @default true
*/
useViewBox?: boolean
}) {
opts = opts || {};
return vNodeToString(this.renderToVNode({
animation: retrieve2(opts.cssAnimation, true),
emphasis: retrieve2(opts.cssEmphasis, true),
willUpdate: false,
compress: true,
useViewBox: retrieve2(opts.useViewBox, true)
}), { newline: true });
}
setBackgroundColor(backgroundColor: SVGPainterBackgroundColor) {
this._backgroundColor = backgroundColor;
}
getSvgRoot() {
return this._mainVNode && this._mainVNode.elm as SVGElement;
}
_paintList(list: Displayable[], scope: BrushScope, out?: SVGVNode[]) {
const listLen = list.length;
const clipPathsGroupsStack: SVGVNode[] = [];
let clipPathsGroupsStackDepth = 0;
let currentClipPathGroup;
let prevClipPaths: Path[];
let clipGroupNodeIdx = 0;
for (let i = 0; i < listLen; i++) {
const displayable = list[i];
if (!displayable.invisible) {
const clipPaths = displayable.__clipPaths;
const len = clipPaths && clipPaths.length || 0;
const prevLen = prevClipPaths && prevClipPaths.length || 0;
let lca;
// Find the lowest common ancestor
for (lca = Math.max(len - 1, prevLen - 1); lca >= 0; lca--) {
if (clipPaths && prevClipPaths
&& clipPaths[lca] === prevClipPaths[lca]
) {
break;
}
}
// pop the stack
for (let i = prevLen - 1; i > lca; i--) {
clipPathsGroupsStackDepth--;
// svgEls.push(closeGroup);
currentClipPathGroup = clipPathsGroupsStack[clipPathsGroupsStackDepth - 1];
}
// Pop clip path group for clipPaths not match the previous.
for (let i = lca + 1; i < len; i++) {
const groupAttrs: SVGVNodeAttrs = {};
setClipPath(
clipPaths[i],
groupAttrs,
scope
);
const g = createVNode(
'g',
'clip-g-' + clipGroupNodeIdx++,
groupAttrs,
[]
);
(currentClipPathGroup ? currentClipPathGroup.children : out).push(g);
clipPathsGroupsStack[clipPathsGroupsStackDepth++] = g;
currentClipPathGroup = g;
}
prevClipPaths = clipPaths;
const ret = brush(displayable, scope);
if (ret) {
(currentClipPathGroup ? currentClipPathGroup.children : out).push(ret);
}
}
}
}
resize(width: number, height: number) {
// Save input w/h
const opts = this._opts;
const root = this.root;
const viewport = this._viewport;
width != null && (opts.width = width);
height != null && (opts.height = height);
if (root && viewport) {
// FIXME Why ?
viewport.style.display = 'none';
width = getSize(root, 0, opts);
height = getSize(root, 1, opts);
viewport.style.display = '';
}
if (this._width !== width || this._height !== height) {
this._width = width;
this._height = height;
if (viewport) {
const viewportStyle = viewport.style;
viewportStyle.width = width + 'px';
viewportStyle.height = height + 'px';
}
if (!isPattern(this._backgroundColor)) {
const svgDom = this._svgDom;
if (svgDom) {
// Set width by 'svgRoot.width = width' is invalid
svgDom.setAttribute('width', width as any);
svgDom.setAttribute('height', height as any);
}
const bgEl = this._bgVNode && this._bgVNode.elm as SVGElement;
if (bgEl) {
bgEl.setAttribute('width', width as any);
bgEl.setAttribute('height', height as any);
}
}
else {
// pattern backgroundColor requires a full refresh
this.refresh();
}
}
}
/**
* 获取绘图区域宽度
*/
getWidth() {
return this._width;
}
/**
* 获取绘图区域高度
*/
getHeight() {
return this._height;
}
dispose() {
if (this.root) {
this.root.innerHTML = '';
}
this._svgDom =
this._viewport =
this.storage =
this._oldVNode =
this._bgVNode =
this._mainVNode = null;
}
clear() {
if (this._svgDom) {
this._svgDom.innerHTML = null;
}
this._oldVNode = null;
}
toDataURL(base64?: boolean) {
let str = this.renderToString();
const prefix = 'data:image/svg+xml;';
if (base64) {
str = encodeBase64(str);
return str && prefix + 'base64,' + str;
}
return prefix + 'charset=UTF-8,' + encodeURIComponent(str);
}
refreshHover = createMethodNotSupport('refreshHover') as PainterBase['refreshHover'];
configLayer = createMethodNotSupport('configLayer') as PainterBase['configLayer'];
}
// Not supported methods
function createMethodNotSupport(method: string): any {
return function () {
if (process.env.NODE_ENV !== 'production') {
logError('In SVG mode painter not support method "' + method + '"');
}
};
}
function createBackgroundVNode(
width: number,
height: number,
backgroundColor: SVGPainterBackgroundColor,
scope: BrushScope
) {
let bgVNode;
if (backgroundColor && backgroundColor !== 'none') {
bgVNode = createVNode(
'rect',
'bg',
{
width,
height,
x: '0',
y: '0'
}
);
if (isGradient(backgroundColor)) {
setGradient({ fill: backgroundColor as any }, bgVNode.attrs, 'fill', scope);
}
else if (isPattern(backgroundColor)) {
setPattern({
style: {
fill: backgroundColor
},
dirty: noop,
getBoundingRect: () => ({ width, height })
} as any, bgVNode.attrs, 'fill', scope);
}
else {
const { color, opacity } = normalizeColor(backgroundColor);
bgVNode.attrs.fill = color;
opacity < 1 && (bgVNode.attrs['fill-opacity'] = opacity);
}
}
return bgVNode;
}
export default SVGPainter;

Some files were not shown because too many files have changed in this diff Show More