A fun little beat machine
A Pen by Landon Schropp on CodePen.
h1 Percussion | |
section.switches.loading | |
.start.hidden | |
button type="button" Start | |
- keys = [ "a", "s", "d", "f", "j", "k", "l", ";" ] | |
- %w( highHat crash bell rim snare tom1 tom2 kick ).each_with_index do |instrument, i| | |
.instrument data-instrument=instrument | |
- 16.times do |tick| | |
button.tick type="checkbox" data-tick=tick = keys[i] | |
p Built by <a href="http://twitter.com/LandonSchropp" target="_blank">Landon Schropp</a> as part of the <a href="http://codepen.io/collection/fDxJj/" target="_blank">Randoms Collection</a> |
A fun little beat machine
A Pen by Landon Schropp on CodePen.
# Enable FastClick | |
$ -> FastClick.attach(document.body) | |
# Represets a sound. | |
class Sound | |
# Constructs the sound. | |
constructor: (@_name) -> @howlerSound() | |
# Returns a promise that's resolved when the sound loads. | |
loadingPromise: -> @_loadingPromise ?= new $.Deferred() | |
# Returns the URL for the Sound with the provided name. | |
url: -> "https://s3-us-west-2.amazonaws.com/s.cdpn.io/49705/#{ @_name }.mp3" | |
# Plays the Sound. | |
play: -> @howlerSound().play() | |
# Returns the Howler object for this Sound. | |
howlerSound: -> | |
@_howlerSound ?= new Howl({ | |
urls: [ @url() ] | |
onload: => @loadingPromise().resolve() | |
onloaderror: => @loadingPromise().reject() | |
}) | |
class SoundBoard | |
@SOUND_NAMES = [ "highHat", "crash", "bell", "rim", "snare", "tom1", "tom2", "kick", "metronome" ] | |
# Constructs this SoundBoard. | |
constructor: -> @sounds() | |
# An object containing the sounds in this SoundBobard. | |
sounds: -> | |
return @_sounds if @_sounds? | |
@_sounds = {} | |
SoundBoard.SOUND_NAMES.forEach (soundName) => @_sounds[soundName] = new Sound(soundName) | |
# Returns a sound in this SoundBoard. | |
sound: (soundName) -> @_sounds[soundName] | |
# Plays the sound with the provided name. | |
play: (soundName) -> @sound(soundName).play() | |
# Returns a promise that resolves when all of the sounds have loaded. | |
loadingPromise: -> | |
@_loadingPromise ?= $.when.apply($, | |
@_soundsArray().map (sound) => sound.loadingPromise() | |
) | |
# An array of the sound objects in this SoundBoard. | |
_soundsArray: -> Object.keys(@sounds()).map (soundName) => @sound(soundName) | |
class BeatMachine | |
# Constructs the BeatMachine. | |
constructor: (@_beatsPerMinute, @_beatsPerMeasure, @_ticksPerBeat) -> | |
@_soundBoard = new SoundBoard() | |
# Returns the sound board for this BeatMachine. | |
soundBoard: -> @_soundBoard | |
# Returns a promise that resolves when the BeatMachine is loaded. | |
loadingPromise: -> @_soundBoard.loadingPromise() | |
# Called every time the BeatMachine ticks. | |
play: (tick) -> | |
SoundBoard.SOUND_NAMES.forEach (soundName) => | |
@_soundBoard.play(soundName) if @switches()[soundName][tick] | |
# Toggles the sound for the provided tick. Returns true if the sound was toggled on and false if it was toggled off. | |
toggle: (soundName, tick) -> @switches()[soundName][tick] = not @switches()[soundName][tick] | |
# Returns the number of ticks per measure | |
ticksPerMeasure: -> @_beatsPerMeasure * @_ticksPerBeat | |
# Returns the number of ticks per minute. | |
ticksPerMinute: -> @_beatsPerMinute * @_ticksPerBeat | |
switches: -> | |
return @_switches if @_switches? | |
@_switches = {} | |
SoundBoard.SOUND_NAMES.forEach (soundName) => | |
@_switches[soundName] = [0...(@ticksPerMeasure())].map -> false | |
[0...@_beatsPerMeasure].forEach (beat) => @toggle("metronome", beat * @_ticksPerBeat) | |
@_switches | |
class BeatMachineView | |
# The keyboard keys. | |
@KEYS: [ "a", "s", "d", "f", "j", "k", "l", ";" ] | |
# Constructs the BeatMachineView. | |
constructor: (@_$element, @_beatMachine) -> | |
@_$element.find(".tick").click (event) => @toggle($(event.target)) | |
$("body").keypress (event) => @_keyPressed(String.fromCharCode(event.which)) | |
$(".start").click => @start() | |
@_beatMachine.loadingPromise().then => | |
@_$element.removeClass("loading") | |
if $("html").hasClass("touch") then $(".start").removeClass("hidden") else @start() | |
# Starts the BeatMachineView. | |
start: -> | |
@_subtick = -1 | |
@_tick = -1 | |
setInterval((=> @_subticked()), 60000 / @_beatMachine.ticksPerMinute() / 2) | |
# hide the controls if they're not already hidden | |
$(".start").addClass("hidden") | |
# Play a sound when started so sound is enabled on mobile browsers | |
@_beatMachine.soundBoard().play("metronome") if $("html").hasClass("touch") | |
# Called every time a subtick occurred. This is necessary to allow keyboard input to fire before the sound has played. | |
_subticked: -> | |
@_subtick = (@_subtick + 1) % 2 | |
@_ticked() if @_subtick is 0 | |
# Called every time the sound ticks. | |
_ticked: -> | |
@_tick = (@_tick + 1) % @_beatMachine.ticksPerMeasure() | |
@_beatMachine.play(@_tick) | |
$(".tick").removeClass("current") | |
$("[data-tick='#{ @_tick }']").addClass("current") | |
# Fired whenever a key is pressed. | |
_keyPressed: (key) -> | |
instrument = SoundBoard.SOUND_NAMES[BeatMachineView.KEYS.indexOf(key)] | |
return unless instrument? | |
tick = (@_tick + @_subtick;) % @_beatMachine.ticksPerMeasure() | |
@toggle(@$tick(instrument, tick)) | |
# Retrieves the element for the provided instrument and tick. | |
$tick: (instrument, tick) -> | |
@_$element.find("[data-instrument='#{ instrument }']").find("[data-tick='#{ tick }']") | |
# Toggles the provided tick. | |
toggle: ($tick) -> | |
instrument = $tick.parent().data("instrument") | |
tick = $tick.data("tick") | |
active = @_beatMachine.toggle(instrument, tick) | |
$tick.toggleClass("active", active) | |
@_beatMachine.soundBoard().play(instrument) if active and @_subtick is 0 | |
# Kick things off. | |
beatMachine = new BeatMachine(120, 4, 4) | |
$switches = $(".switches") | |
beatMachineView = new BeatMachineView($switches, beatMachine) | |
# Set the BeatMachine's initial values | |
switches = { | |
"highHat": [ false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false ], | |
"crash": [ false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false ], | |
"bell": [ false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false ], | |
"rim": [ true, false, false, false, true, true, true, false, false, true, false, false, true, false, false, false ], | |
"snare": [ true, false, false, false, false, false, true, false, false, true, false, false, true, false, false, false ], | |
"tom1": [ true, false, false, false, true, false, false, false, true, false, false, false, true, false, false, false ], | |
"tom2": [ true, false, true, false, false, false, true, false, false, false, true, false, false, true, false, false ], | |
"kick": [ true, false, false, false, true, false, false, false, true, false, false, false, true, false, false, false ] | |
} | |
Object.keys(switches).forEach (instrument) => | |
switches[instrument].forEach (enabled, tick) => | |
beatMachineView.toggle(beatMachineView.$tick(instrument, tick)) if enabled |
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script> | |
<script src="//cdnjs.cloudflare.com/ajax/libs/howler/1.1.17/howler.min.js"></script> | |
<script src="//cdnjs.cloudflare.com/ajax/libs/fastclick/1.0.0/fastclick.js"></script> |
$purple: #B36FFE | |
html, body | |
height: 100% | |
min-height: 50vw | |
margin: 0 | |
padding: 0 | |
font-size: 16px | |
overflow: hidden | |
font-family: 'Open Sans', sans-serif | |
-webkit-text-size-adjust: none | |
@media (min-width: 640px) | |
font-size: 18px | |
* | |
box-sizing: border-box | |
outline: none | |
body | |
display: flex | |
flex-direction: column | |
align-items: center | |
justify-content: center | |
background-color: #333 | |
h1, p, a | |
color: #888 | |
margin: 1rem 0 | |
text-align: center | |
line-height: 1.25rem | |
button | |
cursor: pointer | |
h1 | |
font-size: 1.75rem | |
font-weight: 800 | |
.switches | |
width: 95vw | |
height: 47.5vw | |
max-width: 960px | |
max-height: 480px | |
position: relative | |
display: flex | |
flex-direction: column | |
&::before, &::after | |
position: absolute | |
content: "" | |
top: 0 | |
right: 0 | |
bottom: 0 | |
left: 0 | |
opacity: 0 | |
transition: opacity 0.15s linear | |
pointer-events: none | |
&::before | |
background-color: transparentize(#333, 0.1) | |
&::after | |
background-image: url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/49705/spinner_purple.svg) | |
background-repeat: no-repeat | |
background-position: 50% | |
background-size: 4% | |
animation: rotate 0.75s infinite linear | |
&.loading::before, &.loading::after | |
opacity: 1 | |
pointer-events: auto | |
.start | |
position: absolute | |
top: 0 | |
right: 0 | |
bottom: 0 | |
left: 0 | |
display: flex | |
align-items: center | |
justify-content: center | |
transition: opacity 0.15s linear | |
opacity: 1 | |
background-color: transparentize(#333, 0.1) | |
&.hidden | |
opacity: 0 | |
pointer-events: none | |
button | |
background-color: $purple | |
border-width: 0 | |
border-radius: 0.25rem | |
color: lighten($purple, 20%) | |
font-size: 2.5vw | |
padding: 1vw 3vw | |
.instrument | |
display: flex | |
flex: 1 | |
.tick | |
flex: 1 | |
padding: 0 | |
margin: 0 | |
font-size: 3vw | |
color: transparent | |
border-width: 0 | |
margin: 1px | |
background-color: #444 | |
&.current | |
background-color: #555 | |
color: #777 | |
&.active | |
background-color: $purple | |
box-shadow: inset 1px 1px darken($purple, 5%), inset -1px -1px darken($purple, 5%) | |
&.current.active | |
color: lighten($purple, 10%) | |
// HACK: For some reason, iOS doens't like the hit aniamtion. This ensures it only occurs on desktop browsers. | |
.no-touch &.current.active | |
animation: hit 0.25s | |
@keyframes hit | |
0% | |
transform: scale3d(1, 1, 1) | |
5% | |
transform: scale3d(1.2, 1.2, 1) | |
100% | |
transform: scale3d(1, 1, 1) | |
@keyframes rotate | |
0% | |
transform: rotateZ(0deg) | |
100% | |
transform: rotateZ(360deg) |