changeset 2240:650375a47213

start color-picker port from polymer to lit
author drewp@bigasterisk.com
date Thu, 25 May 2023 11:47:20 -0700
parents b136c450ebee
children 8d6792a6ffdb
files light9/web/light9-color-picker.html light9/web/light9-color-picker.ts
diffstat 2 files changed, 338 insertions(+), 339 deletions(-) [+]
line wrap: on
line diff
--- a/light9/web/light9-color-picker.html	Thu May 25 11:46:22 2023 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,339 +0,0 @@
-<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;
-         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>
-
-<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');
-
-       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)'
-     ]; }
-     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();
-
-       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);
-
-       // 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>
-
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/light9/web/light9-color-picker.ts	Thu May 25 11:47:20 2023 -0700
@@ -0,0 +1,338 @@
+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");
+
+@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
+
+    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;
+      }
+
+      #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. */
+      }
+    `,
+  ];
+  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 = 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);
+
+    // 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));
+  }
+}