Changeset - 9645581bff24
[Not reviewed]
default
0 2 1
drewp@bigasterisk.com - 20 months ago 2023-05-26 22:46:16
drewp@bigasterisk.com
redo color picker code in lit
3 files changed with 393 insertions and 328 deletions:
0 comments (0 inline, 0 general)
light9/live/Light9LiveControl.ts
Show inline comments
 
@@ -2,15 +2,20 @@ import debug from "debug";
 
import { css, html, LitElement, PropertyValues } from "lit";
 
import { customElement, property } from "lit/decorators.js";
 
import { Literal, NamedNode } from "n3";
 
import { SubEvent } from "sub-events";
 
import { SyncedGraph } from "../web/SyncedGraph";
 
import { ControlValue } from "./Effect";
 
import { GraphToControls } from "./GraphToControls";
 
import { DeviceAttrRow } from "./Light9DeviceControl";
 
import { Choice } from "./Light9Listbox";
 
export { Slider } from "@material/mwc-slider";
 
export { Light9ColorPicker } from "../web/light9-color-picker";
 

	
 
const log = debug("control");
 

	
 
export { Slider } from "@material/mwc-slider";
 

	
 
const makeType = (d: "scalar" | "color" | "choice") => new NamedNode(`http://light9.bigasterisk.com/${d}`);
 

	
 
// UI for one device attr (of any type).
 
@customElement("light9-live-control")
 
export class Light9LiveControl extends LitElement {
 
@@ -25,45 +30,12 @@ export class Light9LiveControl extends L
 
      #colorControls > * {
 
