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>