diff --git a/package.json b/package.json index b1a23e4..85712c2 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "packageManager": "yarn@4.2.2", "dependencies": { "@astrojs/starlight": "^0.23.0", - "d3-color": "^3.1.0", + "colorjs.io": "^0.5.0", "konva": "^9.3.8", "lit": "^3.1.3", "manatea": "^0.6.1", @@ -26,7 +26,6 @@ }, "devDependencies": { "@astrojs/react": "^3.3.4", - "@types/d3-color": "^3.1.3", "@types/react": "^18.3.2", "@types/react-dom": "^18.3.0", "astro": "^4.8.6", diff --git a/src/components/page-color-visualizer/ColorPicker/ColorPicker.ts b/src/components/page-color-visualizer/ColorPicker/ColorPicker.ts index 92f3f17..76164fa 100644 --- a/src/components/page-color-visualizer/ColorPicker/ColorPicker.ts +++ b/src/components/page-color-visualizer/ColorPicker/ColorPicker.ts @@ -7,11 +7,7 @@ import "./ColorSlider"; import { colorController, type Type, type Kind } from "../color-controller"; import { ManateaController } from "../manatea-controller"; -function toHex(color: number) { - return Math.floor(color).toString(16).padStart(2, "0"); -} - -function toFixed(number: number, decimal: number = 2) { +function toFixed(number: number, decimal: number = 0) { const dec = 10 ** decimal; return Math.round(number * dec) / dec; } @@ -63,8 +59,8 @@ export class ColorPicker extends LitElement { const clamp = (n: number) => Math.min(max, Math.max(min, n)); const stored = colorController(); - // @ts-expect-error - const value = stored[category].values[kind]; + const colorInSpace = stored.to(category); + const value = colorInSpace[kind]; const id = `${category}--${kind}`; @@ -76,7 +72,7 @@ export class ColorPicker extends LitElement { .max=${max} .step=${step} .value=${value} - .referenceColor=${stored[category].raw} + .referenceColor=${colorInSpace} .valueToModify=${kind} @input=${(event: Event) => { const element = event.target as ColorSlider; @@ -114,9 +110,7 @@ export class ColorPicker extends LitElement { override render() { const stored = colorController(); - const hexRGB = `#${toHex(stored.rgb.values.r)}${toHex( - stored.rgb.values.g - )}${toHex(stored.rgb.values.b)}`; + return html`
@@ -145,11 +139,9 @@ export class ColorPicker extends LitElement { max: 360, })} -
LCH(${toFixed(
-              stored.lch.values.l
-            )}% ${toFixed(stored.lch.values.c)} ${toFixed(
-              stored.lch.values.h
-            )})
+
${stored
+              .to("lch")
+              .toString({ precision: 3 })}
@@ -181,45 +173,47 @@ export class ColorPicker extends LitElement { max: 127, })} -
Lab(${toFixed(
-              stored.lab.values.l
-            )}% ${toFixed(stored.lab.values.a)} ${toFixed(
-              stored.lab.values.b
-            )})
+
${stored
+              .to("lab")
+              .toString({ precision: 3 })}
- +

RGB

