harunpehlivan
7/26/2018 - 8:57 PM

Musical Particles II

Musical Particles II

<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" />
$z-layers: (
	canvas: 0,
	ui: 1,
	overlay: 2
);

@function z($name){
	@return map-get($z-layers, $name);
}

$amber: #ffb300;

html, body {
	background: #010101;
	overflow: hidden;
}

#lbl-menu-toggle {
	position: fixed;
	top: 20px;
	left: 30px;
	z-index: z("overlay");
	display: block;
	width: 24px;
	height: 20px;
	cursor: pointer;
	transform: translateZ(0);
	transition: opacity 0.5s, transform 0.5s;
	&.hidden {
		opacity: 0;
		transform: translateY(-10px) translateZ(0);
	}
	.bar {
		position: absolute;
		display: block;
		height: 2px;
		width: 100%;
		background-color: $amber;
		transition: transform 0.2s;
		&.line:first-of-type {
			top: 2px;
			transform-origin: right center;
			transition-delay: 0.1s;
		}
		&.cross {
			top: 8px;
			transform-origin: center center;
		}
		&.line:last-of-type {
			top: 14px;
			transform-origin: left center;
			transition-delay: 0.1s;
		}
	}
}

#ui-drawer {
	position: absolute;
	top: 0;
	left: 0;
	height: calc(100vh - 65px);
	width: 40vw;
	max-width: 500px;
	padding-top: 60px;
	box-sizing: border-box;
	font-family: "Josefin Sans", sans-serif;
	background: #222;
	box-shadow: 0 0 4px rgba(0,0,0,0.8);
	overflow: hidden;
	opacity: 0;
	transform: translateX(-40.15vw) translateZ(0);
	transition: opacity 0.4s, transform 0.4s;
	.drawer-folder__label {
		position: relative;
		z-index: z("overlay");
		padding: 12px 30px 10px 30px;
		color: $amber;
		background: #333;
		box-shadow: 0 1px 2px rgba(0,0,0,0.5);
	}
	.drawer-folder__option {
		position: relative;
		.option__sub-menu {
			position: relative;
		}
		.option__label {
			position: relative;
			width: 100%;
			display: block;
			padding: 12px 30px 10px 30px;
			font-size: 0.95em;
			color: white;
			box-sizing: border-box;
			background-color: #292929;
			transition: background-color 0.3s;
			&:not(#track--label) {
				cursor: pointer;
				&:hover {
					background-color: #2f2f2f;
				}
			}
		}
		&:not(:last-child) .option__label {
			border-bottom: 1px solid #1c1c1c;
		}
		.fa {
			width: 16px;
			margin-right: 10px;
			box-sizing: border-box;
			color: #6f6f6f;
			transition: color 0.3s;
		}
		.fa-lightbulb-o {
			padding: 0 3px;
		}
		.fa-signal {
			transform: scaleX(-1);
		}
		.fa-music, .fa-plus {
			color: $amber;
		}
		.fa-plus {
			margin-right: 0;
		}
		#btn-add-track {
			position: absolute;
			top: 12px;
			right: 30px;
			#in-add-track {
				display: none;
			}
			#lbl-add-track {
				cursor: pointer;
				&:hover:before {
					opacity: 1;
					transform: translateX(-4px);
				}
				&:before {
					position: absolute;
					display: block;
					content: "Add";
					white-space: nowrap;
					padding: 4px 8px 4px 4px;
					top: -2px;
					right: 14px;
					opacity: 0;
					transition: opacity 0.3s, transform 0.3s;
				}
			}
		}
		input[type="checkbox"] {
			display: none;
			&:checked ~ .fa {
				color: $amber;
			}
		}
	}
	.drawer-folder__option {
		height: 100%;
	}
	#track-list {
		position: relative;
		overflow-y: scroll;
		max-height: calc(100vh - 400px);
		width: calc(100% + 18px);
		.track-option {
			padding: 12px 30px 10px 30px;
			font-size: 0.95em;
			color: white;
			background-color: #1a1a1a;
			cursor: pointer;
			border-bottom: 1px solid #121212;
			transition: background-color 0.3s;
			&:hover {
				background-color: #1f1f1f;
			}
		}
	}
	#frm-track-title {
		position: absolute;
		top: 20px;
		right: 20px;
		width: calc(60% - 60px);
		padding: 20px;
		background-color: #3f3f3f;
		box-shadow: 0 2px 3px rgba(0,0,0,0.6);
		transform: translateZ(0);
		transition: opacity 0.3s, transform 0.3s;
		&.hidden {
			pointer-events: none;
			opacity: 0;
			transform: translateY(-10px) translateZ(0);
		}
		&:before {
			position: absolute;
			right: 12px;
			top: -7px;
			display: block;
			content: "";
			height: 16px;
			width: 16px;
			background-color: #3f3f3f;
			transform: rotate(45deg);
		}
		#lbl-track-title {
			display: block;
			margin-bottom: 8px;
			color: $amber;
		}
		#txt-track-title {
			width: 100%;
			box-sizing: border-box;
			padding: 4px;
			border-width: 0 0 1px 0;
			border-color: $amber;
			color: white;
			font-family: "Josefin Sans", sans-serif;
			background-color: transparent;
			&:focus {
				outline: none;
			}
			&::-webkit-input-placeholder {
				color: #9f9f9f;
			}
			&:-ms-input-placeholder { 
				color: #9f9f9f;
			}
			&::-moz-placeholder {
				color: #9f9f9f;
				opacity: 1;
			}
			&:-moz-placeholder {
				color: #9f9f9f;
				opacity: 1;
			}
		}
		#btn-submit-title {
      position: relative;
    	left: 100%;
			padding: 6px 12px;
			margin-top: 10px;
    	transform: translateX(-100%);
			background: transparent;
			border: 1px solid $amber;
			cursor: pointer;
			color: white;
			font-family: "Josefin Sans", sans-serif;
			transition: color 0.3s;
			&:hover,
			&:hover .fa {
				color: $amber;
			}
			&:focus {
				outline: none;
			}
			.fa {
				width: auto;
				margin-right: 0;
				margin-left: 8px;
				color: white;
			}
		}
	}
}

