Skip to content

Instantly share code, notes, and snippets.

@harunpehlivan
Created July 20, 2021 18:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save harunpehlivan/197519ffea5737b9855b977b21d24183 to your computer and use it in GitHub Desktop.
Save harunpehlivan/197519ffea5737b9855b977b21d24183 to your computer and use it in GitHub Desktop.
Musical Bubble Sort (CPC Bubble Sort)
<div id="app">
<div class="container">
<div class="main">
<div class="title">
<h1>Musical Bubble Sort</h1>
</div>
<div class="graph">
<transition-group name="list-complete" tag="div" class="graph__bars">
<item v-for="(num,i) in unsorted" :num="num" :music="music" :modearray="modeArray" :key="num" :class="arrayClass(i)" :ref="num" class="graph__bar"/>
</transition-group>
<div class="graph__randomize">
<button @click="randomize" :disabled="!controls"><i class="fas fa-redo-alt"></i></button>
</div>
</div>
<div class="bass">
<div class="bass__title">
Bass Pattern
</div>
<div class="bass__notes">
<div v-for="(note, i) in music.bassPattern" class="bass__box" :class="{'active' : note}" ref="bass">
<label :for="`box-${i}`">
<input type="checkbox" v-model="music.bassPattern[i]" :id="`box-${i}`">
</label>
</div>
</div>
<div class="bass__randomize">
<button @click="randomizeBassLoop"><i class="fas fa-redo-alt"></i></button>
</div>
</div>
<div class="begin">
<button @click="beginSort">
<span v-if="controls">begin</span>
<span v-else>stop & reset</span>
</button>
</div>
<div class="controls">
<transition name="pop-down">
<div class="toast" key="toast" v-show="toastActive" ref="toast">
{{toastMessage}}
</div>
</transition>
<div class="info-header">
CONTROLS
</div>
<div class="controls__data">
<div class="controls__key">
<div class="controls__key-title">
Key
</div>
<select name="" id="" v-model="music.key" :disabled="!controls">
<option value="C">C</option>
<option value="C#">C#</option>
<option value="D">D</option>
<option value="D#">D#</option>
<option value="E">E</option>
<option value="F">F</option>
<option value="F#">F#</option>
<option value="G">G</option>
<option value="G#">G#</option>
<option value="A">A</option>
<option value="A#">A#</option>
<option value="B">B</option>
</select>
<select name="" id="" v-model="music.mode" :disabled="!controls">
<option value="M">Major</option>
<option value="m">Minor</option>
<option value="L">Locrian (caution: spooky!)</option>
</select>
</div>
<div class="controls__tempo">
<div class="controls__tempo-title">
Tempo
</div>
<input type="range" id=tempo min="0" max="200" v-model.number="music.tempo" :disabled="!controls">
<label for="tempo">{{music.tempo}}</label>
</div>
<div class="controls__seed">
<div class="controls__seed-title">
Seed
</div>
<input type="text" id="seed" ref="seed" @keydown="handleSeedInput" v-model="seed" @click="selectSeedBox">
<div class="controls__seed-buttons">
<button @click="setSeed($refs.seed.value)">set</button>
<button @click="copySeed">copy</button>
<button @click="saveSeed">save</button>
</div>
</div>
</div>
<div class="controls__saved-seeds">
<div class="controls__saved-seeds-title">saved seeds (click to activate):</div>
<p v-for="seed in savedSeeds.seeds" @click="setSeed(seed)" class="controls__saved-seeds-seed">
{{seed}}
</p>
<button @click="clearSavedSeeds">clear all saved</button>
</div>
</div>
</div>
</div>
</div>

Musical Bubble Sort (CPC Bubble Sort)

Bubble sort - made musical! Each pitch is assigned a number which corresponds to its scale degree (ex in C Major a C would get 0 from numbers 0-7). Then, every time a swap occurs in the algorithm, the pitch on the right side plays! The animation is synced to the set tempo. Key, mode, tempo and bass pattern are all customizable thanks to the magic of Vue. You can even save/copy a particular pattern you like via seeds and come back later to listen again!

A Pen by HARUN PEHLİVAN on CodePen.

