UNPKG

zrender

Version:

A lightweight graphic library providing 2d draw for Apache ECharts

994 lines (862 loc) 31.2 kB
/** * Path 代理,可以在`buildPath`中用于替代`ctx`, 会保存每个path操作的命令到pathCommands属性中 * 可以用于 isInsidePath 判断以及获取boundingRect */ // TODO getTotalLength, getPointAtLength, arcTo /* global Float32Array */ import * as vec2 from './vector'; import BoundingRect from './BoundingRect'; import {devicePixelRatio as dpr} from '../config'; import { fromLine, fromCubic, fromQuadratic, fromArc } from './bbox'; import { cubicLength, cubicSubdivide, quadraticLength, quadraticSubdivide } from './curve'; const CMD = { M: 1, L: 2, C: 3, Q: 4, A: 5, Z: 6, // Rect R: 7 }; // const CMD_MEM_SIZE = { // M: 3, // L: 3, // C: 7, // Q: 5, // A: 9, // R: 5, // Z: 1 // }; interface ExtendedCanvasRenderingContext2D extends CanvasRenderingContext2D { dpr?: number } const tmpOutX: number[] = []; const tmpOutY: number[] = []; const min: number[] = []; const max: number[] = []; const min2: number[] = []; const max2: number[] = []; const mathMin = Math.min; const mathMax = Math.max; const mathCos = Math.cos; const mathSin = Math.sin; const mathAbs = Math.abs; const PI = Math.PI; const PI2 = PI * 2; const hasTypedArray = typeof Float32Array !== 'undefined'; const tmpAngles: number[] = []; function modPI2(radian: number) { // It's much more stable to mod N instedof PI const n = Math.round(radian / PI * 1e8) / 1e8; return (n % 2) * PI; } /** * Normalize start and end angles. * startAngle will be normalized to 0 ~ PI*2 * sweepAngle(endAngle - startAngle) will be normalized to 0 ~ PI*2 if clockwise. * -PI*2 ~ 0 if anticlockwise. */ export function normalizeArcAngles(angles: number[], anticlockwise: boolean): void { let newStartAngle = modPI2(angles[0]); if (newStartAngle < 0) { // Normlize to 0 - PI2 newStartAngle += PI2; } let delta = newStartAngle - angles[0]; let newEndAngle = angles[1]; newEndAngle += delta; // https://github.com/chromium/chromium/blob/c20d681c9c067c4e15bb1408f17114b9e8cba294/third_party/blink/renderer/modules/canvas/canvas2d/canvas_path.cc#L184 // Is circle if (!anticlockwise && newEndAngle - newStartAngle >= PI2) { newEndAngle = newStartAngle + PI2; } else if (anticlockwise && newStartAngle - newEndAngle >= PI2) { newEndAngle = newStartAngle - PI2; } // Make startAngle < endAngle when clockwise, otherwise endAngle < startAngle. // The sweep angle can never been larger than P2. else if (!anticlockwise && newStartAngle > newEndAngle) { newEndAngle = newStartAngle + (PI2 - modPI2(newStartAngle - newEndAngle)); } else if (anticlockwise && newStartAngle < newEndAngle) { newEndAngle = newStartAngle - (PI2 - modPI2(newEndAngle - newStartAngle)); } angles[0] = newStartAngle; angles[1] = newEndAngle; } export default class PathProxy { dpr = 1 data: number[] | Float32Array /** * Version is for tracking if the path has been changed. */ private _version: number /** * If save path data. */ private _saveData: boolean /** * If the line segment is too small to draw. It will be added to the pending pt. * It will be added if the subpath needs to be finished before stroke, fill, or starting a new subpath. */ private _pendingPtX: number; private _pendingPtY: number; // Distance of pending pt to previous point. // 0 if there is no pending point. // Only update the pending pt when distance is larger. private _pendingPtDist: number; private _ctx: ExtendedCanvasRenderingContext2D private _xi = 0 private _yi = 0 private _x0 = 0 private _y0 = 0 private _len = 0 // Calculating path len and seg len. private _pathSegLen: number[] private _pathLen: number // Unit x, Unit y. Provide for avoiding drawing that too short line segment private _ux: number private _uy: number static CMD = CMD constructor(notSaveData?: boolean) { if (notSaveData) { this._saveData = false; } if (this._saveData) { this.data = []; } } increaseVersion() { this._version++; } /** * Version can be used outside for compare if the path is changed. * For example to determine if need to update svg d str in svg renderer. */ getVersion() { return this._version; } /** * @readOnly */ setScale(sx: number, sy: number, segmentIgnoreThreshold?: number) { // Compat. Previously there is no segmentIgnoreThreshold. segmentIgnoreThreshold = segmentIgnoreThreshold || 0; if (segmentIgnoreThreshold > 0) { this._ux = mathAbs(segmentIgnoreThreshold / dpr / sx) || 0; this._uy = mathAbs(segmentIgnoreThreshold / dpr / sy) || 0; } } setDPR(dpr: number) { this.dpr = dpr; } setContext(ctx: ExtendedCanvasRenderingContext2D) { this._ctx = ctx; } getContext(): ExtendedCanvasRenderingContext2D { return this._ctx; } beginPath() { this._ctx && this._ctx.beginPath(); this.reset(); return this; } /** * Reset path data. */ reset() { // Reset if (this._saveData) { this._len = 0; } if (this._pathSegLen) { this._pathSegLen = null; this._pathLen = 0; } // Update version this._version++; } moveTo(x: number, y: number) { // Add pending point for previous path. this._drawPendingPt(); this.addData(CMD.M, x, y); this._ctx && this._ctx.moveTo(x, y); // x0, y0, xi, yi 是记录在 _dashedXXXXTo 方法中使用 // xi, yi 记录当前点, x0, y0 在 closePath 的时候回到起始点。 // 有可能在 beginPath 之后直接调用 lineTo,这时候 x0, y0 需要 // 在 lineTo 方法中记录,这里先不考虑这种情况,dashed line 也只在 IE10- 中不支持 this._x0 = x; this._y0 = y; this._xi = x; this._yi = y; return this; } lineTo(x: number, y: number) { const dx = mathAbs(x - this._xi); const dy = mathAbs(y - this._yi); const exceedUnit = dx > this._ux || dy > this._uy; this.addData(CMD.L, x, y); if (this._ctx && exceedUnit) { this._ctx.lineTo(x, y); } if (exceedUnit) { this._xi = x; this._yi = y; this._pendingPtDist = 0; } else { const d2 = dx * dx + dy * dy; // Only use the farthest pending point. if (d2 > this._pendingPtDist) { this._pendingPtX = x; this._pendingPtY = y; this._pendingPtDist = d2; } } return this; } bezierCurveTo(x1: number, y1: number, x2: number, y2: number, x3: number, y3: number) { this._drawPendingPt(); this.addData(CMD.C, x1, y1, x2, y2, x3, y3); if (this._ctx) { this._ctx.bezierCurveTo(x1, y1, x2, y2, x3, y3); } this._xi = x3; this._yi = y3; return this; } quadraticCurveTo(x1: number, y1: number, x2: number, y2: number) { this._drawPendingPt(); this.addData(CMD.Q, x1, y1, x2, y2); if (this._ctx) { this._ctx.quadraticCurveTo(x1, y1, x2, y2); } this._xi = x2; this._yi = y2; return this; } arc(cx: number, cy: number, r: number, startAngle: number, endAngle: number, anticlockwise?: boolean) { this._drawPendingPt(); tmpAngles[0] = startAngle; tmpAngles[1] = endAngle; normalizeArcAngles(tmpAngles, anticlockwise); startAngle = tmpAngles[0]; endAngle = tmpAngles[1]; let delta = endAngle - startAngle; this.addData( CMD.A, cx, cy, r, r, startAngle, delta, 0, anticlockwise ? 0 : 1 ); this._ctx && this._ctx.arc(cx, cy, r, startAngle, endAngle, anticlockwise); this._xi = mathCos(endAngle) * r + cx; this._yi = mathSin(endAngle) * r + cy; return this; } // TODO arcTo(x1: number, y1: number, x2: number, y2: number, radius: number) { this._drawPendingPt(); if (this._ctx) { this._ctx.arcTo(x1, y1, x2, y2, radius); } return this; } // TODO rect(x: number, y: number, w: number, h: number) { this._drawPendingPt(); this._ctx && this._ctx.rect(x, y, w, h); this.addData(CMD.R, x, y, w, h); return this; } closePath() { // Add pending point for previous path. this._drawPendingPt(); this.addData(CMD.Z); const ctx = this._ctx; const x0 = this._x0; const y0 = this._y0; if (ctx) { ctx.closePath(); } this._xi = x0; this._yi = y0; return this; } fill(ctx: CanvasRenderingContext2D) { ctx && ctx.fill(); this.toStatic(); } stroke(ctx: CanvasRenderingContext2D) { ctx && ctx.stroke(); this.toStatic(); } len() { return this._len; } setData(data: Float32Array | number[]) { const len = data.length; if (!(this.data && this.data.length === len) && hasTypedArray) { this.data = new Float32Array(len); } for (let i = 0; i < len; i++) { this.data[i] = data[i]; } this._len = len; } appendPath(path: PathProxy | PathProxy[]) { if (!(path instanceof Array)) { path = [path]; } const len = path.length; let appendSize = 0; let offset = this._len; for (let i = 0; i < len; i++) { appendSize += path[i].len(); } if (hasTypedArray && (this.data instanceof Float32Array)) { this.data = new Float32Array(offset + appendSize); } for (let i = 0; i < len; i++) { const appendPathData = path[i].data; for (let k = 0; k < appendPathData.length; k++) { this.data[offset++] = appendPathData[k]; } } this._len = offset; } /** * 填充 Path 数据。 * 尽量复用而不申明新的数组。大部分图形重绘的指令数据长度都是不变的。 */ addData( cmd: number, a?: number, b?: number, c?: number, d?: number, e?: number, f?: number, g?: number, h?: number ) { if (!this._saveData) { return; } let data = this.data; if (this._len + arguments.length > data.length) { // 因为之前的数组已经转换成静态的 Float32Array // 所以不够用时需要扩展一个新的动态数组 this._expandData(); data = this.data; } for (let i = 0; i < arguments.length; i++) { data[this._len++] = arguments[i]; } } private _drawPendingPt() { if (this._pendingPtDist > 0) { this._ctx && this._ctx.lineTo(this._pendingPtX, this._pendingPtY); this._pendingPtDist = 0; } } private _expandData() { // Only if data is Float32Array if (!(this.data instanceof Array)) { const newData = []; for (let i = 0; i < this._len; i++) { newData[i] = this.data[i]; } this.data = newData; } } /** * Convert dynamic array to static Float32Array * * It will still use a normal array if command buffer length is less than 10 * Because Float32Array itself may take more memory than a normal array. * * 10 length will make sure at least one M command and one A(arc) command. */ toStatic() { if (!this._saveData) { return; } this._drawPendingPt(); const data = this.data; if (data instanceof Array) { data.length = this._len; if (hasTypedArray && this._len > 11) { this.data = new Float32Array(data); } } } getBoundingRect() { min[0] = min[1] = min2[0] = min2[1] = Number.MAX_VALUE; max[0] = max[1] = max2[0] = max2[1] = -Number.MAX_VALUE; const data = this.data; let xi = 0; let yi = 0; let x0 = 0; let y0 = 0; let i; for (i = 0; i < this._len;) { const cmd = data[i++] as number; const isFirst = i === 1; if (isFirst) { // 如果第一个命令是 L, C, Q // 则 previous point 同绘制命令的第一个 point // 第一个命令为 Arc 的情况下会在后面特殊处理 xi = data[i]; yi = data[i + 1]; x0 = xi; y0 = yi; } switch (cmd) { case CMD.M: // moveTo 命令重新创建一个新的 subpath, 并且更新新的起点 // 在 closePath 的时候使用 xi = x0 = data[i++]; yi = y0 = data[i++]; min2[0] = x0; min2[1] = y0; max2[0] = x0; max2[1] = y0; break; case CMD.L: fromLine(xi, yi, data[i], data[i + 1], min2, max2); xi = data[i++]; yi = data[i++]; break; case CMD.C: fromCubic( xi, yi, data[i++], data[i++], data[i++], data[i++], data[i], data[i + 1], min2, max2 ); xi = data[i++]; yi = data[i++]; break; case CMD.Q: fromQuadratic( xi, yi, data[i++], data[i++], data[i], data[i + 1], min2, max2 ); xi = data[i++]; yi = data[i++]; break; case CMD.A: const cx = data[i++]; const cy = data[i++]; const rx = data[i++]; const ry = data[i++]; const startAngle = data[i++]; const endAngle = data[i++] + startAngle; // TODO Arc 旋转 i += 1; const anticlockwise = !data[i++]; if (isFirst) { // 直接使用 arc 命令 // 第一个命令起点还未定义 x0 = mathCos(startAngle) * rx + cx; y0 = mathSin(startAngle) * ry + cy; } fromArc( cx, cy, rx, ry, startAngle, endAngle, anticlockwise, min2, max2 ); xi = mathCos(endAngle) * rx + cx; yi = mathSin(endAngle) * ry + cy; break; case CMD.R: x0 = xi = data[i++]; y0 = yi = data[i++]; const width = data[i++]; const height = data[i++]; // Use fromLine fromLine(x0, y0, x0 + width, y0 + height, min2, max2); break; case CMD.Z: xi = x0; yi = y0; break; } // Union vec2.min(min, min, min2); vec2.max(max, max, max2); } // No data if (i === 0) { min[0] = min[1] = max[0] = max[1] = 0; } return new BoundingRect( min[0], min[1], max[0] - min[0], max[1] - min[1] ); } private _calculateLength(): number { const data = this.data; const len = this._len; const ux = this._ux; const uy = this._uy; let xi = 0; let yi = 0; let x0 = 0; let y0 = 0; if (!this._pathSegLen) { this._pathSegLen = []; } const pathSegLen = this._pathSegLen; let pathTotalLen = 0; let segCount = 0; for (let i = 0; i < len;) { const cmd = data[i++] as number; const isFirst = i === 1; if (isFirst) { // 如果第一个命令是 L, C, Q // 则 previous point 同绘制命令的第一个 point // 第一个命令为 Arc 的情况下会在后面特殊处理 xi = data[i]; yi = data[i + 1]; x0 = xi; y0 = yi; } let l = -1; switch (cmd) { case CMD.M: // moveTo 命令重新创建一个新的 subpath, 并且更新新的起点 // 在 closePath 的时候使用 xi = x0 = data[i++]; yi = y0 = data[i++]; break; case CMD.L: { const x2 = data[i++]; const y2 = data[i++]; const dx = x2 - xi; const dy = y2 - yi; if (mathAbs(dx) > ux || mathAbs(dy) > uy || i === len - 1) { l = Math.sqrt(dx * dx + dy * dy); xi = x2; yi = y2; } break; } case CMD.C: { const x1 = data[i++]; const y1 = data[i++]; const x2 = data[i++]; const y2 = data[i++]; const x3 = data[i++]; const y3 = data[i++]; // TODO adaptive iteration l = cubicLength(xi, yi, x1, y1, x2, y2, x3, y3, 10); xi = x3; yi = y3; break; } case CMD.Q: { const x1 = data[i++]; const y1 = data[i++]; const x2 = data[i++]; const y2 = data[i++]; l = quadraticLength(xi, yi, x1, y1, x2, y2, 10); xi = x2; yi = y2; break; } case CMD.A: // TODO Arc 判断的开销比较大 const cx = data[i++]; const cy = data[i++]; const rx = data[i++]; const ry = data[i++]; const startAngle = data[i++]; let delta = data[i++]; const endAngle = delta + startAngle; // TODO Arc 旋转 i += 1; if (isFirst) { // 直接使用 arc 命令 // 第一个命令起点还未定义 x0 = mathCos(startAngle) * rx + cx; y0 = mathSin(startAngle) * ry + cy; } // TODO Ellipse l = mathMax(rx, ry) * mathMin(PI2, Math.abs(delta)); xi = mathCos(endAngle) * rx + cx; yi = mathSin(endAngle) * ry + cy; break; case CMD.R: { x0 = xi = data[i++]; y0 = yi = data[i++]; const width = data[i++]; const height = data[i++]; l = width * 2 + height * 2; break; } case CMD.Z: { const dx = x0 - xi; const dy = y0 - yi; l = Math.sqrt(dx * dx + dy * dy); xi = x0; yi = y0; break; } } if (l >= 0) { pathSegLen[segCount++] = l; pathTotalLen += l; } } // TODO Optimize memory cost. this._pathLen = pathTotalLen; return pathTotalLen; } /** * Rebuild path from current data * Rebuild path will not consider javascript implemented line dash. * @param {CanvasRenderingContext2D} ctx */ rebuildPath(ctx: PathRebuilder, percent: number) { const d = this.data; const ux = this._ux; const uy = this._uy; const len = this._len; let x0; let y0; let xi; let yi; let x; let y; const drawPart = percent < 1; let pathSegLen; let pathTotalLen; let accumLength = 0; let segCount = 0; let displayedLength; let pendingPtDist = 0; let pendingPtX: number; let pendingPtY: number; if (drawPart) { if (!this._pathSegLen) { this._calculateLength(); } pathSegLen = this._pathSegLen; pathTotalLen = this._pathLen; displayedLength = percent * pathTotalLen; if (!displayedLength) { return; } } lo: for (let i = 0; i < len;) { const cmd = d[i++]; const isFirst = i === 1; if (isFirst) { // 如果第一个命令是 L, C, Q // 则 previous point 同绘制命令的第一个 point // 第一个命令为 Arc 的情况下会在后面特殊处理 xi = d[i]; yi = d[i + 1]; x0 = xi; y0 = yi; } // Only lineTo support ignoring small segments. // Otherwise if the pending point should always been flushed. if (cmd !== CMD.L && pendingPtDist > 0) { ctx.lineTo(pendingPtX, pendingPtY); pendingPtDist = 0; } switch (cmd) { case CMD.M: x0 = xi = d[i++]; y0 = yi = d[i++]; ctx.moveTo(xi, yi); break; case CMD.L: { x = d[i++]; y = d[i++]; const dx = mathAbs(x - xi); const dy = mathAbs(y - yi); // Not draw too small seg between if (dx > ux || dy > uy) { if (drawPart) { const l = pathSegLen[segCount++]; if (accumLength + l > displayedLength) { const t = (displayedLength - accumLength) / l; ctx.lineTo(xi * (1 - t) + x * t, yi * (1 - t) + y * t); break lo; } accumLength += l; } ctx.lineTo(x, y); xi = x; yi = y; pendingPtDist = 0; } else { const d2 = dx * dx + dy * dy; // Only use the farthest pending point. if (d2 > pendingPtDist) { pendingPtX = x; pendingPtY = y; pendingPtDist = d2; } } break; } case CMD.C: { const x1 = d[i++]; const y1 = d[i++]; const x2 = d[i++]; const y2 = d[i++]; const x3 = d[i++]; const y3 = d[i++]; if (drawPart) { const l = pathSegLen[segCount++]; if (accumLength + l > displayedLength) { const t = (displayedLength - accumLength) / l; cubicSubdivide(xi, x1, x2, x3, t, tmpOutX); cubicSubdivide(yi, y1, y2, y3, t, tmpOutY); ctx.bezierCurveTo(tmpOutX[1], tmpOutY[1], tmpOutX[2], tmpOutY[2], tmpOutX[3], tmpOutY[3]); break lo; } accumLength += l; } ctx.bezierCurveTo(x1, y1, x2, y2, x3, y3); xi = x3; yi = y3; break; } case CMD.Q: { const x1 = d[i++]; const y1 = d[i++]; const x2 = d[i++]; const y2 = d[i++]; if (drawPart) { const l = pathSegLen[segCount++]; if (accumLength + l > displayedLength) { const t = (displayedLength - accumLength) / l; quadraticSubdivide(xi, x1, x2, t, tmpOutX); quadraticSubdivide(yi, y1, y2, t, tmpOutY); ctx.quadraticCurveTo(tmpOutX[1], tmpOutY[1], tmpOutX[2], tmpOutY[2]); break lo; } accumLength += l; } ctx.quadraticCurveTo(x1, y1, x2, y2); xi = x2; yi = y2; break; } case CMD.A: const cx = d[i++]; const cy = d[i++]; const rx = d[i++]; const ry = d[i++]; let startAngle = d[i++]; let delta = d[i++]; const psi = d[i++]; const anticlockwise = !d[i++]; const r = (rx > ry) ? rx : ry; // const scaleX = (rx > ry) ? 1 : rx / ry; // const scaleY = (rx > ry) ? ry / rx : 1; const isEllipse = mathAbs(rx - ry) > 1e-3; let endAngle = startAngle + delta; let breakBuild = false; if (drawPart) { const l = pathSegLen[segCount++]; if (accumLength + l > displayedLength) { endAngle = startAngle + delta * (displayedLength - accumLength) / l; breakBuild = true; } accumLength += l; } if (isEllipse && ctx.ellipse) { ctx.ellipse(cx, cy, rx, ry, psi, startAngle, endAngle, anticlockwise); } else { ctx.arc(cx, cy, r, startAngle, endAngle, anticlockwise); } if (breakBuild) { break lo; } if (isFirst) { // 直接使用 arc 命令 // 第一个命令起点还未定义 x0 = mathCos(startAngle) * rx + cx; y0 = mathSin(startAngle) * ry + cy; } xi = mathCos(endAngle) * rx + cx; yi = mathSin(endAngle) * ry + cy; break; case CMD.R: x0 = xi = d[i]; y0 = yi = d[i + 1]; x = d[i++]; y = d[i++]; const width = d[i++]; const height = d[i++]; if (drawPart) { const l = pathSegLen[segCount++]; if (accumLength + l > displayedLength) { let d = displayedLength - accumLength; ctx.moveTo(x, y); ctx.lineTo(x + mathMin(d, width), y); d -= width; if (d > 0) { ctx.lineTo(x + width, y + mathMin(d, height)); } d -= height; if (d > 0) { ctx.lineTo(x + mathMax(width - d, 0), y + height); } d -= width; if (d > 0) { ctx.lineTo(x, y + mathMax(height - d, 0)); } break lo; } accumLength += l; } ctx.rect(x, y, width, height); break; case CMD.Z: if (drawPart) { const l = pathSegLen[segCount++]; if (accumLength + l > displayedLength) { const t = (displayedLength - accumLength) / l; ctx.lineTo(xi * (1 - t) + x0 * t, yi * (1 - t) + y0 * t); break lo; } accumLength += l; } ctx.closePath(); xi = x0; yi = y0; } } } clone() { const newProxy = new PathProxy(); const data = this.data; newProxy.data = data.slice ? data.slice() : Array.prototype.slice.call(data); newProxy._len = this._len; return newProxy; } private static initDefaultProps = (function () { const proto = PathProxy.prototype; proto._saveData = true; proto._ux = 0; proto._uy = 0; proto._pendingPtDist = 0; proto._version = 0; })() } export interface PathRebuilder { moveTo(x: number, y: number): void lineTo(x: number, y: number): void bezierCurveTo(x: number, y: number, x2: number, y2: number, x3: number, y3: number): void quadraticCurveTo(x: number, y: number, x2: number, y2: number): void arc(cx: number, cy: number, r: number, startAngle: number, endAngle: number, anticlockwise: boolean): void // eslint-disable-next-line max-len ellipse(cx: number, cy: number, radiusX: number, radiusY: number, rotation: number, startAngle: number, endAngle: number, anticlockwise: boolean): void rect(x: number, y: number, width: number, height: number): void closePath(): void }