#chk-menu-toggle {
	display: none;
	&:checked {
		+ #lbl-menu-toggle {
			.line {
				transform: scaleX(0);
				transition-delay: 0;
			}
			.cross {
				transition-delay: 0.2s;
				&:nth-of-type(2) {
					transform: rotate(45deg) scale(0.8);
				}
				&:nth-of-type(3) {
					transform: rotate(135deg) scale(0.8);
				}
			}
		}
		~ #ui-drawer {
			opacity: 1;
			transform: translateX(0) translateZ(0);
		}
	}
}

#ui-overlay {
	position: absolute;
	width: 100vw;
	height: 100vh;
	top: 0;
	left: 0;
	z-index: z("overlay");
}

#btn-initialize {
	position: absolute;
	top: 50%;
	left: 50%;
	padding: 0.8em 1em 0.6em 1em;
	border: 2px solid;
	border-radius: 1.5em;
	color: white;
	font-family: "Josefin Sans", sans-serif;
	font-size: 1.2em;
	cursor: pointer;
	transform: translateX(-50%) translateY(-50%);
	transition: color 0.3s, opacity 0.5s;
	&:hover {
		color: $amber;
	}
	&:active {
		transform: translateX(-50%) translateY(-50%) scale(0.95);
	}
	&.disabled {
		opacity: 0;
		pointer-events: none;
	}
}

#loader {
	display: flex;
	align-items: center;
	justify-content: center;
	position: absolute;
	width: 50px;
	height: 20px;
	top: 50%;
	left: 50%;
	transform: translateX(-50%) translateY(-50%);
	transition: opacity 0.5s;
	&.hidden {
		opacity: 0;
	}
	&.loading .load-bar {
		animation: load-bar-animation 2s linear infinite;
	}
	.load-bar {
		flex: 1;
		height: 100%;
		background: $amber;
		opacity: 0.5;
		transform: scaleY(1) translateZ(0);
		&:not(:last-child) {
			margin-right: 2px;
		}
		@for $i from 1 through 5 {
			&:nth-child(#{$i}) {
				animation-delay: #{$i * 0.2}s;
			}
		}
	}
}

