diff --git a/.eleventy.js b/.eleventy.js index a4fec16..ea7f08a 100644 --- a/.eleventy.js +++ b/.eleventy.js @@ -1,5 +1,6 @@ export default (eleventyConfig) => { eleventyConfig.addPassthroughCopy("css"); + eleventyConfig.addPassthroughCopy("swatch-picker"); eleventyConfig.addPassthroughCopy("favicon.ico"); eleventyConfig.addPassthroughCopy("favicon.png"); return { diff --git a/package-lock.json b/package-lock.json index e071d3a..4608a9e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { - "name": "open-swatch", - "version": "0.1.0", + "name": "openswatch", + "version": "0.0.0-dev", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "open-swatch", - "version": "0.1.0", + "name": "openswatch", + "version": "0.0.0-dev", "license": "MIT", "devDependencies": { "@11ty/eleventy": "^3.0.0" diff --git a/swatch-picker/color-converters.js b/swatch-picker/color-converters.js new file mode 100644 index 0000000..9e40ec3 --- /dev/null +++ b/swatch-picker/color-converters.js @@ -0,0 +1,109 @@ +const oklchToOklab = (oklch) => { + const [ lightness, chroma, hue, alpha = 1 ] = oklch; + const hueRad = (hue * Math.PI) / 180; // Convert degrees to radians + const a = chroma * Math.cos(hueRad); + const b = chroma * Math.sin(hueRad); + return { lightness, a, b, alpha }; +} + +const oklabToXyzD65 = (oklab) => { + const { lightness, a, b, alpha } = oklab; + + // Calculate LMS values + let l = lightness * 1.0 + a * 0.3963377773761749 + b * 0.2158037573099136; + let m = lightness * 1.0 + a * -0.1055613458156586 + b * -0.0638541728258133; + let s = lightness * 1.0 + a * -0.0894841775298119 + b * -1.2914855480194092; + + // Apply the power of 3 to LMS values + l = Math.pow(l, 3); + m = Math.pow(m, 3); + s = Math.pow(s, 3); + + // Convert LMS to XYZ + const x = l * 1.2268798758459243 + m * -0.5578149944602171 + s * 0.2813910456659647; + const y = l * -0.0405757452148008 + m * 1.112286803280317 + s * -0.0717110580655164; + const z = l * -0.0763729366746601 + m * -0.4214933324022432 + s * 1.5869240198367816; + + // Return the XYZD65 object + return { + x: x * 100.0, + y: y * 100.0, + z: z * 100.0, + alpha: alpha + }; +} + +const xyzD65ToLinearRgb = (xyzD65) => { + const { x, y, z, alpha } = xyzD65; + + // Normalize x, y, z by dividing by 100 + const xNorm = x / 100.0; + const yNorm = y / 100.0; + const zNorm = z / 100.0; + + // Calculate red, green, and blue components + const red = xNorm * (12831.0 / 3959.0) + yNorm * (-329.0 / 214.0) + zNorm * (-1974.0 / 3959.0); + const green = xNorm * (-851781.0 / 878810.0) + yNorm * (1648619.0 / 878810.0) + zNorm * (36519.0 / 878810.0); + const blue = xNorm * (705.0 / 12673.0) + yNorm * (-2585.0 / 12673.0) + zNorm * (705.0 / 667.0); + + // Return the LinearRgb object + return { + red, + green, + blue, + alpha + }; +} + +const linearRgbToSrgb = (linearRgb) => { + const { red, green, blue, alpha } = linearRgb; + + // Helper function to clamp values between 0 and 1 + const clamp01 = (value) => Math.max(0, Math.min(1, value)); + + // Helper function for gamma correction + const gamma = (value) => { + return value <= 0.0031308 + ? 12.92 * value + : 1.055 * Math.pow(value, 1 / 2.4) - 0.055; + }; + + // Convert LinearRgb to Srgb + return { + red: Math.round(gamma(clamp01(red)) * 255), + green: Math.round(gamma(clamp01(green)) * 255), + blue: Math.round(gamma(clamp01(blue)) * 255), + alpha + }; +} + +export const oklchToHex = (oklch) => { + + const oklchOb = oklch.match(/oklch\(([^)]+)\)/)[1].replaceAll(' ', ',').split(',').map(Number); + + const oklab = oklchToOklab(oklchOb); + + const xyzD65 = oklabToXyzD65(oklab); + + const linearRgb = xyzD65ToLinearRgb(xyzD65); + + const srgb = linearRgbToSrgb(linearRgb); + + const r = Math.round(srgb.red); + const g = Math.round(srgb.green); + const b = Math.round(srgb.blue); + return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`; +} + +export const oklchToRgb = (oklch) => { + const oklchOb = oklch.match(/oklch\(([^)]+)\)/)[1].replaceAll(' ', ',').split(',').map(Number); + const oklab = oklchToOklab(oklchOb); + const xyzD65 = oklabToXyzD65(oklab); + const linearRgb = xyzD65ToLinearRgb(xyzD65); + const srgb = linearRgbToSrgb(linearRgb); + + const r = Math.round(srgb.red); + const g = Math.round(srgb.green); + const b = Math.round(srgb.blue); + return `rgb(${r}, ${g}, ${b})`; +} diff --git a/swatch-picker/index.html b/swatch-picker/index.html new file mode 100644 index 0000000..ea79a28 --- /dev/null +++ b/swatch-picker/index.html @@ -0,0 +1,109 @@ + + + + + + Swatch Picker | open swatch - the open color system + + + +
+

Open Swatch

+
+

Swatch picker

+ < Back to openswatch +
+
+ +
+
+
+
+

Default use

+

By default the picker will return the variable

+

Example

+ +

Usage

+
+          
+            <open-swatch-picker></open-swatch-picker>
+          
+        
+
+
+
+

Return Hex Code

+

Example

+ +

Usage

+
+          
+            <open-swatch-picker output-type="hex"></open-swatch-picker>
+          
+        
+
+
+
+

Return RGB

+

Example

+ +

Usage

+
+          
+            <open-swatch-picker output-type="rgb"></open-swatch-picker>
+          
+        
+
+
+
+

Hide output

+

Default: false.

+

Example

+ + +

Usage

+
+          
+            <open-swatch-picker hide-output-label="true">
+            </open-swatch-picker>
+          
+        
+
+ +
+

Custom label

+

Default: false. To hide, just dont set `show output label`;

+

Example

+ + +

Usage

+
+          
+            <open-swatch-picker label="Pick a color">
+            </open-swatch-picker>
+          
+        
+
+ +
+

Populate form controls

+

Example

+ + + + +

Usage

+
+          
+            <open-swatch-picker id="color-input"></open-swatch-picker>
+            <output for="color-input"></output>
+            <input type="text" open-swatch-picker="color-input" />
+          
+        
+
+ +
+ + + + diff --git a/swatch-picker/open-swatch-picker.js b/swatch-picker/open-swatch-picker.js new file mode 100644 index 0000000..3ff934c --- /dev/null +++ b/swatch-picker/open-swatch-picker.js @@ -0,0 +1,206 @@ +import { oklchToHex, oklchToRgb } from "./color-converters.js"; + + +const styles = new CSSStyleSheet() +styles.replaceSync(/* css */` + dialog { + border-radius: 1rem; + + &::backdrop { + background: rgba(0, 0, 0, 0.5); + opacity: 0.75; + } + + div[scale] { + display: flex; + + h2 { + display: none; + } + h2[active] { + grid-column: span 13; + display: block; + } + div[swatch] { + margin: 2px; + + button { + width: 2rem; + height: 2rem; + border: none; + border-radius: 0.5rem; + cursor: pointer; + outline: 2px solid transparent; + + &:hover, &:focus { + outline: 2px solid; + } + } + span { + display: none; + } + } + } + } + [part="swatch-picker"] { + display: flex; + align-items: center; + gap: 1rem; + } + [part="output"] { + display: none; + align-items: center; + gap: 0.5rem; + + &[hide-label] { + & [part="output-indicator"]{ + width: 60px; + } + & [part="output-value"]{ + display: none; + } + } + &[active] { + display: flex; + } + } + [part="output-indicator"] { + width: 20px; + height: 20px; + border-radius: 3px; + display: inline-block; + } + + +`); +const scales = ['neutral', 'stone', 'slate', 'red', 'orange', 'amber', 'gold', 'yellow', 'lime', 'green', 'emerald', 'teal', 'cyan', 'sky', 'blue', 'violet', 'purple', 'fuchsia', 'magenta', 'pink', 'rose']; + +const makeScale = (name) => { + let length = name === 'neutral' ? 13 : 12; + return /* html */` +
+

${name}

+ ${Array.from({ length: length }, (_, i) => makeColorSwatch(name, i + 1)).join('')} +
` +} + +const makeColorSwatch = (name, i) => /* html */` +
+ + ${name} ${i} +
` + +const template = document.createElement("template") +template.innerHTML = /* html */` + + +
+ ${scales.map(scale=>makeScale(scale)).join('')} +
+
+` +export class OpenSwatchPicker extends HTMLElement { + static define(tagName = "open-swatch-picker") { + customElements.define(tagName, this) + } + shadowRoot = this.attachShadow({ mode: "open" }); + + #value = ''; + dialog = null; + oklchValue = ''; + + get value() { + return this.#value; + } + + set value(newValue) { + if (this.#value !== newValue) { + this.#value = newValue; + this.onValueChange(newValue); // Call the watcher function + } + } + + get outputType() { + return this.getAttribute('output-type'); + } + + get hideOutputLabel() { + return this.getAttribute('hide-output-label'); + } + + get rgbValue() { return oklchToRgb(this.oklchValue) } + + get hexValue() { return oklchToHex(this.oklchValue) } + + + onValueChange(newValue) { + if (!newValue) return; + const labelText = this.shadowRoot.querySelector('[part=label]'); + labelText.style.display = 'none'; + this.updateLabel(); + this.dialog.close(); + } + + updateLabel() { + + let outputVal = this.value; + + if(this.outputType === 'hex') outputVal = this.hexValue; + if(this.outputType === 'rgb') outputVal = this.rgbValue; + if(this.outputType === 'oklch') outputVal = this.oklchValue; + + this.shadowRoot.querySelector('[part=output-value]').textContent = outputVal; + this.shadowRoot.querySelector('[part=output-indicator]').style.background = outputVal; + + const output = this.shadowRoot.querySelector('[part=output]') + output.setAttribute('active', ''); + if(this.hideOutputLabel === 'true') output.setAttribute('hide-label', ''); + + this.updateFormControls(); + + } + + updateFormControls() { + for (const el of this.getRootNode().querySelectorAll('input[open-swatch-picker],output[for]')) { + if (el instanceof HTMLInputElement && el.getAttribute('open-swatch-picker') == this.id) { + el.value = this.value + } else if (el instanceof HTMLOutputElement && el.htmlFor == this.id) { + el.textContent = this.value + } + } + } + + connectedCallback() { + this.shadowRoot.adoptedStyleSheets = [styles] + this.shadowRoot.replaceChildren(template.content.cloneNode(true)) + + this.dialog = this.shadowRoot.querySelector("dialog"); + + + this.shadowRoot + .querySelector('span[part="label"]') + .textContent = this.getAttribute('label') || 'Choose Color'; + + this.shadowRoot.addEventListener('click', (e) => { + + if(e.target.matches('button') && e.target.closest('[swatch]')) { + const swatchStyle = getComputedStyle(e.target); + const swatchColor = swatchStyle.backgroundColor; + + this.oklchValue = swatchColor; + this.value = e.target.style.background; + } + }); + + } + + +} + +OpenSwatchPicker.define() \ No newline at end of file