Changeset - 650375a47213
[Not reviewed]
default
1 0 1
drewp@bigasterisk.com - 20 months ago 2023-05-25 18:47:20
drewp@bigasterisk.com
start color-picker port from polymer to lit
1 file changed with 331 insertions and 332 deletions:
0 comments (0 inline, 0 general)
light9/web/light9-color-picker.ts
Show inline comments
 
file renamed from light9/web/light9-color-picker.html to light9/web/light9-color-picker.ts
 
<link rel="import" href="/lib/polymer/polymer.html">
 
<link rel="import" href="/lib/paper-slider/paper-slider.html">
 
import debug from "debug";
 
import { css, html, LitElement, PropertyValues } from "lit";
 
import { customElement, property } from "lit/decorators.js";
 
import { Literal, NamedNode } from "n3";
 
import { SyncedGraph } from "../web/SyncedGraph";
 
import { ControlValue } from "./Effect";
 
import { GraphToControls } from "./GraphToControls";
 
import { DeviceAttrRow } from "./Light9DeviceControl";
 
import { Choice } from "./Light9Listbox";
 
import { SubEvent } from "sub-events";
 

	
 
//<script src="/lib/color/one-color.js"></script>
 
const log = debug("control");
 

	
 
<dom-module id="light9-color-picker-float">
 
  <template>
 
    <style>
 
     :host {
 
         z-index: 10;
 
         position: fixed;
 
         width: 400px;
 
         height: 200px;
 
         border: 10px solid #000;
 
         box-shadow: 8px 11px 40px 0px rgba(0, 0, 0, 0.74);
 
         /* This display (and border-color) are replaced later. */
 
         display: none;
 
     }
 
     #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;
 
         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"
 
      ></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>
 
@customElement("light9-color-picker-float")
 
export class Light9ColorPickerFloat extends LitElement {
 
  static styles = [
 
    css`
 
      :host {
 
        z-index: 10;
 
        position: fixed;
 
        width: 400px;
 
        height: 200px;
 
        border: 10px solid #000;
 
        box-shadow: 8px 11px 40px 0px rgba(0, 0, 0, 0.74);
 
        /* This display (and border-color) are replaced later. */
 
        display: none;
 
      }
 
      #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;
 
        left: 0x;
 
        top: 0;
 
      }
 
      #largeRainbow {
 
        background: url(/colorpick_rainbow_large.png);
 
        width: 400px;
 
        height: 200px;
 
        user-select: none;
 
      }
 
    `,
 
  ];
 
  render() {
 
    return html`
 
      <div id="largeRainbowComp">
 
        <div id="largeRainbow" on-mousemove="onCanvasMove" on-mouseup="hideLarge"></div>
 
        <div id="largeCrosshair"></div>
 
      </div>
 
    `;
 
  }
 
  // more methods get added by Light9ColorPicker
 
}
 

	
 
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
 

	
 
<dom-module id="light9-color-picker">
 
  <template>
 
    <style>
 
     :host {
 
         position: relative;
 
         display: flex;
 
         align-items: center;
 
         flex-wrap: wrap;
 
         user-select: none;
 
     }
 
     
 
     #swatch {
 
         display: inline-block;
 
         width: 50px; 
 
         height: 30px;
 
         margin-right: 3px;
 
         border: 1px solid #333;
 
     }
 
     
 
     paper-slider {
 
         width: 160px;
 
     }
 
     
 
     #vee {
 
         display: flex;
 
         align-items: center;
 
     }
 
     
 
     #outOfBounds {
 
         user-select: none;
 
         z-index: 1;
 
         background: #00000060;
 
         position: fixed;
 
         left: 0;
 
         top: 0;
 
         width: 100%;
 
         height: 100%;
 
         display: none; /* Toggledlater. */
 
     }
 
    </style>
 
    <div id="swatch" style="background-color: {{color}}"
 
         on-mousedown="onDownSmall"></div>
 
    <span id="vee">
 
      V:
 
      <paper-slider min="0"
 
                    max="255"
 
                    step="1"
 
                    value="{{sliderWriteValue}}"
 
                    immediate-value="{{value}}"></paper-slider>
 
    </span>
 
    <!-- Temporary scrim on the rest of the page. It looks like we're dimming
 
         the page to look pretty, but really this is so we can track the mouse
 
         when it's outside the large canvas. -->
 
    <div id="outOfBounds"
 
         on-mousemove="onOutOfBoundsMove"
 
         on-mouseup="hideLarge"></div>
 
    <!--  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');
 
    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");
 
  }
 
}
 

	
 
