mirror of
https://gitgud.io/BondageProjects/Bondage-College.git
synced 2026-04-28 04:19:50 +00:00
ENH: Add a custom element for color tint pickers
Functions as some kind of 2D `<input type='range'>` input for selecting the color's saturation and brightness
This commit is contained in:
parent
435dbe1236
commit
aa2387770d
4 changed files with 373 additions and 0 deletions
|
|
@ -362,3 +362,29 @@ select:invalid:not(:disabled):not(:read-only) {
|
|||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
bc-tint-input {
|
||||
--hue: 0;
|
||||
|
||||
display: inline-block;
|
||||
aspect-ratio: 1 / 0.6;
|
||||
width: 160px;
|
||||
position: relative;
|
||||
border-width: 2px;
|
||||
border-style: inset;
|
||||
border-color: gray;
|
||||
box-sizing: border-box;
|
||||
background-blend-mode: multiply;
|
||||
background:
|
||||
linear-gradient(90deg, white, hsl(var(--hue), 100%, 50%)),
|
||||
linear-gradient(white, black);
|
||||
}
|
||||
|
||||
bc-tint-input:invalid {
|
||||
background-color: #fbb;
|
||||
box-shadow: 0 0 3px 2px #c22;
|
||||
}
|
||||
|
||||
bc-tint-input:disabled {
|
||||
border-color: black;
|
||||
}
|
||||
|
|
|
|||
37
BondageClub/CSS/tint-input.css
Normal file
37
BondageClub/CSS/tint-input.css
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
.knob {
|
||||
display: grid;
|
||||
align-items: center;
|
||||
justify-items: center;
|
||||
position: absolute;
|
||||
width: min(5vh, 2.5vw);
|
||||
height: min(5vh, 2.5vw);
|
||||
cursor: pointer;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.knob-circle {
|
||||
border: black 2px solid;
|
||||
border-radius: 100%;
|
||||
width: 50%;
|
||||
height: 50%;
|
||||
box-shadow:
|
||||
0 0 0 5px white,
|
||||
0 0 0 7px black;
|
||||
}
|
||||
|
||||
:host(bc-tint-input:disabled) .knob {
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
:host(bc-tint-input:disabled) .knob-circle {
|
||||
box-shadow:
|
||||
0 0 0 3px gray,
|
||||
0 0 0 5px black;
|
||||
}
|
||||
|
||||
@supports (height: 100dvh) {
|
||||
.knob {
|
||||
width: min(5dvh, 2.5dvw);
|
||||
height: min(5dvh, 2.5dvw);
|
||||
}
|
||||
}
|
||||
|
|
@ -2610,3 +2610,309 @@ function ElementFitText(el) {
|
|||
if (size < 8) break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML element for color tint pickers, functioning as some kind of 2D `<input type='range'>` input for selecting the color's saturation and brightness.
|
||||
*/
|
||||
class HTMLColorTintElement extends HTMLElement {
|
||||
static observedAttributes = ["value", "disabled"];
|
||||
static formAssociated = true;
|
||||
|
||||
/** @type {null | ElementInternals} */
|
||||
internals_ = null;
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @type {null | string}
|
||||
*/
|
||||
_pressedOldValue = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
if ("attachInternals" in this && typeof this.attachInternals === "function") {
|
||||
this.internals_ = this.attachInternals();
|
||||
}
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
if (this.shadowRoot) {
|
||||
return;
|
||||
}
|
||||
const shadow = this.attachShadow({ mode: "open" });
|
||||
|
||||
shadow.append(
|
||||
ElementCreate({
|
||||
tag: "div",
|
||||
classList: ["knob"],
|
||||
attributes: { "aria-hidden": "true" },
|
||||
children: [{ tag: "div", classList: ["knob-circle"] }],
|
||||
}),
|
||||
ElementCreate({
|
||||
tag: "link",
|
||||
attributes: { rel: "stylesheet", href: "CSS/tint-input.css" },
|
||||
}),
|
||||
);
|
||||
|
||||
this.tabIndex = this.disabled ? -1 : 0;
|
||||
this.defaultValue = this.value = this.getAttribute("value") ?? "#FFFFFF";
|
||||
|
||||
const tintPicker = this;
|
||||
|
||||
/** @type {(this: Document, ev: PointerEvent) => void} */
|
||||
function pointermove(ev) {
|
||||
if (tintPicker._pressedOldValue == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @type {null | HTMLElement} */
|
||||
const knob = tintPicker.shadowRoot.querySelector(".knob");
|
||||
if (!tintPicker || !knob) {
|
||||
ev.stopImmediatePropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
const { top, left, width, height } = tintPicker.getBoundingClientRect();
|
||||
const saturation = CommonClamp((ev.pageX - left) / width, 0, 1);
|
||||
const brightness = CommonClamp((ev.pageY - top) / height, 0, 1);
|
||||
tintPicker._setKnobPosition(100 * saturation, 100 * brightness);
|
||||
tintPicker.brightness = (1 - brightness) * 255;
|
||||
tintPicker.saturation = saturation * 255;
|
||||
tintPicker.dispatchEvent(new InputEvent("input"));
|
||||
}
|
||||
|
||||
/** @type {(this: Document, ev: PointerEvent) => void} */
|
||||
function pointerup(ev) {
|
||||
if (tintPicker._pressedOldValue == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
document.removeEventListener("pointerup", pointerup);
|
||||
document.removeEventListener("pointercancel", pointerup);
|
||||
document.removeEventListener("pointermove", pointermove);
|
||||
if (ev.type !== "pointercancel" && tintPicker._pressedOldValue !== tintPicker.value) {
|
||||
// Round the knob position such to the nearest 1/255 x- & y-coordinate
|
||||
const top = (255 - tintPicker.brightness) / (255 / 100);
|
||||
const left = tintPicker.saturation / (255 / 100);
|
||||
tintPicker._setKnobPosition(left, top);
|
||||
tintPicker.dispatchEvent(new Event("change"));
|
||||
}
|
||||
tintPicker._pressedOldValue = null;
|
||||
}
|
||||
|
||||
/** @type {(this: HTMLColorTintElement, ev: PointerEvent) => void} */
|
||||
function pointerdown(ev) {
|
||||
if (this.disabled || ev.button !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
tintPicker._pressedOldValue = this.value;
|
||||
document.addEventListener("pointerup", pointerup);
|
||||
document.addEventListener("pointercancel", pointerup);
|
||||
document.addEventListener("pointermove", pointermove);
|
||||
document.dispatchEvent(new PointerEvent("pointermove", ev));
|
||||
}
|
||||
|
||||
this.addEventListener("pointerdown", pointerdown);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {null | string} oldValue
|
||||
* @param {null | string} newValue
|
||||
*/
|
||||
attributeChangedCallback(name, oldValue, newValue) {
|
||||
switch (name) {
|
||||
case "value": {
|
||||
if (newValue !== oldValue) {
|
||||
this.defaultValue = this.value = newValue ?? "#FFFFFF";
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "disabled": {
|
||||
this.tabIndex = newValue == null ? 0 : -1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* See {@link HTMLInputElement.disabled}
|
||||
* @type {boolean}
|
||||
*/
|
||||
get disabled() {
|
||||
return this.hasAttribute("disabled");
|
||||
}
|
||||
set disabled(value) {
|
||||
this.toggleAttribute("disabled", value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or set the color hue on a scale of 0 to 360.
|
||||
* @type {number}
|
||||
*/
|
||||
get hue() {
|
||||
return Math.round(this._value.H * 360);
|
||||
}
|
||||
set hue(value) {
|
||||
const hsv = this.valueAsHSV;
|
||||
hsv.H = CommonClamp(value / 360, 0, 1);
|
||||
this.valueAsHSV = hsv;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or set the color saturation on a scale of 0 to 255.
|
||||
* @type {number}
|
||||
*/
|
||||
get saturation() {
|
||||
return Math.round(this._value.S * 255);
|
||||
}
|
||||
set saturation(value) {
|
||||
const hsv = this.valueAsHSV;
|
||||
hsv.S = CommonClamp(value / 255, 0, 1);
|
||||
this.valueAsHSV = hsv;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or set the color brightness on a scale of 0 to 255.
|
||||
* @type {number}
|
||||
*/
|
||||
get brightness() {
|
||||
return Math.round(this._value.V * 255);
|
||||
}
|
||||
set brightness(value) {
|
||||
const hsv = this.valueAsHSV;
|
||||
hsv.V = CommonClamp(value / 255, 0, 1);
|
||||
this.valueAsHSV = hsv;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the error message that would be displayed if the user submits the form, or an empty string if no error message.
|
||||
* It also triggers the standard error message, such as "this is a required field".
|
||||
* The result is that the user sees validation messages without actually submitting.
|
||||
*
|
||||
* See {@link HTMLInputElement.validationMessage}
|
||||
* @type {string}
|
||||
*/
|
||||
get validationMessage() {
|
||||
return this.internals_?.validationMessage ?? "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a ValidityState object that represents the validity states of an element.
|
||||
*
|
||||
* See {@link HTMLInputElement.validity}
|
||||
* @returns {ValidityState}
|
||||
*/
|
||||
get validity() {
|
||||
return this.internals_?.validity ?? Object.freeze({
|
||||
badInput: false,
|
||||
customError: false,
|
||||
patternMismatch: false,
|
||||
rangeOverflow: false,
|
||||
rangeUnderflow: false,
|
||||
stepMismatch: false,
|
||||
tooLong: false,
|
||||
tooShort: false,
|
||||
typeMismatch: false,
|
||||
valid: true,
|
||||
valueMissing: false,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* See {@link HTMLInputElement.reportValidity}
|
||||
* @returns {boolean}
|
||||
*/
|
||||
reportValidity() {
|
||||
return this.internals_?.reportValidity() ?? true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets or retrieves the initial contents of the object.
|
||||
*
|
||||
* See {@link HTMLInputElement.defaultValue}
|
||||
* @type {string}
|
||||
*/
|
||||
defaultValue = "#FFFFFF";
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @type {Readonly<HSVColor>}
|
||||
*/
|
||||
_value = { H: 0, S: 0, V: 1 };
|
||||
|
||||
/**
|
||||
* Get or set the color {@link value} via an object with [HSV](https://en.wikipedia.org/wiki/HSL_and_HSV) color values.
|
||||
* All HSV values are expected to be normalized to the `[0, 1]` range.
|
||||
* @type {HSVColor}
|
||||
*/
|
||||
get valueAsHSV() {
|
||||
return { ...this._value };
|
||||
}
|
||||
set valueAsHSV(value) {
|
||||
if (
|
||||
CommonIsObject(value)
|
||||
&& CommonIsFinite(value.H, 0, 1)
|
||||
&& CommonIsFinite(value.S, 0, 1)
|
||||
&& CommonIsFinite(value.V, 0, 1)
|
||||
) {
|
||||
value = CommonPick(value, ["H", "S", "V"]);
|
||||
} else {
|
||||
value = { H: 0, S: 0, V: 1 };
|
||||
}
|
||||
|
||||
this._value = value;
|
||||
const rgb = ColorPickerHSVToCSS(value);
|
||||
/** @type {null | HTMLElement} */
|
||||
const knob = this.shadowRoot?.querySelector(".knob-circle");
|
||||
knob?.style.setProperty("background-color", rgb);
|
||||
this.style.setProperty("--hue", (value.H * 360).toString());
|
||||
if (this._pressedOldValue == null) {
|
||||
this._setKnobPosition(100 * value.S, 100 * (1 - value.V));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets or retrieves the initial contents of the object.
|
||||
*
|
||||
* See {@link HTMLInputElement.value}
|
||||
* @type {string}
|
||||
*/
|
||||
get value() {
|
||||
return ColorPickerHSVToCSS(this._value);
|
||||
}
|
||||
set value(value) {
|
||||
this.valueAsHSV = ColorPickerCSSToHSV(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets or retrieves the name of the object.
|
||||
*
|
||||
* See {@link HTMLInputElement.name}
|
||||
* @type {string}
|
||||
*/
|
||||
get name() {
|
||||
return this.getAttribute("name");
|
||||
}
|
||||
set name(value) {
|
||||
this.setAttribute("name", value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the position the knob
|
||||
* @private
|
||||
* @param {number} left - The relative left position on a scale of 0-100
|
||||
* @param {number} top - The relative top position on a scale of 0-100
|
||||
*/
|
||||
_setKnobPosition(left, top) {
|
||||
/** @type {null | HTMLElement} */
|
||||
const knob = this.shadowRoot?.querySelector(".knob");
|
||||
if (!knob) {
|
||||
return;
|
||||
}
|
||||
knob.style.left = `${left}%`;
|
||||
knob.style.top = `${top}%`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("bc-tint-input", HTMLColorTintElement);
|
||||
|
|
|
|||
4
BondageClub/Scripts/Typedef.d.ts
vendored
4
BondageClub/Scripts/Typedef.d.ts
vendored
|
|
@ -134,6 +134,10 @@ interface HTMLElementEventMap {
|
|||
bcTouchHold: MouseEvent;
|
||||
}
|
||||
|
||||
interface HTMLElementTagNameMap {
|
||||
"bc-tint-input": HTMLTintInputElement,
|
||||
}
|
||||
|
||||
declare namespace ElementButton {
|
||||
/** An input type for representing one or more text nodes and/or **non-interactive** DOM elements (_e.g._ `<i>` or `<code>`) */
|
||||
type StaticNode = null | string | Node | HTMLOptions<any> | readonly (null | undefined | string | Node | HTMLOptions<any>)[];
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue