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) |