#audio-controls {
	position: absolute;
	display: flex;
	align-items: center;
	justify-content: center;
	flex-flow: column;
	left: 0;
	bottom: 0;
	width: 100%;
	height: 65px;
	color: $amber;
	opacity: 1;
	box-shadow: 0 0 4px rgba(0,0,0,0.8);
	transition: opacity 0.5s, transform 0.5s;
	&.hidden {
		transform: translateY(10px);
		opacity: 0;
	}
	&.disabled {
		pointer-events: none;
		.fa {
			opacity: 0.2;
		}
	}
	#controls__container {
		position: relative;
		background-color: #0c0c0c;
		display: flex;
		align-items: center;
		justify-content: center;
		width: 100%;
		padding: 20px 30px;
		box-sizing: border-box;
	}
	#button-controls {
		display: flex;
		align-items: center;
		justify-content: flex-start;
		margin-right: 16px;
		.btn {
			display: flex;
			align-items: center;
			width: 16px;
			&:not(:last-child) {
				justify-content: center;
				margin-right: 12px;
			}
		}
	}
	#title {
		color: $amber;
		flex: 1;
		padding-top: 4px;
		font-family: "Josefin Sans", sans-serif;
		font-size: 1.2em;
		text-align: right;
		transition: opacity 0.3s, transform 0.3s;
	}
	#seek-bar {
		position: absolute;
		top: 0;
		z-index: z("overlay");
		height: 6px;
		width: 100%;
		background-color: #4f4f4f;
		cursor: pointer;
		transition: transform 0.5s;
		#progress-bar {
			height: 100%;
			width: 100%;
			background: $amber;
			transform: scaleX(0);
			transform-origin: center left;
			transition: transform 0.5s;
		}
	}
	#btn-prev, #btn-next {
		transform: scaleX(1.5);
		&:active {
			transform: scaleY(0.85) scaleX(1.35);
		}
	}
	.fa {
		cursor: pointer;
		transition: opacity 0.3s;
		&:active:not(#icon-volume):not(#btn-prev):not(#btn-next) {
			transform: scale(0.85);
		}
	}
	#icon-volume {
		position: relative;
		&:hover .input-container {
			opacity: 1;
		}
		.input-container {
			position: absolute;
			top: -8px;
			left: 12px;
			width: 60px;
			padding: 8px;
			opacity: 0;
			transition: opacity 0.3s;
		}
		#rng-volume {
			position: relative;
			bottom: 4px;
			width: 100%;
			-webkit-appearance: none;
			&:focus {
				outline: none;
			}
			&::-webkit-slider-runnable-track {
				width: 100%;
				height: 2px;
				cursor: pointer;
				background: #444;
			}
			&::-webkit-slider-thumb {
				height: 14px;
				width: 14px;
				border-radius: 50%;
				background: $amber;
				cursor: pointer;
				-webkit-appearance: none;
				margin-top: -6px;
			}
			&::-webkit-range-track {
				width: 100%;
				height: 2px;
				cursor: pointer;
				background: #444;
			}
			&::-webkit-range-thumb {
				height: 16px;
				width: 16px;
				border-radius: 50%;
				border: 2px solid #444;
				background: transparent;
				cursor: pointer;
				-webkit-appearance: none;
				margin-top: -7px;
			}
			&::-moz-slider-runnable-track {
				width: 100%;
				height: 2px;
				cursor: pointer;
				background: #444;
			}
			&::-moz-slider-thumb {
				height: 14px;
				width: 14px;
				border-radius: 50%;
				background: $amber;
				cursor: pointer;
				-webkit-appearance: none;
				margin-top: -6px;
			}
			&::-moz-range-track {
				width: 100%;
				height: 2px;
				cursor: pointer;
				background: #444;
			}
			&::-moz-range-thumb {
				height: 16px;
				width: 16px;
				border-radius: 50%;
				border: 2px solid #444;
				background: transparent;
				cursor: pointer;
				-webkit-appearance: none;
				margin-top: -7px;
			}
		}
	}
}

#codepen-link {
  position: absolute;
  top: 20px;
  right: 30px;
  height: 40px;
  width: 40px;
  z-index: 10;
  border-radius: 50%;
  box-sizing: border-box;
  background-image: url('https://cdn-cms.f-static.com/uploads/1017556/400_5aade1f55d888.png');
  background-position: center center;
  background-size: cover;
  opacity: 0.4;
  transition: all 0.25s;
  
  &:hover {
    opacity: 0.8;
    box-shadow: 0 0 6px #efefef;
	}
}

@keyframes load-bar-animation {
	30% {
		opacity: 1;
		transform: scaleY(1.5) translateZ(0);
	}
	60% {
		opacity: 0.5;
		transform: scaleY(1) translateZ(0);
	}
	100% {
		opacity: 0.5;
		transform: scaleY(1) translateZ(0);
	}
}
<script src="https://rawgit.com/SeanFree/Vector2/master/Vector2.js"></script>
"use strict";

// All songs courtesy of Archive.org community audio:
// https://archive.org/details/audio

const { PI, cos, sin, abs, sqrt, pow, floor, round } = Math;
const HALF_PI = 0.5 * PI;
const TAU = 2 * PI;
const rand = n => n * Math.random();
const randRange = n => n - rand(2 * n);
const fadeInOut = (t, m) => {
	let hm = 0.5 * m;
	return abs((t + hm) % m - hm) / (hm);
};

