creative playground using poline https://meodai.github.io/poline/
A Pen by David Aerne on CodePen.
creative playground using poline https://meodai.github.io/poline/
A Pen by David Aerne on CodePen.
<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); | |
} |