import colour_names from './colour_names.js'; import { formatHex, parseHex, converter, differenceCiede2000, formatCss } from './culori.mjs'; import * as culori from './culori.mjs'; const COLOUR_WEIGHT = 1.2; const convertToLab = converter('lab'); const convertToOklab = converter('oklab'); const color_difference = differenceCiede2000(); const weighted_color_difference = (a,b) => { const d = color_difference(a,b); return d*Math.pow(COLOUR_WEIGHT,b.weight); } const named_colours = colour_names.map(({name, hex, weight}) => { const color = convertToLab(parseHex(hex)); color.name = name; color.weight = weight; return color; }) export const nearestNamedColors = culori.nearest(named_colours, weighted_color_difference); export function name_colour(color) { const closest = nearestNamedColors(color, 3); return closest; } export class ColorPickerElement extends HTMLElement { static observedAttributes = ['value']; constructor() { super(); let template = document.getElementById("color-picker-template"); let templateContent = template.content; const sheet = new CSSStyleSheet(); sheet.replaceSync(` * {box-sizing: border-box;} .color-input { display: grid; grid-gap: 0.5em; grid-template-columns: 1fr 1fr; } .color-input label { grid-column: 1; display: grid; grid-template-columns: auto 1fr; align-items: center; grid-gap: 1em; } .color-input input { width: 100%; } .color-input .color-preview { display: block; width: 100%; height: 100%; grid-column: 2; grid-row: 1/5; background: red; padding: 1em; } .color-input span { background: white; padding: 0.2em; display: inline-block; } .color-input .names { list-style: none; padding: 0; } `); const shadowRoot = this.attachShadow({ mode: "open" }); shadowRoot.appendChild(templateContent.cloneNode(true)); shadowRoot.adoptedStyleSheets = [sheet]; const colour_name_datalist = shadowRoot.querySelector('#colour-names'); named_colours.forEach(({name}) => { const option = document.createElement('option'); option.value = option.textContent = name; colour_name_datalist.append(option); }) this.name_input = shadowRoot.querySelector('input[name="name"]'); Array.from(shadowRoot.querySelectorAll('.color-input input[type="range"]')).forEach(i => i.addEventListener('input', () => { this.name_input.value = ''; this.update(); })); this.inputs = Object.fromEntries(Array.from(this.shadowRoot.querySelector('.color-input').elements).map(e=>[e.name, e])); this.name_input.addEventListener('input', e => { this.value = e.target.value.trim(); }); this.value = this.getAttribute('value'); this.update(); } attributeChangedCallback(name, oldValue, newValue) { if(name == 'value') { this.value = newValue; this.name_input.value = ''; } } set value(value) { const col = culori.parse(value); if(col) { this.set_inputs(col); this.update(); } } set_inputs(col) { const okcol = convertToOklab(col); this.inputs.L.value = okcol.l; this.inputs.a.value = okcol.a; this.inputs.b.value = okcol.b; } get value() { const values = Object.fromEntries(Array.from(this.shadowRoot.querySelector('.color-input').elements).map(e=>[e.name, e.valueAsNumber])); const {L, a, b} = values; const col = {mode: 'oklab', l: L, a: a, b: b}; return col; } update() { const col = this.value; const hex = formatHex(col); const closest = name_colour(col); this.shadowRoot.querySelector('output').style.background = formatCss(col); this.shadowRoot.querySelector('.hex').textContent = hex; Array.from(this.shadowRoot.querySelectorAll('.names li span')).forEach((e,i) => { e.textContent = closest[i].name; }) this.dispatchEvent(new CustomEvent('input',{detail: col})); } } customElements.define( "color-picker", ColorPickerElement );