Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[apps/gamut-mapping] Add gradient tool view #471

Merged
merged 9 commits into from Mar 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
83 changes: 83 additions & 0 deletions apps/gamut-mapping/gradients.css
@@ -0,0 +1,83 @@
:root {
--font-mono: Consolas, Inconsolata, Monaco, monospace;
}
css-color input {
padding: .15em .3em .1em;
border: 1px solid hsl(220 10% 78%);
border-radius: .25em;
font-family: var(--font-mono);

&:invalid {
background-color: hsl(0 60% 95%);
border-color: hsl(0 60% 80%);
}
}

body {
font: 100%/1.5 system-ui;
max-width: 84em;
margin: 1em auto;
padding-inline: 1em;

.gradient {
height: 100px;
display: flex;
> div {
flex: auto;
background: var(--step-color);
}
}
.color-inputs {
display: flex;
justify-content: space-between;
}

.oog .gradient {
height: 15px;
}

.method .info {
display: none;
}
.gradients:not(.flush) {
.method .info {
display: block;
}
}
.gamut-legend {
ul{
list-style: none;
display: flex;
gap: 3em;
padding: 0;
}
.color-block {
height: 1em;
width: 1em;
display: inline-block;
background-color: var(--step-color);
}
}
.footer {
margin-top: 2em;
text-align: center;
}
}
details.timing-info {
margin-top: 2em;
> dl {
display: flex;
}
.timing-result{
padding: 1em;
dd {
margin: 0;
}
}
.metric > div {
display: flex;
justify-content: space-between;
gap: 1em;
}

}
59 changes: 59 additions & 0 deletions apps/gamut-mapping/gradients.html
@@ -0,0 +1,59 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gamut Mapping Experiments - Gradients</title>
<link rel="stylesheet" href="gradients.css" />
<link rel="shortcut icon" />
<script type="module" src="../../elements/css-color/css-color.js"></script>
<script type="module" src="gradients.js"></script>
</head>
<body>
<header>
<h1>Gamut Mapping Gradients</h1>
<p>Use keyboard arrow keys to increment/decrement, share by copying the URL</p>
</header>
<div class="controls">
<label for="space">Interpolation space:</label>
<select v-model="space" name="space">
<option v-for="space in interpolationSpaces" :value="space">{{ space }}</option>
</select><br/>
<label for="maxDeltaE">Max DeltaE between steps:</label>
<input v-model="maxDeltaE" type="number" min="1" name="maxDeltaE"/><br/>
<div>{{steps.length}} Gradient Steps</div>
<label for="flush">Flush:</label> <input type="checkbox" v-model="flush" name="flush">
</div>
<div class="color-inputs">
<css-color swatch="large" @colorchange="colorChangeFrom" :value="from">
<input v-model="from" />
</css-color>
<css-color swatch="large" @colorchange="colorChangeTo" :value="to">
<input v-model="to" />
</css-color>
</div>
<div class="mapped-gradient oog">
<details class="gamut-legend"><summary><strong>Gamut indicator</strong></summary>
Shows the smallest gamut that the color from the unmapped gradient fits in.
<ul>
<li><span class="color-block" style="--step-color:yellowgreen"></span> In sRGB</li>
<li><span class="color-block" style="--step-color:gold"></span> In p3</li>
<li><span class="color-block" style="--step-color:orange"></span> In rec2020</li>
<li><span class="color-block" style="--step-color:red"></span> Out of rec2020</li>
</ul>
</details>
<div class="gradient">
<div v-for="[title, step] in oogSteps" :style="{'--step-color': step}" :title="title"></div>
</div>
</div>
<div :class="{flush, 'gradients': true}">
<article class="method" v-for="(i, method) of methods">
<mapped-gradient :key="i" :steps="steps" :method="i" @report-time="reportTime"/>
</article>
</div>
<timing-info :run-results="runResults" />
<div class="footer">
<a href="./index.html">Gamut mapping Playground</a>
</div>
</body>
</html>
113 changes: 113 additions & 0 deletions apps/gamut-mapping/gradients.js
@@ -0,0 +1,113 @@
import { createApp } from "https://unpkg.com/vue@3.2.37/dist/vue.esm-browser.js";
import Color from "../../dist/color.js";
import Gradient from "./mapped-gradient.js";
import TimingInfo from "./timing-info.js";

globalThis.Color = Color;

