UNPKG

zrender

Version:

A lightweight graphic library providing 2d draw for Apache ECharts

515 lines (461 loc) 16 kB
import Path, { PathProps } from '../graphic/Path'; import PathProxy from '../core/PathProxy'; import transformPath from './transformPath'; import { VectorArray } from '../core/vector'; import { MatrixArray } from '../core/matrix'; import { extend } from '../core/util'; // command chars // const cc = [ // 'm', 'M', 'l', 'L', 'v', 'V', 'h', 'H', 'z', 'Z', // 'c', 'C', 'q', 'Q', 't', 'T', 's', 'S', 'a', 'A' // ]; const mathSqrt = Math.sqrt; const mathSin = Math.sin; const mathCos = Math.cos; const PI = Math.PI; function vMag(v: VectorArray): number { return Math.sqrt(v[0] * v[0] + v[1] * v[1]); }; function vRatio(u: VectorArray, v: VectorArray): number { return (u[0] * v[0] + u[1] * v[1]) / (vMag(u) * vMag(v)); }; function vAngle(u: VectorArray, v: VectorArray): number { return (u[0] * v[1] < u[1] * v[0] ? -1 : 1) * Math.acos(vRatio(u, v)); }; function processArc( x1: number, y1: number, x2: number, y2: number, fa: number, fs: number, rx: number, ry: number, psiDeg: number, cmd: number, path: PathProxy ) { // https://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes const psi = psiDeg * (PI / 180.0); const xp = mathCos(psi) * (x1 - x2) / 2.0 + mathSin(psi) * (y1 - y2) / 2.0; const yp = -1 * mathSin(psi) * (x1 - x2) / 2.0 + mathCos(psi) * (y1 - y2) / 2.0; const lambda = (xp * xp) / (rx * rx) + (yp * yp) / (ry * ry); if (lambda > 1) { rx *= mathSqrt(lambda); ry *= mathSqrt(lambda); } const f = (fa === fs ? -1 : 1) * mathSqrt((((rx * rx) * (ry * ry)) - ((rx * rx) * (yp * yp)) - ((ry * ry) * (xp * xp))) / ((rx * rx) * (yp * yp) + (ry * ry) * (xp * xp)) ) || 0; const cxp = f * rx * yp / ry; const cyp = f * -ry * xp / rx; const cx = (x1 + x2) / 2.0 + mathCos(psi) * cxp - mathSin(psi) * cyp; const cy = (y1 + y2) / 2.0 + mathSin(psi) * cxp + mathCos(psi) * cyp; const theta = vAngle([ 1, 0 ], [ (xp - cxp) / rx, (yp - cyp) / ry ]); const u = [ (xp - cxp) / rx, (yp - cyp) / ry ]; const v = [ (-1 * xp - cxp) / rx, (-1 * yp - cyp) / ry ]; let dTheta = vAngle(u, v); if (vRatio(u, v) <= -1) { dTheta = PI; } if (vRatio(u, v) >= 1) { dTheta = 0; } if (dTheta < 0) { const n = Math.round(dTheta / PI * 1e6) / 1e6; // Convert to positive dTheta = PI * 2 + (n % 2) * PI; } path.addData(cmd, cx, cy, rx, ry, theta, dTheta, psi, fs); } const commandReg = /([mlvhzcqtsa])([^mlvhzcqtsa]*)/ig; // Consider case: // (1) delimiter can be comma or space, where continuous commas // or spaces should be seen as one comma. // (2) value can be like: // '2e-4', 'l.5.9' (ignore 0), 'M-10-10', 'l-2.43e-1,34.9983', // 'l-.5E1,54', '121-23-44-11' (no delimiter) const numberReg = /-?([0-9]*\.)?[0-9]+([eE]-?[0-9]+)?/g; // const valueSplitReg = /[\s,]+/; function createPathProxyFromString(data: string) { const path = new PathProxy(); if (!data) { return path; } // const data = data.replace(/-/g, ' -') // .replace(/ /g, ' ') // .replace(/ /g, ',') // .replace(/,,/g, ','); // const n; // create pipes so that we can split the data // for (n = 0; n < cc.length; n++) { // cs = cs.replace(new RegExp(cc[n], 'g'), '|' + cc[n]); // } // data = data.replace(/-/g, ',-'); // create array // const arr = cs.split('|'); // init context point let cpx = 0; let cpy = 0; let subpathX = cpx; let subpathY = cpy; let prevCmd; const CMD = PathProxy.CMD; // commandReg.lastIndex = 0; // const cmdResult; // while ((cmdResult = commandReg.exec(data)) != null) { // const cmdStr = cmdResult[1]; // const cmdContent = cmdResult[2]; const cmdList = data.match(commandReg); if (!cmdList) { // Invalid svg path. return path; } for (let l = 0; l < cmdList.length; l++) { const cmdText = cmdList[l]; let cmdStr = cmdText.charAt(0); let cmd; // String#split is faster a little bit than String#replace or RegExp#exec. // const p = cmdContent.split(valueSplitReg); // const pLen = 0; // for (let i = 0; i < p.length; i++) { // // '' and other invalid str => NaN // const val = parseFloat(p[i]); // !isNaN(val) && (p[pLen++] = val); // } // Following code will convert string to number. So convert type to number here const p = cmdText.match(numberReg) as any[] as number[] || []; const pLen = p.length; for (let i = 0; i < pLen; i++) { p[i] = parseFloat(p[i] as any as string); } let off = 0; while (off < pLen) { let ctlPtx; let ctlPty; let rx; let ry; let psi; let fa; let fs; let x1 = cpx; let y1 = cpy; let len: number; let pathData: number[] | Float32Array; // convert l, H, h, V, and v to L switch (cmdStr) { case 'l': cpx += p[off++]; cpy += p[off++]; cmd = CMD.L; path.addData(cmd, cpx, cpy); break; case 'L': cpx = p[off++]; cpy = p[off++]; cmd = CMD.L; path.addData(cmd, cpx, cpy); break; case 'm': cpx += p[off++]; cpy += p[off++]; cmd = CMD.M; path.addData(cmd, cpx, cpy); subpathX = cpx; subpathY = cpy; cmdStr = 'l'; break; case 'M': cpx = p[off++]; cpy = p[off++]; cmd = CMD.M; path.addData(cmd, cpx, cpy); subpathX = cpx; subpathY = cpy; cmdStr = 'L'; break; case 'h': cpx += p[off++]; cmd = CMD.L; path.addData(cmd, cpx, cpy); break; case 'H': cpx = p[off++]; cmd = CMD.L; path.addData(cmd, cpx, cpy); break; case 'v': cpy += p[off++]; cmd = CMD.L; path.addData(cmd, cpx, cpy); break; case 'V': cpy = p[off++]; cmd = CMD.L; path.addData(cmd, cpx, cpy); break; case 'C': cmd = CMD.C; path.addData( cmd, p[off++], p[off++], p[off++], p[off++], p[off++], p[off++] ); cpx = p[off - 2]; cpy = p[off - 1]; break; case 'c': cmd = CMD.C; path.addData( cmd, p[off++] + cpx, p[off++] + cpy, p[off++] + cpx, p[off++] + cpy, p[off++] + cpx, p[off++] + cpy ); cpx += p[off - 2]; cpy += p[off - 1]; break; case 'S': ctlPtx = cpx; ctlPty = cpy; len = path.len(); pathData = path.data; if (prevCmd === CMD.C) { ctlPtx += cpx - pathData[len - 4]; ctlPty += cpy - pathData[len - 3]; } cmd = CMD.C; x1 = p[off++]; y1 = p[off++]; cpx = p[off++]; cpy = p[off++]; path.addData(cmd, ctlPtx, ctlPty, x1, y1, cpx, cpy); break; case 's': ctlPtx = cpx; ctlPty = cpy; len = path.len(); pathData = path.data; if (prevCmd === CMD.C) { ctlPtx += cpx - pathData[len - 4]; ctlPty += cpy - pathData[len - 3]; } cmd = CMD.C; x1 = cpx + p[off++]; y1 = cpy + p[off++]; cpx += p[off++]; cpy += p[off++]; path.addData(cmd, ctlPtx, ctlPty, x1, y1, cpx, cpy); break; case 'Q': x1 = p[off++]; y1 = p[off++]; cpx = p[off++]; cpy = p[off++]; cmd = CMD.Q; path.addData(cmd, x1, y1, cpx, cpy); break; case 'q': x1 = p[off++] + cpx; y1 = p[off++] + cpy; cpx += p[off++]; cpy += p[off++]; cmd = CMD.Q; path.addData(cmd, x1, y1, cpx, cpy); break; case 'T': ctlPtx = cpx; ctlPty = cpy; len = path.len(); pathData = path.data; if (prevCmd === CMD.Q) { ctlPtx += cpx - pathData[len - 4]; ctlPty += cpy - pathData[len - 3]; } cpx = p[off++]; cpy = p[off++]; cmd = CMD.Q; path.addData(cmd, ctlPtx, ctlPty, cpx, cpy); break; case 't': ctlPtx = cpx; ctlPty = cpy; len = path.len(); pathData = path.data; if (prevCmd === CMD.Q) { ctlPtx += cpx - pathData[len - 4]; ctlPty += cpy - pathData[len - 3]; } cpx += p[off++]; cpy += p[off++]; cmd = CMD.Q; path.addData(cmd, ctlPtx, ctlPty, cpx, cpy); break; case 'A': rx = p[off++]; ry = p[off++]; psi = p[off++]; fa = p[off++]; fs = p[off++]; x1 = cpx, y1 = cpy; cpx = p[off++]; cpy = p[off++]; cmd = CMD.A; processArc( x1, y1, cpx, cpy, fa, fs, rx, ry, psi, cmd, path ); break; case 'a': rx = p[off++]; ry = p[off++]; psi = p[off++]; fa = p[off++]; fs = p[off++]; x1 = cpx, y1 = cpy; cpx += p[off++]; cpy += p[off++]; cmd = CMD.A; processArc( x1, y1, cpx, cpy, fa, fs, rx, ry, psi, cmd, path ); break; } } if (cmdStr === 'z' || cmdStr === 'Z') { cmd = CMD.Z; path.addData(cmd); // z may be in the middle of the path. cpx = subpathX; cpy = subpathY; } prevCmd = cmd; } path.toStatic(); return path; } type SVGPathOption = Omit<PathProps, 'shape' | 'buildPath'> interface InnerSVGPathOption extends PathProps { applyTransform?: (m: MatrixArray) => void } class SVGPath extends Path { applyTransform(m: MatrixArray) {} } function isPathProxy(path: PathProxy | CanvasRenderingContext2D): path is PathProxy { return (path as PathProxy).setData != null; } // TODO Optimize double memory cost problem function createPathOptions(str: string, opts: SVGPathOption): InnerSVGPathOption { const pathProxy = createPathProxyFromString(str); const innerOpts: InnerSVGPathOption = extend({}, opts); innerOpts.buildPath = function (path: PathProxy | CanvasRenderingContext2D) { if (isPathProxy(path)) { path.setData(pathProxy.data); // Svg and vml renderer don't have context const ctx = path.getContext(); if (ctx) { path.rebuildPath(ctx, 1); } } else { const ctx = path; pathProxy.rebuildPath(ctx, 1); } }; innerOpts.applyTransform = function (this: SVGPath, m: MatrixArray) { transformPath(pathProxy, m); this.dirtyShape(); }; return innerOpts; } /** * Create a Path object from path string data * http://www.w3.org/TR/SVG/paths.html#PathData * @param opts Other options */ export function createFromString(str: string, opts?: SVGPathOption): SVGPath { // PENDING return new SVGPath(createPathOptions(str, opts)); } /** * Create a Path class from path string data * @param str * @param opts Other options */ export function extendFromString(str: string, defaultOpts?: SVGPathOption): typeof SVGPath { const innerOpts = createPathOptions(str, defaultOpts); class Sub extends SVGPath { constructor(opts: InnerSVGPathOption) { super(opts); this.applyTransform = innerOpts.applyTransform; this.buildPath = innerOpts.buildPath; } } return Sub; } /** * Merge multiple paths */ // TODO Apply transform // TODO stroke dash // TODO Optimize double memory cost problem export function mergePath(pathEls: Path[], opts: PathProps) { const pathList: PathProxy[] = []; const len = pathEls.length; for (let i = 0; i < len; i++) { const pathEl = pathEls[i]; pathList.push(pathEl.getUpdatedPathProxy(true)); } const pathBundle = new Path(opts); // Need path proxy. pathBundle.createPathProxy(); pathBundle.buildPath = function (path: PathProxy | CanvasRenderingContext2D) { if (isPathProxy(path)) { path.appendPath(pathList); // Svg and vml renderer don't have context const ctx = path.getContext(); if (ctx) { // Path bundle not support percent draw. path.rebuildPath(ctx, 1); } } }; return pathBundle; } /** * Clone a path. */ export function clonePath(sourcePath: Path, opts?: { /** * If bake global transform to path. */ bakeTransform?: boolean /** * Convert global transform to local. */ toLocal?: boolean }) { opts = opts || {}; const path = new Path(); if (sourcePath.shape) { path.setShape(sourcePath.shape); } path.setStyle(sourcePath.style); if (opts.bakeTransform) { transformPath(path.path, sourcePath.getComputedTransform()); } else { // TODO Copy getLocalTransform, updateTransform since they can be changed. if (opts.toLocal) { path.setLocalTransform(sourcePath.getComputedTransform()); } else { path.copyTransform(sourcePath); } } // These methods may be overridden path.buildPath = sourcePath.buildPath; (path as SVGPath).applyTransform = (path as SVGPath).applyTransform; path.z = sourcePath.z; path.z2 = sourcePath.z2; path.zlevel = sourcePath.zlevel; return path; }