<div class="radios"><!-- need to change this to radios and make sure spacebar mapping works for accessibility -->
  <p>Animations:</p>
  <div>
    <input type="checkbox" name="animations" value="animations" id="animations" checked>
    <label for="animations">
      <span>Less</span>
      <span>More</span>
    </label>
  </div>
</div>
<ul>
  <li><button>1</button><i></i><i></i><i></i></li>
  <li><button>2</button><i></i><i></i><i></i></li>
  <li><button>3</button><i></i><i></i><i></i></li>
  <li><button>4</button><i></i><i></i><i></i></li>
  <li><button>5</button><i></i><i></i><i></i></li>
  <li><button>6</button><i></i><i></i><i></i></li>
  <li><button>7</button><i></i><i></i><i></i></li>
  <li><button>8</button><i></i><i></i><i></i></li>
  <li><button>9</button><i></i><i></i><i></i></li>
  <li><button>0</button><i></i><i></i><i></i></li>
</ul>
<ul>
  <li><button>Q</button><i></i><i></i><i></i></li>
  <li><button>W</button><i></i><i></i><i></i></li>
  <li><button>E</button><i></i><i></i><i></i></li>
  <li><button>R</button><i></i><i></i><i></i></li>
  <li><button>T</button><i></i><i></i><i></i></li>
  <li><button>Y</button><i></i><i></i><i></i></li>
  <li><button>U</button><i></i><i></i><i></i></li>
  <li><button>I</button><i></i><i></i><i></i></li>
  <li><button>O</button><i></i><i></i><i></i></li>
  <li><button>P</button><i></i><i></i><i></i></li>
</ul><ul>
  <li><button>A</button><i></i><i></i><i></i></li>
  <li><button>S</button><i></i><i></i><i></i></li>
  <li><button>D</button><i></i><i></i><i></i></li>
  <li><button>F</button><i></i><i></i><i></i></li>
  <li><button>G</button><i></i><i></i><i></i></li>
  <li><button>H</button><i></i><i></i><i></i></li>
  <li><button>J</button><i></i><i></i><i></i></li>
  <li><button>K</button><i></i><i></i><i></i></li>
  <li><button>L</button><i></i><i></i><i></i></li>
</ul>
<ul>
  <li><button>Z</button><i></i><i></i><i></i></li>
  <li><button>X</button><i></i><i></i><i></i></li>
  <li><button>C</button><i></i><i></i><i></i></li>
  <li><button>V</button><i></i><i></i><i></i></li>
  <li><button>B</button><i></i><i></i><i></i></li>
  <li><button>N</button><i></i><i></i><i></i></li>
  <li><button>M</button><i></i><i></i><i></i></li>
  <li><button>?</button><i></i><i></i><i></i></li>
</ul>
<ul id="controls">
  <li><button id="space">Start</button></li>
</ul>
$light: #FAFAFE;
$dark: #16161d;

:root {
  --border1: #93b6d7;
  --border2: #93d7b6;
  --border3: #93b;
}

body {
  height: 100vh;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  justify-content: space-around;
  background: $dark;
  padding: 10vmin 5vmin;
  font-family: -apple-system, 'Segoe UI', sans-serif;
  user-select: none;
}
ul {
  display: flex;
  width: 100%;
  justify-content: space-around;
  height: 10vmin;
  position: relative;
  transition: opacity .2s ease-in, transform .2s ease-in;
  
  li {
    width: 8vmin;
    height: 8vmin;
    i {
      opacity: 0;
      position: absolute;
      display: block;
      top: -1vmin;
      left: -1vmin;
      width: 10vmin;
      height: 10vmin;
      border: .5vmin solid #93b6d7;
      border-color: var(--border1);
      border-radius: 50%;
      z-index: -1;
      
      &:nth-of-type(2) {
        border-color: #93d7b6;
        border-color: var(--border2);
      }
      &:nth-of-type(3) {
        border-color: #93b;
        border-color: var(--border3);
      }
    }
  }
}
ul:nth-of-type(odd) {
  li:nth-of-type(even) {
    align-self: flex-end;
  }
}
ul:nth-of-type(even) {
  li:nth-of-type(odd) {
    align-self: flex-end;
  }
}
button {
  width: 8vmin;
  height: 8vmin;
  line-height: 6vmin;
  font-size: 5vmin;
  appearance: none;
  background: transparent;
  border: .5vmin solid currentColor;
  border-radius: 50%;
  vertical-align: middle;
  padding: 0;
  color: $light;
  touch-action: none;
  
  &:focus {
    outline: none;
    box-shadow: 0 0 3px 3px $light;
  }
  &:disabled {
    opacity: .1;
  }
}
#controls {
  li {
    width: 50vmin;
  }
}
#space {
  width: 50vmin;
  border-radius: 4vmin;
}

.muted {
  ul:not(#controls) {
    transition: opacity .2s ease-out, transform .2s ease-out;
    opacity: 0.6;
  }
}
.muted.more-animations {
  ul:not(#controls) {
    transform: scale(.75);
  }
}

