Skip to content

Instantly share code, notes, and snippets.

Created April 12, 2017 17:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save anonymous/e84128b623e33998e88cc801f8e5ea3a to your computer and use it in GitHub Desktop.
Save anonymous/e84128b623e33998e88cc801f8e5ea3a to your computer and use it in GitHub Desktop.
Percussion
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>
# 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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment