From 2af19574a7f0396ad21adec212405d16f7d7ce77 Mon Sep 17 00:00:00 2001 From: Lea Verou Date: Wed, 14 Feb 2024 17:45:35 -0500 Subject: [PATCH] [apps/gamut-mapping] Support multiple colors --- apps/gamut-mapping/index.html | 78 +++---------- apps/gamut-mapping/index.js | 130 +++------------------ apps/gamut-mapping/map-color.js | 198 ++++++++++++++++++++++++++++++++ apps/gamut-mapping/style.css | 53 +++++++++ 4 files changed, 285 insertions(+), 174 deletions(-) create mode 100644 apps/gamut-mapping/map-color.js diff --git a/apps/gamut-mapping/index.html b/apps/gamut-mapping/index.html index 2a6135588..b6a70e7dd 100644 --- a/apps/gamut-mapping/index.html +++ b/apps/gamut-mapping/index.html @@ -7,7 +7,7 @@ - +
@@ -15,66 +15,22 @@

Gamut Mapping Playground

Use keyboard arrow keys to increment/decrement, share by copying the URL

-
-
-

Browser rendering

- -
-
-
Input - The color as displayed directly by the browser. -
-
- - - -
- Raw coordinates -
-
-
{{ space.name }}
-
-
-
-
{{ c.toUpperCase() }}
-
{{ toPrecision(info.value, 3) }}
-
-
-
-
-
-
-
-
-
-
- -
-

Gamut mapped

