/**
|
* 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
|
}
|