UNPKG

konva

Version:

HTML5 2d canvas library for interactive graphics, design editors, whiteboards, and diagrams.

1,196 lines (1,190 loc) 39.9 kB
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], }; } /** * 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); } }, };