<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: 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');
}
This Pen doesn't use any external CSS resources.