let app = createApp({
data () {
let params = new URLSearchParams(location.search);
const urlFromColor = params.get("from");
const urlToColor = params.get("to");
const from = urlFromColor || "oklch(90% .8 250)";
const to = urlToColor || "oklch(40% .1 20)";
const methods = ["none", "clip", "scale-lh", "css", "raytrace", "edge-seeker"];
const runResults = {};
methods.forEach(method => runResults[method] = []);
return {
methods: methods,
from: from,
to: to,
parsedFrom: this.tryParse(from),
parsedTo: this.tryParse(to),
space: "oklch",
maxDeltaE: 10,
flush: false,
params: params,
interpolationSpaces: ["oklch", "oklab", "p3", "rec2020", "lab"],
runResults: runResults,
};
},

computed: {
steps () {
if ( !this.parsedFrom || !this.parsedTo) {
return [];
}
const from = new Color(this.parsedFrom);
let steps = from.steps(this.parsedTo, {
maxDeltaE: this.maxDeltaE,
space: this.space,
});
return steps;
},
oogSteps () {
return this.steps.map(step => {
switch (true) {
case step.inGamut("srgb"):
return ["in srgb", "yellowgreen"];
case step.inGamut("p3"):
return ["in p3", "gold"];
case step.inGamut("rec2020"):
return ["in rec2020", "orange"];
default:
return ["out of rec2020", "red"];
}
});
},
},

methods: {
colorChangeFrom (event) {
this.parsedFrom = this.tryParse(event.detail.color) || this.parsedFrom;
},
colorChangeTo (event) {
this.parsedTo = this.tryParse(event.detail.color) || this.parsedFrom;
},
tryParse (input) {
try {
const color = new Color.parse(input);
return color;
}
catch (error) {
// do nothing
}
},
reportTime ({time, method}) {
this.runResults[method].push(time);
this.runResults = {...this.runResults};
},
},

watch: {
from: {
handler (value) {
this.params.set("from", value);
history.pushState(null, "", "?" + this.params.toString());
},
deep: true,
immediate: true,
},
to: {
handler (value) {
this.params.set("to", value);
history.pushState(null, "", "?" + this.params.toString());
},
deep: true,
immediate: true,
},
},

components: {
"mapped-gradient": Gradient,
"timing-info": TimingInfo,
},
compilerOptions: {
isCustomElement (tag) {
return tag === "css-color";
},
},
}).mount(document.body);

globalThis.app = app;
69 changes: 69 additions & 0 deletions apps/gamut-mapping/mapped-gradient.js
@@ -0,0 +1,69 @@
import methods from "./methods.js";

export default {
props: {
method: String | Object,
steps: Array,
},

emits: ["report-time"],

data () {
return {
time: 0,
mappedSteps: [],
};
},

computed: {
name () {
return methods[this.method]?.label || "None";
},
},

methods: {
mapSteps () {
const start = performance.now();
let steps = this.steps.map(step => {
let mappedColor;
if (this.method === "none") {
return step;
}
if (methods[this.method].compute) {
mappedColor = methods[this.method].compute(step);
}
else {
mappedColor = step.clone().toGamut({ space: "p3", method: this.method });
}
return mappedColor;
});
this.time = Color.util.toPrecision(performance.now() - start, 4);
this.$emit("report-time", {time: this.time, method: this.method});
this.mappedSteps = steps;
},
},

watch: {
steps: {
handler () {
this.mapSteps();
},
immediate: true,
},
},

compilerOptions: {
isCustomElement (tag) {
return tag === "css-color";
},
},

template: `
<div class="mapped-gradient">
<div class="info"><strong>{{ name }}</strong> {{time}}ms</div>
<div class="gradient" :title="name">
<div v-for="step in mappedSteps" :style="{'--step-color': step}" :title="name + ' ' + step"></div>
</div>
</div>
`,
};
47 changes: 47 additions & 0 deletions apps/gamut-mapping/timing-info.js
@@ -0,0 +1,47 @@
import methods from "./methods.js";
import Color from "../../dist/color.js";

export default {
props: {
runResults: Object,
},

computed: {
results () {
const [none, ...methodsTested] = Object.keys(this.runResults);
return methodsTested.map(method => {
const data = this.runResults[method];
const total = data.reduce((acc, value) => acc + value);

return {
id: method,
name: methods[method].label,
metrics: {
runs: data.length,
min: Color.util.toPrecision(Math.min(...data), 8),
max: Color.util.toPrecision(Math.max(...data), 8),
mean: Color.util.toPrecision(total / data.length, 8),
},
};
}).sort((a, b) => a.metrics.mean - b.metrics.mean);
},
},
template: `
<details class="timing-info"><summary>Timing info</summary>
<dl>
<div v-for="method in results" :key="method.id" class="timing-result">
<dt><strong>{{ method.name }}</strong></dt>
<dd>
<dl class="metric">
<div v-for="(value, metric) of method.metrics">
<dt :title="metric">{{ metric.toUpperCase() }}</dt>
<dd>{{ value }}</dd>
</div>
</dl>
</dd>
</div>
</dl>
</details>
`,
};