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();
});
~ Pythagoras
A Pen by HARUN PEHLİVAN on CodePen.
#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')