Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save meodai/3532a28d5b6bf9b5db5d4e1590ca31a1 to your computer and use it in GitHub Desktop.
Save meodai/3532a28d5b6bf9b5db5d4e1590ca31a1 to your computer and use it in GitHub Desktop.
generative color compositions using poline
<div class="debug"></div>
<div class="bg">
<div class="bg__inner" data-bg></div>
</div>
<div class="thing" data-thing>
<div class="thing__inner">
<div class="thing__half"></div>
</div>
</div>
<div class="settings" data-settings></div>
console.clear()
import {Poline, positionFunctions} from "https://cdn.skypack.dev/poline@0.7.0";
import {createNoise2D} from "https://cdn.skypack.dev/simplex-noise@4.0.1";
import seedrandom from "https://cdn.skypack.dev/seedrandom@3.0.5";
import Tweakpane from "https://cdn.skypack.dev/tweakpane@3.1.7";
import * as EssentialsPlugin from "https://cdn.skypack.dev/@tweakpane/plugin-essentials@0.1.8";
let seed = Math.random();
let rnd = new seedrandom(seed);
let noise2D = createNoise2D();
const PARAMS = {
seed: Math.random(),
muted: false,
brighten: true,
p3: false,
moreHues: true,
baseHue: rnd() * 360,
shadeLight: {min: 0.01, max: .1},
};
const $settings = document.querySelector('[data-settings]');
const pane = new Tweakpane.Pane({
title: 'Settings',
container: $settings,
});
pane.registerPlugin(EssentialsPlugin);
pane.expanded = false;
pane.on('fold', (ev) => {
document.documentElement.classList.toggle('is-open', ev.expanded);
})
/*
pane.addInput(PARAMS, 'shadeLight', {
min: 0,
max: 1,
step: .01,
});*/
// color harmonies (180deg beeing complementary colors for example)
const harmonies = [-30, 30, 180, -90, 90, 120, -120];
const rndRange = (min = 0, max = 1) => min + rnd() * (max - min);
const rndArr = (items, min = 0, max = 1) => items[Math.floor(rndRange(min, max)*items.length)];
let colorMethod = 'colorsCSS'; // colorsCSSoklch, colorsCSSlch, colorsCSS
const settins = {
"2paths": {
// anchors are color "points" set in the poline lib
anchors: (baseHue, randomHarmony) => [
[ // sets a random relatively bright and saturated point at a randomHue
baseHue,
0.6 + rnd() * 0.4,
(
PARAMS.muted ?
0 : 0.5
) + rnd() * 0.5
],
[ // sets a very dark color in the center of the color system
baseHue - randomHarmony * .5,
.1 + rnd() * .2,
rnd() * 0.2
],
[ // sets a final saturated and bright color
baseHue + randomHarmony,
(PARAMS.muted ? 0.2 : 0.7) + rnd() * 0.3,
0.8 + rnd() * 0.2
],
],
positionFunctionY: positionFunctions.sinusoidalPosition,
positionFunctionX: positionFunctions.quadraticPosition,
positionFunctionZ: positionFunctions.linearPosition,
ranges: {
// selects the random range for each color "category" within the colors generated by poline. Looking ot the debug gradients on the very bottom of the result the light colors will be within 0.1% - 10% of the gradient
light: [0.01,.1],
// dark colors will be kinda in the middle of the gradient
dark: [.5,.51],
// etc...
strong: [.65,.85],
strongAlt: [.7, .8],
strongAlt2: [.2,.45],
},
},
"1path": {
anchors: (baseHue, randomHarmony) => [
[
baseHue,
(PARAMS.muted ? 0 : 0.5) + rnd() * 0.2,
0.5 + rnd() * 0.2
],
[
baseHue + 180,
(PARAMS.muted ? 0.3 : 0.7) + rnd() * 0.3,
0.8 + rnd() * 0.2
],
],
positionFunctionY: positionFunctions.asinusoidalPosition,
positionFunctionX: positionFunctions.asinusoidalPosition,
positionFunctionZ: positionFunctions.asinusoidalPosition,
ranges: {
light: [0.01,.1],
dark: [.5,.51],
strong: [.85,.95],
strongAlt: [.7, .8],
strongAlt2: [.8,.95],
},
}
}
const rndFromRange = (arr, rangeArr) => rndArr(arr, rangeArr[0], rangeArr[1]);
let gradientKind = '1path';
let hueshift = true;
function doit (amountOfColors) {
const randomHarmony = rndArr(harmonies);
const poline = new Poline({
numPoints: amountOfColors,
anchorColors: settins[gradientKind].anchors(PARAMS.baseHue, randomHarmony),
positionFunctionY: settins[gradientKind].positionFunctionY,
positionFunctionX: settins[gradientKind].positionFunctionX,
positionFunctionZ: settins[gradientKind].positionFunctionZ,
});
let cssColors = poline[colorMethod];
document.documentElement.style.setProperty('--c-white', poline[colorMethod][poline.colors.length - 1]);
document.documentElement.style.setProperty('--c-light', rndFromRange(cssColors, settins[gradientKind].ranges.light));
document.documentElement.style.setProperty('--c-dark', rndFromRange(cssColors, settins[gradientKind].ranges.dark));
document.documentElement.style.setProperty('--c-strong', rndFromRange(cssColors, settins[gradientKind].ranges.strong));
document.documentElement.style.setProperty(
'--clrs',
poline[colorMethod].map((c, i) => `${c} ${i/poline.colors.length * 100}% ${(i+1)/poline.colors.length * 100}%`).join(', ')
);
if (PARAMS.moreHues) {
poline.shiftHue(rndArr(harmonies));
}
poline.invertedLightness = true;
document.documentElement.style.setProperty(
'--clrs2',
poline[colorMethod].map((c, i) => `${c} ${i/poline.colors.length * 100}% ${(i+1)/poline.colors.length * 100}%`).join(', ')
);
cssColors = poline[colorMethod]
document.documentElement.style.setProperty('--c-strongAlt', rndFromRange(cssColors, settins[gradientKind].ranges.strongAlt));
document.documentElement.style.setProperty('--c-strongAlt2', rndFromRange(cssColors, settins[gradientKind].ranges.strongAlt2));
const $bg = document.querySelector('[data-bg]');
const gridSize = 50;
$bg.innerHTML = '';
new Array(gridSize * gridSize).fill('').forEach((_,i)=> {
const x = i % gridSize;
const y = Math.floor(i / gridSize);
const xRel = x / gridSize;
const yRel = y / gridSize;
const n = noise2D(xRel * 2, yRel * 2);
if (n > 0) {
const $d = document.createElement('i');
$d.classList.add('bg__dot');
$d.style.setProperty('--x', xRel);
$d.style.setProperty('--y', yRel);
$d.style.setProperty('--n', n);
$d.style.setProperty('--c',
poline[colorMethod][Math.floor(n * (poline.colors.length * 0.5))]
);
$bg.appendChild($d);
}
});
}
let colors = 20;
doit(colors);
pane.on('change', (ev) => {
rnd = new seedrandom(seed);
doit(colors);
});
pane.addInput(PARAMS, 'moreHues', {label: 'more hues'}).on('change', (ev) => {
hueshift = ev.value;
});
pane.addInput(PARAMS, 'muted', {label: 'muted'});
//pane.addInput(PARAMS, 'brighten', {label: 'brighten'});
pane.addInput(PARAMS, 'p3', {label: 'p3 gamut'}).on('change', (ev) => {
colorMethod = ev.value ? 'colorsCSSoklch' : 'colorsCSS';
})
const baseHue = pane.addInput(PARAMS, 'baseHue', {
min: 0,
max: 360,
step: 0.01
});
pane.addBlade({
view: 'list',
label: 'preset',
options: [
{text: 'single path', value: '1path'},
{text: 'knee path', value: '2paths'},
],
value: '1path',
}).on('change', (ev) => {
gradientKind = ev.value;
});
const newPal = () => {
PARAMS.baseHue = rnd() * 360;
baseHue.refresh();
seed = Math.random()
rnd = new seedrandom(seed);
noise2D = createNoise2D();
doit(colors);
}
const $world = document.querySelector('[data-thing]').addEventListener('click', newPal);
pane.addButton({
title: 'New Colors',
}).on('click', newPal)
:root {
height: 100vh;
background: var(--c-dark);
overflow: hidden;
--tp-base-background-color: var(--c-white);
--tp-base-shadow-color: var(--c-dark);
--tp-button-background-color: var(--c-dark);
--tp-button-background-color-active: var(--c-strong);
--tp-button-background-color-focus: var(--c-strong);
--tp-button-background-color-hover: var(--c-strongAlt);
--tp-button-foreground-color: var(--c-white);
--tp-container-background-color: hsla(230, 15%, 30%, 0.20);
--tp-container-background-color-active: hsla(230, 15%, 30%, 0.32);
--tp-container-background-color-focus: hsla(230, 15%, 30%, 0.28);
--tp-container-background-color-hover: hsla(230, 15%, 30%, 0.24);
--tp-container-foreground-color: hsla(230, 10%, 30%, 1.00);
--tp-groove-foreground-color: hsla(230, 15%, 30%, 0.10);
--tp-input-background-color: hsla(230, 15%, 30%, 0.10);
--tp-input-background-color-active: hsla(230, 15%, 30%, 0.22);
--tp-input-background-color-focus: hsla(230, 15%, 30%, 0.18);
--tp-input-background-color-hover: hsla(230, 15%, 30%, 0.14);
--tp-input-foreground-color: hsla(230, 10%, 30%, 1.00);
--tp-label-foreground-color: hsla(230, 10%, 30%, 0.70);
--tp-monitor-background-color: hsla(230, 15%, 30%, 0.10);
--tp-monitor-foreground-color: hsla(230, 10%, 30%, 0.50);
}
.bg {
position: absolute;
inset: 0;
&__inner {
position: absolute;
width: 100vmax;
height: 100vmax;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
&__dot {
position: absolute;
width: 1%;
aspect-ratio: 1;
top: calc(100% * var(--y));
left: calc(100% * var(--x));
background: var(--c);
border-radius: calc(75% - var(--n) * 50%);
transform: scale(calc(var(--n) * var(--n) * var(--n) * 2));
}
}
.debug {
position: absolute;
bottom: 0;
left: 50%;
height: 5vmin;
width: 75vmin;
background-image: linear-gradient(90deg, var(--clrs)),
linear-gradient(90deg, var(--clrs2));
background-size: 100% 50%;
background-repeat: no-repeat;
background-position: 0 0, 0 100%;
z-index: 2;
transform: translateY(101%) translateX(-50%);
transform-origin: 0 0;
transition: 300ms transform cubic-bezier(.3, .7,0,1) 0ms;
}
.is-open .debug {
transform: translateY(0) translateX(-50%);
transition: 300ms transform cubic-bezier(.3, .7,0,1) 300ms;
}
.thing {
position: fixed;
top: 50%;
left: 50%;
width: 75vmin;
aspect-ratio: 1;
transform: translate(-50%, -50%);
background: linear-gradient(135deg, var(--c-light), var(--c-strongAlt2));
//box-shadow: 0 0 0 1vmin var(--c-strongAlt2);
transition: 300ms transform cubic-bezier(.3, .7,0,1) 100ms;
cursor: pointer;
}
.is-open .thing {
transform: translate(-50%, -50%) translateY(-25vmin);
transition: 300ms transform cubic-bezier(.3, .7,0,1);
}
.thing__inner {
position: absolute;
transform: translate(-50%, -50%);
top: 50%;
left: 50%;
width: 50%;
aspect-ratio: 1;
background: var(--c-dark);
&::after {
content: 'Aa';
position: absolute;
color: var(--c-white);
top: 10%;
left: 10%;
border-radius: 50%;
line-height: 1;
font-weight: bold;
text-decoration: underline;
text-decoration-color: var(--c-strong);
font-size: 4vmin;
}
}
.thing__half {
position: absolute;
inset: 0 0 0 50%;
background: var(--c-strong);
&::after {
content: '';
position: absolute;
inset: 50% 0 0;
background: linear-gradient(180deg, var(--c-strongAlt), var(--c-strongAlt2));;
}
}
.settings {
position: fixed;
width: 300px;
max-width: 75vmin;
bottom: 3vmin;
left: 50%;
transform: translateX(-50%) translateY(-50%);
transition: 300ms transform cubic-bezier(.3, .7,0,1);
}
.is-open .settings {
width: 75vmin;
transform: translateX(-50%) translateY(-5vmin);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment