package.src.svg.graphic.ts Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of zrender Show documentation
Show all versions of zrender Show documentation
A lightweight graphic library providing 2d draw for Apache ECharts
The newest version!
// 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 = {
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);
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy