zrender
Version:
A lightweight graphic library providing 2d draw for Apache ECharts
994 lines (862 loc) • 31.2 kB
text/typescript
/**
* 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
}