color-elements
Version:
A set of web components for working with color. A Color.js project.
320 lines (280 loc) • 6.73 kB
JavaScript
import "../color-slider/color-slider.js";
import * as dom from "../common/dom.js";
import ColorElement from "../common/color-element.js";
import { getStep } from "../common/util.js";
const Self = class ChannelSlider extends ColorElement {
static tagName = "channel-slider";
static url = import.meta.url;
static styles = import("./channel-slider.css", { with: { type: "css" } }).catch(
() => new URL("./channel-slider.css", import.meta.url),
);
static shadowTemplate = `
<label class="color-slider-label" part="label">
<slot>
<span id="channel_info" part="channel-info"></span>
<input type="number" part="spinner" min="0" max="1" step="0.01" id="spinner" />
</slot>
<color-slider part="color_slider" exportparts="slider" id="slider"></color-slider>
</label>`;
constructor () {
super();
this._el = dom.named(this);
this._el.slot = this.shadowRoot.querySelector("slot");
}
connectedCallback () {
super.connectedCallback?.();
this._el.slider.addEventListener("input", this);
this._el.slot.addEventListener("input", this);
}
disconnectedCallback () {
this._el.slider.removeEventListener("input", this);
this._el.slot.removeEventListener("input", this);
}
handleEvent (event) {
if (event.type === "input") {
this.value = event.target.value;
}
}
colorAt (value) {
let color = this.defaultColor.clone();
if (this.channel === "alpha") {
color.alpha = value / 100;
}
else if (this.channel in color.space.coords) {
color.set(this.channel, value);
}
return color;
}
colorAtProgress (progress) {
// Map progress to min - max range
let value = this.min + progress * (this.max - this.min);
return this.colorAt(value);
}
get minColor () {
return this.colorAt(this.min);
}
get maxColor () {
return this.colorAt(this.max);
}
get stops () {
return [
this.minColor,
this.colorAtProgress(0.25),
this.colorAtProgress(0.5),
this.colorAtProgress(0.75),
this.maxColor,
];
}
get progress () {
return this._el.slider.progress;
}
progressAt (p) {
return this._el.slider.progressAt(p);
}
updated ({ changed }) {
for (let name of ["space", "min", "max", "step", "value", "defaultValue"]) {
if (!changed.has(name)) {
continue;
}
this._el.slider[name] = this[name];
if (name !== "space") {
this._el.spinner[name] = this[name];
if (name === "value" && this.value !== undefined) {
this._el.spinner.value = Number(this.value.toPrecision(4));
if (!CSS.supports("field-sizing", "content")) {
let valueStr = this._el.spinner.value;
this._el.spinner.style.setProperty("--value-length", valueStr.length);
}
}
}
}
if (
changed.has("defaultColor") ||
changed.has("space") ||
changed.has("channel") ||
changed.has("min") ||
changed.has("max")
) {
this._el.slider.stops = this.stops;
if (
changed.has("space") ||
changed.has("channel") ||
changed.has("min") ||
changed.has("max")
) {
this._el.channel_info.innerHTML = `${this.channelName} <em>(${this.min} – ${this.max})</em>`;
}
}
}
get channelName () {
return this.channelSpec?.name ?? this.channel;
}
static props = {
space: {
default: "oklch",
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;
},
},
gamut: {
type: String,
default: "",
convert (value) {
if (value && value !== "auto" && value !== "none" && !(value in Self.Color.spaces)) {
return this.gamut;
}
return value;
},
changed () {
this._el.slider.gamut = this.gamut;
},
},
channel: {
type: String,
default () {
return Object.keys(this.space.coords)[0];
},
// get () {
// let value = this.props.channel;
// let space = this.space;
// console.log(this.props, value, space);
// if (!space || space.coords[value]) {
// return value;
// }
// return Object.keys(this.space.coords)[0];
// },
// set: true,
// reflect: true,
},
channelSpec: {
get () {
if (this.channel === "alpha") {
return {
name: "Alpha",
};
}
let channelSpec = this.space?.coords[this.channel];
if (!channelSpec && this.space) {
channelSpec = Object.values(this.space.coords)[0];
console.warn(
`Unknown channel ${this.channel} in space ${this.space}. Using first channel (${channelSpec.name}) instead.`,
);
}
return channelSpec;
},
},
refRange: {
get () {
let channelSpec = this.channelSpec;
return channelSpec?.refRange ?? channelSpec?.range ?? [0, 100];
},
},
min: {
type: Number,
default () {
return this.refRange[0];
},
},
max: {
type: Number,
default () {
return this.refRange[1];
},
},
step: {
type: Number,
default () {
return getStep(this.max, this.min, { minSteps: 100 });
},
},
defaultValue: {
type: Number,
default () {
if (this.channel === "alpha") {
return this.defaultColor.alpha * 100;
}
else if (this.channel in this.defaultColor.space.coords) {
return this.defaultColor.get(this.channel);
}
else {
let firstChannel = Object.keys(this.defaultColor.space.coords)[0];
return this.defaultColor.get(firstChannel);
}
},
reflect: {
from: "value",
},
},
value: {
type: Number,
defaultProp: "defaultValue",
reflect: false,
},
defaultColor: {
get type () {
return Self.Color;
},
convert (color) {
return color.to(this.space);
},
default () {
let coords = [];
for (let channel in this.space.coords) {
let spec = this.space.coords[channel];
let range = spec.refRange ?? spec.range ?? [0, 100];
coords.push((range[0] + range[1]) / 2);
}
return new Self.Color(this.space, coords);
},
reflect: {
from: "color",
},
},
color: {
get type () {
return Self.Color;
},
get () {
return this.colorAt(this.value);
},
dependencies: ["defaultColor", "space", "channel", "value"],
set (value) {
this.defaultColor = value;
this.value = this.defaultValue;
},
},
};
static events = {
change: {
from () {
return this._el.slider;
},
},
input: {
from () {
return this._el.slider;
},
},
valuechange: {
propchange: "value",
},
colorchange: {
propchange: "color",
},
};
static formBehavior = {
like: el => el._el.slider,
role: "slider",
valueProp: "value",
changeEvent: "valuechange",
};
};
Self.define();
export default Self;