Skip to content

Commit 63a15c2

Browse files
committed
[apps/gamut-mapping] Move to Vue
1 parent 4549779 commit 63a15c2

File tree

4 files changed

+271
-183
lines changed

4 files changed

+271
-183
lines changed

apps/gamut-mapping/index.html

+54-26
Original file line numberDiff line numberDiff line change
@@ -15,35 +15,63 @@ <h1>Gamut Mapping Playground</h1>
1515
<p>Use keyboard arrow keys to increment/decrement, share by copying the URL</p>
1616
</header>
1717

18-
<h2>Browser rendering</h2>
18+
<section id="rendering">
19+
<h2>Browser rendering</h2>
1920

20-
<dl class="swatches">
21-
<div>
22-
<dt>Input</dt>
23-
<dd>
24-
<css-color swatch="large" id="css_color">
25-
<input value="oklch(90% .8 250)" id="css_color_input" />
26-
</css-color>
27-
</dd>
28-
</div>
21+
<dl class="swatches">
22+
<div>
23+
<dt>Input</dt>
24+
<dd>
25+
<css-color swatch="large" @colorchange="event => colorNullable = event.detail.color" :value="colorInput">
26+
<input v-model="colorInput" />
27+
</css-color>
28+
</dd>
29+
</div>
30+
</dl>
31+
</section>
2932

30-
<div>
31-
<dt>To P3</dt>
32-
<dd>
33-
<css-color swatch="large" id="to_p3"></css-color>
34-
</dd>
35-
</div>
33+
<section id="coordinates">
34+
<h2>Coordinates</h2>
35+
<dl id="coords">
36+
<div v-for="(space, spaceIndex) of spaces">
37+
<dt>{{ space.name }}</dt>
38+
<dd>
39+
<dl class="coords">
40+
<div v-for="(info, c) of space.coords">
41+
<dt :title="info.name">{{ c.toUpperCase() }}</dt>
42+
<dd>{{ toPrecision(info.value, 3) }}</dd>
43+
</div>
44+
</dl>
45+
</dd>
46+
</div>
47+
</dl>
48+
</section>
3649

37-
<div>
38-
<dt>To P3 Linear</dt>
39-
<dd>
40-
<css-color swatch="none" id="to_p3linear"></css-color>
41-
</dd>
42-
</div>
43-
</dl>
50+
<section id="gamut-mapped">
51+
<h2>Gamut mapped</h2>
4452

45-
<h2>Gamut mapped</h2>
46-
47-
<dl class="swatches" id="gamut_mapped"></dl>
53+
<dl class="swatches" id="gamut_mapped">
54+
<div v-for="(config, method) in methods" :id="'method-' + method">
55+
<dt>
56+
{{ config.label ?? method[0].toUpperCase() + method.slice(1) }}
57+
<small v-if="config.description" class="description">{{ config.description }}</small>
58+
</dd>
59+
<dd>
60+
<css-color swatch="large" :color="mapped[method].color"></css-color>
61+
<dl class="deltas" v-if="!color.inGamut('p3')">
62+
<template v-for="(delta, c) of mapped[method].deltas">
63+
<dt>Δ{{ c }}</dt>
64+
<dd :class="{
65+
positive: delta > 0,
66+
negative: delta < 0,
67+
zero: delta === 0,
68+
min: minDeltas[c] === delta,
69+
}">{{ delta }}</dd>
70+
</template>
71+
</dl>
72+
</dd>
73+
</div>
74+
</dl>
75+
</section>
4876
</body>
4977
</html>

apps/gamut-mapping/index.js

+121-137
Original file line numberDiff line numberDiff line change
@@ -1,160 +1,144 @@
1+
import { createApp } from "https://unpkg.com/[email protected]/dist/vue.esm-browser.prod.js";
12
import Color from "../../dist/color.js";
23
import methods from "./methods.js";
34

45
globalThis.Color = Color;
56

67
const favicon = document.querySelector('link[rel="shortcut icon"]');
78
const lch = ["L", "C", "H"];
9+
let spacesToShow = [Color.spaces.oklch, Color.spaces.p3, Color.spaces["p3-linear"]]
810