        margin: 0 3px;
 
      }
 
      #colorControls paper-slider {
 
      }
 
      paper-slider {
 
        width: 100%;
 
        height: 25px;
 
      }
 

	
 
      paper-slider {
 
        --paper-slider-knob-color: var(--paper-red-500);
 
        --paper-slider-active-color: var(--paper-red-500);
 

	
 
        --paper-slider-font-color: white;
 
        --paper-slider-input: {
 
          width: 75px;
 

	
 
          background: black;
 
          display: inline-block;
 
        }
 
      }
 
    :host {
 
      border: 2px solid white;
 
    }
 
    `,
 
  ];
 

	
 
  render() {
 
    if (this.dataType.value === "http://light9.bigasterisk.com/scalar") {
 
      return html`<mwc-slider .value=${this.sliderValue} step=${1 / 255} min="0" max="1" @input=${this.onSliderInput}></mwc-slider> `;
 
    } else if (this.dataType.value === "http://light9.bigasterisk.com/color") {
 
      return html`
 
        <div id="colorControls">
 
          <button on-click="goBlack">0.0</button>
 
          <light9-color-picker color="{{value}}"></light9-color-picker>
 
        </div>
 
      `;
 
    } else if (this.dataType.value === "http://light9.bigasterisk.com/choice") {
 
      return html` <light9-listbox choices="{{deviceAttrRow.choices}}" value="{{choiceValue}}"> </light9-listbox> `;
 
    } else {
 
      throw new Error(`${this.dataType} unknown`);
 
    }
 
  }
 

	
 
  // passed from parent
 
  @property() device!: NamedNode;
 
  @property() dataType: NamedNode;
 
@@ -83,15 +55,34 @@ export class Light9LiveControl extends L
 
  @property() pickedChoice: Choice | null = null;
 
  @property() choiceValue: Choice | null = null;
 

	
 
  valueChanged: SubEvent<Literal> = new SubEvent();
 

	
 
  constructor() {
 
    super();
 
    this.dataType = new NamedNode("http://light9.bigasterisk.com/scalar");
 
    this.dataType = makeType("color");
 
    // getTopGraph().then((g) => {
 
    //   this.graph = g;
 
    //   // this.graph.runHandler(this.graphReads.bind(this), `${this.device} ${this.deviceAttrRow.uri} reads`);
 
    // });
 
  }
 

	
 
  render() {
 
    const dbg=html`
 
       ]`
 
    if (this.dataType.equals(makeType("scalar"))) {
 
      return html`${dbg} <mwc-slider .value=${this.sliderValue} step=${1 / 255} min="0" max="1" @input=${this.onSliderInput}></mwc-slider> `;
 
    } else if (this.dataType.equals(makeType("color"))) {
 
      return html` ${dbg}
 
        <div id="colorControls">
 
          <button on-click="goBlack">0.0</button>
 
          <light9-color-picker color="${this.value}"></light9-color-picker>
 
        </div>
 
      `;
 
    } else if (this.dataType.equals(makeType("choice"))) {
 
      return html`${dbg} <light9-listbox choices="{{deviceAttrRow.choices}}" value="{{choiceValue}}"> </light9-listbox> `;
 
    }
 
  }
 

	
 
  // graphReads() {
 
  //   const U = this.graph.U();
 
  // }
 
@@ -115,7 +106,7 @@ export class Light9LiveControl extends L
 
  onGraphValueChanged(v: ControlValue | null) {
 
    // log("change: control must display", v);
 
    // this.enableChange = false;
 
    if (this.dataType.value == "http://light9.bigasterisk.com/scalar") {
 
    if (this.dataType.equals(makeType("scalar"))) {
 
      if (v !== null) {
 
        setTimeout(() => {
 
          // only needed once per page layout
light9/web/floating_color_picker.ts
Show inline comments
 
new file 100644
 
// Note that this file deals only with hue+sat. See Light9ColorPicker for the value component.
 

	
 
import debug from "debug";
 
import { css, html, LitElement } from "lit";
 
import { customElement, query } from "lit/decorators.js";
 
import color from "onecolor";
 
import { SubEvent } from "sub-events";
 

	
 
const log = debug("control");
 

	
 
function clamp(x: number, lo: number, hi: number) {
 
  return Math.max(lo, Math.min(hi, x));
 
}
 

	
 
class RainbowCoord {
 
  // origin is rainbow top-lefft
 
  constructor(public x: number, public y: number) {}
 
}
 

	
 
export class ClientCoord {
 
  //  origin is top-left of client viewport (regardless of page scroll)
 
  constructor(public x: number, public y: number) {}
 
}
 

	
 
// Load the rainbow, and map between colors and pixels.
 
class RainbowCanvas {
 
  ctx: CanvasRenderingContext2D;
 
  colorPos: { [color: string]: RainbowCoord } = {};
 
  _loaded = false;
 
  _loadWatchers: (() => void)[] = [];
 
  constructor(url: string, public size: RainbowCoord) {
 
    var elem = document.createElement("canvas");
 
    elem.width = size.x;
 
    elem.height = size.y;
 
    this.ctx = elem.getContext("2d")!;
 

	
 
    var img = new Image();
 
    img.onload = () => {
 
      this.ctx.drawImage(img, 0, 0);
 
      this._readImage();
 
      this._loaded = true;
 
      this._loadWatchers.forEach(function (cb) {
 
        cb();
 
      });
 
      this._loadWatchers = [];
 
    };
 
    img.src = url;
 
  }
 

	
 
  onLoad(cb: () => void) {
 
    // 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.x, this.size.y).data;
 
    for (var y = 0; y < this.size.y; y += 1) {
 
      for (var x = 0; x < this.size.x; x += 1) {
 
        var base = (y * this.size.x + 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.x}) returns 0,0,0`);
 
        }
 
        var c = color(px).hex();
 
        this.colorPos[c] = new RainbowCoord(x, y);
 
      }
 
    }
 
  }
 

	
 
  colorAt(pos: RainbowCoord) {
 
    var data = this.ctx.getImageData(pos.x, pos.y, 1, 1).data;
 
    return color([data[0], data[1], data[2], 255]).hex();
 
  }
 

	
 
  posFor(col: string): RainbowCoord {
 
    if (col == "#000000") {
 
      throw new Error("no match");
 
    }
 

	
 
    let bright = color(col).value(1).hex();
 
    let r = parseInt(bright.slice(1, 3), 16),
 
      g = parseInt(bright.slice(3, 5), 16),
 
      b = parseInt(bright.slice(5, 7), 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: number): number {
 
      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.
 
          const color2 = color([r + dr, g + dg, b + db, 255]);
 
          const pos = this.colorPos[color2.hex()];
 
          if (pos !== undefined) {
 
            return pos;
 
          }
 
        }
 
      }
 
    }
 
    throw new Error("no match");
 
  }
 
}
 

	
 