@customElement("light9-color-picker")
 
export class Light9ColorPicker extends LitElement {
 
  static styles = [
 
    css`
 
      :host {
 
        position: relative;
 
        display: flex;
 
        align-items: center;
 
        flex-wrap: wrap;
 
        user-select: none;
 
      }
 

	
 
       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');
 
     }
 
   }
 
      #swatch {
 
        display: inline-block;
 
        width: 50px;
 
        height: 30px;
 
        margin-right: 3px;
 
        border: 1px solid #333;
 
      }
 

	
 
      paper-slider {
 
        width: 160px;
 
      }
 

	
 
      #vee {
 
        display: flex;
 
        align-items: center;
 
      }
 

	
 
   
 
   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)'
 
     ]; }
 
     ready() {
 
       super.ready();
 
       if (!window.pickerCanvases) {
 
         window.pickerCanvases = {
 
           large: new RainbowCanvas(
 
             '/colorpick_rainbow_large.png', [400, 200]),           
 
         };
 
       }
 
       this.large = window.pickerCanvases.large;
 
       this.$.large.onCanvasMove = this.onCanvasMove.bind(this);
 
       this.$.large.hideLarge = this.hideLarge.bind(this);
 
       document.body.append(this.$.large);
 
     }
 
     disconnectedCallback() {
 
       super.disconnectedCallback();
 
       document.body.removeChild(this.$.large);
 
     }
 
     onValue(value) {
 
       if (this.hueSatColor === null) {
 
         this.hueSatColor = '#ffffff';
 
       }
 
       let neverBlack = .1 + .9 * value / 255;
 
       this.$.swatch.style.filter = `brightness(${neverBlack})`;
 
     }
 
     writeColor(hueSatColor, value) {
 
       if (hueSatColor === null || this.pauseWrites) { return; }
 
       this.color = one.color(hueSatColor).value(value / 255).hex();
 
       this.$.large.style.borderColor = this.color;
 
     }
 
     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();
 
      #outOfBounds {
 
        user-select: none;
 
        z-index: 1;
 
        background: #00000060;
 
        position: fixed;
 
        left: 0;
 
        top: 0;
 
        width: 100%;
 
        height: 100%;
 
        display: none; /* Toggledlater. */
 
      }
 
    `,
 
  ];
 
