Based on http://gka.github.io/palettes/
A Pen by Andreas Borgen on CodePen.
Based on http://gka.github.io/palettes/
A Pen by Andreas Borgen on CodePen.
| <script> | |
| //window.onerror = function(msg, url, line) { alert('Error: '+msg+'\nURL: '+url+'\nLine: '+line); }; | |
| </script> | |
| <script type='text/javascript' src="//cdnjs.cloudflare.com/ajax/libs/chroma-js/0.7.4/chroma.min.js"></script> | |
| <link rel="stylesheet" type="text/css" href="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/2.3.2/css/bootstrap.min.css"> | |
| <link rel="stylesheet" type="text/css" href="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/2.3.2/css/bootstrap-responsive.min.css"> | |
| <div id="main" class="container"> | |
| <header> | |
| <h2>Chroma.js Color Scale Helper</h2> | |
| <p>This <a href="https://github.com/gka/chroma.js" target="_blank">chroma.js</a>-powered tool is here to help us <a target="_blank" href="http://vis4.net/blog/posts/mastering-multi-hued-color-scales/">mastering multi-hued, multi-stops color scales</a>.</p> | |
| <p><a href="https://www.handprint.com/HP/WCL/color2.html#uniquehues" target="_blank">More color theory</a></p> | |
| </header> | |
| <!-- template id="ui-template"--> | |
| <div class="color-scale span10" style="display:none;"> | |
| <div class="form well span10"> | |
| <div class="row"> | |
| <div class="span3"> | |
| <label>Color names or hex codes:</label> | |
| <input class="colors" checked="checked" type="text" value='black, orange, "#f0f", "darkred", yellow' /> | |
| </div> | |
| <div class="span3"> | |
| <label>Step count</label> | |
| <input class="steps" type="number" value="9" /> | |
| </div> | |
| <div class="gradient-wrapper span4"></div> | |
| </div> | |
| <div class="row"> | |
| <div class="span6"> | |
| <label class="checkbox"> | |
| <input class="bez" type="checkbox" checked /> Bezier interpolation | |
| </label> | |
| <br /> | |
| <label class="checkbox"> | |
| <input class="coL" type="checkbox" checked /> Correct lightness gradient | |
| </label> | |
| </div> | |
| <div class="exports-wrapper span4"></div> | |
| </div> | |
| <div class="row"> | |
| <div class="lightness-wrapper span4"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- /template --> | |
| </div> |
| Array.from = Array.from || function(list) { return Array.prototype.slice.call(list); }; | |
| function $$(selector, context) { | |
| context = context || document; | |
| return context.querySelector(selector); | |
| } | |
| function $$$(selector, context) { | |
| context = context || document; | |
| return Array.from(context.querySelectorAll(selector)); | |
| } | |
| function colorScale(ui, is2d) { | |
| "use strict"; | |
| const _2d = is2d, | |
| _colors = $$('.colors', ui), | |
| _steps = $$('.steps', ui), | |
| _bez = $$('.bez', ui), | |
| _coL = $$('.coL', ui), | |
| _gradient = $$('.gradient-wrapper', ui), | |
| _lightness = $$('.lightness-wrapper', ui), | |
| _exports = $$('.exports-wrapper', ui); | |
| $$$('input', ui).forEach(input => { | |
| input.onchange = update; | |
| }); | |
| update(); | |
| function update() { | |
| const colors = clean(_colors.value), | |
| steps = _steps.value, | |
| bez = _bez.checked, | |
| coL = _coL.checked; | |
| _gradient.innerHTML = ''; | |
| _lightness.innerHTML = ''; | |
| _exports.innerHTML = ''; | |
| if(_2d) { | |
| const csTop = createScale([colors[0], colors[1]], bez, coL), | |
| csLeft = createScale([colors[0], colors[2]], bez, coL), | |
| csBottom = createScale([colors[2], colors[3]], bez, coL), | |
| csRight = createScale([colors[1], colors[3]], bez, coL); | |
| showTable(csTop, csLeft, csBottom, csRight, steps); | |
| } | |
| else { | |
| // initialize chroma.scale | |
| const cs = createScale(colors, bez, coL); | |
| // visualize scale | |
| showScale(cs, steps); | |
| } | |
| } | |
| function clean(s) { | |
| return s.match(/[#|\w]+/g); | |
| } | |
| function createScale(colors, bez, coL) { | |
| // initialize chroma.scale | |
| if(bez) colors = chroma.interpolate.bezier(colors); | |
| const cs = chroma.scale(colors).mode('lab').correctLightness(coL); | |
| return cs; | |
| } | |
| function sampleScale(cs, steps) { | |
| var cols = []; | |
| loop(steps, function(i) { | |
| var t = i/(steps-1); | |
| cols.push(cs(t).hex()); | |
| }); | |
| return cols; | |
| } | |
| function showTable(csTop, csLeft, csBottom, csRight, steps) { | |
| var table = createElement('table', _gradient, { class: 'gradient' }); | |
| //Saturation/lightness: | |
| const color = sampleScale(csLeft, 2)[0], | |
| rowCount = Number(steps); //+ 1; | |
| csLeft = createScale([color, color, 'black'], true, false); | |
| csRight = createScale(['white', 'white', '#bbb'], true, false); | |
| var columnFirst = sampleScale(csLeft, rowCount), | |
| columnLast = sampleScale(csRight, rowCount); | |
| for(let row=0; row < rowCount; row++) { | |
| const tr = createElement('tr', table), | |
| rowColors = sampleScale(createScale([columnFirst[row], columnLast[row],columnLast[row]], true, false), steps); | |
| //for(let col=0; col < steps; col++) { | |
| rowColors.forEach(color => { | |
| const td = createElement('td', tr, createAttrs(color)); | |
| //createElement('div', td, { class: 'color', style: 'background:' + color }); | |
| }); | |
| } | |
| } | |
| function showScale(cs, steps) { | |
| var c = createElement('div', _gradient, { class: 'gradient' }); | |
| var cols = sampleScale(cs, steps); | |
| cols.forEach(color => createElement('div', c, createAttrs(color))); | |
| //showLightnessCurve(cs, steps); | |
| var list = '\'' + cols.join('\',\'') + '\'', | |
| colors = cols.join(' '), | |
| hexlist = cols.map(function (c) { return c.replace('#','0x'); }).join(','); | |
| var link = location.href; | |
| //var range = []; | |
| //loop(steps, function(s) { | |
| // range.push('min+'+(s+1)+'*d'); | |
| //}); | |
| //var d3_syntax = 'd3.scale.threshold()\n .range(['+list+']);', | |
| // d3_syntax_full = 'function palette(min, max) {\n var d = (max-min)/'+steps+';\n' + ' return d3.scale.threshold()\n .range(['+list+'])\n .domain(['+range.join(',')+']);\n}'; | |
| export_palette(colors); | |
| export_palette(list); | |
| //export_palette(d3_syntax); | |
| //export_palette(d3_syntax_full); | |
| //export_palette(hexlist); | |
| } | |
| function createAttrs(color) { | |
| return { | |
| class: 'color', | |
| 'data-color': color, | |
| title: color, | |
| style: 'background:' + color, | |
| } | |
| } | |
| function showLightnessCurve(cs, steps) { | |
| const svg = document.createElementNS('http://www.w3.org/2000/svg','svg'), | |
| path = document.createElementNS('http://www.w3.org/2000/svg','path'), | |
| w = 200, margin = 5, dy = 100; | |
| svg.setAttribute('width', w); | |
| svg.setAttribute('height', dy + 2*margin); | |
| const l_steps = []; | |
| loop(steps, function(i) { | |
| var t = i/(steps-1); | |
| l_steps.push( cs(t).lab()[0] ); | |
| }); | |
| const sx = linearScale({ domain: [0,steps], range: [5, w-5] }), | |
| //sy = d3.scale.linear().domain(d3.extent(l_steps)).range([h-5, 5]); | |
| //sy = (y) => y + margin; | |
| sy = (y) => (dy - y) + margin; | |
| let d = ''; | |
| l_steps.forEach(function(l, i) { | |
| var x0 = sx(i), | |
| x1 = sx(i+1), | |
| y = sy(l); | |
| if (d == '') d = 'M'+[x0,y]; | |
| d += ' V'+y; | |
| d += ' H'+x1; | |
| }); | |
| path.setAttribute('d', d); | |
| svg.appendChild(path); | |
| _lightness.appendChild(svg); | |
| } | |
| function export_palette(str) { | |
| createElement('pre', _exports).textContent = str; | |
| } | |
| /* Utils */ | |
| //linearScale.js | |
| //https://gist.github.com/vectorsize/7031902 | |
| function linearScale(opts) { | |
| var istart = opts.domain[0], istop = opts.domain[1], ostart = opts.range[0], ostop = opts.range[1]; | |
| return function scale(value) { return ostart + (ostop - ostart) * ((value - istart) / (istop - istart)); } | |
| } | |
| function loop(iterations, callback) { | |
| for(let i=0; i<iterations; i++) { callback(i); } | |
| } | |
| function createElement(tag, parent, attributes) { | |
| const elm = document.createElement(tag); | |
| if(attributes) { | |
| for(var key in attributes) { elm.setAttribute(key, attributes[key]); } | |
| } | |
| if(parent) { parent.appendChild(elm); } | |
| return elm; | |
| } | |
| } | |
| (function() { | |
| // | |
| const main = $$('#main'), | |
| templ = $$('.color-scale'); //$$('#ui-template'); | |
| function createScaler(colors, steps, adjustLightness, is2d) { | |
| //IE support.. | |
| // //Create color scalers from the template: | |
| // //var ui = document.importNode(templ.content, true); | |
| var ui = templ.cloneNode(true); | |
| ui.style.display = ''; | |
| //https://stackoverflow.com/questions/31855108/get-element-from-document-importnode | |
| // "You must query the contents of the document fragment *before* it's appended to the DOM" | |
| $$('.colors', ui).value = colors; | |
| $$('.steps', ui).value = steps; | |
| $$('.coL', ui).checked = adjustLightness || false; | |
| colorScale(ui, is2d); | |
| main.appendChild(ui); | |
| return ui; | |
| } | |
| //Persepted hues separation: | |
| //createScaler('red, #fd0, yellow', 6/*8*/, true); | |
| createScaler('red, #ff6500, #ffea00, yellow', 6/*8*/); | |
| createScaler('yellow, #ef0, lime', 4); | |
| //createScaler('lime, #0fc, cyan', 3, true); | |
| createScaler('lime, cyan', 4); | |
| createScaler('cyan, #0cf, blue', 6/*8*/); | |
| //createScaler('blue, #c0f, #f0f', 6, true); | |
| createScaler('blue, #5e00ff, #e500ff, #f0f', 5); | |
| createScaler('#f0f, #ff00bb, red', 4/*3*/); | |
| /* Saturation/lightness | |
| createScaler('black, white, white', 8); | |
| createScaler('lime,white,white', 6); | |
| createScaler('lime, black', 6); | |
| createScaler('lime, #bbb, #bbb', 7); | |
| //*/ | |
| //Saturation/lightness tables | |
| //createScaler('yellow,white,black,#bbb', 6, false, true); | |
| const allColors = $$$('.gradient .color:not(:last-child)').map(x => x.dataset.color); | |
| //console.log('hues', allColors); | |
| allColors.forEach(c => createScaler([c,c,c,c].join(','), 6, false, true)); | |
| var huesList = $$$('table.gradient') | |
| .map(t => $$$('tr:not(:last-child)', t) | |
| .map(tr => $$$('td:not(:last-child)', tr).map(td => td.title)) | |
| ); | |
| const grays = createScaler('#bbb,white,black,#bbb', 5, false, true), | |
| graysArr = $$$('tr', grays) | |
| .map(tr => $$$('td', tr).map(td => td.title)); | |
| huesList.push(graysArr); | |
| //console.log('hsv ', huesList); | |
| const huesObj = {}; | |
| huesList.forEach(h => huesObj[h[0][0]] = h); | |
| let output = JSON.stringify(huesObj, null, 4); | |
| output = output.replace( /\[[^[{]*?]/g, (match => match.replace(/\s+/g, ' ')) ); | |
| console.log(`const _hsl = ${output};`); | |
| })(); |
| body { | |
| padding: 20px; | |
| } | |
| header { | |
| text-align: center; | |
| padding-bottom: 1em; | |
| } | |
| h2 { | |
| font-weight: 300; | |
| } | |
| input { | |
| max-width: 100%; | |
| } | |
| label { | |
| white-space: nowrap; | |
| } | |
| label.checkbox { display: inline-block; } | |
| .color-scale { | |
| .well { | |
| padding: 0 1em; | |
| padding-top: 5px; | |
| margin-bottom: .5em; | |
| } | |
| .steps { | |
| text-align: right; | |
| } | |
| .colors { | |
| width: 98%; | |
| } | |
| .gradient { | |
| display: inline-block; | |
| padding: 10px 10px 6px; | |
| border: 1px solid gainsboro; | |
| border-radius: 10px; | |
| background: white; | |
| text-align: center; | |
| white-space: nowrap; | |
| .color { | |
| display: inline-block; | |
| width: 50px; | |
| height: 50px; | |
| color: rgba(0,0,0,0); | |
| } | |
| td.color { | |
| padding: 0; | |
| } | |
| } | |
| svg { | |
| display: block; | |
| margin: 10px 0; | |
| background: #ffa; | |
| path { | |
| stroke: #000; | |
| stroke-width: 2px; | |
| fill: none; | |
| shape-rendering: crispEdges; | |
| } | |
| } | |
| pre { | |
| border: 0; | |
| padding: 0; | |
| margin: 0; | |
| margin-top: .25em; | |
| color: #777; | |
| white-space: nowrap; | |
| } | |
| } |