${this.renderInput({ - category: "rgb", + category: "srgb", kind: "r", label: "red", min: 0, - max: 255, + max: 1, + step: 0.01, + mod: (v) => toFixed(v * 255), })} ${this.renderInput({ - category: "rgb", + category: "srgb", kind: "g", label: "green", min: 0, - max: 255, + max: 1, + step: 0.01, + mod: (v) => toFixed(v * 255), })} ${this.renderInput({ - category: "rgb", + category: "srgb", kind: "b", label: "blue", min: 0, - max: 255, + max: 1, + step: 0.01, + mod: (v) => toFixed(v * 255), })} -
rgb(${toFixed(
-              stored.rgb.values.r
-            )} ${toFixed(stored.rgb.values.g)} ${toFixed(
-              stored.rgb.values.b
-            )})
-${hexRGB}
+
${stored
+              .to("srgb")
+              .toString({ precision: 3 })}
+${stored.to("srgb").toString({ format: "hex" })}
@@ -238,9 +232,7 @@ export class ColorPicker extends LitElement { kind: "s", label: "saturation", min: 0, - step: 0.01, - max: 1, - mod: (v) => v * 100, + max: 100, unit: "%", })} ${this.renderInput({ @@ -248,17 +240,13 @@ export class ColorPicker extends LitElement { kind: "l", label: "lightness", min: 0, - step: 0.01, - max: 1, - mod: (v) => v * 100, + max: 100, unit: "%", })} -
hsl(${toFixed(
-              stored.hsl.values.h
-            )} ${toFixed(stored.hsl.values.s) * 100}% ${toFixed(
-              stored.hsl.values.l * 100
-            )}%)
+
${stored
+              .to("hsl")
+              .toString({ precision: 3 })}
diff --git a/src/components/page-color-visualizer/ColorPicker/ColorSlider.ts b/src/components/page-color-visualizer/ColorPicker/ColorSlider.ts index 0d0a3ec..2b767bf 100644 --- a/src/components/page-color-visualizer/ColorPicker/ColorSlider.ts +++ b/src/components/page-color-visualizer/ColorPicker/ColorSlider.ts @@ -1,7 +1,7 @@ import { LitElement, html, css } from "lit"; import { customElement, property, state } from "lit/decorators.js"; -import { type ColorSpaceObject, rgb } from "d3-color"; +import Color from "colorjs.io"; import type { Kind, Type } from "../color-controller"; const height = 20; @@ -47,7 +47,7 @@ export class ColorSlider extends LitElement { value: number | undefined; @state() - referenceColor: ColorSpaceObject = rgb(0, 0, 0); + referenceColor: Color = new Color("srgb", [0, 0, 0]); @state() valueToModify: Kind[Type] = "h"; @@ -58,8 +58,11 @@ export class ColorSlider extends LitElement { getBackgroundRange = () => { clearTimeout(this.throttleId); - const color = this.referenceColor.copy(); - const colorToCompare: Record = { ...color }; + const color = new Color(this.referenceColor); + const keys = Object.keys(color.space.coords); + const colorToCompare: Record = Object.fromEntries( + keys.map((key) => [key as Kind[Type], color[key as Kind[Type]]]) + ); delete colorToCompare[this.valueToModify]; const stringColorToCompare = JSON.stringify(colorToCompare); if (this.prevBackgroundRange) { @@ -80,13 +83,12 @@ export class ColorSlider extends LitElement { const colorArray = new Uint8ClampedArray(nbOfPoints * 4); for (let i = 0; i <= nbOfPoints; i++) { - // @ts-ignore color[this.valueToModify] = i * this.step + this.min; - const rgb = color.rgb(); + const rgb = color.to("srgb"); const position = 4 * i; - colorArray[position + 0] = rgb.r; // R value - colorArray[position + 1] = rgb.g; // G value - colorArray[position + 2] = rgb.b; // B value + colorArray[position + 0] = rgb.r * 255; // R value + colorArray[position + 1] = rgb.g * 255; // G value + colorArray[position + 2] = rgb.b * 255; // B value colorArray[position + 3] = 255; // A value } diff --git a/src/components/page-color-visualizer/LCHPaint/LCHPaint.ts b/src/components/page-color-visualizer/LCHPaint/LCHPaint.ts index 7e70179..42903d1 100644 --- a/src/components/page-color-visualizer/LCHPaint/LCHPaint.ts +++ b/src/components/page-color-visualizer/LCHPaint/LCHPaint.ts @@ -4,7 +4,7 @@ import { query } from "lit/decorators/query.js"; import { colorController } from "../color-controller"; -import { createGenerateColors } from "./generate-colors"; +import { createGenerateColors } from "./generate-lch-colors"; @customElement("lch-paint") export class LCHPaint extends LitElement { @@ -58,7 +58,7 @@ export class LCHPaint extends LitElement { return; } const rect = canvas.getBoundingClientRect(); - const lch = colorController().lch.values; + const lch = colorController().to("lch"); const x = Math.floor((lch.c / 132) * rect.width); const y = Math.floor((1 - lch.l / 100) * rect.height); @@ -75,7 +75,7 @@ export class LCHPaint extends LitElement { return; } - const lch = colorController().lch.values; + const lch = colorController().to("lch"); this.generateColors(lch.h, this.width, this.height).then((colorArray) => { const imageData = new ImageData(colorArray, this.width, this.height); diff --git a/src/components/page-color-visualizer/LCHPaint/generate-colors/index.ts b/src/components/page-color-visualizer/LCHPaint/generate-colors/index.ts deleted file mode 100644 index ed97aa0..0000000 --- a/src/components/page-color-visualizer/LCHPaint/generate-colors/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -import GenerateColorsWorker from "./generate-colors.worker.ts?worker"; - -const worker = new GenerateColorsWorker(); - -export const createGenerateColors = () => { - return ( - hue: number, - width: number, - height: number - ): Promise => { - return new Promise((res) => { - worker.onmessage = (event: MessageEvent) => { - res(event.data.slice()); - }; - worker.postMessage([hue, width, height]); - }); - }; -}; diff --git a/src/components/page-color-visualizer/LCHPaint/generate-colors/generate-colors.ts b/src/components/page-color-visualizer/LCHPaint/generate-lch-colors/generate-lch-colors.ts similarity index 65% rename from src/components/page-color-visualizer/LCHPaint/generate-colors/generate-colors.ts rename to src/components/page-color-visualizer/LCHPaint/generate-lch-colors/generate-lch-colors.ts index da8b8a3..9042509 100644 --- a/src/components/page-color-visualizer/LCHPaint/generate-colors/generate-colors.ts +++ b/src/components/page-color-visualizer/LCHPaint/generate-lch-colors/generate-lch-colors.ts @@ -1,4 +1,4 @@ -import { hcl } from "d3-color"; +import Color from "colorjs.io"; interface Step { coordinates: { x: number; y: number }; @@ -14,14 +14,19 @@ export function* generateColors( width: number, height: number ): Generator { - const color = hcl(hue, 0, 0); + const color = new Color("lch", [50, 50, hue]); for (let x = 0; x < width; x += 1) { for (let y = 0; y < height; y += 1) { color.c = Math.floor((x / width) * 132); // chroma goes to 132 color.l = Math.floor((1 - y / height) * 100); + const rgb = color.to("srgb"); yield { coordinates: { x, y }, - colors: color.rgb(), + colors: { + r: Math.floor(rgb.r * 255), + g: Math.floor(rgb.g * 255), + b: Math.floor(rgb.b * 255), + }, }; } } diff --git a/src/components/page-color-visualizer/LCHPaint/generate-colors/generate-colors.worker.ts b/src/components/page-color-visualizer/LCHPaint/generate-lch-colors/generate-lch-colors.worker.ts similarity index 90% rename from src/components/page-color-visualizer/LCHPaint/generate-colors/generate-colors.worker.ts rename to src/components/page-color-visualizer/LCHPaint/generate-lch-colors/generate-lch-colors.worker.ts index c25dc0b..d5a13d5 100644 --- a/src/components/page-color-visualizer/LCHPaint/generate-colors/generate-colors.worker.ts +++ b/src/components/page-color-visualizer/LCHPaint/generate-lch-colors/generate-lch-colors.worker.ts @@ -1,7 +1,6 @@ -import { generateColors } from "./generate-colors"; +import { generateColors } from "./generate-lch-colors"; function send(message: Uint8ClampedArray) { - // @ts-ignore postMessage(message); } diff --git a/src/components/page-color-visualizer/LCHPaint/generate-lch-colors/index.ts b/src/components/page-color-visualizer/LCHPaint/generate-lch-colors/index.ts new file mode 100644 index 0000000..61f4556 --- /dev/null +++ b/src/components/page-color-visualizer/LCHPaint/generate-lch-colors/index.ts @@ -0,0 +1,29 @@ +import GenerateColorsWorker from "./generate-lch-colors.worker.ts?worker"; + +let prevWorker: Worker | null = null; +const isRunningMap = new WeakMap(); + +export const createGenerateColors = () => { + return ( + hue: number, + width: number, + height: number + ): Promise => { + if (prevWorker && isRunningMap.get(prevWorker)) { + prevWorker.terminate(); + } + + const worker = new GenerateColorsWorker(); + prevWorker = worker; + + return new Promise((res) => { + worker.onmessage = (event: MessageEvent) => { + isRunningMap.set(worker, false); + res(event.data.slice()); + }; + + isRunningMap.set(worker, true); + worker.postMessage([hue, width, height]); + }); + }; +}; diff --git a/src/components/page-color-visualizer/color-controller.ts b/src/components/page-color-visualizer/color-controller.ts index f6a26e5..ececcb8 100644 --- a/src/components/page-color-visualizer/color-controller.ts +++ b/src/components/page-color-visualizer/color-controller.ts @@ -1,21 +1,11 @@ import { orderCup } from "manatea"; -import { - lch, - lab, - rgb, - hsl, - type LabColor, - type HCLColor, - type RGBColor, - type HSLColor, - type ColorSpaceObject, -} from "d3-color"; +import Color from "colorjs.io"; -export type Type = "lch" | "lab" | "rgb" | "hsl"; +export type Type = "lch" | "lab" | "srgb" | "hsl"; export interface Kind { lch: "l" | "c" | "h"; lab: "l" | "a" | "b"; - rgb: "r" | "g" | "b"; + srgb: "r" | "g" | "b"; hsl: "h" | "s" | "l"; } interface TriggerUpdate { @@ -24,82 +14,20 @@ interface TriggerUpdate { value: number; } -interface Stored { - lch: { - values: Record; - raw: HCLColor; - }; - lab: { - values: Record; - raw: LabColor; - }; - rgb: { - values: Record; - raw: RGBColor; - }; - hsl: { - values: Record; - raw: HSLColor; - }; -} - -const bounds: { [type in Type]: Record } = { - lch: { l: [0, 100], c: [0, 132], h: [0, 360] }, - lab: { l: [0, 100], a: [-128, 127], b: [-128, 127] }, - rgb: { r: [0, 255], g: [0, 255], b: [0, 255] }, - hsl: { h: [0, 360], s: [0, 1], l: [0, 1] }, -}; - -const initialLCHColor = lch(57, 110, 283.8); -const initialStored = createStored(initialLCHColor); +const initialLCHColor = new Color("srgb", [0.2, 0.45, 0.98]); const computeUpdate = ( update: TriggerUpdate, - previouslyStored: Stored -): Stored => { - switch (update.type) { - case "lch": { - const newColor = lch(previouslyStored.lch.raw); - const kind = update.kind as Kind["lch"]; - newColor[kind] = clamp(update.value, ...bounds["lch"][kind]); - if (areEqual(newColor, previouslyStored.lch.raw)) { - return previouslyStored; - } - return createStored(newColor); - } - case "lab": { - const newColor = lab(previouslyStored.lab.raw); - const kind = update.kind as Kind["lab"]; - newColor[kind] = clamp(update.value, ...bounds["lab"][kind]); - if (areEqual(newColor, previouslyStored.lab.raw)) { - return previouslyStored; - } - return createStored(newColor); - } - case "rgb": { - const newColor = rgb(previouslyStored.rgb.raw); - const kind = update.kind as Kind["rgb"]; - newColor[kind] = clamp(update.value, ...bounds["rgb"][kind]); - if (areEqual(newColor, previouslyStored.rgb.raw)) { - return previouslyStored; - } - return createStored(newColor); - } - case "hsl": { - const newColor = hsl(previouslyStored.hsl.raw); - const kind = update.kind as Kind["hsl"]; - newColor[kind] = clamp(update.value, ...bounds["hsl"][kind]); - if (areEqual(newColor, previouslyStored.hsl.raw)) { - return previouslyStored; - } - return createStored(newColor); - } - } + previouslyStored: Color +): Color => { + const newColor = new Color(previouslyStored).to(update.type); + newColor[update.kind] = update.value; + return newColor; }; -export const colorController = orderCup( +export const colorController = orderCup( [], - (updates, previouslyStored = initialStored) => { + (updates, previouslyStored = initialLCHColor) => { let valueToStore = previouslyStored; for (const update of updates) { valueToStore = computeUpdate(update, valueToStore); @@ -107,57 +35,3 @@ export const colorController = orderCup( return valueToStore; } ); - -function clamp(number: number, min: number, max: number) { - return Math.min(max, Math.max(min, number || 0)); -} - -function norm(color: ColorSpaceObject) { - return JSON.stringify( - Object.entries(color).sort(([a, b]) => a[0]!.localeCompare(b[0])) - ); -} -function areEqual(colorA: ColorSpaceObject, colorB: ColorSpaceObject) { - return norm(colorA) === norm(colorB); -} - -function createStored(color: ColorSpaceObject): Stored { - const lchColor = lch(color); - const labColor = lab(color); - const rgbColor = rgb(color.formatHex()); - const hslColor = hsl(color.formatHex()); - return { - lch: { - values: { - l: lchColor.l, - c: lchColor.c, - h: lchColor.h, - }, - raw: lchColor, - }, - lab: { - values: { - l: labColor.l, - a: labColor.a, - b: labColor.b, - }, - raw: labColor, - }, - rgb: { - values: { - r: rgbColor.r, - g: rgbColor.g, - b: rgbColor.b, - }, - raw: rgbColor, - }, - hsl: { - values: { - h: hslColor.h, - s: hslColor.s, - l: hslColor.l, - }, - raw: hslColor, - }, - }; -} diff --git a/src/components/page-color-visualizer/index.ts b/src/components/page-color-visualizer/index.ts index ad69319..ee77c54 100644 --- a/src/components/page-color-visualizer/index.ts +++ b/src/components/page-color-visualizer/index.ts @@ -2,9 +2,10 @@ import "./LCHPaint/LCHPaint.ts"; import "./ColorPicker/ColorPicker.ts"; import { colorController } from "./color-controller.ts"; -document.documentElement.style.backgroundColor = - colorController().rgb.raw.formatHex(); +document.documentElement.style.backgroundColor = colorController() + .to("srgb") + .toString(); -colorController.on(({ rgb }) => { - document.documentElement.style.backgroundColor = rgb.raw.formatHex(); +colorController.on((raw) => { + document.documentElement.style.backgroundColor = raw.to("srgb").toString(); }); diff --git a/yarn.lock b/yarn.lock index cf5fe9c..9268f73 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1385,13 +1385,6 @@ __metadata: languageName: node linkType: hard -"@types/d3-color@npm:^3.1.3": - version: 3.1.3 - resolution: "@types/d3-color@npm:3.1.3" - checksum: 10c0/65eb0487de606eb5ad81735a9a5b3142d30bc5ea801ed9b14b77cb14c9b909f718c059f13af341264ee189acf171508053342142bdf99338667cea26a2d8d6ae - languageName: node - linkType: hard - "@types/debug@npm:^4.0.0": version: 4.1.12 resolution: "@types/debug@npm:4.1.12" @@ -2134,6 +2127,13 @@ __metadata: languageName: node linkType: hard +"colorjs.io@npm:^0.5.0": + version: 0.5.0 + resolution: "colorjs.io@npm:0.5.0" + checksum: 10c0/3fa5ed32f2bc62ea133d8df9b267a2bd2e33d3acb8f86bf057b125967f4b7ac607179df15fe4371e4f883760ea939a93a234d6b655fac605166d68e98a55a24d + languageName: node + linkType: hard + "comma-separated-tokens@npm:^2.0.0": version: 2.0.3 resolution: "comma-separated-tokens@npm:2.0.3" @@ -2196,13 +2196,6 @@ __metadata: languageName: node linkType: hard -"d3-color@npm:^3.1.0": - version: 3.1.0 - resolution: "d3-color@npm:3.1.0" - checksum: 10c0/a4e20e1115fa696fce041fbe13fbc80dc4c19150fa72027a7c128ade980bc0eeeba4bcf28c9e21f0bce0e0dbfe7ca5869ef67746541dcfda053e4802ad19783c - languageName: node - linkType: hard - "debug@npm:4, debug@npm:^4.0.0, debug@npm:^4.1.0, debug@npm:^4.3.1, debug@npm:^4.3.4": version: 4.3.4 resolution: "debug@npm:4.3.4" @@ -5032,11 +5025,10 @@ __metadata: dependencies: "@astrojs/react": "npm:^3.3.4" "@astrojs/starlight": "npm:^0.23.0" - "@types/d3-color": "npm:^3.1.3" "@types/react": "npm:^18.3.2" "@types/react-dom": "npm:^18.3.0" astro: "npm:^4.8.6" - d3-color: "npm:^3.1.0" + colorjs.io: "npm:^0.5.0" konva: "npm:^9.3.8" lit: "npm:^3.1.3" manatea: "npm:^0.6.1" -- 2.51.2 From abfd0dca1134429d959ee003537bb1074fef729a Mon Sep 17 00:00:00 2001 From: Ayc0 Date: Fri, 17 May 2024 23:59:59 +0200 Subject: [PATCH 2/6] add oklch & oklab --- .../ColorPicker/ColorPicker.ts | 73 +++++++++++++++++++ .../page-color-visualizer/color-controller.ts | 4 +- 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/src/components/page-color-visualizer/ColorPicker/ColorPicker.ts b/src/components/page-color-visualizer/ColorPicker/ColorPicker.ts index 76164fa..dfa1998 100644 --- a/src/components/page-color-visualizer/ColorPicker/ColorPicker.ts +++ b/src/components/page-color-visualizer/ColorPicker/ColorPicker.ts @@ -113,6 +113,79 @@ export class ColorPicker extends LitElement { return html`
+
+

OkLCH

+
+ ${this.renderInput({ + category: "oklch", + kind: "l", + label: "luminance", + min: 0, + step: 0.01, + max: 1, + unit: "%", + mod: (v) => toFixed(v * 100, 1), + })} + ${this.renderInput({ + category: "oklch", + kind: "c", + label: "chroma", + min: 0, + step: 0.01, + max: 0.4, + })} + ${this.renderInput({ + category: "oklch", + kind: "h", + label: "hue", + min: 0, + max: 360, + })} + +
${stored
+              .to("oklch")
+              .toString({ precision: 3 })}
+
+
+ +
+

OkLab

+
+ ${this.renderInput({ + category: "oklab", + kind: "l", + label: "luminance", + min: 0, + step: 0.01, + max: 1, + unit: "%", + mod: (v) => toFixed(v * 100, 1), + })} + ${this.renderInput({ + category: "oklab", + kind: "a", + label: "a", + shortName: "a", + min: -0.4, + step: 0.01, + max: 0.4, + })} + ${this.renderInput({ + category: "oklab", + kind: "b", + label: "b", + shortName: "b", + min: -0.4, + step: 0.01, + max: 0.4, + })} + +
${stored
+              .to("oklab")
+              .toString({ precision: 3 })}
+
+
+

LCH

diff --git a/src/components/page-color-visualizer/color-controller.ts b/src/components/page-color-visualizer/color-controller.ts index ececcb8..4911c46 100644 --- a/src/components/page-color-visualizer/color-controller.ts +++ b/src/components/page-color-visualizer/color-controller.ts @@ -1,12 +1,14 @@ import { orderCup } from "manatea"; import Color from "colorjs.io"; -export type Type = "lch" | "lab" | "srgb" | "hsl"; +export type Type = "lch" | "lab" | "srgb" | "hsl" | "oklch" | "oklab"; export interface Kind { lch: "l" | "c" | "h"; lab: "l" | "a" | "b"; srgb: "r" | "g" | "b"; hsl: "h" | "s" | "l"; + oklch: "l" | "c" | "h"; + oklab: "l" | "a" | "b"; } interface TriggerUpdate { type: T; -- 2.51.2 From 006b8812f8b864e360c8b96a4c4a74cd2934344a Mon Sep 17 00:00:00 2001 From: Ayc0 Date: Sat, 18 May 2024 00:33:47 +0200 Subject: [PATCH 3/6] use colorsjs gradient --- .../ColorPicker/ColorSlider.ts | 65 ++++++------------- 1 file changed, 19 insertions(+), 46 deletions(-) diff --git a/src/components/page-color-visualizer/ColorPicker/ColorSlider.ts b/src/components/page-color-visualizer/ColorPicker/ColorSlider.ts index 2b767bf..3ca3dd9 100644 --- a/src/components/page-color-visualizer/ColorPicker/ColorSlider.ts +++ b/src/components/page-color-visualizer/ColorPicker/ColorSlider.ts @@ -21,7 +21,7 @@ const thumbStyles = css` `; // Use both the previous and the current to avoid flickers on FF / Safari -const trackBackground = css`var(--track-background, #9e9e9e), var(--previous-track-background, #9e9e9e)`; +const trackBackground = css`var(--track-background, #9e9e9e)`; // const trackBackgroundHover = css`var(--track-background-hover, #bbbbbb)`; // const trackBackgroundFocus = css`var(--track-background-focus, #cbcbcb)`; const trackStyles = css` @@ -57,54 +57,28 @@ export class ColorSlider extends LitElement { throttleId?: number; getBackgroundRange = () => { - clearTimeout(this.throttleId); - const color = new Color(this.referenceColor); - const keys = Object.keys(color.space.coords); - const colorToCompare: Record = Object.fromEntries( - keys.map((key) => [key as Kind[Type], color[key as Kind[Type]]]) - ); - delete colorToCompare[this.valueToModify]; - const stringColorToCompare = JSON.stringify(colorToCompare); - if (this.prevBackgroundRange) { - // If the color hasn't changed, no need to re-compute - if (this.prevColor === stringColorToCompare) { - return this.prevBackgroundRange; - } - const now = performance.now(); - // Only compute every 100ms - if (this.timePrevOp != null && now - this.timePrevOp < 100) { - // Start a timeout of 100ms so that if the last update was cancelled, we'll still have a correct render - this.throttleId = window.setTimeout(() => this.requestUpdate(), 100); - return this.prevBackgroundRange; - } + const coord = this.referenceColor.space.coords[this.valueToModify]; + if (!coord) { + return this.prevBackgroundRange; } - - const nbOfPoints = Math.round((this.max - this.min) / this.step); - - const colorArray = new Uint8ClampedArray(nbOfPoints * 4); - for (let i = 0; i <= nbOfPoints; i++) { - color[this.valueToModify] = i * this.step + this.min; - const rgb = color.to("srgb"); - const position = 4 * i; - colorArray[position + 0] = rgb.r * 255; // R value - colorArray[position + 1] = rgb.g * 255; // G value - colorArray[position + 2] = rgb.b * 255; // B value - colorArray[position + 3] = 255; // A value + const range = coord.refRange || coord.range; + if (!range) { + return this.prevBackgroundRange; } - const canvas = document.createElement("canvas"); - canvas.width = nbOfPoints; - canvas.height = 1; + const from = new Color(this.referenceColor); + from[this.valueToModify] = range[0]; - const imageData = new ImageData(colorArray, nbOfPoints, 1); - const ctx = canvas.getContext("2d", { alpha: false })!; - ctx.putImageData(imageData, 0, 0); + const to = new Color(this.referenceColor); + to[this.valueToModify] = range[1]; + + const stops = Color.steps( + from.range(to, { space: this.referenceColor.space.id, hue: "raw" }), + { steps: 50 } + ); - const dataUrl = canvas.toDataURL(); - this.prevBackgroundRange = dataUrl; - this.prevColor = stringColorToCompare; - this.timePrevOp = performance.now(); - return dataUrl; + this.prevBackgroundRange = `linear-gradient(to right, ${stops.join(", ")})`; + return this.prevBackgroundRange; }; override render() { @@ -113,8 +87,7 @@ export class ColorSlider extends LitElement { .max=${this.max} .step=${this.step} .value=${this.value} - style="--previous-track-background: url(${this - .prevBackgroundRange}); --track-background: url(${this.getBackgroundRange()})" + style="--track-background: ${this.getBackgroundRange()}" type="range" @input=${(event: Event) => { const element = event.target as HTMLInputElement; -- 2.51.2 From ad6265297e9030beb340f7f0cfd9ba16f4ab25b7 Mon Sep 17 00:00:00 2001 From: Ayc0 Date: Sat, 18 May 2024 00:45:50 +0200 Subject: [PATCH 4/6] make canvas smaller + allow to have multiple canvas --- src/components/page-color-visualizer/LCHPaint/LCHPaint.ts | 6 +++--- src/pages/color-visualizer.astro | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/page-color-visualizer/LCHPaint/LCHPaint.ts b/src/components/page-color-visualizer/LCHPaint/LCHPaint.ts index 42903d1..6372f55 100644 --- a/src/components/page-color-visualizer/LCHPaint/LCHPaint.ts +++ b/src/components/page-color-visualizer/LCHPaint/LCHPaint.ts @@ -9,9 +9,9 @@ import { createGenerateColors } from "./generate-lch-colors"; @customElement("lch-paint") export class LCHPaint extends LitElement { @property({ type: Number }) - width = 500; + width = 300; @property({ type: Number }) - height = 300; + height = 200; @query("canvas") canvas?: HTMLCanvasElement | null; @@ -133,7 +133,7 @@ export class LCHPaint extends LitElement { :host canvas { border-radius: 5px; border: 1px solid grey; - max-width: calc(100vw - 4em); + width: 100%; /* Avoid making the page scroll on mobile when we are pressing on it */ touch-action: none; } diff --git a/src/pages/color-visualizer.astro b/src/pages/color-visualizer.astro index fd00b37..47ee66e 100644 --- a/src/pages/color-visualizer.astro +++ b/src/pages/color-visualizer.astro @@ -39,7 +39,7 @@ import Page from '@layouts/Page.astro'; -
+
-- 2.51.2 From 9179c6446aa32847dd7f6044d2155837ef754888 Mon Sep 17 00:00:00 2001 From: Ayc0 Date: Sat, 18 May 2024 01:02:08 +0200 Subject: [PATCH 5/6] add oklch gradient --- .../OkLCHPaint/OkLCHPaint.ts | 163 ++++++++++++++++++ .../generate-oklch-colors.ts | 33 ++++ .../generate-oklch-colors.worker.ts | 21 +++ .../OkLCHPaint/generate-oklch-colors/index.ts | 29 ++++ src/components/page-color-visualizer/index.ts | 1 + src/pages/color-visualizer.astro | 9 +- 6 files changed, 255 insertions(+), 1 deletion(-) create mode 100644 src/components/page-color-visualizer/OkLCHPaint/OkLCHPaint.ts create mode 100644 src/components/page-color-visualizer/OkLCHPaint/generate-oklch-colors/generate-oklch-colors.ts create mode 100644 src/components/page-color-visualizer/OkLCHPaint/generate-oklch-colors/generate-oklch-colors.worker.ts create mode 100644 src/components/page-color-visualizer/OkLCHPaint/generate-oklch-colors/index.ts diff --git a/src/components/page-color-visualizer/OkLCHPaint/OkLCHPaint.ts b/src/components/page-color-visualizer/OkLCHPaint/OkLCHPaint.ts new file mode 100644 index 0000000..5c2a4ea --- /dev/null +++ b/src/components/page-color-visualizer/OkLCHPaint/OkLCHPaint.ts @@ -0,0 +1,163 @@ +import { LitElement, html, css } from "lit"; +import { customElement, property, eventOptions } from "lit/decorators.js"; +import { query } from "lit/decorators/query.js"; + +import { colorController } from "../color-controller"; + +import { createGenerateColors } from "./generate-oklch-colors"; + +@customElement("oklch-paint") +export class OkLCHPaint extends LitElement { + @property({ type: Number }) + width = 300; + @property({ type: Number }) + height = 200; + + @query("canvas") + canvas?: HTMLCanvasElement | null; + + @query(".marker") + marker?: HTMLDivElement | null; + + isPressed = false; + + generateColors = createGenerateColors(); + + @eventOptions({ passive: true }) + onPositionChange(event: PointerEvent) { + const canvas = this.canvas; + if (!canvas || !this.isPressed) { + return; + } + const rect = canvas.getBoundingClientRect(); + + const x = Math.min(Math.max(event.clientX - rect.x, 0), rect.width); + const y = Math.min(Math.max(event.clientY - rect.y, 0), rect.height); + + const l = 1 - y / rect.height; + const c = (x / rect.width) * 0.4; + + colorController([ + { + type: "oklch", + kind: "l", + value: l, + }, + { + type: "oklch", + kind: "c", + value: c, + }, + ]); + } + + updateMarkerPosition = () => { + const marker = this.marker; + const canvas = this.canvas; + if (!marker || !canvas) { + return; + } + const rect = canvas.getBoundingClientRect(); + const oklch = colorController().to("oklch"); + const x = Math.floor((oklch.c / 0.4) * rect.width); + const y = Math.floor((1 - oklch.l) * rect.height); + + marker.style.cssText = `transform: translate(calc(${x}px - 50%), calc(${y}px - 50%))`; + }; + + updateCanvasColors = () => { + const canvas = this.canvas; + if (!canvas) { + return; + } + const ctx = canvas.getContext("2d"); + if (!ctx) { + return; + } + + const oklch = colorController().to("oklch"); + + this.generateColors(oklch.h, this.width, this.height).then((colorArray) => { + const imageData = new ImageData(colorArray, this.width, this.height); + ctx.putImageData(imageData, 0, 0); + }); + }; + + constructor() { + super(); + colorController.on(() => { + this.updateCanvasColors(); + this.updateMarkerPosition(); + }); + } + + override firstUpdated() { + this.updateMarkerPosition(); + } + + override updated(changed: Map) { + if (changed.has("width") || changed.has("height")) { + this.updateCanvasColors(); + } + } + + override render() { + return html` +
+ { + // If we were pressing on the canvas and the mouse moved out of the canvas, it’ll keep the canvas focused + (event.currentTarget as HTMLCanvasElement).setPointerCapture( + event.pointerId + ); + this.isPressed = true; + this.onPositionChange(event); + }} + @pointermove=${this.onPositionChange} + @pointerup=${(event: PointerEvent) => { + // On pointer up, we can release the pointer + (event.currentTarget as HTMLCanvasElement).releasePointerCapture( + event.pointerId + ); + this.isPressed = false; + }} + width="${this.width}px" + height="${this.height}px" + > +
+
+ `; + } + + static override styles = css` + :host canvas { + border-radius: 5px; + border: 1px solid grey; + width: 100%; + /* Avoid making the page scroll on mobile when we are pressing on it */ + touch-action: none; + } + + :host .wrapper { + position: relative; + } + + :host .marker { + position: absolute; + top: 0px; + left: 0px; + pointer-events: none; + width: 6px; + height: 6px; + border-radius: 50%; + border: 1px solid var(--contrast); + z-index: 1; + } + `; +} + +declare global { + interface HTMLElementTagNameMap { + "oklch-paint": OkLCHPaint; + } +} diff --git a/src/components/page-color-visualizer/OkLCHPaint/generate-oklch-colors/generate-oklch-colors.ts b/src/components/page-color-visualizer/OkLCHPaint/generate-oklch-colors/generate-oklch-colors.ts new file mode 100644 index 0000000..23b87f8 --- /dev/null +++ b/src/components/page-color-visualizer/OkLCHPaint/generate-oklch-colors/generate-oklch-colors.ts @@ -0,0 +1,33 @@ +import Color from "colorjs.io"; + +interface Step { + coordinates: { x: number; y: number }; + colors: { + r: number; + g: number; + b: number; + }; +} + +export function* generateColors( + hue: number, + width: number, + height: number +): Generator { + const color = new Color("oklch", [0.5, 0.2, hue]); + for (let x = 0; x < width; x += 1) { + for (let y = 0; y < height; y += 1) { + color.c = (x / width) * 0.4; // chroma goes to 0.4 + color.l = 1 - y / height; + const rgb = color.to("srgb"); + yield { + coordinates: { x, y }, + colors: { + r: Math.floor(rgb.r * 255), + g: Math.floor(rgb.g * 255), + b: Math.floor(rgb.b * 255), + }, + }; + } + } +} diff --git a/src/components/page-color-visualizer/OkLCHPaint/generate-oklch-colors/generate-oklch-colors.worker.ts b/src/components/page-color-visualizer/OkLCHPaint/generate-oklch-colors/generate-oklch-colors.worker.ts new file mode 100644 index 0000000..f25a7ab --- /dev/null +++ b/src/components/page-color-visualizer/OkLCHPaint/generate-oklch-colors/generate-oklch-colors.worker.ts @@ -0,0 +1,21 @@ +import { generateColors } from "./generate-oklch-colors"; + +function send(message: Uint8ClampedArray) { + postMessage(message); +} + +onmessage = function (event: MessageEvent<[number, number, number]>) { + const [hue, width, height] = event.data; + + const colorArray = new Uint8ClampedArray(width * height * 4); + + for (const { coordinates, colors } of generateColors(hue, width, height)) { + const position = 4 * (coordinates.y * width + coordinates.x); + colorArray[position + 0] = colors.r; // R value + colorArray[position + 1] = colors.g; // G value + colorArray[position + 2] = colors.b; // B value + colorArray[position + 3] = 255; // A value + } + + send(colorArray); +}; diff --git a/src/components/page-color-visualizer/OkLCHPaint/generate-oklch-colors/index.ts b/src/components/page-color-visualizer/OkLCHPaint/generate-oklch-colors/index.ts new file mode 100644 index 0000000..0b1a850 --- /dev/null +++ b/src/components/page-color-visualizer/OkLCHPaint/generate-oklch-colors/index.ts @@ -0,0 +1,29 @@ +import GenerateColorsWorker from "./generate-oklch-colors.worker.ts?worker"; + +let prevWorker: Worker | null = null; +const isRunningMap = new WeakMap(); + +export const createGenerateColors = () => { + return ( + hue: number, + width: number, + height: number + ): Promise => { + if (prevWorker && isRunningMap.get(prevWorker)) { + prevWorker.terminate(); + } + + const worker = new GenerateColorsWorker(); + prevWorker = worker; + + return new Promise((res) => { + worker.onmessage = (event: MessageEvent) => { + isRunningMap.set(worker, false); + res(event.data.slice()); + }; + + isRunningMap.set(worker, true); + worker.postMessage([hue, width, height]); + }); + }; +}; diff --git a/src/components/page-color-visualizer/index.ts b/src/components/page-color-visualizer/index.ts index ee77c54..56da9f6 100644 --- a/src/components/page-color-visualizer/index.ts +++ b/src/components/page-color-visualizer/index.ts @@ -1,4 +1,5 @@ import "./LCHPaint/LCHPaint.ts"; +import "./OkLCHPaint/OkLCHPaint.ts"; import "./ColorPicker/ColorPicker.ts"; import { colorController } from "./color-controller.ts"; diff --git a/src/pages/color-visualizer.astro b/src/pages/color-visualizer.astro index 47ee66e..c354d06 100644 --- a/src/pages/color-visualizer.astro +++ b/src/pages/color-visualizer.astro @@ -40,6 +40,13 @@ import Page from '@layouts/Page.astro';
- +
+ In LCH: + +
+
+ In OkLCH: + +
-- 2.51.2 From f266d9436b9717ebf48c44e1fc46e5c7027008d8 Mon Sep 17 00:00:00 2001 From: Ayc0 Date: Sat, 18 May 2024 01:02:53 +0200 Subject: [PATCH 6/6] collapse LCH on mobile --- src/components/page-color-visualizer/ColorPicker/ColorPicker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/page-color-visualizer/ColorPicker/ColorPicker.ts b/src/components/page-color-visualizer/ColorPicker/ColorPicker.ts index dfa1998..f02367c 100644 --- a/src/components/page-color-visualizer/ColorPicker/ColorPicker.ts +++ b/src/components/page-color-visualizer/ColorPicker/ColorPicker.ts @@ -186,7 +186,7 @@ export class ColorPicker extends LitElement {
-
+

LCH

${this.renderInput({