123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596 |
- // 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 } 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';
- 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;
- }
- }, style, el, false);
- setShadow(el, attrs, scope);
- }
- 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 buitinShapesDef: 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 = buitinShapesDef[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 {
- if (!el.path) {
- el.createPathProxy();
- }
- const path = el.path;
- if (el.shapeChanged()) {
- path.beginPath();
- el.buildPath(path, el.shape);
- el.pathUpdated();
- }
- const pathVersion = path.getVersion();
- const elExt = el as PathWithSVGBuildPath;
- let svgPathBuilder = elExt.__svgPathBuilder;
- if (elExt.__svgPathVersion !== pathVersion
- || !svgPathBuilder
- || 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);
- scope.animation && createCSSAnimation(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);
- 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 vertial align, we always
- // set vertialAlign as 'middle', and use 'y' to locate text vertically.
- const x = style.x || 0;
- const y = adjustTextY(style.y || 0, getLineHeight(font), style.textBaseline);
- const textAlign = TEXT_ALIGN_TO_ANCHOR[style.textAlign as keyof typeof TEXT_ALIGN_TO_ANCHOR]
- || style.textAlign;
- 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);
- 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);
- }
- }
- 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);
- }
- function setPattern(
- el: Displayable,
- attrs: SVGVNodeAttrs,
- target: 'fill' | 'stroke',
- scope: BrushScope
- ) {
- const val = el.style[target] as ImagePatternObject | SVGPatternObject;
- const patternAttrs: SVGVNodeAttrs = {
- 'patternUnits': 'userSpaceOnUse'
- };
- 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;
- const width = (vNode.attrs.width = imageWidth || img.width);
- const height = (vNode.attrs.height = imageHeight || img.height);
- if (svgEl) {
- svgEl.setAttribute('width', width as any);
- svgEl.setAttribute('height', height as any);
- }
- }
- };
- const createdImage = createOrUpdateImage(
- imageSrc, null, el, (img) => {
- 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;
- }
- patternAttrs.patternTransform = getSRTTransformString(val);
- // 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);
- }
|