Skip to content

Instantly share code, notes, and snippets.

@meodai
Created September 17, 2023 16:43
Show Gist options
  • Save meodai/23e717056d03f52b1c2fad75f28acf23 to your computer and use it in GitHub Desktop.
Save meodai/23e717056d03f52b1c2fad75f28acf23 to your computer and use it in GitHub Desktop.
vanilla JS color-scale generator (supports p3/rec2020)
<div class="world"></div>
/**
* Generates a CSS gradient string with hard stops based on an array of colors.
*
* @param {string[]} arrOfColors - An array of color strings.
* @returns {string} - A CSS gradient string with hard stops.
*/
const hardStopsGradient = (arrOfColors) => {
const l = arrOfColors.length;
return arrOfColors.map(
(c, i) => `${c} ${(i/l*100).toFixed(2)}% ${((i+1)/l*100).toFixed(2)}%`
).join(',')
}
/**
* Linearly interpolates between two values.
*
* @param {number} amt - The interpolation amount (usually between 0 and 1).
* @param {number} from - The starting value.
* @param {number} to - The ending value.
* @returns {number} - The interpolated value.
*/
const lerp = (amt, from, to) => from + amt * (from - to);
/**
* Scales and spreads an array to the target size using interpolation.
*
* @param {Array} initial - The initial array of values.
* @param {number} targetSize - The desired size of the resulting array.
* @param {function} fillFunction - The interpolation function (default is lerp).
* @returns {Array} The scaled and spread array.
* @throws {Error} If the initial array is empty or target size is invalid.
*/
const scaleSpreadArray = (
initial,
targetSize,
fillFunction = lerp,
padding = 0
) => {
if (initial.length === 0) {
throw new Error("Initial array must not be empty.");
}
if (targetSize < initial.length) {
throw new Error("Target size must be greater than or equal to the initial array length.");
}
const valuesToAdd = targetSize - initial.length;
const chunkArray = initial.map((value) => [value]);
//const shouldDistributeEvenly = targetSize > 2 * initial.length ? 1 : 0;
for (let i = 0; i < valuesToAdd; i++) {
chunkArray[i % (initial.length - 1)].push(null);
}
for (let i = 0; i < chunkArray.length - 1; i++) {
const currentChunk = chunkArray[i];
const nextChunk = chunkArray[i + 1];
const currentValue = currentChunk[0];
const nextValue = nextChunk[0];
for (let j = 1; j < currentChunk.length; j++) {
const percent = j / currentChunk.length;
currentChunk[j] = fillFunction(percent, currentValue, nextValue);
}
}
return chunkArray.flat();
};
/**
* Converts Hxx (Hue, Chroma, Lightness) values to a CSS `oklch()` color function string.
*
* @param {Object} hxx - An object with hue, chroma, and lightness properties.
* @param {number} hxx.hue - The hue value.
* @param {number} hxx.chroma - The chroma value.
* @param {number} hxx.lightness - The lightness value.
* @returns {string} - The CSS color function string in the format `oklch(lightness% chroma hue)`.
*/
const hxxToCSSokLCH = ({hue, chroma, lightness} = hxx) => `oklch(${(lightness * 100).toFixed(2)}% ${(chroma * .4).toFixed(4)} ${hue.toFixed(2)})`;
/**
* Generates an array of Hxx (Hue, Chroma, Lightness) color objects with specified parameters.
*
* @param {number} colors - The number of colors to generate.
* @param {number} minHueDiffAngle - The minimum hue difference angle between colors (in degrees).
* @returns {Object[]} - An array of Hxx color objects, each containing hue, chroma, and lightness properties.
*/
const generateHxxRamp = (
colors = 4,
minHueDiffAngle = 60,
) => {
minHueDiffAngle = Math.min(minHueDiffAngle, 360/colors);
const baseHue = Math.random() * 360;
const huesToPickFrom = new Array(
Math.round(360 / minHueDiffAngle)
).fill('').map((_, i) =>
(baseHue + i * minHueDiffAngle) % 360
);
while (huesToPickFrom.length > colors) {
const randomIndex = Math.floor(Math.random() * huesToPickFrom.length);
huesToPickFrom.splice(randomIndex, 1);
}
const minLightness = Math.random() * .2;
const lightnessRange = .9 - minLightness;
const minChroma = .005 + Math.random() * .25;
const maxChroma = minChroma + 0.05 + Math.random() * .4;
/*
I intentionally go well over 0.4 to make some palettes
super staurated
*/
/* alternative way of handeling lightness
const maxLightness = Math.min(minLightness + .7 + Math.random() * .2, .95);
const lightnessRange = maxLightness - minLightness; */
return huesToPickFrom.map((hue, i) => ({
lightness: minLightness + Math.random() * .1 + (lightnessRange/(colors-1)) * i,
chroma: minChroma + Math.random() * (maxChroma - minChroma),
hue,
}));
}
/**
* Generates a color palette with specified parameters.
*
* @param {number} colorsToGenerate - The number of base colors to generate.
* @param {number} colorsFinal - The total number of colors in the palette.
* @param {number} minHueDiffAngle - The minimum hue difference angle between base colors (in degrees).
* @param {string} mixIn - The color space to use for mixing (e.g., 'oklab', 'oklch', etc.).
* @returns {string[]} - An array of CSS color stop strings or color mix functions.
*/
const generateColors = (
colorsToGenerate,
colorsFinal,
minHueDiffAngle = 60,
mixIn = 'oklab', //``oklch shorter hue``,
) => {
const baseColors = generateHxxRamp(colorsToGenerate, minHueDiffAngle);
const cssColorStops = baseColors.map(hxxToCSSokLCH);
return colorsFinal > colorsToGenerate ?
scaleSpreadArray(
cssColorStops,
colorsFinal,
(percent, lastValue, nextValue) =>
`color-mix(in ${mixIn}, ${nextValue} ${(percent * 100).toFixed(2)}%, ${lastValue})`
) : cssColorStops;
}
// the actual demo
const $w = document.documentElement;
const doit = () => {
const gradient = generateColors(
2 + Math.round(Math.random() * 2),
5 + Math.round(Math.random() * 4),
);
$w.style.setProperty('--bg', hardStopsGradient(gradient));
$w.style.setProperty('--bgs', gradient.join()); // smooth CSS gradient for blur effect
}
document.documentElement.addEventListener('click', doit);
doit();
.world {
--angle: 0deg;
background: linear-gradient(var(--angle), var(--bg));
height: 80vmin;
width: 60vmin;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
cursor: pointer;
&::before {
content: '';
position: absolute;
inset: 0;
background: inherit;
filter: blur(6vmin);
transform: scale(.95);
}
}
@media (orientation: landscape) {
.world {
--angle: 90deg;
height: 35vmin;
width: 80vmin;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment