1221 lines
40 KiB
JavaScript
1221 lines
40 KiB
JavaScript
import { Konva } from "./Global.js";
|
|
const NODE_ERROR = `Konva.js unsupported environment.
|
|
|
|
Looks like you are trying to use Konva.js in Node.js environment (or in a Web Worker), because "document" object is undefined.
|
|
|
|
To use Konva.js in Node.js environment, you need to use the "canvas-backend" or "skia-backend" module.
|
|
|
|
bash: npm install canvas
|
|
js: import "konva/canvas-backend";
|
|
|
|
or
|
|
|
|
bash: npm install skia-canvas
|
|
js: import "konva/skia-backend";
|
|
`;
|
|
const ensureBrowser = () => {
|
|
if (typeof document === 'undefined') {
|
|
throw new Error(NODE_ERROR);
|
|
}
|
|
};
|
|
/*
|
|
* Last updated November 2011
|
|
* By Simon Sarris
|
|
* www.simonsarris.com
|
|
* sarris@acm.org
|
|
*
|
|
* Free to use and distribute at will
|
|
* So long as you are nice to people, etc
|
|
*/
|
|
/*
|
|
* The usage of this class was inspired by some of the work done by a forked
|
|
* project, KineticJS-Ext by Wappworks, which is based on Simon's Transform
|
|
* class. Modified by Eric Rowell
|
|
*/
|
|
/**
|
|
* Transform constructor.
|
|
* In most of the cases you don't need to use it in your app. Because it is for internal usage in Konva core.
|
|
* But there is a documentation for that class in case you still want
|
|
* to make some manual calculations.
|
|
* @constructor
|
|
* @param {Array} [m] Optional six-element matrix
|
|
* @memberof Konva
|
|
*/
|
|
export class Transform {
|
|
constructor(m) {
|
|
this.dirty = false;
|
|
this.m = m ? m.slice() : [1, 0, 0, 1, 0, 0];
|
|
}
|
|
reset() {
|
|
this.m[0] = 1;
|
|
this.m[1] = 0;
|
|
this.m[2] = 0;
|
|
this.m[3] = 1;
|
|
this.m[4] = 0;
|
|
this.m[5] = 0;
|
|
}
|
|
/**
|
|
* Copy Konva.Transform object
|
|
* @method
|
|
* @name Konva.Transform#copy
|
|
* @returns {Konva.Transform}
|
|
* @example
|
|
* const tr = shape.getTransform().copy()
|
|
*/
|
|
copy() {
|
|
return new Transform(this.m);
|
|
}
|
|
copyInto(tr) {
|
|
tr.m[0] = this.m[0];
|
|
tr.m[1] = this.m[1];
|
|
tr.m[2] = this.m[2];
|
|
tr.m[3] = this.m[3];
|
|
tr.m[4] = this.m[4];
|
|
tr.m[5] = this.m[5];
|
|
}
|
|
/**
|
|
* Transform point
|
|
* @method
|
|
* @name Konva.Transform#point
|
|
* @param {Object} point 2D point(x, y)
|
|
* @returns {Object} 2D point(x, y)
|
|
*/
|
|
point(point) {
|
|
const m = this.m;
|
|
return {
|
|
x: m[0] * point.x + m[2] * point.y + m[4],
|
|
y: m[1] * point.x + m[3] * point.y + m[5],
|
|
};
|
|
}
|
|
// Axis-aligned bounds of a rectangle after this transform.
|
|
_getTransformedRect(rect) {
|
|
const [a, b, c, d, e, f] = this.m;
|
|
const x1 = rect.x, y1 = rect.y, x2 = rect.x + rect.width, y2 = rect.y + rect.height;
|
|
const xs = [
|
|
a * x1 + c * y1 + e,
|
|
a * x2 + c * y1 + e,
|
|
a * x2 + c * y2 + e,
|
|
a * x1 + c * y2 + e,
|
|
];
|
|
const ys = [
|
|
b * x1 + d * y1 + f,
|
|
b * x2 + d * y1 + f,
|
|
b * x2 + d * y2 + f,
|
|
b * x1 + d * y2 + f,
|
|
];
|
|
const minX = Math.min(xs[0], xs[1], xs[2], xs[3]);
|
|
const minY = Math.min(ys[0], ys[1], ys[2], ys[3]);
|
|
return {
|
|
x: minX,
|
|
y: minY,
|
|
width: Math.max(xs[0], xs[1], xs[2], xs[3]) - minX,
|
|
height: Math.max(ys[0], ys[1], ys[2], ys[3]) - minY,
|
|
};
|
|
}
|
|
/**
|
|
* Apply translation
|
|
* @method
|
|
* @name Konva.Transform#translate
|
|
* @param {Number} x
|
|
* @param {Number} y
|
|
* @returns {Konva.Transform}
|
|
*/
|
|
translate(x, y) {
|
|
this.m[4] += this.m[0] * x + this.m[2] * y;
|
|
this.m[5] += this.m[1] * x + this.m[3] * y;
|
|
return this;
|
|
}
|
|
/**
|
|
* Apply scale
|
|
* @method
|
|
* @name Konva.Transform#scale
|
|
* @param {Number} sx
|
|
* @param {Number} sy
|
|
* @returns {Konva.Transform}
|
|
*/
|
|
scale(sx, sy) {
|
|
this.m[0] *= sx;
|
|
this.m[1] *= sx;
|
|
this.m[2] *= sy;
|
|
this.m[3] *= sy;
|
|
return this;
|
|
}
|
|
/**
|
|
* Apply rotation
|
|
* @method
|
|
* @name Konva.Transform#rotate
|
|
* @param {Number} rad Angle in radians
|
|
* @returns {Konva.Transform}
|
|
*/
|
|
rotate(rad) {
|
|
const c = Math.cos(rad);
|
|
const s = Math.sin(rad);
|
|
const m11 = this.m[0] * c + this.m[2] * s;
|
|
const m12 = this.m[1] * c + this.m[3] * s;
|
|
const m21 = this.m[0] * -s + this.m[2] * c;
|
|
const m22 = this.m[1] * -s + this.m[3] * c;
|
|
this.m[0] = m11;
|
|
this.m[1] = m12;
|
|
this.m[2] = m21;
|
|
this.m[3] = m22;
|
|
return this;
|
|
}
|
|
/**
|
|
* Returns the translation
|
|
* @method
|
|
* @name Konva.Transform#getTranslation
|
|
* @returns {Object} 2D point(x, y)
|
|
*/
|
|
getTranslation() {
|
|
return {
|
|
x: this.m[4],
|
|
y: this.m[5],
|
|
};
|
|
}
|
|
/**
|
|
* Apply skew
|
|
* @method
|
|
* @name Konva.Transform#skew
|
|
* @param {Number} sx
|
|
* @param {Number} sy
|
|
* @returns {Konva.Transform}
|
|
*/
|
|
skew(sx, sy) {
|
|
const m11 = this.m[0] + this.m[2] * sy;
|
|
const m12 = this.m[1] + this.m[3] * sy;
|
|
const m21 = this.m[2] + this.m[0] * sx;
|
|
const m22 = this.m[3] + this.m[1] * sx;
|
|
this.m[0] = m11;
|
|
this.m[1] = m12;
|
|
this.m[2] = m21;
|
|
this.m[3] = m22;
|
|
return this;
|
|
}
|
|
/**
|
|
* Transform multiplication
|
|
* @method
|
|
* @name Konva.Transform#multiply
|
|
* @param {Konva.Transform} matrix
|
|
* @returns {Konva.Transform}
|
|
*/
|
|
multiply(matrix) {
|
|
const m11 = this.m[0] * matrix.m[0] + this.m[2] * matrix.m[1];
|
|
const m12 = this.m[1] * matrix.m[0] + this.m[3] * matrix.m[1];
|
|
const m21 = this.m[0] * matrix.m[2] + this.m[2] * matrix.m[3];
|
|
const m22 = this.m[1] * matrix.m[2] + this.m[3] * matrix.m[3];
|
|
const dx = this.m[0] * matrix.m[4] + this.m[2] * matrix.m[5] + this.m[4];
|
|
const dy = this.m[1] * matrix.m[4] + this.m[3] * matrix.m[5] + this.m[5];
|
|
this.m[0] = m11;
|
|
this.m[1] = m12;
|
|
this.m[2] = m21;
|
|
this.m[3] = m22;
|
|
this.m[4] = dx;
|
|
this.m[5] = dy;
|
|
return this;
|
|
}
|
|
/**
|
|
* Invert the matrix
|
|
* @method
|
|
* @name Konva.Transform#invert
|
|
* @returns {Konva.Transform}
|
|
*/
|
|
// a transform with a zero scale on an axis has no inverse
|
|
isInvertible() {
|
|
return this.m[0] * this.m[3] - this.m[1] * this.m[2] !== 0;
|
|
}
|
|
invert() {
|
|
const d = 1 / (this.m[0] * this.m[3] - this.m[1] * this.m[2]);
|
|
const m0 = this.m[3] * d;
|
|
const m1 = -this.m[1] * d;
|
|
const m2 = -this.m[2] * d;
|
|
const m3 = this.m[0] * d;
|
|
const m4 = d * (this.m[2] * this.m[5] - this.m[3] * this.m[4]);
|
|
const m5 = d * (this.m[1] * this.m[4] - this.m[0] * this.m[5]);
|
|
this.m[0] = m0;
|
|
this.m[1] = m1;
|
|
this.m[2] = m2;
|
|
this.m[3] = m3;
|
|
this.m[4] = m4;
|
|
this.m[5] = m5;
|
|
return this;
|
|
}
|
|
/**
|
|
* return matrix
|
|
* @method
|
|
* @name Konva.Transform#getMatrix
|
|
*/
|
|
getMatrix() {
|
|
return this.m;
|
|
}
|
|
/**
|
|
* convert transformation matrix back into node's attributes
|
|
* @method
|
|
* @name Konva.Transform#decompose
|
|
* @returns {Konva.Transform}
|
|
*/
|
|
decompose() {
|
|
const a = this.m[0];
|
|
const b = this.m[1];
|
|
const c = this.m[2];
|
|
const d = this.m[3];
|
|
const e = this.m[4];
|
|
const f = this.m[5];
|
|
const delta = a * d - b * c;
|
|
const result = {
|
|
x: e,
|
|
y: f,
|
|
rotation: 0,
|
|
scaleX: 0,
|
|
scaleY: 0,
|
|
skewX: 0,
|
|
skewY: 0,
|
|
};
|
|
// Apply the QR-like decomposition. A zero determinant (a scale of 0, or
|
|
// a shear that collapses the plane) has no skew, rather than a NaN or
|
|
// an infinite one
|
|
if (a != 0 || b != 0) {
|
|
const r = Math.sqrt(a * a + b * b);
|
|
result.rotation = b > 0 ? Math.acos(a / r) : -Math.acos(a / r);
|
|
result.scaleX = r;
|
|
result.scaleY = delta / r;
|
|
result.skewX = delta && (a * c + b * d) / delta;
|
|
result.skewY = 0;
|
|
}
|
|
else if (c != 0 || d != 0) {
|
|
const s = Math.sqrt(c * c + d * d);
|
|
result.rotation =
|
|
Math.PI / 2 - (d > 0 ? Math.acos(-c / s) : -Math.acos(c / s));
|
|
result.scaleX = delta / s;
|
|
result.scaleY = s;
|
|
result.skewX = 0;
|
|
result.skewY = delta && (a * c + b * d) / delta;
|
|
}
|
|
else {
|
|
// a = b = c = d = 0
|
|
}
|
|
result.rotation = Util._getRotation(result.rotation);
|
|
return result;
|
|
}
|
|
}
|
|
// CONSTANTS
|
|
const OBJECT_ARRAY = '[object Array]', OBJECT_NUMBER = '[object Number]', OBJECT_STRING = '[object String]', OBJECT_BOOLEAN = '[object Boolean]', PI_OVER_DEG180 = Math.PI / 180, DEG180_OVER_PI = 180 / Math.PI, HASH = '#', EMPTY_STRING = '', ZERO = '0', KONVA_WARNING = 'Konva warning: ', KONVA_ERROR = 'Konva error: ', COLORS = {
|
|
aliceblue: [240, 248, 255],
|
|
antiquewhite: [250, 235, 215],
|
|
aqua: [0, 255, 255],
|
|
aquamarine: [127, 255, 212],
|
|
azure: [240, 255, 255],
|
|
beige: [245, 245, 220],
|
|
bisque: [255, 228, 196],
|
|
black: [0, 0, 0],
|
|
blanchedalmond: [255, 235, 205],
|
|
blue: [0, 0, 255],
|
|
blueviolet: [138, 43, 226],
|
|
brown: [165, 42, 42],
|
|
burlywood: [222, 184, 135],
|
|
cadetblue: [95, 158, 160],
|
|
chartreuse: [127, 255, 0],
|
|
chocolate: [210, 105, 30],
|
|
coral: [255, 127, 80],
|
|
cornflowerblue: [100, 149, 237],
|
|
cornsilk: [255, 248, 220],
|
|
crimson: [220, 20, 60],
|
|
cyan: [0, 255, 255],
|
|
darkblue: [0, 0, 139],
|
|
darkcyan: [0, 139, 139],
|
|
darkgoldenrod: [184, 134, 11],
|
|
darkgray: [169, 169, 169],
|
|
darkgreen: [0, 100, 0],
|
|
darkgrey: [169, 169, 169],
|
|
darkkhaki: [189, 183, 107],
|
|
darkmagenta: [139, 0, 139],
|
|
darkolivegreen: [85, 107, 47],
|
|
darkorange: [255, 140, 0],
|
|
darkorchid: [153, 50, 204],
|
|
darkred: [139, 0, 0],
|
|
darksalmon: [233, 150, 122],
|
|
darkseagreen: [143, 188, 143],
|
|
darkslateblue: [72, 61, 139],
|
|
darkslategray: [47, 79, 79],
|
|
darkslategrey: [47, 79, 79],
|
|
darkturquoise: [0, 206, 209],
|
|
darkviolet: [148, 0, 211],
|
|
deeppink: [255, 20, 147],
|
|
deepskyblue: [0, 191, 255],
|
|
dimgray: [105, 105, 105],
|
|
dimgrey: [105, 105, 105],
|
|
dodgerblue: [30, 144, 255],
|
|
firebrick: [178, 34, 34],
|
|
floralwhite: [255, 250, 240],
|
|
forestgreen: [34, 139, 34],
|
|
fuchsia: [255, 0, 255],
|
|
gainsboro: [220, 220, 220],
|
|
ghostwhite: [248, 248, 255],
|
|
gold: [255, 215, 0],
|
|
goldenrod: [218, 165, 32],
|
|
gray: [128, 128, 128],
|
|
green: [0, 128, 0],
|
|
greenyellow: [173, 255, 47],
|
|
grey: [128, 128, 128],
|
|
honeydew: [240, 255, 240],
|
|
hotpink: [255, 105, 180],
|
|
indianred: [205, 92, 92],
|
|
indigo: [75, 0, 130],
|
|
ivory: [255, 255, 240],
|
|
khaki: [240, 230, 140],
|
|
lavender: [230, 230, 250],
|
|
lavenderblush: [255, 240, 245],
|
|
lawngreen: [124, 252, 0],
|
|
lemonchiffon: [255, 250, 205],
|
|
lightblue: [173, 216, 230],
|
|
lightcoral: [240, 128, 128],
|
|
lightcyan: [224, 255, 255],
|
|
lightgoldenrodyellow: [250, 250, 210],
|
|
lightgray: [211, 211, 211],
|
|
lightgreen: [144, 238, 144],
|
|
lightgrey: [211, 211, 211],
|
|
lightpink: [255, 182, 193],
|
|
lightsalmon: [255, 160, 122],
|
|
lightseagreen: [32, 178, 170],
|
|
lightskyblue: [135, 206, 250],
|
|
lightslategray: [119, 136, 153],
|
|
lightslategrey: [119, 136, 153],
|
|
lightsteelblue: [176, 196, 222],
|
|
lightyellow: [255, 255, 224],
|
|
lime: [0, 255, 0],
|
|
limegreen: [50, 205, 50],
|
|
linen: [250, 240, 230],
|
|
magenta: [255, 0, 255],
|
|
maroon: [128, 0, 0],
|
|
mediumaquamarine: [102, 205, 170],
|
|
mediumblue: [0, 0, 205],
|
|
mediumorchid: [186, 85, 211],
|
|
mediumpurple: [147, 112, 219],
|
|
mediumseagreen: [60, 179, 113],
|
|
mediumslateblue: [123, 104, 238],
|
|
mediumspringgreen: [0, 250, 154],
|
|
mediumturquoise: [72, 209, 204],
|
|
mediumvioletred: [199, 21, 133],
|
|
midnightblue: [25, 25, 112],
|
|
mintcream: [245, 255, 250],
|
|
mistyrose: [255, 228, 225],
|
|
moccasin: [255, 228, 181],
|
|
navajowhite: [255, 222, 173],
|
|
navy: [0, 0, 128],
|
|
oldlace: [253, 245, 230],
|
|
olive: [128, 128, 0],
|
|
olivedrab: [107, 142, 35],
|
|
orange: [255, 165, 0],
|
|
orangered: [255, 69, 0],
|
|
orchid: [218, 112, 214],
|
|
palegoldenrod: [238, 232, 170],
|
|
palegreen: [152, 251, 152],
|
|
paleturquoise: [175, 238, 238],
|
|
palevioletred: [219, 112, 147],
|
|
papayawhip: [255, 239, 213],
|
|
peachpuff: [255, 218, 185],
|
|
peru: [205, 133, 63],
|
|
pink: [255, 192, 203],
|
|
plum: [221, 160, 221],
|
|
powderblue: [176, 224, 230],
|
|
purple: [128, 0, 128],
|
|
rebeccapurple: [102, 51, 153],
|
|
red: [255, 0, 0],
|
|
rosybrown: [188, 143, 143],
|
|
royalblue: [65, 105, 225],
|
|
saddlebrown: [139, 69, 19],
|
|
salmon: [250, 128, 114],
|
|
sandybrown: [244, 164, 96],
|
|
seagreen: [46, 139, 87],
|
|
seashell: [255, 245, 238],
|
|
sienna: [160, 82, 45],
|
|
silver: [192, 192, 192],
|
|
skyblue: [135, 206, 235],
|
|
slateblue: [106, 90, 205],
|
|
slategray: [112, 128, 144],
|
|
slategrey: [112, 128, 144],
|
|
snow: [255, 250, 250],
|
|
springgreen: [0, 255, 127],
|
|
steelblue: [70, 130, 180],
|
|
tan: [210, 180, 140],
|
|
teal: [0, 128, 128],
|
|
thistle: [216, 191, 216],
|
|
transparent: [0, 0, 0, 0],
|
|
tomato: [255, 99, 71],
|
|
turquoise: [64, 224, 208],
|
|
violet: [238, 130, 238],
|
|
wheat: [245, 222, 179],
|
|
white: [255, 255, 255],
|
|
whitesmoke: [245, 245, 245],
|
|
yellow: [255, 255, 0],
|
|
yellowgreen: [154, 205, 50],
|
|
};
|
|
// Cache for canvas farbling detection
|
|
let _isCanvasFarblingActive = null;
|
|
// A stage may be rendered in another window than the one Konva was imported
|
|
// into, and a window that is not visible gives almost no frames. So frames are
|
|
// asked from the window of the stage, and the callbacks are kept per window
|
|
const defaultWindow = typeof window !== 'undefined' ? window : {};
|
|
const animQueues = new WeakMap();
|
|
const requestFrame = (win, f) => {
|
|
if (typeof win.requestAnimationFrame === 'function') {
|
|
win.requestAnimationFrame(f);
|
|
}
|
|
else if (typeof requestAnimationFrame !== 'undefined') {
|
|
requestAnimationFrame(f);
|
|
}
|
|
else {
|
|
setTimeout(f, 16); // 60fps ≈ 16.67ms per frame
|
|
}
|
|
};
|
|
const capitalizeCache = new Map();
|
|
// the common ancestor of every typed array
|
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray#description
|
|
export const TypedArray = Object.getPrototypeOf(Int8Array);
|
|
// Split the components of rgb()/hsl(). CSS separates them with commas (legacy
|
|
// syntax), or with spaces and a slash before the alpha (CSS Color 4 syntax),
|
|
// but never with both. Keep the two apart, so that a space inside a
|
|
// comma-separated color adds no component. A component the user left empty
|
|
// stays as "", which then fails the color.
|
|
const splitColorComponents = (str) => {
|
|
const components = str.trim();
|
|
return components.indexOf(',') === -1
|
|
? components.split(/\s*\/\s*|\s+/)
|
|
: components.split(/\s*,\s*/);
|
|
};
|
|
// A CSS number: "50", "-0.5", ".5", "1e2". Everything else, from "50abc" to
|
|
// "0x10" and "1.2.3", is not a number and makes the color fail.
|
|
const NUMBER_SOURCE = '[+-]?(?:\\d+\\.?\\d*|\\.\\d+)(?:e[+-]?\\d+)?';
|
|
// A component is a number with an optional "%" sign.
|
|
const COLOR_COMPONENT_REGEX = new RegExp(`^(${NUMBER_SOURCE})(%?)$`, 'i');
|
|
// Parse one color component. 100% is `max`: 255 for the color channels of
|
|
// rgb(), 1 for the alpha, 100 for the saturation and the lightness of hsl().
|
|
// CSS clamps a component to [0, max], and so do we: Konva multiplies and
|
|
// interpolates these numbers (shadowOpacity, Tween) before the canvas sees
|
|
// them. NaN stays NaN, so a bad value still fails.
|
|
const parseColorComponent = (value, max) => {
|
|
const match = COLOR_COMPONENT_REGEX.exec(value);
|
|
if (!match) {
|
|
return NaN;
|
|
}
|
|
const n = match[2] ? (Number(match[1]) / 100) * max : Number(match[1]);
|
|
return Math.min(Math.max(n, 0), max);
|
|
};
|
|
// parseInt() stops at the first character it can not read, so "#0g0000" would
|
|
// give a valid black instead of failing. The hex parsers test the shape first.
|
|
const HEX_COLOR_REGEX = /^#[0-9a-f]+$/i;
|
|
// The hue of hsl() is the same number, with a CSS Color 4 angle unit
|
|
// instead of the "%" sign. No unit means degrees.
|
|
const HUE_REGEX = new RegExp(`^(${NUMBER_SOURCE})(deg|grad|rad|turn)?$`, 'i');
|
|
const HUE_UNITS = {
|
|
deg: 1,
|
|
grad: 0.9,
|
|
rad: DEG180_OVER_PI,
|
|
turn: 360,
|
|
};
|
|
/**
|
|
* @namespace Util
|
|
* @memberof Konva
|
|
*/
|
|
// a negative corner radius would throw in context.arc()
|
|
function clampRadius(radius, max) {
|
|
return Math.min(Math.max(radius || 0, 0), max);
|
|
}
|
|
export const Util = {
|
|
/*
|
|
* cherry-picked utilities from underscore.js
|
|
*/
|
|
_isElement(obj) {
|
|
return !!(obj && obj.nodeType == 1);
|
|
},
|
|
_isFunction(obj) {
|
|
return !!(obj && obj.constructor && obj.call && obj.apply);
|
|
},
|
|
_isPlainObject(obj) {
|
|
return !!obj && obj.constructor === Object;
|
|
},
|
|
_isArray(obj) {
|
|
return Object.prototype.toString.call(obj) === OBJECT_ARRAY;
|
|
},
|
|
_isNumber(obj) {
|
|
return (Object.prototype.toString.call(obj) === OBJECT_NUMBER &&
|
|
!isNaN(obj) &&
|
|
isFinite(obj));
|
|
},
|
|
_isString(obj) {
|
|
return Object.prototype.toString.call(obj) === OBJECT_STRING;
|
|
},
|
|
_isBoolean(obj) {
|
|
return Object.prototype.toString.call(obj) === OBJECT_BOOLEAN;
|
|
},
|
|
// arrays are objects too
|
|
isObject(val) {
|
|
return val instanceof Object;
|
|
},
|
|
isValidSelector(selector) {
|
|
if (typeof selector !== 'string') {
|
|
return false;
|
|
}
|
|
const firstChar = selector[0];
|
|
return (firstChar === '#' ||
|
|
firstChar === '.' ||
|
|
firstChar === firstChar.toUpperCase());
|
|
},
|
|
_sign(number) {
|
|
if (number === 0) {
|
|
// that is not what sign usually returns
|
|
// but that is what we need
|
|
return 1;
|
|
}
|
|
if (number > 0) {
|
|
return 1;
|
|
}
|
|
else {
|
|
return -1;
|
|
}
|
|
},
|
|
requestAnimFrame(callback, win) {
|
|
const target = (win && !win.closed && win) || defaultWindow;
|
|
let queue = animQueues.get(target);
|
|
if (!queue) {
|
|
queue = [];
|
|
animQueues.set(target, queue);
|
|
requestFrame(target, function () {
|
|
animQueues.delete(target);
|
|
queue.forEach(function (cb) {
|
|
cb();
|
|
});
|
|
});
|
|
}
|
|
queue.push(callback);
|
|
},
|
|
createCanvasElement() {
|
|
ensureBrowser();
|
|
const canvas = document.createElement('canvas');
|
|
// on some environments canvas.style is readonly
|
|
try {
|
|
canvas.style = canvas.style || {};
|
|
}
|
|
catch (e) { }
|
|
return canvas;
|
|
},
|
|
createImageElement() {
|
|
ensureBrowser();
|
|
return document.createElement('img');
|
|
},
|
|
/*
|
|
* arg can be an image object or image data
|
|
*/
|
|
_urlToImage(url, callback, onError) {
|
|
// if arg is a string, then it's a data url
|
|
const imageObj = Util.createImageElement();
|
|
imageObj.onload = function () {
|
|
callback(imageObj);
|
|
};
|
|
imageObj.onerror = (event) => {
|
|
onError === null || onError === void 0 ? void 0 : onError(event instanceof Error ? event : new Error('Unable to load image.'));
|
|
};
|
|
imageObj.src = url;
|
|
},
|
|
_rgbToHex(r, g, b) {
|
|
return ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
|
|
},
|
|
_hexToRgb(hex) {
|
|
hex = hex.replace(HASH, EMPTY_STRING);
|
|
const bigint = parseInt(hex, 16);
|
|
return {
|
|
r: (bigint >> 16) & 255,
|
|
g: (bigint >> 8) & 255,
|
|
b: bigint & 255,
|
|
};
|
|
},
|
|
/**
|
|
* return random hex color
|
|
* @method
|
|
* @memberof Konva.Util
|
|
* @example
|
|
* shape.fill(Konva.Util.getRandomColor());
|
|
*/
|
|
getRandomColor() {
|
|
let randColor = ((Math.random() * 0xffffff) << 0).toString(16);
|
|
while (randColor.length < 6) {
|
|
randColor = ZERO + randColor;
|
|
}
|
|
return HASH + randColor;
|
|
},
|
|
/**
|
|
* Check if canvas farbling is active (e.g., Brave browser fingerprinting protection)
|
|
* @method
|
|
* @memberof Konva.Util
|
|
* @returns {Boolean}
|
|
*/
|
|
isCanvasFarblingActive() {
|
|
if (_isCanvasFarblingActive !== null) {
|
|
return _isCanvasFarblingActive;
|
|
}
|
|
if (typeof document === 'undefined') {
|
|
_isCanvasFarblingActive = false;
|
|
return false;
|
|
}
|
|
const c = this.createCanvasElement();
|
|
c.width = 10;
|
|
c.height = 10;
|
|
const ctx = c.getContext('2d', {
|
|
willReadFrequently: true,
|
|
});
|
|
ctx.clearRect(0, 0, 10, 10);
|
|
ctx.fillStyle = '#282828'; // 40, 40, 40
|
|
ctx.fillRect(0, 0, 10, 10);
|
|
const d = ctx.getImageData(0, 0, 10, 10).data;
|
|
let isFarbling = false;
|
|
for (let i = 0; i < 100; i++) {
|
|
if (d[i * 4] !== 40 ||
|
|
d[i * 4 + 1] !== 40 ||
|
|
d[i * 4 + 2] !== 40 ||
|
|
d[i * 4 + 3] !== 255) {
|
|
isFarbling = true;
|
|
break;
|
|
}
|
|
}
|
|
_isCanvasFarblingActive = isFarbling;
|
|
this.releaseCanvas(c);
|
|
return _isCanvasFarblingActive;
|
|
},
|
|
/**
|
|
* Get a random color for hit detection (snapped to the hit color grid)
|
|
* @method
|
|
* @memberof Konva.Util
|
|
* @returns {String} hex color string
|
|
*/
|
|
getHitColor() {
|
|
const channel = () => (Math.random() * 256) | 0;
|
|
return this.getHitColorKey(channel(), channel(), channel());
|
|
},
|
|
/**
|
|
* Get hit color key from RGB values (snapped to the hit color grid)
|
|
* @method
|
|
* @memberof Konva.Util
|
|
* @param {Number} r - red component (0-255)
|
|
* @param {Number} g - green component (0-255)
|
|
* @param {Number} b - blue component (0-255)
|
|
* @returns {String} hex color key string
|
|
*/
|
|
getHitColorKey(r, g, b) {
|
|
// hit colours live on a grid, so a pixel of the hit graph that reads
|
|
// back slightly off still rounds to the key of its shape: an edge pixel
|
|
// is stored premultiplied by its alpha and comes back off by up to one
|
|
// per channel, canvas farbling (Brave) adds a little noise on top
|
|
const step = this.isCanvasFarblingActive() ? 5 : 3;
|
|
const snap = (value) => Math.round(value / step) * step;
|
|
return HASH + this._rgbToHex(snap(r), snap(g), snap(b));
|
|
},
|
|
/**
|
|
* Snap a hex color to the hit color grid
|
|
* @method
|
|
* @memberof Konva.Util
|
|
* @param {String} hex - hex color string (e.g., "#ff00ff")
|
|
* @returns {String} snapped hex color string
|
|
*/
|
|
getSnappedHexColor(hex) {
|
|
const { r, g, b } = this._hexToRgb(hex);
|
|
return this.getHitColorKey(r, g, b);
|
|
},
|
|
/**
|
|
* get RGB components of a color
|
|
* @method
|
|
* @memberof Konva.Util
|
|
* @param {String} color
|
|
* @example
|
|
* // each of the following examples return {r:0, g:0, b:255}
|
|
* var rgb = Konva.Util.getRGB('blue');
|
|
* var rgb = Konva.Util.getRGB('#0000ff');
|
|
* var rgb = Konva.Util.getRGB('rgb(0,0,255)');
|
|
*/
|
|
getRGB(color) {
|
|
var _a;
|
|
// black for a color we can not parse
|
|
const { r = 0, g = 0, b = 0 } = (_a = Util.colorToRGBA(color)) !== null && _a !== void 0 ? _a : {};
|
|
return { r, g, b };
|
|
},
|
|
// convert any color string to RGBA object
|
|
// from https://github.com/component/color-parser
|
|
colorToRGBA(str) {
|
|
// a CSS custom property keeps the space after the colon,
|
|
// so "--brand: #ff0000" gives us " #ff0000"
|
|
str = (str || '').trim() || 'black';
|
|
const color = Util._namedColorToRBA(str) ||
|
|
Util._hex3ColorToRGBA(str) ||
|
|
Util._hex4ColorToRGBA(str) ||
|
|
Util._hex6ColorToRGBA(str) ||
|
|
Util._hex8ColorToRGBA(str) ||
|
|
Util._rgbColorToRGBA(str) ||
|
|
Util._hslColorToRGBA(str);
|
|
// a NaN component stays invisible until it reaches the canvas and drops the
|
|
// shape, so a color we can not fully parse must fail as a whole
|
|
if (color && [color.r, color.g, color.b, color.a].every(Util._isNumber)) {
|
|
return color;
|
|
}
|
|
},
|
|
// Parse named css color. Like "green"
|
|
_namedColorToRBA(str) {
|
|
const c = COLORS[str.toLowerCase()];
|
|
if (!c) {
|
|
return null;
|
|
}
|
|
return {
|
|
r: c[0],
|
|
g: c[1],
|
|
b: c[2],
|
|
a: c.length > 3 ? c[3] : 1,
|
|
};
|
|
},
|
|
// Parse rgb(n, n, n), rgba(n, n, n, n) and rgb(n n n / n)
|
|
_rgbColorToRGBA(str) {
|
|
const match = /^rgba?\(([^)]*)\)$/i.exec(str);
|
|
if (!match) {
|
|
return;
|
|
}
|
|
const parts = splitColorComponents(match[1]);
|
|
if (parts.length < 3 || parts.length > 4) {
|
|
return;
|
|
}
|
|
return {
|
|
r: parseColorComponent(parts[0], 255),
|
|
g: parseColorComponent(parts[1], 255),
|
|
b: parseColorComponent(parts[2], 255),
|
|
a: parts.length > 3 ? parseColorComponent(parts[3], 1) : 1,
|
|
};
|
|
},
|
|
// Parse #nnnnnnnn
|
|
_hex8ColorToRGBA(str) {
|
|
if (str.length === 9 && HEX_COLOR_REGEX.test(str)) {
|
|
return {
|
|
r: parseInt(str.slice(1, 3), 16),
|
|
g: parseInt(str.slice(3, 5), 16),
|
|
b: parseInt(str.slice(5, 7), 16),
|
|
a: parseInt(str.slice(7, 9), 16) / 0xff,
|
|
};
|
|
}
|
|
},
|
|
// Parse #nnnnnn
|
|
_hex6ColorToRGBA(str) {
|
|
if (str.length === 7 && HEX_COLOR_REGEX.test(str)) {
|
|
return {
|
|
r: parseInt(str.slice(1, 3), 16),
|
|
g: parseInt(str.slice(3, 5), 16),
|
|
b: parseInt(str.slice(5, 7), 16),
|
|
a: 1,
|
|
};
|
|
}
|
|
},
|
|
// Parse #nnnn
|
|
_hex4ColorToRGBA(str) {
|
|
if (str.length === 5 && HEX_COLOR_REGEX.test(str)) {
|
|
return {
|
|
r: parseInt(str[1] + str[1], 16),
|
|
g: parseInt(str[2] + str[2], 16),
|
|
b: parseInt(str[3] + str[3], 16),
|
|
a: parseInt(str[4] + str[4], 16) / 0xff,
|
|
};
|
|
}
|
|
},
|
|
// Parse #nnn
|
|
_hex3ColorToRGBA(str) {
|
|
if (str.length === 4 && HEX_COLOR_REGEX.test(str)) {
|
|
return {
|
|
r: parseInt(str[1] + str[1], 16),
|
|
g: parseInt(str[2] + str[2], 16),
|
|
b: parseInt(str[3] + str[3], 16),
|
|
a: 1,
|
|
};
|
|
}
|
|
},
|
|
// Code adapted from https://github.com/Qix-/color-convert/blob/master/conversions.js#L244
|
|
// Parse hsl(h, s%, l%), hsla(h, s%, l%, n) and hsl(h s% l% / n)
|
|
_hslColorToRGBA(str) {
|
|
const match = /^hsla?\(([^)]*)\)$/i.exec(str);
|
|
if (!match) {
|
|
return;
|
|
}
|
|
const parts = splitColorComponents(match[1]);
|
|
if (parts.length < 3 || parts.length > 4) {
|
|
return;
|
|
}
|
|
// a hue we can not parse gives a gray color instead of a NaN one,
|
|
// so colorToRGBA() can not catch it at the end
|
|
const hue = HUE_REGEX.exec(parts[0]);
|
|
if (!hue) {
|
|
return;
|
|
}
|
|
const unit = hue[2];
|
|
const degrees = Number(hue[1]) * (unit ? HUE_UNITS[unit.toLowerCase()] : 1);
|
|
if (!isFinite(degrees)) {
|
|
return;
|
|
}
|
|
// the hue is an angle, so keep it in [0, 360) to also accept
|
|
// negative angles and angles over one full turn
|
|
const h = (((degrees % 360) + 360) % 360) / 360;
|
|
// the saturation and the lightness are percentages of 1, written with or
|
|
// without the "%" sign, so read them out of 100 and then scale them down
|
|
const s = parseColorComponent(parts[1], 100) / 100;
|
|
const l = parseColorComponent(parts[2], 100) / 100;
|
|
const a = parts.length > 3 ? parseColorComponent(parts[3], 1) : 1;
|
|
const t2 = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
const t1 = 2 * l - t2;
|
|
const rgb = [0, 0, 0];
|
|
for (let i = 0; i < 3; i++) {
|
|
let t3 = h + (1 / 3) * -(i - 1);
|
|
if (t3 < 0) {
|
|
t3++;
|
|
}
|
|
if (t3 > 1) {
|
|
t3--;
|
|
}
|
|
let val;
|
|
if (6 * t3 < 1) {
|
|
val = t1 + (t2 - t1) * 6 * t3;
|
|
}
|
|
else if (2 * t3 < 1) {
|
|
val = t2;
|
|
}
|
|
else if (3 * t3 < 2) {
|
|
val = t1 + (t2 - t1) * (2 / 3 - t3) * 6;
|
|
}
|
|
else {
|
|
val = t1;
|
|
}
|
|
rgb[i] = val * 255;
|
|
}
|
|
return {
|
|
r: Math.round(rgb[0]),
|
|
g: Math.round(rgb[1]),
|
|
b: Math.round(rgb[2]),
|
|
a,
|
|
};
|
|
},
|
|
// the bounds of a flat [x0, y0, x1, y1, ...] array. A NaN coordinate is
|
|
// skipped so one bad point cannot turn the whole box into NaN; no usable
|
|
// point at all is an empty rect
|
|
_getPointsRect(points) {
|
|
let minX = Infinity;
|
|
let minY = Infinity;
|
|
let maxX = -Infinity;
|
|
let maxY = -Infinity;
|
|
for (let i = 0; i < points.length; i += 2) {
|
|
const x = points[i];
|
|
const y = points[i + 1];
|
|
if (!isNaN(x)) {
|
|
minX = Math.min(minX, x);
|
|
maxX = Math.max(maxX, x);
|
|
}
|
|
if (!isNaN(y)) {
|
|
minY = Math.min(minY, y);
|
|
maxY = Math.max(maxY, y);
|
|
}
|
|
}
|
|
if (!isFinite(minX + minY)) {
|
|
return { x: 0, y: 0, width: 0, height: 0 };
|
|
}
|
|
return {
|
|
x: minX,
|
|
y: minY,
|
|
width: maxX - minX,
|
|
height: maxY - minY,
|
|
};
|
|
},
|
|
/**
|
|
* check intersection of two client rectangles
|
|
* @method
|
|
* @memberof Konva.Util
|
|
* @param {Object} r1 - { x, y, width, height } client rectangle
|
|
* @param {Object} r2 - { x, y, width, height } client rectangle
|
|
* @example
|
|
* const overlapping = Konva.Util.haveIntersection(shape1.getClientRect(), shape2.getClientRect());
|
|
*/
|
|
haveIntersection(r1, r2) {
|
|
return !(r2.x > r1.x + r1.width ||
|
|
r2.x + r2.width < r1.x ||
|
|
r2.y > r1.y + r1.height ||
|
|
r2.y + r2.height < r1.y);
|
|
},
|
|
// a deep copy of plain objects and arrays; class instances and elements
|
|
// are shared, typed arrays are copied
|
|
cloneObject(obj) {
|
|
const copy = {};
|
|
for (const key in obj) {
|
|
copy[key] = Util._cloneValue(obj[key]);
|
|
}
|
|
return copy;
|
|
},
|
|
_cloneValue(val) {
|
|
if (Util._isArray(val)) {
|
|
// a flat array (points) is sliced, an array holding objects copied deep
|
|
return val.some((item) => typeof item === 'object')
|
|
? val.map(Util._cloneValue)
|
|
: val.slice();
|
|
}
|
|
if (Util._isPlainObject(val)) {
|
|
return Util.cloneObject(val);
|
|
}
|
|
return val instanceof TypedArray ? val.slice() : val;
|
|
},
|
|
cloneArray(arr) {
|
|
return arr.slice(0);
|
|
},
|
|
degToRad(deg) {
|
|
return deg * PI_OVER_DEG180;
|
|
},
|
|
radToDeg(rad) {
|
|
return rad * DEG180_OVER_PI;
|
|
},
|
|
_getRotation(radians) {
|
|
return Konva.angleDeg ? Util.radToDeg(radians) : radians;
|
|
},
|
|
// Memoized — called per-attr per setAttrs; input vocabulary is bounded.
|
|
_capitalize(str) {
|
|
const cached = capitalizeCache.get(str);
|
|
if (cached !== undefined)
|
|
return cached;
|
|
const out = str.charAt(0).toUpperCase() + str.slice(1);
|
|
capitalizeCache.set(str, out);
|
|
return out;
|
|
},
|
|
throw(str) {
|
|
throw new Error(KONVA_ERROR + str);
|
|
},
|
|
error(str) {
|
|
console.error(KONVA_ERROR + str);
|
|
},
|
|
warn(str) {
|
|
if (!Konva.showWarnings) {
|
|
return;
|
|
}
|
|
console.warn(KONVA_WARNING + str);
|
|
},
|
|
_batchEvents(batch, run) {
|
|
if (!batch)
|
|
return run();
|
|
let called = false;
|
|
let active = true;
|
|
try {
|
|
batch(() => {
|
|
if (called || !active) {
|
|
Util.warn('eventBatchFunc must call its argument exactly once, synchronously.');
|
|
return;
|
|
}
|
|
called = true;
|
|
run();
|
|
});
|
|
}
|
|
finally {
|
|
active = false;
|
|
if (!called) {
|
|
Util.warn('eventBatchFunc must call its argument synchronously.');
|
|
}
|
|
}
|
|
},
|
|
each(obj, func) {
|
|
for (const key in obj) {
|
|
func(key, obj[key]);
|
|
}
|
|
},
|
|
_inRange(val, left, right) {
|
|
return left <= val && val < right;
|
|
},
|
|
_getProjectionToSegment(x1, y1, x2, y2, x3, y3) {
|
|
let x, y, dist;
|
|
const pd2 = (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2);
|
|
if (pd2 == 0) {
|
|
x = x1;
|
|
y = y1;
|
|
dist = (x3 - x2) * (x3 - x2) + (y3 - y2) * (y3 - y2);
|
|
}
|
|
else {
|
|
const u = ((x3 - x1) * (x2 - x1) + (y3 - y1) * (y2 - y1)) / pd2;
|
|
if (u < 0) {
|
|
x = x1;
|
|
y = y1;
|
|
dist = (x1 - x3) * (x1 - x3) + (y1 - y3) * (y1 - y3);
|
|
}
|
|
else if (u > 1.0) {
|
|
x = x2;
|
|
y = y2;
|
|
dist = (x2 - x3) * (x2 - x3) + (y2 - y3) * (y2 - y3);
|
|
}
|
|
else {
|
|
x = x1 + u * (x2 - x1);
|
|
y = y1 + u * (y2 - y1);
|
|
dist = (x - x3) * (x - x3) + (y - y3) * (y - y3);
|
|
}
|
|
}
|
|
return [x, y, dist];
|
|
},
|
|
// line as array of points.
|
|
// line might be closed
|
|
_getProjectionToLine(pt, line, isClosed) {
|
|
const pc = Util.cloneObject(pt);
|
|
let dist = Number.MAX_VALUE;
|
|
line.forEach(function (p1, i) {
|
|
if (!isClosed && i === line.length - 1) {
|
|
return;
|
|
}
|
|
const p2 = line[(i + 1) % line.length];
|
|
const proj = Util._getProjectionToSegment(p1.x, p1.y, p2.x, p2.y, pt.x, pt.y);
|
|
const px = proj[0], py = proj[1], pdist = proj[2];
|
|
if (pdist < dist) {
|
|
pc.x = px;
|
|
pc.y = py;
|
|
dist = pdist;
|
|
}
|
|
});
|
|
return pc;
|
|
},
|
|
_prepareArrayForTween(startArray, endArray, isClosed) {
|
|
const start = [], end = [];
|
|
if (startArray.length > endArray.length) {
|
|
const temp = endArray;
|
|
endArray = startArray;
|
|
startArray = temp;
|
|
}
|
|
for (let n = 0; n < startArray.length; n += 2) {
|
|
start.push({
|
|
x: startArray[n],
|
|
y: startArray[n + 1],
|
|
});
|
|
}
|
|
for (let n = 0; n < endArray.length; n += 2) {
|
|
end.push({
|
|
x: endArray[n],
|
|
y: endArray[n + 1],
|
|
});
|
|
}
|
|
const newStart = [];
|
|
end.forEach(function (point) {
|
|
const pr = Util._getProjectionToLine(point, start, isClosed);
|
|
newStart.push(pr.x);
|
|
newStart.push(pr.y);
|
|
});
|
|
return newStart;
|
|
},
|
|
// copies plain objects and arrays without DOM elements and circular
|
|
// references, so the input (the live attrs of a node) is never modified.
|
|
// Other objects (Date, class instances) are kept as they are
|
|
_prepareToStringify(obj, ancestors = new Set()) {
|
|
const copy = Util._isArray(obj) ? [] : {};
|
|
ancestors.add(obj);
|
|
for (const key of Object.keys(obj)) {
|
|
const val = obj[key];
|
|
if (Util._isElement(val) || ancestors.has(val)) {
|
|
continue;
|
|
}
|
|
copy[key] =
|
|
Util._isPlainObject(val) || Util._isArray(val)
|
|
? Util._prepareToStringify(val, ancestors)
|
|
: val;
|
|
}
|
|
ancestors.delete(obj);
|
|
return copy;
|
|
},
|
|
// very simplified version of Object.assign
|
|
_assign(target, source) {
|
|
for (const key in source) {
|
|
target[key] = source[key];
|
|
}
|
|
return target;
|
|
},
|
|
_getEventType(type) {
|
|
if (type.indexOf('pointer') >= 0)
|
|
return 'pointer';
|
|
if (type.indexOf('touch') >= 0)
|
|
return 'touch';
|
|
return 'mouse';
|
|
},
|
|
_getFirstPointerId(evt) {
|
|
var _a;
|
|
if (!evt.touches) {
|
|
return (_a = evt.pointerId) !== null && _a !== void 0 ? _a : 999;
|
|
}
|
|
else {
|
|
return evt.changedTouches[0].identifier;
|
|
}
|
|
},
|
|
releaseCanvas(...canvases) {
|
|
if (!Konva.releaseCanvasOnDestroy)
|
|
return;
|
|
canvases.forEach((c) => {
|
|
c.width = 0;
|
|
c.height = 0;
|
|
});
|
|
},
|
|
// [topLeft, topRight, bottomRight, bottomLeft] radii that fit the box
|
|
_cornerRadii(cornerRadius, width, height) {
|
|
const max = Math.min(width, height) / 2;
|
|
if (typeof cornerRadius === 'number') {
|
|
const radius = clampRadius(cornerRadius, max);
|
|
return [radius, radius, radius, radius];
|
|
}
|
|
return [
|
|
clampRadius(cornerRadius[0], max),
|
|
clampRadius(cornerRadius[1], max),
|
|
clampRadius(cornerRadius[2], max),
|
|
clampRadius(cornerRadius[3], max),
|
|
];
|
|
},
|
|
drawRoundedRectPath(context, width, height, cornerRadius) {
|
|
// if negative dimensions, abs width/height and move rectangle
|
|
let xOrigin = width < 0 ? width : 0;
|
|
let yOrigin = height < 0 ? height : 0;
|
|
width = Math.abs(width);
|
|
height = Math.abs(height);
|
|
const [topLeft, topRight, bottomRight, bottomLeft] = Util._cornerRadii(cornerRadius, width, height);
|
|
context.moveTo(xOrigin + topLeft, yOrigin);
|
|
context.lineTo(xOrigin + width - topRight, yOrigin);
|
|
context.arc(xOrigin + width - topRight, yOrigin + topRight, topRight, (Math.PI * 3) / 2, 0, false);
|
|
context.lineTo(xOrigin + width, yOrigin + height - bottomRight);
|
|
context.arc(xOrigin + width - bottomRight, yOrigin + height - bottomRight, bottomRight, 0, Math.PI / 2, false);
|
|
context.lineTo(xOrigin + bottomLeft, yOrigin + height);
|
|
context.arc(xOrigin + bottomLeft, yOrigin + height - bottomLeft, bottomLeft, Math.PI / 2, Math.PI, false);
|
|
context.lineTo(xOrigin, yOrigin + topLeft);
|
|
context.arc(xOrigin + topLeft, yOrigin + topLeft, topLeft, Math.PI, (Math.PI * 3) / 2, false);
|
|
},
|
|
drawRoundedPolygonPath(context, points, sides, radius, cornerRadius) {
|
|
radius = Math.abs(radius);
|
|
for (let i = 0; i < sides; i++) {
|
|
const prev = points[(i - 1 + sides) % sides];
|
|
const curr = points[i];
|
|
const next = points[(i + 1) % sides];
|
|
const vec1 = { x: curr.x - prev.x, y: curr.y - prev.y };
|
|
const vec2 = { x: next.x - curr.x, y: next.y - curr.y };
|
|
const len1 = Math.hypot(vec1.x, vec1.y);
|
|
const len2 = Math.hypot(vec2.x, vec2.y);
|
|
let currCornerRadius;
|
|
if (typeof cornerRadius === 'number') {
|
|
currCornerRadius = cornerRadius;
|
|
}
|
|
else {
|
|
currCornerRadius = i < cornerRadius.length ? cornerRadius[i] : 0;
|
|
}
|
|
const maxCornerRadius = radius * Math.cos(Math.PI / sides);
|
|
// cornerRadius creates perfect circle at 1/2 radius
|
|
currCornerRadius =
|
|
maxCornerRadius * Math.min(1, (currCornerRadius / radius) * 2);
|
|
const normalVec1 = { x: vec1.x / len1, y: vec1.y / len1 };
|
|
const normalVec2 = { x: vec2.x / len2, y: vec2.y / len2 };
|
|
const p1 = {
|
|
x: curr.x - normalVec1.x * currCornerRadius,
|
|
y: curr.y - normalVec1.y * currCornerRadius,
|
|
};
|
|
const p2 = {
|
|
x: curr.x + normalVec2.x * currCornerRadius,
|
|
y: curr.y + normalVec2.y * currCornerRadius,
|
|
};
|
|
if (i === 0) {
|
|
context.moveTo(p1.x, p1.y);
|
|
}
|
|
else {
|
|
context.lineTo(p1.x, p1.y);
|
|
}
|
|
context.arcTo(curr.x, curr.y, p2.x, p2.y, currCornerRadius);
|
|
}
|
|
},
|
|
};
|