|
@@ -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"/>
|
|
@@ -147,4 +147,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>
|