  render() {
 
    return html`
 
      <div id="swatch" style="background-color: {{color}}" on-mousedown="onDownSmall"></div>
 
      <span id="vee">
 
        V:
 
        <paper-slider min="0" max="255" step="1" value="{{sliderWriteValue}}" immediate-value="{{value}}"></paper-slider>
 
      </span>
 
      <!-- Temporary scrim on the rest of the page. It looks like we're dimming
 
        the page to look pretty, but really this is so we can track the mouse
 
        when it's outside the large canvas. -->
 
      <div id="outOfBounds" on-mousemove="onOutOfBoundsMove" on-mouseup="hideLarge"></div>
 
      <!--  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>
 
    `;
 
  }
 
  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)"];
 
  }
 
  ready() {
 
    super.ready();
 
    if (!window.pickerCanvases) {
 
      window.pickerCanvases = {
 
        large: new RainbowCanvas("/colorpick_rainbow_large.png", [400, 200]),
 
      };
 
    }
 
    this.large = window.pickerCanvases.large;
 
    this.$.large.onCanvasMove = this.onCanvasMove.bind(this);
 
    this.$.large.hideLarge = this.hideLarge.bind(this);
 
    document.body.append(this.$.large);
 
  }
 
  disconnectedCallback() {
 
    super.disconnectedCallback();
 
    document.body.removeChild(this.$.large);
 
  }
 
  onValue(value) {
 
    if (this.hueSatColor === null) {
 
      this.hueSatColor = "#ffffff";
 
    }
 
    let neverBlack = 0.1 + (0.9 * value) / 255;
 
    this.$.swatch.style.filter = `brightness(${neverBlack})`;
 
  }
 
  writeColor(hueSatColor, value) {
 
    if (hueSatColor === null || this.pauseWrites) {
 
      return;
 
    }
 
    this.color = one
 
      .color(hueSatColor)
 
      .value(value / 255)
 
      .hex();
 
    this.$.large.style.borderColor = this.color;
 
  }
 
  readColor(color) {
 
    if (this.$.large.style.display == "block") {
 
      // for performance, don't do color searches on covered widget
 
      return;
 
    }
 

	
 
       this.pauseWrites = false;
 
     }    
 
     showLarge(x, y) {
 
       this.$.large.style.display = 'block';
 
       this.$.outOfBounds.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 - this.clamp(pos[0], 0, 400)) + 'px';
 
         this.$.large.style.top = (y - this.clamp(pos[1], 0, 200)) + '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';
 
       this.$.outOfBounds.style.display = 'none';
 
    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.pauseWrites = false;
 
  }
 
  showLarge(x, y) {
 
    this.$.large.style.display = "block";
 
    this.$.outOfBounds.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 - this.clamp(pos[0], 0, 400) + "px";
 
      this.$.large.style.top = y - this.clamp(pos[1], 0, 200) + "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";
 
    this.$.outOfBounds.style.display = "none";
 

	
 
       if (this.color !== undefined) {
 
         this.readColor(this.color);
 
       }
 
       this.closeTime = Date.now();
 
     }
 
     onDownSmall(ev) {
 
       this.showLarge(ev.pageX, ev.pageY);
 
     }
 
     moveLargeCrosshair(pos) {
 
       const ch = this.$.large.shadowRoot.querySelector("#largeCrosshair");
 
       ch.style.left = (pos[0] - ch.offsetWidth / 2) + 'px';
 
       ch.style.top = (pos[1] - ch.offsetHeight / 2) + 'px';
 
     }
 
     onCanvasMove(ev) {
 
       if (ev.buttons != 1) {
 
         this.hideLarge();
 
         return;
 
       }
 
       var canvas = this.$.large.shadowRoot.querySelector('#largeRainbow');
 
       var pos = [ev.offsetX - canvas.offsetLeft,
 
                  ev.offsetY - canvas.offsetTop];
 
       this.setLargePoint(pos);
 
     }
 
     setLargePoint(pos) {
 
       this.moveLargeCrosshair(pos);
 
       this.hueSatColor = this.large.colorAt(pos);
 
    if (this.color !== undefined) {
 
      this.readColor(this.color);
 
    }
 
    this.closeTime = Date.now();
 
  }
 
  onDownSmall(ev) {
 
    this.showLarge(ev.pageX, ev.pageY);
 
  }
 
  moveLargeCrosshair(pos) {
 
    const ch = this.$.large.shadowRoot.querySelector("#largeCrosshair");
 
    ch.style.left = pos[0] - ch.offsetWidth / 2 + "px";
 
    ch.style.top = pos[1] - ch.offsetHeight / 2 + "px";
 
  }
 
  onCanvasMove(ev) {
 
    if (ev.buttons != 1) {
 
      this.hideLarge();
 
      return;
 
    }
 
    var canvas = this.$.large.shadowRoot.querySelector("#largeRainbow");
 
    var pos = [ev.offsetX - canvas.offsetLeft, ev.offsetY - canvas.offsetTop];
 
    this.setLargePoint(pos);
 
  }
 
  setLargePoint(pos) {
 
    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;
 
       }
 
     }
 
     onOutOfBoundsMove(ev) {
 
       const largeX = ev.offsetX - this.$.large.offsetLeft;
 
       const largeY = ev.offsetY - this.$.large.offsetTop;
 
       this.setLargePoint([this.clamp(largeX, 0, 400-1),
 
                           this.clamp(largeY, 0, 200-1)]);
 
     }
 
     clamp(x, lo, hi) {
 
       return Math.max(lo, Math.min(hi, x));
 
     }
 
   }
 
   customElements.define(Light9ColorPicker.is, Light9ColorPicker);
 
  </script>
 
</dom-module>
 

	
 
    // special case: it's useless to adjust the hue/sat of black
 
    if (this.value == 0) {
 
      this.value = 255;
 
    }
 
  }
 
  onOutOfBoundsMove(ev) {
 
    const largeX = ev.offsetX - this.$.large.offsetLeft;
 
    const largeY = ev.offsetY - this.$.large.offsetTop;
 
    this.setLargePoint([this.clamp(largeX, 0, 400 - 1), this.clamp(largeY, 0, 200 - 1)]);
 
  }
 
  clamp(x, lo, hi) {
 
    return Math.max(lo, Math.min(hi, x));
 
  }
 
}
0 comments (0 inline, 0 general)