<canvas></canvas>
<canvas class="clone"></canvas>
<select data-cube></select>
body {
  margin: 0;
  padding: 0;
}
body * {
  all: unset;
}

html {
  overflow: hidden;
}

canvas {
  display: block;
}

.clone {
  opacity: 0;
  pointer-events: none;
}

select {
  position: absolute;
  bottom: 0;
  right: 0;
  max-width: 80vw;
  z-index: 10;
  text-align: right;
  background: #fff;
  padding: 1em 2em;
  
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  
  background: transparent;
  text-decoration: underline;
  color: var(--c);
  /*mix-blend-mode: difference;*/
  font-weight: bold;
  cursor: pointer;
}

@supports (mix-blend-mode: hue) {
  .clone {
    opacity: 1;
    position: absolute;
    top: 0;
    left: 0;
    mix-blend-mode: hue;
    filter: blur(3vmin);
    pointer-events: none;
  }
}
View Compiled
import { ryb2rgb, rybHsl2rgb, cubes } from "https://esm.sh/rybitten/";
import { generateColorRamp } from 'https://esm.sh/rampensau/';
import seedrandom from "https://esm.sh/seedrandom@3.0.5";

let seed = Math.random();
let rnd = new seedrandom(seed);

let currentCube = cubes.get('itten').cube;

const formatCSS = (rgb) => {
  return `rgb(${Math.round(rgb[0] * 255)} ${Math.round(rgb[1] * 255)} ${Math.round(rgb[2] * 255)})`;
};

const newOptions = () => ({
  total: 6,
  hStart: rnd() * 360,
  hCycles: rnd() < .5 ? -1.25 + rnd() : 1.25 + rnd(),
  sRange: rnd() < .5 ? [
    .2 + rnd() * .2, 
    .25 + rnd() * .3
  ] : rnd() < .5 ? [1, rnd()] : [
    .4 + rnd() * .2, 1
  ],
  sEasing: x => Math.pow(x, 2),
  lRange: rnd() < 1 ? [
    rnd() < .5 ? // half of the palettes will become pretty bright
    .55 + rnd() * .3 : 
    .88 + rnd() * .12,
    .01 + rnd() * .4, 
  ] : [1, .5 + rnd() * .2],
  lEasing: x => Math.pow(x, 1.1),// x => -(Math.cos(Math.PI * x)),
});

const $can = document.querySelector('canvas');
const $canClone = document.querySelector('canvas.clone');
const ctx = $can.getContext('2d');
const ctxClone = $canClone.getContext('2d');

function shuffleArray(array) {
  let currentIndex = array.length,  randomIndex;

  // While there remain elements to shuffle.
  while (currentIndex > 0) {

    // Pick a remaining element.
    randomIndex = Math.floor(rnd() * currentIndex);
    currentIndex--;

    // And swap it with the current element.
    [array[currentIndex], array[randomIndex]] = [
      array[randomIndex], array[currentIndex]];
  }

  return array;
}

function drawShape (x, y, size, colorsIn, hasGradient = false) {
  const colors = rnd() < .5 ? shuffleArray(colorsIn) : colorsIn;
  ctx.fillStyle = colors[3];
  ctx.fillRect(x, y, size, size);

  let minimalBg = true;
  if (rnd() < .5) {
    minimalBg = false;
    ctx.fillStyle = colors[4];
    ctx.fillRect(x + size * .5, y, size * .5, size);
  }

  const circleSize = size * .3;
  const circleX = x + size * .5;
  const circleY = y + size * .5;
  ctx.save();
  ctx.translate(circleX, circleY);
  const rotation = Math.PI / 2 * Math.floor(rnd() * 2);
  if (rnd() < .5) {
    ctx.scale(-1, 1);
  }
  ctx.rotate(rotation);

  let fillGradients = hasGradient ? rnd() < .75 : false;
  
  if (fillGradients) {
    const gradient = ctx.createLinearGradient(-circleSize, 0, circleSize, 0);
    gradient.addColorStop(0, colors[0]);
    gradient.addColorStop(1, colors[1]);
    ctx.fillStyle = gradient;
  } else {
    ctx.fillStyle = colors[0];
  }
  

  ctx.beginPath();
  ctx.arc(0, 0, circleSize, 0, 2 * Math.PI);
  ctx.fill();

  if (rnd() < .2) {
  ctx.fillRect(-size * .3 + circleSize, -size * .3 + circleSize - size / 2, size / 2, size / 2);
  }

  if (rnd() < .75 || minimalBg) {
    if (fillGradients && rnd() < .5) {
      const gradient = ctx.createLinearGradient(-circleSize, 0, circleSize, 0);
      gradient.addColorStop(0, colors[1]);
      gradient.addColorStop(1, colors[2]);
      ctx.fillStyle = gradient;
    } else {
      ctx.fillStyle = colors[1];
    }
    ctx.beginPath();
    ctx.arc(0, 0, circleSize, 0, Math.PI);
    ctx.fill();

    if (fillGradients && rnd() < .5) {
      const gradient = ctx.createLinearGradient(-circleSize, 0, circleSize, 0);
      gradient.addColorStop(0, colors[2]);
      gradient.addColorStop(1, colors[3]);
      ctx.fillStyle = gradient;
    } else {
      ctx.fillStyle = colors[2];
    }
    ctx.rotate(Math.PI);
    ctx.beginPath();
    ctx.arc(0, 0, circleSize, 0, Math.PI / 2);
    ctx.lineTo(0, 0);
    ctx.fill();
  }

  ctx.restore();
}

