diff options
author | Kyle Gunger <kgunger12@gmail.com> | 2024-11-12 23:49:21 -0500 |
---|---|---|
committer | Kyle Gunger <kgunger12@gmail.com> | 2024-11-12 23:49:21 -0500 |
commit | 53c95ab94cab5163424646d4a798a7ea7fb13ec7 (patch) | |
tree | baa4551d161e1f074bc5579f4c9c6b061eddd084 | |
parent | 4d93bd73f0a56974bd55db8f9e8ff3f318be195d (diff) |
Toggle, Checkbox, and Slider widgets
-rw-r--r-- | index.html | 12 | ||||
-rw-r--r-- | scripts/gui-common/color.js | 66 | ||||
-rw-r--r-- | scripts/gui-common/widgets.js | 541 | ||||
-rw-r--r-- | scripts/main.js | 36 | ||||
-rw-r--r-- | styles/widgets.css | 109 |
5 files changed, 674 insertions, 90 deletions
@@ -40,17 +40,19 @@ <div class="widget color-temp" style="--percent: 0.3;"></div> <div class="widget color-light" style="--percent: 0.3;"></div> <div class="widget thermostat" style="--percent: 0.3; --arch-color: #0084ff;"> - <div class="arch"></div> - <div class="gague">68</div> - <div class="temp">72°</div> + <arch></arch> + <gague>68</gague> + <temp>72°</temp> </div> - <div class="widget select h"> + <div class="widget sel-button h"> <div class="widget button">Heat</div> <div class="widget button">Cool</div> <div class="widget button" selected>Eco</div> </div> </content> - <script type="module" src="scripts/main.js"></script> + <script src="scripts/gui-common/color.js"></script> + <script src="scripts/gui-common/widgets.js"></script> + <script src="scripts/main.js"></script> </body> </html> 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."); diff --git a/styles/widgets.css b/styles/widgets.css index 8503364..5a29507 100644 --- a/styles/widgets.css +++ b/styles/widgets.css @@ -26,6 +26,8 @@ input { cursor:pointer; position: relative; + + user-select: none; } .widget:hover { @@ -127,7 +129,17 @@ input { width: calc(3 * var(--base-unit)); } -.slider::before { +.slider > .fill { + overflow: hidden; + width: 100%; + height: 100%; + border-radius: 10px; + position: absolute; + top: 0; + left: 0; +} + +.slider > .fill::before { border-radius: 10px; position: absolute; bottom: 0; @@ -136,21 +148,19 @@ input { width: 100%; background-color: var(--w-sl-fill); - transition-duration: 0.15s; - content: ''; } -.slider.h::before { - width: calc(100% * var(--fill-percent)); +.slider.h > .fill::before { + width: calc(100% * var(--percent)); height: 100%; } -.slider:hover::before { +.slider:hover > .fill::before { background-color: var(--w-sl-fill-hover); } -.slider:active::before { +.slider:active > .fill::before { background-color: var(--w-sl-fill-active); } @@ -175,7 +185,7 @@ input { border-bottom: var(--w-sl-dots); } -.slider > .detail { +.slider > .detail, .color-light > .detail, .color-temp > .detail { display: block; position: absolute; @@ -190,15 +200,18 @@ input { z-index: 2; font-weight: bold; - min-width: var(--base-unit); + width: calc(1.2 * var(--base-unit)); height: var(--base-unit); - transition-duration: 0.15s; pointer-events: none; align-content: center; text-align: center; - padding-left: 3px; - padding-right: 3px; + padding: 2px; +} + +.slider.h > .detail, .color-light.h > .detail, .color-temp.h > .detail { + top: calc(100% + 20px); + left: calc(-1.2 * var(--base-unit) / 2 + 100% * var(--percent)); } .slider:active > .detail { @@ -206,7 +219,11 @@ input { color: rgba(255, 255, 255, 1); } -.slider.inactive > .detail { +.color-light:active > .detail, .color-temp:active > .detail { + background-color: var(--detail); +} + +.inactive > .detail { display: none; } @@ -359,60 +376,37 @@ input { overflow: unset; } -.color-temp::after { - content: ''; - width: calc(100% + 8px); - height: 14px; - border: 5px solid #444; - position: absolute; - bottom: calc(100% * var(--percent) - 7px); - left: -4px; - - box-sizing: border-box; - - border-radius: 7px; - transition-duration: 0.15s; -} - -.color-temp:hover::after { - border-color: #888; -} - -.color-temp.inactive::after { - border-color: rgb(68, 68, 68, 0); -} - .color-light { - height: var(--base-unit); - width: calc(3 * var(--base-unit)); - background: linear-gradient(to left, white, black); + width: var(--base-unit); + height: calc(3 * var(--base-unit)); + background: linear-gradient(white, black); --side-color: #aaa; overflow: unset; } -.color-light::after { +.color-temp::after, .color-light::after { content: ''; - height: calc(100% + 8px); - width: 14px; + width: calc(100% + 8px); + height: 14px; border: 5px solid #444; position: absolute; - left: calc(100% * var(--percent) - 7px); - top: -4px; + bottom: calc(100% * var(--percent) - 7px); + left: -4px; box-sizing: border-box; border-radius: 7px; - transition-duration: 0.15s; } -.color-light:hover::after { +.color-temp:hover::after, .color-light:hover::after { border-color: #888; } -.color-light.inactive::after { +.color-temp.inactive::after, .color-light.inactive::after { border-color: rgb(68, 68, 68, 0); } + /** Thermostat */ @@ -425,7 +419,8 @@ input { height: calc(3 * var(--base-unit)); } -.thermostat > .arch { +.thermostat > arch { + display: block; position: absolute; height: 100px; @@ -439,7 +434,7 @@ input { background-color: #222; } -.thermostat > .arch::after { +.thermostat > arch::after { content: ''; display: block; height: 100px; @@ -450,7 +445,8 @@ input { transition-duration: 0.5s; } -.thermostat > .gague { +.thermostat > gague { + display: block; position: absolute; bottom: calc(-13px + ((100% - 100px) / 2) + 90px); left: calc(-13px + 50%); @@ -474,7 +470,8 @@ input { font-weight: bold; } -.thermostat > .temp { +.thermostat > temp { + display: block; position: absolute; bottom: calc((100% - 100px) / 2); left: calc(50% - 65px); @@ -487,7 +484,7 @@ input { Button Select */ -.select { +.sel-button { display: inline-flex; max-height: calc(2 * var(--base-unit)); max-width: calc(8 * var(--base-unit)); @@ -497,7 +494,7 @@ input { overflow-x: auto; } -.select.h { +.sel-button.h { flex-direction: column; height: 100%; overflow-y: auto; @@ -506,7 +503,7 @@ input { overflow-x: hidden; } -.select > div { +.sel-button > div { margin: 5px 5px; padding: 7px; @@ -521,13 +518,13 @@ input { width: fit-content; } -.select.h > div { +.sel-button.h > div { height: auto; width: calc(100% - 12px); } -.select > div[selected] { +.sel-button > div[selected] { --w-bg: var(--w-sel-button-selected); --w-bg-hover: var(--w-sel-button-selected-hover); --w-bg-active: var(--w-sel-button-selected-active); |