diff --git a/esp/esp/themes/theme_data/fruitsalad/less/main.less b/esp/esp/themes/theme_data/fruitsalad/less/main.less index a8ba816da3..feb237428b 100644 --- a/esp/esp/themes/theme_data/fruitsalad/less/main.less +++ b/esp/esp/themes/theme_data/fruitsalad/less/main.less @@ -297,12 +297,17 @@ width: 121px; #menu li .accent { position: absolute; left: -10px; - width: 20px; + transition: left 0.25s ease-out; + width: @rounding_radius + 18px; /* the below bezier hits -16.06px so this gives >1px bleed margin */ border-top-left-radius: @rounding_radius; border-bottom-left-radius: @rounding_radius; height: 100%; z-index: -1; } +#menu li:hover .accent { + left: -15px; + transition: left 0.2s cubic-bezier(0.6, 2.0, 0.8, 0.9); /* small bounce */ +} #menu a { display: block; diff --git a/esp/esp/themes/theme_data/fruitsalad/scripts/piano.js b/esp/esp/themes/theme_data/fruitsalad/scripts/piano.js new file mode 100644 index 0000000000..2664e1251c --- /dev/null +++ b/esp/esp/themes/theme_data/fruitsalad/scripts/piano.js @@ -0,0 +1,97 @@ +(function () { + // Set up AudioContext. If none, then quit. + var AC = window.AudioContext || + window.webkitAudioContext || + window.mozAudioContext || + window.oAudioContext || + window.msAudioContext; + if (!AC) { return; } + var ac = null; // create during event handler in order to start unmuted + // Library of scales + var scales = [ + [1, 9/8, 5/4, 4/3, 3/2, 5/3, 15/8], // Pythagorean-ish major scale + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11].map(function (x) { return Math.pow(2, x/12); }), // Chromatic + [1, 9/8, 5/4, 3/2, 5/3], // Pentatonic Major + [1, 9/8, 5/4, 3/2, 16/9], // Arpeggio + [1, 6/5, 3/2, 5/3, 15/8], // Another pretty arpeggio + ]; + var makeNote = function (buffer) { + // Make a playable (by calling) note out of a buffer + return function () { + var s = ac.createBufferSource(); + s.buffer = buffer; + s.connect(ac.destination); + if (s.noteOn) { s.noteOn(ac.currentTime); } + else { s.start(ac.currentTime); } + }; + }; + var registered_listeners = []; + var makeMusical = function (container) { + // Piano-keys-ize all of the nav tabs in the given container. + // First, find the set of keys + var items = container.querySelectorAll('li:not(.hidden)'); + // Choose a random scale + var scale = scales[Math.floor((scales.length - 0.00001) * Math.random())]; + // Render notes + var base_freq = 440; + var notes = Array.prototype.map.call(items, function (target, i) { + // Create a new sample + var buf = ac.createBuffer(1, ac.sampleRate * 4, ac.sampleRate); + var data = buf.getChannelData(0); + // Choose frequency, decay, and amplitude + var index = items.length - i - 1; + var rate = base_freq * scale[index % scale.length] * Math.pow(2, Math.floor(index/scale.length + 0.00001)) * 2 * Math.PI / ac.sampleRate; + var decay = Math.pow(0.5, 1/(0.2 * ac.sampleRate)); + var y = 0.1 * Math.pow(0.5, index / scale.length) + 0.1 * i / items.length; + var x = 0; + // Calculate waveform + var dx = Math.cos(rate) * decay; + var dy = Math.sin(rate) * decay; + for (var j = 0; j < data.length; j++) { + data[j] = x * dx - y * dy; + y = x * dy + y * dx; + x = data[j]; + data[j] *= Math.min(j / (0.01 * ac.sampleRate), (data.length - j) / data.length); + } + return makeNote(buf); + }); + // Remove any pre-existing scale + for (var i = 0; i < registered_listeners.length; ++i) { + registered_listeners[i][0].removeEventListener('mouseenter', registered_listeners[i][1]); + } + registered_listeners.length = 0; + // Attach notes to tabs + for (var i = 0; i < items.length; i++) { + items[i].addEventListener('mouseenter', notes[i]); + registered_listeners.push([items[i], notes[i]]); + } + return notes; + }; + var codeListener = (function () { + var last_keys = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + var i = 0; + var checkSequence = function (seq) { + var j = i; + while (seq.length > 0) { + j = (j + last_keys.length - 1) % last_keys.length; + if (seq.pop() != last_keys[j]) { return false; } + } + return true; + }; + return function (e) { + var keycode = e.type == "keydown" ? e.which : 13; + if (keycode == 13) { + if (checkSequence([88, 89, 90, 90, 89]) || // Colossal Cave Adventure + checkSequence([73, 68, 68, 81, 68]) || // Doom + checkSequence([38, 38, 40, 40, 37, 39, 37, 39, 66, 65]) // Konami + ) { + if (ac === null) { ac = new AC(); } + makeMusical(document.getElementById('menu')); + } + } + last_keys[i] = keycode; + i = (i + 1) % last_keys.length; + }; + })(); + document.addEventListener('keydown', codeListener); +})(); diff --git a/esp/esp/themes/theme_data/fruitsalad/templates/main.html b/esp/esp/themes/theme_data/fruitsalad/templates/main.html index c3fd32085a..7589af9b50 100644 --- a/esp/esp/themes/theme_data/fruitsalad/templates/main.html +++ b/esp/esp/themes/theme_data/fruitsalad/templates/main.html @@ -65,6 +65,7 @@ + {% endblock %} {% block body %}