class VectorArrayObject {
	constructor(count = 0, max = 0) {
		this.count = count || max;
		this.max = max || count;
		this.values = new Float32Array(max * 2);
	}
	get(i) {
		return {
			x: this.values[i * 2],
			y: this.values[i * 2 + 1]
		};
	}
	getX(i) {
		return this.values[i * 2];
	}
	getY(i) {
		return this.values[i * 2 + 1];
	}
	set(i, x, y) {
		this.values[i * 2] = x;
		this.values[i * 2 + 1] = y;
		return this;
	}
	setX(i, x) {
		this.values[i * 2] = x;
		return this;
	}
	setY(i, y) {
		this.values[i * 2 + 1] = y;
		return this;
	}
}

class VectorArrayObjectController {
	constructor(count = 0, max = 0) {
		this.count = count || max;
		this.max = max || count;
		this.life = new VectorArrayObject(this.count, this.max);
		this.vertices = new VectorArrayObject(this.count, this.max);
		this.velocities = new VectorArrayObject(this.count, this.max);
	}
	getLife(i) {
		return this.life.getX(i);
	}
	getTTL(i) {
		return this.life.getY(i);
	}
	setLife(i, life) {
		this.life.setX(i, life);
		return this;
	}
	setTTL(i, ttl) {
		this.life.setY(i, ttl);
		return this;
	}
	getVertex(i) {
		return this.vertices.get(i);
	}
	setVertex(i, x, y) {
		this.vertices.set(i, x, y);
		return this;
	}
	getVelocity(i) {
		return this.velocities.get(i);
	}
	setVelocity(i, x, y) {
		this.velocities.set(i, x, y);
		return this;
	}
}

class RenderObject {
	constructor(x = 0, y = 0) {
		this.life = 0;
		this.position = new Vector2(x, y);
		this.lastPosition = this.position.clone();
		this.velocity = new Vector2();
	}
	getPosition() {
		return this.position.clone();
	}
	setPosition(x, y) {
		this.position.x = x;
		this.position.y = y;
		return this;
	}
	setLastPosition() {
		this.lastPosition.x = this.position.x;
		this.lastPosition.y = this.position.y;
		return this;
	}
	getVelocity() {
		return this.velocity.clone();
	}
	setVelocity(x, y) {
		this.velocity.x = x;
		this.velocity.y = y;
		return this;
	}
	getLife() {
		return this.life;
	}
	setLife(n) {
		this.life = n;
		return this;
	}
	setTTL(n) {
		this.ttl = n;
		return this;
	}
}

class Particle extends RenderObject {
	constructor(x, y, bounds, controller) {
		super(x, y, bounds);
		this.parent = controller;
		this.hueRange = this.parent.count * 90;
		this.reset = false;
		this.bounds = bounds;
		this.alpha = 0;
		this.hue = 0;
		this.frequency = 0;
		this.size = 0;
	}
	update(vX, vY) {
		this.life++;
		this.setVelocity(vX, vY)
			.setLastPosition()
			.checkLife()
			.checkBounds();

		this.setSize()
			.setHue()
			.setAlpha()
			.setColor();

		this.velocity.multiplyScalar(pow(this.normalizedFrequency * 0.3, 2) + 1);

		this.position.add(this.velocity);

		return this;
	}
	setIndex(i) {
		this.index = i;
		return this;
	}
	checkLife() {
		if (this.life >= this.ttl) this.reset = true;
		return this;
	}
	checkBounds() {
		if (
			this.position.x > this.bounds.x + this.size ||
			this.position.x < -this.size ||
			this.position.y > this.bounds.y + this.size ||
			this.position.y < -this.size
		) {
			this.reset = true;
		}
		return this;
	}
	setSize() {
		this.size = pow(this.normalizedFrequency * 3.75, 3) + 2;
		return this;
	}
	setFrequency(n) {
		this.frequency = n;
		this.normalizedFrequency = n / 256;
		return this;
	}
	setHue() {
		this.hue =
			this.index / this.hueRange - this.frequency + this.parent.delta;
		return this;
	}
	setAlpha() {
		this.alpha = fadeInOut(this.life, this.ttl) * this.normalizedFrequency;
		return this;
	}
	setColor() {
		this.color = `hsla(${this.hue}, 75%, 50%, ${this.alpha})`;
		return this;
	}
	draw(canvas) {
		canvas.buffer.save();
		canvas.arc(this.position.x, this.position.y, this.size, 0, TAU, this.color);
		canvas.buffer.restore();
		return this;
	}
}

