Save Expand icon

Ron Valstar
front-end developer

Experiment: CSS background with radial gradients

Over the past weeks I came upon this old fiddle in my last post, saw some iOS screen or whatever, and was thinking it had been a while since I made an experiment.
Since I still like single element thingies I started coding this animated radial background gradient.

It's a single background seeded with a prng and animated with Perlin noise.

I'll save the detailed explanation for a later post. I just wanted to share the source and some cool varieties.

ps: there's a fullscreen button on the top right!

/**
 * Radial gradients
 * @module experiment/radialgradient
 * @see module:experiment/base
 */
import experiment from './base'
import {animate} from '../signal/signals'
import {random} from '../math/lcg'
import {noise} from '../math/perlin'

let target
let input
const percentDecimals = 4
const speed = 0.000005
const speedScale = Math.PI
const scale = 400
const state = []
const bindings = []

function init(_target){

  target = _target

  const label = document.createElement('label')
  Object.assign(label.style, {
    position: 'absolute'
    ,left: '0'
    ,top: '0'
    ,width: '100%'
    ,height: '100%'
  })

  input = document.createElement('input')
  input.type = 'number'
  input.value = location.hash.substr(1)||8191
  Object.assign(input.style, {
    border: '0'
    ,outline: 'none'
    ,background: 'transparent'
    ,width: '100%'
    ,height: '100%'
    ,verticalAlign: 'top'
    ,fontFamily: 'courier'
    ,textAlign: 'center'
    ,fontSize: '2rem'
    ,fontWeight: 'bold'
    ,opacity: 0
  })
  label.appendChild(input)

  //target.appendChild(label)

  setState()
  initEvents()
}

function initEvents(){
  input.addEventListener('change', onInputChange, false)
  target.addEventListener('click', onTargetClick, false)
  window.addEventListener('hashchange', onHashChange, false)
  bindings.push(animate.add(setColor))
}

function exit(){
  input.removeEventListener('change', onInputChange)
  target.removeEventListener('click', onTargetClick)
  window.removeEventListener('hashchange', onHashChange)
  //animate.remove(setColor)
  bindings.splice(0,Number.MAX_SAFE_INTEGER)
    .forEach(b=>b.remove())
  target.style.removeProperty('background')
}

function onTargetClick(){
  location.hash = input.value = Math.random()*1E9<<0
  setState()
}

function onHashChange(){
  input.value = parseInt(location.hash.substr(1), 10)
  setState()
}

function onInputChange(){
  location.hash = input.value
  setState()
}

function setState(){
  random(input.value, 22)
  state.length = 0
  state.push(...array(3+random()*4<<0 ,()=>({
    positions: array(2,()=>array(2,()=>random()*speedScale))
    ,gradient: getRange(clr(), 2+random()*random()*20<<0)
  })))
}

function setColor(deltaT, millis){
  target.style.background = state.map(({positions,gradient}) =>
    `radial-gradient(circle at ${getPosition(positions, millis)}, ${gradient})`).join(',')
}

function getPosition(start,millis){
  millis = Date.now()
  return start.map(([a,b])=>{
    const n = noise(a + millis*speed, b) - 0.5
    return (n*scale + 50).toFixed(percentDecimals) + '%'
  }).join(' ')
}

function getRange(clr, num=10) {
  const divide = random()
  const parts = array(num+1,(o,i)=>100*easeCirc(i/num,divide).toFixed(percentDecimals))
  const [clrIn, clrOut] = random()>0.5?[clr, 'transparent']:['transparent', clr]
  return array(num).map((o,i)=>`${clrIn} ${parts[i]}%, ${clrOut} ${parts[i+1]}%`).join(', ')
}

function easeOutCirc(x){
  return Math.sqrt(1 - Math.pow(x - 1, 2))
}

function easeInCirc(x){
  return 1 - Math.sqrt(1 - Math.pow(x, 2))
}

function easeCirc(x, part){
  return part<0.4 && easeInCirc(x)
      || part>0.6 && easeOutCirc(x)
      || x
}

function clr(){
  return '#'+('00000'+(random()*(1<<24)|0).toString(16)).slice(-6)
}

function array(num, map){
  const a = new Array(num).fill(0)
  return map?a.map(map):a
}

const inst = experiment('radialgradient', {init, exit})

export const radialgradient = inst.expose

comment send comment