# 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 @@ - - - - - - - - - - - - - - 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)); + } +}