class ParticleController extends VectorArrayObjectController {
	constructor(count, max, canvas) {
		super(count, count);
		this.delta = 0;
		this.canvas = canvas;
		this.bounds = canvas.dimensions;
		this.populate();
	}
	populate() {
		this.renderTarget = new Particle(0, 0, this.bounds, this);
		for (let i = 0; i < this.count; i++) {
			this.initRenderTarget(i);
		}
	}
	initRenderTarget(i) {
		let x, y, theta, vX, vY, ttl;

		x = rand(this.bounds.x);
		y = rand(this.bounds.y);

		this.renderTarget
			.setLife(0)
			.setPosition(x, y)
			.setLastPosition();

		theta = this.canvas.origin.angleTo(this.renderTarget.position);
		vX = cos(theta);
		vY = sin(theta);
		ttl = rand(50) + 100;

		this.renderTarget.setVelocity(vX, vY);

		this.setVertex(i, x, y)
			.setLife(i, 0)
			.setTTL(i, ttl)
			.setVelocity(i, vX, vY);

		this.renderTarget.reset = false;

		return this;
	}
	drawRenderTarget(i, freqData) {
		this.renderTarget
			.setIndex(i)
			.setLife(this.getLife(i))
			.setTTL(this.getTTL(i))
			.setPosition(this.vertices.getX(i), this.vertices.getY(i))
			.setFrequency(freqData)
			.update(this.velocities.getX(i), this.velocities.getY(i))
			.draw(this.canvas);

		this.setVertex(i, this.renderTarget.position.x, this.renderTarget.position.y)
			.setVelocity(i, this.renderTarget.velocity.x, this.renderTarget.velocity.y)
			.setLife(i, this.renderTarget.getLife());

		if (this.renderTarget.reset) {
			this.initRenderTarget(i);
		}
	}
}

