Repository: makaroni4/guitar_bro
Branch: master
Commit: 334ea0450a5a
Files: 14
Total size: 46.7 KB
Directory structure:
gitextract_95i3534h/
├── LICENSE
├── README.md
├── css/
│ └── app.css
├── index.html
└── js/
├── app.js
├── audio_processor.js
├── audio_wave.js
├── config.js
├── explosion_effect.js
├── fretboard.js
├── health_drawer.js
├── helper_functions.js
├── sharing.js
└── song_loader.js
================================================
FILE CONTENTS
================================================
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2017 Anatoli Makarevich
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# Guitar Bro – browser game that helps you learn notes on guitar

## Description
Guitar Bro works completely in browser and is based on [Web Audio API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API). Currently Guitar Bro works only in Chrome, since only Chrome allows to change the [resolution of FFT](https://developer.mozilla.org/en-US/docs/Web/API/AnalyserNode/fftSize) to distinguish different notes on guitar.
[Try it out!](https://makaroni4.github.io/guitar_bro/)
================================================
FILE: css/app.css
================================================
html, body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}
html {
box-sizing: border-box;
}
*, *:before, *:after {
box-sizing: inherit;
}
.allow-mic {
display: none;
background-color: transparent;
}
.allow-mic--active {
display: block;
}
.allow-mic__arrow {
position: absolute;
left: 408px;
top: 60px;
width: 60px;
height: 72px;
background-image: url("../img/allow_mic_arrow.png");
background-size: cover;
}
.allow-mic__message {
position: absolute;
width: 250px;
left: 330px;
top: 140px;
font-family: 'Kalam', cursive;
font-size: 24px;
color: #F1FAEE;
}
.header {
display: flex;
align-items: center;
height: 60px;
}
.header__audio-wave {
position: absolute;
top: 2px;
left: 10px;
}
.header__score {
display: none;
position: absolute;
top: 20px;
right: 40px;
font-size: 32px;
line-height: 48px;
font-weight: bold;
color: #A8DADC;
}
.real-guitar-hero {
background-color: #1D3557;
}
.real-guitar-hero__header {
position: absolute;
}
.real-guitar-hero__canvas {
}
.real-guitar-hero__settings {
display: block;
position: absolute;
top: 10px;
right: 10px;
font-family: 'Source Sans Pro', sans-serif;
font-weight: bold;
color: #F1FAEE;
text-decoration: none;
}
.game-settings {
width: 100%;
}
.game-settings__bpm-input-label {
display: block;
font-family: 'Source Sans Pro', sans-serif;
font-weight: bold;
color: #1D3557;
text-decoration: none;
}
.game-settings__bpm-input {
display: block;
padding: 8px;
width: 100%;
font-size: 24px;
line-height: 32px;
}
.game-settings__song-select-label,
.game-settings__string-select-label,
.game-settings__mode-select-label {
display: block;
margin-top: 16px;
font-family: 'Source Sans Pro', sans-serif;
font-weight: bold;
color: #1D3557;
text-decoration: none;
}
.game-settings__song-select,
.game-settings__string-select {
display: block;
width: 100%;
height: 52px;
background-color: #FFF;
font-family: 'Source Sans Pro', sans-serif;
color: #1D3557;
text-decoration: none;
font-size: 18px;
line-height: 24px;
}
.game-settings__mode-select {
font-family: 'Source Sans Pro', sans-serif;
color: #1D3557;
text-decoration: none;
font-size: 18px;
line-height: 24px;
}
.game-settings__mode-select-hint {
font-size: 14px;
line-height: 14px;
}
.game-settings__mode-select-item + .game-settings__mode-select-item {
margin-top: 10px;
}
.real-guitar-hero__score {
font-size: 24px;
color: #0654C8;
}
.sidebar-menu {
display: none;
flex-direction: column;
justify-content: space-between;
padding: 16px 20px;
position: absolute;
width: 400px;
height: 100%;
top: 0;
right: 0;
z-index: 100;
background-color: #E4EEFB;
}
.sidebar-menu--active {
display: flex;
}
.sidebar-menu__footer {
text-align: right;
}
.footer-link {
font-family: 'Source Sans Pro', sans-serif;
font-size: 16px;
line-height: 20px;
color: #1D3557;
text-decoration: none;
}
.sidebar-menu__footer a + a {
margin-left: 15px;
}
.sidebar-menu__header {
margin: 0;
padding: 0;
font-family: 'Source Sans Pro', sans-serif;
font-size: 24px;
line-height: 32px;
color: #1D3557;
}
.sidebar-menu__subheader {
margin-top: 20px;
font-family: 'Source Sans Pro', sans-serif;
font-size: 16px;
line-height: 20px;
color: #1D3557;
}
.sidebar-menu__settings {
margin-top: 20px;
}
.sidebar-menu__start-button {
margin: 50px auto 0;
display: block;
width: 350px;
padding: 20px 25px;
border: none;
border-radius: 2px;
background-color: #7C69F4;
font-family: 'Source Sans Pro', sans-serif;
font-weight: bold;
font-size: 24px;
line-height: 32px;
color: #FFF;
letter-spacing: 4px;
outline: none;
cursor: pointer;
}
.sidebar-menu__start-button:hover {
opacity: 0.9;
}
.sidebar-menu__install-chrome,
.sidebar-menu__update-chrome {
display: none;
margin-top: 10px;
font-family: 'Source Sans Pro', sans-serif;
font-size: 16px;
line-height: 20px;
color: #1D3557;
}
.sidebar-menu__install-chrome--active,
.sidebar-menu__update-chrome--active {
display: block;
}
.sidebar-menu__install-chrome i,
.sidebar-menu__update-chrome i {
color: red;
}
.sidebar-menu__install-chrome a,
.sidebar-menu__update-chrome a {
font-weight: bold;
text-decoration: none;
}
.sidebar-menu__install-chrome a:hover,
.sidebar-menu__update-chrome a:hover {
text-decoration: underline;
}
.sidebar-menu__sharing-buttons {
display: flex;
margin-top: 20px;
align-items: center;
justify-content: center;
}
.sharing-buttons {
display: flex;
margin-bottom: 20px;
}
.share-button {
display: inline-block;
padding: 10px 15px;
border-radius: 2px;
font-family: 'Source Sans Pro', sans-serif;
font-size: 16px;
line-height: 20px;
color: #F1FAEE;
text-decoration: none;
}
.share-button:hover {
opacity: 0.9;
}
.share-button i {
margin-right: 4px;
}
.share-button + .share-button {
margin-left: 8px;
}
.share-button--fb {
background-color: #3b5998;
}
.share-button--tw {
background-color: #4099FF;
}
.audio-wave {
display: none;
width: 60px;
height: 32px;
}
.audio-wave--active {
display: block;
}
.audio-wave path {
stroke: #F1FAEE;
stroke-width: 1;
fill: none;
opacity: 0.5;
}
.no-sound {
display: none;
padding-top: 8px;
color: white;
font-size: 30px;
}
.no-sound--active {
display: block;
}
.github-corner:hover .octo-arm{animation:octocat-wave 560ms ease-in-out}@keyframes octocat-wave{0%,100%{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}@media (max-width:500px){.github-corner:hover .octo-arm{animation:none}.github-corner .octo-arm{animation:octocat-wave 560ms ease-in-out}}
================================================
FILE: index.html
================================================
Guitar Bro – browser game with a real guitar.
Allow microphone so we can recognize musical notes when you're playing
================================================
FILE: js/app.js
================================================
$(function() {
var $game = $(".real-guitar-hero"),
$score = $game.find(".score__points");
var $settings = $(".game-settings"),
$bpmInput = $settings.find(".game-settings__bpm-input"),
$songSelect = $settings.find(".game-settings__song-select"),
$stringSelect = $settings.find(".game-settings__string-select"),
$modeSelect = $settings.find("input:radio[name=game-mode-select]"),
isSandboxMode = $modeSelect.val() === "sandbox",
isSurvivalMode = !isSandboxMode;
for(string in gameConfig.strings) {
var $option = $(" ");
$option.val(string);
$option.text(gameConfig.strings[string].name);
if(string === "1") {
$option.attr("selected", "selected");
}
$stringSelect.append($option);
}
var $sidebarMenu = $(".sidebar-menu"),
$startButton = $sidebarMenu.find(".js-start");
var songIndex, stringIndex, bpm, fretboard;
// song loader
var songLoader = new SongLoader();
// fps options
var fpsInterval = 1000 / gameConfig.fps,
startTime,
now,
then,
elapsed;
var gameIsOn = false;
//canvas variables
var canvas = document.getElementById("game-canvas");
var ctx = canvas.getContext("2d");
ctx.canvas.width = window.innerWidth;
ctx.canvas.height = window.innerHeight;
var explosion = new ExplosionEffect(ctx);
var healthDrawer = new HealthDrawer(ctx);
// game variables
var continueAnimating = false,
score = 0,
correctNotes = 0,
MAX_HEALTH = 5,
health = MAX_HEALTH;
// block variables
var pegWidth = 1;
// rock variables
var rockWidth = canvas.width / 12;
var rockFontSize = 0.5 * rockWidth;
var rockSpeed;
var rockHeight = rockWidth;
var eightsDurationDistance = rockHeight;
var rocks = [];
function initRocks(songIndex, string) {
rocks = [];
var song = songLoader.loadSong(songIndex, string);
var totalRocks = song.length;
for (var i = 0; i < totalRocks; i++) {
addRock(i, song);
}
}
function calculateRockY(rockIndex) {
var prevRock = rockIndex === 0 ? rocks[rocks.length - 1] : rocks[rockIndex - 1];
var minRockY = rocks.length === 0 ? 0 : Math.min.apply(Math, rocks.map(function(r){return r.y;}));
return rocks.length === 0 ? 0 : minRockY - prevRock.durationDistance;
}
function addRock(rockIndex, song) {
var rock = {
width: rockWidth - pegWidth,
height: rockHeight,
durationDistance: eightsDurationDistance * 8 / song[rockIndex][1]
}
var prevRock = rockIndex === 0 ? rocks[rocks.length - 1] : rocks[rockIndex - 1];
rock.note = song[rockIndex][0];
var noteIndex = songLoader.findNoteIndex(rock.note, stringIndex);
rock.x = noteIndex * rockWidth + pegWidth;
rock.y = calculateRockY(rockIndex);
rocks.push(rock);
}
function toggleMenuCopy(type) {
var $header = $sidebarMenu.find(".sidebar-menu__header"),
$subheader = $sidebarMenu.find(".sidebar-menu__subheader");
$header.text($header.data(type));
$subheader.text($subheader.data(type));
}
function stopGame() {
ga("send", {
hitType: "event",
eventCategory: "Game",
eventAction: "Lost",
eventValue: correctNotes
});
toggleSettings();
$sidebarMenu.addClass("sidebar-menu--active");
toggleMenuCopy("gameOverCopy");
$songSelect.val(songIndex);
$stringSelect.val(stringIndex);
$bpmInput.val(bpm);
gameIsOn = false;
}
function animate() {
if(health === 0 && !continueAnimating) {
clearBackground();
}
if(continueAnimating) {
requestAnimationFrame(animate);
} else {
return;
}
now = Date.now();
elapsed = now - then;
if (elapsed > fpsInterval) {
// Get ready for next frame by setting then=now, but also adjust for your
// specified fpsInterval not being a multiple of RAF's interval (16.7ms)
then = now - (elapsed % fpsInterval);
// Drawing code
for (var i = 0; i < rocks.length; i++) {
var rock = rocks[i];
rock.y += rockSpeed;
if (rock.y > canvas.height) {
if(rock.highlightColor) {
explosion.add(rock.x + rock.width / 2, canvas.height - 5, rock.highlightColor === gameConfig.colors.green);
} else {
decrementScore();
}
rock.y = calculateRockY(i);
rock.highlightColor = undefined;
if(health === 0 && isSurvivalMode) {
stopGame();
}
}
}
drawAll();
}
}
function isColliding(rock) {
return rock.y > canvas.height - rockHeight;
}
function decrementScore() {
score -= 10;
health -= 1;
}
function clearBackground() {
// clear the canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// draw the background
ctx.fillStyle = gameConfig.colors.dark_blue;
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
function drawAll() {
clearBackground();
fretboard.draw();
// draw all rocks
for (var i = 0; i < rocks.length; i++) {
var rock = rocks[i];
if(rock.y + rock.height > 0) {
ctx.font = "bold " + rockFontSize + "px Source Sans Pro, sans-serif";
var lineWidth = 8;
ctx.lineWidth = lineWidth;
ctx.strokeStyle = rock.highlightColor ? rock.highlightColor : gameConfig.colors.white;
ctx.textAlign="center";
ctx.textBaseline = "middle";
var textString = rock.note,
textWidth = ctx.measureText(textString).width;
ctx.beginPath();
ctx.arc(rock.x + rockWidth / 2 - pegWidth / 2, rock.y + rockHeight / 2, rock.width / 2 - lineWidth / 2, 0, 2 * Math.PI);
ctx.stroke();
ctx.fillStyle = gameConfig.colors.dark_blue;
ctx.fill();
ctx.fillStyle = gameConfig.colors.white;
ctx.fillText(textString, rock.x + rockWidth / 2, rock.y + rockHeight / 2);
}
ctx.lineWidth = 1;
}
healthDrawer.draw(health, isSandboxMode);
$score.text(score);
explosion.draw();
}
songLoader.populateSelectMenu($songSelect);
songIndex = $songSelect.val();
stringIndex = $stringSelect.val();
bpm = $bpmInput.val();
if(getChromeVersion() > 57) {
var processor = new AudioProcessor();
processor.setString(stringIndex);
processor.attached();
$(".allow-mic").addClass("allow-mic--active");
} else if(getChromeVersion() < 58) {
$(".sidebar-menu__update-chrome").addClass("sidebar-menu__update-chrome--active");
} else {
$(".sidebar-menu__install-chrome").addClass("sidebar-menu__install-chrome--active");
showNoMic();
}
function showNoMic() {
$(".no-sound").addClass("no-sound--active");
$(".audio-wave").removeClass("audio-wave--active");
}
$(document).on("no_mic", function() {
showNoMic();
});
$(document).on("note_detected", function(event, note) {
if(!gameIsOn) {
return;
}
var rockIndex = rocks.findIndex(function(r) {
return r.y >= canvas.height - 2 * rockHeight;
});
if(rockIndex === -1) {
fretboard.highlightFret(note);
return;
}
var rock = rocks[rockIndex];
if(!isColliding(rock)) {
fretboard.highlightFret(note);
return;
}
if(rock.highlightColor) {
return;
}
var correctAnswer = note === rock.note;
fretboard.highlightFret(note, correctAnswer ? gameConfig.colors.green : gameConfig.colors.red);
if(correctAnswer) {
score += 10;
correctNotes += 1;
if(health < MAX_HEALTH) {
health += 1;
}
} else {
decrementScore();
}
explosion.add(rock.x, rock.y, correctAnswer);
rock.highlightColor = correctAnswer ? gameConfig.colors.green : gameConfig.colors.red;
});
$startButton.on("click", function () {
ga("send", "event", "Game", "Start", isSandboxMode ? "sandbox" : "survival");
fretboard = new Fretboard(canvas, songLoader, stringIndex, rockWidth, pegWidth);
toggleMenuCopy("welcomeCopy");
$(".allow-mic").removeClass("allow-mic--active");
$sidebarMenu.removeClass("sidebar-menu--active");
correctNotes = 0;
var beatDuration = 60 / bpm;
rockSpeed = eightsDurationDistance * 8 / (gameConfig.fps * beatDuration);
then = Date.now();
startTime = then;
continueAnimating = !continueAnimating;
if(continueAnimating) {
gameIsOn = true;
score = 0;
health = MAX_HEALTH;
initRocks(songIndex, stringIndex);
if(getChromeVersion()) {
processor.setString(stringIndex);
}
}
animate();
});
var toggleSettings = function(params) {
$sidebarMenu.toggleClass("sidebar-menu--active");
continueAnimating = !$sidebarMenu.hasClass("sidebar-menu--active");
gameIsOn = continueAnimating;
animate();
}
$songSelect.on("change", function(e) {
songIndex = $(this).val();
});
$stringSelect.on("change", function(e) {
stringIndex = $(this).val();
});
$bpmInput.on("change", function(e) {
bpm = $(this).val();
});
$modeSelect.on("change", function(e) {
isSandboxMode = $(this).val() === "sandbox",
isSurvivalMode = !isSandboxMode;
})
$(document).on("keydown", function(e) {
if(e.keyCode === 32 && elapsed) {
toggleSettings();
}
});
});
================================================
FILE: js/audio_processor.js
================================================
// https://github.com/GoogleChrome/guitar-tuner
function AudioProcessor() {
this.FFTSIZE = 2048 * 4;
this.stream = null;
this.audioContext = new AudioContext();
this.analyser = this.audioContext.createAnalyser();
this.gainNode = this.audioContext.createGain();
this.microphone = null;
this.gainNode.gain.value = 0;
this.analyser.fftSize = this.FFTSIZE;
this.analyser.smoothingTimeConstant = 0.1;
this.frequencyBufferLength = this.FFTSIZE;
this.frequencyBuffer = new Float32Array(this.frequencyBufferLength / 2);
this.timeBuffer = new Float32Array(this.frequencyBufferLength);
this.sendingAudioData = false;
this.lastNoteEnergy = 0;
this.wave_power_threshold = 0.006;
this.last_note_time = -1;
var audioWaveChart = new AudioWaveChart();
var string;
var that = this;
that.requestUserMedia = function () {
navigator.getUserMedia({audio: true}, (stream) => {
that.sendingAudioData = true;
that.stream = stream;
that.microphone = that.audioContext.createMediaStreamSource(stream);
that.microphone.connect(that.analyser);
that.analyser.connect(that.gainNode);
that.gainNode.connect(that.audioContext.destination);
requestAnimationFrame(that.dispatchAudioData);
ga("send", "event", "Game", "MicEnabled");
$(".allow-mic").removeClass("allow-mic--active");
}, (err) => {
ga("send", "event", "Game", "MicDisabled");
$(document).trigger("no_mic");
$(".allow-mic").removeClass("allow-mic--active");
console.log('Unable to access the microphone');
console.log(err);
});
}
this.attached = function() {
// Set up the stream kill / setup code for visibility changes.
document.addEventListener('visibilitychange', this.onVisibilityChange);
// Then call it.
this.onVisibilityChange();
}
this.detached = function() {
this.sendingAudioData = false;
}
this.onVisibilityChange = function() {
if (document.hidden) {
that.sendingAudioData = false;
if (that.stream) {
// Chrome 47+
that.stream.getAudioTracks().forEach((track) => {
if ('stop' in track) {
track.stop();
}
});
// Chrome 46-
if ('stop' in that.stream) {
that.stream.stop();
}
}
that.stream = null;
} else {
that.requestUserMedia();
}
}
this.setString = function(string_num){
string = gameConfig.strings[string_num];
}
this.findNoteFreq = function(time) {
let freq_step = that.audioContext.sampleRate / this.FFTSIZE;
let min_freq_ind = Math.round(string.range[0] / freq_step);
let max_freq_ind = Math.round(string.range[1] / freq_step);
// Fill up the data.
that.analyser.getFloatTimeDomainData(that.timeBuffer);
that.analyser.getFloatFrequencyData(that.frequencyBuffer);
freq = that.frequencyBuffer;
wave = that.timeBuffer;
audioWaveChart.plotWave(wave);
for (let d = Math.round(Math.max(min_freq_ind - 20 / freq_step - 5, 0)); d < Math.min(max_freq_ind + 20 / freq_step + 5, freq.length); d++) {
freq[d] = Math.pow(10, 5 + freq[d] / 10);
}
let max_A = -1000000;
let arg_max = -1;
for (let i = min_freq_ind; i < max_freq_ind; i++) {
if (freq[i] > max_A){
max_A = freq[i];
arg_max = i;
}
}
let total_energy = 0;
for (let i = min_freq_ind; i < max_freq_ind; i++) {
total_energy += freq[i];
}
let maximum_energy = 0;
for (let i = Math.round(arg_max - 20 / freq_step - 1); i <= Math.round(arg_max + 20 / freq_step + 1); i++){
maximum_energy += freq[i];
}
if (maximum_energy < 0.1 || maximum_energy / total_energy < 0.96){
return -1;
}
if (time > this.last_note_time + 100){
this.lastNoteEnergy = 0;
}
// if (maximum_energy < this.lastNoteEnergy){
// return -1;
// }
// console.log(arg_max * freq_step, maximum_energy);
this.last_note_time = time;
this.lastNoteEnergy = maximum_energy;
return arg_max * freq_step;
}
this.dispatchAudioData = function(time) {
if (that.sendingAudioData) {
requestAnimationFrame(that.dispatchAudioData);
}
let frequency = that.findNoteFreq(time);
if (frequency < 0){
return;
}
let freqs = string.freqs;
let min_freq_error = 10000;
let best_chord_ind = 0;
for (let i = 0; i < freqs.length; i++){
let chord_freq = freqs[i][0];
let chord = freqs[i][1];
//let n_div = frequency / chord_freq; //for future
let n_div = 1;
let error = Math.abs( Math.round(n_div) * chord_freq - frequency);
if (error < min_freq_error){
best_chord_ind = i;
min_freq_error = error;
}
}
if (min_freq_error < 20){
$(document).trigger("note_detected", freqs[best_chord_ind][1]);
}
}
}
================================================
FILE: js/audio_wave.js
================================================
function AudioWaveChart() {
var $audioWave = $(".audio-wave");
var w = $audioWave.width();
var h = $audioWave.height();
var x_scale = d3.scaleLinear().range([0, w]).domain([0, 1]);
var y_scale = d3.scaleLinear().range([h, 0]).domain([0, 1]);
var line = d3.line()
.x(function(d) {
return x_scale(d.x);})
.y(function(d) {
return y_scale(d.y);
})
var graph = d3.select(".audio-wave").append("svg:svg")
.attr("width", w)
.attr("height", h)
.append("svg:g");
graph.append("svg:path").attr("class", "line");
var setDomain = function(data_xy){
x_scale.domain(d3.extent(data_xy, function(d){ return d.x}));
var y_range = d3.extent(data_xy, function(d){ return d.y});
y_scale.domain([ Math.min(-0.1, y_range[0]), Math.max(0.1, y_range[1]) ]);
};
var plotD3Wave = function(data_xy) {
setDomain(data_xy);
var svg = d3.select("body").transition();
svg.select(".line")
.duration(0)
.attr("d", line(data_xy));
}
return {
plotWave: function(wave) {
let found_good_ind = 0;
for (let i = 0; i < wave.length - 1; i++){
if (wave[i] < 0 && wave[i + 1] >= 0){
found_good_ind = i;
break;
}
}
found_good_ind = Math.min(found_good_ind, wave.length - 500);
wave_short = wave.slice(found_good_ind, found_good_ind + 500);
data_xy = [];
for (let i = 0; i < wave_short.length; i++){
data_xy.push({x: i, y: wave_short[i]});
}
plotD3Wave(data_xy);
}
}
}
================================================
FILE: js/config.js
================================================
var gameConfig = {
fps: 50,
colors: {
green: "#9BC53D",
yellow: "#FDE74C",
red: "#E55934",
white: "#F1FAEE",
dark_blue: "#1D3557"
},
strings: {
1: {
name: "1. E-string (thinnest)",
range: [325, 665],
freqs: [
[329.6, "E"],
[349.2, "F"],
[370.0, "F#"],
[392.0, "G"],
[415.3, "G#"],
[440.0, "A"],
[466.1, "A#"],
[493.8, "B"],
[523.2, "C"],
[554.3, "C#"],
[587.3, "D"],
[622.2, "D#"],
[659.2, "E"],
]
},
2: {
name: "2. B-string",
range: [242, 499],
freqs: [
[246.9, "B"],
[261.6, "C"],
[277.2, "C#"],
[293.7, "D"],
[311.1, "D#"],
[329.6, "E"],
[349.2, "F"],
[370.0, "F#"],
[392.0, "G"],
[415.3, "G#"],
[440.0, "A"],
[466.2, "A#"],
[493.9, "B"]
]
},
3: {
name: "3. G-string",
range: [191, 499],
freqs: [
[196.0, "G"],
[207.7, "G#"],
[220.0, "A"],
[233.1, "A#"],
[246.9, "B"],
[261.6, "C"],
[277.2, "C#"],
[293.7, "D"],
[311.1, "D#"],
[329.6, "E"],
[349.2, "F"],
[370.0, "F#"],
[392.0, "G"]
]
},
4: {
name: "4. D-string",
range: [142, 289],
freqs: [
[146.8, "D"],
[155.6, "D#"],
[164.8, "E"],
[174.6, "F"],
[185.0, "F#"],
[196.0, "G"],
[207.7, "G#"],
[220.0, "A"],
[233.1, "A#"],
[246.9, "B"],
[261.6, "C"],
[277.2, "C#"],
[293.7, "D"]
]
},
5: {
name: "5. A-string",
range: [105, 215],
freqs: [
[110.0, "A"],
[116.5, "A#"],
[123.5, "B"],
[130.8, "C"],
[138.6, "C#"],
[146.8, "D"],
[155.6, "D#"],
[164.8, "E"],
[174.6, "F"],
[185.0, "F#"],
[196.0, "G"],
[207.7, "G#"],
[220.0, "A"]
]
},
6: {
name: "6. E-string (thickest)",
range: [75, 170],
freqs: [
[82.4, "E"],
[87.3, "F"],
[92.5, "F#"],
[98.0, "G"],
[103.8, "G#"],
[110.0, "A"],
[116.5, "A#"],
[123.5, "B"],
[130.8, "C"],
[138.6, "C#"],
[146.8, "D"],
[155.6, "D#"],
[164.8, "E"],
]
},
7: {
name: "7. For those who do the metal...",
range: [55, 130],
freqs: [
[61.7, "B"],
[65.4, "C"],
[69.3, "C#"],
[73.4, "D"],
[77.8, "D#"],
[82.4, "E"],
[87.3, "F"],
[92.5, "F#"],
[98.0, "G"],
[103.8, "G#"],
[110.0, "A"],
[116.5, "A#"],
[123.5, "B"],
]
}
}
}
================================================
FILE: js/explosion_effect.js
================================================
// https://stackoverflow.com/questions/43498923/html5-canvas-particle-explosion
function ExplosionEffect(ctx) {
const particlesPerExplosion = 25;
const particlesMinSpeed = 5;
const particlesMaxSpeed = 10;
const particlesMinSize = 2;
const particlesMaxSize = 4;
var explosions = [];
function particle(x, y, correctAnswer) {
this.x = x;
this.y = y;
this.xv = randInt(particlesMinSpeed, particlesMaxSpeed, false);
this.yv = randInt(particlesMinSpeed, particlesMaxSpeed, false);
this.size = randInt(particlesMinSize, particlesMaxSize, true);
this.color = correctAnswer ? gameConfig.colors.green : gameConfig.colors.red;
}
function explosion(x, y, correctAnswer) {
this.particles = [];
for (let i = 0; i < particlesPerExplosion; i++) {
this.particles.push(
new particle(x, y, correctAnswer)
);
}
}
return {
draw: function() {
if (explosions.length === 0) {
return;
}
for (let i = 0; i < explosions.length; i++) {
const explosion = explosions[i];
const particles = explosion.particles;
if (particles.length === 0) {
explosions.splice(i, 1);
return;
}
const particlesAfterRemoval = particles.slice();
for (let ii = 0; ii < particles.length; ii++) {
const particle = particles[ii];
// Check particle size
// If 0, remove
if (particle.size <= 0) {
particlesAfterRemoval.splice(ii, 1);
continue;
}
ctx.beginPath();
ctx.arc(particle.x, particle.y, particle.size, Math.PI * 2, 0, false);
ctx.closePath();
ctx.fillStyle = particle.color;
ctx.fill();
// Update
particle.x += particle.xv;
particle.y += particle.yv;
particle.size -= .1;
}
explosion.particles = particlesAfterRemoval;
}
},
add: function(x, y, correctAnswer) {
explosions.push(
new explosion(x, y, correctAnswer)
);
}
}
}
================================================
FILE: js/fretboard.js
================================================
function Fretboard(canvas, songLoader, string, rockWidth, pegWidth) {
var ctx = canvas.getContext("2d"),
blockHeight = rockWidth,
block = {
x: 0,
y: canvas.height - blockHeight,
width: canvas.width,
height: blockHeight
};
var highlightedFret,
highlightedColor = gameConfig.colors.yellow;
function drawCircle(x, y) {
var circleSize = (blockHeight / 6 - 1) / 2;
ctx.fillStyle = gameConfig.colors.white;
ctx.beginPath();
ctx.arc(x, y, circleSize, 0, 2 * Math.PI);
ctx.fill();
}
function drawLine(x, y, x1, y1) {
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x1, y1);
ctx.stroke();
}
return {
draw: function() {
ctx.strokeStyle = gameConfig.colors.white;
ctx.lineWidth = 1;
for(var i = 1; i < gameConfig.strings[string].notes.length; i++) {
var x = i * rockWidth + pegWidth;
drawLine(x, block.y, x, canvas.height);
}
drawLine(0, block.y, canvas.width, block.y);
// draw single circles
var circleFrets = [2, 4, 6, 8];
var cirlceColor = gameConfig.colors.white;
var verticalMiddle = canvas.height - blockHeight / 2;
var circleSize = (blockHeight / 6 - 1) / 2;
circleFrets.forEach(function(fret) {
drawCircle((rockWidth * fret - 1) + rockWidth / 2 + pegWidth, verticalMiddle);
});
// draw double circles
var doubleCirclesFret = 12;
drawCircle((rockWidth * 11) + rockWidth / 2 + pegWidth, canvas.height - circleSize * 2.5);
drawCircle((rockWidth * 11) + rockWidth / 2 + pegWidth, canvas.height - block.height + 2.5 * circleSize);
if(typeof(highlightedFret) === "number") {
ctx.fillStyle = highlightedColor;
ctx.fillRect(highlightedFret * rockWidth + pegWidth, block.y, rockWidth, rockWidth);
}
},
highlightFret: function(note, color) {
var fretIndex = songLoader.findNoteIndex(note, string);
highlightedFret = fretIndex;
highlightedColor = color ? color : gameConfig.colors.yellow;
setTimeout(function() {
highlightedFret = undefined;
}, 100);
}
}
}
================================================
FILE: js/health_drawer.js
================================================
function HealthDrawer(ctx) {
var heartWidth = 40;
var heartHeight = 25;
var c1 = 2;
var c2 = 2;
function drawBezierCurve(x0, y0, x1, y1, x2, y2, x3, y3) {
ctx.moveTo(x0, y0);
ctx.bezierCurveTo(x1, y1, x2, y2, x3, y3);
}
return {
draw: function(health, isSandboxMode) {
if(isSandboxMode) {
return;
}
var prevFillStyle = ctx.fillStyle;
var prevStrokeStyle = ctx.strojeStyle;
ctx.fillStyle = gameConfig.colors.red;
ctx.strokeStyle = gameConfig.colors.red;
for(var i = 0; i < health; i++) {
var x = ctx.canvas.width - heartWidth - i * (heartWidth + 10);
var y = 20;
ctx.beginPath();
drawBezierCurve(x, y, x, y - heartHeight / 2, x - heartWidth / 2, y - heartHeight / 2, x - heartWidth / 2, y);
drawBezierCurve(x - heartWidth / 2, y, x - heartWidth / 2, y + heartHeight / 2, x, y + heartHeight / 2 * c1, x, y + heartHeight / 2 * c2);
drawBezierCurve(x, y + heartHeight / 2 * c2, x, y + heartHeight / 2 * c1, x + heartWidth / 2, y + heartHeight / 2, x + heartWidth / 2, y);
drawBezierCurve(x + heartWidth / 2, y, x + heartWidth / 2, y - heartHeight / 2, x, y - heartHeight / 2, x, y);
ctx.closePath();
ctx.fill();
ctx.beginPath();
ctx.moveTo(x - heartWidth / 2, y);
ctx.lineTo(x + heartWidth / 2, y);
ctx.lineTo(x, y + heartHeight / 2 * c2);
ctx.closePath();
ctx.stroke();
ctx.fill()
}
ctx.fillStyle = prevFillStyle;
ctx.strokeStyle = prevStrokeStyle;
}
}
}
================================================
FILE: js/helper_functions.js
================================================
function randInt(min, max, positive) {
let num;
if (positive === false) {
num = Math.floor(Math.random() * max) - min;
num *= Math.floor(Math.random() * 2) === 1 ? 1 : -1;
} else {
num = Math.floor(Math.random() * max) + min;
}
return num;
}
function pickRandom(array) {
return array[Math.floor(Math.random() * array.length)];
}
function getChromeVersion() {
var raw = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./);
return raw ? parseInt(raw[2], 10) : false;
}
function randomArray(length, max) {
return Array.apply(null, Array(length)).map(function() {
return Math.round(Math.random() * max);
});
}
$.fn.customerPopup = function (e, intWidth, intHeight, blnResize) {
// Prevent default anchor event
e.preventDefault();
// Set values for window
intWidth = intWidth || '500';
intHeight = intHeight || '400';
strResize = (blnResize ? 'yes' : 'no');
// Set title and open popup with focus on it
var strTitle = ((typeof this.attr('title') !== 'undefined') ? this.attr('title') : 'Social Share'),
strParam = 'width=' + intWidth + ',height=' + intHeight + ',resizable=' + strResize,
objWindow = window.open(this.attr('href'), strTitle, strParam).focus();
}
================================================
FILE: js/sharing.js
================================================
$(document).on("click", ".share-button", function(e) {
var $this = $(this);
ga("send", "event", "Game", "Share", $this.data("eventLabel"));
$this.customerPopup(e);
});
================================================
FILE: js/song_loader.js
================================================
function SongLoader() {
const randomSongLength = 10;
Object.keys(gameConfig.strings).forEach(function(string) {
var rows = gameConfig.strings[string].freqs.slice(1, 13);
var notes = rows.map(function(row) {
var note = row[1];
return note;
})
gameConfig.strings[string].notes = notes;
});
const songs = {
"Random notes": randomArray(20, 11).join("--------"),
"Happy Birthday": "0-0-2--0--5-4----0-0-2--0----7-5----0-0-9--7-5-4--2-2----10-10-9--5--7-5",
"Guess what": "0--3--5---0--3--6--5---0--3--5---3--0",
"Abba: Money Money Money v2 (Tempo=200)": [['F', 4], ['G', 8], ['G#', 4], ['F', 8], ['G', 4], ['G#', 4], ['-', 4], ['G#', 4], ['F', 8], ['G', 4], ['G#', 4], ['-', 4], ['G', 4], ['F', 8], ['G#', 4], ['G#', 4], ['-', 16], ['F', 1]],
"Coca Cola(Tempo=125)": [['A', 8], ['A', 8], ['A', 8], ['A', 8], ['A#', 4], ['A', 8], ['G', 4], ['G', 8], ['C', 8], ['A', 4], ['F', 4], ['-', 2]],
"Eiffel 65: Blue v1 (Tempo=140)": [['A', 4], ['A#', 4], ['G', 8], ['A#', 8], ['C', 8], ['F', 8], ['A', 8], ['A#', 4], ['G', 8], ['A#', 8], ['D', 8], ['D#', 4], ['D', 8], ['C', 8], ['A#', 4], ['G', 8], ['A#', 8], ['C', 8], ['F', 8], ['A', 8], ['A#', 4], ['G', 8], ['A#', 8], ['D', 8], ['D#', 4], ['D', 8], ['C', 8], ['A#', 4], ['G', 8], ['A#', 8], ['C', 8], ['F', 8], ['A', 8], ['A#', 4], ['G', 8], ['A#', 8], ['D', 8], ['D#', 4], ['D', 8], ['C', 8], ['A#', 4], ['G', 8], ['A#', 8], ['A', 8], ['F', 8], ['F', 8], ['G', 2]],
"Europe: The Final Countdown (Tempo=125)": [['-', 4], ['-', 8], ['C', 16], ['A#', 16], ['C', 4], ['F', 4], ['-', 4], ['-', 8], ['C#', 16], ['C', 16], ['C#', 8], ['C', 8], ['A#', 4], ['-', 4], ['-', 8], ['C#', 16], ['C', 16], ['C#', 4], ['F', 4], ['-', 4], ['-', 8], ['A#', 16], ['G#', 16], ['A#', 8], ['G#', 8], ['G', 8], ['A#', 8], ['G#', 4], ['-', 8], ['G', 16], ['G#', 16], ['A#', 4], ['-', 8], ['G#', 16], ['A#', 16], ['C', 8], ['A#', 8], ['G#', 8], ['G', 8], ['F', 4], ['C#', 4], ['C', 2], ['-', 4], ['C', 16], ['C#', 16], ['C', 16], ['A#', 16], ['C', 1]],
"Haddaway: What is Love (Tempo=225)": [['A#', 4], ['A', 4], ['A#', 4], ['G', 4], ['A#', 4], ['A', 4], ['A#', 4], ['G', 4], ['A#', 4], ['A', 4], ['A#', 4], ['F', 4], ['A#', 4], ['A', 4], ['A#', 4], ['F', 4], ['A', 4], ['G', 4], ['A', 4], ['F', 4], ['A', 4], ['G', 4], ['A', 4], ['F', 4], ['A', 4], ['G', 4], ['A', 4], ['F', 4], ['A', 4], ['G', 4], ['A', 4], ['F', 4]],
"James Bond: Tomorrow Never Dies (Tempo=125)": [['F', 8], ['G', 16], ['G', 16], ['G', 8], ['G', 4], ['F', 8], ['F', 8], ['F', 8], ['F', 8], ['G#', 16], ['G#', 16], ['G#', 8], ['G#', 4], ['G', 8], ['G', 8], ['G', 8], ['F', 8], ['G', 16], ['G', 16], ['G', 8], ['G', 4], ['F', 8], ['F', 8], ['F', 8], ['F', 8], ['G#', 16], ['G#', 16], ['G#', 8], ['G#', 4], ['G', 8], ['G', 8], ['G', 8]],
"Nirvana: Come as You Are (Tempo=225)": [['F', 8], ['F', 8], ['F#', 8], ['G', 8], ['-', 4], ['-', 4], ['A#', 8], ['G', 8], ['A#', 8], ['G', 8], ['G', 8], ['F#', 8], ['F', 8], ['C', 8], ['F', 8], ['F', 8], ['-', 4], ['-', 4], ['C', 8], ['F', 8], ['F#', 8], ['G', 8], ['-', 4], ['-', 4], ['A#', 8], ['G', 8], ['A#', 8], ['G', 8], ['G', 8], ['F#', 8], ['F', 8], ['C', 8], ['F', 8], ['F', 8], ['-', 4], ['-', 4], ['C', 8]],
"Ricky Martin: Livin La Vida Loca (Tempo=160)": [['A#', 16], ['-', 8], ['-', 16], ['A#', 4], ['-', 16], ['-', 32], ['F#', 8], ['G#', 8], ['B', 16], ['-', 8], ['-', 16], ['B', 16], ['-', 8], ['-', 16], ['A#', 4], ['-', 4], ['-', 8], ['A#', 16], ['-', 8], ['-', 16], ['A#', 4], ['-', 16], ['-', 32], ['F#', 8], ['F', 8], ['G#', 8], ['-', 8], ['G#', 8], ['-', 8], ['F#', 4], ['-', 4], ['-', 8], ['A#', 16], ['-', 8], ['-', 16], ['A#', 4], ['-', 8], ['F#', 8], ['G#', 8], ['B', 16], ['-', 8], ['-', 16], ['B', 16], ['-', 8], ['-', 16], ['A#', 4], ['-', 4], ['-', 8], ['A#', 16], ['-', 8], ['-', 16], ['A#', 4], ['-', 8], ['F#', 8], ['F', 8], ['G#', 16], ['-', 8], ['-', 16], ['G#', 8], ['-', 8], ['F#', 4], ['-', 8]],
"Smoke on the Water (Tempo=112)": [['F', 4], ['G#', 4], ['A#', 4], ['F', 4], ['G#', 4], ['B', 8], ['A#', 4], ['-', 4], ['F', 4], ['G#', 4], ['A#', 4], ['G#', 4], ['F', 4], ['-', 2], ['-', 8], ['F', 4], ['G#', 4], ['A#', 4], ['F', 4], ['G#', 4], ['B', 8], ['A#', 4], ['-', 4], ['F', 4], ['G#', 4], ['A#', 4], ['G#', 4], ['F', 4], ['-', 4]]
}
function parseSong(encodedSong, string) {
let song = [];
let duration = 0;
let last_note;
for (let i = 0; i < encodedSong.length; i++){
if (encodedSong[i] != "-"){
if (duration > 0){
song.push([last_note, 8 / duration]);
}
let fret = parseInt(encodedSong[i]);
last_note = fret === 0 ? "E" : gameConfig.strings[string].notes[fret - 1];
duration = 0;
} else {
duration += 1;
}
}
song.push([last_note, 8/8.0]);
return song;
}
return {
loadSong: function(songIndex, string) {
var encodedSong = songs[songIndex];
if (encodedSong.constructor === Array){
return encodedSong;
}
return parseSong(encodedSong, string);
},
findNoteIndex: function(note, string) {
return gameConfig.strings[string].notes.findIndex(function(n) {
return note === n;
});
},
populateSelectMenu: function($songSelect) {
for(song in songs) {
var $option = $(" ");
$option.val(song);
$option.text(song);
if(song === "Random") {
$option.attr("selected", "selected");
}
$songSelect.append($option);
}
}
}
}