1adfdceaabfb74419e40bfaf4f7d6b31ce59da7fd1a7db38a4fce69bdec07ddb1e8341fa8798be19944c6117278bc4c5fcf2c8d55e70cd658c287c017e793b 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596
  1. // TODO
  2. // 1. shadow
  3. // 2. Image: sx, sy, sw, sh
  4. import {
  5. adjustTextY,
  6. getIdURL,
  7. getMatrixStr,
  8. getPathPrecision,
  9. getShadowKey,
  10. getSRTTransformString,
  11. hasShadow,
  12. isAroundZero,
  13. isGradient,
  14. isImagePattern,
  15. isLinearGradient,
  16. isPattern,
  17. isRadialGradient,
  18. normalizeColor,
  19. round4,
  20. TEXT_ALIGN_TO_ANCHOR
  21. } from './helper';
  22. import Path, { PathStyleProps } from '../graphic/Path';
  23. import ZRImage, { ImageStyleProps } from '../graphic/Image';
  24. import { getLineHeight } from '../contain/text';
  25. import TSpan, { TSpanStyleProps } from '../graphic/TSpan';
  26. import SVGPathRebuilder from './SVGPathRebuilder';
  27. import mapStyleToAttrs from './mapStyleToAttrs';
  28. import { SVGVNodeAttrs, createVNode, SVGVNode, vNodeToString, BrushScope } from './core';
  29. import { MatrixArray } from '../core/matrix';
  30. import Displayable from '../graphic/Displayable';
  31. import { assert, clone, isFunction, isString, logError, map, retrieve2 } from '../core/util';
  32. import Polyline from '../graphic/shape/Polyline';
  33. import Polygon from '../graphic/shape/Polygon';
  34. import { GradientObject } from '../graphic/Gradient';
  35. import { ImagePatternObject, SVGPatternObject } from '../graphic/Pattern';
  36. import { createOrUpdateImage } from '../graphic/helper/image';
  37. import { ImageLike } from '../core/types';
  38. import { createCSSAnimation } from './cssAnimation';
  39. import { hasSeparateFont, parseFontSize } from '../graphic/Text';
  40. import { DEFAULT_FONT, DEFAULT_FONT_FAMILY } from '../core/platform';
  41. const round = Math.round;
  42. function isImageLike(val: any): val is HTMLImageElement {
  43. return val && isString(val.src);
  44. }
  45. function isCanvasLike(val: any): val is HTMLCanvasElement {
  46. return val && isFunction(val.toDataURL);
  47. }
  48. type AllStyleOption = PathStyleProps | TSpanStyleProps | ImageStyleProps;
  49. function setStyleAttrs(attrs: SVGVNodeAttrs, style: AllStyleOption, el: Path | TSpan | ZRImage, scope: BrushScope) {
  50. mapStyleToAttrs((key, val) => {
  51. const isFillStroke = key === 'fill' || key === 'stroke';
  52. if (isFillStroke && isGradient(val)) {
  53. setGradient(style, attrs, key, scope);
  54. }
  55. else if (isFillStroke && isPattern(val)) {
  56. setPattern(el, attrs, key, scope);
  57. }
  58. else {
  59. attrs[key] = val;
  60. }
  61. }, style, el, false);
  62. setShadow(el, attrs, scope);
  63. }
  64. function noRotateScale(m: MatrixArray) {
  65. return isAroundZero(m[0] - 1)
  66. && isAroundZero(m[1])
  67. && isAroundZero(m[2])
  68. && isAroundZero(m[3] - 1);
  69. }
  70. function noTranslate(m: MatrixArray) {
  71. return isAroundZero(m[4]) && isAroundZero(m[5]);
  72. }
  73. function setTransform(attrs: SVGVNodeAttrs, m: MatrixArray, compress?: boolean) {
  74. if (m && !(noTranslate(m) && noRotateScale(m))) {
  75. const mul = compress ? 10 : 1e4;
  76. // Use translate possible to reduce the size a bit.
  77. attrs.transform = noRotateScale(m)
  78. ? `translate(${round(m[4] * mul) / mul} ${round(m[5] * mul) / mul})` : getMatrixStr(m);
  79. }
  80. }
  81. type ShapeMapDesc = (string | [string, string])[];
  82. type ConvertShapeToAttr = (shape: any, attrs: SVGVNodeAttrs, mul?: number) => void;
  83. type ShapeValidator = (shape: any) => boolean;
  84. function convertPolyShape(shape: Polygon['shape'], attrs: SVGVNodeAttrs, mul: number) {
  85. const points = shape.points;
  86. const strArr = [];
  87. for (let i = 0; i < points.length; i++) {
  88. strArr.push(round(points[i][0] * mul) / mul);
  89. strArr.push(round(points[i][1] * mul) / mul);
  90. }
  91. attrs.points = strArr.join(' ');
  92. }
  93. function validatePolyShape(shape: Polyline['shape']) {
  94. return !shape.smooth;
  95. }
  96. function createAttrsConvert(desc: ShapeMapDesc): ConvertShapeToAttr {
  97. const normalizedDesc: [string, string][] = map(desc, (item) =>
  98. (typeof item === 'string' ? [item, item] : item)
  99. );
  100. return function (shape, attrs, mul) {
  101. for (let i = 0; i < normalizedDesc.length; i++) {
  102. const item = normalizedDesc[i];
  103. const val = shape[item[0]];
  104. if (val != null) {
  105. attrs[item[1]] = round(val * mul) / mul;
  106. }
  107. }
  108. };
  109. }
  110. const buitinShapesDef: Record<string, [ConvertShapeToAttr, ShapeValidator?]> = {
  111. circle: [createAttrsConvert(['cx', 'cy', 'r'])],
  112. polyline: [convertPolyShape, validatePolyShape],
  113. polygon: [convertPolyShape, validatePolyShape]
  114. // Ignore line because it will be larger.
  115. };
  116. interface PathWithSVGBuildPath extends Path {
  117. __svgPathVersion: number
  118. __svgPathBuilder: SVGPathRebuilder
  119. __svgPathStrokePercent: number
  120. }
  121. function hasShapeAnimation(el: Displayable) {
  122. const animators = el.animators;
  123. for (let i = 0; i < animators.length; i++) {
  124. if (animators[i].targetName === 'shape') {
  125. return true;
  126. }
  127. }
  128. return false;
  129. }
  130. export function brushSVGPath(el: Path, scope: BrushScope) {
  131. const style = el.style;
  132. const shape = el.shape;
  133. const builtinShpDef = buitinShapesDef[el.type];
  134. const attrs: SVGVNodeAttrs = {};
  135. const needsAnimate = scope.animation;
  136. let svgElType = 'path';
  137. const strokePercent = el.style.strokePercent;
  138. const precision = (scope.compress && getPathPrecision(el)) || 4;
  139. // Using SVG builtin shapes if possible
  140. if (builtinShpDef
  141. // Force to use path if it will update later.
  142. // To avoid some animation(like morph) fail
  143. && !scope.willUpdate
  144. && !(builtinShpDef[1] && !builtinShpDef[1](shape))
  145. // use `path` to simplify the animate element creation logic.
  146. && !(needsAnimate && hasShapeAnimation(el))
  147. && !(strokePercent < 1)
  148. ) {
  149. svgElType = el.type;
  150. const mul = Math.pow(10, precision);
  151. builtinShpDef[0](shape, attrs, mul);
  152. }
  153. else {
  154. if (!el.path) {
  155. el.createPathProxy();
  156. }
  157. const path = el.path;
  158. if (el.shapeChanged()) {
  159. path.beginPath();
  160. el.buildPath(path, el.shape);
  161. el.pathUpdated();
  162. }
  163. const pathVersion = path.getVersion();
  164. const elExt = el as PathWithSVGBuildPath;
  165. let svgPathBuilder = elExt.__svgPathBuilder;
  166. if (elExt.__svgPathVersion !== pathVersion
  167. || !svgPathBuilder
  168. || strokePercent !== elExt.__svgPathStrokePercent
  169. ) {
  170. if (!svgPathBuilder) {
  171. svgPathBuilder = elExt.__svgPathBuilder = new SVGPathRebuilder();
  172. }
  173. svgPathBuilder.reset(precision);
  174. path.rebuildPath(svgPathBuilder, strokePercent);
  175. svgPathBuilder.generateStr();
  176. elExt.__svgPathVersion = pathVersion;
  177. elExt.__svgPathStrokePercent = strokePercent;
  178. }
  179. attrs.d = svgPathBuilder.getStr();
  180. }
  181. setTransform(attrs, el.transform);
  182. setStyleAttrs(attrs, style, el, scope);
  183. scope.animation && createCSSAnimation(el, attrs, scope);
  184. return createVNode(svgElType, el.id + '', attrs);
  185. }
  186. export function brushSVGImage(el: ZRImage, scope: BrushScope) {
  187. const style = el.style;
  188. let image = style.image;
  189. if (image && !isString(image)) {
  190. if (isImageLike(image)) {
  191. image = image.src;
  192. }
  193. // heatmap layer in geo may be a canvas
  194. else if (isCanvasLike(image)) {
  195. image = image.toDataURL();
  196. }
  197. }
  198. if (!image) {
  199. return;
  200. }
  201. const x = style.x || 0;
  202. const y = style.y || 0;
  203. const dw = style.width;
  204. const dh = style.height;
  205. const attrs: SVGVNodeAttrs = {
  206. href: image as string,
  207. width: dw,
  208. height: dh
  209. };
  210. if (x) {
  211. attrs.x = x;
  212. }
  213. if (y) {
  214. attrs.y = y;
  215. }
  216. setTransform(attrs, el.transform);
  217. setStyleAttrs(attrs, style, el, scope);
  218. scope.animation && createCSSAnimation(el, attrs, scope);
  219. return createVNode('image', el.id + '', attrs);
  220. };
  221. export function brushSVGTSpan(el: TSpan, scope: BrushScope) {
  222. const style = el.style;
  223. let text = style.text;
  224. // Convert to string
  225. text != null && (text += '');
  226. if (!text || isNaN(style.x) || isNaN(style.y)) {
  227. return;
  228. }
  229. // style.font has been normalized by `normalizeTextStyle`.
  230. const font = style.font || DEFAULT_FONT;
  231. // Consider different font display differently in vertial align, we always
  232. // set vertialAlign as 'middle', and use 'y' to locate text vertically.
  233. const x = style.x || 0;
  234. const y = adjustTextY(style.y || 0, getLineHeight(font), style.textBaseline);
  235. const textAlign = TEXT_ALIGN_TO_ANCHOR[style.textAlign as keyof typeof TEXT_ALIGN_TO_ANCHOR]
  236. || style.textAlign;
  237. const attrs: SVGVNodeAttrs = {
  238. 'dominant-baseline': 'central',
  239. 'text-anchor': textAlign
  240. };
  241. if (hasSeparateFont(style)) {
  242. // Set separate font attributes if possible. Or some platform like PowerPoint may not support it.
  243. let separatedFontStr = '';
  244. const fontStyle = style.fontStyle;
  245. const fontSize = parseFontSize(style.fontSize);
  246. if (!parseFloat(fontSize)) { // is 0px
  247. return;
  248. }
  249. const fontFamily = style.fontFamily || DEFAULT_FONT_FAMILY;
  250. const fontWeight = style.fontWeight;
  251. separatedFontStr += `font-size:${fontSize};font-family:${fontFamily};`;
  252. // TODO reduce the attribute to set. But should it inherit from the container element?
  253. if (fontStyle && fontStyle !== 'normal') {
  254. separatedFontStr += `font-style:${fontStyle};`;
  255. }
  256. if (fontWeight && fontWeight !== 'normal') {
  257. separatedFontStr += `font-weight:${fontWeight};`;
  258. }
  259. attrs.style = separatedFontStr;
  260. }
  261. else {
  262. // Use set font manually
  263. attrs.style = `font: ${font}`;
  264. }
  265. if (text.match(/\s/)) {
  266. // only enabled when have space in text.
  267. attrs['xml:space'] = 'preserve';
  268. }
  269. if (x) {
  270. attrs.x = x;
  271. }
  272. if (y) {
  273. attrs.y = y;
  274. }
  275. setTransform(attrs, el.transform);
  276. setStyleAttrs(attrs, style, el, scope);
  277. scope.animation && createCSSAnimation(el, attrs, scope);
  278. return createVNode('text', el.id + '', attrs, undefined, text);
  279. }
  280. export function brush(el: Displayable, scope: BrushScope): SVGVNode {
  281. if (el instanceof Path) {
  282. return brushSVGPath(el, scope);
  283. }
  284. else if (el instanceof ZRImage) {
  285. return brushSVGImage(el, scope);
  286. }
  287. else if (el instanceof TSpan) {
  288. return brushSVGTSpan(el, scope);
  289. }
  290. }
  291. function setShadow(
  292. el: Displayable,
  293. attrs: SVGVNodeAttrs,
  294. scope: BrushScope
  295. ) {
  296. const style = el.style;
  297. if (hasShadow(style)) {
  298. const shadowKey = getShadowKey(el);
  299. const shadowCache = scope.shadowCache;
  300. let shadowId = shadowCache[shadowKey];
  301. if (!shadowId) {
  302. const globalScale = el.getGlobalScale();
  303. const scaleX = globalScale[0];
  304. const scaleY = globalScale[1];
  305. if (!scaleX || !scaleY) {
  306. return;
  307. }
  308. const offsetX = style.shadowOffsetX || 0;
  309. const offsetY = style.shadowOffsetY || 0;
  310. const blur = style.shadowBlur;
  311. const {opacity, color} = normalizeColor(style.shadowColor);
  312. const stdDx = blur / 2 / scaleX;
  313. const stdDy = blur / 2 / scaleY;
  314. const stdDeviation = stdDx + ' ' + stdDy;
  315. // Use a simple prefix to reduce the size
  316. shadowId = scope.zrId + '-s' + scope.shadowIdx++;
  317. scope.defs[shadowId] = createVNode(
  318. 'filter', shadowId,
  319. {
  320. 'id': shadowId,
  321. 'x': '-100%',
  322. 'y': '-100%',
  323. 'width': '300%',
  324. 'height': '300%'
  325. },
  326. [
  327. createVNode('feDropShadow', '', {
  328. 'dx': offsetX / scaleX,
  329. 'dy': offsetY / scaleY,
  330. 'stdDeviation': stdDeviation,
  331. 'flood-color': color,
  332. 'flood-opacity': opacity
  333. })
  334. ]
  335. );
  336. shadowCache[shadowKey] = shadowId;
  337. }
  338. attrs.filter = getIdURL(shadowId);
  339. }
  340. }
  341. function setGradient(
  342. style: PathStyleProps,
  343. attrs: SVGVNodeAttrs,
  344. target: 'fill' | 'stroke',
  345. scope: BrushScope
  346. ) {
  347. const val = style[target] as GradientObject;
  348. let gradientTag;
  349. let gradientAttrs: SVGVNodeAttrs = {
  350. 'gradientUnits': val.global
  351. ? 'userSpaceOnUse' // x1, x2, y1, y2 in range of 0 to canvas width or height
  352. : 'objectBoundingBox' // x1, x2, y1, y2 in range of 0 to 1]
  353. };
  354. if (isLinearGradient(val)) {
  355. gradientTag = 'linearGradient';
  356. gradientAttrs.x1 = val.x;
  357. gradientAttrs.y1 = val.y;
  358. gradientAttrs.x2 = val.x2;
  359. gradientAttrs.y2 = val.y2;
  360. }
  361. else if (isRadialGradient(val)) {
  362. gradientTag = 'radialGradient';
  363. gradientAttrs.cx = retrieve2(val.x, 0.5);
  364. gradientAttrs.cy = retrieve2(val.y, 0.5);
  365. gradientAttrs.r = retrieve2(val.r, 0.5);
  366. }
  367. else {
  368. if (process.env.NODE_ENV !== 'production') {
  369. logError('Illegal gradient type.');
  370. }
  371. return;
  372. }
  373. const colors = val.colorStops;
  374. const colorStops = [];
  375. for (let i = 0, len = colors.length; i < len; ++i) {
  376. const offset = round4(colors[i].offset) * 100 + '%';
  377. const stopColor = colors[i].color;
  378. // Fix Safari bug that stop-color not recognizing alpha #9014
  379. const {color, opacity} = normalizeColor(stopColor);
  380. const stopsAttrs: SVGVNodeAttrs = {
  381. 'offset': offset
  382. };
  383. // stop-color cannot be color, since:
  384. // The opacity value used for the gradient calculation is the
  385. // *product* of the value of stop-opacity and the opacity of the
  386. // value of stop-color.
  387. // See https://www.w3.org/TR/SVG2/pservers.html#StopOpacityProperty
  388. stopsAttrs['stop-color'] = color;
  389. if (opacity < 1) {
  390. stopsAttrs['stop-opacity'] = opacity;
  391. }
  392. colorStops.push(
  393. createVNode('stop', i + '', stopsAttrs)
  394. );
  395. }
  396. // Use the whole html as cache key.
  397. const gradientVNode = createVNode(gradientTag, '', gradientAttrs, colorStops);
  398. const gradientKey = vNodeToString(gradientVNode);
  399. const gradientCache = scope.gradientCache;
  400. let gradientId = gradientCache[gradientKey];
  401. if (!gradientId) {
  402. gradientId = scope.zrId + '-g' + scope.gradientIdx++;
  403. gradientCache[gradientKey] = gradientId;
  404. gradientAttrs.id = gradientId;
  405. scope.defs[gradientId] = createVNode(
  406. gradientTag, gradientId, gradientAttrs, colorStops
  407. );
  408. }
  409. attrs[target] = getIdURL(gradientId);
  410. }
  411. function setPattern(
  412. el: Displayable,
  413. attrs: SVGVNodeAttrs,
  414. target: 'fill' | 'stroke',
  415. scope: BrushScope
  416. ) {
  417. const val = el.style[target] as ImagePatternObject | SVGPatternObject;
  418. const patternAttrs: SVGVNodeAttrs = {
  419. 'patternUnits': 'userSpaceOnUse'
  420. };
  421. let child: SVGVNode;
  422. if (isImagePattern(val)) {
  423. let imageWidth = val.imageWidth;
  424. let imageHeight = val.imageHeight;
  425. let imageSrc;
  426. const patternImage = val.image;
  427. if (isString(patternImage)) {
  428. imageSrc = patternImage;
  429. }
  430. else if (isImageLike(patternImage)) {
  431. imageSrc = patternImage.src;
  432. }
  433. else if (isCanvasLike(patternImage)) {
  434. imageSrc = patternImage.toDataURL();
  435. }
  436. if (typeof Image === 'undefined') {
  437. const errMsg = 'Image width/height must been given explictly in svg-ssr renderer.';
  438. assert(imageWidth, errMsg);
  439. assert(imageHeight, errMsg);
  440. }
  441. else if (imageWidth == null || imageHeight == null) {
  442. // TODO
  443. const setSizeToVNode = (vNode: SVGVNode, img: ImageLike) => {
  444. if (vNode) {
  445. const svgEl = vNode.elm as SVGElement;
  446. const width = (vNode.attrs.width = imageWidth || img.width);
  447. const height = (vNode.attrs.height = imageHeight || img.height);
  448. if (svgEl) {
  449. svgEl.setAttribute('width', width as any);
  450. svgEl.setAttribute('height', height as any);
  451. }
  452. }
  453. };
  454. const createdImage = createOrUpdateImage(
  455. imageSrc, null, el, (img) => {
  456. setSizeToVNode(patternVNode, img);
  457. setSizeToVNode(child, img);
  458. }
  459. );
  460. if (createdImage && createdImage.width && createdImage.height) {
  461. // Loaded before
  462. imageWidth = imageWidth || createdImage.width;
  463. imageHeight = imageHeight || createdImage.height;
  464. }
  465. }
  466. child = createVNode(
  467. 'image',
  468. 'img',
  469. {
  470. href: imageSrc,
  471. width: imageWidth,
  472. height: imageHeight
  473. }
  474. );
  475. patternAttrs.width = imageWidth;
  476. patternAttrs.height = imageHeight;
  477. }
  478. else if (val.svgElement) { // Only string supported in SSR.
  479. // TODO it's not so good to use textContent as innerHTML
  480. child = clone(val.svgElement);
  481. patternAttrs.width = val.svgWidth;
  482. patternAttrs.height = val.svgHeight;
  483. }
  484. if (!child) {
  485. return;
  486. }
  487. patternAttrs.patternTransform = getSRTTransformString(val);
  488. // Use the whole html as cache key.
  489. let patternVNode = createVNode(
  490. 'pattern',
  491. '',
  492. patternAttrs,
  493. [child]
  494. );
  495. const patternKey = vNodeToString(patternVNode);
  496. const patternCache = scope.patternCache;
  497. let patternId = patternCache[patternKey];
  498. if (!patternId) {
  499. patternId = scope.zrId + '-p' + scope.patternIdx++;
  500. patternCache[patternKey] = patternId;
  501. patternAttrs.id = patternId;
  502. patternVNode = scope.defs[patternId] = createVNode(
  503. 'pattern',
  504. patternId,
  505. patternAttrs,
  506. [child]
  507. );
  508. }
  509. attrs[target] = getIdURL(patternId);
  510. }
  511. export function setClipPath(
  512. clipPath: Path,
  513. attrs: SVGVNodeAttrs,
  514. scope: BrushScope
  515. ) {
  516. const {clipPathCache, defs} = scope;
  517. let clipPathId = clipPathCache[clipPath.id];
  518. if (!clipPathId) {
  519. clipPathId = scope.zrId + '-c' + scope.clipPathIdx++;
  520. const clipPathAttrs: SVGVNodeAttrs = {
  521. id: clipPathId
  522. };
  523. clipPathCache[clipPath.id] = clipPathId;
  524. defs[clipPathId] = createVNode(
  525. 'clipPath', clipPathId, clipPathAttrs,
  526. [brushSVGPath(clipPath, scope)]
  527. );
  528. }
  529. attrs['clip-path'] = getIdURL(clipPathId);
  530. }