class AudioController {
	constructor() {
		this.playing = false;
		this.initAudio();
		this.btnInitialize = document.querySelector("#btn-initialize");
		this.btnInitialize.addEventListener("click", this.initialize.bind(this));
	}
	initialize() {
		this.element.addEventListener("timeupdate", () => {
			this.progressBar.style = `transform: scaleX(${this.element.currentTime /
				this.element.duration})`;
		});
		this.element.addEventListener("ended", () => {
			this.element.currentTime = 0;
			this.element.pause();
			this.currentTrack =
				this.currentTrack < this.fileNames.length - 1 ? this.currentTrack + 1 : 1;
			this.load();
		});
		this.initUI();
		this.ctx.resume();
		this.load();
	}
	initAudio() {
		this.baseURL =
			"https://res.cloudinary.com/sf-cloudinary/video/upload/v1525614961/";
		this.currentFile = {};
		this.files = {};
		this.fileNames = [
			"beethovensymphony5.mp3",
			"chopinrevolutionary.mp3",
			"mountainking.mp3",
			"lisztliebestraum.mp3",
			"waltzflowers.mp3",
			"clairedelune.mp3",
			"schubertserenade.mp3"
		];
		this.trackTitles = [
			"Ludwig Van Beethoven - Symphony no. 5 mvt. 1",
			"Frederic Chopin - Etude op. 10 no. 9",
			"Edvard Grieg - In the Hall of the Mountain King",
			"Franz Liszt - Liebestraum",
			"Pyotr Tchaikovsky - Waltz of the Flowers",
			"Claude Debussy - Claire De Lune",
			"Franz Schubert - Schwanengesang (Swan Song) no. 4"
		];
		this.currentTrack = floor(rand(this.fileNames.length));

		this.element = document.createElement("audio");
		document.body.appendChild(this.element);
		this.ctx = new AudioContext();
		this.source = this.ctx.createMediaElementSource(this.element);

		this.gainNode = this.ctx.createGain();

		this.analyser = this.ctx.createAnalyser();
		this.analyser.smoothingTimeConstant = 0.88;
		this.analyser.minDecibels = -130;
		this.analyser.maxDecibels = -10;
		this.analyser.fftSize = 1024;

		this.source.connect(this.gainNode);
		this.gainNode.connect(this.analyser);
		this.analyser.connect(this.ctx.destination);

		this.gainNode.gain.value = 0.8;
		this.freqData = new Uint8Array(this.analyser.frequencyBinCount);
	}
	initUI() {
		this.btnInitialize.classList.add("disabled");
		this.controls = {
			menu: {
				toggle: document.querySelector("#lbl-menu-toggle"),
				checkbox: document.querySelector("#chk-menu-toggle")
			},
			parent: document.querySelector("#audio-controls"),
			play: document.querySelector("#btn-play"),
			next: document.querySelector("#btn-next"),
			prev: document.querySelector("#btn-prev"),
			seekBar: document.querySelector("#seek-bar"),
			volume: {
				icon: document.querySelector("#icon-volume"),
				element: document.querySelector("#rng-volume")
			},
			trackList: {
				parent: document.querySelector("#track-list"),
				input: document.querySelector("#in-add-track"),
				titleForm: document.querySelector("#frm-track-title"),
				options: []
			}
		};

		this.progressBar = document.querySelector("#progress-bar");

		this.titleLabel = document.querySelector("#title");
		this.loader = document.querySelector("#loader");
		this.controls.menu.toggle.classList.remove("hidden");
		this.controls.parent.classList.remove("hidden");
		this.controls.play.addEventListener("click", this.playPause.bind(this));
		this.controls.next.addEventListener("click", this.changeTrack.bind(this));
		this.controls.prev.addEventListener("click", this.changeTrack.bind(this));
		this.controls.volume.element.addEventListener(
			"input",
			this.changeVolume.bind(this)
		);
		this.controls.seekBar.addEventListener("click", this.changeTime.bind(this));
		this.controls.trackList.input.addEventListener("change", e => {
			let { name } = e.target.files[0];
			if (this.validFile(name)) {
				this.controls.trackList.titleForm.classList.remove("hidden");
				this.controls.trackList.titleForm.title.value = name;
			} else {
				alert("Audio files only, please! (╯°□°)╯︵ ┻━┻");
			}
		});
		this.controls.trackList.titleForm.addEventListener("submit", e => {
			e.preventDefault();
			this.controls.trackList.titleForm.classList.add("hidden");
			this.uploadFile(
				String(e.target.title.value),
				this.controls.trackList.input.files[0]
			);
		});
		for (let i = 0; i < this.trackTitles.length; i++) {
			this.addTrackOption(i, this.trackTitles[i]);
		}
	}
	changeTime(e) {
		this.element.currentTime =
			this.element.duration * (e.clientX / e.target.offsetWidth);
	}
	changeVolume(e) {
		let { value } = e.target;
		this.gainNode.gain.value = value;
		this.controls.volume.icon.className = "fa";
		this.controls.volume.icon.classList.add(
			value > 0.5 ? "fa-volume-up" : value > 0 ? "fa-volume-down" : "fa-volume-off"
		);
	}
	addTrackOption(i, title) {
		let el = document.createElement("li");
		el.className = "track-option";
		el.setAttribute("data-track", i);
		el.innerHTML = `${i + 1}. ${title}`;
		el.addEventListener("click", e => {
			this.currentTrack = parseInt(e.target.getAttribute("data-track"));
			this.closeMenu();
			this.load();
		});
		this.controls.trackList.parent.appendChild(el);
	}
	validFile(fileName) {
		return /(\.mp3|\.mp4|\.wav|\.flac|\.ogg)/gi.test(fileName);
	}
	uploadFile(title, data) {
		this.files[data.name] = this.currentFile = {
			title,
			data
		};
		this.fileNames.push(data.name);
		this.trackTitles.push(title);
		this.currentTrack = this.fileNames.length - 1;
		this.addTrackOption(this.fileNames.length - 1, title);
		this.closeMenu();
		this.play();
	}
	closeMenu() {
		this.controls.menu.checkbox.checked = false;
	}
	changeTrack(e) {
		let value = parseInt(e.target.getAttribute("data-value"));
		this.currentTrack += value;
		if (this.currentTrack < 0) this.currentTrack = this.fileNames.length - 1;
		if (this.currentTrack > this.fileNames.length - 1) this.currentTrack = 0;
		this.load();
	}
	playPause() {
		if (this.playing) {
			this.controls.play.classList.remove("fa-pause");
			this.controls.play.classList.add("fa-play");
			this.playing = false;
			this.element.pause();
		} else {
			this.controls.play.classList.remove("fa-play");
			this.controls.play.classList.add("fa-pause");
			this.playing = true;
			this.element.play();
		}
	}
	load() {
		this.controls.parent.classList.add("disabled");
		this.loader.classList.remove("hidden");
		this.loader.classList.add("loading");
		if (this.files[this.fileNames[this.currentTrack]]) {
			this.currentFile = this.files[this.fileNames[this.currentTrack]];
			this.play();
		} else {
			let request = new XMLHttpRequest();

			request.open("GET", this.baseURL + this.fileNames[this.currentTrack], true);
			request.responseType = "blob";
			request.onload = () => {
				this.files[this.fileNames[this.currentTrack]] = this.currentFile = {
					title: this.trackTitles[this.currentTrack],
					data: request.response
				};
				this.play();
			};
			request.send();
		}
	}
	play() {
		this.playing = true;
		this.audioReady = true;
		this.controls.parent.classList.remove("disabled");
		this.titleLabel.innerHTML = this.currentFile.title;
		this.loader.classList.remove("loading");
		this.loader.classList.add("hidden");
		this.element.src = window.URL.createObjectURL(this.currentFile.data);
		this.element.play();
	}
	getFrequencyData() {
		this.analyser.getByteFrequencyData(this.freqData);
		return this.freqData;
	}
}

