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(''),