Cap hue shift for consecutive tints

This commit is contained in:
Lea Verou
2025-02-28 12:26:43 -05:00
parent 1993182f43
commit f682293c38
3 changed files with 60 additions and 24 deletions

View File

@@ -79,14 +79,14 @@ export const moreHue = {
export const HUE_SHIFTS = [
// Reds
{ range: [0, 25], peak: [10, 25], shift: { dark: 15, light: -18 } },
{ range: [0, 25], peak: [10, 25], shift: { dark: 15, light: -18 }, maxConsecutive: 2 },
// Yellows
{ range: [30, 125], peak: [70, 100], shift: { dark: -48, light: 16 } },
{ range: [30, 112], peak: [70, 100], shift: { dark: -48, light: 16 }, maxConsecutive: 13 },
// Greens
{ range: [140, 160], peak: [145, 155], shift: { dark: 15, light: -5 } },
{ range: [140, 160], peak: [145, 155], shift: { dark: 15, light: -5 }, maxConsecutive: 2 },
// Blues
{ range: [240, 265], peak: [245, 260], shift: { dark: -3, light: -15 } },
{ range: [240, 265], peak: [245, 260], shift: { dark: -3, light: -15 }, maxConsecutive: 7 },
];
export const MAX_CHROMA_BOUNDS = { min: 0.08, max: 0.3 };

View File

@@ -1,5 +1,6 @@
export function normalizeAngles(angles) {
// First, normalize
angles = angles.filter(h => !isNaN(h));
angles = angles.map(h => ((h % 360) + 360) % 360);
// Remove top and bottom 25% and find average
@@ -80,7 +81,8 @@ export function getRange(ranges, value, options) {
}
}
if (options?.tolerance && Math.abs(closest.distance) > options.tolerance) {
// TODO use angle functions to check tolerance against angles
if (options?.tolerance !== undefined && Math.abs(closest.distance) > options.tolerance) {
return;
}

View File

@@ -41,47 +41,39 @@ export function generateScale(seedColors) {
maxChromaRaw: coreChroma,
};
// 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 = getRange(HUE_SHIFTS, coreColor.get('oklch.h'), {
getRange: v => v.range,
type: 'angle',
tolerance: 0,
});
if (autoHueShift) {
autoHueShift = HUE_SHIFTS[autoHueShift.key];
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 seedColors) {
scale[tint] = seedColors[tint];
}
// For finding lightest and darkest pinned colors
let pinnedTints = Object.keys(seedColors).sort((a, b) => a - b);
// Now generate the rest, starting from the edges
if (!('95' in scale)) {
let color = coreColor.clone().to('oklch');
let lightest = seedColors[pinnedTints[0]];
let color = lightest.clone().to('oklch');
let chromaScale = chromaScaleLightest[coreLevel] ?? 0.1;
let hueShift = getHueShift(lightest, pinnedTints[0], '95');
color.set({
l: L_RANGES[95].mid + distance,
c: clamp(0, coreChroma * chromaScale, 0.1),
h: h => h + hueShift.light * hueShift.intensity,
h: h => h + hueShift,
});
scale[95] = color;
}
if (!('05' in scale)) {
let color = coreColor.clone().to('oklch');
let darkest = seedColors[pinnedTints.at(-1)];
let color = darkest.clone().to('oklch');
let hueShift = getHueShift(darkest, pinnedTints.at(-1), '05');
color.set({
l: L_RANGES['05'].mid + distance,
// TODO c
h: h => h + hueShift.dark * hueShift.intensity,
h: h => h + hueShift,
});
scale['05'] = color;
@@ -166,3 +158,45 @@ export function placeColor(color) {
}
export default generateScale;
/**
* How many tints are between two tints?
* E.g. `getTintDistance('90', '95')` should return `1`
* @param {number | string} tint1
* @param {number | string} tint2
* @returns {number}
*/
export function getTintDistance(tint1, tint2) {
tint1 = String(tint1);
tint2 = String(tint2);
return Math.abs(tints.indexOf(tint2) - tints.indexOf(tint1));
}
export function getHueShift(color, fromTint, toTint) {
let tintDistance = getTintDistance(fromTint, toTint);
let hueShift = getRange(HUE_SHIFTS, color.get('oklch.h'), {
getRange: v => v.range,
type: 'angle',
tolerance: 0,
});
if (!hueShift) {
return 0;
}
hueShift = HUE_SHIFTS[hueShift.key];
let { peak, range } = hueShift;
let h = color.get('oklch.h');
let breakpoints = [range[0], ...peak, range[1]];
let intensity = mapRange(h, breakpoints, [0, 1, 1, 0]);
let type = tintDistance > 0 ? 'light' : 'dark';
let shift = hueShift.shift[type];
let ret = shift * intensity;
let maxShift = Math.sign(shift) * hueShift.maxConsecutive * tintDistance;
console.log(ret, clamp(undefined, ret, maxShift));
ret = clamp(undefined, ret, maxShift);
return ret;
}