Page Content
hide
Update Nov2017
UPDATE: parse.com as now been closed and moved to parseplatform.org. I will update the content of this page an move the back-end to this platform
This is an interesting d3.js and parse.com mashup of a number of topics covered on this site.
The target is to build an app that
- Retrieves color scheme data from a parse.com database, much like the app in Color scheme explorer
- Given a target color, either by clicking on some color or entering a hex code, finds the nearest match(es) in the chosen scheme, and creates palettes based on hue, saturation and lightness using a chosen color model such as hsl or lch.
- Uses a D3.js arc partition chart to display the palettes, using color transitions when flipping between charts.
- Provides color properties for any element of the partition chart when hovering over it, much like in Playing around with GAS color
- Picks up the javaScript code for color comparison from Google Apps Script, rather than from a cdn. In other words – takes code out of Google Apps Script, and runs it locally.
- Makes use of jQuery promises to manage all the asynchronous activity.
- Uses toolTipsy to show color properties.
Example
Here’s a snap of the thing. Given an input color the parameters here will
- Use the Pantone Fashion Home color scheme
- Use the LABch color model
- Create palettes for hue, saturation and lightness, consisting only of members of chosen scheme
- Base the palettes on the 4 nearest in scheme colors to the target color
- Pop up color parameter data on hovering over a color
- Clicking on any color in the chart will cause it to be redrawn using the clicked on color as target.
- Changing scheme will show the same chart, but based on the newly selected scheme
Live example here
The Code
<!DOCTYPE HTML> <html> <head> <title>A mashup of parse.com and d3.js</title> <link rel="stylesheet" type="text/css" href="http://xliberation.com/parse/colortable/css/schemer.css"> <link rel="stylesheet" type="text/css" href="//cdn.jsdelivr.net/tipsy/1.0/stylesheets/tipsy.css"> <style> .tiptable TD { color:white; background-color:chocolate; } </style> <script type="text/javascript" src="//www.google.com/jsapi"></script> <script type="text/javascript" src="//d3js.org/d3.v3.min.js"></script> <script src="//ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min.js"></script> <script src="//cdn.jsdelivr.net/tipsy/1.0/javascripts/jquery.tipsy.js"></script> <script src="//www.parsecdn.com/js/parse-1.2.2.min.js"></script> <script src="//xliberation.com/parse/colortable/js/colortable.js"></script> <script src="//xliberation.com/parse/colortable/js/parsch.js"></script> <script type="text/javascript"> // globals var schemeFromParse; </script> <script type="text/javascript"> google.load('visualization', '1'); </script> <script type="text/javascript"> (function(){ google.setOnLoadCallback(function () { // start with a random color $('#hexinput').val('#'+Math.floor(Math.random()*16777215).toString(16)); // the options addSelectOptions($('#schemeselector'),getKnownSchemes()); addSelectOptions($('#modelselector'),getModels()); addSelectOptions($('#maxselector'),[1,2,3,4,5,6,7,8,9,10],4); addSelectOptions($('#swatchselector'),[1,2,3,4,5,6,7,8,9,10],5); addSelectOptions($('#textselector'),["yes","no"],"yes"); addListeners(); // get the useful code from google apps script $('#status').text("...getting google apps script modules"); makeLight("gas","busy"); $.when.apply($,loadModules (["vEquivalents","hacks","usefulColors"],"mcpher")).then ( function () { makeLight("gas","idle"); makeLight("parse","busy"); getSelectedScheme(getChosenScheme().split(":")[0]) .done( function() { makeLight("parse","idle"); changeDetected(); }); }, function (error) { console.log(JSON.stringify(error)); }); }); function getUiOptions (label) { return { swatchSize : getChosenSwatchSize(), maxNearest : getChosenMaxNearest(), scheme : getChosenScheme(), model : getChosenModel(), target: getChosenColor(), label : label ? label : getChosenColor() }; } function fillBar() { // create random palette - different colored headers each time var p = makeColorProps(htmlHexToRgb(getChosenColor())); $('.paint') .css("background-color",p.htmlHex) .css("color",rgbToHex(p.textColor)); return p; } function constructData (options) { // build data for sunburst chart var promise = $.Deferred(); schemeFromParse.done ( function (schemeArray) { // make all the data makeLight("color","busy"); var data = makeAllPalettes(options,schemeArray); promise.resolve(data); makeLight("color","idle"); }); return promise.promise(); } function makeAllPalettes(options,schemeArray) { var props = ["hue", "lightness", "saturation"]; var pBase = makeColorProps(htmlHexToRgb(options.target)); var base = { name : "base" + getConfigKey() , color: pBase, options:options, label:options.label, children :[], size:0 }; // take a sorted copy of the schemearray var nearest = sortClosestColors(pBase.rgb,schemeArray.slice(0)); // need to repeat nearest object for each prop for (var j=0;j<arrayLength(props);j++) { // name type of palette var cBase = { name : "na"+"pa"+j+"sa" + getConfigKey(), color: pBase, prop: props[j], label:props[j], children :[], size: 0 }; base.children.push(cBase); for (var k=0;k<options.maxNearest;k++) { var p = { name : "n" + k + "p" + j + "sb" + getConfigKey(), color: nearest[k].colorProps, prop: props[j], label:nearest[k].label, children :[], size: compareColorProps(nearest[k].colorProps,pBase) } cBase.children.push(p); // now add a palette var palette = makeAPalette(p.color.rgb, options.model, props[j], options.swatchSize, props[j] == "lightness"); // find the nearest match to each item in the palette // each becomes a child to the previous palette member var t = p.children; for (var m =0; m < options.swatchSize ; m++) { var near = sortClosestColors(palette[m].rgb,schemeArray)[0]; t.push ({ name : "n"+k+"p"+j+"s"+m + getConfigKey(), color: near.colorProps, prop: props[j], label: near.label, children :[], size: compareColorProps(near.colorProps,palette[m]) }); t = t[0].children; } } } return base; } function addListeners () { $(".detect").click(function(){ changeDetected(); }); $('#schemeselector').change(function() { makeLight("parse","busy"); getSelectedScheme($(this).val().split(":")[0]) .done( function() { makeLight("parse","idle"); changeDetected(); }); }); } // need this to be accessible globally var mGlob = { height: 600, padding: 5, duration: 1200, counter:0, configKey: "" }; function changeDetected() { // get the new data fillBar(); makeLight("d3","busy"); mGlob.width = Math.min(mGlob.height, $('#d3container').width()); mGlob.radius = mGlob.width/2; var constructPromise = constructData (getUiOptions(getChosenColor())); var svg = mGlob.svg || d3.select("#d3container").append("svg:svg") .attr("width", mGlob.width) .attr("height", mGlob.height) .append("g") .attr("transform", "translate(" + mGlob.width / 2 + "," + mGlob.height /2 + ")"); mGlob.svg = svg; var partition = d3.layout.partition() .sort(null) .size([2 * Math.PI, mGlob.radius]) .value(function(d) { return 1; }); var arc = d3.svg.arc() .startAngle(function(d) { return d.x; }) .endAngle(function(d) { return d.x + d.dx; }) .innerRadius(function(d) { return d.y; }) .outerRadius(function(d) { return d.y + d.dy; }); // transition to it constructPromise.done( function(data) { var nodes = partition.nodes(data); var pathData = svg.selectAll("path").data(nodes,function(d){return d.name;}); var pathEnter = pathData.enter(); var path = pathEnter .append("svg:path") .attr("d", arc) .style("stroke", "#fff") .on("click", click); // start white for rescales if(mGlob.configKey != getConfigKey()) { path.style("fill", "#ffffff"); mGlob.configKey = getConfigKey(); } // add new pathData.transition() .duration(mGlob.counter ? mGlob.duration : 0) .style("fill", function(d) { return d.color.htmlHex; }); // fade out old pathData.exit().transition() .duration(0) .style("fill", function(d) { return "#ffffff"}); // add fancy tool tips $('path').tipsy({ gravity: $.fn.tipsy.autoWE, html: true, delayIn: 1000, title: function() { return makeTipsy(this.__data__); } }); mGlob.counter++; makeLight("d3","idle"); }); function makeTipsy(d) { var c = d.color; return "<div style='background-color:" + c.htmlHex + ";color:" + rgbToHex(c.textColor) + ";'>" + d.label + "(" + getChosenScheme() + ")" + "</div>" + "<table class='tiptable'><tbody>" + makeATableRow ("rgb",c.htmlHex,c.red,c.green,c.blue) + makeATableRow ("lab",c.LStar.toFixed(1),c.aStar.toFixed(1),c.bStar.toFixed(1)) + makeATableRow ("lch",c.LStar.toFixed(1),c.cStar.toFixed(1),c.hStar.toFixed(1)) + makeATableRow ("xyz",c.x.toFixed(1),c.y.toFixed(1),c.z.toFixed(1)) + makeATableRow ("hsl-v",c.hue.toFixed(1),c.saturation.toFixed(1),c.lightness.toFixed(1),c.value.toFixed(1)) + makeATableRow ("cmyk",c.cyan.toFixed(2),c.magenta.toFixed(2),c.yellow.toFixed(2),c.black.toFixed(2)) + makeATableRow ("contrast",c.contrastRatio.toFixed(1),"closeness",d.size.toFixed(2)) + makeATableRow ("model",getChosenModel(),"palette",d.prop) + "</tbody></table>"; } function makeATableRow() { var t= "<tr>"; for (var i = 0; i < arguments.length; i++) { t += "<td>" + arguments[i] + "</td>"; } return t + "</tr>"; } function click(d) { $('#hexinput').val(d.color.htmlHex); changeDetected(); } } function makeLight (what,img) { $('#'+what+'busy').attr('src', "//xliberation.com/cdn/img/" +img + ".gif"); } function getChosenColor() { return $('#hexinput').val(); } function getChosenScheme() { return $('#schemeselector').val(); } function getChosenSwatchSize() { return $('#swatchselector').val(); } function getChosenMaxNearest() { return $('#maxselector').val(); } function getChosenModel() { return $('#modelselector').val(); } function getChosenTextNeeded() { return $('#textselector').val()=="yes"; } function getConfigKey() { // we use this to force a d3 data refresh if dimensions change return "s" + getChosenSwatchSize() + "m" + getChosenMaxNearest() ; } }()); </script> </head> <body> <div id="randombar" class="paint"> <div><h3>d3 sunburst chart from color schemes held in parse.com</h3> <div>Sunburst design based on <a href='http://www.cc.gatech.edu/gvu/ii/sunburst/'>John Stasko.</a> d3.js by <a href='http://bl.ocks.org/mbostock/4063423'>Mike Bostok.</a> Mashup by <a href='https://ramblings.mcpher.com/color-fiesta/find-nearest-color-match/'> Bruce McPherson</a> </div> </div> </div> <div> <div class="label"> <div class = "button detect" id ="go">Apply this hex color or click on a color in the circle</div> <input type="text"" class = "text" id ="hexinput"></input> <div class="text"> gas<img id="gasbusy" class="busy" src="//xliberation.com/cdn/img/idle.gif"></img> parse<img id="parsebusy" class="busy" src="//xliberation.com/cdn/img/idle.gif"></img> color<img id="colorbusy" class="busy" src="//xliberation.com/cdn/img/idle.gif"></img> d3<img id="d3busy" class="busy" src="//xliberation.com/cdn/img/idle.gif"></img> </div> <br> </div> <div class="label">Scheme <select name="schemeselector" id="schemeselector"></select></div> <div class="label detect">Model <select name="modelselector" id="modelselector"></select></div> <div class="label detect">Swatch size <select name="swatchselector" id="swatchselector"></select></div> <div class="label detect">Maximum nearest <select name="maxselector" id="maxselector"></select></div> <div class="text" id="status"></div> <div id="d3container" style-"width:80%"></div> </div> <div id="show" style="width:100%;"></div> </body> </html>
Library
For more about parse.com, see Parse.com
function loadModules(modules, sourceLibrary) { var promises = []; // now does this in one call - returns promise array for backward compat. var url = "https://script.google.com/macros/s/AKfycbzhzIDmgY9BNeBu87puxMVUlMkJ4UkD_Yvjdt5MhOxR1R6RG88/exec"; var u = url + "?source=script&type=javascript&module=" + modules.join() + "&library=" + sourceLibrary; promises.push ($.getScript(u)); return promises; } // we can do this asynch as soon as it's known function getSelectedScheme (scheme) { //get current parse data for scheme - async $('#status').text("...getting selected scheme " + scheme); var parseKeys = getParseKeys(); Parse.initialize(parseKeys.appId, parseKeys.jsKey); var ColorTable = Parse.Object.extend("ColorTable"); schemeFromParse = schemePromise(ColorTable, scheme); schemeFromParse.fail(function (error) { $('#status').text("error getting scheme " + scheme + JSON.stringify(error)); }); schemeFromParse.done(function (results) { $('#status').text("found " + results.length + " colors in scheme " + scheme ); }); return schemeFromParse; } // return in chunks to the parse.com limit function findChunk(model, scheme, allData) { // we have to find in chunks since there is a limit on query size // will return a promise var limit = 1000; var skip = allData.length; var findPromise = $.Deferred(); var query = new Parse.Query(model); if (scheme) query.equalTo("scheme", scheme); query .limit(limit) .skip(skip) .find() .then(function (results) { findPromise.resolve(allData.concat(results), !results.length); }, function (error) { findPromise.reject(error); }); return findPromise.promise(); } // get the data for chosen scheme from parse function schemePromise(model, scheme, allResults, allPromise) { // find a scheme at a time var schemeArray = []; var promise = allPromise || $.Deferred(); findChunk(model, scheme, allResults || []) .done(function (results, allOver) { if (allOver) { // we are done // normalize to an array from parse ob for (var i =0;i<results.length;i++) { var hex= results[i].get("hex"); // we'll calculate color props since we'll need them later anyway. so its just one time try { var x = parseInt(Right(hex, Len(hex) - 1),16); schemeArray.push({label: results[i].get("label"), hex: hex, scheme: results[i].get("scheme"), code: results[i].get("code"), key: results[i].get("key"), colorProps: makeColorProps(htmlHexToRgb(hex)) }); } catch(err) { console.log(err + " converting scheme color hex " + hex); } } promise.resolve(schemeArray); } else { // may be more schemePromise(model, scheme, results, promise); } }) .fail(function (error) { promise.reject(error); }); return promise.promise(); } function sortClosestColors (targetRgb, schemeArray) { var targetProps = makeColorProps (targetRgb); for (var i= 0; i < schemeArray.length ; i++) { schemeArray[i].comparison = compareColorProps (targetProps, schemeArray[i].colorProps); } return schemeArray.sort (comparisonSort); } function comparisonSort(a,b) { if (a.comparison < b.comparison) return -1; if (a.comparison > b.comparison) return 1; return 0; } function addSelectOptions(selectElem,selectValues,defaultValue) { for (var i = 0; i < selectValues.length; i++ ) { selectElem .append($("<option/>", { value: selectValues[i], text: selectValues[i] })); } if (!(typeof defaultValue == 'undefined'))selectElem.val(defaultValue); } function getKnownSchemes() { // there isnt a unique values type of structure, so we'll need to do something better later return ["pfh:Pantone fashion home","pms:Pantone match system", "dulux:Dulux paints","htm:Html Colors","raf:airplane colors"]; } function getModels() { return ["lch","hsl"]; }
GAS code
The color manipulation modules are hosted on and served up by Google Apps Script. For more about color functions see Playing around with GAS color
//this is all about colors var VBCOLORS = Object.freeze( {vbBlue: 16711680, vbWhite: 16777215, vbBlack: 0, vbGreen: 65280, vbYellow: 65535, vbRed:255 } ); var ECOMPARECOLOR = Object.freeze ( { whiteX : 95.047, whiteY : 100, whiteZ : 108.883, eccieDe2000 : 21000, beige: 10009301 } ); function speedTest() { useTimer('x','10000 iterations of color comparison').start(); for (var i = 1 ; i <= 10000 ;i++) { compareColors (i, VBCOLORS.vbWhite - i); } useTimer('x').stop(); return useTimer().report(); } function testCompareSpeed() { Logger.log(speedTest()); } function beigeness(rgbColor) { return compareColors (rgbColor, ECOMPARECOLOR.beige); } function RGB(r,g,b) { return Math.round(r) + (Math.round(g) << 8) + (Math.round(b) << 16) ; } function rgbRed(rgbColor) { return rgbColor % 0x100; } function rgbGreen(rgbColor) { return Math.floor(rgbColor / 0x100) % 0x100; } function rgbBlue(rgbColor) { return Math.floor(rgbColor / 0x10000) % 0x100; } function heatmapColor(min , max, value) { return rampLibraryRGB("heatmap", min, max, value); } function rgbToHTMLHex(rgbColor) { // just swap the colors round for rgb to bgr as bit representation is reversed return "#" + maskFormat(RGB(rgbBlue(rgbColor), rgbGreen(rgbColor), rgbRed(rgbColor)).toString(16), "000000"); } function rgbToHex(rgbColor) { // this is just a convenience pass throuh .. i originally screwed up the camel case return rgbToHTMLHex(rgbColor); } function rampLibraryHex (sName, min , max, value , optBrighten) { return rgbToHTMLHex(rampLibraryRGB (sName, min , max, value , optBrighten)); } function maskFormat(sIn , f ) { var s = Trim(sIn); if (Len(s) < Len(f)) { s = Left(f, Len(f) - Len(s)) + s ; } return s; } function lumRGB(rgbCom, brighten) { var x = rgbCom * brighten; return x > 255 ? 255 : x < 0 ? 0 : x; } function contrastRatio(rgbColorA, rgbColorB) { var lumA = w3Luminance(rgbColorA); var lumB = w3Luminance(rgbColorB); return (Math.max(lumA, lumB) + 0.05) / (Math.min(lumA, lumB) + 0.05); } function makeColorProps (rgbColor) { return new colorProps().populate(rgbColor); } function colorProps() { return this; } colorProps.prototype.populate = function (rgbColor) { var p = this; //store the source color p.rgb = rgbColor; //split the components p.red = rgbRed(rgbColor); p.green = rgbGreen(rgbColor); p.blue = rgbBlue(rgbColor); //the html hex rgb equivalent p.htmlHex = rgbToHTMLHex(rgbColor); //the w3 algo for luminance p.luminance = w3Luminance(rgbColor); //determine whether black or white background if (p.luminance < 0.5) p.textColor = VBCOLORS.vbWhite; else p.textColor = VBCOLORS.vbBlack; //contrast ratio - to comply with w3 recs 1.4 should be at least 10:1 for text p.contrastRatio = contrastRatio(p.textColor, p.rgb); //cmyk - just an estimate p.black = Math.min(Math.min(1 - p.red / 255, 1 - p.green / 255), 1 - p.blue / 255); if (p.black < 1) { p.cyan = (1 - p.red / 255 - p.black) / (1 - p.black); p.magenta = (1 - p.green / 255 - p.black) / (1 - p.black); p.yellow = (1 - p.blue / 255 - p.black) / (1 - p.black); } // calculate hsl + hsv and other wierd things var p2 = rgbToHsl(p.rgb); p.hue = p2.hue; p.saturation = p2.saturation; p.lightness = p2.lightness; p.value = rgbToHsv(p.rgb).value; p2 = rgbToXyz(p.rgb); p.x = p2.x; p.y = p2.y; p.z = p2.z; p2 = rgbToLab(p.rgb); p.LStar = p2.LStar; p.aStar = p2.aStar; p.bStar = p2.bStar; p2 = rgbToLch(p.rgb); p.cStar = p2.cStar; p.hStar = p2.hStar; return p; } function w3Luminance (rgbColor) { // this is based on // http://en.wikipedia.org/wiki/Luma_(video) return (0.2126 * Math.pow((rgbRed(rgbColor)/255),2.2)) + (0.7152 * Math.pow((rgbGreen(rgbColor)/255),2.2)) + (0.0722 * Math.pow((rgbBlue(rgbColor)/255),2.2)) ; } function cellProperty (r,p) { // makes it compatible with VBA version switch(p) { case "background-color": return sheetCache(r,"getBackgroundColors").getValues(r); case "color": return sheetCache(r,"getFontColor").getValues(r); case "font-size": return sheetCache(r,"getFontSize").getValues(r); default: DebugAssert (false, "unknown cellproperty request " + p); } } function cellCss(r, p ) { return p + ":" + cellProperty(r, p) + ";" ; } function rgbExpose(r , g , b ) { // redundadnt .. just for compatibility with vba return RGB(r, g, b) ; } function htmlHexToRgb(htmlHex){ var s = LTrim(RTrim(htmlHex)); s = (Left(s, 1) == "#" ? '' : '#') + s; DebugAssert (Len(s) > 1 && Left(s, 1) == "#", "invalid hex color" + htmlHex); // -- need to find equivalent --- var x = parseInt(Right(s, Len(s) - 1),16); // these are purposefully reversed since byte order is different in unix return RGB(rgbBlue(x), rgbGreen(x), rgbRed(x)); } function hslToRgb(p) { // from // http://www.easyrgb.com/ var x1 , x2 , h, s , l , red , green , blue ; h = p.hue / 360; s = p.saturation / 100; l = p.lightness / 100; if (s == 0) { return RGB (l * 255, l * 255, l * 255); } else { if (l < 0.5 ) x2 = l * (1 + s); else x2 = (l + s) - (l * s); x1 = 2 * l - x2; red = 255 * hueToRgb(x1, x2, h + (1 / 3)); green = 255 * hueToRgb(x1, x2, h); blue = 255 * hueToRgb(x1, x2, h - (1 / 3)); return RGB (red, green, blue); } } function hueToRgb(a , b , h ) { // from // http://www.easyrgb.com/ if (h < 0) h = h + 1; if (h > 1) h = h - 1; DebugAssert (h >= 0 && h <= 1,"hue outside range 0-1:" + h); if (6 * h < 1) return a + (b - a) * 6 * h; else { if (2 * h < 1) return b; else { if (3 * h < 2) return a + (b - a) * ((2 / 3) - h) * 6; else return a; } } } function hexColorOf(r) { return cellProperty(r,"color") ; } function colorizeCell(target, c ) { if (Len(c) > 1 && Left(c, 1) == "#") { p = makeColorProps(htmlHexToRgb(c)); //target.Interior.Color = p.rgb //target.Font.Color = p.textColor } } function rampLibraryRGB(ramp, min , max, value , optBrighten) { var brighten = fixOptional (optBrighten, 1); if (IsArray(ramp)) { // ramp colors have been passed here return colorRamp(min, max, value, ramp,undefined , brighten); } else { switch(Trim(LCase(ramp))) { case "heatmaptowhite": return colorRamp(min, max, value, [VBCOLORS.vbBlue, VBCOLORS.vbGreen, VBCOLORS.vbYellow, VBCOLORS.vbRed, VBCOLORS.vbWhite],undefined , brighten); case "heatmap": return colorRamp(min, max, value, [VBCOLORS.vbBlue, VBCOLORS.vbGreen, VBCOLORS.vbYellow, VBCOLORS.vbRed], undefined, brighten); case "blacktowhite": return colorRamp(min, max, value, [VBCOLORS.vbBlack, VBCOLORS.vbWhite], undefined, brighten); case "whitetoblack": return colorRamp(min, max, value, [VBCOLORS.vbWhite, VBCOLORS.vbBlack],undefined , brighten); case "hotinthemiddle": return colorRamp(min, max, value, [VBCOLORS.vbBlue, VBCOLORS.vbGreen, VBCOLORS.vbYellow,VBCOLORS.vbRed,VBCOLORS.vbGreen, VBCOLORS.vbBlue], undefined, brighten); case "candylime": return colorRamp(min, max, value, [RGB(255, 77, 121), RGB(255, 121, 77), RGB(255, 210, 77), RGB(210, 255, 77)], undefined, brighten); case "heatcolorblind": return colorRamp(min, max, value, [VBCOLORS.vbBlack,VBCOLORS.vbBlue, VBCOLORS.vbRed, VBCOLORS.vbWhite], undefined, brighten); case "gethotquick": return colorRamp(min, max, value, [VBCOLORS.vbBlue,VBCOLORS.vbGreen, VBCOLORS.vbYellow, VBCOLORS.vbRed],[0, 0.1, 0.25, 1] , brighten); case "greensweep": return colorRamp(min, max, value, [RGB(153, 204, 51), RGB(51, 204, 179)] ,undefined , brighten); case "terrain": return colorRamp(min, max, value, [VBCOLORS.vbBlack, RGB(0, 46, 184), RGB(0, 138, 184), RGB(0, 184, 138), RGB(138, 184, 0), RGB(184, 138, 0), RGB(138, 0, 184), VBCOLORS.vbWhite] ,undefined , brighten); case "terrainnosea": return colorRamp(min, max, value, [VBCOLORS.vbGreen, RGB(0, 184, 138), RGB(138, 184, 0), RGB(184, 138, 0), RGB(138, 0, 184), VBCOLORS.vbWhite] , undefined, brighten); case "greendollar": return colorRamp(min, max, value, [RGB(225, 255, 235), RGB(2, 202, 69)], undefined, brighten); case "lightblue": return colorRamp(min, max, value, [RGB(230, 237, 246), RGB(163, 189, 271)], undefined, brighten); case "lightorange": return colorRamp(min, max, value, [rgb(253, 233, 217), rgb(244, 132, 40)], undefined, brighten); default: DebugAssert(false,"Unknown library entry " + ramp) ; } } } function colorRamp(min, max , value , mileStones , fractionStones, optBrighten) { // color ramp given or default var brighten = fixOptional ( optBrighten,1); var ms = IsMissing(mileStones) ? [VBCOLORS.vbBlue,VBCOLORS.vbGreen,VBCOLORS.vbYellow, VBCOLORS.vbRed,VBCOLORS.vbRed,VBCOLORS.vbWhite] : mileStones; DebugAssert( ms.length , "No milestone colors specified"); // only 1 milestone - thats the color if (ms.length == 1) return ms[lb]; // fractions of range at which to apply these colors var fs = []; if (!IsMissing(fractionStones)) { DebugAssert( fractionStones.length == ms.length, "no of fractions must equal number of steps" ); fs = fractionStones; } else { // equal proportions fs[0]=0; for (var i = 1 ; i < ms.length ; i++ ) fs[i] = i/(ms.length-1) ; } // now calculate the color var spread = max - min; DebugAssert (spread >= 0 , "min is greater than max for color spread"); var ratio = (value - min) / spread; DebugAssert (ratio >= 0 && ratio <= 1, "couldnt calculate ratio for color spread"); //find which slot this value belongs in for (var i = 1; i < ms.length;i++) { if (ratio <= fs[i]) { var r = (ratio - fs[i - 1]) / (fs[i] - fs[i - 1]); var red = rgbRed(ms[i - 1]) + (rgbRed(ms[i]) - rgbRed(ms[i - 1])) * r; var blue = rgbBlue(ms[i - 1]) + (rgbBlue(ms[i]) - rgbBlue(ms[i - 1])) * r; var green = rgbGreen(ms[i - 1]) + (rgbGreen(ms[i]) - rgbGreen(ms[i - 1])) * r; return RGB(lumRGB(red, brighten), lumRGB(green, brighten), lumRGB(blue, brighten)); } } DebugAssert (false,"ColorRamp failed to work - dont know why"); } function rgbToHsl (RGBcolor) { //from // http://www.easyrgb.com/ var r , g , b , d , dr , dg , db , mn , mx , p ={}; r = rgbRed(RGBcolor) / 255 ; g = rgbGreen(RGBcolor) / 255; b = rgbBlue(RGBcolor) / 255; mn = Math.min(Math.min(r, g), b); mx = Math.max(Math.max(r, g), b); d = mx - mn; //HSL sets here p.hue = 0; p.saturation = 0; //lightness p.lightness = (mx + mn) / 2; if (d != 0) { // saturation if (p.lightness < 0.5) p.saturation = d / (mx + mn) ; else p.saturation = d / (2 - mx - mn) ; // hue dr = (((mx - r) / 6) + (d / 2)) / d ; dg = (((mx - g) / 6) + (d / 2)) / d ; db = (((mx - b) / 6) + (d / 2)) / d ; if (r == mx) p.hue = db - dg ; else if(g == mx) p.hue = (1 / 3) + dr - db ; else p.hue = (2 / 3) + dg - dr ; //force between 0 and 1 if (p.hue < 0) p.hue = p.hue + 1 ; if (p.hue > 1) p.hue = p.hue - 1 ; if (!(p.hue >= 0 && p.hue <= 1)) p.hue = 0; // " invalid hue " + p.hue + ":" + JSON.stringify(p)); } p.hue = p.hue * 360 ; p.saturation = p.saturation * 100 ; p.lightness = p.lightness * 100 ; return p; } function rgbToHsv(rgbColor){ // adapted from // http://www.easyrgb.com/ var r = rgbRed(rgbColor) / 255; var g = rgbGreen(rgbColor) / 255; var b = rgbBlue(rgbColor) / 255; var mn = Math.min(r, g, b); var mx = Math.max(r, g, b); // this is the same as hsl and hsv are the same. var p = rgbToHsl(rgbColor); // HSV sets here p.value = mx; return p; } function xyzCorrection(v) { if (v > 0.04045) return Math.pow( ((v + 0.055) / 1.055) , 2.4); else return v / 12.92 ; } function xyzCieCorrection(v) { return v > 0.008856 ? Math.pow(v , 1 / 3) : (7.787 * v) + (16 / 116); } function rgbToXyz(rgbColor) { // adapted from // http://www.easyrgb.com/ var r = xyzCorrection(rgbRed(rgbColor) / 255) * 100; var g = xyzCorrection(rgbGreen(rgbColor) / 255) * 100; var b = xyzCorrection(rgbBlue(rgbColor) / 255) * 100; var p = new colorProps(); p.x = r * 0.4124 + g * 0.3576 + b * 0.1805; p.y = r * 0.2126 + g * 0.7152 + b * 0.0722; p.z = r * 0.0193 + g * 0.1192 + b * 0.9505; return p; } function rgbToLab(rgbColor) { // adapted from // http://www.easyrgb.com/ var p = rgbToXyz(rgbColor); var x = xyzCieCorrection(p.x / ECOMPARECOLOR.whiteX); var y = xyzCieCorrection(p.y / ECOMPARECOLOR.whiteY); var z = xyzCieCorrection(p.z / ECOMPARECOLOR.whiteZ); p.LStar = (116 * y) - 16; p.aStar = 500 * (x - y); p.bStar = 200 * (y - z); return p; } function compareColorProps (p1, p2 , optCompareType) { switch (fixOptional(optCompareType, ECOMPARECOLOR.eccieDe2000)) { case ECOMPARECOLOR.eccieDe2000: var t= cieDe2000(p1, p2); p1 = p2 = null; return t; default: DebugAssert (false, "unknown color comparision " + optCompareType); } } function compareColors(rgb1, rgb2 , optCompareType) { return compareColorProps(makeColorProps(rgb1),makeColorProps(rgb2)); } function cieDe2000(p1, p2 ) { // calculates the distance between 2 colors using CIEDE200 // see http://www.ece.rochester.edu/~gsharma/cieDe2000/cieDe2000noteCRNA.pdf var kp = Math.pow(25 , 7), kl = 1,kc = 1, kh = 1; // calculate c & g values var c1 = Math.sqrt(Math.pow(p1.aStar , 2) + Math.pow(p1.bStar , 2)); var c2 = Math.sqrt(Math.pow(p2.aStar , 2) + Math.pow(p2.bStar , 2)); var c = (c1 + c2) / 2; var g = 0.5 * (1 - Math.sqrt(Math.pow(c , 7) / (Math.pow(c , 7) + kp))); //adjusted ab* var a1 = (1 + g) * p1.aStar; var a2 = (1 + g) * p2.aStar; // adjusted cs var c1Tick = Math.sqrt(a1 *a1 + p1.bStar *p1.bStar); var c2Tick = Math.sqrt(a2 *a2 + p2.bStar * p2.bStar); //adjusted h var h1 = computeH(a1, p1.bStar); var h2 = computeH(a2, p2.bStar); // deltas var dh; if (h2 - h1 > 180) dh = h2 - h1 - 360; else if (h2 - h1 < -180) dh = h2 - h1 + 360 ; else dh = h2 - h1; var dl = p2.LStar - p1.LStar; var dc = c2Tick - c1Tick; var dBigH = (2 * Math.sqrt(c1Tick * c2Tick) * Math.sin(toRadians(dh / 2))); // averages var lTickAvg = (p1.LStar + p2.LStar) / 2; var cTickAvg = (c1Tick + c2Tick) / 2; var hTickAvg; if (c1Tick * c2Tick == 0) hTickAvg = h1 + h2; else if (Math.abs(h2 - h1) <= 180) hTickAvg = (h1 + h2) / 2; else if (h2 + h1 < 360) hTickAvg = (h1 + h2) / 2 + 180; else hTickAvg = (h1 + h2) / 2 - 180; var l50 = Math.pow(lTickAvg - 50,2); var sl = 1 + (0.015 * l50 / Math.sqrt(20 + l50)); var sc = 1 + 0.045 * cTickAvg; var t = 1 - 0.17 * Math.cos(toRadians(hTickAvg - 30)) + 0.24 * Math.cos(toRadians(2 * hTickAvg)) + 0.32 * Math.cos(toRadians(3 * hTickAvg + 6)) - 0.2 * Math.cos(toRadians(4 * hTickAvg - 63)); var sh = 1 + 0.015 * cTickAvg * t; var dTheta = 30 * Math.exp(-1 * Math.pow((hTickAvg - 275) / 25 , 2)); var rc = 2 * Math.sqrt(Math.pow(cTickAvg , 7) / (Math.pow(cTickAvg , 7) + kp)); var rt = -Math.sin(toRadians(2 * dTheta)) * rc; var dlk = dl / sl / kl; var dck = dc / sc / kc; var dhk = dBigH / sh / kh; return Math.sqrt(dlk *dlk + dck *dck + dhk *dhk + rt * dck * dhk); } function computeH(a , b ) { if (a == 0 && b == 0) return 0; else if (b < 0) return fromRadians(Atan2(a,b)) + 360 ; else return fromRadians(Atan2(a,b)) ; } function lchToLab (p) { var h = toRadians(p.hStar); p.aStar = Math.cos(h) * p.cStar; p.bStar = Math.sin(h) * p.cStar; return p; } function labxyzCorrection(x ) { if (Math.pow(x , 3) > 0.008856) return Math.pow(x , 3); else return (x - 16 / 116) / 7.787; } function lchToRgb(p) { return xyzToRgb(labToXyz(lchToLab(p))); } function labToXyz(p) { p.y = (p.LStar + 16) / 116; p.x = p.aStar / 500 + p.y; p.z = p.y - p.bStar / 200; p.x = labxyzCorrection(p.x) * ECOMPARECOLOR.whiteX; p.y = labxyzCorrection(p.y) * ECOMPARECOLOR.whiteY; p.z = labxyzCorrection(p.z) * ECOMPARECOLOR.whiteZ; return p; } function xyzrgbCorrection(x) { if (x > 0.0031308) return 1.055 * (Math.pow(x , (1 / 2.4))) - 0.055; else return 12.92 * x; } function xyzToRgb(p) { var x = p.x / 100, y = p.y / 100 ,z = p.z / 100; var x1 = x * 0.8951 + y * 0.2664 + z * -0.1614; var y1 = x * -0.7502 + y * 1.7135 + z * 0.0367; var z1 = x * 0.0389 + y * -0.0685 + z * 1.0296; var x2 = x1 * 0.98699 + y1 * -0.14705 + z1 * 0.15997; var y2 = x1 * 0.43231 + y1 * 0.51836 + z1 * 0.04929; var z2 = x1 * -0.00853 + y1 * 0.04004 + z1 * 0.96849; r = xyzrgbCorrection(x2 * 3.240479 + y2 * -1.53715 + z2 * -0.498535); g = xyzrgbCorrection(x2 * -0.969256 + y2 * 1.875992 + z2 * 0.041556); b = xyzrgbCorrection(x2 * 0.055648 + y2 * -0.204043 + z2 * 1.057311); var c = RGB(Math.min(255, Math.max(0, CLng(r * 255))), Math.min(255, Math.max(0, CLng(g * 255))), Math.min(255, Math.max(0, CLng(b * 255)))); return c; } function rgbToLch(rgbColor) { //convert from cieL*a*b* to cieL*CH //adapted from http://www.brucelindbloom.com/index.html?Equations.html var p = rgbToLab(rgbColor); if (rgbColor == 0 ) p.hStar = 0 ; else { p.hStar =Atan2(p.aStar, p.bStar); if (p.hStar > 0) p.hStar = fromRadians(p.hStar); else p.hStar = 360 - fromRadians(Math.abs(p.hStar)); } p.cStar = Math.sqrt(p.aStar * p.aStar + p.bStar * p.bStar); return p; } function colorPropBigger(a, b, byProp) { DebugAssert (a.hasOwnProperty(byProp) && b.hasOwnProperty(byProp),"unknown color prop in sort " + byProp); return a[byProp] > b[byProp]; } function rgbWashout(rgbColor) { var p = makeColorProps(rgbColor); p.saturation = p.saturation * 0.2; p.lightness = p.lightness * 0.9; return hslToRgb(p) } function sortColorProp(pArray, byProp,optDescending ) { descending = fixOptional (optDescending ,false) ? -1 : 1; return pArray.sort (function (a,b) { var result = a[byProp] > b[byProp] ? 1 : a[byProp] < b[byProp] ? -1 : 0 ; return descending * result ; } ); } function makeAPalette(rgbColor, optModel, optiType, optHowMany, optDescending) { var model = fixOptional(optModel, "lch"); var iType = fixOptional(optiType, "hue"); var howMany = fixOptional(optHowMany, 5); var ph,ps,pl,pf,h,pv,a=[],g; if (model == "lch") { ph = "hStar", ps = "cStar", pl = "LStar", pf = lchToRgb; } else { ph = "hue", ps = "saturation", pl = "lightness", pf = hslToRgb; } var top = (iType == "hue" ? 360 : 100); g = top / howMany; pv = (iType == "hue" ? ph : (iType == "saturation" ? ps : pl)); var p = makeColorProps(rgbColor); h = p[pv]; // do a number of equally spaced props and store in array for (var i =0;i < howMany ;i++ ) { if (h > top) h -= top; // store new value p[pv] =h; // convert back to rgb and redo p =makeColorProps(pf(p)); a.push(p); // make a new copy p = makeColorProps(p.rgb); h += g; } return sortColorProp (a, pv, optDescending); } function arrayLength(a) { return a.length; }
For more color stuff, see Color Fiesta