diff --git a/src/color-scale/color-scale.js b/src/color-scale/color-scale.js index 28e305c3..402a3f43 100644 --- a/src/color-scale/color-scale.js +++ b/src/color-scale/color-scale.js @@ -73,6 +73,19 @@ const Self = class ColorScale extends NudeElement { swatch.textContent = name; if (this.info) { swatch.info = this.info; + + if (this.vs && !["previous", "next"].includes(this.vs)) { + // If there is a vs color, use it as the reference for the deltas + swatch.vs = this.vs; + } + else if (this.vs === "previous" && i > 0) { + // Otherwise, use the previous color + swatch.vs = colors[i - 1].color; + } + else if (this.vs === "next" && i < colors.length - 1) { + // Or the next color + swatch.vs = colors[i + 1].color; + } } i++; } @@ -155,6 +168,15 @@ const Self = class ColorScale extends NudeElement { additionalDependencies: ["info"], }, info: {}, + vs: { + parse (value) { + if (value instanceof Color || value === "previous" || value === "next" || value === null || value === undefined) { + return value; + } + + return new Color(value); + }, + }, }; } diff --git a/src/color-swatch/README.md b/src/color-swatch/README.md index 0658998d..ff5600b1 100644 --- a/src/color-swatch/README.md +++ b/src/color-swatch/README.md @@ -257,6 +257,7 @@ If you don’t, the `` element will be used. | `info` | `info` | `string` | - | Comma-separated list of coords of the current color to be shown. | | `value` | `value` | `string` | - | The current value of the swatch. | | `size` | - | `large` | - | The size of the swatch. Currently, it is used only to make a large swatch. | +| `vs` | `vs` | `Color` | `string` | - | The second color to use when calculating the difference (delta) and contrast with the current color. | | `property` | `property` | `string` | - | CSS property to bind to. | | `scope` | `scope` | `string` | `:root` | CSS selector to use as the scope for the specified CSS property. | | `gamuts` | `gamuts` | `string` | `srgb, p3, rec2020: P3+, prophoto: PP` | Comma-separated list of gamuts to be used by the gamut indicator. | diff --git a/src/color-swatch/color-swatch.css b/src/color-swatch/color-swatch.css index 279c0a99..4de7c750 100644 --- a/src/color-swatch/color-swatch.css +++ b/src/color-swatch/color-swatch.css @@ -7,6 +7,8 @@ 0 0 / calc(2 * var(--_transparency-cell-size)) calc(2 * var(--_transparency-cell-size)) content-box border-box var(--_transparency-background) ); + --_positive-deltaE-color: var(--positive-deltaE-color, hsl(120, 80%, 25%)); + --_negative-deltaE-color: var(--negative-deltaE-color, hsl(0, 85%, 40%)); position: relative; display: inline-flex; @@ -67,26 +69,39 @@ slot { display: none; gap: .5em; + dd { + margin: 0; + font-weight: bold; + font-variant-numeric: tabular-nums; + } + &:is(:host([size="large"]) &) { display: grid; - grid-template-columns: max-content auto; - gap: .1em .2em; - font-size: max(9px, 80%); - justify-content: start; + grid-template-columns: subgrid; - .coord { + .info { display: contents; + + dd:not(.deltaE, :has(~ dd)) { + grid-column: span 2; + } } } - .coord { + .info { display: flex; gap: .2em; - dd { - margin: 0; - font-weight: bold; - font-variant-numeric: tabular-nums; + dd.deltaE { + color: var(--_deltaE-color); + + &.positive { + --_deltaE-color: var(--_positive-deltaE-color); + } + + &.negative { + --_deltaE-color: var(--_negative-deltaE-color); + } } } } @@ -110,6 +125,18 @@ slot { font-size: 80%; } + &:is(:host([size="large"]) &) { + display: grid; + grid-template-columns: repeat(3, max-content); + gap: .1em .2em; + font-size: max(9px, 80%); + justify-content: start; + + & > * { + grid-column: 1 / -1; + } + } + @container style(--details-style: compact) { --_border-color: var(--border-color, color-mix(in oklab, buttonborder 20%, oklab(none none none / 0%))); --_pointer-height: var(--pointer-height, .5em); diff --git a/src/color-swatch/color-swatch.js b/src/color-swatch/color-swatch.js index 288d68e0..d1efdfdb 100644 --- a/src/color-swatch/color-swatch.js +++ b/src/color-swatch/color-swatch.js @@ -78,7 +78,7 @@ const Self = class ColorSwatch extends NudeElement { part: "gamut", exportparts: "label: gamutLabel", gamuts: this.gamuts, - color: this.color + color: this.color, }); this.shadowRoot.append(this._el.gamutIndicator); @@ -116,27 +116,103 @@ const Self = class ColorSwatch extends NudeElement { this.style.setProperty("--color", colorString); } - if (name === "info") { - if (!this.info.length) { - return; - } + if (name === "info" || name === "vs") { + let infoHTML = []; + let coords = []; + let other = []; // DeltaE and contrast - this._el.info ??= Object.assign(document.createElement("dl"), {part: "info"}); - if (!this._el.info.parentNode) { - this._el.colorWrapper.after(this._el.info); - } + if (this.info.length || this.vs) { + this._el.info ??= Object.assign(document.createElement("dl"), {part: "info"}); + if (!this._el.info.parentNode) { + this._el.colorWrapper.after(this._el.info); + } - let info = []; - for (let coord of this.info) { - let [label, channel] = Object.entries(coord)[0]; + for (let item of this.info) { + let [label, data] = Object.entries(item)[0]; + if (label === "deltaE" || label === "contrast") { + let method = label; + let algorithm = data.replace(/(^deltaE)|(\s+contrast$)/, ""); + label = algorithm; + + if (method === "deltaE") { + label = "ΔE " + label; + } + + other.push({method, label, algorithm}); + } + else { + // Color coord + coords.push([label, data]); + } + } + } - let value = this.color.get(channel); - value = typeof value === "number" ? Number(value.toPrecision(4)) : value; + if (coords.length) { + for (let [label, channel] of coords) { + let value = this.color.get(channel); + + let deltaString; + if (this.vs) { + let vsValue = this.vs.get(channel); + + let hasDelta = typeof value === "number" && !Number.isNaN(value) && + typeof vsValue === "number" && !Number.isNaN(vsValue); + if (hasDelta) { + let delta; + + let {space, index} = Color.Space.resolveCoord(channel); + let spaceCoord = Object.values(space.coords)[index]; + + let isAngle = spaceCoord.type === "angle"; + if (isAngle) { + // Constrain angles (shorter arc) + [value, vsValue] = [value, vsValue].map(v => ((v % 360) + 360) % 360); + let angleDiff = vsValue - value; + if (angleDiff > 180) { + value += 360; + } + else if (angleDiff < -180) { + vsValue += 360; + } + + delta = value - vsValue; + } + else { + delta = (value - vsValue) / value * 100; + } + + delta = Number(delta.toPrecision(isAngle ? 4 : 2)); + if (delta !== 0) { + let sign = delta > 0 ? "+" : ""; + let className = delta > 0 ? "positive" : "negative"; + deltaString = `