# HG changeset patch
# User drewp@bigasterisk.com
# Date 1685040440 25200
# Node ID 650375a47213851c1cda110d0ff67abf0b7fcaa8
# Parent b136c450ebee76dde087c295027b1a3533a7d892
start color-picker port from polymer to lit
diff -r b136c450ebee -r 650375a47213 light9/web/light9-color-picker.html
--- 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 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- V:
-
-
-
-
-
-
-
-
-
-
-
diff -r b136c450ebee -r 650375a47213 light9/web/light9-color-picker.ts
--- /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";
+
+//
+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`
+
+ `;
+ }
+ // 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`
+
+
+ V:
+
+
+
+
+
+
+ `;
+ }
+ 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));
+ }
+}