diff --git a/apps/gamut-mapping/gradients.css b/apps/gamut-mapping/gradients.css new file mode 100644 index 00000000..8dab6c0c --- /dev/null +++ b/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; + } + +} \ No newline at end of file diff --git a/apps/gamut-mapping/gradients.html b/apps/gamut-mapping/gradients.html new file mode 100644 index 00000000..62bfd836 --- /dev/null +++ b/apps/gamut-mapping/gradients.html @@ -0,0 +1,59 @@ + + + + + + Gamut Mapping Experiments - Gradients + + + + + + +
+

Gamut Mapping Gradients

+

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

+
+
+ +
+ +
+
{{steps.length}} Gradient Steps
+ +
+
+ + + + + + +
+
+
Gamut indicator + Shows the smallest gamut that the color from the unmapped gradient fits in. + +
+
+
+
+
+
+
+ +
+
+ + + + \ No newline at end of file diff --git a/apps/gamut-mapping/gradients.js b/apps/gamut-mapping/gradients.js new file mode 100644 index 00000000..bbd9011d --- /dev/null +++ b/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; diff --git a/apps/gamut-mapping/mapped-gradient.js b/apps/gamut-mapping/mapped-gradient.js new file mode 100644 index 00000000..d3ece3fc --- /dev/null +++ b/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: ` +
+
{{ name }} {{time}}ms
+
+
+
+
+ `, +}; diff --git a/apps/gamut-mapping/timing-info.js b/apps/gamut-mapping/timing-info.js new file mode 100644 index 00000000..c1c19a16 --- /dev/null +++ b/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: ` +
Timing info +
+
+
{{ method.name }}
+
+
+
+
{{ metric.toUpperCase() }}
+
{{ value }}
+
+
+
+
+
+
+ `, +}; +