Przeglądaj źródła

Merge branch 'master' of https://github.com/AkiraLaine/music-app

unknown 9 lat temu
rodzic
commit
a59b449ebe
5 zmienionych plików z 568 dodań i 125 usunięć
  1. 7 16
      app/app.css
  2. 84 31
      app/app.js
  3. 109 73
      app/templates/admin.html
  4. 1 1
      app/templates/playlist.html
  5. 367 4
      app/templates/room.html

+ 7 - 16
app/app.css

@@ -551,18 +551,6 @@ footer a:hover{
   clear: both;
   background-color: rgba(16, 140, 146, 0.8);
 }
-.privacy {
-  margin: 0 auto;
-  padding: 100px 0;
-  height: 900px;
-  text-align: center;
-}
-.terms {
-  margin: 0 auto;
-  padding: 100px 0;
-  height: 900px;
-  text-align: center;
-}
 .song-input{
   -webkit-appearance: none;
      -moz-appearance: none;
@@ -666,7 +654,6 @@ footer a:hover{
 }
 .hidden {
   visibility: hidden;
-  height: 0;
 }
 .footerButtons {
   background:none!important;
@@ -764,7 +751,7 @@ footer a:hover{
 #s3 {
   opacity: 0.33333333333333;
 }
-#add-song-button{
+#add-song-button, #find-img-button, #save-song-button {
   display: block;
   margin: 0 auto;
 }
@@ -852,7 +839,7 @@ footer a:hover{
 }
 
 #volume-container {
-  width: 160px; /*12.5 px each side*/
+  width: 172px; /*12.5 px each side*/
   float: right;
   height: 100%;
 }
@@ -862,6 +849,7 @@ footer a:hover{
   padding-right: 12px;
   padding-left: 12px;
   margin-left: 12px;
+  float: left;
 }
 
 #volume-container-admin {
@@ -878,7 +866,10 @@ footer a:hover{
 }
 
 #volume-icon{
-  margin-left: 320px !important;
+  /*margin-left: 320px !important;*/
+  margin: 0 !important;
+  float: left;
+  margin-top: 7px !important;
 }
 
 #settings{

+ 84 - 31
app/app.js

