summaryrefslogtreecommitdiff
path: root/scripts
diff options
context:
space:
mode:
authorKyle Gunger <kgunger12@gmail.com>2024-11-12 23:49:21 -0500
committerKyle Gunger <kgunger12@gmail.com>2024-11-12 23:49:21 -0500
commit53c95ab94cab5163424646d4a798a7ea7fb13ec7 (patch)
treebaa4551d161e1f074bc5579f4c9c6b061eddd084 /scripts
parent4d93bd73f0a56974bd55db8f9e8ff3f318be195d (diff)
Toggle, Checkbox, and Slider widgets
Diffstat (limited to 'scripts')
-rw-r--r--scripts/gui-common/color.js66
-rw-r--r--scripts/gui-common/widgets.js541
-rw-r--r--scripts/main.js36
3 files changed, 614 insertions, 29 deletions
diff --git a/scripts/gui-common/color.js b/scripts/gui-common/color.js
new file mode 100644
index 0000000..90eac14
--- /dev/null
+++ b/scripts/gui-common/color.js
@@ -0,0 +1,66 @@
+class Color
+{
+ /** @type {Array<number>} */
+ channels = []
+
+ /**
+ * Construct a color
+ * @param {...number} nums
+ */
+ constructor(...nums)
+ {
+ this.channels = nums;
+ }
+
+ /**
+ * @param {Color} b
+ * @param {number} i
+ * @returns {Color}
+ */
+ interpolate(b, i)
+ {
+ let out = new Color();
+ for (let c = 0; c < this.channels.length && c < b.channels.length; c++)
+ {
+ out.channels.push(b.channels[c] * i + this.channels[c] * (1 - i))
+ }
+ return out;
+ }
+
+ /** Get the CSS string representing rgb
+ * @returns {string}
+ */
+ rgb()
+ {
+ return `rgb(${Math.trunc(this.channels[0] * 255)}, ${Math.trunc(this.channels[1] * 255)}, ${Math.trunc(this.channels[2] * 255)})`;
+ }
+
+ /** Get the CSS string representing rgba
+ * @returns {string}
+ */
+ rgba()
+ {
+ return `rgba(${Math.trunc(this.channels[0] * 255)}, ${Math.trunc(this.channels[1] * 255)}, ${Math.trunc(this.channels[2] * 255)}, ${this.channels[3]})`;
+ }
+
+ static from_rgb(r, g, b)
+ {
+ return new Color(r / 255, g / 255, b / 255);
+ }
+
+ static from_rgba(r, g, b)
+ {
+ return new Color(r / 255, g / 255, b / 255, a);
+ }
+}
+
+/**
+ * Interpolate between two colors
+ * @param {Color} a
+ * @param {Color} b
+ * @param {number} p
+ */
+function interpolate(a, b, p)
+{
+
+} \ No newline at end of file
diff --git a/scripts/gui-common/widgets.js b/scripts/gui-common/widgets.js
index e365f56..88f1db8 100644
--- a/scripts/gui-common/widgets.js
+++ b/scripts/gui-common/widgets.js
@@ -1,37 +1,524 @@
/**
- * @typedef {{
- * el: HTMLElement;
- * value: T;
- * set: (value: T) => void;
- * get: () => T}} Widget<T>
- * @template {any} T
+ * @typedef {"button" | "toggle" | "slider" | "checkbox" | "color-wheel" | "color-temp" | "color-light" | "thermostat" | "sel-button"} WidgetType
*/
-/**
- * @typedef {"button" | "toggle" | "slider" | "checkbox"} WidgetType
- */
+/** @template {*} T */
+class Widget extends EventTarget{
+ /** @type {T} */
+ #value = null;
-/**
- * @template {any} T
- * @param {WidgetType} type
- * @returns {Widget<T>}
- */
-function Widget (type = "button") {
- /** @type {Widget<Number>} */
- let out = {
- el: document.createElement("div"),
- value: 0,
- set: (e) => {value = e},
- get: () => this.value,
- };
+ /** @type {HTMLElement} */
+ element = null;
+
+ /** @type {boolean} */
+ inactive = false;
+
+ /**
+ * Construct a new widget
+ */
+ constructor ()
+ {
+ super();
+ this.element = document.createElement("div");
+ this.element.classList.add("widget");
+
+ this.element.addEventListener("mousedown", this.#emitMouseEvent.bind(this));
+ this.element.addEventListener("mouseup", this.#emitMouseEvent.bind(this));
+ this.element.addEventListener("mousemove", this.#emitMouseEvent.bind(this));
+ this.element.addEventListener("mouseleave", this.#emitMouseEvent.bind(this));
+ this.element.addEventListener("mouseenter", this.#emitMouseEvent.bind(this));
+
+ this.element.addEventListener("touchstart", this.#emitTouchEvent.bind(this));
+ this.element.addEventListener("touchend", this.#emitTouchEvent.bind(this));
+ this.element.addEventListener("touchmove", this.#emitTouchEvent.bind(this));
+ this.element.addEventListener("touchcancel", this.#emitTouchEvent.bind(this));
+
+ this.element.addEventListener("contextmenu", this.#emitContextEvent.bind(this));
+ }
+
+ /** @returns {T} */
+ get()
+ {
+ return this.#value;
+ }
+
+ /** @param {T} v */
+ set(v)
+ {
+ this.#value = v;
+ this.#emitChangeEvent();
+ }
+
+ /** @param {boolean} i */
+ setInactive (i)
+ {
+ this.element.classList.toggle("inactive", i);
+ this.inactive = i;
+ }
+
+ /** @returns {boolean} */
+ getInactive ()
+ {
+ return this.inactive;
+ }
+
+ #emitChangeEvent()
+ {
+ this.dispatchEvent(new CustomEvent("change", {detail: this}));
+ }
+
+ /** @param {MouseEvent} event */
+ #emitMouseEvent(event)
+ {
+ if (this.inactive)
+ this.dispatchEvent(new CustomEvent("inactive", {detail: {widget: this, event: new MouseEvent(event.type, event)}}));
+ else
+ this.dispatchEvent(new MouseEvent(event.type, event));
+ }
+
+ /** @param {TouchEvent} event */
+ #emitTouchEvent(event)
+ {
+ if (this.inactive)
+ this.dispatchEvent(new CustomEvent("inactive", {detail: {widget: this, event: new TouchEvent(event.type, event)}}));
+ else
+ this.dispatchEvent(new TouchEvent(event.type, event));
+ }
+
+ /** @param {Event} event */
+ #emitContextEvent(event)
+ {
+ if (this.inactive)
+ this.dispatchEvent(new CustomEvent("inactive", {detail: {widget: this, event: new Event(event.type, event)}}));
+ else
+ this.dispatchEvent(new Event(event.type, event));
+ event.preventDefault();
+ }
+
+ handleClick() {}
+}
+
+/** @typedef {Widget<boolean>} WidgetButton */
+class WidgetButton extends Widget
+{
+ /** @type {number} */
+ pressing = 0;
+
+ gone = 0;
+
+ #bound = null;
+
+ constructor ()
+ {
+ super();
+ this.element.classList.add("button");
+ this.addEventListener("mousedown", this.#press);
+ this.addEventListener("mouseup", this.#unpress);
+ this.addEventListener("mouseleave", this.#leave);
+ this.addEventListener("mouseenter", this.#enter);
+ this.#bound = this.#unpress.bind(this);
+ }
+
+ /** @param {MouseEvent} event */
+ #press(event)
+ {
+ this.pressing |= (1 << event.button);
+ this.set(true);
+ }
+
+ #unpress (event)
+ {
+ if (((1 << event.button) & this.pressing) == 0)
+ return;
+
+ this.pressing -= (1 << event.button);
+ if (this.pressing == 0)
+ {
+ this.set(false);
+ if (this.gone)
+ {
+ this.gone = 0;
+ window.removeEventListener("mouseup", this.#bound);
+ }
+ }
+ }
+
+ /** @param {MouseEvent} event */
+ #leave(event)
+ {
+ if (this.pressing == 0)
+ return;
+ this.gone = 1;
+ window.addEventListener("mouseup", this.#bound);
+ }
+
+ /** @param {MouseEvent} event */
+ #enter(event)
+ {
+ if (this.primed == 0)
+ return;
+ this.gone = 0;
+ window.removeEventListener("mouseup", this.#bound);
+ }
+}
+
+/** @typedef {Widget<boolean>} WidgetToggle */
+class WidgetToggle extends Widget
+{
+ /** @type {number} */
+ primed = 0;
+
+ /** @type {number} */
+ gone = 0;
+
+ #bound = null;
+
+ constructor ()
+ {
+ super();
+ this.element.classList.add("button");
+ this.element.classList.add("toggle");
+ this.addEventListener("mousedown", this.#prime);
+ this.addEventListener("mouseup", this.#toggle);
+ this.addEventListener("mouseleave", this.#leave);
+ this.addEventListener("mouseenter", this.#enter);
+ this.#bound = this.#toggle.bind(this);
+ }
+
+ /** @param {MouseEvent} event */
+ #prime(event)
+ {
+ this.primed |= (1 << event.button);
+ }
+
+ /** @param {MouseEvent} event */
+ #toggle(event)
+ {
+ if (this.gone)
+ {
+ this.primed = 0;
+ this.gone = 0;
+ window.removeEventListener("mouseup", this.#bound);
+ }
+ if (((1 << event.button) & this.primed) == 0)
+ return;
+ this.set(!this.get());
+ this.element.classList.toggle("active", this.get());
+ this.primed -= (1 << event.button);
+ }
+
+ /** @param {MouseEvent} event */
+ #leave(event)
+ {
+ if (this.primed == 0)
+ return;
+ this.gone = 1;
+ window.addEventListener("mouseup", this.#bound);
+ }
+
+ /** @param {MouseEvent} event */
+ #enter(event)
+ {
+ if (this.primed == 0)
+ return;
+ this.gone = 0;
+ window.removeEventListener("mouseup", this.#bound);
+ }
+}
+
+class WidgetCheckbox extends WidgetToggle {
+
+ constructor()
+ {
+ super();
+ this.element.classList.remove("toggle");
+ this.element.classList.add("checkbox");
+ }
+}
+
+class WidgetSlider extends Widget
+{
+ /** @type {number} */
+ #primed = 0;
+
+ /** @type {number} */
+ #gone = 0;
+
+ #boundUp = null;
+ #boundMove = null;
+
+ /** @type {HTMLElement} */
+ #detail = null;
+ /** @type {(e: HTMLElement, v: number, p: number) => void} */
+ #detailUpdater = null;
+
+ /** @type {number} */
+ #tmpNum = 0;
+
+ /** @type {number} */
+ #max;
+ /** @type {number} */
+ #min;
+ /** @type {number} */
+ #step;
+ /** @type {number} */
+ #precision;
+ /** @type {boolean} */
+ #percent;
+
+
+ /**
+ * Constructor
+ * @param {number} [max] Value the slider represents at maximum
+ * @param {number} [min] Value the slider represents at minimum
+ * @param {number} [step] Step amount
+ * @param {number} [trunc] Decimal places to keep
+ * @param {boolean} [percent] Whether to show a percentage instead of the raw number
+ */
+ constructor (max = 10, min = 1, step = 0.1, trunc = 1, percent = false)
+ {
+ super();
+ this.element.classList.add("slider");
+ let fill = document.createElement("div");
+ fill.classList.add("fill");
+ this.element.appendChild(fill);
+
+ this.#detail = document.createElement("div");
+ this.#detail.classList.add("detail");
+ this.element.appendChild(this.#detail);
+
+ this.addEventListener("mousedown", this.#press);
+ this.addEventListener("mousemove", this.#move);
+ this.addEventListener("mouseup", this.#unpress);
+ this.addEventListener("mouseleave", this.#leave);
+ this.addEventListener("mouseenter", this.#enter);
+ this.addEventListener("change", this.#change);
+
+ this.#boundUp = this.#unpress.bind(this);
+ this.#boundMove = this.#move.bind(this);
+ this.#max = max;
+ this.#min = min;
+ this.#step = step;
+ this.#precision = trunc;
+ this.#percent = percent;
+
+ this.#detailUpdater = this.update_detail.bind(this);
+ }
+
+ /** @param {MouseEvent} event */
+ #press(event)
+ {
+ this.#primed |= (1 << event.button);
+ this.#move(event);
+ }
+
+ /** @param {MouseEvent} event */
+ #move(event)
+ {
+ if (this.#primed == 0)
+ return;
+
+ let rect = this.element.getBoundingClientRect();
+ let top = 0, bot = 0, point = 0;
+ if (this.element.classList.contains("h"))
+ {
+ top = rect.right;
+ bot = rect.left;
+ point = event.clientX;
+ }
+ else
+ {
+ top = rect.bottom;
+ bot = rect.top;
+ point = top - event.clientY + bot;
+ }
+
+ if (point < bot && this.#tmpNum != this.#min)
+ this.#tmpNum = this.#min;
+ else if (point > top && this.#tmpNum != this.#max)
+ this.#tmpNum = this.#max;
+ else if (bot < point && point < top)
+ {
+ let v = ((point - bot) / (top - bot)) * (this.#max - this.#min) + this.#min;
+ let r = v % this.#step;
+ v -= v % this.#step;
+ if (r >= this.#step / 2)
+ v += this.#step;
+ this.#tmpNum = v;
+ }
+ this.#update_ui();
+ }
+
+ #change ()
+ {
+ this.#tmpNum = this.get();
+ this.#update_ui();
+ }
+
+ #update_ui()
+ {
+ if (this.#tmpNum < this.#min || this.#max < this.#tmpNum)
+ this.#tmpNum = Math.min(Math.max(this.#tmpNum, this.#min), this.#max);
+ let percent = (this.#tmpNum - this.#min) / (this.#max - this.#min);
+
+ this.#detailUpdater(this.#detail, this.#tmpNum, percent, this.#percent);
+ this.element.style.setProperty("--percent", percent);
+ }
+
+ /** @param {MouseEvent} event */
+ #unpress(event)
+ {
+ if (((1 << event.button) & this.#primed) == 0)
+ return;
+ this.#primed -= (1 << event.button);
+ if (this.#primed == 0)
+ {
+ this.set(this.#tmpNum);
+ if (this.#gone)
+ {
+ this.#gone = 0;
+ window.removeEventListener("mouseup", this.#boundUp);
+ window.removeEventListener("mousemove", this.#boundMove);
+ }
+ }
+ }
- out.el.classList = ["widget", type];
- if (type == "checkbox" || type == "toggle")
+ /** @param {MouseEvent} event */
+ #leave(event)
{
- out.el.classList.add("button");
+ if (this.#primed == 0)
+ return;
+ this.#gone = 1;
+ window.addEventListener("mouseup", this.#boundUp);
+ window.addEventListener("mousemove", this.#boundMove);
}
- return out;
+ /** @param {MouseEvent} event */
+ #enter(event)
+ {
+ if (this.#primed == 0)
+ return;
+ this.#gone = 0;
+ window.removeEventListener("mouseup", this.#boundUp);
+ window.removeEventListener("mousemove", this.#boundMove);
+ }
+
+ update_detail(el, val, percent)
+ {
+ if (this.#percent)
+ el.innerText = `${Math.trunc(percent * 100)}%`;
+ else
+ el.innerText = `${Math.trunc(Math.pow(10, this.#precision) * val) / Math.pow(10, this.#precision)}`
+ }
+
+ setDetailUpdater(updater)
+ {
+ this.#detailUpdater = updater;
+ }
+
+ /** @param {number} m */
+ setMin(m)
+ {
+ this.#min = m;
+ this.#update_ui();
+ }
+
+ /** @param {number} m */
+ setMax(m)
+ {
+ this.#max = m;
+ this.#update_ui();
+ }
+
+ /** @param {number} s */
+ setStep(s)
+ {
+ this.#step = s;
+ this.#update_ui();
+ }
+}
+
+class WidgetColorTemp extends WidgetSlider
+{
+ /** @type {Color} */
+ ORANGE = Color.from_rgb(250, 160, 100);
+ /** @type {Color} */
+ WHITE = new Color(1, 1, 1);
+ /** @type {Color} */
+ BLUE = Color.from_rgb(190, 200, 255);
+
+ constructor ()
+ {
+ super(6000, 2700, 100);
+ this.element.classList.replace("slider", "color-temp");
+
+ let fills = this.element.getElementsByClassName("fill");
+ for(let f of fills)
+ {
+ f.remove();
+ }
+
+ this.setDetailUpdater(this.update_detail.bind(this));
+ this.set(2700);
+ }
+
+ /**
+ * Update the detail for the color temp slider
+ * @param {HTMLElement} el
+ * @param {number} val
+ * @param {number} percent
+ */
+ update_detail(el, val, percent)
+ {
+ let out = null;
+ if (percent < 0.7)
+ out = this.ORANGE.interpolate(this.WHITE, percent / 0.7);
+ else
+ out = this.WHITE.interpolate(this.BLUE, (percent - 0.7) / 0.3);
+ el.style.setProperty("--detail", out.rgb());
+ }
+}
+
+class WidgetColorLight extends WidgetSlider
+{
+ /** @type {Color} */
+ WHITE = new Color(1, 1, 1);
+ /** @type {Color} */
+ BLACK = new Color(0, 0, 0);
+
+ constructor ()
+ {
+ super(1, 0, 0.01, 0, 1);
+ this.element.classList.replace("slider", "color-light");
+
+ let fills = this.element.getElementsByClassName("fill");
+ for(let f of fills)
+ {
+ f.remove();
+ }
+
+ this.setDetailUpdater(this.update_detail.bind(this));
+ this.set(0);
+ }
+
+ /**
+ * Update the detail for the color temp slider
+ * @param {HTMLElement} el
+ * @param {number} val
+ * @param {number} percent
+ */
+ update_detail(el, val, percent)
+ {
+ let out = this.BLACK.interpolate(this.WHITE, percent);
+ el.style.setProperty("--detail", out.rgb());
+ }
}
-export { Widget }; \ No newline at end of file
+/** @typedef {Widget<number>} WidgetSlider */
+/** @typedef {Widget<string>} WidgetColorWheel */
+/** @typedef {Widget<number>} WidgetColorTemp */
+/** @typedef {Widget<number>} WidgetColorLight */
+/** @typedef {Widget<number> & {getGague: () => number, setGague: (value: number) => void}} WidgetThermostat */
+/** @typedef {Widget<any> & {addSelection: (name: string, value: any) => void, setSelection: (name: string) => boolean, removeSelection: (name: string) => void, getSelection: () => string}} WidgetSelectButton */
+
+// export { Widget }; \ No newline at end of file
diff --git a/scripts/main.js b/scripts/main.js
index ce0dfe4..ac6dbe9 100644
--- a/scripts/main.js
+++ b/scripts/main.js
@@ -1,3 +1,35 @@
-import * as w from './gui-common/widgets.js';
+class Client {
-let a = w.Widget();
+
+
+ /**
+ * @param {HTMLElement} content The base element where page content is placed
+ */
+ constructor (content)
+ {
+ /** @type {WidgetToggle} */
+ this.toggle = new WidgetCheckbox();
+ content.appendChild(this.toggle.element);
+ this.slider = new WidgetSlider();
+ content.appendChild(this.slider.element);
+ this.temp = new WidgetColorTemp();
+ content.appendChild(this.temp.element);
+ this.light = new WidgetColorLight();
+ content.appendChild(this.light.element);
+ // content.appendChild(Widget("button").el);
+ // content.appendChild(Widget("checkbox").el);
+ // content.appendChild(Widget("slider").el);
+ // content.appendChild(Widget("color-wheel").el);
+ // content.appendChild(Widget("color-temp").el);
+ // content.appendChild(Widget("color-light").el);
+ // content.appendChild(Widget("thermostat").el);
+ // content.appendChild(Widget("sel-button").el);
+ this.content = content;
+ }
+}
+
+let contents = document.getElementsByTagName("content");
+if (contents.length > 0)
+ OSmClient = new Client(contents[0]);
+else
+ console.error("Unable to find content tag, OSm stopping client.");