Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URL's added here will be added as <link>s in order, and before the CSS in the editor. If you link to another Pen, it will include the CSS from that Pen. If the preprocessor matches, it will attempt to combine them before processing.

+ add another resource

JavaScript

Babel is required to process package imports. If you need a different preprocessor remove all packages first.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Behavior

Save Automatically?

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                <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">
    <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">Mute</button></li>
</ul>
              
            
!

CSS

              
                $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;
  }
}
#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;
    }
  }
}


              
            
!

JS

              
                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;

//setup for primary volume and audio context
if (window.AudioContext || window.webkitAudioContext) {
  context = new (window.AudioContext || window.webkitAudioContext)();
  primaryGain = context.createGain();
  primaryGain.gain.value = ON;
  primaryGain.connect(context.destination);
}

//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'
  })
});

//set up pointer event listeners
//set up (and pause/cancel) frantic animations for active keys
//set up audio mapping
buttons.forEach(function(btn, i) {
  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 (matchMedia('(prefers-reduced-motion)').matches) {
  checkbox.checked = true;
}
  
  
  
  //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);
  }
});




//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) {
  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');
}
              
            
!
999px

Console