A Pen by David Aerne on CodePen.
Created
September 17, 2023 16:43
-
-
Save meodai/23e717056d03f52b1c2fad75f28acf23 to your computer and use it in GitHub Desktop.
vanilla JS color-scale generator (supports p3/rec2020)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<div class="world"></div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.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