// One-per-page element that floats above everything. Plus the scrim element, which is also per-page.
 
@customElement("light9-color-picker-float")
 
class Light9ColorPickerFloat extends LitElement {
 
  static styles = [
 
    css`
 
      :host {
 
        z-index: 10;
 
        position: fixed; /* host coords are the same as client coords /*
 
        left: 0;
 
        top: 0;
 
        width: 100%;
 
        height: 100%;
 

	
 
        /* Updated 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 {
 
        z-index: 2;
 
        position: relative;
 
        width: 400px;
 
        height: 200px;
 
        border: 10px solid #000;
 
        box-shadow: 8px 11px 40px 0px rgba(0, 0, 0, 0.74);
 
        overflow: hidden;
 
      }
 
      #largeRainbow {
 
        background: url(/colorpick_rainbow_large.png);
 
        width: 400px;
 
        height: 200px;
 
        user-select: none;
 
      }
 
      #outOfBounds {
 
        user-select: none;
 
        z-index: 1;
 
        background: #00000060;
 
        position: absolute;
 
        left: 0;
 
        top: 0;
 
        width: 100%;
 
        height: 100%;
 
      }
 
    `,
 
  ];
 

	
 
  @query("#largeCrosshair") largeCrosshairEl!: HTMLElement;
 
  @query("#largeRainbow") largeRainbowEl!: HTMLElement;
 

	
 
  canvasMove: SubEvent<RainbowCoord> = new SubEvent();
 
  outsideMove: SubEvent<ClientCoord> = new SubEvent();
 
  mouseUp: SubEvent<void> = new SubEvent();
 

	
 
  render() {
 
    return html`
 
      <!-- 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" @mousemove=${this.onOutOfBoundsMove} @mouseup=${this.onMouseUp}></div>
 
      <div id="largeRainbowComp">
 
        <div id="largeRainbow" @mousemove=${this.onCanvasMove} @mouseup=${this.onMouseUp}></div>
 
        <div id="largeCrosshair"></div>
 
      </div>
 
    `;
 
  }
 

	
 
  // make top-left of rainbow image be at this pos
 
  placeRainbow(pos: ClientCoord) {
 
    const el = this.shadowRoot?.querySelector("#largeRainbowComp")! as HTMLElement;
 
    const cssBorder = 10;
 
    el.style.left = pos.x - cssBorder + "px";
 
    el.style.top = pos.y - cssBorder + "px";
 
  }
 

	
 
  moveLargeCrosshair(pos: RainbowCoord) {
 
    const ch = this.largeCrosshairEl;
 
    ch.style.left = pos.x - ch.offsetWidth / 2 + "px";
 
    ch.style.top = pos.y - ch.offsetHeight / 2 + "px";
 
  }
 

	
 
  private onCanvasMove(ev: MouseEvent) {
 
    this.canvasMove.emit(new RainbowCoord(ev.offsetX, ev.offsetY));
 
  }
 

	
 
  private onMouseUp(ev: MouseEvent) {
 
    this.mouseUp.emit();
 
  }
 

	
 
  private onOutOfBoundsMove(ev: MouseEvent) {
 
    this.outsideMove.emit(new ClientCoord(ev.clientX, ev.clientY));
 
  }
 
}
 

	
 