let t = 0;
function doIt () {
  const w = window.innerWidth;
  const h = window.innerHeight;

  $can.width = w;
  $can.height = h;

  const outerShapeSize = Math.min(w, h) * .4;

  const options = newOptions();
  const colorHSL = generateColorRamp(options);
  const colors = colorHSL.map(hsl => formatCSS(rybHsl2rgb(hsl, {cube: currentCube})));
  document.documentElement.style.setProperty('--w', formatCSS(ryb2rgb([0,0,0], {cube: currentCube})));
  document.documentElement.style.setProperty('--b', formatCSS(ryb2rgb([1,1,1], {cube: currentCube})));
  if (rnd() < .5) {
    ctx.fillStyle = colors.shift();
    
  } else {
    if (rnd() < .5) {
      colors.unshift(colors.pop());
    }
    colors.pop()
    ctx.fillStyle = colors.pop();
  }

  ctx.fillRect(0, 0, w, h);

  const hasGradient = rnd() < .3;

  if ( rnd() < .8 ) {
  const shapeCount = 2 + Math.floor(rnd() * 4);
  const shapeSize = outerShapeSize / shapeCount * 1.2;
  const shapeMargin = shapeSize * .25;
  const shapeTotalSize = shapeSize + shapeMargin;
  const shapeTotalCount = shapeCount * shapeCount;
  const shapeTotalWidth = shapeTotalSize * shapeCount;
  const shapeTotalHeight = shapeTotalSize * shapeCount;
  const shapeStartX = w/2 - shapeTotalWidth/2;
  const shapeStartY = h/2 - shapeTotalHeight/2;

  for (let i = 0; i < shapeTotalCount; i++) {
    options.hStart += 360 / shapeTotalCount;
    options.hStart %= 360;
    const newColors = generateColorRamp(options).map(hsl => formatCSS(rybHsl2rgb(hsl, {cube: currentCube})));
    const x = shapeStartX + (i % shapeCount) * shapeTotalSize + shapeMargin/2;
    const y = shapeStartY + Math.floor(i / shapeCount) * shapeTotalSize + shapeMargin/2;
    drawShape(x, y, shapeSize, newColors, hasGradient);
  }
  } else {
    drawShape(w/2 - outerShapeSize/2, h/2 - outerShapeSize/2, outerShapeSize, colors, hasGradient);
  }


  $canClone.width = w;
  $canClone.height = h;
  if (rnd() < .3) {
    ctxClone.drawImage($can, 0, 0);
  } else {
    ctxClone.clearRect(0, 0, w, h);
  }
  
}

doIt();


let isRunning = true;
let int = setInterval(() => {
  if (isRunning) {
    seed = Math.random();
    rnd = new seedrandom(seed);
    doIt();
  }
}, 1100);

$can.addEventListener('click', () => {
  if(!isRunning) {
    seed = Math.random();
    rnd = new seedrandom(seed);
    doIt();
  }
  clearInterval(isRunning);
  isRunning = false;
});

const $select = document.querySelector('[data-cube]');


for (const [cubeKey, cube] of cubes) {
  const $opt = document.createElement('option');
  $opt.value = cubeKey;
  $opt.innerHTML =  cube.title + ' — ' + cube.author + ` / ${cube.year}`;
  if (cubeKey === 'itten') {
    $opt.selected = 'selected';
  }
  $select.appendChild($opt);
}

$select.addEventListener('change', (e) => {
  isRunning = false;
  
  rnd = new seedrandom(seed);
  currentCube = cubes.get(e.target.value).cube;
  doIt();
});

window.addEventListener('resize', () => {
  rnd = new seedrandom(seed);
  doIt();
})
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.