Mercurial > code > home > repos > light9
view light9/web/light9-color-picker.html @ 1794:c97c0ac03597
ok to edit a color by value only
Ignore-this: fcda3510bf9c8bd49c3fb1cf92d5b3c4
author | Drew Perttula <drewp@bigasterisk.com> |
---|---|
date | Thu, 07 Jun 2018 23:04:26 +0000 |
parents | 4bd88d5fcaf8 |
children | 0c54bd6e1630 |
line wrap: on
line source
<link rel="import" href="/lib/polymer/polymer.html"> <link rel="import" href="/lib/paper-slider/paper-slider.html"> <dom-module id="light9-color-picker-float"> <template> <style> :host { z-index: 10; position: fixed; } #largeCrosshair { position: absolute; left: -60px; top: -62px; pointer-events: none; } #largeCrosshair { background: url(/colorpick_crosshair_large.svg); width: 1000px; height: 1000px; } #largeRainbowComp { display: inline-block; overflow: hidden; position: relative; } #largeRainbowComp { position: absolute; border: 4px solid #545454; box-shadow: 8px 11px 40px 0px rgba(0, 0, 0, 0.74); left: 0x; top: 0; } #largeRainbow { background: url(/colorpick_rainbow_large.png); width: 400px; height: 200px; user-select: none; } </style> <div id="largeRainbowComp"> <div id="largeRainbow" on-mousemove="onCanvasMove" on-mouseup="hideLarge" on-mouseleave="hideLarge"></div> <div id="largeCrosshair"></div> </div> </template> <script> class Light9ColorPickerFloat extends Polymer.Element { static get is() { return "light9-color-picker-float"; } // more methods get added by Light9ColorPicker } customElements.define(Light9ColorPickerFloat.is, Light9ColorPickerFloat); </script> </dom-module> <dom-module id="light9-color-picker"> <template> <style> :host { position: relative; display: flex; align-items: center; flex-wrap: wrap; } #smallRainbowComp { display: inline-block; overflow: hidden; position: relative; } #smallRainbow { background: url(/colorpick_rainbow_small.png); width: 150px; height: 30px; } #smallCrosshair { position: absolute; left: -60px; top: -62px; pointer-events: none; } #smallCrosshair { background: url(/colorpick_crosshair_small.svg); /* this can't be too tall, or chrome will cull it in the second column if its top goes above the top of the columns */ width: 400px; height: 60px; } #smallRainbowComp { margin-right: 3px; } paper-slider { width: 170px; } #vee { display: flex; align-items: center; } #large { display: none; } </style> <div id="smallRainbowComp"> <div id="smallRainbow" on-mouseenter="onEnterSmall"></div> <div id="smallCrosshair"></div> </div> <span id="vee"> V: <paper-slider min="0" max="255" step="1" value="{{sliderWriteValue}}" immediate-value="{{value}}"></paper-slider> </span> <!-- Large might span multiple columns, and chrome won't send events for those parts. Workaround: take it out of the columns. --> <light9-color-picker-float id="large"></light9-color-picker-float> </template> <script src="/lib/color/one-color.js"></script> <script> class RainbowCanvas { constructor(url, size) { this.size = size; var elem = document.createElement('canvas'); elem.width = size[0]; elem.height = size[1]; this.ctx = elem.getContext('2d'); this.colorPos = {} // color: pos this._loaded = false; this._loadWatchers = []; // callbacks var img = new Image(); img.onload = function() { this.ctx.drawImage(img, 0, 0); this._readImage(); this._loaded = true; this._loadWatchers.forEach(function(cb) { cb(); }); this._loadWatchers = []; }.bind(this); img.src = url; } onLoad(cb) { // we'll call this when posFor is available if (this._loaded) { cb(); return; } this._loadWatchers.push(cb); } _readImage() { var data = this.ctx.getImageData( 0, 0, this.size[0], this.size[1]).data; for (var y = 0; y < this.size[1]; y+=1) { for (var x = 0; x < this.size[0]; x+=1) { var base = (y * this.size[0] + x) * 4; let px = [data[base + 0], data[base + 1], data[base + 2], 255]; if (px[0] == 0 && px[1] == 0 && px[2] == 0) { // (there's no black on the rainbow images) throw new Error(`color picker canvas (${this.size[0]}) returns 0,0,0`); } var c = one.color(px).hex(); this.colorPos[c] = [x, y]; } } } colorAt(pos) { var data = this.ctx.getImageData(pos[0], pos[1], 1, 1).data; return one.color([data[0], data[1], data[2], 255]).hex(); } posFor(color) { if (color == '#000000') { throw new Error('no match'); } let bright = one.color(color).value(1).hex(); let r = parseInt(bright.substr(1, 2), 16), g = parseInt(bright.substr(3, 2), 16), b = parseInt(bright.substr(5, 2), 16); // We may not have a match for this color exactly (e.g. on // the small image), so we have to search for a near one. // 0, 1, -1, 2, -2, ... let walk = function(x) { return -x + (x > 0 ? 0 : 1); } var radius = 8; for (var dr = 0; dr < radius; dr = walk(dr)) { for (var dg = 0; dg < radius; dg = walk(dg)) { for (var db = 0; db < radius; db = walk(db)) { // Don't need bounds check- out of range // corrupt colors just won't match. color = one.color([r + dr, g + dg, b + db, 255]).hex(); var pos = this.colorPos[color]; if (pos !== undefined) { return pos; } } } } throw new Error('no match'); } } class Light9ColorPicker extends Polymer.Element { static get is() { return "light9-color-picker"; } static get properties() { return { color: { type: String, notify: true }, hueSatColor: { type: String, notify: true, value: null }, value: { type: Number, notify: true }, // 0..255 sliderWriteValue: { type: Number, notify: true }, }; } static get observers() { return [ 'readColor(color)', 'onValue(value)', 'writeColor(hueSatColor, value)' ]; } displayed() { // call this when the smallcrosshair first has a size this._updateSmallCrosshair(); } ready() { super.ready(); if (!window.pickerCanvases) { window.pickerCanvases = { large: new RainbowCanvas( '/colorpick_rainbow_large.png', [400, 200]), small: new RainbowCanvas( '/colorpick_rainbow_small.png', [150, 30]), }; } this.large = window.pickerCanvases.large; this.small = window.pickerCanvases.small; this.small.onLoad(function() { // color may have been set before our image came this._updateSmallCrosshair(); }.bind(this)); this.$.large.onCanvasMove = this.onCanvasMove.bind(this); this.$.large.hideLarge = this.hideLarge.bind(this); document.body.append(this.$.large); this.$.large.style.display = 'none'; } disconnectedCallback() { super.disconnectedCallback(); document.body.removeChild(this.$.large); } onValue(value) { if (this.hueSatColor === null) { this.hueSatColor = '#ffffff'; } let neverBlack = .1 + .9 * value / 255; this.$.smallRainbow.style.filter = `brightness(${neverBlack})`; } writeColor(hueSatColor, value) { if (hueSatColor === null || this.pauseWrites) { return; } this.color = one.color(hueSatColor).value(value / 255).hex(); } readColor(color) { if (this.$.large.style.display == 'block') { // for performance, don't do color searches on covered widget return; } this.pauseWrites = true; var colorValue = one.color(color).value() * 255; // writing back to immediate-value doesn't work on paper-slider this.sliderWriteValue = colorValue; // don't update this if only the value changed, or we desaturate this.hueSatColor = one.color(color).value(1).hex(); this._updateSmallCrosshair(); this.pauseWrites = false; } _updateSmallCrosshair() { try { var pos = this.small.posFor(this.color); } catch(e) { this.moveSmallCrosshair([-999, -999]); return; } this.moveSmallCrosshair(pos); } showLarge(x, y) { this.$.large.style.display = 'block'; try { let pos; try { pos = this.large.posFor(this.color); } catch(e) { pos = [-999, -999]; } this.moveLargeCrosshair(pos); this.$.large.style.left = (x - Math.max(60, Math.min(380, pos[0]))) + 'px'; this.$.large.style.top = (y - Math.max(60, Math.min(180, pos[1]))) + 'px'; } catch(e) { this.moveLargeCrosshair([-999, -999]); this.$.large.style.left = (400 / 2) + 'px'; this.$.large.style.top = (200 / 2) + 'px'; return; } } hideLarge() { this.$.large.style.display = 'none'; if (this.color !== undefined) { this.readColor(this.color); } this.closeTime = Date.now(); } onEnterSmall(ev) { if (this.closeTime && this.closeTime > Date.now() - 500) { return; } // if scrolling put us here, don't open large. require deliberate entering motion. this.showLarge(ev.pageX, ev.pageY); } moveLargeCrosshair(pos, _elem) { _elem = _elem || this.$.large.shadowRoot.querySelector("#largeCrosshair"); _elem.style.left = (pos[0] - _elem.offsetWidth / 2) + 'px'; _elem.style.top = (pos[1] - _elem.offsetHeight / 2) + 'px'; } moveSmallCrosshair(pos) { this.moveLargeCrosshair(pos, this.$.smallCrosshair); } onCanvasMove(ev) { if (ev.buttons != 1) { return; } var canvas = this.$.large.shadowRoot.querySelector('#largeRainbow'); var pos = [ev.offsetX - canvas.offsetLeft, ev.offsetY - canvas.offsetTop]; this.moveLargeCrosshair(pos); this.hueSatColor = this.large.colorAt(pos); // special case: it's useless to adjust the hue/sat of black if (this.value == 0) { this.value = 255; } } } customElements.define(Light9ColorPicker.is, Light9ColorPicker); </script> </dom-module>