@@ -13,6 +13,7 @@ if (Meteor.isClient) {
 
     Meteor.subscribe("queues");
 
+    var minterval;
     var hpSound = undefined;
     var songsArr = [];
     var ytArr = [];
@@ -21,10 +22,15 @@ if (Meteor.isClient) {
     var id = parts.pop();
     var type = id.toLowerCase();
 
-    function getSpotifyInfo(title, cb) {
+    function getSpotifyInfo(title, cb, artist) {
+        var q = "";
+        q = title;
+        if (artist !== undefined) {
+            q += " artist:" + artist;
+        }
         $.ajax({
             type: "GET",
-            url: 'https://api.spotify.com/v1/search?q=' + encodeURIComponent(title.toLowerCase()) + '&type=track',
+            url: 'https://api.spotify.com/v1/search?q=' + encodeURIComponent(q) + '&type=track',
             applicationType: "application/json",
             contentType: "json",
             success: function (data) {
@@ -190,6 +196,9 @@ if (Meteor.isClient) {
 
     Template.dashboard.onCreated(function() {
         if (_sound !== undefined) _sound.stop();
+        if (minterval !== undefined) {
+            Meteor.clearInterval(minterval);
+        }
         Meteor.subscribe("history");
     });
 
@@ -211,16 +220,14 @@ if (Meteor.isClient) {
         },
         "click #toggle-video": function(e){
             e.preventDefault();
-            if (Session.get("videoHidden")) {
-                $("#player").removeClass("hidden");
+            if (Session.get("mediaHidden")) {
+                $("#media-container").removeClass("hidden");
                 $("#toggle-video").text("Hide video");
-                var player = document.getElementById("player");
-                player.style.height = (player.offsetWidth / 16 * 9) + "px";
-                Session.set("videoHidden", false);
+                Session.set("mediaHidden", false);
             } else {
-                $("#player").addClass("hidden");
+                $("#media-container").addClass("hidden");
                 $("#toggle-video").text("Show video");
-                Session.set("videoHidden", true);
+                Session.set("mediaHidden", true);
             }
         },
         "click #return": function(e){
@@ -332,10 +339,6 @@ if (Meteor.isClient) {
     });
 
     Template.room.onRendered(function() {
-        $(window).resize(function() {
-            var player = document.getElementById("player");
-            player.style.height = (player.offsetWidth / 16 * 9) + "px";
-        });
         $(document).ready(function() {
             function makeSlider(){
                 var slider = $("#volume-slider").slider();
@@ -413,6 +416,16 @@ if (Meteor.isClient) {
         "click .preview-button": function(e){
             Session.set("song", this);
         },
+        "click .edit-button": function(e){
+            Session.set("song", this);
+            Session.set("genre", $(e.toElement).data("genre"));
+            $("#type").val(this.type);
+            $("#artist").val(this.artist);
+            $("#title").val(this.title);
+            $("#img").val(this.img);
+            $("#id").val(this.id);
+            $("#duration").val(this.duration);
+        },
         "click #add-song-button": function(e){
             var genre = $(e.toElement).data("genre") || $(e.toElement).parent().data("genre");
             Meteor.call("addSongToPlaylist", genre, this);
@@ -485,6 +498,24 @@ if (Meteor.isClient) {
                     window.location = "/" + $("#croom").val();
                 }
             });
+        },
+        "click #find-img-button": function() {
+            getSpotifyInfo($("#title").val().replace(/\[.*\]/g, ""), function(data) {
+                if (data.tracks.items.length > 0) {
+                    $("#img").val(data.tracks.items[0].album.images[1].url);
+                }
+            }, $("#artist").val());
+        },
+        "click #save-song-button": function() {
+            var newSong = {};
+            newSong.title = $("#title").val();
+            newSong.artist = $("#artist").val();
+            newSong.img = $("#img").val();
+            newSong.type = $("#type").val();
+            newSong.duration = $("#duration").val();
+            Meteor.call("updateQueueSong", Session.get("genre"), Session.get("song"), newSong, function() {
+                $('#editModal').modal('hide');
+            });
         }
     });
 
@@ -540,6 +571,14 @@ if (Meteor.isClient) {
         playlist_songs: function() {
             var data = Playlists.find({type: type}).fetch();
             if (data !== undefined && data.length > 0) {
+                data[0].songs.map(function(song) {
+                    if (song.title === Session.get("title")) {
+                        song.current = true;
+                    } else {
+                        song.current = false;
+                    }
+                    return song;
+                });
                 return data[0].songs;
             } else {
                 return [];
@@ -592,25 +631,30 @@ if (Meteor.isClient) {
 
                 var volume = localStorage.getItem("volume") || 20;
 
+                $("#media-container").empty();
+                yt_player = undefined;
                 if (currentSong.type === "SoundCloud") {
-                  $("#player").attr("src", "")
-                  getSongInfo(currentSong);
-                  SC.stream("/tracks/" + currentSong.id + "#t=20s", function(sound){
-                    _sound = sound;
-                    sound.setVolume(volume / 100);
-                    sound.play();
-                    var interval = setInterval(function() {
-                        if (sound.getState() === "playing") {
-                            sound.seek(getTimeElapsed());
-                            window.clearInterval(interval);
-                        }
-                    }, 200);
-                    // Session.set("title", currentSong.title || "Title");
-                    // Session.set("artist", currentSong.artist || "Artist");
-                    Session.set("duration", currentSong.duration);
-                    resizeSeekerbar();
-                  });
+                    // Change id from visualizer to media-container
+                    $("#player").attr("src", "");
+                    getSongInfo(currentSong);
+                    SC.stream("/tracks/" + currentSong.id, function(sound){
+                        _sound = sound;
+                        sound.setVolume(volume / 100);
+                        startVisualizer(sound._player._html5Audio);
+                        sound.play();
+                        var interval = setInterval(function() {
+                            if (sound.getState() === "playing") {
+                                sound.seek(getTimeElapsed());
+                                window.clearInterval(interval);
+                            }
+                        }, 200);
+                        // Session.set("title", currentSong.title || "Title");
+                        // Session.set("artist", currentSong.artist || "Artist");
+                        Session.set("duration", currentSong.duration);
+                        resizeSeekerbar();
+                    });
                 } else {
+                    $("#media-container").append('<div id="player" class="embed-responsive-item"></div>');
                     if (yt_player === undefined) {
                         yt_player = new YT.Player("player", {
                             height: 540,
@@ -655,7 +699,7 @@ if (Meteor.isClient) {
                 window.location = "/";
             } else {
                 Session.set("loaded", true);
-                Meteor.setInterval(function () {
+                minterval = Meteor.setInterval(function () {
                     var data = undefined;
                     var dataCursorH = History.find({type: type});
                     var dataCursorP = Playlists.find({type: type});
@@ -928,6 +972,11 @@ if (Meteor.isServer) {
                 throw new Meteor.error(403, "Invalid genre.");
             }
         },
+        updateQueueSong: function(genre, oldSong, newSong) {
+            newSong.id = oldSong.id;
+            Queues.update({type: genre, "songs": oldSong}, {$set: {"songs.$": newSong}});
+            return true;
+        },
         removeSongFromQueue: function(type, songId) {
             type = type.toLowerCase();
             Queues.update({type: type}, {$pull: {songs: {id: songId}}});
@@ -1075,6 +1124,10 @@ Router.route("/admin", {
     }
 });
 
+Router.route("/vis", {
+    template: "visualizer"
+});
+
 Router.route("/:type", {
     template: "room"
 });

+ 109 - 73
app/templates/admin.html

@@ -1,92 +1,128 @@
 <template name="admin">
     <div class="landing">
-      {{> header}}
-      <div class="row">
-        {{#each queues}}
-            <div class="col-md-8 col-md-offset-2 admin-queue-panel">
-                <div class="panel panel-primary">
-                    <div class="panel-heading">
-                        <h3 class="panel-title">{{type}} review queue</h3>
-                    </div>
-                    <div class="panel-body">
-                        <table class="table table-striped">
-                            <thead>
-                                <tr>
-                                    <th>Title</th>
-                                    <th>Artist(s)</th>
-                                    <th>Type</th>
-                                    <th>Id</th>
-                                    <th>Img</th>
-                                    <th>Preview</th>
-                                    <th>Add</th>
-                                    <th colspan="10">Remove</th>
-                                </tr>
-                            </thead>
-                            <tbody>
-                                {{#each songs}}
+        {{> header}}
+        <div class="row">
+            {{#each queues}}
+                <div class="col-md-8 col-md-offset-2 admin-queue-panel">
+                    <div class="panel panel-primary">
+                        <div class="panel-heading">
+                            <h3 class="panel-title">{{type}} review queue</h3>
+                        </div>
+                        <div class="panel-body">
+                            <table class="table table-striped">
+                                <thead>
                                     <tr>
-                                        <th scope="row">{{title}}</th>
-                                        <td>{{artist}}</td>
-                                        <td>{{type}}</td>
-                                        <td>{{id}}</td>
-                                        <td class="column-small"><a href="{{img}}" target="_blank"><button class="btn btn-primary preview-button">Preview Image</button></a></td>
-                                        <td class="column-small"><button class="btn btn-primary preview-button" data-toggle="modal" data-target="#previewModal">Preview</button></td>
-                                        <td class="column-small"><button class="btn btn-success" id="add-song-button" data-genre="{{../type}}"><i class="fa fa-check-circle"></i></button></td>
-                                        <td class="column-small"><button class="btn btn-danger" id="deny-song-button" data-genre="{{../type}}"><i class="fa fa-ban"></i></button></td>
+                                        <th>Title</th>
+                                        <th>Artist(s)</th>
+                                        <th>Type</th>
+                                        <th>Id</th>
+                                        <th>Img</th>
+                                        <th>Preview</th>
+                                        <th>Edit</th>
+                                        <th>Add</th>
+                                        <th colspan="10">Remove</th>
                                     </tr>
-                                {{/each}}
-                            </tbody>
-                        </table>
+                                </thead>
+                                <tbody>
+                                    {{#each songs}}
+                                        <tr>
+                                            <th scope="row">{{title}}</th>
+                                            <td>{{artist}}</td>
+                                            <td>{{type}}</td>
+                                            <td>{{id}}</td>
+                                            <td class="column-small"><a href="{{img}}" target="_blank"><button class="btn btn-primary preview-button">Preview Image</button></a></td>
+                                            <td class="column-small"><button class="btn btn-primary preview-button" data-toggle="modal" data-target="#previewModal">Preview</button></td>
+                                            <td class="column-small"><button class="btn btn-primary edit-button" data-genre="{{../type}}" data-toggle="modal" data-target="#editModal">Edit</button></td>
+                                            <td class="column-small"><button class="btn btn-success" id="add-song-button" data-genre="{{../type}}"><i class="fa fa-check-circle"></i></button></td>
+                                            <td class="column-small"><button class="btn btn-danger" id="deny-song-button" data-genre="{{../type}}"><i class="fa fa-ban"></i></button></td>
+                                        </tr>
+                                    {{/each}}
+                                </tbody>
+                            </table>
+                        </div>
                     </div>
                 </div>
-            </div>
-        {{/each}}
+            {{/each}}
 
-        <div class="col-md-4 col-md-offset-4">
-            <div id="croom_container">
-                <label for="croom" id="croom_label">Room Name:</label>
-                <div class="input-group">
-                    <input type="text" id="croom" name="croom" required />
+            <div class="col-md-4 col-md-offset-4">
+                <div id="croom_container">
+                    <label for="croom" id="croom_label">Room Name:</label>
+                    <div class="input-group">
+                        <input type="text" id="croom" name="croom" required />
+                    </div>
+                    <button class="btn btn-warning btn-block" id="croom_create">Create</button>
                 </div>
-                <button class="btn btn-warning btn-block" id="croom_create">Create</button>
             </div>
-        </div>
 
-        <div id="previewModal" class="modal fade" role="dialog">
-            <div class="modal-dialog">
-                <!-- Modal content-->
-                <div class="modal-content">
-                    <div class="modal-header">
-                        <button type="button" class="close" data-dismiss="modal">&times;</button>
-                        <h4 class="modal-title">Preview</h4>
-                    </div>
-                    <div class="modal-body">
-                        <div width="960" height="540" id="previewPlayer"></div>
-                        <button id="play" class="btn btn-success"><i class="fa fa-play"></i></button>
-                        <button id="stop" class="btn btn-danger" disabled><i class="fa fa-stop"></i></button>
-                        <div id="volume-container-admin">
-                            <input type="text" id="volume-slider" class="span2" value="" data-slider-min="0" data-slider-max="100" data-slider-step="1" data-slider-value="50" data-slider-orientation="horizontal" data-slider-selection="after" data-slider-tooltip="hide">
+            <div id="previewModal" class="modal fade" role="dialog">
+                <div class="modal-dialog">
+                    <!-- Modal content-->
+                    <div class="modal-content">
+                        <div class="modal-header">
+                            <button type="button" class="close" data-dismiss="modal">&times;</button>
+                            <h4 class="modal-title">Preview</h4>
+                        </div>
+                        <div class="modal-body">
+                            <div width="960" height="540" id="previewPlayer"></div>
+                            <button id="play" class="btn btn-success"><i class="fa fa-play"></i></button>
+                            <button id="stop" class="btn btn-danger" disabled><i class="fa fa-stop"></i></button>
+                            <div id="volume-container-admin">
+                                <input type="text" id="volume-slider" class="span2" value="" data-slider-min="0" data-slider-max="100" data-slider-step="1" data-slider-value="50" data-slider-orientation="horizontal" data-slider-selection="after" data-slider-tooltip="hide">
+                            </div>
+                        </div>
+                        <div class="modal-footer">
+                            <button id="close-modal" type="button" class="btn btn-default" data-dismiss="modal">Close</button>
                         </div>
                     </div>
-                    <div class="modal-footer">
-                        <button id="close-modal" type="button" class="btn btn-default" data-dismiss="modal">Close</button>
+                </div>
+            </div>
+
+            <div id="editModal" class="modal fade" role="dialog">
+                <div class="modal-dialog">
+                    <!-- Modal content-->
+                    <div class="modal-content">
+                        <div class="modal-header">
+                            <button type="button" class="close" data-dismiss="modal">&times;</button>
+                            <h4 class="modal-title">Edit</h4>
+                        </div>
+                        <div class="modal-body">
+                            <label for="type" class="song-input-label">Song Type</label>
+                            <select name="type" id="type" class="song-input-select">
+                                <option name="youtube" id="youtube">YouTube</option>
+                                <option name="soundcloud" id="soundcloud">SoundCloud</option>
+                            </select>
+                            <label for="id" class="song-input-label">Song ID</label>
+                            <input class="song-input" name="id" id="id" type="text" />
+                            <label for="id" class="song-input-label">Song Artist</label>
+                            <input class="song-input" name="artist" id="artist" type="text" />
+                            <label for="title" class="song-input-label">Song Title</label>
+                            <input class="song-input" name="title" id="title" type="text" />
+                            <label for="title" class="song-input-label">Song Duration</label>
+                            <input class="song-input" name="duration" id="duration" type="number" />
+                            <label for="img" class="song-input-label">Song Image</label>
+                            <input class="song-input" name="img" id="img" type="text" />
+                            <button type="button" id="find-img-button" class="button">Find Image</button>
+                            <button type="button" id="save-song-button" class="button">Save Changes</button>
+                        </div>
+                        <div class="modal-footer">
+                            <button id="close-modal" type="button" class="btn btn-default" data-dismiss="modal">Close</button>
+                        </div>
                     </div>
                 </div>
             </div>
-        </div>
 
-        <ul class="bg-bubbles">
-            <li></li>
-            <li></li>
-            <li></li>
-            <li></li>
-            <li></li>
-            <li></li>
-            <li></li>
-            <li></li>
-            <li></li>
-            <li></li>
-        </ul>
+            <ul class="bg-bubbles">
+                <li></li>
+                <li></li>
+                <li></li>
+                <li></li>
+                <li></li>
+                <li></li>
+                <li></li>
+                <li></li>
+                <li></li>
+                <li></li>
+            </ul>
     </div>
   </div>
 </template>

+ 1 - 1
app/templates/playlist.html

@@ -3,7 +3,7 @@
   <ul id="playlist">
       {{#each playlist_songs}}
         <li>
-            <strong>{{title}}</strong> - {{artist}}
+            {{#if current}}<i class="fa fa-arrow-right" style="color: red;"></i>{{/if}} <strong>{{title}}</strong> - {{artist}}
         </li>
       {{/each}}
   </ul>

+ 367 - 4
app/templates/room.html

@@ -6,8 +6,8 @@
               <nav>
                   <a class="back" href="/"><i class="fa fa-chevron-left"></i></a>
                   <h3>{{{type}}}</h3>
-                  <i class="fa fa-volume-off" id="volume-icon"></i>
                   <div id="volume-container">
+                      <i class="fa fa-volume-off" id="volume-icon"></i>
                       <input type="text" id="volume-slider" class="span2" value="" data-slider-min="0" data-slider-max="100" data-slider-step="1" data-slider-value="50" data-slider-orientation="horizontal" data-slider-selection="after" data-slider-tooltip="hide">
                   </div>
               </nav>
@@ -16,9 +16,9 @@
               </div>
               <div class="row" id="song-media">
                 <div class="col-md-8">
-                  <div class="embed-responsive embed-responsive-16by9">
-                    <div id="player" class="embed-responsive-item"></div>
-                  </div>
+                    <div class="embed-responsive embed-responsive-16by9" id="media-container">
+                        <!--div id="player" class="embed-responsive-item"></div-->
+                    </div>
                 </div>
                 <div class="col-md-4" style="margin-top:15px">
                   <img class="song-img" id="song-img"/>
@@ -155,4 +155,367 @@
             <li></li>
         </ul>
     </div>
+    <script>
+        /**
+         * Created by Michael on 31/12/13.
+         */
+
+        /**
+         * The *AudioSource object creates an analyzer node, sets up a repeating function with setInterval
+         * which samples the input and turns it into an FFT array. The object has two properties:
+         * streamData - this is the Uint8Array containing the FFT data
+         * volume - cumulative value of all the bins of the streaData.
+         *
+         * The MicrophoneAudioSource uses the getUserMedia interface to get real-time data from the user's microphone. Not used currently but included for possible future use.
+         */
+
+        var SoundCloudAudioSource = function(player) {
+            var self = this;
+            var analyser;
+            var audioCtx = new (window.AudioContext || window.webkitAudioContext);
+            analyser = audioCtx.createAnalyser();
+            analyser.fftSize = 256;
+            player.crossOrigin = "anonymous";
+            var source = audioCtx.createMediaElementSource(player);
+            source.connect(analyser);
+            analyser.connect(audioCtx.destination);
+            var sampleAudioStream = function() {
+                analyser.getByteFrequencyData(self.streamData);
+                // calculate an overall volume value
+                var total = 0;
+                for (var i = 0; i < 80; i++) { // get the volume from the first 80 bins, else it gets too loud with treble
+                    total += self.streamData[i];
+                }
+                self.volume = total;
+            };
+            setInterval(sampleAudioStream, 20);
+            // public properties and methods
+            this.volume = 0;
+            this.streamData = new Uint8Array(128);
+        };
+        /**
+         * The Visualizer object, after being instantiated, must be initialized with the init() method,
+         * which takes an options object specifying the element to append the canvases to and the audiosource which will
+         * provide the data to be visualized.
+         */
+        var Visualizer = function() {
+            var tileSize;
+            var tiles = [];
+            var stars = [];
+            // canvas vars
+            var fgCanvas;
+            var fgCtx;
+            var fgRotation = 0.001;
+            var sfCanvas;
+            var sfCtx;
+            var audioSource;
+
+            function Polygon(sides, x, y, tileSize, ctx, num) {
+                this.sides = sides;
+                this.tileSize = tileSize;
+                this.ctx = ctx;
+                this.num = num; // the number of the tile, starting at 0
+                this.high = 0; // the highest colour value, which then fades out
+                this.decay = this.num > 42 ? 1.5 : 2; // increase this value to fade out faster.
+                this.highlight = 0; // for highlighted stroke effect;
+                // figure out the x and y coordinates of the center of the polygon based on the
+                // 60 degree XY axis coordinates passed in
+                var step = Math.round(Math.cos(Math.PI/6)*tileSize*2);
+                this.y = Math.round(step * Math.sin(Math.PI/3) * -y  );
+                this.x = Math.round(x * step + y * step/2 );
+
+                // calculate the vertices of the polygon
+                this.vertices = [];
+                for (var i = 1; i <= this.sides;i += 1) {
+                    x = this.x + this.tileSize * Math.cos(i * 2 * Math.PI / this.sides + Math.PI/6);
+                    y = this.y + this.tileSize * Math.sin(i * 2 * Math.PI / this.sides + Math.PI/6);
+                    this.vertices.push([x, y]);
+                }
+            }
+            Polygon.prototype.rotateVertices = function() {
+                // rotate all the vertices to achieve the overall rotational effect
+                var rotation = fgRotation;
+                rotation -= audioSource.volume > 10000 ? Math.sin(audioSource.volume/800000) : 0;
+                for (var i = 0; i <= this.sides-1;i += 1) {
+                    this.vertices[i][0] = this.vertices[i][0] -  this.vertices[i][1] * Math.sin(rotation);
+                    this.vertices[i][1] = this.vertices[i][1] +  this.vertices[i][0] * Math.sin(rotation);
+                }
+            };
+            var minMental = 0, maxMental = 0;
+            Polygon.prototype.calculateOffset = function(coords) {
+                var angle = Math.atan(coords[1]/coords[0]);
+                var distance = Math.sqrt(Math.pow(coords[0], 2) + Math.pow(coords[1], 2)); // a bit of pythagoras
+                var mentalFactor = Math.min(Math.max((Math.tan(audioSource.volume/6000) * 0.5), -20), 2); // this factor makes the visualization go crazy wild
+                /*
+                 // debug
+                 minMental = mentalFactor < minMental ? mentalFactor : minMental;
+                 maxMental = mentalFactor > maxMental ? mentalFactor : maxMental;*/
+                var offsetFactor = Math.pow(distance/3, 2) * (audioSource.volume/2000000) * (Math.pow(this.high, 1.3)/300) * mentalFactor;
+                var offsetX = Math.cos(angle) * offsetFactor;
+                var offsetY = Math.sin(angle) * offsetFactor;
+                offsetX *= (coords[0] < 0) ? -1 : 1;
+                offsetY *= (coords[0] < 0) ? -1 : 1;
+                return [offsetX, offsetY];
+            };
+            Polygon.prototype.drawPolygon = function() {
+                var bucket = Math.ceil(audioSource.streamData.length/tiles.length*this.num);
+                var val = Math.pow((audioSource.streamData[bucket]/255),2)*255;
+                val *= this.num > 42 ? 1.1 : 1;
+                // establish the value for this tile
+                if (val > this.high) {
+                    this.high = val;
+                } else {
+                    this.high -= this.decay;
+                    val = this.high;
+                }
+
+                // figure out what colour to fill it and then draw the polygon
+                var r, g, b, a;
+                if (val > 0) {
+                    this.ctx.beginPath();
+                    var offset = this.calculateOffset(this.vertices[0]);
+                    this.ctx.moveTo(this.vertices[0][0] + offset[0], this.vertices[0][1] + offset[1]);
+                    // draw the polygon
+                    for (var i = 1; i <= this.sides-1;i += 1) {
+                        offset = this.calculateOffset(this.vertices[i]);
+                        this.ctx.lineTo (this.vertices[i][0] + offset[0], this.vertices[i][1] + offset[1]);
+                    }
+                    this.ctx.closePath();
+
+                    if (val > 128) {
+                        r = (val-128)*2;
+                        g = ((Math.cos((2*val/128*Math.PI/2)- 4*Math.PI/3)+1)*128);
+                        b = (val-105)*3;
+                    }
+                    else if (val > 175) {
+                        r = (val-128)*2;
+                        g = 255;
+                        b = (val-105)*3;
+                    }
+                    else {
+                        r = ((Math.cos((2*val/128*Math.PI/2))+1)*128);
+                        g = ((Math.cos((2*val/128*Math.PI/2)- 4*Math.PI/3)+1)*128);
+                        b = ((Math.cos((2.4*val/128*Math.PI/2)- 2*Math.PI/3)+1)*128);
+                    }
+                    if (val > 210) {
+                        this.cubed = val; // add the cube effect if it's really loud
+                    }
+                    if (val > 120) {
+                        this.highlight = 100; // add the highlight effect if it's pretty loud
+                    }
+                    // set the alpha
+                    var e = 2.7182;
+                    a = (0.5/(1 + 40 * Math.pow(e, -val/8))) + (0.5/(1 + 40 * Math.pow(e, -val/20)));
+
+                    this.ctx.fillStyle = "rgba(" +
+                            Math.round(r) + ", " +
+                            Math.round(g) + ", " +
+                            Math.round(b) + ", " +
+                            a + ")";
+                    this.ctx.fill();
+                    // stroke
+                    if (val > 20) {
+                        var strokeVal = 20;
+                        this.ctx.strokeStyle =  "rgba(" + strokeVal + ", " + strokeVal + ", " + strokeVal + ", 0.5)";
+                        this.ctx.lineWidth = 1;
+                        this.ctx.stroke();
+                    }
+                }
+                // display the tile number for debug purposes
+                /*this.ctx.font = "bold 12px sans-serif";
+                 this.ctx.fillStyle = 'grey';
+                 this.ctx.fillText(this.num, this.vertices[0][0], this.vertices[0][1]);*/
+            };
+            Polygon.prototype.drawHighlight = function() {
+                this.ctx.beginPath();
+                // draw the highlight
+                var offset = this.calculateOffset(this.vertices[0]);
+                this.ctx.moveTo(this.vertices[0][0] + offset[0], this.vertices[0][1] + offset[1]);
+                // draw the polygon
+                for (var i = 0; i <= this.sides-1;i += 1) {
+                    offset = this.calculateOffset(this.vertices[i]);
+                    this.ctx.lineTo (this.vertices[i][0] + offset[0], this.vertices[i][1] + offset[1]);
+                }
+                this.ctx.closePath();
+                var a = this.highlight/100;
+                this.ctx.strokeStyle =  "rgba(255, 255, 255, " + a + ")";
+                this.ctx.lineWidth = 1;
+                this.ctx.stroke();
+                this.highlight -= 0.5;
+            };
+
+            var makePolygonArray = function() {
+                tiles = [];
+                /**
+                 * Arrange into a grid x, y, with the y axis at 60 degrees to the x, rather than
+                 * the usual 90.
+                 * @type {number}
+                 */
+                var i = 0; // unique number for each tile
+                tiles.push(new Polygon(6, 0, 0, tileSize, fgCtx, i)); // the centre tile
+                i++;
+                for (var layer = 1; layer < 7; layer++) {
+                    tiles.push(new Polygon(6, 0, layer, tileSize, fgCtx, i)); i++;
+                    tiles.push(new Polygon(6, 0, -layer, tileSize, fgCtx, i)); i++;
+                    for(var x = 1; x < layer; x++) {
+                        tiles.push(new Polygon(6, x, -layer, tileSize, fgCtx, i)); i++;
+                        tiles.push(new Polygon(6, -x, layer, tileSize, fgCtx, i)); i++;
+                        tiles.push(new Polygon(6, x, layer-x, tileSize, fgCtx, i)); i++;
+                        tiles.push(new Polygon(6, -x, -layer+x, tileSize, fgCtx, i)); i++;
+                    }
+                    for(var y = -layer; y <= 0; y++) {
+                        tiles.push(new Polygon(6, layer, y, tileSize, fgCtx, i)); i++;
+                        tiles.push(new Polygon(6, -layer, -y, tileSize, fgCtx, i)); i++;
+                    }
+                }
+            };
+
+            function Star(x, y, starSize, ctx) {
+                this.x = x;
+                this.y = y;
+                this.angle = Math.atan(Math.abs(y)/Math.abs(x));
+                this.starSize = starSize;
+                this.ctx = ctx;
+                this.high = 0;
+            }
+            Star.prototype.drawStar = function() {
+                var distanceFromCentre = Math.sqrt(Math.pow(this.x, 2) + Math.pow(this.y, 2));
+
+                // stars as lines
+                var brightness = 200 + Math.min(Math.round(this.high * 5), 55);
+                this.ctx.lineWidth= 0.5 + distanceFromCentre/2000 * Math.max(this.starSize/2, 1);
+                this.ctx.strokeStyle='rgba(' + brightness + ', ' + brightness + ', ' + brightness + ', 1)';
+                this.ctx.beginPath();
+                this.ctx.moveTo(this.x,this.y);
+                var lengthFactor = 1 + Math.min(Math.pow(distanceFromCentre,2)/30000 * Math.pow(audioSource.volume, 2)/6000000, distanceFromCentre);
+                var toX = Math.cos(this.angle) * -lengthFactor;
+                var toY = Math.sin(this.angle) * -lengthFactor;
+                toX *= this.x > 0 ? 1 : -1;
+                toY *= this.y > 0 ? 1 : -1;
+                this.ctx.lineTo(this.x + toX, this.y + toY);
+                this.ctx.stroke();
+                this.ctx.closePath();
+
+                // starfield movement coming towards the camera
+                var speed = lengthFactor/20 * this.starSize;
+                this.high -= Math.max(this.high - 0.0001, 0);
+                if (speed > this.high) {
+                    this.high = speed;
+                }
+                var dX = Math.cos(this.angle) * this.high;
+                var dY = Math.sin(this.angle) * this.high;
+                this.x += this.x > 0 ? dX : -dX;
+                this.y += this.y > 0 ? dY : -dY;
+
+                var limitY = fgCanvas.height/2 + 500;
+                var limitX = fgCanvas.width/2 + 500;
+                if ((this.y > limitY || this.y < -limitY) || (this.x > limitX || this.x < -limitX)) {
+                    // it has gone off the edge so respawn it somewhere near the middle.
+                    this.x = (Math.random() - 0.5) * fgCanvas.width/3;
+                    this.y = (Math.random() - 0.5) * fgCanvas.height/3;
+                    this.angle = Math.atan(Math.abs(this.y)/Math.abs(this.x));
+                }
+            };
+
+            var makeStarArray = function() {
+                var x, y, starSize;
+                stars = [];
+                var limit = fgCanvas.width / 15; // how many stars?
+                for (var i = 0; i < limit; i ++) {
+                    x = (Math.random() - 0.5) * fgCanvas.width;
+                    y = (Math.random() - 0.5) * fgCanvas.height;
+                    starSize = (Math.random()+0.1)*3;
+                    stars.push(new Star(x, y, starSize, sfCtx));
+                }
+            };
+
+            this.resizeCanvas = function() {
+                if (fgCanvas) {
+                    // resize the foreground canvas
+                    fgCanvas.width = window.innerWidth;
+                    fgCanvas.height = window.innerHeight;
+                    fgCtx.translate(fgCanvas.width/2,fgCanvas.height/2);
+
+                    // resize the starfield canvas
+                    sfCanvas.width = window.innerWidth;
+                    sfCanvas.height = window.innerHeight;
+                    sfCtx.translate(fgCanvas.width/2,fgCanvas.height/2);
+
+                    tileSize = fgCanvas.width > fgCanvas.height ? fgCanvas.width / 25 : fgCanvas.height / 25;
+
+                    makePolygonArray();
+                    makeStarArray()
+                }
+            };
+
+            var rotateForeground = function() {
+                tiles.forEach(function(tile) {
+                    tile.rotateVertices();
+                });
+            };
+
+            var draw = function() {
+                fgCtx.clearRect(-fgCanvas.width, -fgCanvas.height, fgCanvas.width*2, fgCanvas.height *2);
+                sfCtx.clearRect(-fgCanvas.width/2, -fgCanvas.height/2, fgCanvas.width, fgCanvas.height);
+
+                stars.forEach(function(star) {
+                    star.drawStar();
+                });
+                tiles.forEach(function(tile) {
+                    tile.drawPolygon();
+                });
+                tiles.forEach(function(tile) {
+                    if (tile.highlight > 0) {
+                        tile.drawHighlight();
+                    }
+                });
+
+                // debug
+                /* fgCtx.font = "bold 24px sans-serif";
+                 fgCtx.fillStyle = 'grey';
+                 fgCtx.fillText("minMental:" + minMental, 10, 10);
+                 fgCtx.fillText("maxMental:" + maxMental, 10, 40);*/
+                requestAnimationFrame(draw);
+            };
+
+            this.init = function(options) {
+                audioSource = options.audioSource;
+                var container = document.getElementById(options.containerId);
+
+                // foreground hexagons layer
+                fgCanvas = document.createElement('canvas');
+                fgCanvas.setAttribute('style', 'position: absolute; z-index: 10');
+                fgCanvas.setAttribute('class', 'embed-responsive-item');
+                fgCtx = fgCanvas.getContext("2d");
+                container.appendChild(fgCanvas);
+
+                // middle starfield layer
+                sfCanvas = document.createElement('canvas');
+                sfCtx = sfCanvas.getContext("2d");
+                sfCanvas.setAttribute('style', 'position: absolute; z-index: 5');
+                sfCanvas.setAttribute('class', 'embed-responsive-item');
+                container.appendChild(sfCanvas);
+
+                makePolygonArray();
+                makeStarArray();
+
+                this.resizeCanvas();
+                draw();
+
+                setInterval(rotateForeground, 20);
+                // resize the canvas to fill browser window dynamically
+                window.addEventListener('resize', this.resizeCanvas, false);
+            };
+        };
+
+        function startVisualizer(audioElement) {
+            // sound._player._html5Audio
+            var audioSource = new SoundCloudAudioSource(audioElement);
+            var visualizer = new Visualizer();
+            visualizer.init({
+                containerId: 'media-container',
+                audioSource: audioSource
+            });
+        }
+    </script>
 </template>