UNPKG

@scidian/osui

Version:

Lightweight JavaScript UI library.

803 lines (705 loc) 34.9 kB
/** * @description Iris * @about Color library with support for RGB, RYB, HSL color models and RYB hue shifting. * @author Stephens Nunnally <@stevinz> * @license MIT - Copyright (c) 2021 Stephens Nunnally * @source https://github.com/scidian/iris */ /******************** SEE END OF FILE FOR LICENSE / ACKNOWLEDGEMENTS ********************/ // Initialization // const color = new Iris(); // ... // new Iris(); // Defaults to white, 0xffffff // new Iris(0xff0000); // Hexadecimal (0xff0000, i.e. 16711680) // // new Iris(1.0, 0.0, 0.0); // RGB Values (0.0 to 1.0) // // new Iris(255, 0, 0, 'rgb'); // RGB Values (0 to 255) // new Iris(255, 0, 0, 'ryb'); // RYB Values (0 to 255) // new Iris(360, 1.0, 0.5, 'hsl'); // HSL Values (H: 0 to 360, SL: 0.0 to 1.0) // // new Iris({ r: 1.0, g: 0.0, b: 0.0 }); // Object with RGB Properties (0.0 to 1.0) // new Iris({ r: 1.0, y: 0.0, b: 0.0 }); // Object with RYB Properties (0.0 to 1.0) // new Iris({ h: 1.0, s: 1.0, l: 0.5 }); // Object with HSL Properties (0.0 to 1.0) // // new Iris([ 1.0, 0.0, 0.0 ], offset); // RGB Array (0.0 to 1.0), optional array offset // // new Iris('#ff0000'); // Hex String (also 3 digits: #f00) // new Iris('rgb(255, 0, 0)'); // CSS Color String // new Iris('red'); // X11 Color Name // // new Iris(fromIris); // Copy from Iris Object // new Iris(fromThreeColor); // Copy from Three.js Color Object // Properties // color.r // 0.0 to 1.0 // color.g // 0.0 to 1.0 // color.b // 0.0 to 1.0 // Static // Iris.hexString(inputColorData) // Color (i.e. 0xff0000 / 16711680) to hex string, ex: '#ff0000' // Iris.randomHex(); // Returns random color as integer (i.e. 16711680) // Output // color.cssString(); // Returns string, ex: 'rgb(255, 0, 0)' // color.hex(); // Returns number, ex: 16711680 (equivalent to 0xff0000) // color.hexString(); // Returns string, ex: '#ff0000' // color.rgbString(); // Returns string, ex: '255, 0, 0' // // color.getHSL(target); // Copies HSL values into target, values from 0.0 to 1.0 // color.getRGB(target); // Copies RGB values into target, values from 0.0 to 1.0 // color.getRYB(target); // Copies RYB values into target, values from 0.0 to 1.0 // color.toArray(array); // Copies RGB values into array, values from 0.0 to 1.0 // // color.red(); // Returns red value of color, 0 to 255 // color.green(); // Returns green value of color, 0 to 255 // color.blue(); // Returns blue value of color, 0 to 255 // // color.redF(); // Returns red value of color, 0.0 to 1.0 // color.greenF(); // Returns green value of color, 0.0 to 1.0 // color.blueF(); // Returns blue value of color, 0.0 to 1.0 // // color.hue(); // Returns hue value of color, 0 to 360 // color.saturation(); // Returns saturation value of color, 0 to 1.0 // color.lightness(); // Returns lightness value of color, 0 to 1.0 // // color.hueF(); // Returns hue value of color, 0.0 to 1.0 // color.hueRYB(); // Returns RGB hue mapped to hue in the RYB, 0 to 360 /** Color library with support for RGB, RYB, HSL color models and RYB hue shifting */ class Iris { static get NAMES() { return COLOR_KEYWORDS; } constructor(r = 0xffffff, g, b, format = '') { this.isColor = true; this.isIris = true; this.type = 'Color'; this.r = 1; // 0.0 to 1.0 this.g = 1; // 0.0 to 1.0 this.b = 1; // 0.0 to 1.0 this.set(r, g, b, format); } /******************** COPY / CLONE */ copy(colorObject) { return this.set(colorObject); } clone() { return new this.constructor(this.r, this.g, this.b); } /******************** ASSIGNMENT */ set(r = 0, g, b, format = '') { // No arguments passed if (arguments.length === 0) { return this.set(0); // No valid arguments passed } else if (r === undefined || r === null || Number.isNaN(r)) { if (g || b) console.warn(`Iris: Passed some valid arguments, however 'r' was ${r}`); // nothing to do // r is Object, Hexidecimal, or String } else if (g === undefined && b === undefined) { let value = r; if (typeof value === 'number' || value === 0) { return this.setHex(value); } else if (value && isRGB(value)) { return this.setRGBF(value.r, value.g, value.b); } else if (value && isHSL(value)) { return this.setHSL(value.h * 360, value.s, value.l); } else if (value && isRYB(value)) { return this.setRYB(value.r * 255, value.y * 255, value.b * 255); } else if (Array.isArray(value) && value.length > 2) { const offset = (g != null && ! Number.isNaN(g) && g > 0) ? g : 0; return this.setRGBF(value[offset], value[offset + 1], value[offset + 2]) } else if (typeof value === 'string') { return this.setStyle(value); } // Three arguments were passed } else { switch (format) { case 'rgb': return this.setRGB(r, g, b); case 'hsl': return this.setHSL(r, g, b); case 'ryb': return this.setRYB(r, g, b); default: return this.setRGBF(r, g, b); } } return this; } setColorName(style) { const hex = COLOR_KEYWORDS[ style.toLowerCase() ]; if (hex) return this.setHex(hex); console.warn(`Iris: Unknown color ${style}`); return this; } setHex(hexColor) { hexColor = Math.floor(hexColor); if (hexColor > 0xffffff || hexColor < 0) { console.warn(`Iris: Given decimal outside of range, value was ${hexColor}`); hexColor = clamp(hexColor, 0, 0xffffff); } const r = (hexColor & 0xff0000) >> 16; const g = (hexColor & 0x00ff00) >> 8; const b = (hexColor & 0x0000ff); return this.setRGB(r, g, b); } setHSL(h, s, l) { h = keepInRange(h, 0, 360); s = clamp(s, 0, 1); l = clamp(l, 0, 1); let c = (1 - Math.abs(2 * l - 1)) * s; let x = c * (1 - Math.abs((h / 60) % 2 - 1)); let m = l - (c / 2); let r = 0, g = 0, b = 0; if (h < 60) { r = c; g = x; b = 0; } else if ( 60 <= h && h < 120) { r = x; g = c; b = 0; } else if (120 <= h && h < 180) { r = 0; g = c; b = x; } else if (180 <= h && h < 240) { r = 0; g = x; b = c; } else if (240 <= h && h < 300) { r = x; g = 0; b = c; } else if (300 <= h) { r = c; g = 0; b = x; } this.setRGBF(r + m, g + m, b + m); return this; } setRandom() { return this.setRGBF(Math.random(), Math.random(), Math.random()); }; /** 0 to 255 */ setRGB(r, g, b) { return this.setRGBF(r / 255, g / 255, b / 255); } /** 0.0 to 1.0 */ setRGBF(r, g, b) { this.r = clamp(r, 0, 1); this.g = clamp(g, 0, 1); this.b = clamp(b, 0, 1); return this; } /** 0 to 255 */ setRYB(r, y, b) { const hexColor = cubicInterpolation(clamp(r, 0, 255), clamp(y, 0, 255), clamp(b, 0, 255), 255, CUBE.RYB_TO_RGB); return this.setHex(hexColor); } /** 0 to 255 */ setScalar(scalar) { return this.setRGB(scalar, scalar, scalar); } /* 0.0 to 1.0 */ setScalarF(scalar) { return this.setRGBF(scalar, scalar, scalar); } setStyle(style) { // CSS Color: rgb() / rgba() / hsl() / hsla() let m; if (m = /^((?:rgb|hsl)a?)\(([^\)]*)\)/.exec(style)) { let color; const name = m[1]; const components = m[2]; switch (name) { case 'rgb': case 'rgba': if (color = /^\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*(\d*\.?\d+)\s*)?$/.exec(components)) { // rgb(255,0,0) rgba(255,0,0,0.5) const r = Math.min(255, parseInt(color[1], 10)); const g = Math.min(255, parseInt(color[2], 10)); const b = Math.min(255, parseInt(color[3], 10)); return this.setRGB(r, g, b); } if (color = /^\s*(\d+)\%\s*,\s*(\d+)\%\s*,\s*(\d+)\%\s*(?:,\s*(\d*\.?\d+)\s*)?$/.exec(components)) { // rgb(100%,0%,0%) rgba(100%,0%,0%,0.5) const r = (Math.min(100, parseInt(color[1], 10)) / 100); const g = (Math.min(100, parseInt(color[2], 10)) / 100); const b = (Math.min(100, parseInt(color[3], 10)) / 100); return this.setRGBF(r, g, b); } break; case 'hsl': case 'hsla': if (color = /^\s*(\d*\.?\d+)\s*,\s*(\d+)\%\s*,\s*(\d+)\%\s*(?:,\s*(\d*\.?\d+)\s*)?$/.exec(components)) { // hsl(120,50%,50%) hsla(120,50%,50%,0.5) const h = parseFloat(color[1]); const s = parseInt(color[2], 10) / 100; const l = parseInt(color[3], 10) / 100; return this.setHSL(h, s, l); } break; } // Hex Color, i.e. #FF0000 } else if (m = /^\#([A-Fa-f\d]+)$/.exec(style)) { const hex = m[1]; const size = hex.length; // #FF0 if (size === 3) { const r = parseInt(hex.charAt(0) + hex.charAt(0), 16); const g = parseInt(hex.charAt(1) + hex.charAt(1), 16); const b = parseInt(hex.charAt(2) + hex.charAt(2), 16); return this.setRGB(r, g, b); // #FF0000 } else if (size === 6) { const r = parseInt(hex.charAt(0) + hex.charAt(1), 16); const g = parseInt(hex.charAt(2) + hex.charAt(3), 16); const b = parseInt(hex.charAt(4) + hex.charAt(5), 16); return this.setRGB(r, g, b); } } // X11 Color Name if (style && style.length > 0) { return this.setColorName(style); } return this; } /******************** OUTPUT */ /** Example output: 'rgb(255, 0, 0)' */ cssString(alpha /* optional */) { return ('rgb(' + this.rgbString(alpha) + ')'); } /** Returns decimal, i.e. 16711680 (equivalent to 0xff0000) */ hex() { return ((this.red() << 16) + (this.green() << 8) + this.blue()); } /** Example output: '#ff0000' */ hexString(inputColorData /* optional */){ if (inputColorData) this.set(inputColorData); return Iris.hexString(this.hex()); } /** Example output: '#ff0000' */ static hexString(inputColorData = 0x000000){ _temp.set(inputColorData); return '#' + ('000000' + ((_temp.hex()) >>> 0).toString(16)).slice(-6); } /** Returns random color as integer (i.e. 16711680) */ static randomHex() { return _random.setRandom().hex(); } /** Example output: '255, 0, 0' */ rgbString(alpha) { const rgb = this.red() + ', ' + this.green() + ', ' + this.blue(); return ((alpha !== undefined && alpha !== null) ? String(rgb + ', ' + alpha) : rgb); } /** Export to JSON */ toJSON() { return this.hex(); } /******************** COLOR DATA */ /** Copies HSL values into optional target, or returns new Object, values range from 0.0 to 1.0 */ getHSL(target) { if (target && isHSL(target)) { target.h = hueF(this.hex()); target.s = saturation(this.hex()); target.l = lightness(this.hex()); } else { return { h: hueF(this.hex()), s: saturation(this.hex()), l: lightness(this.hex()) }; } } /** Copies RGB values into optional target, or returns new Object, values range from 0.0 to 1.0 */ getRGB(target) { if (target && isHSL(target)) { target.r = this.r; target.g = this.g; target.b = this.b; } else { return { r: this.r, g: this.g, b: this.b }; } } /** Copies RYB values into optional target, or returns new Object, values range from 0.0 to 1.0 */ getRYB(target) { let rybAsHex = cubicInterpolation(this.r, this.g, this.b, 1.0, CUBE.RGB_TO_RYB); if (target && isRYB(target)) { target.r = redF(rybAsHex); target.y = greenF(rybAsHex); target.b = blueF(rybAsHex); return target; } return { r: redF(rybAsHex), y: greenF(rybAsHex), b: blueF(rybAsHex) }; } /** Copies RGB values into optional array, or returns a new Array, values range from 0.0 to 1.0 */ toArray(array = [], offset = 0) { array[offset] = this.r; array[offset + 1] = this.g; array[offset + 2] = this.b; return array; } /******************** COMPONENTS */ red() { return clamp(Math.floor(this.r * 255), 0, 255); } green() { return clamp(Math.floor(this.g * 255), 0, 255); } blue() { return clamp(Math.floor(this.b * 255), 0, 255); } redF() { return this.r; } greenF() { return this.g; } blueF() { return this.b; } hue() { return hue(this.hex()); } saturation() { return saturation(this.hex()); } lightness() { return lightness(this.hex()); } hueF() { return hueF(this.hex()); } /** Map a color's RGB hue to the closest hue in the RYB spectrum */ hueRYB() { for (let i = 1; i < RYB_OFFSET.length; i++) { if (RYB_OFFSET[i] > this.hue()) return i - 2; } } /******************** ADJUSTMENT */ /** Adds RGB values from color to this color */ add(color) { if (! color.isColor) console.warn(`Iris: add() was not called with a 'Color' object`); return this.setRGBF(this.r + color.r, this.g + color.g, this.b + color.b); } /** Adds scalar value to this colors RGB values, range -255 to 255 */ addScalar(scalar) { return this.setRGB(this.red() + scalar, this.green() + scalar, this.blue() + scalar); } /** Adds scalar value to this colors RGB values, range -1.0 to 1.0 */ addScalarF(scalar) { return this.setRGBF(this.r + scalar, this.g + scalar, this.b + scalar); } /** * Lightens color by amount * @param {Number} [amount] Percentage to lighten (default = 0.5) towards 1.0, possible values are 0.0 to 1.0 */ brighten(amount = 0.5 /* percentage from 0 to 1 */ ) { let h = hue(this.hex()); let s = saturation(this.hex()); let l = lightness(this.hex()); l = l + ((1.0 - l) * amount) this.setHSL(h, s, l); return this; } /** * Darkens color by amount * @name darken * @param {Number} [amount] Percentage to darken (default = 0.5), 0 = fully dark, 1 = no change, 2 = twice as bright */ darken(amount = 0.5 /* percentage from 0 to 1 */ ) { let h = hue(this.hex()); let s = saturation(this.hex()); let l = lightness(this.hex()) * amount; return this.setHSL(h, s, l); } /** Converts color to grayscale */ greyscale(percent = 1.0, format = 'luminosity') { return this.grayscale(percent, format) } grayscale(percent = 1.0, format = 'luminosity') { let gray = 0; switch (format) { case 'luminosity': gray = (this.r * 0.21) + (this.g * 0.72) + (this.b * 0.07); case 'average': default: gray = (this.r + this.g + this.b) / 3; } percent = clamp(percent, 0, 1); const r = (this.r * (1.0 - percent)) + (percent * gray); const g = (this.g * (1.0 - percent)) + (percent * gray); const b = (this.b * (1.0 - percent)) + (percent * gray); return this.setRGBF(r, g, b); } hslOffset(h, s, l) { return this.setHSL(this.hue() + h, this.saturation() + s, this.lightness() + l); } /** Mixes in 'color' by percent to this color */ mix(color, percent = 0.5) { if (! color.isColor) console.warn(`Iris: mix() was not called with a 'Color' object`); percent = clamp(percent, 0, 1); const r = (this.r * (1.0 - percent)) + (percent * color.r); const g = (this.g * (1.0 - percent)) + (percent * color.g); const b = (this.b * (1.0 - percent)) + (percent * color.b); return this.setRGBF(r, g, b); } multiply(color) { if (! color.isColor) console.warn(`Iris: multiply() was not called with a 'Color' object`); return this.setRGBF(this.r * color.r, this.g * color.g, this.b * color.b); } /** Multiplies RGB values from this color with scalar value, -Infinity to Infinity */ multiplyScalar(scalar) { return this.setRGBF(this.r * scalar, this.g * scalar, this.b * scalar); } rgbComplementary() { return this.rgbRotateHue(180); } /** Rotates the hue of a color in the RGB spectrum by degrees */ rgbRotateHue(degrees = 90) { const newHue = keepInRange(this.hue() + degrees); return this.setHSL(newHue, this.saturation(), this.lightness()); } /** Adjusts the RGB values to fit in the RYB spectrum as best as possible */ rybAdjust() { return this.setHSL(hue(matchSpectrum(this.hue(), SPECTRUM.RYB)), this.saturation(), this.lightness()); } rybComplementary() { return this.rybRotateHue(180); } /** Rotates the hue of a color in the RYB spectrum by degrees */ rybRotateHue(degrees = 90) { const newHue = keepInRange(this.hueRYB() + degrees); return this.setHSL(hue(matchSpectrum(newHue, SPECTRUM.RYB)), this.saturation(), this.lightness()); } /** Subtract RGB values from color to this color */ subtract(color) { if (! color.isColor) console.warn(`Iris: subtract() was not called with a 'Color' object`); return this.setRGBF(this.r - color.r, this.g - color.g, this.b - color.b); } /******************** COMPARISON */ /** Returns true if the RGB values of 'color' are the same as those of this object. */ equals(color) { if (! color.isColor) console.warn(`Iris: equals() was not called with a 'Color' object`); return (fuzzy(this.r, color.r) && fuzzy(this.g, color.g) && fuzzy(this.b, color.b)); } /** Returns true if the RGB values of 'color' are the same as those of this object. */ isEqual(color) { return this.equals(color); } /** Return true if lightness is < 60% for blue / purple / red, or else < 32% for all other colors */ isDark() { const h = this.hue(); const l = this.lightness(); return ((l < 0.60 && (h >= 210 || h <= 27)) || (l <= 0.32)); } /** Returns true if color is generally light-ish, false if dark-ish */ isLight() { return (! this.isDark()); } } export { Iris }; /******************** INTERNAL ********************/ function isRGB(object) { return (object.r !== undefined && object.g !== undefined && object.b !== undefined); } function isHSL(object) { return (object.h !== undefined && object.s !== undefined && object.l !== undefined); } function isRYB(object) { return (object.r !== undefined && object.y !== undefined && object.b !== undefined); } function clamp(value, min, max) { return Math.max(min, Math.min(max, value)); } function red(hexColor) { return clamp((hexColor & 0xff0000) >> 16, 0, 255); } function green(hexColor) { return clamp((hexColor & 0x00ff00) >> 8, 0, 255); } function blue(hexColor) { return clamp((hexColor & 0x0000ff), 0, 255); } function redF(hexColor) { return red(hexColor) / 255.0; } function greenF(hexColor) { return green(hexColor) / 255.0; } function blueF(hexColor) { return blue(hexColor) / 255.0; } function hue(hexColor) { return hsl(hexColor, 'h'); } function hueF(hexColor) { return hue(hexColor) / 360; } function saturation(hexColor) { return hsl(hexColor, 's'); } function lightness(hexColor) { return hsl(hexColor, 'l'); } function fuzzy(a, b, tolerance = 0.0015) { return ((a < (b + tolerance)) && (a > (b - tolerance))); } function keepInRange(value, min = 0, max = 360) { while (value >= max) value -= (max - min); while (value < min) value += (max - min); return value; } let _hslHex, _hslH, _hslS, _hslL; /** Return hue (0 to 360), saturation (0 to 1), and lightness (0 to 1) */ function hsl(hexColor, channel = 'h') { if (hexColor !== _hslHex) { if (hexColor === undefined || hexColor === null) return 0; const r = redF(hexColor), g = greenF(hexColor), b = blueF(hexColor); const max = Math.max(r, g, b), min = Math.min(r, g, b); const delta = max - min; _hslL = (max + min) / 2; if (delta === 0) { _hslH = _hslS = 0; } else { _hslS = (_hslL <= 0.5) ? (delta / (max + min)) : (delta / (2 - max - min)); switch (max) { case r: _hslH = (g - b) / delta + (g < b ? 6 : 0); break; case g: _hslH = (b - r) / delta + 2; break; case b: _hslH = (r - g) / delta + 4; break; } _hslH = Math.round(_hslH * 60); if (_hslH < 0) _hslH += 360; } _hslHex = hexColor; } switch (channel) { case 'h': return _hslH; case 's': return _hslS; case 'l': return _hslL; default: console.warn(`Iris: Unknown channel (${channel}) requested in hsl()`); } return 0; } const _interpolate = new Iris(); const _mix1 = new Iris(); const _mix2 = new Iris(); const _random = new Iris(); const _temp = new Iris(); /** Match to 'matchHue' into 'spectrum' */ function matchSpectrum(matchHue, spectrum = SPECTRUM.RYB) { let colorDegrees = 360 / spectrum.length; let degreeCount = colorDegrees; let stopCount = 0; for (let i = 0; i < spectrum.length; i++) { if (matchHue < degreeCount) { let percent = (degreeCount - matchHue) / colorDegrees; _mix1.set(spectrum[stopCount + 1]); return _mix1.mix(_mix2.set(spectrum[stopCount]), percent).hex(); } else { degreeCount = degreeCount + colorDegrees; stopCount = stopCount + 1 } } } /** * cubicInterpolation * @param {*} v1 Input number 1 (probably r of rgb, or r of ryb) * @param {*} v2 Input number 2 (probably g of rgb, or y of ryb) * @param {*} v3 Input number 3 (probably b of rgb, or b of ryb) * @param {*} scale The range of input values, should be either 1 (0 to 1) or 255 (0 to 255) * @param {*} table Table to use for cubic interpolation * @returns Hexidecimal that has 3 new values embedded */ function cubicInterpolation(v1, v2, v3, scale = 255, table = CUBE.RYB_TO_RGB) { v1 = clamp(v1 / scale, 0, 1); v2 = clamp(v2 / scale, 0, 1); v3 = clamp(v3 / scale, 0, 1); // Cube Points // f0=000, f1=001, f2=010, f3=011, f4=100, f5=101, f6=110, f7=111 const f0 = table[0], f1 = table[1], f2 = table[2], f3 = table[3]; const f4 = table[4], f5 = table[5], f6 = table[6], f7 = table[7]; const i1 = 1.0 - v1; const i2 = 1.0 - v2; const i3 = 1.0 - v3; const c0 = i1 * i2 * i3; const c1 = i1 * i2 * v3; const c2 = i1 * v2 * i3; const c3 = v1 * i2 * i3; const c4 = i1 * v2 * v3; const c5 = v1 * i2 * v3; const c6 = v1 * v2 * i3; const v7 = v1 * v2 * v3; const o1 = c0*f0[0] + c1*f1[0] + c2*f2[0] + c3*f3[0] + c4*f4[0] + c5*f5[0] + c6*f6[0] + v7*f7[0]; const o2 = c0*f0[1] + c1*f1[1] + c2*f2[1] + c3*f3[1] + c4*f4[1] + c5*f5[1] + c6*f6[1] + v7*f7[1]; const o3 = c0*f0[2] + c1*f1[2] + c2*f2[2] + c3*f3[2] + c4*f4[2] + c5*f5[2] + c6*f6[2] + v7*f7[2]; return _interpolate.set(o1, o2, o3, 'gl').hex(); } const CUBE = { RYB_TO_RGB: [ [ 1.000, 1.000, 1.000 ], // white [ 0.163, 0.373, 0.600 ], // blue [ 1.000, 1.000, 0.000 ], // yellow [ 1.000, 0.000, 0.000 ], // red [ 0.000, 0.660, 0.200 ], // green [ 0.500, 0.000, 0.500 ], // purple [ 1.000, 0.500, 0.000 ], // orange [ 0.000, 0.000, 0.000 ] // black ], RGB_TO_RYB: [ [ 1.000, 1.000, 1.000 ], // black [ 0.000, 0.000, 1.000 ], // blue [ 0.000, 1.000, 0.483 ], // green [ 1.000, 0.000, 0.000 ], // red [ 0.000, 0.053, 0.210 ], // cyan [ 0.309, 0.000, 0.469 ], // magenta [ 0.000, 1.000, 0.000 ], // yellow [ 0.000, 0.000, 0.000 ] // white ] }; // Stop values for RYB color wheel const SPECTRUM = { RYB: [ 0xFF0000, 0xFF4900, 0xFF7400, 0xFF9200, 0xFFAA00, 0xFFBF00, 0xFFD300, 0xFFE800, 0xFFFF00, 0xCCF600, 0x9FEE00, 0x67E300, 0x00CC00, 0x00AF64, 0x009999, 0x0B61A4, 0x1240AB, 0x1B1BB3, 0x3914AF, 0x530FAD, 0x7109AA, 0xA600A6, 0xCD0074, 0xE40045, 0xFF0000 /* <-- addded first value to end */ ] }; // Map of the RYB wheel to RGB wheel offset const RYB_OFFSET = [ 0, 1, 2, 3, 5, 6, 7, 8, 9, 10, 11, 13, 14, 15, 16, 17, 18, 19, 19, 20, 21, 21, 22, 23, 23, 24, 25, 25, 26, 27, 27, 28, 28, 29, 29, 30, 30, 31, 31, 32, 32, 32, 33, 33, 34, 34, 35, 35, 35, 36, 36, 37, 37, 37, 38, 38, 38, 39, 39, 40, 40, 40, 41, 41, 41, 42, 42, 42, 43, 43, 43, 44, 44, 44, 45, 45, 45, 46, 46, 46, 47, 47, 47, 47, 48, 48, 48, 49, 49, 49, 50, 50, 50, 51, 51, 51, 52, 52, 52, 53, 53, 53, 54, 54, 54, 55, 55, 55, 56, 56, 56, 57, 57, 57, 58, 58, 59, 59, 59, 60, 60, 61, 61, 62, 63, 63, 64, 65, 65, 66, 67, 68, 68, 69, 70, 70, 71, 72, 72, 73, 73, 74, 75, 75, 76, 77, 77, 78, 79, 79, 80, 81, 82, 82, 83, 84, 85, 86, 87, 88, 88, 89, 90, 91, 92, 93, 95, 96, 98, 100, 102, 104, 105, 107, 109, 111, 113, 115, 116, 118, 120, 122, 125, 127, 129, 131, 134, 136, 138, 141, 143, 145, 147, 150, 152, 154, 156, 158, 159, 161, 163, 165, 166, 168, 170, 171, 173, 175, 177, 178, 180, 182, 184, 185, 187, 189, 191, 192, 194, 196, 198, 199, 201, 203, 205, 206, 207, 208, 209, 210, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 226, 227, 228, 229, 230, 232, 233, 234, 235, 236, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 251, 252, 253, 254, 255, 256, 257, 257, 258, 259, 260, 260, 261, 262, 263, 264, 264, 265, 266, 267, 268, 268, 269, 270, 271, 272, 273, 274, 274, 275, 276, 277, 278, 279, 280, 282, 283, 284, 286, 287, 289, 290, 292, 293, 294, 296, 297, 299, 300, 302, 303, 305, 307, 309, 310, 312, 314, 316, 317, 319, 321, 323, 324, 326, 327, 328, 329, 330, 331, 332, 333, 334, 336, 337, 338, 339, 340, 341, 342, 343, 344, 345, 347, 348, 349, 350, 352, 353, 354, 355, 356, 358, 359, 999 ]; // X11 Color Names (http://www.w3.org/TR/css3-color/#svg-color) const COLOR_KEYWORDS = { 'aliceblue': 0xF0F8FF, 'antiquewhite': 0xFAEBD7, 'aqua': 0x00FFFF, 'aquamarine': 0x7FFFD4, 'azure': 0xF0FFFF, 'beige': 0xF5F5DC, 'bisque': 0xFFE4C4, 'black': 0x000000, 'blanchedalmond': 0xFFEBCD, 'blue': 0x0000FF, 'blueviolet': 0x8A2BE2, 'brown': 0xA52A2A, 'burlywood': 0xDEB887, 'cadetblue': 0x5F9EA0, 'chartreuse': 0x7FFF00, 'chocolate': 0xD2691E, 'coral': 0xFF7F50, 'cornflowerblue': 0x6495ED, 'cornsilk': 0xFFF8DC, 'crimson': 0xDC143C, 'cyan': 0x00FFFF, 'darkblue': 0x00008B, 'darkcyan': 0x008B8B, 'darkgoldenrod': 0xB8860B, 'darkgray': 0xA9A9A9, 'darkgreen': 0x006400, 'darkgrey': 0xA9A9A9, 'darkkhaki': 0xBDB76B, 'darkmagenta': 0x8B008B, 'darkolivegreen': 0x556B2F, 'darkorange': 0xFF8C00, 'darkorchid': 0x9932CC, 'darkred': 0x8B0000, 'darksalmon': 0xE9967A, 'darkseagreen': 0x8FBC8F, 'darkslateblue': 0x483D8B, 'darkslategray': 0x2F4F4F, 'darkslategrey': 0x2F4F4F, 'darkturquoise': 0x00CED1, 'darkviolet': 0x9400D3, 'deeppink': 0xFF1493, 'deepskyblue': 0x00BFFF, 'dimgray': 0x696969, 'dimgrey': 0x696969, 'dodgerblue': 0x1E90FF, 'firebrick': 0xB22222, 'floralwhite': 0xFFFAF0, 'forestgreen': 0x228B22, 'fuchsia': 0xFF00FF, 'gainsboro': 0xDCDCDC, 'ghostwhite': 0xF8F8FF, 'gold': 0xFFD700, 'goldenrod': 0xDAA520, 'gray': 0x808080, 'green': 0x008000, 'greenyellow': 0xADFF2F, 'grey': 0x808080, 'honeydew': 0xF0FFF0, 'hotpink': 0xFF69B4, 'indianred': 0xCD5C5C, 'indigo': 0x4B0082, 'ivory': 0xFFFFF0, 'khaki': 0xF0E68C, 'lavender': 0xE6E6FA, 'lavenderblush': 0xFFF0F5, 'lawngreen': 0x7CFC00, 'lemonchiffon': 0xFFFACD, 'lightblue': 0xADD8E6, 'lightcoral': 0xF08080, 'lightcyan': 0xE0FFFF, 'lightgoldenrodyellow': 0xFAFAD2, 'lightgray': 0xD3D3D3, 'lightgreen': 0x90EE90, 'lightgrey': 0xD3D3D3, 'lightpink': 0xFFB6C1, 'lightsalmon': 0xFFA07A, 'lightseagreen': 0x20B2AA, 'lightskyblue': 0x87CEFA, 'lightslategray': 0x778899, 'lightslategrey': 0x778899, 'lightsteelblue': 0xB0C4DE, 'lightyellow': 0xFFFFE0, 'lime': 0x00FF00, 'limegreen': 0x32CD32, 'linen': 0xFAF0E6, 'magenta': 0xFF00FF, 'maroon': 0x800000, 'mediumaquamarine': 0x66CDAA, 'mediumblue': 0x0000CD, 'mediumorchid': 0xBA55D3, 'mediumpurple': 0x9370DB, 'mediumseagreen': 0x3CB371, 'mediumslateblue': 0x7B68EE, 'mediumspringgreen': 0x00FA9A, 'mediumturquoise': 0x48D1CC, 'mediumvioletred': 0xC71585, 'midnightblue': 0x191970, 'mintcream': 0xF5FFFA, 'mistyrose': 0xFFE4E1, 'moccasin': 0xFFE4B5, 'navajowhite': 0xFFDEAD, 'navy': 0x000080, 'oldlace': 0xFDF5E6, 'olive': 0x808000, 'olivedrab': 0x6B8E23, 'orange': 0xFFA500, 'orangered': 0xFF4500, 'orchid': 0xDA70D6, 'palegoldenrod': 0xEEE8AA, 'palegreen': 0x98FB98, 'paleturquoise': 0xAFEEEE, 'palevioletred': 0xDB7093, 'papayawhip': 0xFFEFD5, 'peachpuff': 0xFFDAB9, 'peru': 0xCD853F, 'pink': 0xFFC0CB, 'plum': 0xDDA0DD, 'powderblue': 0xB0E0E6, 'purple': 0x800080, 'rebeccapurple': 0x663399, 'red': 0xFF0000, 'rosybrown': 0xBC8F8F, 'royalblue': 0x4169E1, 'saddlebrown': 0x8B4513, 'salmon': 0xFA8072, 'sandybrown': 0xF4A460, 'seagreen': 0x2E8B57, 'seashell': 0xFFF5EE, 'sienna': 0xA0522D, 'silver': 0xC0C0C0, 'skyblue': 0x87CEEB, 'slateblue': 0x6A5ACD, 'slategray': 0x708090, 'slategrey': 0x708090, 'snow': 0xFFFAFA, 'springgreen': 0x00FF7F, 'steelblue': 0x4682B4, 'tan': 0xD2B48C, 'teal': 0x008080, 'thistle': 0xD8BFD8, 'tomato': 0xFF6347, 'turquoise': 0x40E0D0, 'transparent': 0x000000, 'violet': 0xEE82EE, 'wheat': 0xF5DEB3, 'white': 0xFFFFFF, 'whitesmoke': 0xF5F5F5, 'yellow': 0xFFFF00, 'yellowgreen': 0x9ACD32 }; ///////////////////////////////////////////////////////////////////////////////////// ///// Acknowledgements ///////////////////////////////////////////////////////////////////////////////////// // // Some portions of this code adapted from: // Description: Color Schemer // Author: Scott Kellum <@scottkellum> and Mason Wendell <@canarymason> // License: Distributed under the MIT License // Source(s): https://github.com/at-import/color-schemer/blob/master/stylesheets/color-schemer/_ryb.scss // // Description: three.js // Author: mrdoob and three.js authors // License: Distributed under the MIT License // Source(s): https://github.com/mrdoob/three.js/blob/master/src/math/Color.js // // Description: RYB // Author: Ilya Kolbin // License: Distributed under the MIT License // Source(s): https://github.com/iskolbin/lryb/blob/master/ryb.lua // // Thanks to: // Description: RYB and RGB Color Space Conversion // Author: Jean-Olivier Irisson // Source(s): https://math.stackexchange.com/questions/305395/ryb-and-rgb-color-space-conversion // // Description: Paint Inspired Color Mixing and Compositing for Visualization // Author: Nathan Gossett & Baoquan Chen // Source(s): http://vis.computer.org/vis2004/DVD/infovis/papers/gossett.pdf ///////////////////////////////////////////////////////////////////////////////////// ///// License ///////////////////////////////////////////////////////////////////////////////////// // // MIT License // // Iris // Copyright (c) 2021 Stephens Nunnally <@stevinz> // // Some Portions // Copyright (c) 2011 Scott Kellum <@scottkellum> and Mason Wendell <@canarymason> // Copyright (c) 2010 mrdoob and three.js authors // Copyright (c) 2018 Ilya Kolbin // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE.