<canvas></canvas>
*, *:before, *:after {
	box-sizing: border-box;
	margin: 0;
	padding: 0;
}
body {
	display: flex;
	justify-content: center;
	height: 100vh;
	overflow: hidden;
}
canvas {
	display: block;
	margin: auto;
	max-width: 1136px;
}
window.requestAnimFrame = (function(){
  return window.requestAnimationFrame || 
      window.webkitRequestAnimationFrame || 
      window.mozRequestAnimationFrame || 
      window.oRequestAnimationFrame ||
      window.msRequestAnimationFrame ||
        function(callback){
          window.setTimeout(callback, 1000/60);
        };
})();
window.addEventListener("load",app);

function app() {
	var canvas = document.getElementsByTagName("canvas")[0],
		ctx = canvas.getContext("2d"),
		// canvas dimensions
		w = 568,
		h = 320,
		// scale, keep at 2 for best retina results
		s = 2;
	// set canvas dimensions with scale
	canvas.width = w * s;
	canvas.height = h * s;
	canvas.style.width = "auto";
    canvas.style.height = "100%";
	ctx.scale(s,s);
	
	var rand = function(min, max) {
			return Math.floor(Math.random() * ((max + 1) - min)) + min;
		},
		ribbon = function() {
			let colors = ["#f0f","#f80","#f81","#ff0","#8c4","#0ff","#00f"];
			this.r = rand(1,2);
			this.rLimit = this.r + rand(6,12);
			this.rInc = this.rLimit/100;
			this.dir = rand(0,1);
			// -1 = left/up, 0 = right/down
			if (this.dir === 0) {
				--this.dir;
			}
			this.dirAdjChance = 0.01;
			this.dirAdjLimit = 2;
			this.dirTimesAdj = 0;
			this.axis = rand(0,1); // 0 = x, 1 = y
			this.x = 0;
			this.y = 0;
			if (this.axis == 1) {
				this.x = rand(0,w);
				this.y = (this.dir == 1 ? -this.rLimit : h + this.rLimit);
			} else {
				this.x = (this.dir == 1 ? -this.rLimit : w + this.rLimit);
				this.y = rand(0,h);
			}
			this.speed = 2;
			this.amp = rand(1,2);
			this.freq = rand(100,400);
			this.freqAdjChance = 0.05;
			this.freqMaxAdj = 10;
			this.color = colors[rand(0,colors.length-1)];
			this.opDec = 0.002;
			this.opDelayInc = 0.001;
			this.lightMid = rand(0,1);
			this.lightColor = "#fff";
			let colorIndex = colors.indexOf(this.color);
			if (colorIndex == 1 || colorIndex == 4) {
				this.lightColor = "#ff0";
			}
			this.exitedX = false;
			this.exitedY = false;
			this.members = [];
		},
		rbns = [],
		setup = function() {
			let ribbonCt = 6;
			for (let r = 0; r < ribbonCt; ++r) {
				rbns.push(new ribbon());
			}
		},
		drawScreen = function() {
			ctx.globalAlpha = 1;
			ctx.fillStyle = "#000";
			ctx.fillRect(0,0,w,h);
			
			for (let i in rbns) {
				let fill;
				// ribbon coloring
				if (rbns[i].lightMid) {
					fill = ctx.createRadialGradient(
						rbns[i].x,rbns[i].y,rbns[i].r/2,
						rbns[i].x,rbns[i].y,rbns[i].r*1
					);
					fill.addColorStop(0,rbns[i].lightColor);
					fill.addColorStop(1,rbns[i].color);
				} else {
					fill = rbns[i].color;
				}
				// render ribbon head
				ctx.fillStyle = fill;
				ctx.globalAlpha = 0;
				ctx.beginPath();
				ctx.arc(rbns[i].x,rbns[i].y,rbns[i].r,0,2*Math.PI);
				ctx.fill();
				ctx.closePath();
				
				for (let m in rbns[i].members) {
					// render members
					let mmbr = rbns[i].members;
					
					if (rbns[i].lightMid) {
						fill = ctx.createRadialGradient(
							mmbr[m].x,mmbr[m].y,mmbr[m].r/2,
							mmbr[m].x,mmbr[m].y,mmbr[m].r
						);
						fill.addColorStop(0,rbns[i].lightColor);
						fill.addColorStop(1,rbns[i].color);
						ctx.fillStyle = fill;
					}
					ctx.globalAlpha = mmbr[m].op > 1 ? 1 : mmbr[m].op;
					ctx.beginPath();
					ctx.arc(mmbr[m].x,mmbr[m].y,mmbr[m].r,0,2*Math.PI);
					ctx.fill();
					ctx.closePath();
					
					// update chain
					if (mmbr[m].r < rbns[i].rLimit) {
						mmbr[m].r += rbns[i].rInc;
					}
					if (rbns[i].exitedX || rbns[i].exitedY) {
						mmbr[m].op -= rbns[i].opDec;
						if (mmbr[m].op < 0) {
							rbns[i].members.pop();
						}
					}
				}
				if (!rbns[i].exitedX && !rbns[i].exitedY) {
					// generate new member with current position
					rbns[i].members.unshift(
						{
							x: rbns[i].x,
							y: rbns[i].y,
							r: rbns[i].r,
							op: rbns[i].members.length === 0 ? 0.1 : rbns[i].members[0].op + rbns[i].opDelayInc
						}
					);
					// update position
					if (rbns[i].axis == 1) {
						rbns[i].x += rbns[i].amp * Math.cos(Math.PI*rbns[i].y * rbns[i].freq**-1)
						rbns[i].y += rbns[i].speed * rbns[i].dir;
					} else {
						rbns[i].x += rbns[i].speed * rbns[i].dir;
						rbns[i].y += rbns[i].amp * Math.cos(Math.PI*rbns[i].x * rbns[i].freq**-1);
					}
					// randomly alter path
					if (Math.random() < rbns[i].dirAdjChance && rbns[i].dirTimesAdj < rbns[i].dirAdjLimit) {
						rbns[i].dir = -rbns[i].dir
						++rbns[i].dirTimesAdj;
					}
					if (Math.random() < rbns[i].freqAdjChance) {
						rbns[i].freq = rand(rbns[i].freq-rbns[i].freqMaxAdj,rbns[i].freq+rbns[i].freqMaxAdj);
					}
				}
				// redefine ribbon on exit and after all members are dead
				if (rbns[i].x > w + rbns[i].rLimit || rbns[i].x < -rbns[i].rLimit) {
					rbns[i].exitedX = true;
				}
				if (rbns[i].y > h + rbns[i].rLimit || rbns[i].y < -rbns[i].rLimit) {
					rbns[i].exitedY = true;
				}
				if ((rbns[i].exitedX || rbns[i].exitedY) && rbns[i].members.length === 0) {
					rbns[i] = new ribbon();
				}
			}
		},
		run = function(){
			drawScreen();
			requestAnimFrame(run);
		};
	setup();
	run();
}

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.