class PickerFloat {
 
  private rainbow?: RainbowCanvas;
 
  private currentListener?: (hsc: string) => void;
 
  private rainbowOrigin: ClientCoord = new ClientCoord(0, 0);
 
  private floatEl?: Light9ColorPickerFloat;
 

	
 
  pageInit() {
 
    this.getFloatEl();
 
    this.getRainbow();
 
  }
 

	
 
  private getFloatEl(): Light9ColorPickerFloat {
 
    if (!this.floatEl) {
 
      this.floatEl = document.createElement("light9-color-picker-float") as Light9ColorPickerFloat;
 
      this.subscribeToFloatElement(this.floatEl);
 
      document.body.appendChild(this.floatEl);
 
    }
 
    return this.floatEl;
 
  }
 

	
 
  private subscribeToFloatElement(el: Light9ColorPickerFloat) {
 
    el.canvasMove.subscribe(this.onCanvasMove.bind(this));
 
    el.outsideMove.subscribe(this.onOutsideMove.bind(this));
 
    el.mouseUp.subscribe(() => {
 
      this.hide();
 
    });
 
  }
 

	
 
  private onCanvasMove(pos: RainbowCoord) {
 
    pos = new RainbowCoord( //
 
      clamp(pos.x, 0, 400 - 1), //
 
      clamp(pos.y, 0, 200 - 1)
 
    );
 
    this.getFloatEl().moveLargeCrosshair(pos);
 
    if (this.currentListener) {
 
      this.currentListener(this.getRainbow().colorAt(pos));
 
    }
 
  }
 

	
 
  private onOutsideMove(pos: ClientCoord) {
 
    const rp = this.toRainbow(pos);
 
    log("rp", rp);
 
    this.onCanvasMove(rp);
 
  }
 

	
 
  private getRainbow(): RainbowCanvas {
 
    if (!this.rainbow) {
 
      this.rainbow = new RainbowCanvas("/colorpick_rainbow_large.png", new RainbowCoord(400, 200));
 
    }
 
    return this.rainbow;
 
  }
 

	
 
  startPick(clickPoint: ClientCoord, startColor: string, onNewHueSatColor: (hsc: string) => void) {
 
    log("start pick", clickPoint);
 
    const el = this.getFloatEl();
 

	
 
    let pos: RainbowCoord;
 
    try {
 
      pos = this.getRainbow().posFor(startColor);
 
    } catch (e) {
 
      pos = new RainbowCoord(-999, -999);
 
    }
 

	
 
    this.rainbowOrigin = new ClientCoord( //
 
      clickPoint.x - clamp(pos.x, 0, 400), //
 
      clickPoint.y - clamp(pos.y, 0, 200)
 
    );
 
    log("rainbow goes to", this.rainbowOrigin);
 

	
 
    el.placeRainbow(this.rainbowOrigin);
 
    setTimeout(() => {
 
      this.getFloatEl().moveLargeCrosshair(pos);
 
    }, 1);
 

	
 
    el.style.display = "block";
 
    log("set listener");
 
    this.currentListener = onNewHueSatColor;
 
  }
 

	
 
  private hide() {
 
    const el = this.getFloatEl();
 
    el.style.display = "none";
 
    this.currentListener = undefined;
 
  }
 

	
 
  private toRainbow(pos: ClientCoord): RainbowCoord {
 
    return new RainbowCoord( //
 
      pos.x - this.rainbowOrigin.x, //
 
      pos.y - this.rainbowOrigin.y
 
    );
 
  }
 
}
 

	
 
export const pickerFloat = new PickerFloat();
light9/web/light9-color-picker.ts
Show inline comments
 
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 { css, html, LitElement, PropertyValueMap } from "lit";
 
import { customElement, property, query } from "lit/decorators.js";
 
import color from "onecolor";
 
import { SubEvent } from "sub-events";
 
import color from "onecolor";
 
import { ClientCoord, pickerFloat } from "./floating_color_picker";
 
