<canvas id="c">Nope.</canvas>
<select id="pattern">
  <option value="rnd">random</option>
  <option value="circles">circles</option>
  <option value="shapes">shapes</option>
  <option value="squares">squares</option>
  <option value="plane">plane</option>
</select>
#c
{
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
}
audio
{
  position: absolute;
  bottom: 0;
  left: 50%;
  width: 400px;
  margin-left: -200px;
}
#pattern
{
  position: absolute;
  top: 1px;
  left: 0;
}
function pickRandom(obj)
{
  var result;
  var count = 0;
  for(var prop in obj)
    if(Math.random() < 1/++count)
      result = prop;
  return obj[result];
}
Math.hPI = 0.5 * Math.PI;
Math.PI2 = 2 * Math.PI;
$('body').one('click', function(){
  var $c = $('#c'),
      c = $c[0],
      ctx = c.getContext('2d'),
      maxRadius = Infinity;
  var counter = 0,
      nextPatternAt = 60 * (Math.random() * 4 + 2);
  var url = 'https://pause-geek.fr/audio/temp/zikconcours.wav';
  //url = 'https://pause-geek.fr/ajax/getfilecontent.php?mime=audio/wave&url='+encodeURIComponent(url); //Allow cross-origin requests (not working)
  var aa = new audioAnalyser()
    .ready(function(){
      //console.log('ready');
      /*if(c.requestFullscreen)
        c.requestFullscreen();
      else if(c.mozRequestFullScreen)
        c.mozRequestFullScreen();
      else if(c.webkitRequestFullscreen)
        c.webkitRequestFullscreen();*/
      })
    .set('autoplay', true)
    .set('cors', true)
    // .volume(.5)
    //.set('appendTo', document.body) //<- Uncomment to show the audio element
    .load(url);//.mute();
  
  var circlePattern = function()
  {
    var middleSize = 0.02, scale = 1,
        circles = [],
        linesGap = 0.08, linesGapNeeded = 0.05,
        linesOffset = 0, linesOffsetSpeed = 0,
        rotation = 0, rotationSpeed = 0,
        angleNeeded = Math.PI2, sensRotation = 1;
    this.step = function(aa)
    {
      var circle;
      for(var i = 0, l = circles.length; i < l; i++)
      {
        circle = circles[i];
        circle.size += circle.speed;
        if(circle.size > 1)
          circles.splice(i--, 1), l--;
      }
      rotationSpeed = aa.amplitude * 0.15;
      linesOffsetSpeed = 2 * aa.amplitude;
      rotation += sensRotation * rotationSpeed;
      if(sensRotation * (rotation - angleNeeded) >= 0)
      {
        sensRotation *= -1;
        angleNeeded = rotation + sensRotation * (Math.random() * 6*Math.PI + Math.hPI);
      }
      linesOffset += linesOffsetSpeed;
      linesGap = comeCloser(linesGap, linesGapNeeded, 1 / (aa.amplitude * 0.03), 0.001);
      if(linesGap == linesGapNeeded)
        linesGapNeeded = 0.035 + Math.random() * 0.1;
      linesOffset %= linesGap;
      var previous = middleSize;
      middleSize = 0.02 + aa.amplitude * 0.03;
      scale = 1 + aa.amplitude * 0.2;
      if(middleSize - previous > 0.004)
        circles.push({size: middleSize, speed: middleSize - previous});
    };
    this.draw = function(ctx,w,h)
    {
      var i, l, circle, mid = {x: w*0.5, y: h*0.5};
      ctx.fillStyle = 'rgba(255,255,255,0.7)';
      ctx.fillRect(0,0,w,h);
      ctx.fillStyle = 'black';
      ctx.save();
      ctx.translate(mid.x, mid.y);
      ctx.rotate(rotation);
      ctx.lineWidth = 1;
      ctx.beginPath();
      var maxRadius = (w+h)*0.5;
      var lgStep = maxRadius * linesGap;
      for(i = -maxRadius-linesOffset; i < maxRadius; i += lgStep)
      {
        ctx.moveTo(i, -maxRadius);
        ctx.lineTo(i, maxRadius);
      }
      ctx.stroke();
      ctx.restore();
      ctx.save();
      ctx.translate(mid.x, mid.y);
      ctx.scale(scale, scale);
      ctx.beginPath();
      ctx.arc(0, 0, maxRadius * middleSize, 0, Math.PI2, false);
      ctx.fill();
      var mult;
      for(i = 0, l = circles.length; i < l; i++)
      {
        circle = circles[i];
        mult = maxRadius * circle.size;
        ctx.lineWidth = 0.05 * mult;
        ctx.beginPath();
        ctx.arc(0, 0, mult, 0, Math.PI2, false);
        //Useless moveTo to prevent random arc stroking bug in chrome
        ctx.moveTo(mult+1,0);
        ctx.stroke();
      }
      ctx.restore();
    };
    return this;
  };
  var shapesPattern = function()
  {
    var sin = Math.sin, cos = Math.cos, atan2 = Math.atan2, sqrt = Math.sqrt;
    var bufferSize = 1024, step = Math.PI2 / bufferSize,
        phi, sqrDA = Math.PI / 4, triDA = Math.PI / 3,
        sqrDA2 = 2*sqrDA, triDA2 = 2*triDA, triCos = 0.5*(1+cos(triDA)),
        deltaf = 0.0001 * Math.PI, hdeltaf = deltaf * 0.5;
    var shapes = {
      circle: [],
      square: [],
      triangle: []
      //heart: []
    };
    var currentShape = [];
    var alteredShape = [];
    var neededShape = shapes.circle;
    var shapeAngle = 0, shapeSpeed = 0;
    var previous = 0;
    var balls = [];
    var points = [], pointsAngle = 0, pointsSpeed = 0;
    var i, n;
    for(i = 0; i < 10; i++)
      balls.push({p: {x: Math.random() * 500 - 250,
                      y: Math.random() * 500 - 250},
                  v: {x: 0, y:0},
                  a: {x: 0, y:0},
                  s: Math.random() * 30 + 5,
                  f: {x:0, y:0}});
    for(i = 0; i < 50; i++)
      points.push({d: Math.random()+0.1, a: Math.PI2 * Math.random()});
    for(i = 0, n = Math.PI2; i < n; i += step)
    {
      currentShape.push({a: i, d: 1});
      shapes.circle.push({a: i, d: 1});
      phi = (i + sqrDA) % sqrDA2 - sqrDA;
      shapes.square.push({a: i, d: 1/cos(phi)});
      phi = (i + triDA) % triDA2 - triDA;
      shapes.triangle.push({a: i, d: triCos/cos(phi)});
      //shapes.heart.push(multiplyPoint(toPolar(heartShape(i)), 0.1));
    }
    // Buggy
    shapes.circle[1023].d = 0.9999;
    //shapes.heart.sort(orderPolar); //Angles are messed up
    function orderPolar(a,b) { return a.a - b.a; }
    /*function heartShape(t)
    {
      var sint = sin(t);
      return {x: 16*sint*sint*sint,
              y: cos(4*t) + 2*cos(3*t) + 5*cos(2*t) - 13*cos(t) - 3};
    }*/
    function addPoint(a,b) { return {x: a.x+b.x, y: a.y+b.y}; }
    function multiplyPoint(p, f) { return {a: p.a, d: p.d * f}; }
    function toPolar(p)
    {
      return {a: (atan2(p.y, p.x) + Math.PI2) % Math.PI2,
              d: sqrt(p.y*p.y + p.x*p.x)};
    }
    function toPoint(p)
    {
      return {x: p.d * cos(p.a),
              y: p.d * sin(p.a)};
    }
    this.step = function(aa)
    {
      var a = aa.amplitude, tmp;
      if(a - previous > 0.2)
      {
        while((tmp = pickRandom(shapes)) == neededShape);
        neededShape = tmp;
      }
      previous = a;
      shapeSpeed = a * 0.02;
      shapeAngle += shapeSpeed;
      shapeAngle %= Math.PI2;
      var i, l, floor, b, pol;
      for(i = 0, l = bufferSize; i < l; i++)
      {
        floor = currentShape[i];
        floor.a = comeCloser(floor.a, neededShape[i].a, 15);
        floor.d = comeCloser(floor.d, neededShape[i].d, 15);
        alteredShape[i] = {a: floor.a,
                           d: floor.d + aa.signal[i] * 0.2};
      }
      for(i = 0, l = balls.length; i < l; i++)
      {
        b = balls[i];
        pol = toPolar(b.p);
        b.f = toPoint(multiplyPoint(pol, -pol.d*0.01));
        b.f = addPoint(b.f, toPoint(multiplyPoint(pol, 340 * a*a*a*a / (0.01*pol.d+1))));
        tmp = toPolar(b.f);
        tmp.a += tmp.d * (Math.random() * deltaf - hdeltaf);
        b.f = toPoint(tmp);
        b.a = toPoint(multiplyPoint(toPolar(b.f), 1 / (Math.PI2 * b.s)));
        b.v = addPoint(b.v, b.a);
        b.v = toPoint(multiplyPoint(toPolar(b.v), 0.96));
        b.p = addPoint(b.p, b.v);
      }
      pointsSpeed = 0.1 * a;
      pointsAngle += pointsSpeed;
      pointsAngle %= Math.PI2;
    };
    this.draw = function(ctx,w,h)
    {
      var i, l, circle, mid = {x: w*0.5, y: h*0.5};
      ctx.fillStyle = 'rgba(255,255,255,0.7)';
      ctx.fillRect(0,0,w,h);
      ctx.fillStyle = 'black';
      ctx.save();
      ctx.translate(mid.x, mid.y);
      ctx.save();
      var m = Math.min(w,h) * 0.3;
      ctx.scale(m,m);
      ctx.rotate(-shapeAngle);
      ctx.lineWidth = 2 / m;
      ctx.beginPath();
      //ctx.arc(0,0,1,0,Math.PI2,false);
      var p = toPoint(alteredShape[0]), b;
      ctx.moveTo(p.x, p.y);
      for(i = 1, l = alteredShape.length; i < l; i++)
      {
        p = toPoint(alteredShape[i]);
        ctx.lineTo(p.x, p.y);
      }
      ctx.fill();
      //ctx.clip();
      ctx.restore();
      ctx.lineWidth = 1;
      ctx.fillStyle = 'rgba(255,255,255,1)';
      //ctx.globalCompositeOperation = 'xor';
      //ctx.beginPath();
      for(i = 0, l = balls.length; i < l; i++)
      {
        ctx.beginPath();
        b = balls[i];
        ctx.arc(b.p.x, b.p.y, b.s, 0, Math.PI2, false);
        ctx.fill();
      }
      //ctx.fill();
      ctx.fillStyle = 'rgba(0,0,0,1)';
      var r = 0.5 * Math.max(w, h), newR, newA;
      //ctx.beginPath();
      for(i = 0, l = points.length; i < l; i++)
      {
        b = points[i];
        newR = r*b.d;
        newA = pointsAngle + b.a;
        p = {x: 1.3*newR * Math.cos(newA),
             y: 0.6*newR * Math.sin(newA)};
        ctx.beginPath();
        ctx.arc(p.x, p.y, 1.5, 0, Math.PI2, false);
        ctx.fill();
      }
      //ctx.fill();
      ctx.restore();
    };
    return this;
  };
  var squarePattern = function()
  {
    var invertBG = false, filledWidth = 0, nextFilledWidth = 0;
    var bgSquares = [];
    var fillers = [];
    var scale = 1;
    var previous = 0;
    var angle = 0;
    var angleSpeed = 0;
    var coverSize = Math.PI;
    newBG();
    function newBG()
    {
      bgSquares = [];
      var theSquare;
      for(var i = 0, l = Math.floor(Math.random() * 10 + 25); i < l; i++)
      {
        theSquare = {
          w: Math.floor(Math.random() * 10 + 4) * 0.01,
          h: Math.floor(Math.random() * 10 + 4) * 0.01
        };
        theSquare.l = Math.floor(100 * Math.random() * (1 - theSquare.w)) * 0.01;
        theSquare.t = Math.floor(100 * Math.random() * (1 - theSquare.h)) * 0.01;
        bgSquares.push(theSquare);
      }
    }
    this.step = function(aa)
    {
      var a = aa.amplitude, i, l, filler, p;
      scale = 1 + a*a*a * 0.5;
      var d = a - previous;
      if(d > 0.12)
      {
        newBG();
        var nw = d * 0.3;
        fillers.push({r: 1, rt: nextFilledWidth, w: nw});
        nextFilledWidth += nw;
        if(nextFilledWidth >= 1)
          nextFilledWidth = 0;
      }
      //if(d > 0.2) newBG();
      previous = a;
      for(i = 0, l = fillers.length; i < l; i++)
      {
        filler = fillers[i];
        filler.r = comeCloser(filler.r, filler.rt, 15, 0.001);
        if(filler.r == filler.rt)
        {
          filledWidth = filler.r + filler.w;
          if(filledWidth >= 1)
          {
            invertBG = !invertBG;
            filledWidth = 0;
          }
          fillers.splice(0, i+1);
          l -= i+1;
          i = 0;
        }
      }
      angleSpeed = a*a * 0.3;
      angle += angleSpeed;
      angle %= Math.PI2;
      /*if(coverSize != Math.PI)
        coverSize = comeCloser(coverSize, Math.PI, 5/a, 0.001);*/
    };
    this.draw = function(ctx,w,h)
    {
      var i, l, bgs, filler, mid = {x: w*0.5, y: h*0.5};
      ctx.save();
      ctx.clearRect(0,0,w,h);
      ctx.fillStyle = ctx.strokeStyle = 'black';
      ctx.globalCompositeOperation = 'xor';
      ctx.translate(mid.x, mid.y);
      ctx.beginPath();
      ctx.moveTo(0,0);
      ctx.arc(0,0,w + h,angle,angle+coverSize,false);
      ctx.fill();
      ctx.scale(scale, scale);
      ctx.translate(-mid.x, -mid.y);
      for(i = 0, l = bgSquares.length; i < l; i++)
      {
        bgs = bgSquares[i];
        ctx.fillRect(w*bgs.l, h*bgs.t, w*bgs.w, h*bgs.h);
      }
      if(invertBG)
        ctx.fillRect(0,0,w,h);
      ctx.fillRect(w * (1 - filledWidth),0,w*filledWidth,h);
      for(i = 0, l = fillers.length; i < l; i++)
      {
        filler = fillers[i];
        ctx.fillRect(w * (1 - filler.r - filler.w),0,w*filler.w,h);
      }
      ctx.restore();
    };
    return this;
  };
  //Plane because we transform an audio signal into a list of 2D points
  var planePattern = function()
  {
    var c = [], p = [];
    var circ = [], circCount = 16;
    var dragon = [];
    var i;
    for(i = 0; i < 512; i++)
      c.push({x: 0, y: 0});
    for(i = 0; i < circCount; i++)
      circ.push({angle: Math.random() * Math.PI2,
                 angleSpeed: 0,
                 size: Math.PI / 8 + Math.random() * Math.PI / 16,
                 direction: 2*Math.round(Math.random()) - 1});
    for(i = 0; i < 50; i++)
      dragon.push({p: {x: 0, y: 0},
                   s: {d: 0, a: 0},
                   size: 0.01,
                   amp: 0,
                   ampDir: 1});
    this.step = function(aa)
    {
      var s = aa.signal, _c, _p, i, l;
      for(i = 0; i < 512; i++)
      {
        _c = c[i];
        _p = p[i] = {x: s[i], y: s[i+512]};
        _c.x = comeCloser(_c.x, _p.x, 10);
        _c.y = comeCloser(_c.y, _p.y, 10);
      }
      //spL should be sp.length so 512 but heh, there's a lot of low numbers at the end
      //And using the whole range makes it boring
      var sp = aa.fft.spectrum, spL = 64, spD = spL / circCount;
      for(i = 0; i < circCount; i++)
      {
        _c = circ[i];
        _c.angleSpeed = 2 * sp[Math.floor(i * spD)];
        _c.angle += _c.direction * _c.angleSpeed;
        _c.angle %= Math.PI2;
      }
      var drgn = dragon[0], drgnp;
      var a = aa.amplitude,
          spd = {d: 0.005 * a,
                 a: (Math.random() - 0.5) * Math.PI * 0.3};
      drgn.s.d += spd.d;
      drgn.s.a += spd.a + Math.PI2;
      drgn.s.a %= Math.PI2;
      drgn.s.d *= 0.9;
      var drgnA = (Math.atan2(-drgn.p.y, -drgn.p.x) + Math.PI2) % Math.PI2;
      if(Math.abs(drgnA - drgn.s.a) > Math.PI) drgn.s.a -= Math.PI2;
      var drgnDist = Math.min(1, Math.sqrt(drgn.p.x*drgn.p.x + drgn.p.y*drgn.p.y));
      drgn.s.a += (drgnA - drgn.s.a) * drgnDist*drgnDist*drgnDist*drgnDist;
      drgn.p.x += drgn.s.d * Math.cos(drgn.s.a);
      drgn.p.y += drgn.s.d * Math.sin(drgn.s.a);
      for(i = 1, l = dragon.length; i < l; i++)
      {
        drgnp = drgn;
        drgn = dragon[i];
        sa = Math.atan2(drgn.p.y - drgnp.p.y, drgn.p.x - drgnp.p.x);
        drgn.p.x = drgnp.p.x + drgn.size * Math.cos(sa);
        drgn.p.y = drgnp.p.y + drgn.size * Math.sin(sa);
      }
    };
    this.draw = function(ctx,w,h)
    {
      ctx.save();
      ctx.fillStyle = 'rgba(255,255,255,0.7)';
      ctx.fillRect(0,0,w,h);
      ctx.fillStyle = 'black';
      var hw = 0.5 * w, hh = 0.5 * h, i, _c, r = Math.min(w,h)*0.35;
      ctx.translate(hw, hh);
      ctx.save();
      ctx.scale(2,2);
      for(i = 0; i < 512; i++)
      {
        ctx.fillRect(hw*c[i].x, hh*c[i].y, 1, 1);
      }
      ctx.restore();
      ctx.lineWidth = 2;
      for(i = 0; i < circCount; i++)
      {
        _c = circ[i];
        ctx.beginPath();
        ctx.arc(0, 0, r, _c.angle, _c.angle + _c.size, false);
        ctx.stroke();
      }
      var l, drgn = dragon[0];
      ctx.beginPath();
      ctx.moveTo(hw*drgn.p.x, hh*drgn.p.y);
      for(i = 1, l = dragon.length; i < l; i++)
      {
        drgn = dragon[i];
        ctx.lineTo(hw*drgn.p.x, hh*drgn.p.y);
      }
      ctx.lineWidth = 1.1;
      ctx.stroke();
      ctx.restore();
    };
    return this;
  };
  var patterns = {
    circles: new circlePattern(),
    shapes: new shapesPattern(),
    squares: new squarePattern(),
    plane: new planePattern()
  };
  var pattern = newPattern();
  //Using hash to request a pattern
  //But not working on codepen due to frame things
  var reqPattern = window.location.hash.substring(1);
  if(reqPattern in patterns)
  {
    $('#pattern').val(reqPattern);
    pattern = patterns[reqPattern];
    nextPatternAt = Infinity;
  }
  
  function newPattern()
  {
    /* Maybe shuffle a list & use it as a sequence ? */
    return pickRandom(patterns);
  }
  function step()
  {
    requestAnimationFrame(step);
    if(aa.isready && aa.playing() && counter++ >= nextPatternAt)
    {
      var previous = pattern;
      while(pattern == previous) pattern = newPattern();
      pattern = newPattern();
      nextPatternAt = 60 * (Math.random() * 4 + 2);
      counter = 0;
    }
    pattern.step(aa);
    draw();
  }
  requestAnimationFrame(step);
  function draw()
  {
    if(!aa.audio) return;
    if(!aa.isready)
    {
      ctx.save();
      ctx.clearRect(0,0,c.width,c.height);
      ctx.translate(c.width * 0.5, c.height * 0.5);
      ctx.textBaseline = 'bottom';
      ctx.textAlign = 'center';
      ctx.font = '36px Consolas, monaco, monospace';
      ctx.fillText('WAIT FOR THE MUSIC TO LOAD',0,0,c.width);
      ctx.textBaseline = 'top';
      ctx.font = '20px Consolas, monaco, monospace';
      ctx.fillText('Unless there\'s a bug',0,0,c.width);
      ctx.strokeRect(-100, 40, 200, 20);
      var b = aa.audio.buffered;
      var percent = (!b.length || !aa.audio.duration) ? 0 : b.end(b.length - 1) / aa.audio.duration;
      ctx.fillRect(-100, 40, 200 * percent, 20);
      ctx.textBaseline = 'middle';
      ctx.font = '16px Consolas, monaco, monospace';
      ctx.globalCompositeOperation = 'xor';
      ctx.fillText(Math.round(100 * percent) + '%',0,50,200);
      ctx.restore();
      return;
    }
    ctx.save();
    pattern.draw(ctx, c.width, c.height);
    ctx.restore();
    ctx.fillStyle = 'white';
    ctx.fillRect(0,0,c.width, 1);
    ctx.fillStyle = 'black';
    ctx.fillRect(0,0,c.width * aa.audio.currentTime / aa.audio.duration, 1);
  }
  
  $('#pattern').change(function(){
    var reqPattern = $(this).val();
    if(reqPattern in patterns)
    {
      pattern = patterns[reqPattern];
      nextPatternAt = Infinity;
    }
    else
    {
      nextPatternAt = 60 * (Math.random() * 4 + 1);
    }
  });
  $(window).resize(function(){
    c.width = $c.width();
    c.height = $c.height();
    maxRadius = (c.width + c.height) * 0.5;
  }).resize();var movingTime = false;
  $c.mousedown(function(e){
    if(!aa || !aa.isready) return;
    if(e.pageY <= 10)
    {
      e.preventDefault();
      movingTime = true;
      aa.audio.currentTime = aa.audio.duration * e.pageX / c.width;
    }
    else if(aa.playing()) aa.stop(); else aa.play();
  }).mousemove(function(e){
    if(movingTime)
    {
      e.preventDefault();
      aa.audio.currentTime = aa.audio.duration * e.pageX / c.width;
    }
  }).mouseup(function(e){
    movingTime = false;
  });
});

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. //cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js
  2. https://pause-geek.fr/public/scripts/audio.js
  3. https://pause-geek.fr/public/scripts/raf.js
  4. https://pause-geek.fr/public/scripts/comeCloser.js