* {
  box-sizing: border-box;
}

.radios {
  opacity: .9;
  display: flex;
  align-items: center;
  width: 14rem;
  margin: 0 auto;
  border: .5vmin solid $light;
  border-radius: 2rem;
  padding: .2rem;
  font-size: .875rem;
  
  p {
    flex: 1;
    color: $light;
    padding-left: .5rem;
  }
  
  div {
    flex: 1;
    input {
      position: absolute;
      opacity: 0;
    }
    label {
      vertical-align: middle;
      display: flex;
      text-align: center;
      
      span {
        flex: 1;
        text-align: center;
        border: 1px solid $dark;
        padding: .5rem;
        border-radius: 2rem;
        transition: background .2s ease-in-out, color .2s ease-in-out;
        background: $light;
        color: $dark;
        &:first-of-type {
          background: $dark;
          color: $light;
        }
      }
    }
    input:checked + label {
      span {
        background: $dark;
        color: $light;
        &:first-of-type {
          background: $light;
          color: $dark;
        }
      }
    }
    input:focus + label {
      border: 1px solid #444;
    }
  }
}

button:disabled {
  opacity: .1;
}
View Compiled
var lis = [].slice.call(document.querySelectorAll(':not(#controls) > li'));
var buttons = [].slice.call(document.querySelectorAll('button:not(#space)'));
var controls = document.querySelector('#controls li');
var space = document.getElementById('space');

polyfillKey(); //I like the simplicity of e.key in keyboard event listeners, which has almost full support in line with the web audio api outside of Safari, so polyfill as needed.

var checkbox = document.getElementById('animations');

var liAnimations = {};
var buttonAnimations = {};


var context;
var oscillators = {};
var gainNodes = {};
var primaryGain;
var baseFrequency = 110;
var EFFECTIVELY_OFF = 0.0000001;
var ON = 0.02;
var LITTLE_ON = 0.2;


buttons.forEach(function(btn, i) {
  btn.setAttribute('disabled', 'disabled');
});


//slight animation for mute button
controls.animate([
  {transform: 'rotate(-1.2deg)'},
  {transform: 'rotate(1.2deg)'}
], {
  duration: Math.random() * 2000 + 8000,
  easing: 'ease-in-out',
  iterations: Infinity,
  direction: 'alternate',
  delay: -12000
});

//slow animations for each key item
lis.forEach(function(item) {
  var x = Math.random() * 20 - 35;
  var y2 = Math.random() * 20 + 4;
  
  item.animate([
    { transform: 'rotate('+x+'deg)' },
    { transform: 'rotate('+y2+'deg)' }
  ], {
    duration: Math.random() * 6000 + 4000,
    iterations: Infinity,
    direction: 'alternate',
    delay: -(Math.random() * 17000),
    easing: 'ease-in-out'
  })
});





//in addition to pointer events, keyboard events are also mapped on the body
document.body.addEventListener('keypress', keyDown);

function pointerDown(e) {
  start(e.currentTarget.textContent.toLowerCase());
}
function pointerUp(e) {
  end(e.currentTarget.textContent.toLowerCase());
}
function pointerOut(e) {
  var char = e.currentTarget.textContent.toLowerCase();
  if (buttonAnimations[char].playState === 'running') {
    end(char);
  }
}

//check if a valid key for notes, or if spacebar (for muting)
function keyDown(e) {
  var char = (e.key || '').toLowerCase();
  if (buttonAnimations[char]) {
    if (buttonAnimations[char].playState === 'running' || liAnimations[char][0].currentTime > 0) {
      end(char);
    } else {
      start(char);
    }
  } else if (char === ' ') {
    spacer(e);
  }
}

//Turn up the gain on the specified key, start the animations (full shaking and speed, if enabled)
function start(char) {
  console.log('start', char);
  if (!checkbox.checked) {
    buttonAnimations[char].play();
  } 
  liAnimations[char].forEach(function(animation) {
    animation.play();
    animation.currentTime = 100 + 225 * Math.random();
  });
  gainNodes[char].gain.setValueAtTime(LITTLE_ON, context.currentTime);
}

//Fade out via the gain on the specified key, cancel the animations
function end(char) {
  console.log('end', char);
  buttonAnimations[char].pause();
  liAnimations[char].forEach(function(animation) {
    animation.cancel();
  });
  gainNodes[char].gain.linearRampToValueAtTime(EFFECTIVELY_OFF, context.currentTime + .4);
}

//map the special handler for spacebar via pointer, for muting
space.addEventListener('click', spacer);