License.

//SEED GENERATOR: covert array to string, add together and then back to numbers
Vue.component('item', {
props: ['num', 'music', 'modearray'],
template: '<div>{{noteName}}</div>',
data() {
return {
filter: new Tone.Filter(800, 'lowpass'),
synth: null
}
},
computed: {
note() {
if(this.num === 7) {
return Tone.Frequency(this.music.key + '4');
}
return Tone.Frequency(this.music.key + '3').transpose(this.modearray[this.num]);
},
noteName() {
return this.note.toNote();
},
height() {
return `${30*(this.num+1)}px`;
},
color() {
let startColor = [25, 48, 115];
let endColor = [61, 121, 242];
let finalColor = [];
startColor.forEach((color, i)=>{
let increment = (endColor[i]-color)/8;
finalColor.push(color+(increment*this.num));
});
return `rgb(${finalColor[0]}, ${finalColor[1]}, ${finalColor[2]})`;
}
},
methods: {
triggerSynth(time) {
this.synth.triggerAttackRelease(this.note, '8n', time);
},
schedule(iteration) {
Tone.Transport.schedule(this.triggerSynth, iteration*Tone.Time('4n'));
}
},
beforeMount() {
this.synth = new Tone.Synth({
oscillator: {
type: 'square'
}
}).chain(this.filter, Tone.Master)
},
mounted() {
this.$el.style.height = this.height;
this.$el.style.backgroundColor = this.color;
this.synth.volume.value = -10;
}
})
let app = new Vue({
el: "#app",
data: {
unsorted: chance.unique(chance.integer, 8, {min: 0, max: 7}),
startArray: null,
animation: {
active: 0,
checking: 0,
anchor: 0,
iteration: 0,
finished: false,
animating: false,
delay: 500,
interval: null,
color: 'rgb(242,29,228)'
},
music: {
key: 'C',
mode: 'M',
tempo: 120,
bassPattern: [1,0,0,1,0,0,1,0,0,0,1,0,1,0,0,0],
bass: new Tone.Synth({
type: 'triangle'
}).toMaster(),
kick: new Tone.MembraneSynth().toMaster()
},
controls: true,
toastMessage: 'Seed copied to clipboard!',
toastActive: false,
savedSeeds: {
seeds: [],
selected: null
}
},
computed: {
modeArray() {
switch(this.music.mode) {
case 'M':
return [0,2,4,5,7,9,11];
break;
case 'm':
return [0,2,3,5,7,8,10];
break;
case 'L':
return [0,1,3,5,6,8,10];
break;
}
},
seedBassPattern() {
let temp = [];
for(let note of this.music.bassPattern) {
if(note) {
temp.push(1);
} else {
temp.push(0);
}
}
return temp;
},
seed: {
get: function() {
let seed = '';
for(let num of this.unsorted) {
seed += `${num}`;
}
let bassPattern = this.seedBassPattern.join('');
let stringTempo;
if(this.music.tempo <= 9) {
stringTempo = '00' + this.music.tempo;
} else if(this.music.tempo <= 99) {
stringTempo = '0' + this.music.tempo;
} else {
stringTempo = this.music.tempo;
}
seed += stringTempo;
seed += bassPattern;
seed += this.music.key;
seed += this.music.mode;
return seed;
},
set: function(newValue) {
}
}
},
watch: {
'music.key': function(newVal, oldVal) {
Tone.Transport.cancel();
this.presort();
},
'music.mode': function(newVal, oldVal) {
Tone.Transport.cancel();
this.presort();
},
'music.tempo': function(newVal, oldVal) {
Tone.Transport.bpm.value = newVal;
}
},
methods: {
triggerToast(type, message) {
if(this.toastActive != true) {
setTimeout(()=>{
this.toastActive = false;
}, 1000);
}
this.toastActive = true;
switch (type) {
case 'success':
this.$refs.toast.style.backgroundColor = '#3D79F2';
break;
case 'failure':
this.$refs.toast.style.backgroundColor = '#F21DE4';
break;
}
this.toastMessage = message;
},
selectSeedBox() {
this.$refs.seed.select();
},
saveSeed() {
let seed = this.$refs.seed.value;
let invalid = false;
for(let savedSeed of this.savedSeeds.seeds) {
if(seed === savedSeed) {
invalid = true;
}
}
if(invalid) {
this.triggerToast('failure', 'seed already in saved seeds!');
return;
}
this.savedSeeds.seeds.push(seed);
localStorage.setItem('seeds', JSON.stringify(this.savedSeeds.seeds));
},
clearSavedSeeds() {
this.savedSeeds.seeds = [];
localStorage.clear();
},
copySeed() {
this.$refs.seed.select();
document.execCommand('copy');
this.triggerToast('success', 'Seed copied to clipboard!');
},
setSeed(value) {
let invalid = false;
let data = value.split('');
let notes = data.splice(0,8);
let tempo = data.splice(0,3);
tempo = tempo.join('');
let bassPattern = data.splice(0,16);
let key = data[0];
let mode = data[1];
if(data[1] === '#') {
key += data[1];
mode = data[2];
}
notes.forEach((note, i)=>{
notes[i] = parseInt(note);
if(!(/[0-7]/.test(notes[i]))) {
invalid = true;
return;
}
});
bassPattern.forEach((note, i)=>{
bassPattern[i] = parseInt(note);
if(!(/[0-1]/.test(bassPattern[i]))) {
invalid = true;
return;
}
});
if(!(/[m,M,L]/.test(mode))) {
invalid = true;
}
if(invalid) {
this.triggerToast('failure', 'Invalid seed!');
return;
}
this.unsorted = notes;
this.music.key = key;
this.music.mode = mode;
this.music.tempo = tempo;
this.music.bassPattern = bassPattern;
this.presort();
},
handleSeedInput(e) {
// console.log(e);
if(e.code === "Enter") {
e.preventDefault();
this.setSeed(this.$refs.seed.value);
}
},
//musical methods
presort() {
Tone.Transport.cancel();
let arr = this.unsorted.slice(0);
let iteration = 0;
for(let i = arr.length; i>0; i--) {
for(let j=0; j<i-1; j++) {
if(arr[j] > arr[j+1]) {
//schedule musical event
this.$refs[arr[j+1]][0].schedule(iteration);
//swap
let temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
iteration++;
}
}
},
generateBassLoop(pattern, time) {
let {bass, key} = this.music;
for(let i=0; i<pattern.length; i++) {
if(pattern[i]) {
bass.triggerAttackRelease(`${key}2`, "8n", time + i*Tone.Time('16n'));
}
}
},
randomizeBassLoop() {
let bp = this.music.bassPattern;
for(let i=0; i<bp.length; i++) {
Vue.set(this.music.bassPattern, i, chance.integer({min: 0, max: 1}));
}
},
bassLoop() {
let {bass, kick, key} = this.music;
let animationCounter = 0;
let loop = new Tone.Loop((time)=>{
this.generateBassLoop(this.music.bassPattern, time);
kick.triggerAttackRelease(`${key}1`, "8n", time);
kick.triggerAttackRelease(`${key}1`, "8n", time + Tone.Time("4n"));
kick.triggerAttackRelease(`${key}1`, "8n", time + 2*Tone.Time("4n"));
kick.triggerAttackRelease(`${key}1`, "8n", time + 3*Tone.Time("4n"));
animationCounter = 0;
}, "1m");
let animationLoop = new Tone.Loop((time)=>{
animationCounter++;
if(this.music.bassPattern[animationCounter-1]) {
let activeBox = this.$refs.bass[animationCounter-1];
function flashEnd() {
TweenLite.to(activeBox, .1, {scale: 1});
}
TweenLite.to(activeBox, .05, {scale: 1.2, onComplete: flashEnd});
}
}, "16n");
loop.start(0).stop('8m');
animationLoop.start(0).stop('8m');
},
//styling methods
arrayClass(i) {
if(i>this.animation.anchor || this.animation.anchor == 0) {
return 'complete';
}
switch(i) {
case this.animation.active:
return 'active';
break;
case this.animation.checking:
return 'checking';
break;
}
},
//bubble sort methods
randomize() {
Tone.Transport.cancel();
this.unsorted = chance.unique(chance.integer, 8, {min: 0, max: 7});
this.presort();
},
firstPass() {
this.animation.checking = this.animation.active+1;
this.animation.anchor = (this.unsorted.length-1) - this.animation.iteration;
},
nextPass() {
this.animation.active = 0;
this.animation.checking = 1;
this.animation.iteration++;
this.animation.anchor--;
if(this.animation.anchor === 0) {
this.animation.finished = true;
clearInterval(this.animation.interval);
}
},
bubbleStep() {
if(!this.animation.finished) {
let arr = this.unsorted;
let {active, checking, anchor} = this.animation;
if(arr[active] > arr[checking]) {
//animation
let activeNote = this.$refs[arr[checking]][0].$el;
let originalColor = this.$refs[checking][0].color;
function flashFinish() {
TweenLite.to(activeNote, .2, {backgroundColor: originalColor, zIndex: 0, delay: .2});
}
TweenLite.to(activeNote, .05, {backgroundColor: this.animation.color, zIndex: 100, onComplete: flashFinish});
//swap
let temp = arr[checking];
arr[checking] = arr[active];
arr[active] = temp;
}
this.animation.active++;
this.animation.checking++;
if(this.animation.checking === anchor+1) {
this.nextPass();
}
}
},
beginSort() {
if(this.controls) {
this.animation.finished = false;
this.startArray = this.unsorted.slice(0);
this.controls = false;
this.bassLoop();
let loop = new Tone.Loop((time)=>{
Tone.Draw.schedule(()=>{
this.bubbleStep();
}, time);
}, "4n");
loop.start(0).stop('8m');
Tone.Transport.toggle();
} else {
this.resetSort();
}
},
resetSort() {
Tone.Transport.toggle();
Tone.Transport.cancel();
this.unsorted = this.startArray;
//reset animation back to original
this.animation.active = 0;
this.animation.checking = 1;
this.animation.anchor = this.unsorted.length-1;
this.animation.iteration = 0;
this.presort();
this.controls = true;
}
},
beforeMount() {
let seeds = JSON.parse(localStorage.getItem('seeds'));
if(seeds) {
this.savedSeeds.seeds = seeds;
}
},
mounted() {
this.firstPass();
this.presort();
this.music.kick.volume.value = -9;
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/2.1.2/TweenMax.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/tone/13.8.9/Tone.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/chance/1.0.18/chance.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.js"></script>
//vars
$color-pink: #F21DE4;
$color-pink-light: #DC56DB;
$color-blue: #3D79F2;
$color-blue-dark: #193073;
$color-cyan: #79F2F2;
//resets
html, body {
margin: 0;
padding: 0;
box-sizing: border-box;
}
h1 {
margin: 0;
padding: 0;
}
button {
margin: 0;
padding: 0;
border: none;
background-color: transparent;
color: white;
cursor: pointer;
}
i.fa-redo-alt {
font-size: 1.5rem;
margin-left: 1rem;
}
//styling
.toast {
position: absolute;
top: 0;
right: 0;
width: 200px;
height: 50px;
background-color: green;
color: white;
display: flex;
justify-content: center;
align-items: center;
text-align:center;
border-radius: 5px;
}
.container {
display: flex;
flex-wrap: wrap;
justify-content: center;
min-height: 100vh;
align-content: flex-start;
background: $color-blue-dark;
color: white;
}
.main {
margin-top: 2rem;
margin-bottom: 2rem;
display: flex;
flex-wrap: wrap;
font-family: 'IBM Plex Sans', sans-serif;
background: #22293c;
box-shadow: 8px 8px 10px 2px rgba(0,0,0,.3);
border-radius: 5px;
width: 700px;
padding: 2rem;
position: relative;
}
.title {
flex: 0 1 100%;
display: flex;
align-items: center;
justify-content: center;
margin-top: 1rem;
margin-bottom: 3rem;
font-size: 1.5rem;
text-transform: uppercase;
}
.graph {
flex: 0 1 100%;
display: flex;
justify-content: center;
align-items: flex-end;
&__bars {
display: flex;
align-items: flex-end;
}
&__bar {
background-color: $color-blue-dark;
color: white;
height: 20px;
width: 50px;
transition: transform .5s, opacity .1s;
margin-right: 10px;
display: flex;
justify-content: center;
align-items: flex-end;
opacity: .5;
position: relative;
&.active {
opacity: 1;
}
&.checking {
opacity: 1;
}
&.complete {
background-color: green !important;
opacity: 1;
}
}
&__randomize {
color: red;
& button {
transition: opacity .1s;
&:disabled {
opacity: 0.5;
cursor: initial;
}
}
}
}
.bass {
flex: 0 1 100%;
display: flex;
flex-wrap: wrap;
justify-content: center;
margin-top: 2rem;
&__title {
font-size: 1.3rem;
flex: 0 1 100%;
font-weight: bold;
text-align: center;
margin-bottom: 1rem;
}
&__notes {
display: flex;
flex: 0;
}
&__box {
width: 20px;
margin-left: 15px;
height: 20px;
background-color: $color-pink;
opacity: .2;
cursor: pointer;
position: relative;
&.active {
opacity: .7;
}
& label {
width: 100%;
height: 100%;
position: absolute;
cursor: pointer;
}
& input {
display: none;
}
}
&__randomize {
margin-left: 10px;
}
}
.begin {
flex: 0 1 100%;
text-align: center;
margin-top: 2rem;
text-transform: uppercase;
& button {
background-color: $color-blue-dark;
padding: 1rem 3rem;
border-radius: 2px;
font-weight: bold;
font-size: 1.5rem;
cursor: pointer;
box-shadow: 2px 2px 5px rgba(0,0,0,0.2);
}
}
.info-header {
flex: 0 1 100%;
margin-top: 1.5rem;
margin-bottom: 1rem;
text-align: center;
font-weight: bold;
font-size: 1.5rem;
}
.controls {
flex: 0 1 100%;
display: flex;
flex-wrap: wrap;
justify-content: center;
margin-top: 2rem;
background: lighten(#22293c, 10%);
padding: 0 3.2rem;
padding-right: 2rem;
padding-bottom: 2rem;
border-radius: 5px;
position: relative;
&__key {
&-title {
text-transform: uppercase;
font-weight: bold;
margin-bottom: 5px;
text-decoration: underline;
}
}
&__tempo {
margin-top: 10px;
display: flex;
flex-wrap: wrap;
&-title {
flex: 0 1 100%;
text-transform: uppercase;
font-weight: bold;
margin-bottom: 5px;
text-decoration: underline;
}
& label {
margin-left: 8px;
}
}
&__seed {
&-title {
flex: 0 1 100%;
text-transform: uppercase;
font-weight: bold;
margin-bottom: 10px;
text-decoration: underline;
}
&-buttons {
display: flex;
margin-top: 10px;
& button {
background-color: $color-blue-dark;
text-transform: uppercase;
font-size: .9rem;
padding: 5px 10px;
margin-right: 10px;
border-radius: 2px;
box-shadow: 2px 2px 2px rgba(0,0,0,0.3);
}
}
}
&__data {
flex: 0 1 50%;
}
&__saved-seeds {
flex: 0 1 50%;
&-title {
flex: 0 1 100%;
text-transform: uppercase;
font-weight: bold;
margin-bottom: 10px;
text-decoration: underline;
}
&-seed {
cursor: pointer;
}
& button {
background-color: $color-blue-dark;
text-transform: uppercase;
font-size: .9rem;
padding: 5px 10px;
margin-right: 10px;
border-radius: 2px;
box-shadow: 2px 2px 2px rgba(0,0,0,0.3);
}
}
}
//debug
//vue transitions
.pop-down-enter-active, .pop-down-leave-active {
transition: all .3s;
}
.pop-down-enter, .pop-down-leave-to /* .fade-leave-active below version 2.1.8 */ {
opacity: 0;
transform: translateY(-10px);
}
.list-complete-enter, .list-complete-leave-to
/* .list-complete-leave-active below version 2.1.8 */ {
opacity: 0;
transform: translateY(30px);
}
.list-complete-move {
}
.list-complete-leave-active {
position: absolute;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment