[
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2017 Anatoli Makarevich\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Guitar Bro – browser game that helps you learn notes on guitar\n\n![Guitar Bro Snapshot](https://user-images.githubusercontent.com/768070/27518743-17a23ec4-59e7-11e7-8873-5b8ee3be5251.png)\n\n## Description\n\nGuitar 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.\n\n[Try it out!](https://makaroni4.github.io/guitar_bro/)\n"
  },
  {
    "path": "css/app.css",
    "content": "html, body {\n  width:  100%;\n  height: 100%;\n  margin: 0;\n  padding: 0;\n  overflow: hidden;\n}\n\nhtml {\n  box-sizing: border-box;\n}\n\n*, *:before, *:after {\n  box-sizing: inherit;\n}\n\n.allow-mic {\n  display: none;\n\n  background-color: transparent;\n}\n\n.allow-mic--active {\n  display: block;\n}\n\n.allow-mic__arrow {\n  position: absolute;\n  left: 408px;\n  top: 60px;\n  width: 60px;\n  height: 72px;\n\n  background-image: url(\"../img/allow_mic_arrow.png\");\n  background-size: cover;\n}\n\n.allow-mic__message {\n  position: absolute;\n  width: 250px;\n  left: 330px;\n  top: 140px;\n\n  font-family: 'Kalam', cursive;\n  font-size: 24px;\n  color: #F1FAEE;\n}\n\n.header {\n  display: flex;\n  align-items: center;\n  height: 60px;\n}\n\n.header__audio-wave {\n  position: absolute;\n  top: 2px;\n  left: 10px;\n}\n\n.header__score {\n  display: none;\n  position: absolute;\n  top: 20px;\n  right: 40px;\n\n  font-size: 32px;\n  line-height: 48px;\n  font-weight: bold;\n  color: #A8DADC;\n}\n\n.real-guitar-hero {\n  background-color: #1D3557;\n}\n\n.real-guitar-hero__header {\n  position: absolute;\n}\n\n.real-guitar-hero__canvas {\n}\n\n.real-guitar-hero__settings {\n  display: block;\n  position: absolute;\n  top: 10px;\n  right: 10px;\n\n  font-family: 'Source Sans Pro', sans-serif;\n  font-weight: bold;\n  color: #F1FAEE;\n  text-decoration: none;\n}\n\n.game-settings {\n  width: 100%;\n}\n\n\n.game-settings__bpm-input-label {\n  display: block;\n\n  font-family: 'Source Sans Pro', sans-serif;\n  font-weight: bold;\n  color: #1D3557;\n  text-decoration: none;\n}\n\n.game-settings__bpm-input {\n  display: block;\n  padding: 8px;\n  width: 100%;\n\n  font-size: 24px;\n  line-height: 32px;\n}\n\n.game-settings__song-select-label,\n.game-settings__string-select-label,\n.game-settings__mode-select-label {\n  display: block;\n  margin-top: 16px;\n\n  font-family: 'Source Sans Pro', sans-serif;\n  font-weight: bold;\n  color: #1D3557;\n  text-decoration: none;\n}\n\n.game-settings__song-select,\n.game-settings__string-select {\n  display: block;\n  width: 100%;\n  height: 52px;\n\n  background-color: #FFF;\n\n  font-family: 'Source Sans Pro', sans-serif;\n  color: #1D3557;\n  text-decoration: none;\n  font-size: 18px;\n  line-height: 24px;\n}\n\n.game-settings__mode-select {\n  font-family: 'Source Sans Pro', sans-serif;\n  color: #1D3557;\n  text-decoration: none;\n  font-size: 18px;\n  line-height: 24px;\n}\n\n.game-settings__mode-select-hint {\n  font-size: 14px;\n  line-height: 14px;\n}\n\n.game-settings__mode-select-item + .game-settings__mode-select-item {\n  margin-top: 10px;\n}\n\n.real-guitar-hero__score {\n  font-size: 24px;\n  color: #0654C8;\n}\n\n.sidebar-menu {\n  display: none;\n  flex-direction: column;\n  justify-content: space-between;\n  padding: 16px 20px;\n  position: absolute;\n  width: 400px;\n  height: 100%;\n  top: 0;\n  right: 0;\n  z-index: 100;\n\n  background-color: #E4EEFB;\n}\n\n.sidebar-menu--active {\n  display: flex;\n}\n\n.sidebar-menu__footer {\n  text-align: right;\n}\n\n.footer-link {\n  font-family: 'Source Sans Pro', sans-serif;\n  font-size: 16px;\n  line-height: 20px;\n  color: #1D3557;\n  text-decoration: none;\n}\n\n.sidebar-menu__footer a + a {\n  margin-left: 15px;\n}\n\n.sidebar-menu__header {\n  margin: 0;\n  padding: 0;\n\n  font-family: 'Source Sans Pro', sans-serif;\n  font-size: 24px;\n  line-height: 32px;\n  color: #1D3557;\n}\n\n.sidebar-menu__subheader {\n  margin-top: 20px;\n\n  font-family: 'Source Sans Pro', sans-serif;\n  font-size: 16px;\n  line-height: 20px;\n  color: #1D3557;\n}\n\n.sidebar-menu__settings {\n  margin-top: 20px;\n}\n\n.sidebar-menu__start-button {\n  margin: 50px auto 0;\n  display: block;\n  width: 350px;\n  padding: 20px 25px;\n\n  border: none;\n  border-radius: 2px;\n\n  background-color: #7C69F4;\n\n  font-family: 'Source Sans Pro', sans-serif;\n  font-weight: bold;\n  font-size: 24px;\n  line-height: 32px;\n  color: #FFF;\n  letter-spacing: 4px;\n\n  outline: none;\n  cursor: pointer;\n}\n\n.sidebar-menu__start-button:hover {\n  opacity: 0.9;\n}\n\n.sidebar-menu__install-chrome,\n.sidebar-menu__update-chrome {\n  display: none;\n  margin-top: 10px;\n\n  font-family: 'Source Sans Pro', sans-serif;\n  font-size: 16px;\n  line-height: 20px;\n  color: #1D3557;\n}\n\n.sidebar-menu__install-chrome--active,\n.sidebar-menu__update-chrome--active {\n  display: block;\n}\n\n.sidebar-menu__install-chrome i,\n.sidebar-menu__update-chrome i {\n  color: red;\n}\n\n.sidebar-menu__install-chrome a,\n.sidebar-menu__update-chrome a {\n  font-weight: bold;\n  text-decoration: none;\n}\n\n.sidebar-menu__install-chrome a:hover,\n.sidebar-menu__update-chrome a:hover {\n  text-decoration: underline;\n}\n\n.sidebar-menu__sharing-buttons {\n  display: flex;\n  margin-top: 20px;\n  align-items: center;\n  justify-content: center;\n}\n\n.sharing-buttons {\n  display: flex;\n  margin-bottom: 20px;\n}\n\n.share-button {\n  display: inline-block;\n  padding: 10px 15px;\n\n  border-radius: 2px;\n\n  font-family: 'Source Sans Pro', sans-serif;\n  font-size: 16px;\n  line-height: 20px;\n  color: #F1FAEE;\n  text-decoration: none;\n}\n\n.share-button:hover {\n  opacity: 0.9;\n}\n\n.share-button i {\n  margin-right: 4px;\n}\n\n.share-button + .share-button {\n  margin-left: 8px;\n}\n\n.share-button--fb {\n  background-color: #3b5998;\n}\n\n.share-button--tw {\n  background-color: #4099FF;\n}\n\n.audio-wave {\n  display: none;\n\n  width: 60px;\n  height: 32px;\n}\n\n.audio-wave--active {\n  display: block;\n}\n\n.audio-wave path {\n  stroke: #F1FAEE;\n  stroke-width: 1;\n  fill: none;\n  opacity: 0.5;\n}\n\n.no-sound {\n  display: none;\n  padding-top: 8px;\n\n  color: white;\n  font-size: 30px;\n}\n\n.no-sound--active {\n  display: block;\n}\n\n.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}}\n"
  },
  {
    "path": "index.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n  <title>Guitar Bro – browser game with a real guitar.</title>\n\n  <meta property=\"og:url\"           content=\"https://makaroni4.github.io/guitar_bro/\" />\n  <meta property=\"og:type\"          content=\"website\" />\n  <meta property=\"og:title\"         content=\"Guitar Bro – open source browser game that helps you learn notes on guitar\" />\n  <meta property=\"og:description\"   content=\"Practice and learn notes on any string – just grab your guitar, select comfortable rythm and play\" />\n  <meta property=\"og:image\"         content=\"https://makaroni4.github.io/guitar_bro/img/og.png\" />\n  <meta property=\"fb:app_id\"        content=\"1611939258817451\">\n\n  <meta name=\"twitter:card\" content=\"https://makaroni4.github.io/guitar_bro/img/og.png\">\n  <meta name=\"twitter:site\" content=\"@makaroni4\">\n  <meta name=\"twitter:title\" content=\"Guitar Bro – open source browser game that helps you learn notes on your guitar\">\n  <meta name=\"twitter:description\" content=\"Practice and learn notes on any string – just grab your guitar, select comfortable rythm and play\">\n  <meta name=\"twitter:creator\" content=\"@makaroni4\">\n  <meta name=\"twitter:image\" content=\"https://makaroni4.github.io/guitar_bro/img/og.png\">\n  <meta name=\"twitter:domain\" content=\"makaroni4.com\">\n\n  <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n\n  <link rel=\"stylesheet\" href=\"css/app.css\">\n  <link rel=\"stylesheet\" href=\"https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css\">\n\n  <link href=\"https://fonts.googleapis.com/css?family=Kalam:700\" rel=\"stylesheet\">\n  <link href=\"https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,700\" rel=\"stylesheet\">\n</head>\n\n<body>\n  <div class=\"allow-mic\">\n    <div class=\"allow-mic__arrow\">\n    </div>\n\n    <div class=\"allow-mic__message\">\n      Allow microphone so we can recognize musical notes when you're playing\n    </div>\n  </div>\n\n  <div class=\"real-guitar-hero\">\n    <div class=\"real-guitar-hero__header\">\n      <header class=\"header\">\n        <div class=\"header__audio-wave\">\n          <div class=\"audio-wave audio-wave--active\">\n          </div>\n\n          <div class=\"no-sound\">\n            <i class=\"fa fa-microphone-slash\" aria-hidden=\"true\"></i>\n          </div>\n        </div>\n\n        <div class=\"header__score score\">\n          Score: <span class=\"score__points\">0</span>\n        </div>\n      </header>\n    </div>\n\n    <div class=\"real-guitar-hero__canvas\">\n      <canvas id=\"game-canvas\"></canvas>\n    </div>\n  </div>\n\n  <div class=\"sidebar-menu sidebar-menu--active\">\n    <div class=\"sidebar-menu__body\">\n      <h1 class=\"sidebar-menu__header\" data-welcome-copy=\"Welcome to Guitar Bro – guitar browser game with a real guitar!\" data-game-over-copy=\"Game over! Did you have some fun?\">\n        Welcome to Guitar Bro – guitar browser game with a real guitar!\n      </h1>\n\n      <div class=\"sidebar-menu__subheader\" data-welcome-copy=\"Pick up a string you want to play on\" data-game-over-copy=\"Game over! Nice one! Try to adjust BPM and try again!\">\n        Pick up a string you want to play on (yep, it's \"unitar\").\n      </div>\n\n      <div class=\"sidebar-menu__settings\">\n        <div class=\"game-settings\">\n          <label class=\"game-settings__bpm-input-label\">\n            BPM\n            <input class=\"game-settings__bpm-input\" type=\"number\" value=\"30\" />\n          </label>\n\n          <label class=\"game-settings__song-select-label\">\n            Select a song\n            <select class=\"game-settings__song-select\">\n            </select>\n          </label>\n\n          <label class=\"game-settings__string-select-label\">\n            Select a string\n            <select class=\"game-settings__string-select\">\n            </select>\n          </label>\n\n          <div class=\"game-settings__mode-select\">\n            <div class=\"game-settings__mode-select-label\">\n              Select a mode\n            </div>\n\n            <div class=\"game-settings__mode-select-item\">\n              <input type=\"radio\" id=\"game-settings-survival-mode\" name=\"game-mode-select\" value=\"survival\" checked>\n              <label for=\"game-settings-survival-mode\" class=\"game-settings__mode-select-input\">\n                Survival\n                <div class=\"game-settings__mode-select-hint\">You start with 5 health (and get 1 health on correct note)</div>\n              </label>\n            </div>\n\n            <div class=\"game-settings__mode-select-item\">\n              <input type=\"radio\" id=\"game-settings-sandbox-mode\" name=\"game-mode-select\" value=\"sandbox\" class=\"game-settings__mode-select-input\">\n              <label for=\"game-settings-sandbox-mode\" class=\"game-settings__mode-select-input\">\n                Sandbox\n                <div class=\"game-settings__mode-select-hint\">The game will stop when you stop it</div>\n              </label>\n            </div>\n          </div>\n        </div>\n      </div>\n\n      <button class=\"sidebar-menu__start-button js-start\">\n        START NEW GAME\n      </button>\n\n      <div class=\"sidebar-menu__install-chrome\">\n        <i class=\"fa fa-exclamation-triangle\" aria-hidden=\"true\"></i> To enjoy Guitar Bro you need to open this web page in <a href=\"https://www.google.com/chrome/browser/desktop/\">Google Chrome</a>. It's the only browser now that supports sound analysis on the client (yep, there is no fancy servers – all magic happens here).\n      </div>\n\n      <div class=\"sidebar-menu__update-chrome\">\n        <i class=\"fa fa-exclamation-triangle\" aria-hidden=\"true\"></i> To enjoy Guitar Bro you need to update you Chrome browser to the latest version. Open <a href=\"chrome://help/\">Chrome settings</a> and make sure you have version 58 or more (latest version supports sound analysis, Guitar Bro works completely in browser).\n      </div>\n    </div>\n\n    <div class=\"sidebar-menu__footer\">\n      <div class=\"sidebar-menu__sharing-buttons\">\n        <div class=\"sharing-buttons\">\n          <a href=\"https://www.facebook.com/sharer.php?u=https://makaroni4.github.io/guitar_bro/\" class=\"share-button share-button--fb\" data-event-label=\"fb\">\n            <i class=\"fa fa-facebook\" aria-hidden=\"true\"></i>\n            Share on Facebook\n          </a>\n          <a href=\"https://twitter.com/share?url=https://makaroni4.github.io/guitar_bro/&amp;text=Try Guitar Bro – open source browser game to learn notes on guitar\" class=\"share-button share-button--tw\" data-event-label=\"tw\">\n            <i class=\"fa fa-twitter\" aria-hidden=\"true\"></i>\n            Share on Twitter\n          </a>\n        </div>\n      </div>\n\n      <a class=\"footer-link\" href=\"https://makaroni4.github.io/triads/\" target=\"_blank\">\n        <i class=\"fa fa-hand-spock-o\" aria-hidden=\"true\"></i>\n        Learn triads\n      </a>\n\n      <a class=\"footer-link\" href=\"http://makaroni4.com/2017/07/10/guitar-bro/\" target=\"_blank\">\n        <i class=\"fa fa-cogs\" aria-hidden=\"true\"></i>\n        How it works?\n      </a>\n\n      <a class=\"footer-link\" href=\"https://github.com/makaroni4/real_guitar_hero\" target=\"_blank\">\n        <i class=\"fa fa-github\" aria-hidden=\"true\"></i>\n        Source Code\n      </a>\n    </div>\n  </div>\n\n  <script>\n    (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){\n    (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),\n    m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)\n    })(window,document,'script','https://www.google-analytics.com/analytics.js','ga');\n\n    ga('create', 'UA-101505240-1', 'auto');\n    ga('send', 'pageview');\n  </script>\n\n  <script src=\"https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js\"></script>\n  <script src=\"https://cdnjs.cloudflare.com/ajax/libs/d3/4.9.1/d3.min.js\"></script>\n  <script src=\"js/config.js\" type=\"text/javascript\" charset=\"utf-8\" async defer></script>\n  <script src=\"js/song_loader.js\" type=\"text/javascript\" charset=\"utf-8\" async defer></script>\n  <script src=\"js/health_drawer.js\" type=\"text/javascript\" charset=\"utf-8\" async defer></script>\n  <script src=\"js/helper_functions.js\" type=\"text/javascript\" charset=\"utf-8\" async defer></script>\n  <script src=\"js/explosion_effect.js\" type=\"text/javascript\" charset=\"utf-8\" async defer></script>\n  <script src=\"js/fretboard.js\" type=\"text/javascript\" charset=\"utf-8\" async defer></script>\n  <script src=\"js/audio_wave.js\" type=\"text/javascript\" charset=\"utf-8\" async defer></script>\n  <script src=\"js/audio_processor.js\" type=\"text/javascript\" charset=\"utf-8\" async defer></script>\n  <script src=\"js/app.js\" type=\"text/javascript\" charset=\"utf-8\" async defer></script>\n  <script src=\"js/sharing.js\" type=\"text/javascript\" charset=\"utf-8\" async defer></script>\n</body>\n</html>\n"
  },
  {
    "path": "js/app.js",
    "content": "$(function() {\n  var $game = $(\".real-guitar-hero\"),\n      $score = $game.find(\".score__points\");\n\n  var $settings = $(\".game-settings\"),\n      $bpmInput = $settings.find(\".game-settings__bpm-input\"),\n      $songSelect = $settings.find(\".game-settings__song-select\"),\n      $stringSelect = $settings.find(\".game-settings__string-select\"),\n      $modeSelect = $settings.find(\"input:radio[name=game-mode-select]\"),\n      isSandboxMode = $modeSelect.val() === \"sandbox\",\n      isSurvivalMode = !isSandboxMode;\n\n  for(string in gameConfig.strings) {\n    var $option = $(\"<option/>\");\n    $option.val(string);\n    $option.text(gameConfig.strings[string].name);\n\n    if(string === \"1\") {\n      $option.attr(\"selected\", \"selected\");\n    }\n\n    $stringSelect.append($option);\n  }\n\n  var $sidebarMenu = $(\".sidebar-menu\"),\n      $startButton = $sidebarMenu.find(\".js-start\");\n\n  var songIndex, stringIndex, bpm, fretboard;\n\n  // song loader\n  var songLoader = new SongLoader();\n\n  // fps options\n  var fpsInterval = 1000 / gameConfig.fps,\n      startTime,\n      now,\n      then,\n      elapsed;\n\n  var gameIsOn = false;\n\n  //canvas variables\n  var canvas = document.getElementById(\"game-canvas\");\n\n  var ctx = canvas.getContext(\"2d\");\n  ctx.canvas.width = window.innerWidth;\n  ctx.canvas.height = window.innerHeight;\n\n  var explosion = new ExplosionEffect(ctx);\n\n  var healthDrawer = new HealthDrawer(ctx);\n\n  // game variables\n  var continueAnimating = false,\n      score = 0,\n      correctNotes = 0,\n      MAX_HEALTH = 5,\n      health = MAX_HEALTH;\n\n  // block variables\n  var pegWidth = 1;\n\n  // rock variables\n  var rockWidth = canvas.width / 12;\n  var rockFontSize = 0.5 * rockWidth;\n  var rockSpeed;\n  var rockHeight = rockWidth;\n  var eightsDurationDistance = rockHeight;\n  var rocks = [];\n\n  function initRocks(songIndex, string) {\n    rocks = [];\n    var song = songLoader.loadSong(songIndex, string);\n    var totalRocks = song.length;\n\n    for (var i = 0; i < totalRocks; i++) {\n      addRock(i, song);\n    }\n  }\n\n  function calculateRockY(rockIndex) {\n    var prevRock = rockIndex === 0 ? rocks[rocks.length - 1] : rocks[rockIndex - 1];\n    var minRockY = rocks.length === 0 ? 0 : Math.min.apply(Math, rocks.map(function(r){return r.y;}));\n\n    return rocks.length === 0 ? 0 : minRockY - prevRock.durationDistance;\n  }\n\n  function addRock(rockIndex, song) {\n    var rock = {\n      width: rockWidth - pegWidth,\n      height: rockHeight,\n      durationDistance: eightsDurationDistance * 8 / song[rockIndex][1]\n    }\n\n    var prevRock = rockIndex === 0 ? rocks[rocks.length - 1] : rocks[rockIndex - 1];\n\n    rock.note = song[rockIndex][0];\n\n    var noteIndex = songLoader.findNoteIndex(rock.note, stringIndex);\n\n    rock.x = noteIndex * rockWidth + pegWidth;\n    rock.y = calculateRockY(rockIndex);\n\n    rocks.push(rock);\n  }\n\n  function toggleMenuCopy(type) {\n    var $header = $sidebarMenu.find(\".sidebar-menu__header\"),\n        $subheader = $sidebarMenu.find(\".sidebar-menu__subheader\");\n\n    $header.text($header.data(type));\n    $subheader.text($subheader.data(type));\n  }\n\n  function stopGame() {\n    ga(\"send\", {\n      hitType: \"event\",\n      eventCategory: \"Game\",\n      eventAction: \"Lost\",\n      eventValue: correctNotes\n    });\n\n    toggleSettings();\n\n    $sidebarMenu.addClass(\"sidebar-menu--active\");\n    toggleMenuCopy(\"gameOverCopy\");\n\n    $songSelect.val(songIndex);\n    $stringSelect.val(stringIndex);\n    $bpmInput.val(bpm);\n\n    gameIsOn = false;\n  }\n\n  function animate() {\n    if(health === 0 && !continueAnimating) {\n      clearBackground();\n    }\n\n    if(continueAnimating) {\n      requestAnimationFrame(animate);\n    } else {\n      return;\n    }\n\n    now = Date.now();\n    elapsed = now - then;\n\n    if (elapsed > fpsInterval) {\n      // Get ready for next frame by setting then=now, but also adjust for your\n      // specified fpsInterval not being a multiple of RAF's interval (16.7ms)\n      then = now - (elapsed % fpsInterval);\n\n      // Drawing code\n      for (var i = 0; i < rocks.length; i++) {\n        var rock = rocks[i];\n\n        rock.y += rockSpeed;\n\n        if (rock.y > canvas.height) {\n          if(rock.highlightColor) {\n            explosion.add(rock.x + rock.width / 2, canvas.height - 5, rock.highlightColor === gameConfig.colors.green);\n          } else {\n            decrementScore();\n          }\n\n          rock.y = calculateRockY(i);\n          rock.highlightColor = undefined;\n\n          if(health === 0 && isSurvivalMode) {\n            stopGame();\n          }\n        }\n      }\n\n      drawAll();\n    }\n  }\n\n  function isColliding(rock) {\n    return rock.y > canvas.height - rockHeight;\n  }\n\n  function decrementScore() {\n    score -= 10;\n    health -= 1;\n  }\n\n  function clearBackground() {\n    // clear the canvas\n    ctx.clearRect(0, 0, canvas.width, canvas.height);\n\n    // draw the background\n    ctx.fillStyle = gameConfig.colors.dark_blue;\n    ctx.fillRect(0, 0, canvas.width, canvas.height);\n  }\n\n  function drawAll() {\n    clearBackground();\n\n    fretboard.draw();\n\n    // draw all rocks\n    for (var i = 0; i < rocks.length; i++) {\n      var rock = rocks[i];\n\n      if(rock.y + rock.height > 0) {\n        ctx.font = \"bold \" + rockFontSize + \"px Source Sans Pro, sans-serif\";\n\n        var lineWidth = 8;\n        ctx.lineWidth = lineWidth;\n        ctx.strokeStyle = rock.highlightColor ? rock.highlightColor : gameConfig.colors.white;\n        ctx.textAlign=\"center\";\n        ctx.textBaseline = \"middle\";\n\n        var textString = rock.note,\n            textWidth = ctx.measureText(textString).width;\n\n        ctx.beginPath();\n        ctx.arc(rock.x + rockWidth / 2 - pegWidth / 2, rock.y + rockHeight / 2, rock.width / 2 - lineWidth / 2, 0, 2 * Math.PI);\n        ctx.stroke();\n        ctx.fillStyle = gameConfig.colors.dark_blue;\n        ctx.fill();\n\n        ctx.fillStyle = gameConfig.colors.white;\n        ctx.fillText(textString, rock.x + rockWidth / 2, rock.y + rockHeight / 2);\n      }\n\n      ctx.lineWidth = 1;\n    }\n\n    healthDrawer.draw(health, isSandboxMode);\n\n    $score.text(score);\n\n    explosion.draw();\n  }\n\n  songLoader.populateSelectMenu($songSelect);\n\n  songIndex = $songSelect.val();\n  stringIndex = $stringSelect.val();\n  bpm = $bpmInput.val();\n\n  if(getChromeVersion() > 57) {\n    var processor = new AudioProcessor();\n\n    processor.setString(stringIndex);\n    processor.attached();\n\n    $(\".allow-mic\").addClass(\"allow-mic--active\");\n  } else if(getChromeVersion() < 58) {\n    $(\".sidebar-menu__update-chrome\").addClass(\"sidebar-menu__update-chrome--active\");\n  } else {\n    $(\".sidebar-menu__install-chrome\").addClass(\"sidebar-menu__install-chrome--active\");\n\n    showNoMic();\n  }\n\n  function showNoMic() {\n    $(\".no-sound\").addClass(\"no-sound--active\");\n    $(\".audio-wave\").removeClass(\"audio-wave--active\");\n  }\n\n  $(document).on(\"no_mic\", function() {\n    showNoMic();\n  });\n\n  $(document).on(\"note_detected\", function(event, note) {\n    if(!gameIsOn) {\n      return;\n    }\n\n    var rockIndex = rocks.findIndex(function(r) {\n      return r.y >= canvas.height - 2 * rockHeight;\n    });\n\n    if(rockIndex === -1) {\n      fretboard.highlightFret(note);\n      return;\n    }\n\n    var rock = rocks[rockIndex];\n\n    if(!isColliding(rock)) {\n      fretboard.highlightFret(note);\n      return;\n    }\n\n    if(rock.highlightColor) {\n      return;\n    }\n\n    var correctAnswer = note === rock.note;\n\n    fretboard.highlightFret(note, correctAnswer ? gameConfig.colors.green : gameConfig.colors.red);\n\n    if(correctAnswer) {\n      score += 10;\n      correctNotes += 1;\n\n      if(health < MAX_HEALTH) {\n        health += 1;\n      }\n    } else {\n      decrementScore();\n    }\n\n    explosion.add(rock.x, rock.y, correctAnswer);\n\n    rock.highlightColor = correctAnswer ? gameConfig.colors.green : gameConfig.colors.red;\n  });\n\n  $startButton.on(\"click\", function () {\n    ga(\"send\", \"event\", \"Game\", \"Start\", isSandboxMode ? \"sandbox\" : \"survival\");\n\n    fretboard = new Fretboard(canvas, songLoader, stringIndex, rockWidth, pegWidth);\n\n    toggleMenuCopy(\"welcomeCopy\");\n    $(\".allow-mic\").removeClass(\"allow-mic--active\");\n    $sidebarMenu.removeClass(\"sidebar-menu--active\");\n\n    correctNotes = 0;\n\n    var beatDuration = 60 / bpm;\n\n    rockSpeed = eightsDurationDistance * 8 / (gameConfig.fps * beatDuration);\n\n    then = Date.now();\n    startTime = then;\n    continueAnimating = !continueAnimating;\n\n    if(continueAnimating) {\n      gameIsOn = true;\n      score = 0;\n      health = MAX_HEALTH;\n      initRocks(songIndex, stringIndex);\n\n      if(getChromeVersion()) {\n        processor.setString(stringIndex);\n      }\n    }\n\n    animate();\n  });\n\n  var toggleSettings = function(params) {\n    $sidebarMenu.toggleClass(\"sidebar-menu--active\");\n\n    continueAnimating = !$sidebarMenu.hasClass(\"sidebar-menu--active\");\n\n    gameIsOn = continueAnimating;\n\n    animate();\n  }\n\n  $songSelect.on(\"change\", function(e) {\n    songIndex = $(this).val();\n  });\n\n  $stringSelect.on(\"change\", function(e) {\n    stringIndex = $(this).val();\n  });\n\n  $bpmInput.on(\"change\", function(e) {\n    bpm = $(this).val();\n  });\n\n  $modeSelect.on(\"change\", function(e) {\n    isSandboxMode = $(this).val() === \"sandbox\",\n    isSurvivalMode = !isSandboxMode;\n  })\n\n  $(document).on(\"keydown\", function(e) {\n    if(e.keyCode === 32 && elapsed) {\n      toggleSettings();\n    }\n  });\n});\n"
  },
  {
    "path": "js/audio_processor.js",
    "content": "// https://github.com/GoogleChrome/guitar-tuner\nfunction AudioProcessor() {\n  this.FFTSIZE = 2048 * 4;\n  this.stream = null;\n  this.audioContext = new AudioContext();\n  this.analyser = this.audioContext.createAnalyser();\n  this.gainNode = this.audioContext.createGain();\n  this.microphone = null;\n\n  this.gainNode.gain.value = 0;\n  this.analyser.fftSize = this.FFTSIZE;\n  this.analyser.smoothingTimeConstant = 0.1;\n\n  this.frequencyBufferLength = this.FFTSIZE;\n  this.frequencyBuffer = new Float32Array(this.frequencyBufferLength / 2);\n  this.timeBuffer = new Float32Array(this.frequencyBufferLength);\n\n  this.sendingAudioData = false;\n\n  this.lastNoteEnergy = 0;\n  this.wave_power_threshold = 0.006;\n  this.last_note_time = -1;\n\n  var audioWaveChart = new AudioWaveChart();\n\n  var string;\n  var that = this;\n\n  that.requestUserMedia = function () {\n    navigator.getUserMedia({audio: true}, (stream) => {\n      that.sendingAudioData = true;\n      that.stream = stream;\n      that.microphone = that.audioContext.createMediaStreamSource(stream);\n      that.microphone.connect(that.analyser);\n      that.analyser.connect(that.gainNode);\n      that.gainNode.connect(that.audioContext.destination);\n\n      requestAnimationFrame(that.dispatchAudioData);\n\n      ga(\"send\", \"event\", \"Game\", \"MicEnabled\");\n\n      $(\".allow-mic\").removeClass(\"allow-mic--active\");\n    }, (err) => {\n      ga(\"send\", \"event\", \"Game\", \"MicDisabled\");\n\n      $(document).trigger(\"no_mic\");\n\n      $(\".allow-mic\").removeClass(\"allow-mic--active\");\n\n      console.log('Unable to access the microphone');\n      console.log(err);\n    });\n  }\n\n  this.attached = function() {\n    // Set up the stream kill / setup code for visibility changes.\n    document.addEventListener('visibilitychange', this.onVisibilityChange);\n\n    // Then call it.\n    this.onVisibilityChange();\n  }\n\n  this.detached = function() {\n    this.sendingAudioData = false;\n  }\n\n  this.onVisibilityChange = function() {\n    if (document.hidden) {\n      that.sendingAudioData = false;\n\n      if (that.stream) {\n        // Chrome 47+\n        that.stream.getAudioTracks().forEach((track) => {\n          if ('stop' in track) {\n            track.stop();\n          }\n        });\n\n        // Chrome 46-\n        if ('stop' in that.stream) {\n          that.stream.stop();\n        }\n      }\n\n      that.stream = null;\n    } else {\n      that.requestUserMedia();\n    }\n\n  }\n\n  this.setString = function(string_num){\n    string = gameConfig.strings[string_num];\n  }\n\n  this.findNoteFreq = function(time) {\n    let freq_step = that.audioContext.sampleRate / this.FFTSIZE;\n    let min_freq_ind = Math.round(string.range[0] / freq_step);\n    let max_freq_ind = Math.round(string.range[1] / freq_step);\n\n    // Fill up the data.\n\n    that.analyser.getFloatTimeDomainData(that.timeBuffer);\n    that.analyser.getFloatFrequencyData(that.frequencyBuffer);\n    freq = that.frequencyBuffer;\n    wave = that.timeBuffer;\n\n    audioWaveChart.plotWave(wave);\n\n    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++) {\n      freq[d] = Math.pow(10, 5 + freq[d] / 10);\n    }\n\n\n    let max_A = -1000000;\n    let arg_max = -1;\n    for (let i = min_freq_ind; i < max_freq_ind; i++) {\n      if (freq[i] > max_A){\n        max_A = freq[i];\n        arg_max = i;\n      }\n    }\n\n    let total_energy = 0;\n    for (let i = min_freq_ind; i < max_freq_ind; i++) {\n      total_energy += freq[i];\n    }\n\n    let maximum_energy = 0;\n    for (let i = Math.round(arg_max - 20 / freq_step - 1); i <= Math.round(arg_max + 20 / freq_step + 1); i++){\n      maximum_energy += freq[i];\n    }\n\n    if (maximum_energy < 0.1 || maximum_energy / total_energy < 0.96){\n      return -1;\n    }\n\n    if (time > this.last_note_time + 100){\n      this.lastNoteEnergy = 0;\n    }\n\n    // if (maximum_energy < this.lastNoteEnergy){\n    //   return -1;\n    // }\n    // console.log(arg_max * freq_step, maximum_energy);\n\n    this.last_note_time = time;\n    this.lastNoteEnergy = maximum_energy;\n\n    return arg_max * freq_step;\n  }\n\n\n  this.dispatchAudioData = function(time) {\n\n    if (that.sendingAudioData) {\n      requestAnimationFrame(that.dispatchAudioData);\n    }\n\n    let frequency = that.findNoteFreq(time);\n    if (frequency < 0){\n      return;\n    }\n\n    let freqs = string.freqs;\n\n    let min_freq_error = 10000;\n    let best_chord_ind = 0;\n    for (let i = 0; i < freqs.length; i++){\n      let chord_freq = freqs[i][0];\n      let chord = freqs[i][1];\n\n      //let n_div = frequency / chord_freq;   //for future\n      let n_div = 1;\n      let error = Math.abs( Math.round(n_div) * chord_freq - frequency);\n      if (error < min_freq_error){\n        best_chord_ind = i;\n        min_freq_error = error;\n      }\n    }\n\n    if (min_freq_error < 20){\n      $(document).trigger(\"note_detected\", freqs[best_chord_ind][1]);\n    }\n\n  }\n}\n"
  },
  {
    "path": "js/audio_wave.js",
    "content": "function AudioWaveChart() {\n    var $audioWave = $(\".audio-wave\");\n\n    var w = $audioWave.width();\n    var h = $audioWave.height();\n\n    var x_scale = d3.scaleLinear().range([0, w]).domain([0, 1]);\n    var y_scale = d3.scaleLinear().range([h, 0]).domain([0, 1]);\n\n    var line = d3.line()\n            .x(function(d) {\n                return x_scale(d.x);})\n            .y(function(d) {\n                return y_scale(d.y);\n            })\n\n    var graph = d3.select(\".audio-wave\").append(\"svg:svg\")\n                  .attr(\"width\", w)\n                  .attr(\"height\", h)\n                  .append(\"svg:g\");\n\n    graph.append(\"svg:path\").attr(\"class\", \"line\");\n\n    var setDomain = function(data_xy){\n        x_scale.domain(d3.extent(data_xy, function(d){ return d.x}));\n\n        var y_range = d3.extent(data_xy, function(d){ return d.y});\n\n        y_scale.domain([ Math.min(-0.1, y_range[0]), Math.max(0.1, y_range[1]) ]);\n    };\n\n    var plotD3Wave = function(data_xy) {\n        setDomain(data_xy);\n        var svg = d3.select(\"body\").transition();\n        svg.select(\".line\")\n            .duration(0)\n            .attr(\"d\", line(data_xy));\n    }\n\n    return {\n        plotWave: function(wave) {\n            let found_good_ind = 0;\n            for (let i = 0; i < wave.length - 1; i++){\n              if (wave[i] < 0 && wave[i + 1] >= 0){\n                found_good_ind = i;\n                break;\n              }\n            }\n            found_good_ind = Math.min(found_good_ind, wave.length - 500);\n\n            wave_short = wave.slice(found_good_ind, found_good_ind + 500);\n            data_xy  = [];\n            for (let i = 0; i < wave_short.length; i++){\n              data_xy.push({x: i, y: wave_short[i]});\n            }\n\n            plotD3Wave(data_xy);\n        }\n    }\n}\n"
  },
  {
    "path": "js/config.js",
    "content": "var gameConfig = {\n  fps: 50,\n  colors: {\n    green: \"#9BC53D\",\n    yellow: \"#FDE74C\",\n    red: \"#E55934\",\n    white: \"#F1FAEE\",\n    dark_blue: \"#1D3557\"\n  },\n  strings: {\n    1: {\n      name: \"1. E-string (thinnest)\",\n      range: [325, 665],\n      freqs: [\n        [329.6, \"E\"],\n        [349.2, \"F\"],\n        [370.0, \"F#\"],\n        [392.0, \"G\"],\n        [415.3, \"G#\"],\n        [440.0, \"A\"],\n        [466.1, \"A#\"],\n        [493.8, \"B\"],\n        [523.2, \"C\"],\n        [554.3, \"C#\"],\n        [587.3, \"D\"],\n        [622.2, \"D#\"],\n        [659.2, \"E\"],\n      ]\n    },\n    2: {\n      name: \"2. B-string\",\n      range: [242, 499],\n      freqs: [\n        [246.9, \"B\"],\n        [261.6, \"C\"],\n        [277.2, \"C#\"],\n        [293.7, \"D\"],\n        [311.1, \"D#\"],\n        [329.6, \"E\"],\n        [349.2, \"F\"],\n        [370.0, \"F#\"],\n        [392.0, \"G\"],\n        [415.3, \"G#\"],\n        [440.0, \"A\"],\n        [466.2, \"A#\"],\n        [493.9, \"B\"]\n      ]\n    },\n    3: {\n      name: \"3. G-string\",\n      range: [191, 499],\n      freqs: [\n        [196.0, \"G\"],\n        [207.7, \"G#\"],\n        [220.0, \"A\"],\n        [233.1, \"A#\"],\n        [246.9, \"B\"],\n        [261.6, \"C\"],\n        [277.2, \"C#\"],\n        [293.7, \"D\"],\n        [311.1, \"D#\"],\n        [329.6, \"E\"],\n        [349.2, \"F\"],\n        [370.0, \"F#\"],\n        [392.0, \"G\"]\n      ]\n    },\n    4: {\n      name: \"4. D-string\",\n      range: [142, 289],\n      freqs: [\n        [146.8, \"D\"],\n        [155.6, \"D#\"],\n        [164.8, \"E\"],\n        [174.6, \"F\"],\n        [185.0, \"F#\"],\n        [196.0, \"G\"],\n        [207.7, \"G#\"],\n        [220.0, \"A\"],\n        [233.1, \"A#\"],\n        [246.9, \"B\"],\n        [261.6, \"C\"],\n        [277.2, \"C#\"],\n        [293.7, \"D\"]\n      ]\n    },\n    5: {\n      name: \"5. A-string\",\n      range: [105, 215],\n      freqs: [\n        [110.0, \"A\"],\n        [116.5, \"A#\"],\n        [123.5, \"B\"],\n        [130.8, \"C\"],\n        [138.6, \"C#\"],\n        [146.8, \"D\"],\n        [155.6, \"D#\"],\n        [164.8, \"E\"],\n        [174.6, \"F\"],\n        [185.0, \"F#\"],\n        [196.0, \"G\"],\n        [207.7, \"G#\"],\n        [220.0, \"A\"]\n      ]\n    },\n    6: {\n      name: \"6. E-string (thickest)\",\n      range: [75, 170],\n      freqs: [\n        [82.4, \"E\"],\n        [87.3, \"F\"],\n        [92.5, \"F#\"],\n        [98.0, \"G\"],\n        [103.8, \"G#\"],\n        [110.0, \"A\"],\n        [116.5, \"A#\"],\n        [123.5, \"B\"],\n        [130.8, \"C\"],\n        [138.6, \"C#\"],\n        [146.8, \"D\"],\n        [155.6, \"D#\"],\n        [164.8, \"E\"],\n      ]\n    },\n    7: {\n      name: \"7. For those who do the metal...\",\n      range: [55, 130],\n      freqs: [\n        [61.7, \"B\"],\n        [65.4, \"C\"],\n        [69.3, \"C#\"],\n        [73.4, \"D\"],\n        [77.8, \"D#\"],\n        [82.4, \"E\"],\n        [87.3, \"F\"],\n        [92.5, \"F#\"],\n        [98.0, \"G\"],\n        [103.8, \"G#\"],\n        [110.0, \"A\"],\n        [116.5, \"A#\"],\n        [123.5, \"B\"],\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "js/explosion_effect.js",
    "content": "// https://stackoverflow.com/questions/43498923/html5-canvas-particle-explosion\nfunction ExplosionEffect(ctx) {\n  const particlesPerExplosion = 25;\n  const particlesMinSpeed     = 5;\n  const particlesMaxSpeed     = 10;\n  const particlesMinSize      = 2;\n  const particlesMaxSize      = 4;\n  var   explosions            = [];\n\n  function particle(x, y, correctAnswer) {\n    this.x    = x;\n    this.y    = y;\n    this.xv   = randInt(particlesMinSpeed, particlesMaxSpeed, false);\n    this.yv   = randInt(particlesMinSpeed, particlesMaxSpeed, false);\n    this.size = randInt(particlesMinSize, particlesMaxSize, true);\n    this.color = correctAnswer ? gameConfig.colors.green : gameConfig.colors.red;\n  }\n\n  function explosion(x, y, correctAnswer) {\n    this.particles = [];\n\n    for (let i = 0; i < particlesPerExplosion; i++) {\n      this.particles.push(\n        new particle(x, y, correctAnswer)\n      );\n    }\n  }\n\n  return {\n    draw: function() {\n      if (explosions.length === 0) {\n        return;\n      }\n\n      for (let i = 0; i < explosions.length; i++) {\n\n        const explosion = explosions[i];\n        const particles = explosion.particles;\n\n        if (particles.length === 0) {\n          explosions.splice(i, 1);\n          return;\n        }\n\n        const particlesAfterRemoval = particles.slice();\n        for (let ii = 0; ii < particles.length; ii++) {\n\n          const particle = particles[ii];\n\n          // Check particle size\n          // If 0, remove\n          if (particle.size <= 0) {\n            particlesAfterRemoval.splice(ii, 1);\n            continue;\n          }\n\n          ctx.beginPath();\n          ctx.arc(particle.x, particle.y, particle.size, Math.PI * 2, 0, false);\n          ctx.closePath();\n          ctx.fillStyle = particle.color;\n          ctx.fill();\n\n          // Update\n          particle.x += particle.xv;\n          particle.y += particle.yv;\n          particle.size -= .1;\n        }\n\n        explosion.particles = particlesAfterRemoval;\n      }\n    },\n    add: function(x, y, correctAnswer) {\n      explosions.push(\n        new explosion(x, y, correctAnswer)\n      );\n    }\n  }\n}\n"
  },
  {
    "path": "js/fretboard.js",
    "content": "function Fretboard(canvas, songLoader, string, rockWidth, pegWidth) {\n  var ctx = canvas.getContext(\"2d\"),\n      blockHeight = rockWidth,\n      block = {\n        x: 0,\n        y: canvas.height - blockHeight,\n        width: canvas.width,\n        height: blockHeight\n      };\n\n  var highlightedFret,\n      highlightedColor = gameConfig.colors.yellow;\n\n  function drawCircle(x, y) {\n    var circleSize = (blockHeight / 6 - 1) / 2;\n\n    ctx.fillStyle = gameConfig.colors.white;\n    ctx.beginPath();\n    ctx.arc(x, y, circleSize, 0, 2 * Math.PI);\n    ctx.fill();\n  }\n\n  function drawLine(x, y, x1, y1) {\n    ctx.beginPath();\n    ctx.moveTo(x, y);\n    ctx.lineTo(x1, y1);\n    ctx.stroke();\n  }\n\n  return {\n    draw: function() {\n      ctx.strokeStyle = gameConfig.colors.white;\n      ctx.lineWidth = 1;\n      for(var i = 1; i < gameConfig.strings[string].notes.length; i++) {\n        var x = i * rockWidth + pegWidth;\n        drawLine(x, block.y, x, canvas.height);\n      }\n      drawLine(0, block.y, canvas.width, block.y);\n\n      // draw single circles\n      var circleFrets = [2, 4, 6, 8];\n      var cirlceColor = gameConfig.colors.white;\n      var verticalMiddle = canvas.height - blockHeight / 2;\n      var circleSize = (blockHeight / 6 - 1) / 2;\n\n      circleFrets.forEach(function(fret) {\n        drawCircle((rockWidth * fret - 1) + rockWidth / 2 + pegWidth, verticalMiddle);\n      });\n\n      // draw double circles\n      var doubleCirclesFret = 12;\n      drawCircle((rockWidth * 11) + rockWidth / 2 + pegWidth, canvas.height - circleSize * 2.5);\n      drawCircle((rockWidth * 11) + rockWidth / 2 + pegWidth, canvas.height - block.height + 2.5 * circleSize);\n\n      if(typeof(highlightedFret) === \"number\") {\n        ctx.fillStyle = highlightedColor;\n        ctx.fillRect(highlightedFret * rockWidth + pegWidth, block.y, rockWidth, rockWidth);\n      }\n    },\n    highlightFret: function(note, color) {\n      var fretIndex = songLoader.findNoteIndex(note, string);\n\n      highlightedFret = fretIndex;\n      highlightedColor = color ? color : gameConfig.colors.yellow;\n\n      setTimeout(function() {\n        highlightedFret = undefined;\n      }, 100);\n    }\n  }\n}\n"
  },
  {
    "path": "js/health_drawer.js",
    "content": "function HealthDrawer(ctx) {\n  var heartWidth = 40;\n  var heartHeight = 25;\n  var c1 = 2;\n  var c2 = 2;\n\n  function drawBezierCurve(x0, y0, x1, y1, x2, y2, x3, y3) {\n    ctx.moveTo(x0, y0);\n    ctx.bezierCurveTo(x1, y1, x2, y2, x3, y3);\n  }\n\n  return {\n    draw: function(health, isSandboxMode) {\n      if(isSandboxMode) {\n        return;\n      }\n\n      var prevFillStyle = ctx.fillStyle;\n      var prevStrokeStyle = ctx.strojeStyle;\n\n      ctx.fillStyle = gameConfig.colors.red;\n      ctx.strokeStyle = gameConfig.colors.red;\n\n      for(var i = 0; i < health; i++) {\n        var x = ctx.canvas.width - heartWidth - i * (heartWidth + 10);\n        var y = 20;\n\n        ctx.beginPath();\n        drawBezierCurve(x, y, x, y - heartHeight / 2, x - heartWidth / 2, y - heartHeight / 2, x - heartWidth / 2, y);\n        drawBezierCurve(x - heartWidth / 2, y, x - heartWidth / 2, y + heartHeight / 2, x, y + heartHeight / 2 * c1, x, y + heartHeight / 2 * c2);\n        drawBezierCurve(x, y + heartHeight / 2 * c2, x, y + heartHeight / 2 * c1, x + heartWidth / 2, y + heartHeight / 2, x + heartWidth / 2, y);\n        drawBezierCurve(x + heartWidth / 2, y, x + heartWidth / 2, y - heartHeight / 2, x, y - heartHeight / 2, x, y);\n        ctx.closePath();\n        ctx.fill();\n\n        ctx.beginPath();\n        ctx.moveTo(x - heartWidth / 2, y);\n        ctx.lineTo(x + heartWidth / 2, y);\n        ctx.lineTo(x, y + heartHeight / 2 * c2);\n        ctx.closePath();\n        ctx.stroke();\n        ctx.fill()\n      }\n\n      ctx.fillStyle = prevFillStyle;\n      ctx.strokeStyle = prevStrokeStyle;\n    }\n  }\n}\n"
  },
  {
    "path": "js/helper_functions.js",
    "content": "function randInt(min, max, positive) {\n  let num;\n  if (positive === false) {\n    num = Math.floor(Math.random() * max) - min;\n    num *= Math.floor(Math.random() * 2) === 1 ? 1 : -1;\n  } else {\n    num = Math.floor(Math.random() * max) + min;\n  }\n\n  return num;\n}\n\nfunction pickRandom(array) {\n  return array[Math.floor(Math.random() * array.length)];\n}\n\nfunction getChromeVersion() {\n  var raw = navigator.userAgent.match(/Chrom(e|ium)\\/([0-9]+)\\./);\n\n  return raw ? parseInt(raw[2], 10) : false;\n}\n\nfunction randomArray(length, max) {\n  return Array.apply(null, Array(length)).map(function() {\n    return Math.round(Math.random() * max);\n  });\n}\n\n$.fn.customerPopup = function (e, intWidth, intHeight, blnResize) {\n  // Prevent default anchor event\n  e.preventDefault();\n\n  // Set values for window\n  intWidth = intWidth || '500';\n  intHeight = intHeight || '400';\n  strResize = (blnResize ? 'yes' : 'no');\n\n  // Set title and open popup with focus on it\n  var strTitle = ((typeof this.attr('title') !== 'undefined') ? this.attr('title') : 'Social Share'),\n      strParam = 'width=' + intWidth + ',height=' + intHeight + ',resizable=' + strResize,\n      objWindow = window.open(this.attr('href'), strTitle, strParam).focus();\n}\n"
  },
  {
    "path": "js/sharing.js",
    "content": "$(document).on(\"click\", \".share-button\", function(e) {\n  var $this = $(this);\n\n  ga(\"send\", \"event\", \"Game\", \"Share\", $this.data(\"eventLabel\"));\n\n  $this.customerPopup(e);\n});\n"
  },
  {
    "path": "js/song_loader.js",
    "content": "function SongLoader() {\n  const randomSongLength = 10;\n\n  Object.keys(gameConfig.strings).forEach(function(string) {\n    var rows = gameConfig.strings[string].freqs.slice(1, 13);\n    var notes = rows.map(function(row) {\n      var note = row[1];\n\n      return note;\n    })\n\n    gameConfig.strings[string].notes = notes;\n  });\n\n  const songs = {\n    \"Random notes\": randomArray(20, 11).join(\"--------\"),\n    \"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\",\n    \"Guess what\": \"0--3--5---0--3--6--5---0--3--5---3--0\",\n\n    \"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]],\n\n    \"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]],\n\n    \"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]],\n\n    \"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]],\n\n    \"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]],\n\n\n    \"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]],\n\n\n    \"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]],\n\n\n    \"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]],\n\n    \"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]]\n  }\n\n  function parseSong(encodedSong, string) {\n    let song = [];\n    let duration = 0;\n    let last_note;\n    for (let i = 0; i < encodedSong.length; i++){\n      if (encodedSong[i] != \"-\"){\n        if (duration > 0){\n          song.push([last_note, 8 / duration]);\n        }\n\n        let fret = parseInt(encodedSong[i]);\n        last_note = fret === 0 ? \"E\" : gameConfig.strings[string].notes[fret - 1];\n        duration = 0;\n      } else {\n        duration += 1;\n      }\n    }\n    song.push([last_note, 8/8.0]);\n    return song;\n  }\n\n\n  return {\n    loadSong: function(songIndex, string) {\n      var encodedSong = songs[songIndex];\n      if (encodedSong.constructor === Array){\n        return encodedSong;\n      }\n      return parseSong(encodedSong, string);\n    },\n    findNoteIndex: function(note, string) {\n      return gameConfig.strings[string].notes.findIndex(function(n) {\n        return note === n;\n      });\n    },\n    populateSelectMenu: function($songSelect) {\n      for(song in songs) {\n        var $option = $(\"<option/>\");\n        $option.val(song);\n        $option.text(song);\n\n        if(song === \"Random\") {\n          $option.attr(\"selected\", \"selected\");\n        }\n\n        $songSelect.append($option);\n      }\n    }\n  }\n}\n"
  }
]