Each component of the sandwich (meat, bun, cheese) corresponds to an instrument in the beat. Swap the components around and see what you can make!
A Pen by HARUN PEHLİVAN on CodePen.
Each component of the sandwich (meat, bun, cheese) corresponds to an instrument in the beat. Swap the components around and see what you can make!
A Pen by HARUN PEHLİVAN on CodePen.
<div id="app"> | |
<div class="container"> | |
<h1>BEAT BURGER</h1> | |
<h2>{{currentBeat}}</h2> | |
<!-- Sandwich Components --> | |
<div class="component__container"> | |
<sandwich-component | |
v-for="component in components" | |
:key="component.name" | |
:synth="component.synth" | |
:type="component.type" | |
:muted="component.muted" | |
v-bind="component.variants[component.currentVariant]"> | |
</sandwich-component> | |
<sandwich-component :type="components.bun.type" | |
:synth="components.bun.synth" | |
:muted="components.bun.muted" | |
v-bind="components.bun.variants[components.bun.currentVariant]"> | |
</sandwich-component> | |
</div> | |
<!-- Controls --> | |
<div class="controls"> | |
<div class="control" v-for="(component, name) in components"> | |
<h3>{{name}}</h3> | |
<p>({{component.instrument}})</p> | |
<div class="control__mute" @click="muteComponent(name)"> | |
<ion-icon :name="component.muted ? 'volume-off' : 'volume-high'"></ion-icon> | |
</div> | |
<select :size="component.variants.length" v-model="component.currentVariant"> | |
<option v-for="(variant, i) in component.variants" | |
:value="i">{{variant.name}}</option> | |
</select> | |
</div> | |
</div> | |
<!-- Play/Pause --> | |
<div class="toggle"> | |
<ion-icon :name="playing ? 'pause' : 'play'" @click="toggle"></ion-icon> | |
</div> | |
</div> | |
</div> |
Vue.component('sandwich-component', { | |
props: { | |
synth: Object, | |
name: String, | |
muted: Boolean, | |
color: String, | |
beat: Object, | |
type: String | |
}, | |
data: () => { | |
return { | |
loop: null, | |
buffer: false, | |
animating: false | |
} | |
}, | |
computed: { | |
style() { | |
let color = this.color; | |
return { | |
backgroundColor: color | |
} | |
}, | |
classList() { | |
return [ | |
//static classes | |
'component', | |
//dynamic classes | |
this.buffer ? 'queued' : '', | |
this.muted ? 'queued' : '', | |
`component__${this.type}`, | |
this.animating ? 'animated pulse' : '' | |
] | |
} | |
}, | |
watch: { | |
name() { | |
this.buffer = true; | |
} | |
}, | |
methods: { | |
setLoop() { | |
this.loop = new Tone.Loop((time) => { | |
if(this.muted) { | |
return; | |
} | |
this.buffer = false; | |
for(let i=0; i<this.beat.data.length; i++) { | |
let currentBeat = this.beat.data[i]; | |
if(currentBeat && this.synth) { | |
if(this.synth instanceof Tone.NoiseSynth || this.synth instanceof Tone.MetalSynth) { | |
this.synth.triggerAttackRelease('8n', time + (Tone.Time('16n') * i)); | |
} else if(this.synth instanceof Tone.Synth) { | |
let notes = ['f2', 'ab2', 'c3', 'f3']; | |
let pickedNoteIndex = Math.floor(Math.random()*notes.length); | |
this.synth.triggerAttackRelease(notes[pickedNoteIndex], '16n', time + (Tone.Time('16n') * i)); | |
} | |
else { | |
this.synth.triggerAttackRelease('f1', '8n', time + (Tone.Time('16n') * i)); | |
} | |
//animation | |
Tone.Draw.schedule(()=>{ | |
this.animating = true; | |
setTimeout(()=>{ | |
this.animating = false; | |
}, 100); | |
}, time + (Tone.Time('16n') * i)); | |
} | |
} | |
}, '1m'); | |
this.loop.start(0); | |
} | |
}, | |
mounted() { | |
this.setLoop(); | |
}, | |
template: ` | |
<div :key="name" :style="style" :class="classList"> | |
</div> | |
` | |
}); | |
let app = new Vue({ | |
el: '#app', | |
data: { | |
//state data | |
playing: false, | |
currentLoops: {}, | |
currentBeat: 0, | |
//Component data | |
components: { | |
bun: { | |
type: 'bun', | |
currentVariant: 0, | |
synth: new Tone.MembraneSynth().toMaster(), | |
instrument: 'bass drum', | |
muted: false, | |
variants: [ | |
{ | |
name: 'white', | |
color: '#eec07b', | |
beat: { | |
name: '4 on the Floor', | |
data: [1,0,0,0, | |
1,0,0,0, | |
1,0,0,0, | |
1,0,0,0] | |
} | |
}, | |
{ | |
name: 'wholewheat', | |
color: '#6d3200', | |
beat: { | |
name: 'Syncopation Nation', | |
data: [1,0,0,1, | |
0,0,1,0, | |
0,1,0,0, | |
1,0,1,0] | |
} | |
}, | |
{ | |
name: 'rye', | |
color: '#ae7646', | |
beat: { | |
name: 'Halftime Vibes', | |
data: [1,0,0,0, | |
0,0,0,0, | |
0,0,1,0, | |
0,1,0,0] | |
} | |
}, | |
{ | |
name: 'pumpernickel', | |
color: '#624f40', | |
beat: { | |
name: 'Sick Metal', | |
data: [1,1,1,1, | |
1,0,0,0, | |
0,1,0,1, | |
0,1,0,1] | |
} | |
} | |
] | |
}, | |
condiment: { | |
type: 'condiment', | |
currentVariant: 0, | |
synth: new Tone.Synth({ | |
oscillator: { | |
type: 'fatsawtooth' | |
}, | |
envelope: { | |
release: .5 | |
} | |
}).toMaster(), | |
muted: false, | |
instrument: 'bass', | |
variants: [ | |
{ | |
name: 'lettuce', | |
color: '#78bf52', | |
beat: { | |
name: 'Straight Down the Middle', | |
data: [1,0,0,0, | |
1,0,0,0, | |
1,0,0,0, | |
1,0,0,0] | |
} | |
}, | |
{ | |
name: 'tomato', | |
color: 'red', | |
beat: { | |
name: 'Syncopation Nation', | |
data: [1,0,0,1, | |
0,0,1,0, | |
0,1,0,0, | |
1,0,1,0] | |
} | |
}, | |
{ | |
name: 'ketchup', | |
color: '#ce2522', | |
beat: { | |
name: 'Offbeat 8ths', | |
data: [0,0,1,0, | |
0,0,1,0, | |
0,0,1,0, | |
0,0,1,0] | |
} | |
}, | |
{ | |
name: 'pineapple', | |
color: '#fee12d', | |
beat: { | |
name: 'Offbeat 16th', | |
data: [1,1,0,1, | |
0,1,0,1, | |
0,1,0,1, | |
0,1,0,1] | |
} | |
} | |
] | |
}, | |
cheese: { | |
type: 'cheese', | |
currentVariant: 0, | |
synth: new Tone.NoiseSynth.presets.Hats(), | |
muted: false, | |
instrument: 'hi hats', | |
variants: [ | |
{ | |
name: 'cheddar', | |
color: '#fd941f', | |
beat: { | |
name: 'Straight Down the Middle', | |
data: [1,0,0,0, | |
1,0,0,0, | |
1,0,0,0, | |
1,0,0,0] | |
} | |
}, | |
{ | |
name: 'mozzerella', | |
color: '#FCF3D9', | |
beat: { | |
name: 'Offbeat Oddity', | |
data: [0,1,1,0, | |
0,1,1,0, | |
0,1,1,0, | |
0,1,1,0] | |
} | |
}, | |
{ | |
name: 'brie', | |
color: '#e3dab2', | |
beat: { | |
name: 'Offbeat 8ths', | |
data: [0,0,1,0, | |
0,0,1,0, | |
0,0,1,0, | |
0,0,1,0] | |
} | |
}, | |
{ | |
name: 'blue', | |
color: 'blue', | |
beat: { | |
name: '16ths Forever', | |
data: [1,1,1,1, | |
1,1,1,1, | |
1,1,1,1, | |
1,1,1,1] | |
} | |
} | |
] | |
}, | |
meat: { | |
type: 'meat', | |
currentVariant: 0, | |
synth: new Tone.NoiseSynth.presets.Snare(), | |
muted: false, | |
instrument: 'snare', | |
variants: [ | |
{ | |
name: 'beef', | |
color: '#542d2d', | |
beat: { | |
name: 'Twos and Fours', | |
data: [0,0,0,0, | |
1,0,0,0, | |
0,0,0,0, | |
1,0,0,0] | |
} | |
}, | |
{ | |
name: 'turkey', | |
color: '#cab5b2', | |
beat: { | |
name: 'The Hot Beat', | |
data: [0,0,0,1, | |
0,0,1,0, | |
0,0,0,1, | |
0,0,1,0] | |
} | |
}, | |
{ | |
name: 'veggie', | |
color: 'green', | |
beat: { | |
name: 'Halftime Vibes', | |
data: [0,0,0,0, | |
0,0,0,0, | |
1,0,0,0, | |
0,0,0,0] | |
} | |
}, | |
{ | |
name: 'salmon', | |
color: 'salmon', | |
beat: { | |
name: 'Hot Beat Part II', | |
data: [0,0,1,1, | |
0,0,1,0, | |
0,0,1,1, | |
0,0,1,0] | |
} | |
} | |
] | |
}, | |
} | |
}, | |
methods: { | |
regenLoops() { | |
for(let component in this.components) { | |
let current = this.components[component]; | |
this.currentLoops[component] = current.variants[current.currentVariant].beat.data; | |
} | |
}, | |
muteComponent(component) { | |
this.components[component].muted = !this.components[component].muted; | |
}, | |
toggle() { | |
this.currentBeat = 0; | |
this.playing = !this.playing; | |
Tone.Transport.toggle(); | |
} | |
}, | |
mounted() { | |
let beatCountLoop = new Tone.Loop((time)=>{ | |
Tone.Draw.schedule(()=>{ | |
this.currentBeat++; | |
if(this.currentBeat>4) { | |
this.currentBeat = 1; | |
} | |
}, time); | |
}, '4n'); | |
beatCountLoop.start(0); | |
} | |
}) |
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/tone/13.8.12/Tone.min.js"></script> | |
<script src="https://codepen.io/sparlos/pen/eaqNOK.js"></script> |
$font-heading: 'Lobster', cursive; | |
html, body { | |
margin: 0; | |
padding: 0; | |
box-sizing: border-box; | |
color: white; | |
} | |
h1 { | |
font-family: $font-heading; | |
font-size: 60px; | |
flex: 0 1 100%; | |
text-align: center; | |
margin: 0; | |
margin-top: 100px; | |
} | |
h2 { | |
font-family: $font-heading; | |
font-size: 40px; | |
flex: 0 1 100%; | |
text-align: center; | |
margin: 20px 0; | |
} | |
h3 { | |
margin: 0; | |
padding: 0; | |
font-family: $font-heading; | |
text-transform: uppercase; | |
font-size: 25px; | |
} | |
.toggle { | |
font-size: 65px; | |
cursor: pointer; | |
margin-top: 30px; | |
} | |
.queued { | |
opacity: .2; | |
} | |
.container { | |
width: 100vw; | |
min-height: 100vh; | |
background: linear-gradient(to bottom, #FA8072, #B22222); | |
display: flex; | |
justify-content: center; | |
align-content: flex-start; | |
flex-wrap: wrap; | |
} | |
.component { | |
$base-height: 10px; | |
border-radius: 5px; | |
flex: 0 1 100%; | |
animation-duration: .1s; | |
&__container { | |
display: flex; | |
align-content: center; | |
width: 400px; | |
flex-wrap: wrap; | |
} | |
&__bun { | |
height: $base-height*5; | |
} | |
&__meat { | |
height: $base-height*3; | |
} | |
&__cheese { | |
height: $base-height*1.5; | |
} | |
&__condiment { | |
height: $base-height; | |
} | |
} | |
.controls { | |
flex: 0 1 100%; | |
display: flex; | |
justify-content: center; | |
margin: 30px 0; | |
} | |
.control { | |
display: flex; | |
flex-wrap: wrap; | |
margin-top: 25px; | |
& * { | |
flex: 0 1 100%; | |
text-align: center; | |
} | |
&__mute { | |
font-size: 30px; | |
cursor: pointer; | |
margin: 5px 0 10px 0; | |
} | |
} | |
//vue transitions | |
.fade-leave-active { | |
transition: opacity .15s; | |
} | |
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ { | |
opacity: .5; | |
} |
<link href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/3.7.2/animate.min.css" rel="stylesheet" /> |