diff --git a/src/color/color.class.ts b/src/color/color.class.ts index c70d6c33760..3d9699187f0 100644 --- a/src/color/color.class.ts +++ b/src/color/color.class.ts @@ -1,18 +1,22 @@ -//@ts-nocheck - -import { max as arrayMax, min as arrayMin } from "../util/index"; - /** - * Color class - * The purpose of {@link Color} is to abstract and encapsulate common color operations; - * {@link Color} is a constructor and creates instances of {@link Color} objects. - * - * @class Color - * @param {String} color optional in hex or rgb(a) or hsl format or from known color list - * @return {Color} thisArg - * @tutorial {@link http://fabricjs.com/fabric-intro-part-2/#colors} - */ - function Color(color) { +//@ts-nocheck +import { max, min } from '../util'; +import { ColorNameMap } from './color_map'; +import { reHSLa, reHex, reRGBa } from './constants'; +import { hue2rgb } from './hue2rgb'; + +/** + * Color class + * The purpose of {@link Color} is to abstract and encapsulate common color operations; + * {@link Color} is a constructor and creates instances of {@link Color} objects. + * + * @class Color + * @param {String} color optional in hex or rgb(a) or hsl format or from known color list + * @return {Color} thisArg + * @tutorial {@link http://fabricjs.com/fabric-intro-part-2/#colors} + */ +export class Color { + constructor(color?) { if (!color) { this.setSource([0, 0, 0, 1]); } @@ -21,469 +25,257 @@ import { max as arrayMax, min as arrayMin } from "../util/index"; } } - Color = Color; + /** + * @private + * @param {String|Array} color Color value to parse + */ + _tryParsingColor(color) { + let source; - Color.prototype = /** @lends Color.prototype */ { + if (color in ColorNameMap) { + color = ColorNameMap[color]; + } - /** - * @private - * @param {String|Array} color Color value to parse - */ - _tryParsingColor: function(color) { - var source; + if (color === 'transparent') { + source = [255, 255, 255, 0]; + } - if (color in Color.colorNameMap) { - color = Color.colorNameMap[color]; - } + if (!source) { + source = Color.sourceFromHex(color); + } + if (!source) { + source = Color.sourceFromRgb(color); + } + if (!source) { + source = Color.sourceFromHsl(color); + } + if (!source) { + //if color is not recognize let's make black as canvas does + source = [0, 0, 0, 1]; + } + if (source) { + this.setSource(source); + } + } - if (color === 'transparent') { - source = [255, 255, 255, 0]; - } + /** + * Adapted from https://github.com/mjijackson + * @private + * @param {Number} r Red color value + * @param {Number} g Green color value + * @param {Number} b Blue color value + * @return {Array} Hsl color + */ + _rgbToHsl(r, g, b) { + r /= 255; g /= 255; b /= 255; - if (!source) { - source = Color.sourceFromHex(color); - } - if (!source) { - source = Color.sourceFromRgb(color); - } - if (!source) { - source = Color.sourceFromHsl(color); - } - if (!source) { - //if color is not recognize let's make black as canvas does - source = [0, 0, 0, 1]; - } - if (source) { - this.setSource(source); - } - }, - - /** - * Adapted from https://github.com/mjijackson - * @private - * @param {Number} r Red color value - * @param {Number} g Green color value - * @param {Number} b Blue color value - * @return {Array} Hsl color - */ - _rgbToHsl: function(r, g, b) { - r /= 255; g /= 255; b /= 255; - - var h, s, l, - max = arrayMax([r, g, b]), - min = arrayMin([r, g, b]); - - l = (max + min) / 2; - - if (max === min) { - h = s = 0; // achromatic - } - else { - var d = max - min; - s = l > 0.5 ? d / (2 - max - min) : d / (max + min); - switch (max) { - case r: - h = (g - b) / d + (g < b ? 6 : 0); - break; - case g: - h = (b - r) / d + 2; - break; - case b: - h = (r - g) / d + 4; - break; - } - h /= 6; - } + let h, s, l, + maxValue = max([r, g, b]), + minValue = min([r, g, b]); - return [ - Math.round(h * 360), - Math.round(s * 100), - Math.round(l * 100) - ]; - }, - - /** - * Returns source of this color (where source is an array representation; ex: [200, 200, 100, 1]) - * @return {Array} - */ - getSource: function() { - return this._source; - }, - - /** - * Sets source of this color (where source is an array representation; ex: [200, 200, 100, 1]) - * @param {Array} source - */ - setSource: function(source) { - this._source = source; - }, - - /** - * Returns color representation in RGB format - * @return {String} ex: rgb(0-255,0-255,0-255) - */ - toRgb: function() { - var source = this.getSource(); - return 'rgb(' + source[0] + ',' + source[1] + ',' + source[2] + ')'; - }, - - /** - * Returns color representation in RGBA format - * @return {String} ex: rgba(0-255,0-255,0-255,0-1) - */ - toRgba: function() { - var source = this.getSource(); - return 'rgba(' + source[0] + ',' + source[1] + ',' + source[2] + ',' + source[3] + ')'; - }, - - /** - * Returns color representation in HSL format - * @return {String} ex: hsl(0-360,0%-100%,0%-100%) - */ - toHsl: function() { - var source = this.getSource(), - hsl = this._rgbToHsl(source[0], source[1], source[2]); - - return 'hsl(' + hsl[0] + ',' + hsl[1] + '%,' + hsl[2] + '%)'; - }, - - /** - * Returns color representation in HSLA format - * @return {String} ex: hsla(0-360,0%-100%,0%-100%,0-1) - */ - toHsla: function() { - var source = this.getSource(), - hsl = this._rgbToHsl(source[0], source[1], source[2]); - - return 'hsla(' + hsl[0] + ',' + hsl[1] + '%,' + hsl[2] + '%,' + source[3] + ')'; - }, - - /** - * Returns color representation in HEX format - * @return {String} ex: FF5555 - */ - toHex: function() { - var source = this.getSource(), r, g, b; - - r = source[0].toString(16); - r = (r.length === 1) ? ('0' + r) : r; - - g = source[1].toString(16); - g = (g.length === 1) ? ('0' + g) : g; - - b = source[2].toString(16); - b = (b.length === 1) ? ('0' + b) : b; - - return r.toUpperCase() + g.toUpperCase() + b.toUpperCase(); - }, - - /** - * Returns color representation in HEXA format - * @return {String} ex: FF5555CC - */ - toHexa: function() { - var source = this.getSource(), a; - - a = Math.round(source[3] * 255); - a = a.toString(16); - a = (a.length === 1) ? ('0' + a) : a; - - return this.toHex() + a.toUpperCase(); - }, - - /** - * Gets value of alpha channel for this color - * @return {Number} 0-1 - */ - getAlpha: function() { - return this.getSource()[3]; - }, - - /** - * Sets value of alpha channel for this color - * @param {Number} alpha Alpha value 0-1 - * @return {Color} thisArg - */ - setAlpha: function(alpha) { - var source = this.getSource(); - source[3] = alpha; - this.setSource(source); - return this; - }, - - /** - * Transforms color to its grayscale representation - * @return {Color} thisArg - */ - toGrayscale: function() { - var source = this.getSource(), - average = parseInt((source[0] * 0.3 + source[1] * 0.59 + source[2] * 0.11).toFixed(0), 10), - currentAlpha = source[3]; - this.setSource([average, average, average, currentAlpha]); - return this; - }, - - /** - * Transforms color to its black and white representation - * @param {Number} threshold - * @return {Color} thisArg - */ - toBlackWhite: function(threshold) { - var source = this.getSource(), - average = (source[0] * 0.3 + source[1] * 0.59 + source[2] * 0.11).toFixed(0), - currentAlpha = source[3]; - - threshold = threshold || 127; - - average = (Number(average) < Number(threshold)) ? 0 : 255; - this.setSource([average, average, average, currentAlpha]); - return this; - }, - - /** - * Overlays color with another color - * @param {String|Color} otherColor - * @return {Color} thisArg - */ - overlayWith: function(otherColor) { - if (!(otherColor instanceof Color)) { - otherColor = new Color(otherColor); + l = (maxValue + minValue) / 2; + + if (maxValue === minValue) { + h = s = 0; // achromatic + } + else { + const d = maxValue - minValue; + s = l > 0.5 ? d / (2 - maxValue - minValue) : d / (maxValue + minValue); + switch (maxValue) { + case r: + h = (g - b) / d + (g < b ? 6 : 0); + break; + case g: + h = (b - r) / d + 2; + break; + case b: + h = (r - g) / d + 4; + break; } + h /= 6; + } - var result = [], - alpha = this.getAlpha(), - otherAlpha = 0.5, - source = this.getSource(), - otherSource = otherColor.getSource(), i; + return [ + Math.round(h * 360), + Math.round(s * 100), + Math.round(l * 100) + ]; + } - for (i = 0; i < 3; i++) { - result.push(Math.round((source[i] * (1 - otherAlpha)) + (otherSource[i] * otherAlpha))); - } + /** + * Returns source of this color (where source is an array representation; ex: [200, 200, 100, 1]) + * @return {Array} + */ + getSource() { + return this._source; + } - result[3] = alpha; - this.setSource(result); - return this; - } - }; + /** + * Sets source of this color (where source is an array representation; ex: [200, 200, 100, 1]) + * @param {Array} source + */ + setSource(source) { + this._source = source; + } /** - * Regex matching color in RGB or RGBA formats (ex: rgb(0, 0, 0), rgba(255, 100, 10, 0.5), rgba( 255 , 100 , 10 , 0.5 ), rgb(1,1,1), rgba(100%, 60%, 10%, 0.5)) - * @static - * @field - * @memberOf Color + * Returns color representation in RGB format + * @return {String} ex: rgb(0-255,0-255,0-255) */ - // eslint-disable-next-line max-len - Color.reRGBa = /^rgba?\(\s*(\d{1,3}(?:\.\d+)?%?)\s*,\s*(\d{1,3}(?:\.\d+)?%?)\s*,\s*(\d{1,3}(?:\.\d+)?%?)\s*(?:\s*,\s*((?:\d*\.?\d+)?)\s*)?\)$/i; + toRgb() { + const source = this.getSource(); + return 'rgb(' + source[0] + ',' + source[1] + ',' + source[2] + ')'; + } /** - * Regex matching color in HSL or HSLA formats (ex: hsl(200, 80%, 10%), hsla(300, 50%, 80%, 0.5), hsla( 300 , 50% , 80% , 0.5 )) - * @static - * @field - * @memberOf Color + * Returns color representation in RGBA format + * @return {String} ex: rgba(0-255,0-255,0-255,0-1) */ - Color.reHSLa = /^hsla?\(\s*(\d{1,3})\s*,\s*(\d{1,3}%)\s*,\s*(\d{1,3}%)\s*(?:\s*,\s*(\d+(?:\.\d+)?)\s*)?\)$/i; + toRgba() { + const source = this.getSource(); + return 'rgba(' + source[0] + ',' + source[1] + ',' + source[2] + ',' + source[3] + ')'; + } /** - * Regex matching color in HEX format (ex: #FF5544CC, #FF5555, 010155, aff) - * @static - * @field - * @memberOf Color + * Returns color representation in HSL format + * @return {String} ex: hsl(0-360,0%-100%,0%-100%) */ - Color.reHex = /^#?([0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{4}|[0-9a-f]{3})$/i; + toHsl() { + const source = this.getSource(), + hsl = this._rgbToHsl(source[0], source[1], source[2]); + + return 'hsl(' + hsl[0] + ',' + hsl[1] + '%,' + hsl[2] + '%)'; + } /** - * Map of the 148 color names with HEX code - * @static - * @field - * @memberOf Color - * @see: https://www.w3.org/TR/css3-color/#svg-color + * Returns color representation in HSLA format + * @return {String} ex: hsla(0-360,0%-100%,0%-100%,0-1) */ - Color.colorNameMap = { - aliceblue: '#F0F8FF', - antiquewhite: '#FAEBD7', - aqua: '#00FFFF', - aquamarine: '#7FFFD4', - azure: '#F0FFFF', - beige: '#F5F5DC', - bisque: '#FFE4C4', - black: '#000000', - blanchedalmond: '#FFEBCD', - blue: '#0000FF', - blueviolet: '#8A2BE2', - brown: '#A52A2A', - burlywood: '#DEB887', - cadetblue: '#5F9EA0', - chartreuse: '#7FFF00', - chocolate: '#D2691E', - coral: '#FF7F50', - cornflowerblue: '#6495ED', - cornsilk: '#FFF8DC', - crimson: '#DC143C', - cyan: '#00FFFF', - darkblue: '#00008B', - darkcyan: '#008B8B', - darkgoldenrod: '#B8860B', - darkgray: '#A9A9A9', - darkgrey: '#A9A9A9', - darkgreen: '#006400', - darkkhaki: '#BDB76B', - darkmagenta: '#8B008B', - darkolivegreen: '#556B2F', - darkorange: '#FF8C00', - darkorchid: '#9932CC', - darkred: '#8B0000', - darksalmon: '#E9967A', - darkseagreen: '#8FBC8F', - darkslateblue: '#483D8B', - darkslategray: '#2F4F4F', - darkslategrey: '#2F4F4F', - darkturquoise: '#00CED1', - darkviolet: '#9400D3', - deeppink: '#FF1493', - deepskyblue: '#00BFFF', - dimgray: '#696969', - dimgrey: '#696969', - dodgerblue: '#1E90FF', - firebrick: '#B22222', - floralwhite: '#FFFAF0', - forestgreen: '#228B22', - fuchsia: '#FF00FF', - gainsboro: '#DCDCDC', - ghostwhite: '#F8F8FF', - gold: '#FFD700', - goldenrod: '#DAA520', - gray: '#808080', - grey: '#808080', - green: '#008000', - greenyellow: '#ADFF2F', - honeydew: '#F0FFF0', - hotpink: '#FF69B4', - indianred: '#CD5C5C', - indigo: '#4B0082', - ivory: '#FFFFF0', - khaki: '#F0E68C', - lavender: '#E6E6FA', - lavenderblush: '#FFF0F5', - lawngreen: '#7CFC00', - lemonchiffon: '#FFFACD', - lightblue: '#ADD8E6', - lightcoral: '#F08080', - lightcyan: '#E0FFFF', - lightgoldenrodyellow: '#FAFAD2', - lightgray: '#D3D3D3', - lightgrey: '#D3D3D3', - lightgreen: '#90EE90', - lightpink: '#FFB6C1', - lightsalmon: '#FFA07A', - lightseagreen: '#20B2AA', - lightskyblue: '#87CEFA', - lightslategray: '#778899', - lightslategrey: '#778899', - lightsteelblue: '#B0C4DE', - lightyellow: '#FFFFE0', - lime: '#00FF00', - limegreen: '#32CD32', - linen: '#FAF0E6', - magenta: '#FF00FF', - maroon: '#800000', - mediumaquamarine: '#66CDAA', - mediumblue: '#0000CD', - mediumorchid: '#BA55D3', - mediumpurple: '#9370DB', - mediumseagreen: '#3CB371', - mediumslateblue: '#7B68EE', - mediumspringgreen: '#00FA9A', - mediumturquoise: '#48D1CC', - mediumvioletred: '#C71585', - midnightblue: '#191970', - mintcream: '#F5FFFA', - mistyrose: '#FFE4E1', - moccasin: '#FFE4B5', - navajowhite: '#FFDEAD', - navy: '#000080', - oldlace: '#FDF5E6', - olive: '#808000', - olivedrab: '#6B8E23', - orange: '#FFA500', - orangered: '#FF4500', - orchid: '#DA70D6', - palegoldenrod: '#EEE8AA', - palegreen: '#98FB98', - paleturquoise: '#AFEEEE', - palevioletred: '#DB7093', - papayawhip: '#FFEFD5', - peachpuff: '#FFDAB9', - peru: '#CD853F', - pink: '#FFC0CB', - plum: '#DDA0DD', - powderblue: '#B0E0E6', - purple: '#800080', - rebeccapurple: '#663399', - red: '#FF0000', - rosybrown: '#BC8F8F', - royalblue: '#4169E1', - saddlebrown: '#8B4513', - salmon: '#FA8072', - sandybrown: '#F4A460', - seagreen: '#2E8B57', - seashell: '#FFF5EE', - sienna: '#A0522D', - silver: '#C0C0C0', - skyblue: '#87CEEB', - slateblue: '#6A5ACD', - slategray: '#708090', - slategrey: '#708090', - snow: '#FFFAFA', - springgreen: '#00FF7F', - steelblue: '#4682B4', - tan: '#D2B48C', - teal: '#008080', - thistle: '#D8BFD8', - tomato: '#FF6347', - turquoise: '#40E0D0', - violet: '#EE82EE', - wheat: '#F5DEB3', - white: '#FFFFFF', - whitesmoke: '#F5F5F5', - yellow: '#FFFF00', - yellowgreen: '#9ACD32' - }; + toHsla() { + const source = this.getSource(), + hsl = this._rgbToHsl(source[0], source[1], source[2]); + + return 'hsla(' + hsl[0] + ',' + hsl[1] + '%,' + hsl[2] + '%,' + source[3] + ')'; + } /** - * @private - * @param {Number} p - * @param {Number} q - * @param {Number} t - * @return {Number} + * Returns color representation in HEX format + * @return {String} ex: FF5555 */ - function hue2rgb(p, q, t) { - if (t < 0) { - t += 1; - } - if (t > 1) { - t -= 1; - } - if (t < 1 / 6) { - return p + (q - p) * 6 * t; - } - if (t < 1 / 2) { - return q; + toHex() { + let source = this.getSource(), r, g, b; + + r = source[0].toString(16); + r = (r.length === 1) ? ('0' + r) : r; + + g = source[1].toString(16); + g = (g.length === 1) ? ('0' + g) : g; + + b = source[2].toString(16); + b = (b.length === 1) ? ('0' + b) : b; + + return r.toUpperCase() + g.toUpperCase() + b.toUpperCase(); + } + + /** + * Returns color representation in HEXA format + * @return {String} ex: FF5555CC + */ + toHexa() { + let source = this.getSource(), a; + + a = Math.round(source[3] * 255); + a = a.toString(16); + a = (a.length === 1) ? ('0' + a) : a; + + return this.toHex() + a.toUpperCase(); + } + + /** + * Gets value of alpha channel for this color + * @return {Number} 0-1 + */ + getAlpha() { + return this.getSource()[3]; + } + + /** + * Sets value of alpha channel for this color + * @param {Number} alpha Alpha value 0-1 + * @return {Color} thisArg + */ + setAlpha(alpha) { + const source = this.getSource(); + source[3] = alpha; + this.setSource(source); + return this; + } + + /** + * Transforms color to its grayscale representation + * @return {Color} thisArg + */ + toGrayscale() { + const source = this.getSource(), + average = parseInt((source[0] * 0.3 + source[1] * 0.59 + source[2] * 0.11).toFixed(0), 10), + currentAlpha = source[3]; + this.setSource([average, average, average, currentAlpha]); + return this; + } + + /** + * Transforms color to its black and white representation + * @param {Number} threshold + * @return {Color} thisArg + */ + toBlackWhite(threshold) { + let source = this.getSource(), + average = (source[0] * 0.3 + source[1] * 0.59 + source[2] * 0.11).toFixed(0), + currentAlpha = source[3]; + + threshold = threshold || 127; + + average = (Number(average) < Number(threshold)) ? 0 : 255; + this.setSource([average, average, average, currentAlpha]); + return this; + } + + /** + * Overlays color with another color + * @param {String|Color} otherColor + * @return {Color} thisArg + */ + overlayWith(otherColor) { + if (!(otherColor instanceof Color)) { + otherColor = new Color(otherColor); } - if (t < 2 / 3) { - return p + (q - p) * (2 / 3 - t) * 6; + + let result = [], + alpha = this.getAlpha(), + otherAlpha = 0.5, + source = this.getSource(), + otherSource = otherColor.getSource(), i; + + for (i = 0; i < 3; i++) { + result.push(Math.round((source[i] * (1 - otherAlpha)) + (otherSource[i] * otherAlpha))); } - return p; + + result[3] = alpha; + this.setSource(result); + return this; } + + /** * Returns new color object, when given a color in RGB format * @memberOf Color * @param {String} color Color value ex: rgb(0-255,0-255,0-255) * @return {Color} */ - Color.fromRgb = function(color) { + static fromRgb(color) { return Color.fromSource(Color.sourceFromRgb(color)); - }; + } /** * Returns array representation (ex: [100, 100, 200, 1]) of a color that's in RGB or RGBA format @@ -491,12 +283,12 @@ import { max as arrayMax, min as arrayMin } from "../util/index"; * @param {String} color Color value ex: rgb(0-255,0-255,0-255), rgb(0%-100%,0%-100%,0%-100%) * @return {Array} source */ - Color.sourceFromRgb = function(color) { - var match = color.match(Color.reRGBa); + static sourceFromRgb(color) { + const match = color.match(reRGBa); if (match) { - var r = parseInt(match[1], 10) / (/%$/.test(match[1]) ? 100 : 1) * (/%$/.test(match[1]) ? 255 : 1), - g = parseInt(match[2], 10) / (/%$/.test(match[2]) ? 100 : 1) * (/%$/.test(match[2]) ? 255 : 1), - b = parseInt(match[3], 10) / (/%$/.test(match[3]) ? 100 : 1) * (/%$/.test(match[3]) ? 255 : 1); + const r = parseInt(match[1], 10) / (/%$/.test(match[1]) ? 100 : 1) * (/%$/.test(match[1]) ? 255 : 1), + g = parseInt(match[2], 10) / (/%$/.test(match[2]) ? 100 : 1) * (/%$/.test(match[2]) ? 255 : 1), + b = parseInt(match[3], 10) / (/%$/.test(match[3]) ? 100 : 1) * (/%$/.test(match[3]) ? 255 : 1); return [ parseInt(r, 10), @@ -505,7 +297,7 @@ import { max as arrayMax, min as arrayMin } from "../util/index"; match[4] ? parseFloat(match[4]) : 1 ]; } - }; + } /** * Returns new color object, when given a color in RGBA format @@ -515,7 +307,7 @@ import { max as arrayMax, min as arrayMin } from "../util/index"; * @param {String} color * @return {Color} */ - Color.fromRgba = Color.fromRgb; + static fromRgba = Color.fromRgb /** * Returns new color object, when given a color in HSL format @@ -523,9 +315,9 @@ import { max as arrayMax, min as arrayMin } from "../util/index"; * @memberOf Color * @return {Color} */ - Color.fromHsl = function(color) { + static fromHsl(color) { return Color.fromSource(Color.sourceFromHsl(color)); - }; + } /** * Returns array representation (ex: [100, 100, 200, 1]) of a color that's in HSL or HSLA format. @@ -535,23 +327,23 @@ import { max as arrayMax, min as arrayMin } from "../util/index"; * @return {Array} source * @see http://http://www.w3.org/TR/css3-color/#hsl-color */ - Color.sourceFromHsl = function(color) { - var match = color.match(Color.reHSLa); + static sourceFromHsl(color) { + const match = color.match(reHSLa); if (!match) { return; } - var h = (((parseFloat(match[1]) % 360) + 360) % 360) / 360, - s = parseFloat(match[2]) / (/%$/.test(match[2]) ? 100 : 1), - l = parseFloat(match[3]) / (/%$/.test(match[3]) ? 100 : 1), - r, g, b; + let h = (((parseFloat(match[1]) % 360) + 360) % 360) / 360, + s = parseFloat(match[2]) / (/%$/.test(match[2]) ? 100 : 1), + l = parseFloat(match[3]) / (/%$/.test(match[3]) ? 100 : 1), + r, g, b; if (s === 0) { r = g = b = l; } else { - var q = l <= 0.5 ? l * (s + 1) : l + s - l * s, - p = l * 2 - q; + const q = l <= 0.5 ? l * (s + 1) : l + s - l * s, + p = l * 2 - q; r = hue2rgb(p, q, h + 1 / 3); g = hue2rgb(p, q, h); @@ -564,7 +356,7 @@ import { max as arrayMax, min as arrayMin } from "../util/index"; Math.round(b * 255), match[4] ? parseFloat(match[4]) : 1 ]; - }; + } /** * Returns new color object, when given a color in HSLA format @@ -574,7 +366,7 @@ import { max as arrayMax, min as arrayMin } from "../util/index"; * @param {String} color * @return {Color} */ - Color.fromHsla = Color.fromHsl; + static fromHsla = Color.fromHsl /** * Returns new color object, when given a color in HEX format @@ -583,9 +375,9 @@ import { max as arrayMax, min as arrayMin } from "../util/index"; * @param {String} color Color value ex: FF5555 * @return {Color} */ - Color.fromHex = function(color) { + static fromHex(color) { return Color.fromSource(Color.sourceFromHex(color)); - }; + } /** * Returns array representation (ex: [100, 100, 200, 1]) of a color that's in HEX format @@ -594,15 +386,15 @@ import { max as arrayMax, min as arrayMin } from "../util/index"; * @param {String} color ex: FF5555 or FF5544CC (RGBa) * @return {Array} source */ - Color.sourceFromHex = function(color) { - if (color.match(Color.reHex)) { - var value = color.slice(color.indexOf('#') + 1), - isShortNotation = (value.length === 3 || value.length === 4), - isRGBa = (value.length === 8 || value.length === 4), - r = isShortNotation ? (value.charAt(0) + value.charAt(0)) : value.substring(0, 2), - g = isShortNotation ? (value.charAt(1) + value.charAt(1)) : value.substring(2, 4), - b = isShortNotation ? (value.charAt(2) + value.charAt(2)) : value.substring(4, 6), - a = isRGBa ? (isShortNotation ? (value.charAt(3) + value.charAt(3)) : value.substring(6, 8)) : 'FF'; + static sourceFromHex(color) { + if (color.match(reHex)) { + const value = color.slice(color.indexOf('#') + 1), + isShortNotation = (value.length === 3 || value.length === 4), + isRGBa = (value.length === 8 || value.length === 4), + r = isShortNotation ? (value.charAt(0) + value.charAt(0)) : value.substring(0, 2), + g = isShortNotation ? (value.charAt(1) + value.charAt(1)) : value.substring(2, 4), + b = isShortNotation ? (value.charAt(2) + value.charAt(2)) : value.substring(4, 6), + a = isRGBa ? (isShortNotation ? (value.charAt(3) + value.charAt(3)) : value.substring(6, 8)) : 'FF'; return [ parseInt(r, 16), @@ -611,7 +403,7 @@ import { max as arrayMax, min as arrayMin } from "../util/index"; parseFloat((parseInt(a, 16) / 255).toFixed(2)) ]; } - }; + } /** * Returns new color object, when given color in array representation (ex: [200, 100, 100, 0.5]) @@ -620,11 +412,14 @@ import { max as arrayMax, min as arrayMin } from "../util/index"; * @param {Array} source * @return {Color} */ - Color.fromSource = function(source) { - var oColor = new Color(); + static fromSource(source) { + const oColor = new Color(); oColor.setSource(source); return oColor; - }; + } + + +} + -export { Color }; diff --git a/src/shapes/polyline.class.ts b/src/shapes/polyline.class.ts index ae701d628c5..545ac4124c5 100644 --- a/src/shapes/polyline.class.ts +++ b/src/shapes/polyline.class.ts @@ -78,15 +78,23 @@ _setPositionDimensions: function(options) { options || (options = {}); var calcDim = this._calcDimensions(options), correctLeftTop, - correctSize = this.exactBoundingBox ? this.strokeWidth : 0; - this.width = calcDim.width - correctSize; - this.height = calcDim.height - correctSize; + correctSizeX = this.exactBoundingBox + ? this.strokeUniform + ? this.strokeWidth / this.scaleX + : this.strokeWidth + : 0, + correctSizeY = this.exactBoundingBox + ? this.strokeUniform + ? this.strokeWidth / this.scaleY + : this.strokeWidth + : 0; + this.width = calcDim.width - correctSizeX; + this.height = calcDim.height - correctSizeY; if (!options.fromSVG) { correctLeftTop = this.translateToGivenOrigin( { - // this looks bad, but is one way to keep it optional for now. - x: calcDim.left - this.strokeWidth / 2 + correctSize / 2, - y: calcDim.top - this.strokeWidth / 2 + correctSize / 2 + x: this.left, + y: this.top }, 'left', 'top', @@ -101,8 +109,8 @@ this.top = options.fromSVG ? calcDim.top : correctLeftTop.y; } this.pathOffset = { - x: calcDim.left + this.width / 2 + correctSize / 2, - y: calcDim.top + this.height / 2 + correctSize / 2 + x: calcDim.left + this.width / 2 + correctSizeX / 2, + y: calcDim.top + this.height / 2 + correctSizeY / 2 }; }, @@ -118,7 +126,7 @@ */ _calcDimensions: function() { - var points = this.exactBoundingBox ? this._projectStrokeOnPoints() : this.points, + var points = this.exactBoundingBox ? this._projectStrokeOnPoints().map(elem => elem.projectedPoint) : this.points, minX = min(points, 'x') || 0, minY = min(points, 'y') || 0, maxX = max(points, 'x') || 0, @@ -134,6 +142,16 @@ }; }, + /** + * After setting scale, recalculates dimensions when the stroke is uniform and stroke line join is bevel or round + * @private + */ + _set(key, value) { + var output = this.callSuper('_set', key, value); + (key === 'scaleX' || key === 'scaleY') && this.strokeUniform && this.strokeLineJoin !== 'round' && this._setPositionDimensions(); + return output; + }, + /** * Returns object representation of an instance * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output diff --git a/src/util/hypot.ts b/src/util/hypot.ts new file mode 100644 index 00000000000..ab1d44156a9 --- /dev/null +++ b/src/util/hypot.ts @@ -0,0 +1,17 @@ + +/** + * Returns the square root of the sum of squares of its arguments\ + * Chrome implements `Math#hypot` with a calculation that affects percision so we hard code it as a util + * @see https://stackoverflow.com/questions/62931950/different-results-of-math-hypot-on-chrome-and-firefox + * @static + * @memberOf fabric.util + * @param {...number} + * @returns {number} + */ +export function hypot(...values: number[]) { + let sumOfSquares = 0; + for (let i = 0; i < values.length; i++) { + sumOfSquares += values[i] * values[i]; + } + return Math.sqrt(sumOfSquares); +} \ No newline at end of file diff --git a/src/util/index.ts b/src/util/index.ts index 17ba33e14c0..eabbfe142a5 100644 --- a/src/util/index.ts +++ b/src/util/index.ts @@ -1,4 +1,5 @@ //@ts-nocheck +export * from './hypot'; export * from './cos'; import { fabric } from '../../HEADER' import './animate'; // optional animation diff --git a/src/util/misc.ts b/src/util/misc.ts index c307af0f5cb..6d7ee0e2b2f 100644 --- a/src/util/misc.ts +++ b/src/util/misc.ts @@ -1,7 +1,8 @@ //@ts-nocheck +import { hypot } from './hypot'; +import { cos } from './cos'; import { Point } from '../point.class'; -import { cos } from './cos'; (function(global) { var fabric = global.fabric, sqrt = Math.sqrt, atan2 = Math.atan2, @@ -39,6 +40,8 @@ import { cos } from './cos'; return Math.sin(angle); }, + hypot, + /** * Removes value from an array. * Presence of value (and its position in an array) is determined via `Array.prototype.indexOf` @@ -139,25 +142,28 @@ import { cos } from './cos'; }, /** - * Calculates angle between 2 vectors using dot product + * Calculates angle between 2 vectors * @static * @memberOf fabric.util * @param {Point} a * @param {Point} b - * @returns the angle in radian between the vectors + * @returns the angle in radians from `a` to `b` */ calcAngleBetweenVectors: function (a, b) { - return Math.acos((a.x * b.x + a.y * b.y) / (Math.hypot(a.x, a.y) * Math.hypot(b.x, b.y))); + var dot = a.x * b.x + a.y * b.y, + det = a.x * b.y - a.y * b.x; + + return Math.atan2(det, dot); }, /** * @static * @memberOf fabric.util * @param {Point} v - * @returns {Point} vector representing the unit vector of pointing to the direction of `v` + * @returns {Point} vector representing the unit vector pointing to the direction of `v` */ getHatVector: function (v) { - return new Point(v.x, v.y).scalarMultiply(1 / Math.hypot(v.x, v.y)); + return new Point(v.x, v.y).scalarMultiply(1 / fabric.util.hypot(v.x, v.y)); }, /** @@ -171,21 +177,37 @@ import { cos } from './cos'; getBisector: function (A, B, C) { var AB = fabric.util.createVector(A, B), AC = fabric.util.createVector(A, C); var alpha = fabric.util.calcAngleBetweenVectors(AB, AC); - // check if alpha is relative to AB->BC - var ro = fabric.util.calcAngleBetweenVectors(fabric.util.rotateVector(AB, alpha), AC); - var phi = alpha * (ro === 0 ? 1 : -1) / 2; return { - vector: fabric.util.getHatVector(fabric.util.rotateVector(AB, phi)), - angle: alpha + vector: fabric.util.getHatVector(fabric.util.rotateVector(AB, alpha / 2)), + angle: Math.abs(alpha) }; }, /** - * Project stroke width on points returning 2 projections for each point as follows: - * - `miter`: 2 points corresponding to the outer boundary and the inner boundary of stroke. - * - `bevel`: 2 points corresponding to the bevel boundaries, tangent to the bisector. + * @static + * @memberOf fabric.util + * @param {Point} vector + * @param {Boolean} [counterClockwise] the direction of the orthogonal vector, defaults to `true` + * @returns {Point} the unit orthogonal vector + */ + getOrthogonalUnitVector: function (vector, counterClockwise = true) { + return fabric.util.getHatVector( + new Point( + counterClockwise ? -vector.y : vector.y, + counterClockwise ? vector.x : -vector.x + ) + ); + }, + + /** + * Project stroke width on points returning projections for each point as follows: + * - `miter`: 1 point corresponding to the outer boundary and the inner boundary of stroke. + * - `bevel`: 2 points corresponding to the bevel possible boundaries, orthogonal to the stroke. * - `round`: same as `bevel` * Used to calculate object's bounding box + * + * @see https://github.com/fabricjs/fabric.js/pull/8083 + * * @static * @memberOf fabric.util * @param {Point[]} points @@ -197,56 +219,156 @@ import { cos } from './cos'; * @param {number} options.scaleX * @param {number} options.scaleY * @param {boolean} [openPath] whether the shape is open or not, affects the calculations of the first and last points - * @returns {Point[]} array of size 2n/4n of all suspected points + * @returns {Object[]} array of size n (for miter stroke) or 2n (for bevel or round) of all suspected points. Each element is an object with projectedPoint, originPoint and bisector. */ projectStrokeOnPoints: function (points, options, openPath) { - var coords = [], s = options.strokeWidth / 2, + + if (points.length <= 1) { return []; } + + var coords = [], + s = options.strokeWidth / 2, + scale = new Point(options.scaleX, options.scaleY), strokeUniformScalar = options.strokeUniform ? - new Point(1 / options.scaleX, 1 / options.scaleY) : new Point(1, 1), - getStrokeHatVector = function (v) { - var scalar = s / (Math.hypot(v.x, v.y)); - return new Point(v.x * scalar * strokeUniformScalar.x, v.y * scalar * strokeUniformScalar.y); - }; - if (points.length <= 1) {return coords;} + new Point(1 / options.scaleX, 1 / options.scaleY) : + new Point(1, 1); + + function scaleHatVector(hatVector, scalar) { + return hatVector.multiply(strokeUniformScalar).scalarMultiply(scalar); + } + points.forEach(function (p, index) { var A = new Point(p.x, p.y), B, C; if (index === 0) { C = points[index + 1]; - B = openPath ? getStrokeHatVector(fabric.util.createVector(C, A)).add(A) : points[points.length - 1]; + B = openPath ? A : points[points.length - 1]; } else if (index === points.length - 1) { B = points[index - 1]; - C = openPath ? getStrokeHatVector(fabric.util.createVector(B, A)).add(A) : points[0]; + C = openPath ? A : points[0]; } else { B = points[index - 1]; C = points[index + 1]; } - var bisector = fabric.util.getBisector(A, B, C), - bisectorVector = bisector.vector, + // safeguard in case `points` are not `Point` + B = new Point(B.x, B.y); + C = new Point(C.x, C.y); + + if (openPath && (index === 0 || index === points.length - 1)) { + var D = index === 0 ? C : B, + scaledA = A.multiply(scale), + scaledD = D.multiply(scale); + + var vector = fabric.util.createVector( + options.strokeUniform ? scaledA : A, + options.strokeUniform ? scaledD : D + ), + hatOrthogonalVector = fabric.util.getOrthogonalUnitVector(vector), + orthogonalVector = scaleHatVector(hatOrthogonalVector, s); + + var proj1 = A.add(orthogonalVector), + proj2 = A.subtract(orthogonalVector); + + [proj1, proj2].forEach(proj => { + coords.push({ + 'projectedPoint': proj, + 'originPoint': A, + 'bisector': bisector + }); + }); + + return; + } + + var bisector, + scaledA, + scaledB, + scaledC; + if (options.strokeUniform) { + scaledA = A.multiply(scale); + scaledB = B.multiply(scale); + scaledC = C.multiply(scale); + bisector = fabric.util.getBisector(scaledA, scaledB, scaledC); + } + else { + bisector = fabric.util.getBisector(A, B, C); + } + + var bisectorVector = bisector.vector, alpha = bisector.angle, scalar, miterVector; + if (options.strokeLineJoin === 'miter') { scalar = -s / Math.sin(alpha / 2); - miterVector = new Point( - bisectorVector.x * scalar * strokeUniformScalar.x, - bisectorVector.y * scalar * strokeUniformScalar.y - ); - if (Math.hypot(miterVector.x, miterVector.y) / s <= options.strokeMiterLimit) { - coords.push(A.add(miterVector)); - coords.push(A.subtract(miterVector)); + miterVector = scaleHatVector(bisectorVector, scalar); + + var strokeMiterLimit; + if (options.strokeUniform) { + var miterLimitVector = scaleHatVector(bisectorVector, options.strokeMiterLimit * s); + strokeMiterLimit = fabric.util.hypot(miterLimitVector.x, miterLimitVector.y) / s; + } + else { + strokeMiterLimit = options.strokeMiterLimit; + } + + if (fabric.util.hypot(miterVector.x, miterVector.y) / s <= strokeMiterLimit) { + var proj1 = A.add(miterVector); + + coords.push({ + 'projectedPoint': proj1, + 'originPoint': A, + 'bisector': bisector + }); return; } } - scalar = -s * Math.SQRT2; - miterVector = new Point( - bisectorVector.x * scalar * strokeUniformScalar.x, - bisectorVector.y * scalar * strokeUniformScalar.y - ); - coords.push(A.add(miterVector)); - coords.push(A.subtract(miterVector)); + if (options.strokeLineJoin === 'round') { + + var correctSideX = Math.abs(Math.atan2(bisectorVector.y, bisectorVector.x)) >= PiBy2 ? 1 : -1, + correctSideY = Math.abs(Math.atan2(bisectorVector.x, bisectorVector.y)) >= PiBy2 ? 1 : -1, + radiusOnAxisX = new Point(s * strokeUniformScalar.x * correctSideX, 0), + radiusOnAxisY = new Point(0, s * strokeUniformScalar.y * correctSideY); + + var proj1 = A.add(radiusOnAxisX), + proj2 = A.add(radiusOnAxisY); + + [proj1, proj2].forEach(proj => { + coords.push({ + 'projectedPoint': proj, + 'originPoint': A, + 'bisector': bisector + }); + }); + } + else { + // bevel or miter greater than stroke miter limit + + var AB = fabric.util.createVector( + options.strokeUniform ? scaledA : A, + options.strokeUniform ? scaledB : B + ), + AC = fabric.util.createVector( + options.strokeUniform ? scaledA : A, + options.strokeUniform ? scaledC : C + ); + + [AB, AC].forEach(function(vector) { + var hatOrthogonal = fabric.util.getOrthogonalUnitVector(vector), + correctSide = Math.abs(fabric.util.calcAngleBetweenVectors(hatOrthogonal, bisectorVector)) >= PiBy2 ? 1 : -1, + orthogonal = scaleHatVector(hatOrthogonal, s * correctSide); + + var proj1 = A.add(orthogonal); + + coords.push({ + 'projectedPoint': proj1, + 'originPoint': A, + 'bisector': bisector + }); + }); + } }); + return coords; }, diff --git a/test/unit/polygon.js b/test/unit/polygon.js index 7dc3b224a10..19d656b191d 100644 --- a/test/unit/polygon.js +++ b/test/unit/polygon.js @@ -78,11 +78,19 @@ var polygon = new fabric.Polygon([{ x: 10, y: 10 }, { x: 20, y: 10 }, { x: 20, y: 100 }], { exactBoundingBox: true, strokeWidth: 60, + stroke: 'blue' }); var dimensions = polygon._getNonTransformedDimensions(); assert.equal(Math.round(dimensions.x), 74); - assert.equal(Math.round(dimensions.y), 162); + assert.equal(Math.round(dimensions.y), 123); + + polygon.set('strokeMiterLimit', 999); + polygon._setPositionDimensions(); + dimensions = polygon._getNonTransformedDimensions(); + assert.equal(Math.round(dimensions.x), 74); + // TODO this is WRONG + assert.equal(Math.round(dimensions.y), 1083); }); QUnit.test('complexity', function(assert) { diff --git a/test/unit/util.js b/test/unit/util.js index a228112dc4c..7ab0515f6d2 100644 --- a/test/unit/util.js +++ b/test/unit/util.js @@ -1110,6 +1110,12 @@ assert.equal(fabric.util.cos(3 * Math.PI / 2), 0,' cos 270 correct'); }); + QUnit.test('fabric.util.hypot', function (assert) { + assert.ok(typeof fabric.util.hypot === 'function'); + assert.equal(fabric.util.hypot(3, 4), 5); + assert.equal(fabric.util.hypot(3, 4, 12), 13); + }); + QUnit.test('fabric.util.getSvgAttributes', function(assert) { assert.ok(typeof fabric.util.getSvgAttributes === 'function'); assert.deepEqual(fabric.util.getSvgAttributes(''),