Hue wheel visualization for every palette

This commit is contained in:
Lea Verou
2025-02-26 21:43:37 -05:00
parent 668666e1c9
commit e9389b8bd5
3 changed files with 144 additions and 2 deletions

View File

@@ -26,9 +26,12 @@
}"
:style="{
'--chroma-scale': chromaScale,
'--gray-chroma': tweaked?.grayChroma ? grayChroma : '',
'--gray-chroma': tweaked?.grayChroma ? grayChroma : originalGrayChroma,
'--max-c': maxChroma,
'--avg-l': L_RANGES[level].mid,
}">
<header id="palette-info">
{% include 'breadcrumbs.njk' %}
<h1 v-if="saved" class="title">
@@ -50,6 +53,21 @@
{{ description | inlineMarkdown | safe }}
</p>
{% endif %}
{% raw %}
<div class="hue-wheel">
<template v-for="color, hue in coreColors">
<div :id="`hue-wheel-${hue}`" class="color"
:style="{
'--color': color,
'--h': color.get('oklch.h'),
'--c': color.get('oklch.c'),
'--l': color.get('oklch.l'),
}"></div>
<wa-tooltip :for="`hue-wheel-${ hue }`" hoist>{{ capitalize(hue) }} {{ coreLevels[hue] }}</wa-tooltip>
</template>
</div>
{% endraw %}
</header>
{% endblock %}
{% block afterContent %}

View File

@@ -168,6 +168,115 @@ wa-dropdown > .color.swatch {
}
[data-slug='custom'] > :not(.seeded) #seed-colors ~ *,
[data-slug='custom'] > :not(.seeded) .hue-wheel,
#outline:has(+ main > [data-slug='custom'] > :not(.seeded)) li:nth-child(n + 2) {
display: none;
}
[id='palette-info'] {
display: grid;
grid-template-columns: 1fr auto;
grid-auto-flow: column;
> * {
grid-column: 1;
}
}
.hue-wheel {
--r: clamp(2em, 6rem, 25vmin);
grid-column: 2;
grid-row: 1 / 5;
position: relative;
width: calc(var(--r) * 2);
aspect-ratio: 1;
border-radius: 50%;
--lc: var(--avg-l) var(--max-c);
--lc2: var(--avg-l) calc(var(--max-c) / 2);
margin-top: calc(var(--r) * -0.05);
background: conic-gradient(
in oklch,
oklch(var(--lc) 0),
oklch(var(--lc) 60),
oklch(var(--lc) 120),
oklch(var(--lc) 180),
oklch(var(--lc) 240),
oklch(var(--lc) 300),
oklch(var(--lc) 360)
);
&,
&::before {
--stops: oklch(var(--lc) 0), oklch(var(--lc) 60), oklch(var(--lc) 120), oklch(var(--lc) 180), oklch(var(--lc) 240),
oklch(var(--lc) 300), oklch(var(--lc) 360);
}
&::before {
content: '';
display: block;
height: 100%;
border-radius: 50%;
-webkit-mask: radial-gradient(white, transparent);
background: radial-gradient(oklch(var(--avg-l) calc(var(--gray-chroma) * var(--max-c)) 0) 5%, transparent 30%),
conic-gradient(
in oklch,
oklch(var(--lc2) 0),
oklch(var(--lc2) 60),
oklch(var(--lc2) 120),
oklch(var(--lc2) 180),
oklch(var(--lc2) 240),
oklch(var(--lc2) 300),
oklch(var(--lc2) 360)
);
}
.color {
--scale-c: calc(var(--c) / var(--max-c));
--distance: calc(var(--r) * var(--scale-c));
top: 50%;
left: 50%;
transform: translate(-50%, -50%) rotate(calc(var(--h) * 1deg - 90deg)) translateX(var(--distance));
position: absolute;
z-index: 1;
width: calc(1.2em + 0.3em * var(--scale-c));
aspect-ratio: 1;
&:hover {
--scale: 1.2;
--line-color: white;
--line-style: solid;
}
&::before {
content: '';
position: absolute;
z-index: -1;
width: 100%;
height: 0;
border-top: 2px var(--line-style, dashed) var(--line-color, var(--wa-color-gray-80));
padding-top: 100%;
top: calc(50% - 1px);
right: 50%;
width: var(--distance);
}
&::after {
content: '';
display: block;
position: relative;
height: 100%;
border-radius: 50%;
border: 2px solid white;
box-shadow: var(--wa-shadow-l);
background: var(--color);
transition: var(--wa-transition-fast);
scale: var(--scale, 1);
}
}
wa-tooltip {
/* Prevent flickering */
pointer-events: none;
}
}

View File

@@ -92,7 +92,7 @@ let paletteAppSpec = {
created() {
// Non-reactive variables to expose
Object.assign(this, { moreHue, HUE_RANGES, hues, tints, MAX_CHROMA_BOUNDS });
Object.assign(this, { moreHue, HUE_RANGES, L_RANGES, hues, tints, MAX_CHROMA_BOUNDS });
// Read URL params and apply them. This facilitates permalinks.
this.permalink.mapObject(this.hueShifts, {
@@ -376,6 +376,14 @@ let paletteAppSpec = {
return ret;
},
maxChroma() {
return Math.max(
...Object.values(this.coreColors)
.map(color => color.get('oklch.c'))
.filter(c => c >= 0),
);
},
coreLevels() {
let ret = {};
@@ -387,6 +395,13 @@ let paletteAppSpec = {
return ret;
},
level() {
let levels = Object.values(this.coreLevels).sort((a, b) => a - b);
levels = levels.slice(levels.length / 4, -levels.length / 4); // Remove top and bottom 25%
let trimmedMean = levels.map(Number).reduce((a, b) => a + b, 0) / levels.length;
return Math.round(trimmedMean / 10) * 10;
},
shiftBounds() {
return Object.fromEntries(
hues.map(hue => {