diff --git a/js/animations/animation.js b/js/animations/animation.js index 6870ae02c..5d76dd3eb 100644 --- a/js/animations/animation.js +++ b/js/animations/animation.js @@ -1217,6 +1217,7 @@ const Animator = { ba.addKeyframe({ time: 0, channel, + uniform: !(b[channel] instanceof Array), data_points: getKeyframeDataPoints(b[channel]), }) } else if (typeof b[channel] === 'object' && b[channel].post) { @@ -1224,6 +1225,7 @@ const Animator = { time: 0, channel, interpolation: b[channel].lerp_mode, + uniform: !(b[channel].post instanceof Array), data_points: getKeyframeDataPoints(b[channel]), }); } else if (typeof b[channel] === 'object') { @@ -1232,6 +1234,7 @@ const Animator = { time: parseFloat(timestamp), channel, interpolation: b[channel][timestamp].lerp_mode, + uniform: !(b[channel][timestamp] instanceof Array), data_points: getKeyframeDataPoints(b[channel][timestamp]), }); } diff --git a/js/animations/timeline_animators.js b/js/animations/timeline_animators.js index 204142777..6fe1c8436 100644 --- a/js/animations/timeline_animators.js +++ b/js/animations/timeline_animators.js @@ -132,13 +132,18 @@ GeneralAnimator.addChannel = function(channel, options) { mutable: typeof options.mutable === 'boolean' ? options.mutable : true, max_data_points: options.max_data_points || 0 } - Timeline.animators.forEach(animator => { - if (animator instanceof this && !animator[channel]) { - Vue.set(animator, channel, []); - if (this.prototype.channels[channel].mutable) { - Vue.set(animator.muted, channel, false); - } - } + ModelProject.all.forEach(project => { + if (!project.animations) + project.animations.forEach(animation => { + animation.animators.forEach(animator => { + if (animator instanceof this && !animator[channel]) { + Vue.set(animator, channel, []); + if (this.prototype.channels[channel].mutable) { + Vue.set(animator.muted, channel, false); + } + } + }) + }) }) Timeline.vue.$forceUpdate(); } @@ -148,9 +153,9 @@ class BoneAnimator extends GeneralAnimator { this.uuid = uuid; this._name = name; - this.rotation = []; - this.position = []; - this.scale = []; + for (let channel in this.channels) { + this[channel] = []; + } } get name() { var group = this.getGroup(); diff --git a/js/modeling/transform.js b/js/modeling/transform.js index 6467d8c34..a9facb3c6 100644 --- a/js/modeling/transform.js +++ b/js/modeling/transform.js @@ -247,6 +247,7 @@ const Vertexsnap = { vectors.push([0, 0, 0]); let points = new THREE.Points(new THREE.BufferGeometry(), new THREE.PointsMaterial().copy(Canvas.meshVertexMaterial)); + points.element_uuid = element.uuid; points.vertices = vectors; let vector_positions = []; vectors.forEach(vector => vector_positions.push(...vector)); diff --git a/js/texturing/edit_texture.js b/js/texturing/edit_texture.js index 3bf8425b7..7d858288f 100644 --- a/js/texturing/edit_texture.js +++ b/js/texturing/edit_texture.js @@ -21,6 +21,7 @@ BARS.defineActions(function() { texture.edit((canvas) => { let ctx = canvas.getContext('2d'); + ctx.clearRect(0, 0, texture.width, texture.height); ctx.filter = 'invert(1)'; ctx.drawImage(canvas, 0, 0); @@ -56,6 +57,7 @@ BARS.defineActions(function() { textures.forEach((texture, i) => { texture.edit((canvas) => { let ctx = canvas.getContext('2d'); + ctx.clearRect(0, 0, texture.width, texture.height); ctx.filter = `brightness(${this.brightness / 100}) contrast(${this.contrast / 100})`; ctx.drawImage(original_imgs[i], 0, 0); @@ -130,6 +132,7 @@ BARS.defineActions(function() { textures.forEach((texture, i) => { texture.edit((canvas) => { let ctx = canvas.getContext('2d'); + ctx.clearRect(0, 0, texture.width, texture.height); ctx.filter = `saturate(${this.saturation / 100}) hue-rotate(${this.hue}deg)`; ctx.drawImage(original_imgs[i], 0, 0); diff --git a/lib/spectrum.js b/lib/spectrum.js index d554ecea8..d17083c13 100644 --- a/lib/spectrum.js +++ b/lib/spectrum.js @@ -1265,26 +1265,24 @@ } }; - // TinyColor v1.1.2 - // https://github.com/bgrins/TinyColor - // Brian Grinstead, MIT License + // TinyColor v1.4.2 +// https://github.com/bgrins/TinyColor +// Brian Grinstead, MIT License - (function() { +(function(Math) { - var trimLeft = /^[\s,#]+/, + var trimLeft = /^\s+/, trimRight = /\s+$/, - tinyCounter = 0, - math = Math, - mathRound = math.round, - mathMin = math.min, - mathMax = math.max, - mathRandom = math.random; - - var tinycolor = function(color, opts) { - + mathRound = Math.round, + mathMin = Math.min, + mathMax = Math.max, + mathRandom = Math.random; + + function tinycolor (color, opts) { + color = (color) ? color : ''; opts = opts || { }; - + // If input is already a tinycolor, return itself if (color instanceof tinycolor) { return color; @@ -1293,7 +1291,7 @@ if (!(this instanceof tinycolor)) { return new tinycolor(color, opts); } - + var rgb = inputToRGB(color); this._originalInput = color, this._r = rgb.r, @@ -1303,7 +1301,7 @@ this._roundA = mathRound(100*this._a) / 100, this._format = opts.format || rgb.format; this._gradientType = opts.gradientType; - + // Don't let the range of [0,255] come back in [0,1]. // Potentially lose a little bit of precision here, but will fix issues where // .5 gets interpreted as half of the total, instead of half of 1 @@ -1311,11 +1309,10 @@ if (this._r < 1) { this._r = mathRound(this._r); } if (this._g < 1) { this._g = mathRound(this._g); } if (this._b < 1) { this._b = mathRound(this._b); } - + this._ok = rgb.ok; - this._tc_id = tinyCounter++; - }; - + } + tinycolor.prototype = { isDark: function() { return this.getBrightness() < 128; @@ -1336,9 +1333,23 @@ return this._a; }, getBrightness: function() { + //http://www.w3.org/TR/AERT#color-contrast var rgb = this.toRgb(); return (rgb.r * 299 + rgb.g * 587 + rgb.b * 114) / 1000; }, + getLuminance: function() { + //http://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef + var rgb = this.toRgb(); + var RsRGB, GsRGB, BsRGB, R, G, B; + RsRGB = rgb.r/255; + GsRGB = rgb.g/255; + BsRGB = rgb.b/255; + + if (RsRGB <= 0.03928) {R = RsRGB / 12.92;} else {R = Math.pow(((RsRGB + 0.055) / 1.055), 2.4);} + if (GsRGB <= 0.03928) {G = GsRGB / 12.92;} else {G = Math.pow(((GsRGB + 0.055) / 1.055), 2.4);} + if (BsRGB <= 0.03928) {B = BsRGB / 12.92;} else {B = Math.pow(((BsRGB + 0.055) / 1.055), 2.4);} + return (0.2126 * R) + (0.7152 * G) + (0.0722 * B); + }, setAlpha: function(value) { this._a = boundAlpha(value); this._roundA = mathRound(100*this._a) / 100; @@ -1372,14 +1383,11 @@ toHexString: function(allow3Char) { return '#' + this.toHex(allow3Char); }, - toHex8: function() { - return rgbaToHex(this._r, this._g, this._b, this._a); + toHex8: function(allow4Char) { + return rgbaToHex(this._r, this._g, this._b, this._a, allow4Char); }, - toHex8String: function() { - return '#' + this.toHex8(); - }, - toInteger: function() { - return Jimp.rgbaToInt(mathRound(this._r), mathRound(this._g), mathRound(this._b), this._a*255) + toHex8String: function(allow4Char) { + return '#' + this.toHex8(allow4Char); }, toRgb: function() { return { r: mathRound(this._r), g: mathRound(this._g), b: mathRound(this._b), a: this._a }; @@ -1401,33 +1409,33 @@ if (this._a === 0) { return "transparent"; } - + if (this._a < 1) { return false; } - + return hexNames[rgbToHex(this._r, this._g, this._b, true)] || false; }, toFilter: function(secondColor) { - var hex8String = '#' + rgbaToHex(this._r, this._g, this._b, this._a); + var hex8String = '#' + rgbaToArgbHex(this._r, this._g, this._b, this._a); var secondHex8String = hex8String; var gradientType = this._gradientType ? "GradientType = 1, " : ""; - + if (secondColor) { var s = tinycolor(secondColor); - secondHex8String = s.toHex8String(); + secondHex8String = '#' + rgbaToArgbHex(s._r, s._g, s._b, s._a); } - + return "progid:DXImageTransform.Microsoft.gradient("+gradientType+"startColorstr="+hex8String+",endColorstr="+secondHex8String+")"; }, toString: function(format) { var formatSet = !!format; format = format || this._format; - + var formattedString = false; var hasAlpha = this._a < 1 && this._a >= 0; - var needsAlphaFormat = !formatSet && hasAlpha && (format === "hex" || format === "hex6" || format === "hex3" || format === "name"); - + var needsAlphaFormat = !formatSet && hasAlpha && (format === "hex" || format === "hex6" || format === "hex3" || format === "hex4" || format === "hex8" || format === "name"); + if (needsAlphaFormat) { // Special case for "transparent", all other non-alpha formats // will return rgba when there is transparency. @@ -1448,6 +1456,9 @@ if (format === "hex3") { formattedString = this.toHexString(true); } + if (format === "hex4") { + formattedString = this.toHex8String(true); + } if (format === "hex8") { formattedString = this.toHex8String(); } @@ -1460,10 +1471,13 @@ if (format === "hsv") { formattedString = this.toHsvString(); } - + return formattedString || this.toHexString(); }, - + clone: function() { + return tinycolor(this.toString()); + }, + _applyModification: function(fn, args) { var color = fn.apply(null, [this].concat([].slice.call(args))); this._r = color._r; @@ -1493,7 +1507,7 @@ spin: function() { return this._applyModification(spin, arguments); }, - + _applyCombination: function(fn, args) { return fn.apply(null, [this].concat([].slice.call(args))); }, @@ -1516,7 +1530,7 @@ return this._applyCombination(tetrad, arguments); } }; - + // If input is an object, force 1 into "1.0" to handle ratios properly // String input requires "1.0" as input, so 1 will be treated as 1 tinycolor.fromRatio = function(color, opts) { @@ -1534,64 +1548,67 @@ } color = newColor; } - + return tinycolor(color, opts); }; - + // Given a string or object, convert that input to RGB // Possible string inputs: // - // "red" - // "#f00" or "f00" - // "#ff0000" or "ff0000" - // "#ff000000" or "ff000000" - // "rgb 255 0 0" or "rgb (255, 0, 0)" - // "rgb 1.0 0 0" or "rgb (1, 0, 0)" - // "rgba (255, 0, 0, 1)" or "rgba 255, 0, 0, 1" - // "rgba (1.0, 0, 0, 1)" or "rgba 1.0, 0, 0, 1" - // "hsl(0, 100%, 50%)" or "hsl 0 100% 50%" - // "hsla(0, 100%, 50%, 1)" or "hsla 0 100% 50%, 1" - // "hsv(0, 100%, 100%)" or "hsv 0 100% 100%" + // "red" + // "#f00" or "f00" + // "#ff0000" or "ff0000" + // "#ff000000" or "ff000000" + // "rgb 255 0 0" or "rgb (255, 0, 0)" + // "rgb 1.0 0 0" or "rgb (1, 0, 0)" + // "rgba (255, 0, 0, 1)" or "rgba 255, 0, 0, 1" + // "rgba (1.0, 0, 0, 1)" or "rgba 1.0, 0, 0, 1" + // "hsl(0, 100%, 50%)" or "hsl 0 100% 50%" + // "hsla(0, 100%, 50%, 1)" or "hsla 0 100% 50%, 1" + // "hsv(0, 100%, 100%)" or "hsv 0 100% 100%" // function inputToRGB(color) { - + var rgb = { r: 0, g: 0, b: 0 }; var a = 1; + var s = null; + var v = null; + var l = null; var ok = false; var format = false; - + if (typeof color == "string") { color = stringInputToObject(color); } - + if (typeof color == "object") { - if (color.hasOwnProperty("r") && color.hasOwnProperty("g") && color.hasOwnProperty("b")) { + if (isValidCSSUnit(color.r) && isValidCSSUnit(color.g) && isValidCSSUnit(color.b)) { rgb = rgbToRgb(color.r, color.g, color.b); ok = true; format = String(color.r).substr(-1) === "%" ? "prgb" : "rgb"; } - else if (color.hasOwnProperty("h") && color.hasOwnProperty("s") && color.hasOwnProperty("v")) { - color.s = convertToPercentage(color.s); - color.v = convertToPercentage(color.v); - rgb = hsvToRgb(color.h, color.s, color.v); + else if (isValidCSSUnit(color.h) && isValidCSSUnit(color.s) && isValidCSSUnit(color.v)) { + s = convertToPercentage(color.s); + v = convertToPercentage(color.v); + rgb = hsvToRgb(color.h, s, v); ok = true; format = "hsv"; } - else if (color.hasOwnProperty("h") && color.hasOwnProperty("s") && color.hasOwnProperty("l")) { - color.s = convertToPercentage(color.s); - color.l = convertToPercentage(color.l); - rgb = hslToRgb(color.h, color.s, color.l); + else if (isValidCSSUnit(color.h) && isValidCSSUnit(color.s) && isValidCSSUnit(color.l)) { + s = convertToPercentage(color.s); + l = convertToPercentage(color.l); + rgb = hslToRgb(color.h, s, l); ok = true; format = "hsl"; } - + if (color.hasOwnProperty("a")) { a = color.a; } } - + a = boundAlpha(a); - + return { ok: ok, format: color.format || format, @@ -1601,14 +1618,14 @@ a: a }; } - - + + // Conversion Functions // -------------------- - + // `rgbToHsl`, `rgbToHsv`, `hslToRgb`, `hsvToRgb` modified from: // - + // `rgbToRgb` // Handle bounds / percentage checking to conform to CSS color spec // @@ -1621,20 +1638,20 @@ b: bound01(b, 255) * 255 }; } - + // `rgbToHsl` // Converts an RGB color value to HSL. // *Assumes:* r, g, and b are contained in [0, 255] or [0, 1] // *Returns:* { h, s, l } in [0,1] function rgbToHsl(r, g, b) { - + r = bound01(r, 255); g = bound01(g, 255); b = bound01(b, 255); - + var max = mathMax(r, g, b), min = mathMin(r, g, b); var h, s, l = (max + min) / 2; - + if(max == min) { h = s = 0; // achromatic } @@ -1646,24 +1663,24 @@ case g: h = (b - r) / d + 2; break; case b: h = (r - g) / d + 4; break; } - + h /= 6; } - + return { h: h, s: s, l: l }; } - + // `hslToRgb` // Converts an HSL color value to RGB. // *Assumes:* h is contained in [0, 1] or [0, 360] and s and l are contained [0, 1] or [0, 100] // *Returns:* { r, g, b } in the set [0, 255] function hslToRgb(h, s, l) { var r, g, b; - + h = bound01(h, 360); s = bound01(s, 100); l = bound01(l, 100); - + function hue2rgb(p, q, t) { if(t < 0) t += 1; if(t > 1) t -= 1; @@ -1672,7 +1689,7 @@ if(t < 2/3) return p + (q - p) * (2/3 - t) * 6; return p; } - + if(s === 0) { r = g = b = l; // achromatic } @@ -1683,26 +1700,26 @@ g = hue2rgb(p, q, h); b = hue2rgb(p, q, h - 1/3); } - + return { r: r * 255, g: g * 255, b: b * 255 }; } - + // `rgbToHsv` // Converts an RGB color value to HSV // *Assumes:* r, g, and b are contained in the set [0, 255] or [0, 1] // *Returns:* { h, s, v } in [0,1] function rgbToHsv(r, g, b) { - + r = bound01(r, 255); g = bound01(g, 255); b = bound01(b, 255); - + var max = mathMax(r, g, b), min = mathMin(r, g, b); var h, s, v = max; - + var d = max - min; s = max === 0 ? 0 : d / max; - + if(max == min) { h = 0; // achromatic } @@ -1716,18 +1733,18 @@ } return { h: h, s: s, v: v }; } - + // `hsvToRgb` // Converts an HSV color value to RGB. // *Assumes:* h is contained in [0, 1] or [0, 360] and s and v are contained in [0, 1] or [0, 100] // *Returns:* { r, g, b } in the set [0, 255] function hsvToRgb(h, s, v) { - + h = bound01(h, 360) * 6; s = bound01(s, 100); v = bound01(v, 100); - - var i = math.floor(h), + + var i = Math.floor(h), f = h - i, p = v * (1 - s), q = v * (1 - f * s), @@ -1736,51 +1753,73 @@ r = [v, q, p, p, t, v][mod], g = [t, v, v, q, p, p][mod], b = [p, p, t, v, v, q][mod]; - + return { r: r * 255, g: g * 255, b: b * 255 }; } - + // `rgbToHex` // Converts an RGB color to hex // Assumes r, g, and b are contained in the set [0, 255] // Returns a 3 or 6 character hex function rgbToHex(r, g, b, allow3Char) { - + var hex = [ pad2(mathRound(r).toString(16)), pad2(mathRound(g).toString(16)), pad2(mathRound(b).toString(16)) ]; - + // Return a 3 character hex if possible if (allow3Char && hex[0].charAt(0) == hex[0].charAt(1) && hex[1].charAt(0) == hex[1].charAt(1) && hex[2].charAt(0) == hex[2].charAt(1)) { return hex[0].charAt(0) + hex[1].charAt(0) + hex[2].charAt(0); } - + return hex.join(""); } - // `rgbaToHex` - // Converts an RGBA color plus alpha transparency to hex - // Assumes r, g, b and a are contained in the set [0, 255] - // Returns an 8 character hex - function rgbaToHex(r, g, b, a) { - - var hex = [ - pad2(convertDecimalToHex(a)), - pad2(mathRound(r).toString(16)), - pad2(mathRound(g).toString(16)), - pad2(mathRound(b).toString(16)) - ]; - - return hex.join(""); + + // `rgbaToHex` + // Converts an RGBA color plus alpha transparency to hex + // Assumes r, g, b are contained in the set [0, 255] and + // a in [0, 1]. Returns a 4 or 8 character rgba hex + function rgbaToHex(r, g, b, a, allow4Char) { + + var hex = [ + pad2(mathRound(r).toString(16)), + pad2(mathRound(g).toString(16)), + pad2(mathRound(b).toString(16)), + pad2(convertDecimalToHex(a)) + ]; + + // Return a 4 character hex if possible + if (allow4Char && hex[0].charAt(0) == hex[0].charAt(1) && hex[1].charAt(0) == hex[1].charAt(1) && hex[2].charAt(0) == hex[2].charAt(1) && hex[3].charAt(0) == hex[3].charAt(1)) { + return hex[0].charAt(0) + hex[1].charAt(0) + hex[2].charAt(0) + hex[3].charAt(0); } - + + return hex.join(""); + } + + // `rgbaToArgbHex` + // Converts an RGBA color to an ARGB Hex8 string + // Rarely used, but required for "toFilter()" + function rgbaToArgbHex(r, g, b, a) { + + var hex = [ + pad2(convertDecimalToHex(a)), + pad2(mathRound(r).toString(16)), + pad2(mathRound(g).toString(16)), + pad2(mathRound(b).toString(16)) + ]; + + return hex.join(""); + } + // `equals` // Can be called with any tinycolor input tinycolor.equals = function (color1, color2) { if (!color1 || !color2) { return false; } return tinycolor(color1).toRgbString() == tinycolor(color2).toRgbString(); }; + tinycolor.random = function() { return tinycolor.fromRatio({ r: mathRandom(), @@ -1788,13 +1827,13 @@ b: mathRandom() }); }; - - + + // Modification Functions // ---------------------- // Thanks to less.js for some of the basics here // - + function desaturate(color, amount) { amount = (amount === 0) ? 0 : (amount || 10); var hsl = tinycolor(color).toHsl(); @@ -1802,7 +1841,7 @@ hsl.s = clamp01(hsl.s); return tinycolor(hsl); } - + function saturate(color, amount) { amount = (amount === 0) ? 0 : (amount || 10); var hsl = tinycolor(color).toHsl(); @@ -1810,11 +1849,11 @@ hsl.s = clamp01(hsl.s); return tinycolor(hsl); } - + function greyscale(color) { return tinycolor(color).desaturate(100); } - + function lighten (color, amount) { amount = (amount === 0) ? 0 : (amount || 10); var hsl = tinycolor(color).toHsl(); @@ -1822,7 +1861,7 @@ hsl.l = clamp01(hsl.l); return tinycolor(hsl); } - + function brighten(color, amount) { amount = (amount === 0) ? 0 : (amount || 10); var rgb = tinycolor(color).toRgb(); @@ -1831,7 +1870,7 @@ rgb.b = mathMax(0, mathMin(255, rgb.b - mathRound(255 * - (amount / 100)))); return tinycolor(rgb); } - + function darken (color, amount) { amount = (amount === 0) ? 0 : (amount || 10); var hsl = tinycolor(color).toHsl(); @@ -1839,27 +1878,27 @@ hsl.l = clamp01(hsl.l); return tinycolor(hsl); } - + // Spin takes a positive or negative amount within [-360, 360] indicating the change of hue. // Values outside of this range will be wrapped into this range. function spin(color, amount) { var hsl = tinycolor(color).toHsl(); - var hue = (mathRound(hsl.h) + amount) % 360; + var hue = (hsl.h + amount) % 360; hsl.h = hue < 0 ? 360 + hue : hue; return tinycolor(hsl); } - + // Combination Functions // --------------------- // Thanks to jQuery xColor for some of the ideas behind these // - + function complement(color) { var hsl = tinycolor(color).toHsl(); hsl.h = (hsl.h + 180) % 360; return tinycolor(hsl); } - + function triad(color) { var hsl = tinycolor(color).toHsl(); var h = hsl.h; @@ -1869,7 +1908,7 @@ tinycolor({ h: (h + 240) % 360, s: hsl.s, l: hsl.l }) ]; } - + function tetrad(color) { var hsl = tinycolor(color).toHsl(); var h = hsl.h; @@ -1880,7 +1919,7 @@ tinycolor({ h: (h + 270) % 360, s: hsl.s, l: hsl.l }) ]; } - + function splitcomplement(color) { var hsl = tinycolor(color).toHsl(); var h = hsl.h; @@ -1890,140 +1929,141 @@ tinycolor({ h: (h + 216) % 360, s: hsl.s, l: hsl.l}) ]; } - + function analogous(color, results, slices) { results = results || 6; slices = slices || 30; - + var hsl = tinycolor(color).toHsl(); var part = 360 / slices; var ret = [tinycolor(color)]; - + for (hsl.h = ((hsl.h - (part * results >> 1)) + 720) % 360; --results; ) { hsl.h = (hsl.h + part) % 360; ret.push(tinycolor(hsl)); } return ret; } - + function monochromatic(color, results) { results = results || 6; var hsv = tinycolor(color).toHsv(); var h = hsv.h, s = hsv.s, v = hsv.v; var ret = []; var modification = 1 / results; - + while (results--) { ret.push(tinycolor({ h: h, s: s, v: v})); v = (v + modification) % 1; } - + return ret; } - + // Utility Functions // --------------------- - + tinycolor.mix = function(color1, color2, amount) { amount = (amount === 0) ? 0 : (amount || 50); - + var rgb1 = tinycolor(color1).toRgb(); var rgb2 = tinycolor(color2).toRgb(); - + var p = amount / 100; - var w = p * 2 - 1; - var a = rgb2.a - rgb1.a; - - var w1; - - if (w * a == -1) { - w1 = w; - } else { - w1 = (w + a) / (1 + w * a); - } - - w1 = (w1 + 1) / 2; - - var w2 = 1 - w1; - + var rgba = { - r: rgb2.r * w1 + rgb1.r * w2, - g: rgb2.g * w1 + rgb1.g * w2, - b: rgb2.b * w1 + rgb1.b * w2, - a: rgb2.a * p + rgb1.a * (1 - p) + r: ((rgb2.r - rgb1.r) * p) + rgb1.r, + g: ((rgb2.g - rgb1.g) * p) + rgb1.g, + b: ((rgb2.b - rgb1.b) * p) + rgb1.b, + a: ((rgb2.a - rgb1.a) * p) + rgb1.a }; - + return tinycolor(rgba); }; - - + + // Readability Functions // --------------------- - // - - // `readability` - // Analyze the 2 colors and returns an object with the following properties: - // `brightness`: difference in brightness between the two colors - // `color`: difference in color/hue between the two colors + // false - tinycolor.isReadable = function(color1, color2) { + // tinycolor.isReadable("#000", "#111") => false + // tinycolor.isReadable("#000", "#111",{level:"AA",size:"large"}) => false + tinycolor.isReadable = function(color1, color2, wcag2) { var readability = tinycolor.readability(color1, color2); - return readability.brightness > 125 && readability.color > 500; + var wcag2Parms, out; + + out = false; + + wcag2Parms = validateWCAG2Parms(wcag2); + switch (wcag2Parms.level + wcag2Parms.size) { + case "AAsmall": + case "AAAlarge": + out = readability >= 4.5; + break; + case "AAlarge": + out = readability >= 3; + break; + case "AAAsmall": + out = readability >= 7; + break; + } + return out; + }; - + // `mostReadable` // Given a base color and a list of possible foreground or background // colors for that base, returns the most readable color. + // Optionally returns Black or White if the most readable color is unreadable. // *Example* - // tinycolor.mostReadable("#123", ["#fff", "#000"]) => "#000" - tinycolor.mostReadable = function(baseColor, colorList) { + // tinycolor.mostReadable(tinycolor.mostReadable("#123", ["#124", "#125"],{includeFallbackColors:false}).toHexString(); // "#112255" + // tinycolor.mostReadable(tinycolor.mostReadable("#123", ["#124", "#125"],{includeFallbackColors:true}).toHexString(); // "#ffffff" + // tinycolor.mostReadable("#a8015a", ["#faf3f3"],{includeFallbackColors:true,level:"AAA",size:"large"}).toHexString(); // "#faf3f3" + // tinycolor.mostReadable("#a8015a", ["#faf3f3"],{includeFallbackColors:true,level:"AAA",size:"small"}).toHexString(); // "#ffffff" + tinycolor.mostReadable = function(baseColor, colorList, args) { var bestColor = null; var bestScore = 0; - var bestIsReadable = false; - for (var i=0; i < colorList.length; i++) { - - // We normalize both around the "acceptable" breaking point, - // but rank brightness constrast higher than hue. - - var readability = tinycolor.readability(baseColor, colorList[i]); - var readable = readability.brightness > 125 && readability.color > 500; - var score = 3 * (readability.brightness / 125) + (readability.color / 500); - - if ((readable && ! bestIsReadable) || - (readable && bestIsReadable && score > bestScore) || - ((! readable) && (! bestIsReadable) && score > bestScore)) { - bestIsReadable = readable; - bestScore = score; + var readability; + var includeFallbackColors, level, size ; + args = args || {}; + includeFallbackColors = args.includeFallbackColors ; + level = args.level; + size = args.size; + + for (var i= 0; i < colorList.length ; i++) { + readability = tinycolor.readability(baseColor, colorList[i]); + if (readability > bestScore) { + bestScore = readability; bestColor = tinycolor(colorList[i]); } } - return bestColor; + + if (tinycolor.isReadable(baseColor, bestColor, {"level":level,"size":size}) || !includeFallbackColors) { + return bestColor; + } + else { + args.includeFallbackColors=false; + return tinycolor.mostReadable(baseColor,["#fff", "#000"],args); + } }; - - + + // Big List of Colors // ------------------ // @@ -2178,14 +2218,14 @@ yellow: "ff0", yellowgreen: "9acd32" }; - + // Make it easy to access colors via `hexNames[hex]` var hexNames = tinycolor.hexNames = flip(names); - - + + // Utilities // --------- - + // `{ 'name1': 'val1' }` becomes `{ 'val1': 'name1' }` function flip(o) { var flipped = { }; @@ -2196,74 +2236,74 @@ } return flipped; } - + // Return a valid alpha value [0,1] with all invalid values being set to 1 function boundAlpha(a) { a = parseFloat(a); - + if (isNaN(a) || a < 0 || a > 1) { a = 1; } - + return a; } - + // Take input from [0, n] and return it as [0, 1] function bound01(n, max) { if (isOnePointZero(n)) { n = "100%"; } - + var processPercent = isPercentage(n); n = mathMin(max, mathMax(0, parseFloat(n))); - + // Automatically convert percentage into number if (processPercent) { n = parseInt(n * max, 10) / 100; } - + // Handle floating point rounding errors - if ((math.abs(n - max) < 0.000001)) { + if ((Math.abs(n - max) < 0.000001)) { return 1; } - + // Convert into [0, 1] range if it isn't already return (n % max) / parseFloat(max); } - + // Force a number between 0 and 1 function clamp01(val) { return mathMin(1, mathMax(0, val)); } - + // Parse a base-16 hex value into a base-10 integer function parseIntFromHex(val) { return parseInt(val, 16); } - + // Need to handle 1.0 as 100%, since once it is a number, there is no difference between it and 1 // function isOnePointZero(n) { return typeof n == "string" && n.indexOf('.') != -1 && parseFloat(n) === 1; } - + // Check to see if string passed in is a percentage function isPercentage(n) { return typeof n === "string" && n.indexOf('%') != -1; } - + // Force a hex value to have 2 characters function pad2(c) { return c.length == 1 ? '0' + c : '' + c; } - + // Replace a decimal with it's percentage value function convertToPercentage(n) { if (n <= 1) { n = (n * 100) + "%"; } - + return n; } - + // Converts a decimal to a hex value function convertDecimalToHex(d) { return Math.round(parseFloat(d) * 255).toString(16); @@ -2272,42 +2312,51 @@ function convertHexToDecimal(h) { return (parseIntFromHex(h) / 255); } - + var matchers = (function() { - + // var CSS_INTEGER = "[-\\+]?\\d+%?"; - + // var CSS_NUMBER = "[-\\+]?\\d*\\.\\d+%?"; - + // Allow positive/negative integer/number. Don't capture the either/or, just the entire outcome. var CSS_UNIT = "(?:" + CSS_NUMBER + ")|(?:" + CSS_INTEGER + ")"; - + // Actual matching. // Parentheses and commas are optional, but not required. // Whitespace can take the place of commas or opening paren var PERMISSIVE_MATCH3 = "[\\s|\\(]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")\\s*\\)?"; var PERMISSIVE_MATCH4 = "[\\s|\\(]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")\\s*\\)?"; - + return { + CSS_UNIT: new RegExp(CSS_UNIT), rgb: new RegExp("rgb" + PERMISSIVE_MATCH3), rgba: new RegExp("rgba" + PERMISSIVE_MATCH4), hsl: new RegExp("hsl" + PERMISSIVE_MATCH3), hsla: new RegExp("hsla" + PERMISSIVE_MATCH4), hsv: new RegExp("hsv" + PERMISSIVE_MATCH3), hsva: new RegExp("hsva" + PERMISSIVE_MATCH4), - hex3: /^([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/, - hex6: /^([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/, - hex8: /^([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/ + hex3: /^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/, + hex6: /^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/, + hex4: /^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/, + hex8: /^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/ }; })(); - + + // `isValidCSSUnit` + // Take in a single string / number and check to see if it looks like a CSS unit + // (see `matchers` above for definition). + function isValidCSSUnit(color) { + return !!matchers.CSS_UNIT.exec(color); + } + // `stringInputToObject` // Permissive string parsing. Take in a number of formats, and output an object // based on detected format. Returns `{ r, g, b }` or `{ h, s, l }` or `{ h, s, v}` function stringInputToObject(color) { - + color = color.replace(trimLeft,'').replace(trimRight, '').toLowerCase(); var named = false; if (names[color]) { @@ -2317,7 +2366,7 @@ else if (color == 'transparent') { return { r: 0, g: 0, b: 0, a: 0, format: "name" }; } - + // Try to match string input using regular expressions. // Keep most of the number bounding out of this function - don't worry about [0,1] or [0,100] or [0,360] // Just return an object and let the conversion functions handle that. @@ -2343,10 +2392,10 @@ } if ((match = matchers.hex8.exec(color))) { return { - a: convertHexToDecimal(match[1]), - r: parseIntFromHex(match[2]), - g: parseIntFromHex(match[3]), - b: parseIntFromHex(match[4]), + r: parseIntFromHex(match[1]), + g: parseIntFromHex(match[2]), + b: parseIntFromHex(match[3]), + a: convertHexToDecimal(match[4]), format: named ? "name" : "hex8" }; } @@ -2358,6 +2407,15 @@ format: named ? "name" : "hex" }; } + if ((match = matchers.hex4.exec(color))) { + return { + r: parseIntFromHex(match[1] + '' + match[1]), + g: parseIntFromHex(match[2] + '' + match[2]), + b: parseIntFromHex(match[3] + '' + match[3]), + a: convertHexToDecimal(match[4] + '' + match[4]), + format: named ? "name" : "hex8" + }; + } if ((match = matchers.hex3.exec(color))) { return { r: parseIntFromHex(match[1] + '' + match[1]), @@ -2366,12 +2424,40 @@ format: named ? "name" : "hex" }; } - + return false; } - - window.tinycolor = tinycolor; - })(); + + function validateWCAG2Parms(parms) { + // return valid WCAG2 parms for isReadable. + // If input parms are invalid, return {"level":"AA", "size":"small"} + var level, size; + parms = parms || {"level":"AA", "size":"small"}; + level = (parms.level || "AA").toUpperCase(); + size = (parms.size || "small").toLowerCase(); + if (level !== "AA" && level !== "AAA") { + level = "AA"; + } + if (size !== "small" && size !== "large") { + size = "small"; + } + return {"level":level, "size":size}; + } + + // Node: Export function + if (typeof module !== "undefined" && module.exports) { + module.exports = tinycolor; + } + // AMD/requirejs: Define the module + else if (typeof define === 'function' && define.amd) { + define(function () {return tinycolor;}); + } + // Browser: Expose to window + else { + window.tinycolor = tinycolor; + } + + })(Math); $(function () { if ($.fn.spectrum.load) {