class Canvas {
	constructor(selector) {
		this.element =
			document.querySelector(selector) ||
			(() => {
				let element = document.createElement("canvas");
				element.style = `position: absolute; top: 0; left: 0; z-index: 0; width: 100vw; height: calc(100vh - 65px);`;
				document.body.appendChild(element);
				return element;
			})();
		this.ctx = this.element.getContext("2d");
		this.frame = document.createElement("canvas");
		this.buffer = this.frame.getContext("2d");
		this.dimensions = new Vector2();
		this.origin = new Vector2();
		window.addEventListener("resize", this.resize.bind(this));
		this.resize();
	}
	resize() {
		this.dimensions.x = this.frame.width = this.element.width = window.innerWidth;
		this.dimensions.y = this.frame.height = this.element.height =
			window.innerHeight;
		this.origin.x = 0.5 * this.dimensions.x;
		this.origin.y = this.dimensions.y;
	}
	clear() {
		this.ctx.clearRect(0, 0, this.dimensions.x, this.dimensions.y);
		this.buffer.clearRect(0, 0, this.dimensions.x, this.dimensions.y);
	}
	line(x1, y1, x2, y2, w, c) {
		this.buffer.beginPath();
		this.buffer.strokeStyle = c;
		this.buffer.lineWidth = w;
		this.buffer.moveTo(x1, y1);
		this.buffer.lineTo(x2, y2);
		this.buffer.stroke();
		this.buffer.closePath();
	}
	fill(c) {
		this.buffer.fillStyle = c;
		this.buffer.fillRect(0, 0, this.dimensions.x, this.dimensions.y);
	}
	rect(x, y, w, h, c) {
		this.buffer.fillStyle = c;
		this.buffer.fillRect(x, y, w, h);
	}
	arc(x, y, r, s, e, c) {
		this.buffer.beginPath();
		this.buffer.fillStyle = c;
		this.buffer.arc(x, y, r, s, e);
		this.buffer.fill();
		this.buffer.closePath();
	}
	render() {
		this.ctx.drawImage(this.frame, 0, 0);
	}
	drawImage(image, x = 0, y = 0) {
		this.buffer.drawImage(image, x, y);
	}
}

class MPApp {
	constructor() {
		this.canvas = new Canvas();
		this.audio = new AudioController();
		this.particles = new ParticleController(
			this.audio.analyser.frequencyBinCount,
			this.audio.analyser.frequencyBinCount,
			this.canvas
		);
		this.initUI();
		this.update();
	}
	initUI() {
		this.controls = {
			particles: document.querySelector("#chk-particles"),
			backlight: document.querySelector("#chk-backlight"),
			spectrum: document.querySelector("#chk-spectrum"),
			glow: document.querySelector("#chk-glow")
		};
		this.drawParticles = this.controls.particles.checked;
		this.controls.particles.addEventListener(
			"click",
			e => (this.drawParticles = e.target.checked)
		);
		this.backlight = this.controls.backlight.checked;
		this.controls.backlight.addEventListener(
			"click",
			e => (this.backlight = e.target.checked)
		);
		this.spectrum = this.controls.spectrum.checked;
		this.controls.spectrum.addEventListener(
			"click",
			e => (this.spectrum = e.target.checked)
		);
		this.glow = this.controls.glow.checked;
		this.controls.glow.addEventListener(
			"click",
			e => (this.glow = e.target.checked)
		);
	}
	draw(freqData) {
		let x, y, norm, hue, scale, data;

		this.particles.delta += 0.15;
		this.canvas.clear();
		this.canvas.buffer.globalCompositeOperation = "lighter";
		for (let i = 0; i < this.particles.count; i++) {
			data = freqData[i];
			if (this.drawParticles) {
				this.particles.drawRenderTarget(i, data);
			}
			if (this.spectrum && !(i % 2)) {
				x = i / freqData.length * this.canvas.origin.x;
				y = this.canvas.dimensions.y;
				norm = data / 256;
				hue = 90 * (1 - norm);
				scale = norm * (0.25 * this.canvas.dimensions.y);

				this.canvas.line(
					this.canvas.origin.x + x + 1,
					y,
					this.canvas.origin.x + x + 1,
					y - scale,
					1,
					`hsla(${hue}, 75%, 50%, 1)`
				);
				this.canvas.line(
					this.canvas.origin.x - x - 1,
					y,
					this.canvas.origin.x - x - 1,
					y - scale,
					1,
					`hsla(${hue}, 75%, 50%, 1)`
				);
			}
		}
		if (this.glow) {
			this.drawGlowLayer();
		}
		if (this.backlight) {
			this.drawBacklight(freqData);
		}
		this.canvas.render();
	}
	drawBacklight(freqData) {
		let avg, hue, gradient;
		avg = freqData.reduce((a, b) => a + b + 1) / freqData.length;
		hue = 128 - avg;
		gradient = this.canvas.buffer.createRadialGradient(
			this.canvas.origin.x,
			this.canvas.origin.y,
			0,
			this.canvas.origin.x,
			this.canvas.origin.y,
			0.5 * this.canvas.dimensions.x
		);
		gradient.addColorStop(0, `hsla(${hue}, 75%, 70%, ${pow(avg / 128, 2)})`);
		gradient.addColorStop(1, `hsla(${hue}, 75%, 40%, 0)`);
		this.canvas.rect(
			0,
			0,
			this.canvas.dimensions.x,
			this.canvas.dimensions.y,
			gradient
		);
	}
	drawGlowLayer() {
		this.canvas.buffer.save();
		this.canvas.buffer.filter = "blur(8px)";
		this.canvas.buffer.drawImage(this.canvas.frame, 0, 0);
		this.canvas.buffer.restore();
	}
	update() {
		this.draw(this.audio.getFrequencyData());
		window.requestAnimationFrame(this.update.bind(this));
	}
}

