前端初始化
This commit is contained in:
+409
@@ -0,0 +1,409 @@
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
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;
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
import { PathRebuilder } from '../core/PathProxy';
|
||||
import { isAroundZero } from './helper';
|
||||
|
||||
const mathSin = Math.sin;
|
||||
const mathCos = Math.cos;
|
||||
const PI = Math.PI;
|
||||
const PI2 = Math.PI * 2;
|
||||
const degree = 180 / PI;
|
||||
|
||||
|
||||
export default class SVGPathRebuilder implements PathRebuilder {
|
||||
private _d: (string | number)[]
|
||||
private _str: string
|
||||
private _invalid: boolean
|
||||
|
||||
// If is start of subpath
|
||||
private _start: boolean
|
||||
private _p: number
|
||||
|
||||
reset(precision?: number) {
|
||||
this._start = true;
|
||||
this._d = [];
|
||||
this._str = '';
|
||||
|
||||
this._p = Math.pow(10, precision || 4);
|
||||
}
|
||||
moveTo(x: number, y: number) {
|
||||
this._add('M', x, y);
|
||||
}
|
||||
lineTo(x: number, y: number) {
|
||||
this._add('L', x, y);
|
||||
}
|
||||
bezierCurveTo(x: number, y: number, x2: number, y2: number, x3: number, y3: number) {
|
||||
this._add('C', x, y, x2, y2, x3, y3);
|
||||
}
|
||||
quadraticCurveTo(x: number, y: number, x2: number, y2: number) {
|
||||
this._add('Q', x, y, x2, y2);
|
||||
}
|
||||
arc(cx: number, cy: number, r: number, startAngle: number, endAngle: number, anticlockwise: boolean) {
|
||||
this.ellipse(cx, cy, r, r, 0, startAngle, endAngle, anticlockwise);
|
||||
}
|
||||
ellipse(
|
||||
cx: number, cy: number,
|
||||
rx: number, ry: number,
|
||||
psi: number,
|
||||
startAngle: number,
|
||||
endAngle: number,
|
||||
anticlockwise: boolean
|
||||
) {
|
||||
let dTheta = endAngle - startAngle;
|
||||
const clockwise = !anticlockwise;
|
||||
|
||||
const dThetaPositive = Math.abs(dTheta);
|
||||
const isCircle = isAroundZero(dThetaPositive - PI2)
|
||||
|| (clockwise ? dTheta >= PI2 : -dTheta >= PI2);
|
||||
|
||||
// Mapping to 0~2PI
|
||||
const unifiedTheta = dTheta > 0 ? dTheta % PI2 : (dTheta % PI2 + PI2);
|
||||
|
||||
let large = false;
|
||||
if (isCircle) {
|
||||
large = true;
|
||||
}
|
||||
else if (isAroundZero(dThetaPositive)) {
|
||||
large = false;
|
||||
}
|
||||
else {
|
||||
large = (unifiedTheta >= PI) === !!clockwise;
|
||||
}
|
||||
|
||||
const x0 = cx + rx * mathCos(startAngle);
|
||||
const y0 = cy + ry * mathSin(startAngle);
|
||||
|
||||
if (this._start) {
|
||||
// Move to (x0, y0) only when CMD.A comes at the
|
||||
// first position of a shape.
|
||||
// For instance, when drawing a ring, CMD.A comes
|
||||
// after CMD.M, so it's unnecessary to move to
|
||||
// (x0, y0).
|
||||
this._add('M', x0, y0);
|
||||
}
|
||||
|
||||
const xRot = Math.round(psi * degree);
|
||||
// It will not draw if start point and end point are exactly the same
|
||||
// We need to add two arcs
|
||||
if (isCircle) {
|
||||
const p = 1 / this._p;
|
||||
const dTheta = (clockwise ? 1 : -1) * (PI2 - p);
|
||||
this._add(
|
||||
'A', rx, ry, xRot, 1, +clockwise,
|
||||
cx + rx * mathCos(startAngle + dTheta),
|
||||
cy + ry * mathSin(startAngle + dTheta)
|
||||
);
|
||||
// TODO.
|
||||
// Usually we can simply divide the circle into two halfs arcs.
|
||||
// But it will cause slightly diff with previous screenshot.
|
||||
// We can't tell it but visual regression test can. To avoid too much breaks.
|
||||
// We keep the logic on the browser as before.
|
||||
// But in SSR mode wich has lower precision. We close the circle by adding another arc.
|
||||
if (p > 1e-2) {
|
||||
this._add('A', rx, ry, xRot, 0, +clockwise, x0, y0);
|
||||
}
|
||||
}
|
||||
else {
|
||||
const x = cx + rx * mathCos(endAngle);
|
||||
const y = cy + ry * mathSin(endAngle);
|
||||
|
||||
// FIXME Ellipse
|
||||
this._add('A', rx, ry, xRot, +large, +clockwise, x, y);
|
||||
}
|
||||
|
||||
}
|
||||
rect(x: number, y: number, w: number, h: number) {
|
||||
this._add('M', x, y);
|
||||
// Use relative coordinates to reduce the size.
|
||||
this._add('l', w, 0);
|
||||
this._add('l', 0, h);
|
||||
this._add('l', -w, 0);
|
||||
// this._add('L', x, y);
|
||||
this._add('Z');
|
||||
}
|
||||
closePath() {
|
||||
// Not use Z as first command
|
||||
if (this._d.length > 0) {
|
||||
this._add('Z');
|
||||
}
|
||||
}
|
||||
|
||||
_add(cmd: string, a?: number, b?: number, c?: number, d?: number, e?: number, f?: number, g?: number, h?: number) {
|
||||
const vals = [];
|
||||
const p = this._p;
|
||||
for (let i = 1; i < arguments.length; i++) {
|
||||
const val = arguments[i];
|
||||
if (isNaN(val)) {
|
||||
this._invalid = true;
|
||||
return;
|
||||
}
|
||||
vals.push(Math.round(val * p) / p);
|
||||
}
|
||||
this._d.push(cmd + vals.join(' '));
|
||||
this._start = cmd === 'Z';
|
||||
}
|
||||
|
||||
generateStr() {
|
||||
this._str = this._invalid ? '' : this._d.join('');
|
||||
this._d = [];
|
||||
}
|
||||
getStr() {
|
||||
return this._str;
|
||||
}
|
||||
}
|
||||
+208
@@ -0,0 +1,208 @@
|
||||
import { keys, map } from '../core/util';
|
||||
import { encodeHTML } from '../core/dom';
|
||||
|
||||
export type CSSSelectorVNode = Record<string, string>
|
||||
export type CSSAnimationVNode = Record<string, Record<string, string>>
|
||||
|
||||
export const SVGNS = 'http://www.w3.org/2000/svg';
|
||||
export const XLINKNS = 'http://www.w3.org/1999/xlink';
|
||||
export const XMLNS = 'http://www.w3.org/2000/xmlns/';
|
||||
export const XML_NAMESPACE = 'http://www.w3.org/XML/1998/namespace';
|
||||
export const META_DATA_PREFIX = 'ecmeta_';
|
||||
|
||||
export function createElement(name: string) {
|
||||
return document.createElementNS(SVGNS, name);
|
||||
}
|
||||
|
||||
export type SVGVNodeAttrs = Record<string, string | number | undefined | boolean>
|
||||
export interface SVGVNode {
|
||||
tag: string,
|
||||
attrs: SVGVNodeAttrs,
|
||||
children?: SVGVNode[],
|
||||
text?: string
|
||||
|
||||
// For patching
|
||||
elm?: Node
|
||||
key: string
|
||||
};
|
||||
export function createVNode(
|
||||
tag: string,
|
||||
key: string,
|
||||
attrs?: SVGVNodeAttrs,
|
||||
children?: SVGVNode[],
|
||||
text?: string
|
||||
): SVGVNode {
|
||||
return {
|
||||
tag,
|
||||
attrs: attrs || {},
|
||||
children,
|
||||
text,
|
||||
key
|
||||
};
|
||||
}
|
||||
|
||||
function createElementOpen(name: string, attrs?: SVGVNodeAttrs) {
|
||||
const attrsStr: string[] = [];
|
||||
if (attrs) {
|
||||
// eslint-disable-next-line
|
||||
for (let key in attrs) {
|
||||
const val = attrs[key];
|
||||
let part = key;
|
||||
// Same with the logic in patch.
|
||||
if (val === false) {
|
||||
continue;
|
||||
}
|
||||
else if (val !== true && val != null) {
|
||||
part += `="${val}"`;
|
||||
}
|
||||
attrsStr.push(part);
|
||||
}
|
||||
}
|
||||
return `<${name} ${attrsStr.join(' ')}>`;
|
||||
}
|
||||
|
||||
function createElementClose(name: string) {
|
||||
return `</${name}>`;
|
||||
}
|
||||
|
||||
export function vNodeToString(el: SVGVNode, opts?: {
|
||||
newline?: boolean
|
||||
}) {
|
||||
opts = opts || {};
|
||||
const S = opts.newline ? '\n' : '';
|
||||
function convertElToString(el: SVGVNode): string {
|
||||
const {children, tag, attrs, text} = el;
|
||||
return createElementOpen(tag, attrs)
|
||||
+ (tag !== 'style' ? encodeHTML(text) : text || '')
|
||||
+ (children ? `${S}${map(children, child => convertElToString(child)).join(S)}${S}` : '')
|
||||
+ createElementClose(tag);
|
||||
}
|
||||
return convertElToString(el);
|
||||
}
|
||||
|
||||
export function getCssString(
|
||||
selectorNodes: Record<string, CSSSelectorVNode>,
|
||||
animationNodes: Record<string, CSSAnimationVNode>,
|
||||
opts?: {
|
||||
newline?: boolean
|
||||
}
|
||||
) {
|
||||
opts = opts || {};
|
||||
const S = opts.newline ? '\n' : '';
|
||||
const bracketBegin = ` {${S}`;
|
||||
const bracketEnd = `${S}}`;
|
||||
const selectors = map(keys(selectorNodes), className => {
|
||||
return className + bracketBegin + map(keys(selectorNodes[className]), attrName => {
|
||||
return `${attrName}:${selectorNodes[className][attrName]};`;
|
||||
}).join(S) + bracketEnd;
|
||||
}).join(S);
|
||||
const animations = map(keys(animationNodes), (animationName) => {
|
||||
return `@keyframes ${animationName}${bracketBegin}` + map(keys(animationNodes[animationName]), percent => {
|
||||
return percent + bracketBegin + map(keys(animationNodes[animationName][percent]), attrName => {
|
||||
let val = animationNodes[animationName][percent][attrName];
|
||||
// postprocess
|
||||
if (attrName === 'd') {
|
||||
val = `path("${val}")`;
|
||||
}
|
||||
return `${attrName}:${val};`;
|
||||
}).join(S) + bracketEnd;
|
||||
}).join(S) + bracketEnd;
|
||||
}).join(S);
|
||||
|
||||
if (!selectors && !animations) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return ['<![CDATA[', selectors, animations, ']]>'].join(S);
|
||||
}
|
||||
|
||||
|
||||
export interface BrushScope {
|
||||
zrId: string
|
||||
|
||||
shadowCache: Record<string, string>
|
||||
gradientCache: Record<string, string>
|
||||
patternCache: Record<string, string>
|
||||
clipPathCache: Record<string, string>
|
||||
|
||||
defs: Record<string, SVGVNode>
|
||||
|
||||
cssNodes: Record<string, CSSSelectorVNode>
|
||||
cssAnims: Record<string, Record<string, Record<string, string>>>
|
||||
/**
|
||||
* Cache for css style string, mapping from style string to class name.
|
||||
*/
|
||||
cssStyleCache: Record<string, string>
|
||||
|
||||
cssAnimIdx: number
|
||||
|
||||
shadowIdx: number
|
||||
gradientIdx: number
|
||||
patternIdx: number
|
||||
clipPathIdx: number
|
||||
// configs
|
||||
/**
|
||||
* If create animates nodes.
|
||||
*/
|
||||
animation?: boolean,
|
||||
/**
|
||||
* If create emphasis styles.
|
||||
*/
|
||||
emphasis?: boolean,
|
||||
|
||||
/**
|
||||
* If will update. Some optimization for string generation can't be applied.
|
||||
*/
|
||||
willUpdate?: boolean
|
||||
|
||||
/**
|
||||
* If compress the output string.
|
||||
*/
|
||||
compress?: boolean
|
||||
|
||||
ssr?: boolean
|
||||
}
|
||||
|
||||
export function createBrushScope(zrId: string): BrushScope {
|
||||
return {
|
||||
zrId,
|
||||
shadowCache: {},
|
||||
patternCache: {},
|
||||
gradientCache: {},
|
||||
clipPathCache: {},
|
||||
defs: {},
|
||||
|
||||
cssNodes: {},
|
||||
cssAnims: {},
|
||||
cssStyleCache: {},
|
||||
|
||||
cssAnimIdx: 0,
|
||||
|
||||
shadowIdx: 0,
|
||||
gradientIdx: 0,
|
||||
patternIdx: 0,
|
||||
clipPathIdx: 0
|
||||
};
|
||||
}
|
||||
|
||||
export function createSVGVNode(
|
||||
width: number | string,
|
||||
height: number | string,
|
||||
children?: SVGVNode[],
|
||||
useViewBox?: boolean
|
||||
) {
|
||||
return createVNode(
|
||||
'svg',
|
||||
'root',
|
||||
{
|
||||
'width': width,
|
||||
'height': height,
|
||||
'xmlns': SVGNS,
|
||||
'xmlns:xlink': XLINKNS,
|
||||
'version': '1.1',
|
||||
'baseProfile': 'full',
|
||||
'viewBox': useViewBox ? `0 0 ${width} ${height}` : false
|
||||
},
|
||||
children
|
||||
);
|
||||
}
|
||||
+366
@@ -0,0 +1,366 @@
|
||||
import Transformable, { copyTransform } from '../core/Transformable';
|
||||
import Displayable from '../graphic/Displayable';
|
||||
import { SVGVNodeAttrs, BrushScope, createBrushScope} from './core';
|
||||
import Path from '../graphic/Path';
|
||||
import SVGPathRebuilder from './SVGPathRebuilder';
|
||||
import PathProxy from '../core/PathProxy';
|
||||
import { getPathPrecision, getSRTTransformString } from './helper';
|
||||
import { each, extend, filter, isNumber, isString, keys } from '../core/util';
|
||||
import Animator from '../animation/Animator';
|
||||
import CompoundPath from '../graphic/CompoundPath';
|
||||
import { AnimationEasing } from '../animation/easing';
|
||||
import { createCubicEasingFunc } from '../animation/cubicEasing';
|
||||
import { getClassId } from './cssClassId';
|
||||
|
||||
export const EASING_MAP: Record<string, string> = {
|
||||
// From https://easings.net/
|
||||
cubicIn: '0.32,0,0.67,0',
|
||||
cubicOut: '0.33,1,0.68,1',
|
||||
cubicInOut: '0.65,0,0.35,1',
|
||||
quadraticIn: '0.11,0,0.5,0',
|
||||
quadraticOut: '0.5,1,0.89,1',
|
||||
quadraticInOut: '0.45,0,0.55,1',
|
||||
quarticIn: '0.5,0,0.75,0',
|
||||
quarticOut: '0.25,1,0.5,1',
|
||||
quarticInOut: '0.76,0,0.24,1',
|
||||
quinticIn: '0.64,0,0.78,0',
|
||||
quinticOut: '0.22,1,0.36,1',
|
||||
quinticInOut: '0.83,0,0.17,1',
|
||||
sinusoidalIn: '0.12,0,0.39,0',
|
||||
sinusoidalOut: '0.61,1,0.88,1',
|
||||
sinusoidalInOut: '0.37,0,0.63,1',
|
||||
exponentialIn: '0.7,0,0.84,0',
|
||||
exponentialOut: '0.16,1,0.3,1',
|
||||
exponentialInOut: '0.87,0,0.13,1',
|
||||
circularIn: '0.55,0,1,0.45',
|
||||
circularOut: '0,0.55,0.45,1',
|
||||
circularInOut: '0.85,0,0.15,1'
|
||||
// TODO elastic, bounce
|
||||
};
|
||||
|
||||
const transformOriginKey = 'transform-origin';
|
||||
|
||||
function buildPathString(el: Path, kfShape: Path['shape'], path: PathProxy) {
|
||||
const shape = extend({}, el.shape);
|
||||
extend(shape, kfShape);
|
||||
|
||||
el.buildPath(path, shape);
|
||||
const svgPathBuilder = new SVGPathRebuilder();
|
||||
svgPathBuilder.reset(getPathPrecision(el));
|
||||
path.rebuildPath(svgPathBuilder, 1);
|
||||
svgPathBuilder.generateStr();
|
||||
// will add path("") when generated to css string in the final step.
|
||||
return svgPathBuilder.getStr();
|
||||
}
|
||||
|
||||
function setTransformOrigin(target: Record<string, string>, transform: Transformable) {
|
||||
const {originX, originY} = transform;
|
||||
if (originX || originY) {
|
||||
target[transformOriginKey] = `${originX}px ${originY}px`;
|
||||
}
|
||||
}
|
||||
|
||||
export const ANIMATE_STYLE_MAP: Record<string, string> = {
|
||||
fill: 'fill',
|
||||
opacity: 'opacity',
|
||||
lineWidth: 'stroke-width',
|
||||
lineDashOffset: 'stroke-dashoffset'
|
||||
// TODO shadow is not supported.
|
||||
};
|
||||
|
||||
type CssKF = Record<string, any>;
|
||||
|
||||
function addAnimation(cssAnim: Record<string, CssKF>, scope: BrushScope) {
|
||||
const animationName = scope.zrId + '-ani-' + scope.cssAnimIdx++;
|
||||
scope.cssAnims[animationName] = cssAnim;
|
||||
return animationName;
|
||||
}
|
||||
|
||||
function createCompoundPathCSSAnimation(
|
||||
el: CompoundPath,
|
||||
attrs: SVGVNodeAttrs,
|
||||
scope: BrushScope
|
||||
) {
|
||||
const paths = el.shape.paths;
|
||||
const composedAnim: Record<string, CssKF> = {};
|
||||
let cssAnimationCfg: string;
|
||||
let cssAnimationName: string;
|
||||
each(paths, path => {
|
||||
const subScope = createBrushScope(scope.zrId);
|
||||
subScope.animation = true;
|
||||
createCSSAnimation(path, {}, subScope, true);
|
||||
const cssAnims = subScope.cssAnims;
|
||||
const cssNodes = subScope.cssNodes;
|
||||
const animNames = keys(cssAnims);
|
||||
const len = animNames.length;
|
||||
if (!len) {
|
||||
return;
|
||||
}
|
||||
cssAnimationName = animNames[len - 1];
|
||||
// Only use last animation because they are conflicted.
|
||||
const lastAnim = cssAnims[cssAnimationName];
|
||||
// eslint-disable-next-line
|
||||
for (let percent in lastAnim) {
|
||||
const kf = lastAnim[percent];
|
||||
composedAnim[percent] = composedAnim[percent] || { d: '' };
|
||||
composedAnim[percent].d += kf.d || '';
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
for (let className in cssNodes) {
|
||||
const val = cssNodes[className].animation;
|
||||
if (val.indexOf(cssAnimationName) >= 0) {
|
||||
// Only pick the animation configuration of last subpath.
|
||||
cssAnimationCfg = val;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!cssAnimationCfg) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove the attrs in the element because it will be set by animation.
|
||||
// Reduce the size.
|
||||
attrs.d = false;
|
||||
const animationName = addAnimation(composedAnim, scope);
|
||||
return cssAnimationCfg.replace(cssAnimationName, animationName);
|
||||
}
|
||||
|
||||
function getEasingFunc(easing: AnimationEasing) {
|
||||
return isString(easing)
|
||||
? EASING_MAP[easing]
|
||||
? `cubic-bezier(${EASING_MAP[easing]})`
|
||||
: createCubicEasingFunc(easing) ? easing : ''
|
||||
: '';
|
||||
}
|
||||
|
||||
export function createCSSAnimation(
|
||||
el: Displayable,
|
||||
attrs: SVGVNodeAttrs,
|
||||
scope: BrushScope,
|
||||
onlyShape?: boolean
|
||||
) {
|
||||
const animators = el.animators;
|
||||
const len = animators.length;
|
||||
|
||||
const cssAnimations: string[] = [];
|
||||
|
||||
if (el instanceof CompoundPath) {
|
||||
const animationCfg = createCompoundPathCSSAnimation(el, attrs, scope);
|
||||
if (animationCfg) {
|
||||
cssAnimations.push(animationCfg);
|
||||
}
|
||||
else if (!len) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (!len) {
|
||||
return;
|
||||
}
|
||||
// Group animators by it's configuration
|
||||
const groupAnimators: Record<string, [string, Animator<any>[]]> = {};
|
||||
for (let i = 0; i < len; i++) {
|
||||
const animator = animators[i];
|
||||
const cfgArr: (string | number)[] = [animator.getMaxTime() / 1000 + 's'];
|
||||
const easing = getEasingFunc(animator.getClip().easing);
|
||||
const delay = animator.getDelay();
|
||||
|
||||
if (easing) {
|
||||
cfgArr.push(easing);
|
||||
}
|
||||
else {
|
||||
cfgArr.push('linear');
|
||||
}
|
||||
if (delay) {
|
||||
cfgArr.push(delay / 1000 + 's');
|
||||
}
|
||||
if (animator.getLoop()) {
|
||||
cfgArr.push('infinite');
|
||||
}
|
||||
const cfg = cfgArr.join(' ');
|
||||
|
||||
// TODO fill mode
|
||||
groupAnimators[cfg] = groupAnimators[cfg] || [cfg, [] as Animator<any>[]];
|
||||
groupAnimators[cfg][1].push(animator);
|
||||
}
|
||||
|
||||
function createSingleCSSAnimation(groupAnimator: [string, Animator<any>[]]) {
|
||||
const animators = groupAnimator[1];
|
||||
const len = animators.length;
|
||||
const transformKfs: Record<string, CssKF> = {};
|
||||
const shapeKfs: Record<string, CssKF> = {};
|
||||
|
||||
const finalKfs: Record<string, CssKF> = {};
|
||||
|
||||
const animationTimingFunctionAttrName = 'animation-timing-function';
|
||||
|
||||
function saveAnimatorTrackToCssKfs(
|
||||
animator: Animator<any>,
|
||||
cssKfs: Record<string, CssKF>,
|
||||
toCssAttrName?: (propName: string) => string
|
||||
) {
|
||||
const tracks = animator.getTracks();
|
||||
const maxTime = animator.getMaxTime();
|
||||
for (let k = 0; k < tracks.length; k++) {
|
||||
const track = tracks[k];
|
||||
if (track.needsAnimate()) {
|
||||
const kfs = track.keyframes;
|
||||
let attrName = track.propName;
|
||||
toCssAttrName && (attrName = toCssAttrName(attrName));
|
||||
if (attrName) {
|
||||
for (let i = 0; i < kfs.length; i++) {
|
||||
const kf = kfs[i];
|
||||
const percent = Math.round(kf.time / maxTime * 100) + '%';
|
||||
const kfEasing = getEasingFunc(kf.easing);
|
||||
const rawValue = kf.rawValue;
|
||||
|
||||
// TODO gradient
|
||||
if (isString(rawValue) || isNumber(rawValue)) {
|
||||
cssKfs[percent] = cssKfs[percent] || {};
|
||||
cssKfs[percent][attrName] = kf.rawValue;
|
||||
|
||||
if (kfEasing) {
|
||||
// TODO. If different property have different easings.
|
||||
cssKfs[percent][animationTimingFunctionAttrName] = kfEasing;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find all transform animations.
|
||||
// TODO origin, parent
|
||||
for (let i = 0; i < len; i++) {
|
||||
const animator = animators[i];
|
||||
const targetProp = animator.targetName;
|
||||
if (!targetProp) {
|
||||
!onlyShape && saveAnimatorTrackToCssKfs(animator, transformKfs);
|
||||
}
|
||||
else if (targetProp === 'shape') {
|
||||
saveAnimatorTrackToCssKfs(animator, shapeKfs);
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
for (let percent in transformKfs) {
|
||||
const transform = {} as Transformable;
|
||||
copyTransform(transform, el);
|
||||
extend(transform, transformKfs[percent]);
|
||||
const str = getSRTTransformString(transform);
|
||||
const timingFunction = transformKfs[percent][animationTimingFunctionAttrName];
|
||||
finalKfs[percent] = str ? {
|
||||
transform: str
|
||||
} : {};
|
||||
// TODO set transform origin in element?
|
||||
setTransformOrigin(finalKfs[percent], transform);
|
||||
|
||||
// Save timing function
|
||||
if (timingFunction) {
|
||||
finalKfs[percent][animationTimingFunctionAttrName] = timingFunction;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
let path: PathProxy;
|
||||
let canAnimateShape = true;
|
||||
// eslint-disable-next-line
|
||||
for (let percent in shapeKfs) {
|
||||
finalKfs[percent] = finalKfs[percent] || {};
|
||||
|
||||
const isFirst = !path;
|
||||
const timingFunction = shapeKfs[percent][animationTimingFunctionAttrName];
|
||||
|
||||
if (isFirst) {
|
||||
path = new PathProxy();
|
||||
}
|
||||
let len = path.len();
|
||||
path.reset();
|
||||
finalKfs[percent].d = buildPathString(el as Path, shapeKfs[percent], path);
|
||||
let newLen = path.len();
|
||||
// Path data don't match.
|
||||
if (!isFirst && len !== newLen) {
|
||||
canAnimateShape = false;
|
||||
break;
|
||||
}
|
||||
|
||||
// Save timing function
|
||||
if (timingFunction) {
|
||||
finalKfs[percent][animationTimingFunctionAttrName] = timingFunction;
|
||||
}
|
||||
};
|
||||
if (!canAnimateShape) {
|
||||
// eslint-disable-next-line
|
||||
for (let percent in finalKfs) {
|
||||
delete finalKfs[percent].d;
|
||||
}
|
||||
}
|
||||
|
||||
if (!onlyShape) {
|
||||
for (let i = 0; i < len; i++) {
|
||||
const animator = animators[i];
|
||||
const targetProp = animator.targetName;
|
||||
if (targetProp === 'style') {
|
||||
saveAnimatorTrackToCssKfs(
|
||||
animator, finalKfs, (propName) => ANIMATE_STYLE_MAP[propName]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const percents = keys(finalKfs);
|
||||
|
||||
// Set transform origin in attribute to reduce the size.
|
||||
let allTransformOriginSame = true;
|
||||
let transformOrigin;
|
||||
for (let i = 1; i < percents.length; i++) {
|
||||
const p0 = percents[i - 1];
|
||||
const p1 = percents[i];
|
||||
if (finalKfs[p0][transformOriginKey] !== finalKfs[p1][transformOriginKey]) {
|
||||
allTransformOriginSame = false;
|
||||
break;
|
||||
}
|
||||
transformOrigin = finalKfs[p0][transformOriginKey];
|
||||
}
|
||||
if (allTransformOriginSame && transformOrigin) {
|
||||
for (const percent in finalKfs) {
|
||||
if (finalKfs[percent][transformOriginKey]) {
|
||||
delete finalKfs[percent][transformOriginKey];
|
||||
}
|
||||
}
|
||||
attrs[transformOriginKey] = transformOrigin;
|
||||
}
|
||||
|
||||
if (filter(
|
||||
percents, (percent) => keys(finalKfs[percent]).length > 0
|
||||
).length) {
|
||||
const animationName = addAnimation(finalKfs, scope);
|
||||
// eslint-disable-next-line
|
||||
// for (const attrName in finalKfs[percents[0]]) {
|
||||
// // Remove the attrs in the element because it will be set by animation.
|
||||
// // Reduce the size.
|
||||
// attrs[attrName] = false;
|
||||
// }
|
||||
// animationName {duration easing delay loop} fillMode
|
||||
return `${animationName} ${groupAnimator[0]} both`;
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
for (let key in groupAnimators) {
|
||||
const animationCfg = createSingleCSSAnimation(groupAnimators[key]);
|
||||
if (animationCfg) {
|
||||
cssAnimations.push(animationCfg);
|
||||
}
|
||||
}
|
||||
|
||||
if (cssAnimations.length) {
|
||||
const className = scope.zrId + '-cls-' + getClassId();
|
||||
scope.cssNodes['.' + className] = {
|
||||
animation: cssAnimations.join(',')
|
||||
};
|
||||
// TODO exists class?
|
||||
attrs.class = className;
|
||||
}
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
let cssClassIdx = 0;
|
||||
|
||||
export function getClassId() {
|
||||
return cssClassIdx++;
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
import Displayable from '../graphic/Displayable';
|
||||
import { liftColor } from '../tool/color';
|
||||
import { BrushScope, SVGVNodeAttrs } from './core';
|
||||
import { getClassId } from './cssClassId';
|
||||
|
||||
export function createCSSEmphasis(
|
||||
el: Displayable,
|
||||
attrs: SVGVNodeAttrs,
|
||||
scope: BrushScope
|
||||
) {
|
||||
if (!el.ignore) {
|
||||
if (el.isSilent()) {
|
||||
// If el is silent, it can not be hovered nor selected.
|
||||
// So set pointer-events to pass through.
|
||||
const style = {
|
||||
'pointer-events': 'none'
|
||||
};
|
||||
setClassAttribute(style, attrs, scope, true);
|
||||
}
|
||||
else {
|
||||
const emphasisStyle = el.states.emphasis && el.states.emphasis.style
|
||||
? el.states.emphasis.style
|
||||
: {};
|
||||
let fill = emphasisStyle.fill;
|
||||
if (!fill) {
|
||||
// No empahsis fill, lift color
|
||||
const normalFill = el.style && el.style.fill;
|
||||
const selectFill = el.states.select
|
||||
&& el.states.select.style
|
||||
&& el.states.select.style.fill;
|
||||
const fromFill = el.currentStates.indexOf('select') >= 0
|
||||
? (selectFill || normalFill)
|
||||
: normalFill;
|
||||
if (fromFill) {
|
||||
fill = liftColor(fromFill);
|
||||
}
|
||||
}
|
||||
let lineWidth = emphasisStyle.lineWidth;
|
||||
if (lineWidth) {
|
||||
// Symbols use transform to set size, so lineWidth
|
||||
// should be divided by scaleX
|
||||
const scaleX = (!emphasisStyle.strokeNoScale && el.transform)
|
||||
? el.transform[0]
|
||||
: 1;
|
||||
lineWidth = lineWidth / scaleX;
|
||||
}
|
||||
const style = {
|
||||
cursor: 'pointer', // TODO: Should this be customized?
|
||||
} as any;
|
||||
if (fill) {
|
||||
style.fill = fill;
|
||||
}
|
||||
if (emphasisStyle.stroke) {
|
||||
style.stroke = emphasisStyle.stroke;
|
||||
}
|
||||
if (lineWidth) {
|
||||
style['stroke-width'] = lineWidth;
|
||||
}
|
||||
setClassAttribute(style, attrs, scope, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setClassAttribute(style: object, attrs: SVGVNodeAttrs, scope: BrushScope, withHover: boolean) {
|
||||
const styleKey = JSON.stringify(style);
|
||||
let className = scope.cssStyleCache[styleKey];
|
||||
if (!className) {
|
||||
className = scope.zrId + '-cls-' + getClassId();
|
||||
scope.cssStyleCache[styleKey] = className;
|
||||
scope.cssNodes['.' + className + (withHover ? ':hover' : '')] = style as any;
|
||||
}
|
||||
attrs.class = attrs.class ? (attrs.class + ' ' + className) : className;
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
export function createTextNode(text: string): Text {
|
||||
return document.createTextNode(text);
|
||||
}
|
||||
|
||||
export function createComment(text: string): Comment {
|
||||
return document.createComment(text);
|
||||
}
|
||||
|
||||
export function insertBefore(
|
||||
parentNode: Node,
|
||||
newNode: Node,
|
||||
referenceNode: Node | null
|
||||
): void {
|
||||
parentNode.insertBefore(newNode, referenceNode);
|
||||
}
|
||||
|
||||
export function removeChild(node: Node, child: Node): void {
|
||||
node.removeChild(child);
|
||||
}
|
||||
|
||||
export function appendChild(node: Node, child: Node): void {
|
||||
node.appendChild(child);
|
||||
}
|
||||
|
||||
export function parentNode(node: Node): Node | null {
|
||||
return node.parentNode;
|
||||
}
|
||||
|
||||
export function nextSibling(node: Node): Node | null {
|
||||
return node.nextSibling;
|
||||
}
|
||||
|
||||
export function tagName(elm: Element): string {
|
||||
return elm.tagName;
|
||||
}
|
||||
|
||||
export function setTextContent(node: Node, text: string | null): void {
|
||||
node.textContent = text;
|
||||
}
|
||||
|
||||
export function getTextContent(node: Node): string | null {
|
||||
return node.textContent;
|
||||
}
|
||||
|
||||
export function isElement(node: Node): node is Element {
|
||||
return node.nodeType === 1;
|
||||
}
|
||||
|
||||
export function isText(node: Node): node is Text {
|
||||
return node.nodeType === 3;
|
||||
}
|
||||
|
||||
export function isComment(node: Node): node is Comment {
|
||||
return node.nodeType === 8;
|
||||
}
|
||||
+661
@@ -0,0 +1,661 @@
|
||||
// TODO
|
||||
// 1. shadow
|
||||
// 2. Image: sx, sy, sw, sh
|
||||
|
||||
import {
|
||||
adjustTextY,
|
||||
getIdURL,
|
||||
getMatrixStr,
|
||||
getPathPrecision,
|
||||
getShadowKey,
|
||||
getSRTTransformString,
|
||||
hasShadow,
|
||||
isAroundZero,
|
||||
isGradient,
|
||||
isImagePattern,
|
||||
isLinearGradient,
|
||||
isPattern,
|
||||
isRadialGradient,
|
||||
normalizeColor,
|
||||
round4,
|
||||
TEXT_ALIGN_TO_ANCHOR
|
||||
} from './helper';
|
||||
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 './SVGPathRebuilder';
|
||||
import mapStyleToAttrs from './mapStyleToAttrs';
|
||||
import { SVGVNodeAttrs, createVNode, SVGVNode, vNodeToString, BrushScope, META_DATA_PREFIX } from './core';
|
||||
import { MatrixArray } from '../core/matrix';
|
||||
import Displayable from '../graphic/Displayable';
|
||||
import { assert, clone, isFunction, isString, logError, map, retrieve2 } from '../core/util';
|
||||
import Polyline from '../graphic/shape/Polyline';
|
||||
import Polygon from '../graphic/shape/Polygon';
|
||||
import { GradientObject } from '../graphic/Gradient';
|
||||
import { ImagePatternObject, SVGPatternObject } from '../graphic/Pattern';
|
||||
import { createOrUpdateImage } from '../graphic/helper/image';
|
||||
import { ImageLike } from '../core/types';
|
||||
import { createCSSAnimation } from './cssAnimation';
|
||||
import { hasSeparateFont, parseFontSize } from '../graphic/Text';
|
||||
import { DEFAULT_FONT, DEFAULT_FONT_FAMILY } from '../core/platform';
|
||||
import { createCSSEmphasis } from './cssEmphasis';
|
||||
import { getElementSSRData } from '../zrender';
|
||||
|
||||
const round = Math.round;
|
||||
|
||||
function isImageLike(val: any): val is HTMLImageElement {
|
||||
return val && isString(val.src);
|
||||
}
|
||||
function isCanvasLike(val: any): val is HTMLCanvasElement {
|
||||
return val && isFunction(val.toDataURL);
|
||||
}
|
||||
|
||||
|
||||
type AllStyleOption = PathStyleProps | TSpanStyleProps | ImageStyleProps;
|
||||
|
||||
function setStyleAttrs(attrs: SVGVNodeAttrs, style: AllStyleOption, el: Path | TSpan | ZRImage, scope: BrushScope) {
|
||||
mapStyleToAttrs((key, val) => {
|
||||
const isFillStroke = key === 'fill' || key === 'stroke';
|
||||
if (isFillStroke && isGradient(val)) {
|
||||
setGradient(style, attrs, key, scope);
|
||||
}
|
||||
else if (isFillStroke && isPattern(val)) {
|
||||
setPattern(el, attrs, key, scope);
|
||||
}
|
||||
else {
|
||||
attrs[key] = val;
|
||||
}
|
||||
if (isFillStroke && scope.ssr && val === 'none') {
|
||||
// When is none, it cannot be interacted when ssr
|
||||
// Setting `pointer-events` as `visible` to make it responding
|
||||
// See also https://www.w3.org/TR/SVG/interact.html#PointerEventsProperty
|
||||
attrs['pointer-events'] = 'visible';
|
||||
}
|
||||
}, style, el, false);
|
||||
|
||||
setShadow(el, attrs, scope);
|
||||
}
|
||||
|
||||
function setMetaData(attrs: SVGVNodeAttrs, el: Path | TSpan | ZRImage) {
|
||||
const metaData = getElementSSRData(el);
|
||||
if (metaData) {
|
||||
metaData.each((val, key) => {
|
||||
val != null && (attrs[(META_DATA_PREFIX + key).toLowerCase()] = val + '');
|
||||
});
|
||||
if (el.isSilent()) {
|
||||
attrs[META_DATA_PREFIX + 'silent'] = 'true';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function noRotateScale(m: MatrixArray) {
|
||||
return isAroundZero(m[0] - 1)
|
||||
&& isAroundZero(m[1])
|
||||
&& isAroundZero(m[2])
|
||||
&& isAroundZero(m[3] - 1);
|
||||
}
|
||||
|
||||
function noTranslate(m: MatrixArray) {
|
||||
return isAroundZero(m[4]) && isAroundZero(m[5]);
|
||||
}
|
||||
|
||||
function setTransform(attrs: SVGVNodeAttrs, m: MatrixArray, compress?: boolean) {
|
||||
if (m && !(noTranslate(m) && noRotateScale(m))) {
|
||||
const mul = compress ? 10 : 1e4;
|
||||
// Use translate possible to reduce the size a bit.
|
||||
attrs.transform = noRotateScale(m)
|
||||
? `translate(${round(m[4] * mul) / mul} ${round(m[5] * mul) / mul})` : getMatrixStr(m);
|
||||
}
|
||||
}
|
||||
|
||||
type ShapeMapDesc = (string | [string, string])[];
|
||||
type ConvertShapeToAttr = (shape: any, attrs: SVGVNodeAttrs, mul?: number) => void;
|
||||
type ShapeValidator = (shape: any) => boolean;
|
||||
|
||||
function convertPolyShape(shape: Polygon['shape'], attrs: SVGVNodeAttrs, mul: number) {
|
||||
const points = shape.points;
|
||||
const strArr = [];
|
||||
for (let i = 0; i < points.length; i++) {
|
||||
strArr.push(round(points[i][0] * mul) / mul);
|
||||
strArr.push(round(points[i][1] * mul) / mul);
|
||||
}
|
||||
attrs.points = strArr.join(' ');
|
||||
}
|
||||
|
||||
function validatePolyShape(shape: Polyline['shape']) {
|
||||
return !shape.smooth;
|
||||
}
|
||||
|
||||
function createAttrsConvert(desc: ShapeMapDesc): ConvertShapeToAttr {
|
||||
const normalizedDesc: [string, string][] = map(desc, (item) =>
|
||||
(typeof item === 'string' ? [item, item] : item)
|
||||
);
|
||||
|
||||
return function (shape, attrs, mul) {
|
||||
for (let i = 0; i < normalizedDesc.length; i++) {
|
||||
const item = normalizedDesc[i];
|
||||
const val = shape[item[0]];
|
||||
if (val != null) {
|
||||
attrs[item[1]] = round(val * mul) / mul;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const builtinShapesDef: Record<string, [ConvertShapeToAttr, ShapeValidator?]> = {
|
||||
circle: [createAttrsConvert(['cx', 'cy', 'r'])],
|
||||
polyline: [convertPolyShape, validatePolyShape],
|
||||
polygon: [convertPolyShape, validatePolyShape]
|
||||
// Ignore line because it will be larger.
|
||||
};
|
||||
|
||||
interface PathWithSVGBuildPath extends Path {
|
||||
__svgPathVersion: number
|
||||
__svgPathBuilder: SVGPathRebuilder
|
||||
__svgPathStrokePercent: number
|
||||
}
|
||||
|
||||
function hasShapeAnimation(el: Displayable) {
|
||||
const animators = el.animators;
|
||||
for (let i = 0; i < animators.length; i++) {
|
||||
if (animators[i].targetName === 'shape') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function brushSVGPath(el: Path, scope: BrushScope) {
|
||||
const style = el.style;
|
||||
const shape = el.shape;
|
||||
const builtinShpDef = builtinShapesDef[el.type];
|
||||
const attrs: SVGVNodeAttrs = {};
|
||||
const needsAnimate = scope.animation;
|
||||
let svgElType = 'path';
|
||||
const strokePercent = el.style.strokePercent;
|
||||
const precision = (scope.compress && getPathPrecision(el)) || 4;
|
||||
// Using SVG builtin shapes if possible
|
||||
if (builtinShpDef
|
||||
// Force to use path if it will update later.
|
||||
// To avoid some animation(like morph) fail
|
||||
&& !scope.willUpdate
|
||||
&& !(builtinShpDef[1] && !builtinShpDef[1](shape))
|
||||
// use `path` to simplify the animate element creation logic.
|
||||
&& !(needsAnimate && hasShapeAnimation(el))
|
||||
&& !(strokePercent < 1)
|
||||
) {
|
||||
svgElType = el.type;
|
||||
const mul = Math.pow(10, precision);
|
||||
builtinShpDef[0](shape, attrs, mul);
|
||||
}
|
||||
else {
|
||||
const needBuildPath = !el.path || el.shapeChanged();
|
||||
if (!el.path) {
|
||||
el.createPathProxy();
|
||||
}
|
||||
const path = el.path;
|
||||
|
||||
if (needBuildPath) {
|
||||
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
|
||||
|| strokePercent !== elExt.__svgPathStrokePercent
|
||||
) {
|
||||
if (!svgPathBuilder) {
|
||||
svgPathBuilder = elExt.__svgPathBuilder = new SVGPathRebuilder();
|
||||
}
|
||||
svgPathBuilder.reset(precision);
|
||||
path.rebuildPath(svgPathBuilder, strokePercent);
|
||||
svgPathBuilder.generateStr();
|
||||
elExt.__svgPathVersion = pathVersion;
|
||||
elExt.__svgPathStrokePercent = strokePercent;
|
||||
}
|
||||
|
||||
attrs.d = svgPathBuilder.getStr();
|
||||
}
|
||||
|
||||
setTransform(attrs, el.transform);
|
||||
setStyleAttrs(attrs, style, el, scope);
|
||||
setMetaData(attrs, el);
|
||||
|
||||
scope.animation && createCSSAnimation(el, attrs, scope);
|
||||
scope.emphasis && createCSSEmphasis(el, attrs, scope);
|
||||
|
||||
return createVNode(svgElType, el.id + '', attrs);
|
||||
}
|
||||
|
||||
export function brushSVGImage(el: ZRImage, scope: BrushScope) {
|
||||
const style = el.style;
|
||||
let image = style.image;
|
||||
|
||||
if (image && !isString(image)) {
|
||||
if (isImageLike(image)) {
|
||||
image = image.src;
|
||||
}
|
||||
// heatmap layer in geo may be a canvas
|
||||
else if (isCanvasLike(image)) {
|
||||
image = image.toDataURL();
|
||||
}
|
||||
}
|
||||
|
||||
if (!image) {
|
||||
return;
|
||||
}
|
||||
|
||||
const x = style.x || 0;
|
||||
const y = style.y || 0;
|
||||
|
||||
const dw = style.width;
|
||||
const dh = style.height;
|
||||
|
||||
const attrs: SVGVNodeAttrs = {
|
||||
href: image as string,
|
||||
width: dw,
|
||||
height: dh
|
||||
};
|
||||
if (x) {
|
||||
attrs.x = x;
|
||||
}
|
||||
if (y) {
|
||||
attrs.y = y;
|
||||
}
|
||||
|
||||
setTransform(attrs, el.transform);
|
||||
setStyleAttrs(attrs, style, el, scope);
|
||||
setMetaData(attrs, el);
|
||||
|
||||
scope.animation && createCSSAnimation(el, attrs, scope);
|
||||
|
||||
return createVNode('image', el.id + '', attrs);
|
||||
};
|
||||
|
||||
export function brushSVGTSpan(el: TSpan, scope: BrushScope) {
|
||||
const style = el.style;
|
||||
|
||||
let text = style.text;
|
||||
// Convert to string
|
||||
text != null && (text += '');
|
||||
if (!text || isNaN(style.x) || isNaN(style.y)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// style.font has been normalized by `normalizeTextStyle`.
|
||||
const font = style.font || DEFAULT_FONT;
|
||||
|
||||
// Consider different font display differently in vertical align, we always
|
||||
// set verticalAlign 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;
|
||||
|
||||
const attrs: SVGVNodeAttrs = {
|
||||
'dominant-baseline': 'central',
|
||||
'text-anchor': textAlign
|
||||
};
|
||||
|
||||
if (hasSeparateFont(style)) {
|
||||
// Set separate font attributes if possible. Or some platform like PowerPoint may not support it.
|
||||
let separatedFontStr = '';
|
||||
const fontStyle = style.fontStyle;
|
||||
const fontSize = parseFontSize(style.fontSize);
|
||||
if (!parseFloat(fontSize)) { // is 0px
|
||||
return;
|
||||
}
|
||||
|
||||
const fontFamily = style.fontFamily || DEFAULT_FONT_FAMILY;
|
||||
const fontWeight = style.fontWeight;
|
||||
separatedFontStr += `font-size:${fontSize};font-family:${fontFamily};`;
|
||||
|
||||
// TODO reduce the attribute to set. But should it inherit from the container element?
|
||||
if (fontStyle && fontStyle !== 'normal') {
|
||||
separatedFontStr += `font-style:${fontStyle};`;
|
||||
}
|
||||
if (fontWeight && fontWeight !== 'normal') {
|
||||
separatedFontStr += `font-weight:${fontWeight};`;
|
||||
}
|
||||
attrs.style = separatedFontStr;
|
||||
}
|
||||
else {
|
||||
// Use set font manually
|
||||
attrs.style = `font: ${font}`;
|
||||
}
|
||||
|
||||
|
||||
if (text.match(/\s/)) {
|
||||
// only enabled when have space in text.
|
||||
attrs['xml:space'] = 'preserve';
|
||||
}
|
||||
if (x) {
|
||||
attrs.x = x;
|
||||
}
|
||||
if (y) {
|
||||
attrs.y = y;
|
||||
}
|
||||
setTransform(attrs, el.transform);
|
||||
setStyleAttrs(attrs, style, el, scope);
|
||||
setMetaData(attrs, el);
|
||||
|
||||
scope.animation && createCSSAnimation(el, attrs, scope);
|
||||
|
||||
return createVNode('text', el.id + '', attrs, undefined, text);
|
||||
}
|
||||
|
||||
export function brush(el: Displayable, scope: BrushScope): SVGVNode {
|
||||
if (el instanceof Path) {
|
||||
return brushSVGPath(el, scope);
|
||||
}
|
||||
else if (el instanceof ZRImage) {
|
||||
return brushSVGImage(el, scope);
|
||||
}
|
||||
else if (el instanceof TSpan) {
|
||||
return brushSVGTSpan(el, scope);
|
||||
}
|
||||
}
|
||||
|
||||
function setShadow(
|
||||
el: Displayable,
|
||||
attrs: SVGVNodeAttrs,
|
||||
scope: BrushScope
|
||||
) {
|
||||
const style = el.style;
|
||||
if (hasShadow(style)) {
|
||||
const shadowKey = getShadowKey(el);
|
||||
const shadowCache = scope.shadowCache;
|
||||
let shadowId = shadowCache[shadowKey];
|
||||
if (!shadowId) {
|
||||
const globalScale = el.getGlobalScale();
|
||||
const scaleX = globalScale[0];
|
||||
const scaleY = globalScale[1];
|
||||
if (!scaleX || !scaleY) {
|
||||
return;
|
||||
}
|
||||
|
||||
const offsetX = style.shadowOffsetX || 0;
|
||||
const offsetY = style.shadowOffsetY || 0;
|
||||
const blur = style.shadowBlur;
|
||||
const {opacity, color} = normalizeColor(style.shadowColor);
|
||||
const stdDx = blur / 2 / scaleX;
|
||||
const stdDy = blur / 2 / scaleY;
|
||||
const stdDeviation = stdDx + ' ' + stdDy;
|
||||
// Use a simple prefix to reduce the size
|
||||
shadowId = scope.zrId + '-s' + scope.shadowIdx++;
|
||||
scope.defs[shadowId] = createVNode(
|
||||
'filter', shadowId,
|
||||
{
|
||||
'id': shadowId,
|
||||
'x': '-100%',
|
||||
'y': '-100%',
|
||||
'width': '300%',
|
||||
'height': '300%'
|
||||
},
|
||||
[
|
||||
createVNode('feDropShadow', '', {
|
||||
'dx': offsetX / scaleX,
|
||||
'dy': offsetY / scaleY,
|
||||
'stdDeviation': stdDeviation,
|
||||
'flood-color': color,
|
||||
'flood-opacity': opacity
|
||||
})
|
||||
]
|
||||
);
|
||||
shadowCache[shadowKey] = shadowId;
|
||||
}
|
||||
attrs.filter = getIdURL(shadowId);
|
||||
}
|
||||
}
|
||||
|
||||
export function setGradient(
|
||||
style: PathStyleProps,
|
||||
attrs: SVGVNodeAttrs,
|
||||
target: 'fill' | 'stroke',
|
||||
scope: BrushScope
|
||||
) {
|
||||
const val = style[target] as GradientObject;
|
||||
let gradientTag;
|
||||
let gradientAttrs: SVGVNodeAttrs = {
|
||||
'gradientUnits': val.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]
|
||||
};
|
||||
if (isLinearGradient(val)) {
|
||||
gradientTag = 'linearGradient';
|
||||
gradientAttrs.x1 = val.x;
|
||||
gradientAttrs.y1 = val.y;
|
||||
gradientAttrs.x2 = val.x2;
|
||||
gradientAttrs.y2 = val.y2;
|
||||
}
|
||||
else if (isRadialGradient(val)) {
|
||||
gradientTag = 'radialGradient';
|
||||
gradientAttrs.cx = retrieve2(val.x, 0.5);
|
||||
gradientAttrs.cy = retrieve2(val.y, 0.5);
|
||||
gradientAttrs.r = retrieve2(val.r, 0.5);
|
||||
}
|
||||
else {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
logError('Illegal gradient type.');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const colors = val.colorStops;
|
||||
|
||||
const colorStops = [];
|
||||
for (let i = 0, len = colors.length; i < len; ++i) {
|
||||
const 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);
|
||||
|
||||
const stopsAttrs: SVGVNodeAttrs = {
|
||||
'offset': offset
|
||||
};
|
||||
// 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
|
||||
|
||||
stopsAttrs['stop-color'] = color;
|
||||
if (opacity < 1) {
|
||||
stopsAttrs['stop-opacity'] = opacity;
|
||||
}
|
||||
colorStops.push(
|
||||
createVNode('stop', i + '', stopsAttrs)
|
||||
);
|
||||
}
|
||||
|
||||
// Use the whole html as cache key.
|
||||
const gradientVNode = createVNode(gradientTag, '', gradientAttrs, colorStops);
|
||||
const gradientKey = vNodeToString(gradientVNode);
|
||||
const gradientCache = scope.gradientCache;
|
||||
let gradientId = gradientCache[gradientKey];
|
||||
if (!gradientId) {
|
||||
gradientId = scope.zrId + '-g' + scope.gradientIdx++;
|
||||
gradientCache[gradientKey] = gradientId;
|
||||
|
||||
gradientAttrs.id = gradientId;
|
||||
scope.defs[gradientId] = createVNode(
|
||||
gradientTag, gradientId, gradientAttrs, colorStops
|
||||
);
|
||||
}
|
||||
|
||||
attrs[target] = getIdURL(gradientId);
|
||||
}
|
||||
|
||||
export function setPattern(
|
||||
el: Displayable,
|
||||
attrs: SVGVNodeAttrs,
|
||||
target: 'fill' | 'stroke',
|
||||
scope: BrushScope
|
||||
) {
|
||||
const val = el.style[target] as ImagePatternObject | SVGPatternObject;
|
||||
const boundingRect = el.getBoundingRect();
|
||||
const patternAttrs: SVGVNodeAttrs = {};
|
||||
const repeat = (val as ImagePatternObject).repeat;
|
||||
const noRepeat = repeat === 'no-repeat';
|
||||
const repeatX = repeat === 'repeat-x';
|
||||
const repeatY = repeat === 'repeat-y';
|
||||
let child: SVGVNode;
|
||||
if (isImagePattern(val)) {
|
||||
let imageWidth = val.imageWidth;
|
||||
let imageHeight = val.imageHeight;
|
||||
let imageSrc;
|
||||
const patternImage = val.image;
|
||||
if (isString(patternImage)) {
|
||||
imageSrc = patternImage;
|
||||
}
|
||||
else if (isImageLike(patternImage)) {
|
||||
imageSrc = patternImage.src;
|
||||
}
|
||||
else if (isCanvasLike(patternImage)) {
|
||||
imageSrc = patternImage.toDataURL();
|
||||
}
|
||||
|
||||
if (typeof Image === 'undefined') {
|
||||
const errMsg = 'Image width/height must been given explictly in svg-ssr renderer.';
|
||||
assert(imageWidth, errMsg);
|
||||
assert(imageHeight, errMsg);
|
||||
}
|
||||
else if (imageWidth == null || imageHeight == null) {
|
||||
// TODO
|
||||
const setSizeToVNode = (vNode: SVGVNode, img: ImageLike) => {
|
||||
if (vNode) {
|
||||
const svgEl = vNode.elm as SVGElement;
|
||||
let width = imageWidth || img.width;
|
||||
let height = imageHeight || img.height;
|
||||
if (vNode.tag === 'pattern') {
|
||||
if (repeatX) {
|
||||
height = 1;
|
||||
width /= boundingRect.width;
|
||||
}
|
||||
else if (repeatY) {
|
||||
width = 1;
|
||||
height /= boundingRect.height;
|
||||
}
|
||||
}
|
||||
vNode.attrs.width = width;
|
||||
vNode.attrs.height = height;
|
||||
if (svgEl) {
|
||||
svgEl.setAttribute('width', width as any);
|
||||
svgEl.setAttribute('height', height as any);
|
||||
}
|
||||
}
|
||||
};
|
||||
const createdImage = createOrUpdateImage(
|
||||
imageSrc, null, el, (img) => {
|
||||
noRepeat || setSizeToVNode(patternVNode, img);
|
||||
setSizeToVNode(child, img);
|
||||
}
|
||||
);
|
||||
if (createdImage && createdImage.width && createdImage.height) {
|
||||
// Loaded before
|
||||
imageWidth = imageWidth || createdImage.width;
|
||||
imageHeight = imageHeight || createdImage.height;
|
||||
}
|
||||
}
|
||||
|
||||
child = createVNode(
|
||||
'image',
|
||||
'img',
|
||||
{
|
||||
href: imageSrc,
|
||||
width: imageWidth,
|
||||
height: imageHeight
|
||||
}
|
||||
);
|
||||
patternAttrs.width = imageWidth;
|
||||
patternAttrs.height = imageHeight;
|
||||
}
|
||||
else if (val.svgElement) { // Only string supported in SSR.
|
||||
// TODO it's not so good to use textContent as innerHTML
|
||||
child = clone(val.svgElement);
|
||||
patternAttrs.width = val.svgWidth;
|
||||
patternAttrs.height = val.svgHeight;
|
||||
}
|
||||
if (!child) {
|
||||
return;
|
||||
}
|
||||
|
||||
let patternWidth;
|
||||
let patternHeight;
|
||||
if (noRepeat) {
|
||||
patternWidth = patternHeight = 1;
|
||||
}
|
||||
else if (repeatX) {
|
||||
patternHeight = 1;
|
||||
patternWidth = (patternAttrs.width as number) / boundingRect.width;
|
||||
}
|
||||
else if (repeatY) {
|
||||
patternWidth = 1;
|
||||
patternHeight = (patternAttrs.height as number) / boundingRect.height;
|
||||
}
|
||||
else {
|
||||
patternAttrs.patternUnits = 'userSpaceOnUse';
|
||||
}
|
||||
|
||||
if (patternWidth != null && !isNaN(patternWidth)) {
|
||||
patternAttrs.width = patternWidth;
|
||||
}
|
||||
if (patternHeight != null && !isNaN(patternHeight)) {
|
||||
patternAttrs.height = patternHeight;
|
||||
}
|
||||
|
||||
const patternTransform = getSRTTransformString(val);
|
||||
patternTransform && (patternAttrs.patternTransform = patternTransform);
|
||||
|
||||
// Use the whole html as cache key.
|
||||
let patternVNode = createVNode(
|
||||
'pattern',
|
||||
'',
|
||||
patternAttrs,
|
||||
[child]
|
||||
);
|
||||
const patternKey = vNodeToString(patternVNode);
|
||||
const patternCache = scope.patternCache;
|
||||
let patternId = patternCache[patternKey];
|
||||
if (!patternId) {
|
||||
patternId = scope.zrId + '-p' + scope.patternIdx++;
|
||||
patternCache[patternKey] = patternId;
|
||||
patternAttrs.id = patternId;
|
||||
patternVNode = scope.defs[patternId] = createVNode(
|
||||
'pattern',
|
||||
patternId,
|
||||
patternAttrs,
|
||||
[child]
|
||||
);
|
||||
}
|
||||
|
||||
attrs[target] = getIdURL(patternId);
|
||||
}
|
||||
|
||||
export function setClipPath(
|
||||
clipPath: Path,
|
||||
attrs: SVGVNodeAttrs,
|
||||
scope: BrushScope
|
||||
) {
|
||||
const {clipPathCache, defs} = scope;
|
||||
let clipPathId = clipPathCache[clipPath.id];
|
||||
if (!clipPathId) {
|
||||
clipPathId = scope.zrId + '-c' + scope.clipPathIdx++;
|
||||
const clipPathAttrs: SVGVNodeAttrs = {
|
||||
id: clipPathId
|
||||
};
|
||||
|
||||
clipPathCache[clipPath.id] = clipPathId;
|
||||
defs[clipPathId] = createVNode(
|
||||
'clipPath', clipPathId, clipPathAttrs,
|
||||
[brushSVGPath(clipPath, scope)]
|
||||
);
|
||||
}
|
||||
attrs['clip-path'] = getIdURL(clipPathId);
|
||||
}
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
// Shared methods of svg and svg-ssr
|
||||
|
||||
import { MatrixArray } from '../core/matrix';
|
||||
import Transformable, { TransformProp } from '../core/Transformable';
|
||||
import { RADIAN_TO_DEGREE, retrieve2, logError } from '../core/util';
|
||||
import Displayable from '../graphic/Displayable';
|
||||
import { GradientObject } from '../graphic/Gradient';
|
||||
import { LinearGradientObject } from '../graphic/LinearGradient';
|
||||
import Path from '../graphic/Path';
|
||||
import { ImagePatternObject, PatternObject, SVGPatternObject } from '../graphic/Pattern';
|
||||
import { RadialGradientObject } from '../graphic/RadialGradient';
|
||||
import { parse } from '../tool/color';
|
||||
|
||||
const mathRound = Math.round;
|
||||
|
||||
export function normalizeColor(color: string): { color: string; opacity: number; } {
|
||||
let opacity;
|
||||
if (!color || color === 'transparent') {
|
||||
color = 'none';
|
||||
}
|
||||
else if (typeof color === 'string' && color.indexOf('rgba') > -1) {
|
||||
const arr = parse(color);
|
||||
if (arr) {
|
||||
// TODO use hex?
|
||||
color = 'rgb(' + arr[0] + ',' + arr[1] + ',' + arr[2] + ')';
|
||||
opacity = arr[3];
|
||||
}
|
||||
}
|
||||
return {
|
||||
color,
|
||||
opacity: opacity == null ? 1 : opacity
|
||||
};
|
||||
}
|
||||
const EPSILON = 1e-4;
|
||||
export function isAroundZero(transform: number) {
|
||||
return transform < EPSILON && transform > -EPSILON;
|
||||
}
|
||||
|
||||
export function round3(transform: number) {
|
||||
return mathRound(transform * 1e3) / 1e3;
|
||||
}
|
||||
export function round4(transform: number) {
|
||||
return mathRound(transform * 1e4) / 1e4;
|
||||
}
|
||||
export function round1(transform: number) {
|
||||
return mathRound(transform * 10) / 10;
|
||||
}
|
||||
|
||||
export function getMatrixStr(m: MatrixArray) {
|
||||
return 'matrix('
|
||||
// Avoid large string of matrix
|
||||
// PENDING If have precision issue when scaled
|
||||
+ round3(m[0]) + ','
|
||||
+ round3(m[1]) + ','
|
||||
+ round3(m[2]) + ','
|
||||
+ round3(m[3]) + ','
|
||||
+ round4(m[4]) + ','
|
||||
+ round4(m[5])
|
||||
+ ')';
|
||||
}
|
||||
|
||||
export const TEXT_ALIGN_TO_ANCHOR = {
|
||||
left: 'start',
|
||||
right: 'end',
|
||||
center: 'middle',
|
||||
middle: 'middle'
|
||||
};
|
||||
|
||||
export function adjustTextY(y: number, lineHeight: number, textBaseline: CanvasTextBaseline): number {
|
||||
// TODO Other baselines.
|
||||
if (textBaseline === 'top') {
|
||||
y += lineHeight / 2;
|
||||
}
|
||||
else if (textBaseline === 'bottom') {
|
||||
y -= lineHeight / 2;
|
||||
}
|
||||
return y;
|
||||
}
|
||||
|
||||
|
||||
export function hasShadow(style: Displayable['style']) {
|
||||
// TODO: textBoxShadowBlur is not supported yet
|
||||
return style
|
||||
&& (style.shadowBlur || style.shadowOffsetX || style.shadowOffsetY);
|
||||
}
|
||||
|
||||
export function getShadowKey(displayable: Displayable) {
|
||||
const style = displayable.style;
|
||||
const globalScale = displayable.getGlobalScale();
|
||||
return [
|
||||
style.shadowColor,
|
||||
(style.shadowBlur || 0).toFixed(2), // Reduce the precision
|
||||
(style.shadowOffsetX || 0).toFixed(2),
|
||||
(style.shadowOffsetY || 0).toFixed(2),
|
||||
globalScale[0],
|
||||
globalScale[1]
|
||||
].join(',');
|
||||
}
|
||||
|
||||
export function getClipPathsKey(clipPaths: Path[]) {
|
||||
let key: number[] = [];
|
||||
if (clipPaths) {
|
||||
for (let i = 0; i < clipPaths.length; i++) {
|
||||
const clipPath = clipPaths[i];
|
||||
key.push(clipPath.id);
|
||||
}
|
||||
}
|
||||
return key.join(',');
|
||||
}
|
||||
|
||||
export function isImagePattern(val: any): val is ImagePatternObject {
|
||||
return val && (!!(val as ImagePatternObject).image);
|
||||
}
|
||||
export function isSVGPattern(val: any): val is SVGPatternObject {
|
||||
return val && (!!(val as SVGPatternObject).svgElement);
|
||||
}
|
||||
export function isPattern(val: any): val is PatternObject {
|
||||
return isImagePattern(val) || isSVGPattern(val);
|
||||
}
|
||||
|
||||
export function isLinearGradient(val: GradientObject): val is LinearGradientObject {
|
||||
return val.type === 'linear';
|
||||
}
|
||||
|
||||
export function isRadialGradient(val: GradientObject): val is RadialGradientObject {
|
||||
return val.type === 'radial';
|
||||
}
|
||||
|
||||
export function isGradient(val: any): val is GradientObject {
|
||||
return val && (
|
||||
(val as GradientObject).type === 'linear'
|
||||
|| (val as GradientObject).type === 'radial'
|
||||
);
|
||||
}
|
||||
|
||||
export function getIdURL(id: string) {
|
||||
return `url(#${id})`;
|
||||
}
|
||||
|
||||
export function getPathPrecision(el: Path) {
|
||||
const scale = el.getGlobalScale();
|
||||
const size = Math.max(scale[0], scale[1]);
|
||||
return Math.max(Math.ceil(Math.log(size) / Math.log(10)), 1);
|
||||
}
|
||||
|
||||
export function getSRTTransformString(
|
||||
transform: Partial<Pick<Transformable, TransformProp>>
|
||||
) {
|
||||
const x = transform.x || 0;
|
||||
const y = transform.y || 0;
|
||||
const rotation = (transform.rotation || 0) * RADIAN_TO_DEGREE;
|
||||
const scaleX = retrieve2(transform.scaleX, 1);
|
||||
const scaleY = retrieve2(transform.scaleY, 1);
|
||||
const skewX = transform.skewX || 0;
|
||||
const skewY = transform.skewY || 0;
|
||||
const res = [];
|
||||
if (x || y) {
|
||||
// TODO not using px unit?
|
||||
res.push(`translate(${x}px,${y}px)`);
|
||||
}
|
||||
if (rotation) {
|
||||
res.push(`rotate(${rotation})`);
|
||||
}
|
||||
if (scaleX !== 1 || scaleY !== 1) {
|
||||
res.push(`scale(${scaleX},${scaleY})`);
|
||||
}
|
||||
if (skewX || skewY) {
|
||||
res.push(`skew(${mathRound(skewX * RADIAN_TO_DEGREE)}deg, ${mathRound(skewY * RADIAN_TO_DEGREE)}deg)`);
|
||||
}
|
||||
|
||||
return res.join(' ');
|
||||
}
|
||||
|
||||
export const encodeBase64 = (function () {
|
||||
if (typeof Buffer !== 'undefined' && typeof Buffer.from === 'function') {
|
||||
return function (str: string) {
|
||||
return Buffer.from(str).toString('base64');
|
||||
};
|
||||
}
|
||||
if (typeof btoa === 'function' && typeof unescape === 'function' && typeof encodeURIComponent === 'function') {
|
||||
return function (str: string) {
|
||||
return btoa(unescape(encodeURIComponent(str)));
|
||||
};
|
||||
}
|
||||
return function (str: string): string {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
logError('Base64 isn\'t natively supported in the current environment.');
|
||||
}
|
||||
return null;
|
||||
};
|
||||
})();
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
|
||||
import Path, { DEFAULT_PATH_STYLE, PathStyleProps } from '../graphic/Path';
|
||||
import ZRImage, { ImageStyleProps } from '../graphic/Image';
|
||||
import TSpan, { TSpanStyleProps } from '../graphic/TSpan';
|
||||
import { getLineDash } from '../canvas/dashStyle';
|
||||
import { map } from '../core/util';
|
||||
import { normalizeColor } from './helper';
|
||||
|
||||
type AllStyleOption = PathStyleProps | TSpanStyleProps | ImageStyleProps;
|
||||
|
||||
const NONE = 'none';
|
||||
const mathRound = Math.round;
|
||||
|
||||
function pathHasFill(style: AllStyleOption): style is PathStyleProps {
|
||||
const fill = (style as PathStyleProps).fill;
|
||||
return fill != null && fill !== NONE;
|
||||
}
|
||||
|
||||
function pathHasStroke(style: AllStyleOption): style is PathStyleProps {
|
||||
const stroke = (style as PathStyleProps).stroke;
|
||||
return stroke != null && stroke !== NONE;
|
||||
}
|
||||
|
||||
const strokeProps = ['lineCap', 'miterLimit', 'lineJoin'] as const;
|
||||
const svgStrokeProps = map(strokeProps, prop => `stroke-${prop.toLowerCase()}`);
|
||||
|
||||
export default function mapStyleToAttrs(
|
||||
updateAttr: (key: string, val: string | number) => void,
|
||||
style: AllStyleOption,
|
||||
el: Path | TSpan | ZRImage,
|
||||
/**
|
||||
* Will try not to set the attribute if it's using default value if not using forceUpdate.
|
||||
* Mainly for reduce the generated size in svg-ssr mode.
|
||||
*/
|
||||
forceUpdate: boolean
|
||||
): void {
|
||||
const opacity = style.opacity == null ? 1 : style.opacity;
|
||||
|
||||
// only set opacity. stroke and fill cannot be applied to svg image
|
||||
if (el instanceof ZRImage) {
|
||||
updateAttr('opacity', opacity);
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathHasFill(style)) {
|
||||
const fill = normalizeColor(style.fill as string);
|
||||
updateAttr('fill', fill.color);
|
||||
const fillOpacity = style.fillOpacity != null
|
||||
? style.fillOpacity * fill.opacity * opacity
|
||||
: fill.opacity * opacity;
|
||||
if (forceUpdate || fillOpacity < 1) {
|
||||
updateAttr('fill-opacity', fillOpacity);
|
||||
}
|
||||
}
|
||||
else {
|
||||
updateAttr('fill', NONE);
|
||||
}
|
||||
|
||||
if (pathHasStroke(style)) {
|
||||
const stroke = normalizeColor(style.stroke as string);
|
||||
updateAttr('stroke', stroke.color);
|
||||
const strokeScale = style.strokeNoScale
|
||||
? (el as Path).getLineScale()
|
||||
: 1;
|
||||
const strokeWidth = (strokeScale ? (style.lineWidth || 0) / strokeScale : 0);
|
||||
const strokeOpacity = style.strokeOpacity != null
|
||||
? style.strokeOpacity * stroke.opacity * opacity
|
||||
: stroke.opacity * opacity;
|
||||
const strokeFirst = style.strokeFirst;
|
||||
|
||||
if (forceUpdate || strokeWidth !== 1) {
|
||||
updateAttr('stroke-width', strokeWidth);
|
||||
}
|
||||
// stroke then fill for text; fill then stroke for others
|
||||
if (forceUpdate || strokeFirst) {
|
||||
updateAttr('paint-order', strokeFirst ? 'stroke' : 'fill');
|
||||
}
|
||||
if (forceUpdate || strokeOpacity < 1) {
|
||||
updateAttr('stroke-opacity', strokeOpacity);
|
||||
}
|
||||
|
||||
if (style.lineDash) {
|
||||
let [lineDash, lineDashOffset] = getLineDash(el);
|
||||
if (lineDash) {
|
||||
lineDashOffset = mathRound(lineDashOffset || 0);
|
||||
updateAttr('stroke-dasharray', lineDash.join(','));
|
||||
if (lineDashOffset || forceUpdate) {
|
||||
updateAttr('stroke-dashoffset', lineDashOffset);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (forceUpdate) {
|
||||
// Reset if force update.
|
||||
updateAttr('stroke-dasharray', NONE);
|
||||
}
|
||||
|
||||
// PENDING reset
|
||||
for (let i = 0; i < strokeProps.length; i++) {
|
||||
const propName = strokeProps[i];
|
||||
if (forceUpdate || style[propName] !== DEFAULT_PATH_STYLE[propName]) {
|
||||
const val = style[propName] || DEFAULT_PATH_STYLE[propName];
|
||||
// TODO reset
|
||||
val && updateAttr(svgStrokeProps[i], val);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (forceUpdate) {
|
||||
updateAttr('stroke', NONE);
|
||||
}
|
||||
}
|
||||
+321
@@ -0,0 +1,321 @@
|
||||
/**
|
||||
* Virtual DOM patching
|
||||
* Modified from snabbdom https://github.com/snabbdom/snabbdom/blob/master/src/init.ts
|
||||
*
|
||||
* The design has been simplified to focus on the purpose in SVG rendering in SVG.
|
||||
*
|
||||
* Licensed under the MIT License
|
||||
* https://github.com/paldepind/snabbdom/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
import { isArray, isObject } from '../core/util';
|
||||
import { createElement, createVNode, SVGVNode, XMLNS, XML_NAMESPACE, XLINKNS } from './core';
|
||||
import * as api from './domapi';
|
||||
|
||||
const colonChar = 58;
|
||||
const xChar = 120;
|
||||
const emptyNode = createVNode('', '');
|
||||
|
||||
type NonUndefined<T> = T extends undefined ? never : T;
|
||||
|
||||
function isUndef(s: any): boolean {
|
||||
return s === undefined;
|
||||
}
|
||||
|
||||
function isDef<A>(s: A): s is NonUndefined<A> {
|
||||
return s !== undefined;
|
||||
}
|
||||
|
||||
function createKeyToOldIdx(
|
||||
children: SVGVNode[],
|
||||
beginIdx: number,
|
||||
endIdx: number
|
||||
): KeyToIndexMap {
|
||||
const map: KeyToIndexMap = {};
|
||||
for (let i = beginIdx; i <= endIdx; ++i) {
|
||||
const key = children[i].key;
|
||||
if (key !== undefined) {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
if (map[key] != null) {
|
||||
console.error(`Duplicate key ${key}`);
|
||||
}
|
||||
}
|
||||
map[key] = i;
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
function sameVnode(vnode1: SVGVNode, vnode2: SVGVNode): boolean {
|
||||
const isSameKey = vnode1.key === vnode2.key;
|
||||
const isSameTag = vnode1.tag === vnode2.tag;
|
||||
|
||||
return isSameTag && isSameKey;
|
||||
}
|
||||
|
||||
type KeyToIndexMap = { [key: string]: number };
|
||||
|
||||
function createElm(vnode: SVGVNode): Node {
|
||||
let i: any;
|
||||
const children = vnode.children;
|
||||
const tag = vnode.tag;
|
||||
// if (tag === '!') {
|
||||
// if (isUndef(vnode.text)) {
|
||||
// vnode.text = '';
|
||||
// }
|
||||
// vnode.elm = api.createComment(vnode.text!);
|
||||
// }
|
||||
// else
|
||||
if (isDef(tag)) {
|
||||
const elm = (vnode.elm = createElement(tag));
|
||||
|
||||
updateAttrs(emptyNode, vnode);
|
||||
|
||||
if (isArray(children)) {
|
||||
for (i = 0; i < children.length; ++i) {
|
||||
const ch = children[i];
|
||||
if (ch != null) {
|
||||
api.appendChild(elm, createElm(ch));
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (isDef(vnode.text) && !isObject(vnode.text)) {
|
||||
api.appendChild(elm, api.createTextNode(vnode.text));
|
||||
}
|
||||
}
|
||||
else {
|
||||
vnode.elm = api.createTextNode(vnode.text!);
|
||||
}
|
||||
return vnode.elm;
|
||||
}
|
||||
|
||||
function addVnodes(
|
||||
parentElm: Node,
|
||||
before: Node | null,
|
||||
vnodes: SVGVNode[],
|
||||
startIdx: number,
|
||||
endIdx: number
|
||||
) {
|
||||
for (; startIdx <= endIdx; ++startIdx) {
|
||||
const ch = vnodes[startIdx];
|
||||
if (ch != null) {
|
||||
api.insertBefore(parentElm, createElm(ch), before);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function removeVnodes(parentElm: Node, vnodes: SVGVNode[], startIdx: number, endIdx: number): void {
|
||||
for (; startIdx <= endIdx; ++startIdx) {
|
||||
const ch = vnodes[startIdx];
|
||||
if (ch != null) {
|
||||
if (isDef(ch.tag)) {
|
||||
const parent = api.parentNode(ch.elm);
|
||||
api.removeChild(parent, ch.elm);
|
||||
}
|
||||
else {
|
||||
// Text node
|
||||
api.removeChild(parentElm, ch.elm!);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function updateAttrs(oldVnode: SVGVNode, vnode: SVGVNode): void {
|
||||
let key: string;
|
||||
const elm = vnode.elm as SVGElement;
|
||||
const oldAttrs = oldVnode && oldVnode.attrs || {};
|
||||
const attrs = vnode.attrs || {};
|
||||
|
||||
if (oldAttrs === attrs) {
|
||||
return;
|
||||
}
|
||||
|
||||
// update modified attributes, add new attributes
|
||||
// eslint-disable-next-line
|
||||
for (key in attrs) {
|
||||
const cur = attrs[key];
|
||||
const old = oldAttrs[key];
|
||||
if (old !== cur) {
|
||||
if (cur === true) {
|
||||
elm.setAttribute(key, '');
|
||||
}
|
||||
else if (cur === false) {
|
||||
elm.removeAttribute(key);
|
||||
}
|
||||
else {
|
||||
if (key === 'style') {
|
||||
elm.style.cssText = cur as string;
|
||||
}
|
||||
else if (key.charCodeAt(0) !== xChar) {
|
||||
elm.setAttribute(key, cur as any);
|
||||
}
|
||||
// TODO
|
||||
else if (key === 'xmlns:xlink' || key === 'xmlns') {
|
||||
elm.setAttributeNS(XMLNS, key, cur as any);
|
||||
}
|
||||
else if (key.charCodeAt(3) === colonChar) {
|
||||
// Assume xml namespace
|
||||
elm.setAttributeNS(XML_NAMESPACE, key, cur as any);
|
||||
}
|
||||
else if (key.charCodeAt(5) === colonChar) {
|
||||
// Assume xlink namespace
|
||||
elm.setAttributeNS(XLINKNS, key, cur as any);
|
||||
}
|
||||
else {
|
||||
elm.setAttribute(key, cur as any);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// remove removed attributes
|
||||
// use `in` operator since the previous `for` iteration uses it (.i.e. add even attributes with undefined value)
|
||||
// the other option is to remove all attributes with value == undefined
|
||||
for (key in oldAttrs) {
|
||||
if (!(key in attrs)) {
|
||||
elm.removeAttribute(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function updateChildren(parentElm: Node, oldCh: SVGVNode[], newCh: SVGVNode[]) {
|
||||
let oldStartIdx = 0;
|
||||
let newStartIdx = 0;
|
||||
let oldEndIdx = oldCh.length - 1;
|
||||
let oldStartVnode = oldCh[0];
|
||||
let oldEndVnode = oldCh[oldEndIdx];
|
||||
let newEndIdx = newCh.length - 1;
|
||||
let newStartVnode = newCh[0];
|
||||
let newEndVnode = newCh[newEndIdx];
|
||||
let oldKeyToIdx: KeyToIndexMap | undefined;
|
||||
let idxInOld: number;
|
||||
let elmToMove: SVGVNode;
|
||||
let before: any;
|
||||
|
||||
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
|
||||
if (oldStartVnode == null) {
|
||||
oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left
|
||||
}
|
||||
else if (oldEndVnode == null) {
|
||||
oldEndVnode = oldCh[--oldEndIdx];
|
||||
}
|
||||
else if (newStartVnode == null) {
|
||||
newStartVnode = newCh[++newStartIdx];
|
||||
}
|
||||
else if (newEndVnode == null) {
|
||||
newEndVnode = newCh[--newEndIdx];
|
||||
}
|
||||
else if (sameVnode(oldStartVnode, newStartVnode)) {
|
||||
patchVnode(oldStartVnode, newStartVnode);
|
||||
oldStartVnode = oldCh[++oldStartIdx];
|
||||
newStartVnode = newCh[++newStartIdx];
|
||||
}
|
||||
else if (sameVnode(oldEndVnode, newEndVnode)) {
|
||||
patchVnode(oldEndVnode, newEndVnode);
|
||||
oldEndVnode = oldCh[--oldEndIdx];
|
||||
newEndVnode = newCh[--newEndIdx];
|
||||
}
|
||||
else if (sameVnode(oldStartVnode, newEndVnode)) {
|
||||
// Vnode moved right
|
||||
patchVnode(oldStartVnode, newEndVnode);
|
||||
api.insertBefore(parentElm, oldStartVnode.elm!, api.nextSibling(oldEndVnode.elm!));
|
||||
oldStartVnode = oldCh[++oldStartIdx];
|
||||
newEndVnode = newCh[--newEndIdx];
|
||||
}
|
||||
else if (sameVnode(oldEndVnode, newStartVnode)) {
|
||||
// Vnode moved left
|
||||
patchVnode(oldEndVnode, newStartVnode);
|
||||
api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!);
|
||||
oldEndVnode = oldCh[--oldEndIdx];
|
||||
newStartVnode = newCh[++newStartIdx];
|
||||
}
|
||||
else {
|
||||
if (isUndef(oldKeyToIdx)) {
|
||||
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
|
||||
}
|
||||
idxInOld = oldKeyToIdx[newStartVnode.key];
|
||||
if (isUndef(idxInOld)) {
|
||||
// New element
|
||||
api.insertBefore(parentElm, createElm(newStartVnode), oldStartVnode.elm!);
|
||||
}
|
||||
else {
|
||||
elmToMove = oldCh[idxInOld];
|
||||
if (elmToMove.tag !== newStartVnode.tag) {
|
||||
api.insertBefore(parentElm, createElm(newStartVnode), oldStartVnode.elm!);
|
||||
}
|
||||
else {
|
||||
patchVnode(elmToMove, newStartVnode);
|
||||
oldCh[idxInOld] = undefined;
|
||||
api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!);
|
||||
}
|
||||
}
|
||||
newStartVnode = newCh[++newStartIdx];
|
||||
}
|
||||
}
|
||||
if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
|
||||
if (oldStartIdx > oldEndIdx) {
|
||||
before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
|
||||
addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx);
|
||||
}
|
||||
else {
|
||||
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function patchVnode(oldVnode: SVGVNode, vnode: SVGVNode) {
|
||||
const elm = (vnode.elm = oldVnode.elm)!;
|
||||
const oldCh = oldVnode.children;
|
||||
const ch = vnode.children;
|
||||
if (oldVnode === vnode) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateAttrs(oldVnode, vnode);
|
||||
|
||||
if (isUndef(vnode.text)) {
|
||||
if (isDef(oldCh) && isDef(ch)) {
|
||||
if (oldCh !== ch) {
|
||||
updateChildren(elm, oldCh, ch);
|
||||
}
|
||||
}
|
||||
else if (isDef(ch)) {
|
||||
if (isDef(oldVnode.text)) {
|
||||
api.setTextContent(elm, '');
|
||||
}
|
||||
addVnodes(elm, null, ch, 0, ch.length - 1);
|
||||
}
|
||||
else if (isDef(oldCh)) {
|
||||
removeVnodes(elm, oldCh, 0, oldCh.length - 1);
|
||||
}
|
||||
else if (isDef(oldVnode.text)) {
|
||||
api.setTextContent(elm, '');
|
||||
}
|
||||
}
|
||||
else if (oldVnode.text !== vnode.text) {
|
||||
if (isDef(oldCh)) {
|
||||
removeVnodes(elm, oldCh, 0, oldCh.length - 1);
|
||||
}
|
||||
api.setTextContent(elm, vnode.text!);
|
||||
}
|
||||
}
|
||||
|
||||
export default function patch(oldVnode: SVGVNode, vnode: SVGVNode): SVGVNode {
|
||||
if (sameVnode(oldVnode, vnode)) {
|
||||
patchVnode(oldVnode, vnode);
|
||||
}
|
||||
else {
|
||||
const elm = oldVnode.elm!;
|
||||
const parent = api.parentNode(elm);
|
||||
|
||||
createElm(vnode);
|
||||
|
||||
if (parent !== null) {
|
||||
api.insertBefore(parent, vnode.elm!, api.nextSibling(elm));
|
||||
removeVnodes(parent, [oldVnode], 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
return vnode;
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import {registerPainter} from '../zrender';
|
||||
import Painter from './Painter';
|
||||
|
||||
registerPainter('svg', Painter);
|
||||
Reference in New Issue
Block a user