UNPKG

amplitudejs

Version:

A JavaScript library that allows you to control the design of your media controls in your webpage -- not the browser. No dependencies (jQuery not required) https://521dimensions.com/open-source/amplitudejs

735 lines (641 loc) 18.5 kB
/* Visualization adapted from Michael Bromley's Soundcloud visualizer. https://github.com/michaelbromley/soundcloud-visualizer */ function MichaelBromleyVisualization(){ /* Sets the ID and Name of the visualization. */ this.id = 'michaelbromley_visualization'; this.name = 'Michael Bromley Visualization'; /* Initializes the analyser for the visualization. */ this.analyser = ''; /* Initializes the container and preferences. */ this.container = ''; this.preferences = { width: 500, height: 500, fullscreen: false, inherit: true } /* Default tile size for the visualization */ this.tileSize = ''; /* Initializes the tiles and stars arrays */ this.tiles = []; this.stars = []; /* Initializes the variables used for foregorund elements. */ this.fgCanvas = ''; this.fgCtx = ''; this.fgRotation = 0.001; /* Initializes the variables used for background elements. */ this.bgCanvas = ''; this.bgCtx = ''; /* Initializes the variables used for the starfield elements. */ this.sfCanvas = ''; this.sfCtx = ''; /* Initializes the volume and stream data. */ this.volume = 0; this.streamData = new Uint8Array( 128 ); /* Initializes the rotation intervals. */ this.drawBgInterval = ''; this.rotateForegroundInterval = ''; this.sampleAudioStreamInterval = ''; /* Initializes the animation frames. */ this.animationFrame = ''; /** * Sets the user defined preferences for the visualization. * * @param {object} userPreferences - The preferences passed in by the user for the visualization. */ this.setPreferences = function( userPreferences ){ for( var key in this.preferences ){ if( userPreferences[ key ] != undefined) { this.preferences[key] = userPreferences[key]; } } } /** * Starts the visualization. * * @param {Node} element - The element we are starting the visualization with. */ this.startVisualization = function( element ){ /* Set the analyser and the container elements. */ this.analyser = Amplitude.getAnalyser(); this.container = element; /* If we are inheriting the width and height of the container, set the container to the width and height of the element. */ if( this.preferences.inherit ){ this.preferences.width = this.container.offsetWidth; this.preferences.height = this.container.offsetHeight; } /* Foreground Hexagons Layer */ this.fgCanvas = document.createElement('canvas'); this.fgCanvas.setAttribute('style', 'position: absolute; z-index: 10'); this.fgCtx = this.fgCanvas.getContext("2d"); this.container.appendChild( this.fgCanvas ); /* Middle Starfield Layer */ this.sfCanvas = document.createElement('canvas'); this.sfCtx = this.sfCanvas.getContext("2d"); this.sfCanvas.setAttribute('style', 'position: absolute; z-index: 5'); this.container.appendChild( this.sfCanvas ); /* Background Image Layer */ this.bgCanvas = document.createElement('canvas'); this.bgCtx = this.bgCanvas.getContext("2d"); this.container.appendChild( this.bgCanvas ); /* Make the polygon and star arrays. */ this.makePolygonArray(); this.makeStarArray(); /* Resize the canvas and draw the visualization. */ this.resizeCanvas(); this.draw(); /* Set the sample audio interval. */ this.sampleAudioStreamInterval = setInterval( this.sampleAudioStream.bind(this), 20 ); /* Set the drawing of the background and rotation of the foreground interval. */ this.drawBgInterval = setInterval( this.drawBg.bind(this), 100 ); this.rotateForegroundInterval = setInterval( this.rotateForeground.bind(this), 20 ); /* When the window is resized, resize the canvas. */ window.addEventListener('resize', this.resizeCanvas, false ); } /* Make the polygon array. */ this.makePolygonArray = function(){ /* Initialize the tiles to an array. */ this.tiles = []; /** * Arrange into a grid x, y, with the y axis at 60 degrees to the x, rather than * the usual 90. * @type {number} */ let i = 0; /* Unique number for each tile */ this.tiles.push( new Polygon(6, 0, 0, this.tileSize, this.fgCtx, i, this.analyser, this.streamData, this.tiles ) ); /* The centre tile */ i++; /* Build the tiles needed for the the visualization. */ for( var layer = 1; layer < 7; layer++ ){ this.tiles.push(new Polygon( 6, 0, layer, this.tileSize, this.fgCtx, i, this.analyser, this.streamData, this.tiles, this.fgRotation ) ); i++; this.tiles.push(new Polygon( 6, 0, -layer, this.tileSize, this.fgCtx, i, this.analyser, this.streamData, this.tiles, this.fgRotation ) ); i++; for(var x = 1; x < layer; x++) { this.tiles.push(new Polygon( 6, x, -layer, this.tileSize, this.fgCtx, i, this.analyser, this.streamData, this.tiles, this.fgRotation ) ); i++; this.tiles.push(new Polygon( 6, -x, layer, this.tileSize, this.fgCtx, i, this.analyser, this.streamData, this.tiles, this.fgRotation ) ); i++; this.tiles.push(new Polygon( 6, x, layer-x, this.tileSize, this.fgCtx, i, this.analyser, this.streamData, this.tiles, this.fgRotation ) ); i++; this.tiles.push(new Polygon( 6, -x, -layer+x, this.tileSize, this.fgCtx, i, this.analyser, this.streamData, this.tiles, this.fgRotation ) ); i++; } for(var y = -layer; y <= 0; y++) { this.tiles.push(new Polygon( 6, layer, y, this.tileSize, this.fgCtx, i, this.analyser, this.streamData, this.tiles, this.fgRotation ) ); i++; this.tiles.push(new Polygon( 6, -layer, -y, this.tileSize, this.fgCtx, i, this.analyser, this.streamData, this.tiles, this.fgRotation ) ); i++; } } }, /* Build the star array. */ this.makeStarArray = function(){ var x; var y; var starSize; this.stars = []; var limit = this.fgCanvas.width / 15; for( var i = 0; i < limit; i++ ){ x = ( Math.random() - 0.5 ) * this.fgCanvas.width; y = ( Math.random() - 0.5 ) * this.fgCanvas.height; starSize = ( Math.random() + 0.1 ) * 3; this.stars.push( new Star( x, y, starSize, this.sfCtx, this.fgCanvas, this.analyser, this.streamData ) ); } }, /* Resize the canvas. */ this.resizeCanvas = function(){ if( this.fgCanvas ){ if( this.preferences.fullscreen ){ /* Resize the foreground canvas */ this.fgCanvas.width = window.innerWidth; this.fgCanvas.height = window.innerHeight; this.fgCtx.translate(this.fgCanvas.width/2,this.fgCanvas.height/2); /* Resize the bg canvas */ this.bgCanvas.width = window.innerWidth; this.bgCanvas.height = window.innerHeight; /* Resize the starfield canvas */ this.sfCanvas.width = window.innerWidth; this.bgCanvas.height = window.innerHeight; this.sfCtx.translate(this.fgCanvas.width/2,this.fgCanvas.height/2); }else{ /* Resize the foreground canvas */ this.fgCanvas.width = this.preferences.width; this.fgCanvas.height = this.preferences.height; this.fgCtx.translate(this.fgCanvas.width/2,this.fgCanvas.height/2); /* Resize the bg canvas */ this.bgCanvas.width = this.preferences.width; this.bgCanvas.height = this.preferences.height; /* Resize the starfield canvas */ this.sfCanvas.width = this.preferences.width; this.bgCanvas.height = this.preferences.height; this.sfCtx.translate(this.fgCanvas.width/2,this.fgCanvas.height/2); } this.tileSize = this.fgCanvas.width > this.fgCanvas.height ? this.fgCanvas.width / 25 : this.fgCanvas.height / 25; this.drawBg(); this.makePolygonArray(); this.makeStarArray(); } }, /* Draw the visualization. */ this.draw = function(){ /* Clear the foreground */ this.fgCtx.clearRect(-this.fgCanvas.width, -this.fgCanvas.height, this.fgCanvas.width*2, this.fgCanvas.height *2); /* Clear the star field */ this.sfCtx.clearRect(-this.fgCanvas.width/2, -this.fgCanvas.height/2, this.fgCanvas.width, this.fgCanvas.height); /* Draw all of the stars */ this.stars.forEach(function(star) { star.drawStar(); }); /* Draw all of the tiles. */ this.tiles.forEach(function(tile) { tile.drawPolygon(); }); /* Draw all of the highlights */ this.tiles.forEach(function(tile) { if (tile.highlight > 0) { tile.drawHighlight(); } }); /* Request the animation frame. */ this.animationFrame = window.requestAnimationFrame( this.draw.bind(this) ); }, /** * Draw the background */ this.drawBg = function(){ this.bgCtx.clearRect(0, 0, this.bgCanvas.width, this.bgCanvas.height); var r, g, b, a; var val = this.volume/1000; r = 200 + (Math.sin(val) + 1) * 28; g = val * 2; b = val * 8; a = Math.sin(val+3*Math.PI/2) + 1; this.bgCtx.beginPath(); this.bgCtx.rect(0, 0, this.bgCanvas.width, this.bgCanvas.height); /* Create radial gradient */ var grd = this.bgCtx.createRadialGradient(this.bgCanvas.width/2, this.bgCanvas.height/2, val, this.bgCanvas.width/2, this.bgCanvas.height/2, this.bgCanvas.width-Math.min(Math.pow(val, 2.7), this.bgCanvas.width - 20)); /* Centre is transparent black */ grd.addColorStop(0, 'rgba(0,0,0,0)'); grd.addColorStop(0.8, "rgba(" + Math.round(r) + ", " + Math.round(g) + ", " + Math.round(b) + ", 0.4)"); this.bgCtx.fillStyle = grd; this.bgCtx.fill(); }, /** * Sample the audio stream. */ this.sampleAudioStream = function(){ this.analyser.getByteFrequencyData( this.streamData ); /* Calculate an overall volume value */ var total = 0; /* Get the volume from the first 80 bins, else it gets too loud with treble */ for (var i = 0; i < 80; i++) { total += this.streamData[i]; } this.volume = total; }, /** * Rotate foreground */ this.rotateForeground = function(){ for( let i = 0; i < this.tiles.length; i++ ){ this.tiles[i].rotateVertices(); } }, /** * Returns the name of the visualization. */ this.getName = function(){ return name; }, /** * Returns the id of the visualization. */ this.getID = function(){ return this.id; }, /** * Stops the visualization. */ this.stopVisualization = function(){ clearInterval(this.sampleAudioStreamInterval); clearInterval(this.drawBgInterval); clearInterval(this.rotateForegroundInterval); window.cancelAnimationFrame( this.animationFrame ); this.container.innerHTML = ''; }, /** * Returns the volume of the visualization. */ this.getVolume = function(){ return this.volume; } } /** * Defines the polygon object. * @param {number} sides * @param {number} x * @param {number} y * @param {number} tileSize * @param {context} ctx * @param {number} num * @param {Uint8Array} streamData * @param {array} tiles * @param {integer} fgRotation */ function Polygon( sides, x, y, tileSize, ctx, num, analyser, streamData, tiles, fgRotation ){ this.analyser = analyser; this.sides = sides; this.tileSize = tileSize; this.ctx = ctx; this.tiles = tiles; this.fgRotation = fgRotation; /* The number of the tile, starting at 0 */ this.num = num; /* The highest colour value, which then fades out */ this.high = 0; /* Increase this value to fade out faster. */ this.decay = this.num > 42 ? 1.5 : 2; /* 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 */ this.highlight = 0; 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]); } this.streamData = streamData; } /** * Roate vertices */ Polygon.prototype.rotateVertices = function(){ /* Rotate all the vertices to achieve the overall rotational effect */ var rotation = this.fgRotation; rotation -= this.analyser.volume > 10000 ? Math.sin(this.analyser.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); } } /** * Draw polygon */ Polygon.prototype.drawPolygon = function(){ var bucket = Math.ceil(this.streamData.length/this.tiles.length*this.num); var val = Math.pow((this.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) { /* Add the cube effect if it's really loud */ this.cubed = val; } if (val > 120) { /* Add the highlight effect if it's pretty loud */ this.highlight = 100; } /* 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(); } } } /** * Calculate the offset * * @param {array} coords */ Polygon.prototype.calculateOffset = function( coords ) { this.analyser.getByteFrequencyData( this.streamData ); /* Calculate an overall volume value */ var total = 0; /* Get the volume from the first 80 bins, else it gets too loud with treble */ for (var i = 0; i < 80; i++) { total += this.streamData[i]; } var volume = total; var angle = Math.atan(coords[1]/coords[0]); /* A bit of pythagoras */ var distance = Math.sqrt(Math.pow(coords[0], 2) + Math.pow(coords[1], 2)); /* This factor makes the visualization go crazy wild */ var mentalFactor = Math.min(Math.max((Math.tan(volume/6000) * 0.5), -20), 2); var offsetFactor = Math.pow(distance/3, 2) * (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]; }; /** * Draw the highlight */ 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; }; /** * Define the star object * * @param {number} x * @param {number} y * @param {number} starSize * @param {context} ctx * @param {canvas} fgCanvas * @param {analyser} analyser * @param {Uint8Array} streamData */ function Star( x, y, starSize, ctx, fgCanvas, analyser, streamData ){ 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; this.fgCanvas = fgCanvas; this.analyser = analyser; this.streamData = streamData; } /** * Draws the star. */ Star.prototype.drawStar = function(){ var distanceFromCentre = Math.sqrt( Math.pow( this.x, 2 ) + Math.pow( this.y, 2 ) ); this.analyser.getByteFrequencyData( this.streamData ); /* Calculate an overall volume value */ var total = 0; /* Get the volume from the first 80 bins, else it gets too loud with treble */ for (var i = 0; i < 80; i++) { total += this.streamData[i]; } var volume = total; /* 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(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 = this.fgCanvas.height/2 + 500; var limitX = this.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) * this.fgCanvas.width/3; this.y = (Math.random() - 0.5) * this.fgCanvas.height/3; this.angle = Math.atan(Math.abs(this.y)/Math.abs(this.x)); } }