UNPKG

color-elements

Version:

A set of web components for working with color. A Color.js project.

321 lines (272 loc) 7.29 kB
import ColorElement from "../common/color-element.js"; import { getStep } from "../common/util.js"; let supports = { inSpace: CSS?.supports("background", "linear-gradient(in oklab, red, tan)"), fieldSizing: CSS?.supports("field-sizing", "content"), }; const Self = class ColorSlider extends ColorElement { static tagName = "color-slider"; static url = import.meta.url; static styles = "./color-slider.css"; static shadowTemplate = ` <input type="range" class="color-slider" part="slider" min="0" max="1" step="0.01" /> <slot name="tooltip" class="slider-tooltip"> <input type="number" part="spinner" min="0" max="1" step="0.01" /> </slot> <slot></slot>`; constructor () { super(); this._el = { slider: this.shadowRoot.querySelector(".color-slider"), spinner: this.shadowRoot.querySelector("input[type=number]"), }; } connectedCallback () { super.connectedCallback?.(); this._el.slider.addEventListener("input", this); this._el.spinner.addEventListener("input", this); } disconnectedCallback () { this._el.slider.removeEventListener("input", this); this._el.spinner.removeEventListener("input", this); } handleEvent (event) { if (event.type === "input") { if (this.tooltip === "progress" && event.target === this._el.spinner) { // Convert to value let value = this._el.spinner.value; this.value = this.valueAt(value / 100); } else { this.value = event.target.value; } this.dispatchEvent(new event.constructor(event.type, {...event})); } } propChangedCallback ({name, prop, detail: change}) { if (["min", "max", "step", "value", "defaultValue"].includes(name)) { prop.applyChange(this._el.slider, change); let value = change.value; if (this.tooltip === "progress") { if (name === "value" || name === "defaultValue") { value = +(this.progress * 100).toPrecision(4); } else { // Spinner values when tooltip is "progress" value = ({ min: 1, max: 100, step: 1 })[name]; } } prop.applyChange(this._el.spinner, {...change, value: +(+value).toPrecision(4)}); } if (name === "stops") { // FIXME will fail if there are none values let stops = this.stops; let supported = stops.every(color => CSS.supports("color", color)); // CSS does not support (yet?) a raw hue interpolation, // so we need to fake it with tessellateStops() in cases of polar space and far-apart stops. let farApart = false; let space = this.space; if (space.isPolar) { for (let i = 1; i < stops.length; i++) { // Even though space is polar, color stops might be in non-polar spaces let first = stops[i - 1].to(space); let second = stops[i].to(space); let firstHue = first.get("h"); let secondHue = second.get("h"); if (Math.abs(firstHue - secondHue) >= 180) { farApart = true; break; } } } if (!supported || farApart) { stops = this.tessellateStops({ steps: 3 }); } stops = stops.map(color => color.display()).join(", "); this.style.setProperty("--slider-color-stops", stops); } else if (name === "space" && supports.inSpace) { let space = this.space; let spaceId = space.id; let supported = CSS.supports("background", `linear-gradient(in ${ spaceId }, red, tan)`); if (!supported) { spaceId = this.space.isPolar ? "oklch" : "oklab"; } this.style.setProperty("--color-space", spaceId); } else if (name === "color" || name === "defaultColor") { let color = this.color; if (color) { let displayedColor = color.display(); this.style.setProperty("--color", displayedColor); } } else if (name === "value" || name === "min" || name === "max") { this.style.setProperty("--progress", this.progress); if (name === "value" && !supports.fieldSizing) { let valueStr = this.value + ""; this._el.spinner.style.setProperty("--value-length", valueStr.length); } } else if (name === "tooltip") { if (change.value !== undefined) { let values = this; if (change.value === "progress") { values = { min: 1, max: 100, step: 1, value: +(this.progress * 100).toPrecision(4), }; } ["min", "max", "step", "value"].forEach(name => { this._el.spinner[name] = values[name]; }); } } } tessellateStops (options = {}) { let stops = this.stops; let tessellated = []; for (let i = 1; i < stops.length; i++) { let start = stops[i - 1]; let end = stops[i]; let steps = start.steps(end, { space: this.space, ...options }); tessellated.push(...steps); if (i < stops.length - 1) { tessellated.pop(); } } return tessellated; } get progress () { return this.progressAt(this.value); } progressAt (value) { return (value - this.min) / (this.max - this.min); } valueAt (progress) { return this.min + progress * (this.max - this.min); } colorAt (value) { let progress = this.progressAt(value); return this.colorAtProgress(progress); } colorAtProgress (progress) { let bands = this.scales?.length; if (bands <= 0) { return null; } // FIXME the values outside of [0, 1] should be scaled if (progress >= 1) { return this.scales.at(-1)(progress); } else if (progress <= 0) { return this.scales[0](progress); } let band = 1 / bands; let scaleIndex = Math.max(0, Math.min(Math.floor(progress / band), bands - 1)); let scale = this.scales[scaleIndex]; let color = scale((progress % band) * bands); return color; } static props = { min: { type: Number, default: 0, }, max: { type: Number, default: 1, }, step: { type: Number, default () { return getStep(this.max, this.min, { minSteps: 100 }); }, }, stops: { type: { is: Array, get values () { return Self.Color; }, }, default: el => [], }, defaultValue: { type: Number, default () { return (this.min + this.max) / 2; }, reflect: { from: "value", }, }, value: { type: Number, defaultProp: "defaultValue", reflect: false, }, space: { default () { return this.stops[0]?.space ?? "oklab"; }, parse (value) { if (value instanceof Self.Color.Space || value === null || value === undefined) { return value; } value += ""; return Self.Color.Space.get(value); }, stringify (value) { return value?.id; }, }, color: { get type () { return Self.Color; }, get () { return this.colorAt(this.value); }, dependencies: ["scales", "value"], }, scales: { get () { let stops = this.stops; let scales = []; for (let i = 1; i < stops.length; i++) { let start = stops[i - 1]; let end = stops[i]; let range = start.range(end, { space: this.space }); scales.push(range); } return scales; }, dependencies: ["stops", "space"], }, tooltip: { type: String, }, }; static events = { change: { from () { return this._el.slider; }, }, valuechange: { propchange: "value", }, colorchange: { propchange: "color", }, }; static formAssociated = { like: el => el._el.slider, role: "slider", valueProp: "value", changeEvent: "valuechange", }; }; Self.define(); export default Self;