9-
for (let method in methods) {
10-
let config = methods[method];
11-
let label = config.label ?? method[0].toUpperCase() + method.slice(1);
12-
13-
gamut_mapped.insertAdjacentHTML("beforeend", `
14-
<div>
15-
<dt>
16-
${ label }
17-
${ config.description? `<small class="description">${ config.description }</small>` : "" }
18-
</dd>
19-
<dd>
20-
<css-color swatch="large" data-method="${ method }"></css-color>
21-
</dd>
22-
</div>`);
23-
}
24-
25-
css_color_input.addEventListener("input", evt => {
26-
if (css_color.color === null) {
27-
// Probably typing
28-
return;
29-
}
30-
31-
colorUpdated()
32-
33-
let inputColor = css_color.color;
34-
let p3color = inputColor.to("p3");
35-
let p3Linear = inputColor.to("p3-linear");
36-
37-
to_p3.color = p3color;
38-
to_p3linear.color = p3Linear;
39-
40-
let minDeltas;
11+
let app = createApp({
12+
data () {
13+
let params = new URLSearchParams(location.search);
14+
let defaultValue = "oklch(90% .8 250)";
15+
let colorInput = params.get("color") || defaultValue;
16+
let color;
4117

42-
for (let cssColor of gamut_mapped.querySelectorAll("css-color")) {
43-
let method = cssColor.dataset.method;
44-
let color = p3color.clone();
45-
46-
let dd = cssColor.closest("dd");
47-
let stats = dd.querySelector(".deltas");
48-
49-
if (!stats) {
50-
dd.insertAdjacentHTML("beforeend", `<dl class="deltas"></dl>`);
51-
stats = dd.querySelector(".deltas");
18+
try {
19+
color = new Color(colorInput);
5220
}
53-
54-
if (color.inGamut("p3")) {
55-
cssColor.color = color;
56-
stats.innerHTML = "";
57-
continue;
21+
catch (e) {
22+
color = new Color("transparent");
5823
}
5924

60-
minDeltas ??= [];
61-
62-
let mappedColor;
63-
64-
let methodConfig = methods[method];
65-
66-
if (methodConfig.compute) {
67-
mappedColor = methodConfig.compute(inputColor);
68-
}
69-
else {
70-
mappedColor = color.toGamut({ method });
25+
return {
26+
color,
27+
colorNullable: color,
28+
colorInput,
29+
defaultValue,
30+
methods,
31+
params,
32+
Color,
33+
lch: ["L", "C", "H"],
7134
}
72-
73-
cssColor.color = mappedColor;
74-
75-
// Show deltas
76-
let deltas = getDeltas(inputColor, mappedColor, minDeltas, stats);
77-
78-
stats.innerHTML = deltas.map((delta, i) => {
79-
let cl = delta < 0? "negative" : delta > 0? "positive" : "zero";
80-
return `<dt>Δ${ lch[i] }</dt><dd class="${ cl }">${ delta }</dd>`
81-
}).join("");
82-
}
83-
84-
// Find min deltas
85-
if (minDeltas) {
86-
for (let minDelta of minDeltas) {
87-
let index = minDelta.index * 2 + 1;
88-
for (let dl of [].concat(minDelta.stats)) {
89-
let dd = dl.children[minDelta.index * 2 + 1];
90-
dd.classList.add("min");
35+
},
36+
37+
computed: {
38+
colorLCH () {
39+
return this.color.to("oklch");
40+
},
41+
42+
spaces () {
43+
/*
44+
<div v-for="(c, i) of color.to(space).coords">
45+
<dt :title="coordInfo[spaceIndex][i][1].name">{{ coordInfo[spaceIndex][i][0].toUpperCase() }}</dt>
46+
<dd>{{ toPrecision(c, 3) }}</dd>
47+
</div>
48+
*/
49+
return spacesToShow.map(space => {
50+
let coordInfo = Object.entries(space.coords);
51+
let coords = this.color.to(space).coords.map(c => this.toPrecision(c, 3));
52+
return {
53+
name: space.name,
54+
coords: Object.fromEntries(coordInfo.map(([c, info], i) => [c, {value: coords[i], name: info.name, id: c}]))
55+
}
56+
});
57+
},
58+
59+
mapped () {
60+
return Object.fromEntries(Object.entries(this.methods).map(([method, config]) => {
61+
let mappedColor;
62+
if (config.compute) {
63+
mappedColor = config.compute(this.color);
64+
}
65+
else {
66+
mappedColor = this.color.clone().toGamut({ space: "p3", method });
67+
}
68+
69+
let mappedColorLCH = mappedColor.to("oklch");
70+
let deltas = Object.fromEntries(lch.map((c, i) => {
71+
let delta = this.colorLCH.coords[i] - mappedColorLCH.coords[i];
72+
delta = this.toPrecision(delta, 2);
73+
return [c, delta];
74+
}));
75+
76+
deltas.L *= 100; // L is percentage
77+
78+
// Hue is angular, so we need to normalize it
79+
deltas.H = ((deltas.H % 360) + 720) % 360;
80+
deltas.H = this.toPrecision(Math.min(360 - deltas.H, deltas.H), 2);
81+
82+
return [method, {color: mappedColor, deltas}];
83+
}));
84+
},
85+
86+
minDeltas () {
87+
let ret = {};
88+
for (let method in this.mapped) {
89+
let {deltas} = this.mapped[method];
90+
91+
for (let c in deltas) {
92+
let delta = Math.abs(deltas[c]);
93+
let minDelta = ret[c];
94+
95+
if (!minDelta || minDelta >= delta) {
96+
ret[c] = delta;
97+
}
98+
}
99+
}
100+
return ret;
101+
},
102+
},
103+
104+
methods: {
105+
toPrecision: Color.util.toPrecision,
106+
},
107+
108+
watch: {
109+
colorNullable () {
110+
if (this.colorNullable === null) {
111+
// Probably typing
112+
return;
91113
}
92-
}
93-
}
94-
});
95-
96-
let params = new URLSearchParams(location.search);
97-
let color = params.get("color");
98-
99-
if (color) {
100-
css_color.value = color;
101-
}
102-
103-
css_color_input.dispatchEvent(new InputEvent("input"));
104-
105-
106-
107-
function colorUpdated () {
108-
let input = css_color_input;
109-
110-
// Update URL to create a permalink
111-
let params = new URLSearchParams(location.search);
112-
let hadColor = params.has("color");
113-
let value = input.value;
114-
let defaultValue = input.getAttribute("value");
115-
116-
if (value !== defaultValue) {
117-
params.set("color", value);
118-
}
119-
else {
120-
params.delete("color");
121-
}
122-
123-
history[hadColor == params.has("color") ? "replaceState" : "pushState"](null, "", "?" + params.toString());
124-
125-
// Update favicon
126-
favicon.href = `data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg"><rect width="100%" height="100%" fill="${ encodeURIComponent(value) }" /></svg>`;
127-
128-
// Update title
129-
document.title = value + " • Gamut Mapping Playground";
130-
}
131-
132-
function getDeltas (inputColor, mappedColor, minDeltas, stats) {
133-
let mappedColorLCH = mappedColor.to("oklch").coords;
134-
let deltas = inputColor.to("oklch").coords.map((c, i) => {
135-
let delta = mappedColorLCH[i] - c;
136114

137-
if (i === 2) {
138-
// Hue is angular, so we need to normalize it
139-
delta = (delta + 720) % 360;
140-
delta = delta > 180 ? 360 - delta : delta;
141-
}
115+
this.color = this.colorNullable;
116+
},
142117

143-
delta = Color.util.toPrecision(delta, 2);
118+
colorInput (value) {
119+
// Update URL to create a permalink
120+
let hadColor = this.params.has("color");
144121

145-
let minDelta = minDeltas[i];
146-
if (!minDelta || minDelta.value >= Math.abs(delta)) {
147-
if (minDelta && minDelta.value == Math.abs(delta)) {
148-
minDelta.stats = [].concat(minDelta.stats);
149-
minDelta.stats.push(stats);
122+
if (!value || value !== this.defaultValue) {
123+
this.params.set("color", value);
150124
}
151125
else {
152-
minDeltas[i] = {value: Math.abs(delta), stats, index: i};
126+
this.params.delete("color");
153127
}
154128

129+
history[hadColor == this.params.has("color") ? "replaceState" : "pushState"](null, "", "?" + this.params.toString());
130+
131+
// Update favicon
132+
favicon.href = `data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg"><rect width="100%" height="100%" fill="${ encodeURIComponent(value) }" /></svg>`;
133+
134+
// Update title
135+
document.title = value + " • Gamut Mapping Playground";
155136
}
137+
},
138+
139+
isCustomElement (el) {
140+
return el.tagName.toLowerCase() !== "css-color";
141+
}
142+
}).mount(document.body);
156143

157-
return delta;
158-
})
159-
return deltas;
160-
}
144+
globalThis.app = app;

0 commit comments

Comments
 (0)