mirror of
https://github.com/shoelace-style/webawesome.git
synced 2026-01-12 20:19:13 +00:00
Interpolate subsequent hues
This commit is contained in:
@@ -167,6 +167,7 @@
|
||||
'--color-base': colorsMinusCurrentTweak[hue][tint],
|
||||
colorScheme: tint > 60 ? 'light' : 'dark'
|
||||
}">
|
||||
<wa-icon name="thumbtack" v-if="seedHues[hue]?.[tint]"></wa-icon>
|
||||
<wa-copy-button :value="`--wa-color-${ hue }-${ tint }`" :copy-label="`--wa-color-${ hue }-${ tint }`"></wa-copy-button>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
@@ -43,7 +43,7 @@ export const hueRanges = {
|
||||
pink: { min: 320, max: 375 }, // 55
|
||||
};
|
||||
|
||||
export const lRanges = {
|
||||
export const L_RANGES = {
|
||||
'05': { min: 0.18, max: 0.2 },
|
||||
10: { min: 0.23, max: 0.25 },
|
||||
20: { min: 0.31, max: 0.35 },
|
||||
@@ -57,6 +57,10 @@ export const lRanges = {
|
||||
95: { min: 0.95, max: 0.97 },
|
||||
};
|
||||
|
||||
for (let lightness in L_RANGES) {
|
||||
L_RANGES[lightness].mid = (L_RANGES[lightness].min + L_RANGES[lightness].max) / 2;
|
||||
}
|
||||
|
||||
export const moreHue = {
|
||||
red: 'Redder',
|
||||
orange: 'More orange', // https://www.reddit.com/r/grammar/comments/u9n0uo/is_it_oranger_or_more_orange/
|
||||
|
||||
@@ -76,6 +76,11 @@ export function camelCase(str) {
|
||||
}
|
||||
|
||||
export function capitalize(str) {
|
||||
if (!str) {
|
||||
return str;
|
||||
}
|
||||
|
||||
str = str + '';
|
||||
return str[0].toUpperCase() + str.slice(1);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ order: 99
|
||||
---
|
||||
<link href="{{ page.url }}../edit/custom.css" rel="stylesheet">
|
||||
|
||||
<p>Create your own color palette from scratch.</p>
|
||||
<p>Create your own color palette from scratch, from one or more seed colors.</p>
|
||||
|
||||
<h2>Seed color(s)</h2>
|
||||
|
||||
@@ -14,7 +14,7 @@ order: 99
|
||||
<template v-for="color, i in seedColors">
|
||||
<core-color-input v-model="seedColors[i]" @delete="seedColors.splice(i, 1)"></core-color-input>
|
||||
</template>
|
||||
<wa-button class="add-button" appearance="outlined" @click="seedColors.push(seedColorSamples.shift())">
|
||||
<wa-button class="add-button" appearance="outlined" @click="seedColors.push(seedColorSamples.shift() || '')">
|
||||
<wa-icon slot="prefix" name="plus" variant="regular"></wa-icon>
|
||||
Add color
|
||||
</wa-button>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Color from 'https://colorjs.io/dist/color.js';
|
||||
import { hueRanges, lRanges } from '/assets/scripts/tweak/data.js';
|
||||
import { hueRanges, L_RANGES } from '/assets/scripts/tweak/data.js';
|
||||
import { capitalize, findClosestRange } from '/assets/scripts/tweak/util.js';
|
||||
|
||||
export default {
|
||||
@@ -22,11 +22,13 @@ export default {
|
||||
},
|
||||
|
||||
closestHue() {
|
||||
if (!this.color) return '';
|
||||
return findClosestRange(hueRanges, this.color.get('oklch.h'), { type: 'angle' })?.key;
|
||||
},
|
||||
|
||||
closestLevel() {
|
||||
return findClosestRange(lRanges, this.color.get('oklch.l'))?.key;
|
||||
if (!this.color) return '';
|
||||
return findClosestRange(L_RANGES, this.color.get('oklch.l'))?.key;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
@@ -40,7 +42,7 @@ export default {
|
||||
<div slot="image" :style="{'--color': modelValue, colorScheme: closestLevel <= 60 ? 'dark' : 'light'}">
|
||||
<wa-icon-button name="trash" label="Delete" variant="regular" class="delete-button" @click="$emit('delete')"></wa-icon-button>
|
||||
</div>
|
||||
<div class="name">{{ capitalize(closestHue) }} {{ closestLevel }}</div>
|
||||
<div class="name">{{ capitalize(closestHue) || 'New color' }} {{ closestLevel }}</div>
|
||||
<wa-input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)"></wa-input>
|
||||
</wa-card>
|
||||
`,
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
HUE_SHIFTS,
|
||||
hueRanges,
|
||||
hues,
|
||||
lRanges,
|
||||
L_RANGES,
|
||||
MAX_CHROMA_BOUNDS,
|
||||
maxGrayChroma,
|
||||
moreHue,
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
capitalize,
|
||||
clamp,
|
||||
findClosestRange,
|
||||
interpolate,
|
||||
mapRange,
|
||||
progress,
|
||||
subtractAngles,
|
||||
@@ -67,7 +68,15 @@ let paletteAppSpec = {
|
||||
return {
|
||||
uid: undefined,
|
||||
seedColors: [],
|
||||
seedColorSamples: ['oklch(77% 0.19 70)', 'rgb(95, 59, 255)', '#f06', 'yellowgreen', 'oklch(82% 0.185 195)'],
|
||||
seedColorSamples: [
|
||||
'oklch(77% 0.19 70)',
|
||||
'rgb(95, 59, 255)',
|
||||
'#f06',
|
||||
'yellowgreen',
|
||||
'oklch(82% 0.185 195)',
|
||||
'oklch(30% 0.18 175)',
|
||||
'oklch(30% 0.18 150)',
|
||||
],
|
||||
paletteId,
|
||||
paletteTitle: palette.title,
|
||||
originalColors: palette.colors,
|
||||
@@ -147,7 +156,7 @@ let paletteAppSpec = {
|
||||
|
||||
for (let color of this.seedColorObjects) {
|
||||
let hue = findClosestRange(hueRanges, color.get('oklch.h'), { type: 'angle' }).key;
|
||||
let level = findClosestRange(lRanges, color.get('oklch.l')).key;
|
||||
let level = findClosestRange(L_RANGES, color.get('oklch.l')).key;
|
||||
ret[hue] ??= {};
|
||||
ret[hue][level] = color;
|
||||
}
|
||||
@@ -186,77 +195,129 @@ let paletteAppSpec = {
|
||||
|
||||
// Generate scales from seed hues
|
||||
for (let hue in this.seedHues) {
|
||||
let [coreLevel, coreColor] = Object.entries(this.seedHues[hue])[0];
|
||||
// Find core color
|
||||
let coreColor, maxChroma, coreLevel;
|
||||
|
||||
let distance = coreColor.get('oklch.l') - (lRanges[coreLevel].max + lRanges[coreLevel].min) / 2;
|
||||
for (let level in this.seedHues[hue]) {
|
||||
let color = this.seedHues[hue][level];
|
||||
let chroma = color.get('oklch.c');
|
||||
|
||||
if (!(chroma < maxChroma)) {
|
||||
// not < will also kick in when they are empty
|
||||
coreColor = color;
|
||||
maxChroma = chroma;
|
||||
coreLevel = level;
|
||||
}
|
||||
}
|
||||
|
||||
let distance = coreColor.get('oklch.l') - L_RANGES[coreLevel].mid;
|
||||
let coreChroma = coreColor.get('oklch.c');
|
||||
ret[hue] ??= {
|
||||
|
||||
ret[hue] = {
|
||||
maxChromaTint: coreLevel,
|
||||
maxChromaTintRaw: coreLevel,
|
||||
maxChroma: coreChroma,
|
||||
maxChromaRaw: coreChroma,
|
||||
};
|
||||
|
||||
let scale = getLightestChromaScale(hue, coreLevel, coreChroma);
|
||||
let chroma95 = clamp(0, coreChroma * scale, 0.1);
|
||||
|
||||
// Find if any hue shift applies to this hue (we assume defined hue shifts are mutually exclusive)
|
||||
// Find if any hue shift applies to this hue (we assume defined hue shift ranges are mutually exclusive)
|
||||
let hueShift = { dark: 0, light: 0, intensity: 0 };
|
||||
let autoHueShift = HUE_SHIFTS.find(
|
||||
({ range }) =>
|
||||
subtractAngles(range[0], coreColor.get('oklch.h')) <= 0 &&
|
||||
subtractAngles(coreColor.get('oklch.h'), range[1]) <= 0,
|
||||
);
|
||||
|
||||
if (autoHueShift) {
|
||||
hueShift = { ...autoHueShift.shift };
|
||||
let hueRange = [autoHueShift.range[0], ...autoHueShift.peak, autoHueShift.range[1]];
|
||||
hueShift.intensity = mapRange(coreColor.get('oklch.h'), hueRange, [0, 1, 1, 0]);
|
||||
}
|
||||
|
||||
// First, add pinned colors
|
||||
for (let tint in this.seedHues[hue]) {
|
||||
ret[hue][tint] = this.seedHues[hue][tint];
|
||||
}
|
||||
|
||||
// Now generate the rest, starting from the edges
|
||||
if (!('95' in ret[hue])) {
|
||||
let color = coreColor.clone().to('oklch');
|
||||
let scale = getLightestChromaScale(hue, coreLevel, coreChroma);
|
||||
|
||||
color.set({
|
||||
l: L_RANGES[95].mid + distance,
|
||||
c: clamp(0, coreChroma * scale, 0.1),
|
||||
h: h => h + hueShift.light * hueShift.intensity,
|
||||
});
|
||||
|
||||
ret[hue][95] = color;
|
||||
}
|
||||
|
||||
if (!('05' in ret[hue])) {
|
||||
let color = coreColor.clone().to('oklch');
|
||||
|
||||
color.set({
|
||||
l: L_RANGES['05'].mid + distance,
|
||||
// TODO c
|
||||
h: h => h + hueShift.dark * hueShift.intensity,
|
||||
});
|
||||
|
||||
ret[hue]['05'] = color;
|
||||
}
|
||||
|
||||
let pinnedLevels = Object.keys(this.seedHues[hue]).sort((a, b) => a - b);
|
||||
let levelBefore = '05';
|
||||
|
||||
for (let tint of tints) {
|
||||
if (tint in this.seedHues[hue]) {
|
||||
ret[hue][tint] = this.seedHues[hue][tint];
|
||||
} else {
|
||||
let color = coreColor.clone().to('oklch');
|
||||
if (tint in ret[hue]) {
|
||||
// Pinned or already generated
|
||||
levelBefore = tint;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Lightness
|
||||
let mid = (lRanges[tint].max + lRanges[tint].min) / 2;
|
||||
color.set('l', mid + distance);
|
||||
// Generated color
|
||||
// First, find closest pinned colors before and after
|
||||
let levelAfter = pinnedLevels.findLast(level => level < tint) ?? '95';
|
||||
let colorBefore = ret[hue][levelBefore];
|
||||
let colorAfter = ret[hue][levelAfter];
|
||||
|
||||
// Calculate auto hue shift
|
||||
let deltaLevel = tint - coreLevel;
|
||||
let edgeLevel = deltaLevel < 0 ? '05' : '95';
|
||||
let color = coreColor.clone().to('oklch');
|
||||
|
||||
if (autoHueShift && tint !== '05') {
|
||||
// No hue shift for darkest tint
|
||||
let intensity = 1;
|
||||
// Lightness
|
||||
color.set('l', L_RANGES[tint].mid + distance);
|
||||
|
||||
if (coreColor.get('oklch.h') < autoHueShift.peak[0]) {
|
||||
intensity = progress(coreColor.get('oklch.h'), autoHueShift.range[0], autoHueShift.peak[0]);
|
||||
} else if (coreColor.get('oklch.h') > autoHueShift.peak[1]) {
|
||||
intensity = progress(coreColor.get('oklch.h'), autoHueShift.peak[1], autoHueShift.range[1]);
|
||||
}
|
||||
// Interpolate hue linearly and chroma with a power curve
|
||||
color.set({
|
||||
l: L_RANGES[tint].mid + distance,
|
||||
h: mapRange(tint, {
|
||||
from: [levelBefore, levelAfter],
|
||||
to: [colorBefore.get('oklch.h'), colorAfter.get('oklch.h')],
|
||||
}),
|
||||
});
|
||||
|
||||
let maxShift = deltaLevel < 0 ? autoHueShift.shift.dark : autoHueShift.shift.light;
|
||||
let p = progress(tint, coreLevel, Math.max(edgeLevel, 10));
|
||||
let shift = maxShift * intensity * p ** 0.75;
|
||||
color.set('oklch.h', coreColor.get('oklch.h') + shift);
|
||||
}
|
||||
|
||||
// Chroma
|
||||
if (tint > coreLevel) {
|
||||
// Lighter, reduce chroma
|
||||
let chroma = mapRange(tint, {
|
||||
from: [coreLevel, 95],
|
||||
to: [coreChroma, chroma95],
|
||||
if (tint > coreLevel) {
|
||||
color.set(
|
||||
'c',
|
||||
mapRange(tint, {
|
||||
from: [levelBefore, levelAfter],
|
||||
to: [colorBefore.get('oklch.c'), colorAfter.get('oklch.c')],
|
||||
progression: p => p ** this.factor,
|
||||
});
|
||||
color.set('c', chroma);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
color = color.toGamut('p3');
|
||||
ret[hue][tint] = color;
|
||||
}
|
||||
|
||||
ret[hue][tint] = color;
|
||||
for (let tint in ret[hue]) {
|
||||
if (!(tint in this.seedHues) && ret[hue][tint].toGamut) {
|
||||
ret[hue][tint] = ret[hue][tint].toGamut('p3');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get rest of hues from default palette
|
||||
// TODO generate from existing colors
|
||||
// Fill in remaining hues
|
||||
// TODO generate from seed hues
|
||||
for (let hue of hues) {
|
||||
if (hue in ret) {
|
||||
continue;
|
||||
|
||||
Reference in New Issue
Block a user