- -
-
-
- {{ config.label ?? method[0].toUpperCase() + method.slice(1) }} - {{ config.description }} - -
- -
-
-
Δ{{ c }}
-
{{ delta }}
- -
-
-
-
-
+
+
+ + +
+
+ \ No newline at end of file diff --git a/apps/gamut-mapping/index.js b/apps/gamut-mapping/index.js index 48a9138ef..3f6a748fe 100644 --- a/apps/gamut-mapping/index.js +++ b/apps/gamut-mapping/index.js @@ -1,31 +1,21 @@ -import { createApp } from "https://unpkg.com/vue@3.2.37/dist/vue.esm-browser.prod.js"; +import { createApp } from "https://unpkg.com/vue@3.2.37/dist/vue.esm-browser.js"; import Color from "../../dist/color.js"; import methods from "./methods.js"; +import MapColor from "./map-color.js"; globalThis.Color = Color; const favicon = document.querySelector('link[rel="shortcut icon"]'); -const lch = ["L", "C", "H"]; -let spacesToShow = [Color.spaces.oklch, Color.spaces.p3, Color.spaces["p3-linear"]]; let app = createApp({ data () { let params = new URLSearchParams(location.search); + let urlColors = params.getAll("color").filter(Boolean); let defaultValue = "oklch(90% .8 250)"; - let colorInput = params.get("color") || defaultValue; - let color; - - try { - color = new Color(colorInput); - } - catch (e) { - color = new Color("transparent"); - } + let colors = urlColors.length > 0 ? urlColors : [defaultValue]; return { - color, - colorNullable: color, - colorInput, + colors, defaultValue, methods, params, @@ -35,84 +25,6 @@ let app = createApp({ }, computed: { - colorLCH () { - return this.color.to("oklch"); - }, - - spaces () { - /* -
-
{{ coordInfo[spaceIndex][i][0].toUpperCase() }}
-
{{ toPrecision(c, 3) }}
-
- */ - return spacesToShow.map(space => { - let coordInfo = Object.entries(space.coords); - let coords = this.color.to(space).coords.map(c => this.toPrecision(c, 3)); - return { - name: space.name, - coords: Object.fromEntries(coordInfo.map(([c, info], i) => [c, {value: coords[i], name: info.name, id: c}])) - }; - }); - }, - - mapped () { - return Object.fromEntries(Object.entries(this.methods).map(([method, config]) => { - let mappedColor; - if (config.compute) { - mappedColor = config.compute(this.color); - } - else { - mappedColor = this.color.clone().toGamut({ space: "p3", method }); - } - - let mappedColorLCH = mappedColor.to("oklch"); - let deltas = {E: this.toPrecision(this.color.deltaE(mappedColor, { method: "2000" }), 2)}; - - lch.forEach((c, i) => { - let delta = mappedColorLCH.coords[i] - this.colorLCH.coords[i]; - - if (c === "L") { - // L is percentage - delta *= 100; - } - else if (c === "H") { - // Hue is angular, so we need to normalize it - delta = ((delta % 360) + 720) % 360; - delta = Math.min(360 - delta, delta); - } - - delta = this.toPrecision(delta, 2); - deltas[c] = delta; - }); - - return [method, {color: mappedColor, deltas}]; - })); - }, - - minDeltas () { - let ret = {}; - for (let method in this.mapped) { - let {deltas} = this.mapped[method]; - - for (let c in deltas) { - let delta = Math.abs(deltas[c]); - let minDelta = ret[c]; - - if (!minDelta || minDelta >= delta) { - ret[c] = delta; - } - } - } - return ret; - }, - - ranking () { - let deltaEs = Object.entries(this.mapped).map(([method, {deltas}]) => deltas.E); - deltaEs = deltaEs.map(e => this.toPrecision(e, 2)); - deltaEs.sort((a, b) => a - b); - return deltaEs; - } }, methods: { @@ -121,42 +33,34 @@ let app = createApp({ }, watch: { - colorNullable () { - if (this.colorNullable === null) { - // Probably typing - return; - } - - this.color = this.colorNullable; - }, - - colorInput: { + colors: { handler (value) { // Update URL to create a permalink let hadColor = this.params.has("color"); + this.params.delete("color"); + let colors = value.filter(c => c && c !== this.defaultValue); - if (!value || value !== this.defaultValue) { - this.params.set("color", value); - } - else { - this.params.delete("color"); + if (colors.length > 0) { + colors.forEach(c => this.params.append("color", c)); } history[hadColor == this.params.has("color") ? "replaceState" : "pushState"](null, "", "?" + this.params.toString()); // Update favicon - favicon.href = `data:image/svg+xml,`; + let rects = colors.map((c, i) => ``); + favicon.href = `data:image/svg+xml,${ rects }`; // Update title - document.title = value + " • Gamut Mapping Playground"; + document.title = value.join(", ") + " • Gamut Mapping Playground"; }, immediate: true, + deep: true } }, - isCustomElement (el) { - return el.tagName.toLowerCase() !== "css-color"; - } + components: { + "map-color": MapColor + }, }).mount(document.body); globalThis.app = app; diff --git a/apps/gamut-mapping/map-color.js b/apps/gamut-mapping/map-color.js new file mode 100644 index 000000000..cda2e2fd6 --- /dev/null +++ b/apps/gamut-mapping/map-color.js @@ -0,0 +1,198 @@ +import Color from "../../dist/color.js"; +import methods from "./methods.js"; + +const lch = ["L", "C", "H"]; +let spacesToShow = [Color.spaces.oklch, Color.spaces.p3, Color.spaces["p3-linear"]] + +export default { + props: { + modelValue: String + }, + emits: ["update:modelValue"], + data () { + let defaultValue = "oklch(90% .8 250)"; + let color; + + try { + color = new Color(this.modelValue); + } + catch (e) { + color = new Color("transparent"); + } + + return { + color, + colorNullable: color, + defaultValue, + methods, + Color, + lch: ["L", "C", "H"], + }; + }, + + computed: { + colorInput: { + get () { + return this.modelValue; + }, + set (value) { + this.$emit("update:modelValue", value); + } + }, + colorLCH () { + return this.color.to("oklch"); + }, + + spaces () { + return spacesToShow.map(space => { + let coordInfo = Object.entries(space.coords); + let coords = this.color.to(space).coords.map(c => this.toPrecision(c, 3)); + return { + name: space.name, + coords: Object.fromEntries(coordInfo.map(([c, info], i) => [c, {value: coords[i], name: info.name, id: c}])) + } + }); + }, + + mapped () { + return Object.fromEntries(Object.entries(this.methods).map(([method, config]) => { + let mappedColor; + if (config.compute) { + mappedColor = config.compute(this.color); + } + else { + mappedColor = this.color.clone().toGamut({ space: "p3", method }); + } + + let mappedColorLCH = mappedColor.to("oklch"); + let deltas = {E: this.toPrecision(this.color.deltaE(mappedColor, { method: "2000" }), 2)}; + + lch.forEach((c, i) => { + let delta = mappedColorLCH.coords[i] - this.colorLCH.coords[i]; + + if (c === "L") { + // L is percentage + delta *= 100; + } + else if (c === "H") { + // Hue is angular, so we need to normalize it + delta = ((delta % 360) + 720) % 360; + delta = Math.min(360 - delta, delta); + } + + delta = this.toPrecision(delta, 2); + deltas[c] = delta; + }); + + return [method, {color: mappedColor, deltas}]; + })); + }, + + minDeltas () { + let ret = {}; + for (let method in this.mapped) { + let {deltas} = this.mapped[method]; + + for (let c in deltas) { + let delta = Math.abs(deltas[c]); + let minDelta = ret[c]; + + if (!minDelta || minDelta >= delta) { + ret[c] = delta; + } + } + } + return ret; + }, + + ranking () { + let deltaEs = Object.entries(this.mapped).map(([method, {deltas}]) => deltas.E); + deltaEs = deltaEs.map(e => this.toPrecision(e, 2)); + deltaEs.sort((a, b) => a - b); + return deltaEs; + } + }, + + methods: { + toPrecision: Color.util.toPrecision, + abs: Math.abs + }, + + watch: { + colorNullable () { + if (this.colorNullable === null) { + // Probably typing + return; + } + + this.color = this.colorNullable; + }, + }, + + compilerOptions: { + isCustomElement (tag) { + return tag === "css-color"; + }, + }, + + template: ` +
+

Browser rendering

+ +
+
+
Input + The color as displayed directly by the browser. +
+
+ + + +
+ Raw coordinates +
+
+
{{ space.name }}
+
+
+
+
{{ c.toUpperCase() }}
+
{{ toPrecision(info.value, 3) }}
+
+
+
+
+
+
+
+
+
+
+ +
+

Gamut mapped

+ +
+
+
+ {{ config.label ?? method[0].toUpperCase() + method.slice(1) }} + {{ config.description }} +
+
+ +
+
+
Δ{{ c }}
+
{{ delta }}
+
+
+
+
+
+
` +}; \ No newline at end of file diff --git a/apps/gamut-mapping/style.css b/apps/gamut-mapping/style.css index 2e783ac8c..d876d9d38 100644 --- a/apps/gamut-mapping/style.css +++ b/apps/gamut-mapping/style.css @@ -41,6 +41,12 @@ body > header { } } +h2 { + margin-top: 0; + margin-bottom: 0; + font-size: 150%; +} + input { font: inherit; } @@ -61,6 +67,8 @@ dl.swatches { display: grid; gap: 1.5em; grid-template-columns: repeat(auto-fill, minmax(15em, 1fr)); + margin: 0; + margin-top: .5rem; > div { position: relative; @@ -252,4 +260,49 @@ dl.deltas { .deltas { justify-content: space-between; } +} + +article.color { + position: relative; + margin-bottom: 1rem; + + &:not(:first-child) { + margin-top: 3rem; + } +} + +button { + display: flex; + align-items: center; + padding: .3em .7em; + font: inherit; + background: hsl(220 10% 94%); + border: none; + border-radius: .25em; + cursor: pointer; + transform-origin: bottom; + transition: .1s; + width: max-content; + + &:hover { + background: hsl(220 10% 90%) + } + + &:active { + scale: .9; + } + + svg { + height: 1em; + margin-right: .3em; + opacity: .4; + } +} + +.controls { + position: absolute; + top: 0; + right: 0; + display: flex; + gap: .3em; } \ No newline at end of file