diff --git a/src/color-swatch/README.md b/src/color-swatch/README.md index d95c4182..eda39845 100644 --- a/src/color-swatch/README.md +++ b/src/color-swatch/README.md @@ -189,7 +189,7 @@ If used as a property and is not defined via the `label` attribute, its value is ### The `info` attribute You can use the `info` attribute to show information about the color. -Currently, the only type of information supported is color coords (in any color space), but more will be added in the future. +Currently, the types of information supported are color coords (in any color space), the difference (deltaE) and contrast between the current color and another one (specified via [the `vs` attribute](./#the-vs-attribute)). ```html @@ -213,6 +213,25 @@ The `info` attribute plays quite nicely with the `--details-style: compact` styl oklch(70% 0.25 138) ``` +### The `vs` attribute + +You can calculate the difference (deltaE) and contrast between the current color and another one. +To do so, provide the new color via the `vs` attribute and specify one of the [supported algorithms for calculating the difference](https://colorjs.io/docs/color-difference#delta-e-e) ([contrast](https://colorjs.io/docs/contrast) or both) between two colors inside [the `info` attribute](./#the-info-attribute). + +```html + + oklch(70% 0.25 138) + +``` + +If color coords are also specified, the deltas on a coord-by-coord basis will be shown: + +```html + + oklch(70% 0.25 138) + +``` + ### With slot content Before and after: @@ -316,6 +335,7 @@ If you don’t, the `` element will be used. | `value` | `value` | `string` | - | The current value of the swatch. | | `label` | `label` | `string` | - | The label of the swatch (e.g., color name). Defaults to the element text content. | | `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. | @@ -338,6 +358,8 @@ These properties are read-only. | `--transparency-cell-size` | `` | The size of the cells of the transparency gradient. | | `--transparcency-background` | `` | The background color of the transparency gradient. | | `--transparency-darkness` | `` | The opacity of the black color used for dark parts of the transparency gradient. | +| `--positive-delta-color` | `` | The color used for the positive color difference in color coords. | +| `--negative-delta-color` | `` | The color used for the negative color difference in color coords. | ### Parts diff --git a/src/color-swatch/color-swatch.css b/src/color-swatch/color-swatch.css index 6ae2ddb5..395898cf 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-delta-color: var(--positive-delta-color, hsl(120, 80%, 25%)); + --_negative-delta-color: var(--negative-delta-color, hsl(0, 85%, 40%)); position: relative; display: inline-flex; @@ -64,22 +66,29 @@ slot { [part="info"] { margin: 0; display: inline-flex; - display: none; gap: .5em; &:is(:host([size="large"]) &) { display: grid; - grid-template-columns: max-content auto; + grid-template-columns: repeat(3, max-content); gap: .1em .2em; font-size: max(9px, 80%); justify-content: start; - .coord { + & > * { + grid-column: 1 / -1; + } + + .data { display: contents; + + dd:not(.delta, :has(~ dd)) { + grid-column: span 2; + } } } - .coord { + .data { display: flex; gap: .2em; @@ -87,6 +96,34 @@ slot { margin: 0; font-weight: bold; font-variant-numeric: tabular-nums; + + &.delta { + color: var(--_delta-color); + + &::before { + content: "("; + } + + &::after { + content: ")"; + } + + &.positive { + --_delta-color: var(--_positive-delta-color); + + &::before { + content: "(+"; + } + } + + &.negative { + --_delta-color: var(--_negative-delta-color); + } + + &:not(.angle)::after { + content: "%)"; + } + } } } } diff --git a/src/color-swatch/color-swatch.js b/src/color-swatch/color-swatch.js index 10694d6d..ddc479c3 100644 --- a/src/color-swatch/color-swatch.js +++ b/src/color-swatch/color-swatch.js @@ -130,7 +130,7 @@ const Self = class ColorSwatch extends ColorElement { this.style.setProperty("--color", colorString); } - if (name === "colorInfo") { + if (name === "colorInfo" || name === "vsInfo") { if (!this.colorInfo) { return; } @@ -140,21 +140,39 @@ const Self = class ColorSwatch extends ColorElement { this._el.colorWrapper.after(this._el.info); } - let info = []; - for (let coord of this.info) { - let [label, channel] = Object.entries(coord)[0]; + let html = []; + for (let data of this.info) { + let [label, key] = Object.entries(data)[0]; - let value = this.colorInfo[channel]; - if (value === undefined) { + let rawValue = this.colorInfo[key] ?? this.vsInfo?.[key]; + if (rawValue === undefined) { continue; } - value = typeof value === "number" ? Number(value.toPrecision(4)) : value; + let value = typeof rawValue === "number" ? Number(rawValue.toPrecision(4)) : rawValue; + let ret = `
${ label }
${ value }
`; - info.push(`
${ label }
${ value }
`); + if (this.colorDeltas?.[key] && this.infoCoordsResolved?.[key]) { + let delta = this.colorDeltas[key]; + let classes = delta > 0 ? "positive" : "negative"; + + if (this.infoCoordsResolved[key]?.type === "angle") { + classes += " angle"; + } + else { + delta = delta / rawValue * 100; + } + + delta = typeof delta === "number" ? Number(delta.toPrecision(4)) : delta; + ret += `
${ delta }
`; + } + + ret += "
"; + + html.push(ret); } - this._el.info.innerHTML = info.join("\n"); + this._el.info.innerHTML = html.join("\n"); } } @@ -188,17 +206,17 @@ const Self = class ColorSwatch extends ColorElement { }, color: { get type () { - return ColorSwatch.Color; + return Self.Color; }, get () { if (!this.value) { return null; } - return ColorSwatch.Color.get(this.value); + return Self.Color.get(this.value); }, set (value) { - this.value = ColorSwatch.Color.get(value)?.display(); + this.value = Self.Color.get(value)?.display(); }, reflect: false, }, @@ -207,7 +225,19 @@ const Self = class ColorSwatch extends ColorElement { is: Array, values: { is: Object, - defaultKey: (coord, i) => ColorSwatch.Color.Space.resolveCoord(coord)?.name, + defaultKey: (value, i) => { + if (value.startsWith("deltaE.") || value.startsWith("contrast.")) { + let [method, algorithm] = value.split("."); + let label = method === "deltaE" ? `ΔE ${algorithm}` : `${algorithm} Contrast`; + return label; + } + else if (value.includes(".")) { + return Self.Color.Space.resolveCoord(value)?.name; + } + else { + return value; + } + }, }, }, default: [], @@ -215,23 +245,169 @@ const Self = class ColorSwatch extends ColorElement { from: true, }, }, + /** + * Specified coords + * @example ["oklch.h", "oklch.c", "oklch.l"] + */ + infoCoords: { + get () { + if (!this.info.length) { + return; + } + + let ret = []; + for (let data of this.info) { + let [key, value] = Object.entries(data)[0]; + if (value.includes(".") && !value.startsWith("deltaE") && !value.startsWith("contrast")) { + ret.push(value); + } + } + + return ret; + }, + }, + // We need this to correctly work (calculate and show in the UI) with coords of type "angle" + infoCoordsResolved: { + get () { + if (!this.infoCoords) { + return; + } + + let ret = {}; + for (let coord of this.infoCoords) { + try { + let { space, index } = Self.Color.Space.resolveCoord(coord); + ret[coord] = Object.values(space.coords)[index]; + } + catch (e) { + console.error(e); + } + } + + return ret; + }, + }, + /** + * Specified deltaE and contrast + * @example ["deltaE.2000", "contrast.WCAG21"] + */ + infoOther: { + get () { + if (!this.info.length) { + return; + } + + let ret = []; + for (let data of this.info) { + let [key, value] = Object.entries(data)[0]; + if (!this.infoCoords.includes(value)) { + ret.push(value); + } + } + + return ret; + }, + }, + /** + * Coords for `this.color` + * @example {"oklch.l": 0.7,"oklch.c": 0.25, "oklch.h": 138} + */ colorInfo: { get () { - if (!this.info.length || !this.color) { + if (!this.color || !this.infoCoords) { + return; + } + + let ret = {}; + for (let coord of this.infoCoords) { + try { + ret[coord] = this.color.get(coord); + } + catch (e) { + console.error(e); + } + } + + return ret; + }, + }, + /** + * Color deltas (between `this.color` and `this.vs`) + * @example {"oklch.l": -0.3,"oklch.c": 0.35, "oklch.h": 108} + */ + colorDeltas: { + get () { + if (!this.infoCoordsResolved || !this.vsInfo) { + return; + } + + // TODO: Use Color.js deltas() instead (when v0.6.0 is released) + let ret = {}; + for (let coord of this.infoCoords) { + let value = this.colorInfo[coord]; + let vsValue = this.vsInfo[coord]; + + let isAngle = this.infoCoordsResolved[coord]?.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; + } + } + + ret[coord] = value - vsValue; + } + + return ret; + }, + }, + /** + * Color to compare `this.color` with + */ + vs: { + get type () { + return Self.Color; + }, + }, + /** + * Coords, deltaE, contrast for `this.vs` + * @example {"oklch.l": 1, "oklch.c": 0, "oklch.h": null, "deltaE.2000": 37.69, "contrast.WCAG21": 2.46} + */ + vsInfo: { + get () { + if (!this.color || !this.vs || !this.info.length) { return; } let ret = {}; - for (let coord of this.info) { - let [label, channel] = Object.entries(coord)[0]; + + for (let coord of this.infoCoords) { try { - ret[channel] = this.color.get(channel); + ret[coord] = this.vs.get(coord); } catch (e) { console.error(e); } } + for (let data of this.infoOther) { + let [method, algorithm] = data.split("."); + + if (method && algorithm) { + try { + ret[data] = this.color[method](this.vs, algorithm); + } + catch (e) { + console.error(e); + } + } + } + return ret; }, },