import { Slider } from "@material/mwc-slider";
 

	
 
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");
 
  }
 
}
 
type int8 = number;
 

	
 
@customElement("light9-color-picker")
 
export class Light9ColorPicker extends LitElement {
 
@@ -173,7 +29,7 @@ export class Light9ColorPicker extends L
 
        border: 1px solid #333;
 
      }
 

	
 
      paper-slider {
 
      mwc-slider {
 
        width: 160px;
 
      }
 

	
 
@@ -181,158 +37,65 @@ export class Light9ColorPicker extends L
 
        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>
 
      <div id="swatch" style="background-color: ${this.color}; border-color: ${this.hueSatColor}" @mousedown=${this.startFloatingPick}></div>
 
      <span id="vee"> V: <mwc-slider id="value" step="1" min="0" max="255" @input=${this.onVSliderChange}></mwc-slider> </span>
 
    `;
 
  }
 
  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);
 
  @property() color: string = "#000"; // actual output color, computed by value*hueSatColor
 
  @property() hueSatColor: string = "#fff"; // always V=1, to be scaled down
 
  @property() value: int8 = 0;
 

	
 
  @query("#swatch") swatchEl!: HTMLElement;
 
  @query("#outOfBounds") outOfBoundsEl!: HTMLElement;
 

	
 
  connectedCallback(): void {
 
    super.connectedCallback();
 
    pickerFloat.pageInit();
 
  }
 
  onValue(value) {
 
    if (this.hueSatColor === null) {
 
      this.hueSatColor = "#ffffff";
 
  update(changedProperties: PropertyValueMap<this>) {
 
    if (changedProperties.has("color")) {
 
      this.setColor(this.color);
 
    }
 
    let neverBlack = 0.1 + (0.9 * value) / 255;
 
    this.$.swatch.style.filter = `brightness(${neverBlack})`;
 
  }
 
  writeColor(hueSatColor, value) {
 
    if (hueSatColor === null || this.pauseWrites) {
 
      return;
 
    if (changedProperties.has("value") || changedProperties.has("hueSatColor")) {
 
      this.color = color(this.hueSatColor)
 
        .value(this.value / 255)
 
        .hex();
 
      const sl = this.shadowRoot?.querySelector("#value") as Slider;
 
      if (sl) {
 
        sl.value = this.value;
 
      }
 
    }
 
    this.color = one
 
      .color(hueSatColor)
 
      .value(value / 255)
 
      .hex();
 
    this.$.large.style.borderColor = this.color;
 
    super.update(changedProperties);
 
  }
 

	
 
  private onVSliderChange(ev: CustomEvent) {
 
    this.value = ev.detail.value;
 
    this.swatchEl.style.borderColor = this.hueSatColor;
 
  }
 
  readColor(color) {
 
    if (this.$.large.style.display == "block") {
 
      // for performance, don't do color searches on covered widget
 
      return;
 

	
 
  // for outside users of the component
 
  setColor(col: string) {
 
    log(`set color pick to color ${col}`);
 
    if (col == "") {
 
      col = "#000";
 
    }
 

	
 
    this.pauseWrites = true;
 
    var colorValue = one.color(color).value() * 255;
 
    // writing back to immediate-value doesn't work on paper-slider
 
    this.sliderWriteValue = colorValue;
 
    this.value = color(col).value() * 255;
 

	
 
    // 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();
 
    this.hueSatColor = color(col).value(1).hex();
 
  }
 
  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;
 

	
 
  private startFloatingPick(ev: MouseEvent) {
 
    if (this.value < (20 as int8)) {
 
      log("boost");
 
      this.value = 255 as int8;
 
    }
 
    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));
 
    pickerFloat.startPick(new ClientCoord(ev.clientX, ev.clientY), this.color, (hsc: string) => {
 
      this.hueSatColor = hsc;
 
    });
 
  }
 
}
0 comments (0 inline, 0 general)