123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514 |
- 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;
- }
|