//mute/unmute
function spacer(e) {
//setup for primary volume and audio context
  if (!context && (window.AudioContext || window.webkitAudioContext)) {
    console.log('setup audio context')
    context = new (window.AudioContext || window.webkitAudioContext)();
    primaryGain = context.createGain();
    primaryGain.gain.value = ON;
    primaryGain.connect(context.destination);
    
//set up pointer event listeners
//set up (and pause/cancel) frantic animations for active keys
//set up audio mapping
buttons.forEach(function(btn, i) {
  btn.removeAttribute('disabled');
  if (window.PointerEvent) {
    btn.addEventListener('pointerdown', pointerDown);
    btn.addEventListener('pointerup', pointerUp);
    btn.addEventListener('pointerout', pointerOut);
  } else {
    btn.addEventListener('touchstart', pointerDown);
    btn.addEventListener('touchend', pointerUp);
    btn.addEventListener('touchcancel', pointerOut);
    btn.addEventListener('mousedown', pointerDown);
    btn.addEventListener('mouseup', pointerUp);
    btn.addEventListener('mouseout', pointerOut);
  }
  
  var char = btn.textContent.toLowerCase();
  
  //fast button shake
  buttonAnimations[char] = btn.animate([
    { transform: 'translate(-.5vmin, -.25vmin)' },
    { transform: 'translate(.5vmin, .25vmin)' }
  ], {
    duration: 68,
    iterations: Infinity,
    direction: 'alternate'
  });
  buttonAnimations[char].cancel();
  
  liAnimations[char] = [];
 
  //three color circles scaling up behind button
  [].slice.call(btn.parentNode.querySelectorAll('i')).forEach(function(border, i) {
    var newAnimation = border.animate([
      { transform: 'scale(.8)', opacity: 1 },
      { transform: 'scale(1)', opacity: 1 }
    ], {
      duration: 225,
      iterations: Infinity,
      direction: 'alternate',
      delay: 225 / 3 * i
    });
    newAnimation.pause();
    liAnimations[char].push(newAnimation);
  });
  
  
  
  
setupAnimationsToggle();
  
  
  
  //if web audio api supported, create a new oscillator for the key, with a frequency such that the top left of the keyboard is A2 and goes up by half steps going right and down. The '?' therefore is A5.
  //Turn the gain low by default
  if (context) {
    var oscillator = context.createOscillator();
    var gainNode = context.createGain();

    oscillator.type = 'sawtooth';
    oscillator.frequency.value = baseFrequency * (Math.pow(1.059463094359, i)); // value in hertz, http://www.phy.mtu.edu/~suits/NoteFreqCalcs.html
    
    console.log(char, oscillator.frequency.value)
    oscillator.detune.value = 100; // value in cents
    oscillator.connect(gainNode);

    gainNode.gain.value = EFFECTIVELY_OFF;
    gainNode.connect(primaryGain);

    oscillator.start(0);
    
    oscillators[char] = (oscillator);
    gainNodes[char] = (gainNode);
  }
});
  }
  if (space.textContent === 'Mute') {
    document.documentElement.classList.add('muted');
    primaryGain.gain.value = EFFECTIVELY_OFF;
    space.textContent = 'Muted';
  } else { document.documentElement.classList.remove('muted');
    primaryGain.gain.value = ON;
    space.textContent = 'Mute';
  }
}

//add event for animations toggle and affect all running animations based on new value
function setupAnimationsToggle() {
checkbox.addEventListener('change', function(e) {
  if (checkbox.checked) {
    document.documentElement.classList.remove('more-animations');
    for (var key in buttonAnimations) {
      buttonAnimations[key].cancel();
        
      liAnimations[key].forEach(function(animation) {
        animation.playbackRate = 0;
      });
    }
  } else {
    document.documentElement.classList.add('more-animations');
    for (var key in liAnimations) {
      if (liAnimations[key][0].playState === 'running') {
        buttonAnimations[key].play();
      }
        
      liAnimations[key].forEach(function(animation) {
        animation.playbackRate = 1;
      });
    }
  }
});
}
  







//just the e.key polyfill
function polyfillKey() {
  if (!('KeyboardEvent' in window) ||
        'key' in KeyboardEvent.prototype) {
    return false;
  }
  
  console.log('polyfilling KeyboardEvent.prototype.key')
  var keys = {};
  var letter = '';
  for (var i = 48; i < 58; ++i) {
    letter = String.fromCharCode(i);
    keys[i] = letter;
  }
  for (var i = 65; i < 91; ++i) {
    letter = String.fromCharCode(i);
    keys[i] = letter.toUpperCase();
  }
  for (var i = 97; i < 123; ++i) {
    letter = String.fromCharCode(i);
    keys[i] = letter.toLowerCase();
  }
  keys[63] = '?';
  var proto = {
    get: function (x) {
      var key = keys[this.which || this.keyCode];
      console.log(key);
      return key;
    }
  };
  Object.defineProperty(KeyboardEvent.prototype, 'key', proto);
}






//the inevitable easter egg-ish thing
var spoooooookyMonth = (new Date()).getMonth() === 9;
if (spoooooookyMonth) {
  document.documentElement.style.setProperty('--border1', '#FF9305'); document.documentElement.style.setProperty('--border2', '#4A4A4A'); document.documentElement.style.setProperty('--border3', '#A75401');
}
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://s3-us-west-2.amazonaws.com/s.cdpn.io/61811/web-animations-next-2.2.0.min.js