window.addEventListener("load", () => {
	let app = new MPApp();
});

Musical Particles II

"There is geometry in the humming of the strings, there is music in the spacing of the spheres."

~ Pythagoras

A Pen by HARUN PEHLİVAN on CodePen.

License.

#ui-overlay
	#loader.hidden.loading
		-for (let i = 0; i < 5; i++)
			.load-bar
	input#chk-menu-toggle(type="checkbox" name="chk-menu-toggle")
	label#lbl-menu-toggle.hidden(for="chk-menu-toggle")
		span.bar.line
		span.bar.cross
		span.bar.cross
		span.bar.line
	#ui-drawer
		.ui-drawer__container
			.drawer-folder
				.drawer-folder__label Visuals
				.drawer-folder__option.option--checkbox
					label.option__label(for="chk-particles")
						input#chk-particles(type="checkbox" checked=true)
						i.fa.fa-spinner
						| Particles
				.drawer-folder__option.option--checkbox
					label.option__label(for="chk-backlight")
						input#chk-backlight(type="checkbox" checked=true)
						i.fa.fa-lightbulb-o
						| Backlight
				.drawer-folder__option.option--checkbox
					label.option__label(for="chk-spectrum")
						input#chk-spectrum(type="checkbox" checked=true)
						i.fa.fa-signal
						| Spectrum
				.drawer-folder__option.option--checkbox
					label.option__label(for="chk-glow")
						input#chk-glow(type="checkbox" checked=true)
						i.fa.fa-sun-o
						| Glow
			.drawer-folder
				.drawer-folder__label Audio
				.drawer-folder__option
					.option__label#track--label 
						i.fa.fa-music
						| Tracks
						.btn#btn-add-track
							input#in-add-track(type="file" accept="audio/*")
							label#lbl-add-track(for="in-add-track")
								i.fa.fa-plus
					.option__sub-menu
						ul#track-list
						form#frm-track-title.hidden(name="frm-track-title")
							label#lbl-track-title(for="txt-track-title") Enter a Track Title:
							input#txt-track-title(type="text" name="title" placeholder="Track Title" maxlength=50 required)
							button#btn-submit-title 
								| Submit
								i.fa.fa-caret-right
	#audio-controls.hidden.disabled
		#seek-bar
			#progress-bar
		#controls__container
			#button-controls
				.btn
					i.fa.fa-step-backward#btn-prev(data-value="-1")
				.btn
					i.fa.fa-pause#btn-play
				.btn
					i.fa.fa-step-forward#btn-next(data-value="1")
				.btn
					i.fa.fa-volume-up#icon-volume
						span.input-container
							input#rng-volume(type="range" value=0.8 min=0 max=1 step=0.1)
			#title

	#btn-initialize Click to Start

a#codepen-link(href='https://www.codepen.io/harunpehlivan' target='_blank')