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
+
+
+
+
+
+
+ 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>
+
+
+
+
+
+
+
+
+
+
+
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 */`
+
+
+`
+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