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