<div id="jsi-rain-container" class="container"></div>
html,body {
    width: 100%;
    height: 100%;
    margin: 0;
    padding: 0;
    overflow: hidden;
}
.container{
    width: 100%;
    height: 100%;
    margin: 0;
    padding: 0;
}
var RENDERER = {
	INIT_RAIN_DROP_COUNT : 1500,
	RAIN_DROP_COUNT : 8,
	HUMAN_COUNT : 30,
	COLOR : 'hsl(%hue, 20%, %luminance%)',
	HUE_OFFSET : Math.PI / 1000,
	LUMINANCE_OFFSET : Math.PI / 1500,
	
	init : function(){
		this.setParameters();
		this.reconstructMethod();
		this.createRainDrops(this.INIT_RAIN_DROP_COUNT, true);
		this.createHumans();
		this.render();
	},
	setParameters : function(){
		this.$container = $('#jsi-rain-container');
		this.width = this.$container.width();
		this.height = this.$container.height();
		this.context = $('<canvas />').attr({width : this.width, height : this.height}).appendTo(this.$container).get(0).getContext('2d');
		
		this.rainDrops = [];
		this.humans = [];
		this.theta = 0;
		this.phi = 0;
	},
	reconstructMethod : function(){
		this.render = this.render.bind(this);
	},
	getRandomValue : function(range){
		return range.min + (range.max - range.min) * Math.random();
	},
	createRainDrops : function(count, toInit){
		for(var i = 0; i < count; i++){
			this.rainDrops.push(new RAIN_DROP(this.width, this.height, toInit, this));
		}
	},
	createHumans : function(){
		for(var i = 0, length = this.HUMAN_COUNT; i < length; i++){
			this.humans.push(new HUMAN(this.width, this.height, this));
		}
	},
	render : function(){
		requestAnimationFrame(this.render);
		
		this.color = this.COLOR.replace('%hue', 205 + 5 * Math.sin(this.phi));
		this.context.fillStyle = this.color.replace('%luminance', 35 + 5 * Math.sin(this.theta));
		this.context.fillRect(0, 0, this.width, this.height);
		
		for(var i = this.rainDrops.length - 1; i >= 0; i--){
			if(!this.rainDrops[i].render(this.context, false)){
				this.rainDrops.splice(i, 1);
			}
		}
		this.humans.sort(function(human1, human2){
			return human1 - human2;
		});
		for(var i = 0, length = this.humans.length; i < length; i++){
			this.humans[i].renderShadow(this.context);
		}
		for(var i = 0, length = this.humans.length; i < length; i++){
			this.humans[i].renderSubstance(this.context);
		}
		for(var i = this.rainDrops.length - 1; i >= 0; i--){
			if(!this.rainDrops[i].render(this.context, true)){
				this.rainDrops.splice(i, 1);
			}
		}
		this.createRainDrops(this.RAIN_DROP_COUNT, false);
		this.theta += this.LUMINANCE_OFFSET;
		this.theta %= Math.PI * 2;
		this.phi += this.HUE_OFFSET;
		this.phi %= Math.PI * 2;
	}
};
var RAIN_DROP = function(width, height, toInit, renderer){
	this.width = width;
	this.height = height;
	this.toInit = toInit;
	this.renderer = renderer;
	
	this.init();
};
RAIN_DROP.prototype = {
	SCALE_RANGE : {min : 0.2, max : 1},
	VELOCITY_RANGE : {min : -1.5, max : -1},
	VELOCITY_RATE : 3,
	LENGTH_RATE : 20,
	ACCELARATION_RATE : 0.01,
	VERTICAL_OFFSET_RATE : 0.04,
	FRONT_THRESHOLD : 0.8,
	REFLECTION_RADIUS_RATE : 0.02,
	COLOR : 'rgba(255, 255, 255, 0.5)',
	RADIUS_RATE : 0.2,
	THRESHOLD_RATE : 0.6,
	
	init : function(){
		this.scale = this.renderer.getRandomValue(this.SCALE_RANGE);
		this.length = this.LENGTH_RATE * this.scale;
		this.vx = this.renderer.getRandomValue(this.VELOCITY_RANGE) * this.scale;
		this.vy = this.VELOCITY_RATE * this.scale;
		this.ay = this.ACCELARATION_RATE * this.scale;
		
		this.theta = Math.atan2(this.vy, this.vx);
		
		this.offset = this.height * this.VERTICAL_OFFSET_RATE;
		this.x = this.renderer.getRandomValue({min : 0, max : this.width - this.height * Math.cos(this.theta)});
		this.y = (this.toInit ? this.renderer.getRandomValue({min : 0, max : this.height}) : 0) - this.offset;
		
		this.radius = this.length * this.REFLECTION_RADIUS_RATE;
	},
	render : function(context, toFront){
		if(toFront && this.scale < this.FRONT_THRESHOLD || !toFront && this.scale >= this.FRONT_THRESHOLD){
			return true;
		}
		context.save();
		context.strokeStyle = this.COLOR;
		
		if(this.y >= this.height * (1 - (1 - this.scale) * this.THRESHOLD_RATE) - this.offset){
			context.lineWidth = 3;
			context.globalAlpha = (1 - this.radius / this.length / this.RADIUS_RATE) * 0.5;
			context.beginPath();
			context.arc(this.x, this.y, this.radius, Math.PI, Math.PI * 2, false);
			context.stroke();
			context.restore();
			
			this.radius *= 1.05;
			
			if(this.radius > this.length * this.RADIUS_RATE){
				return false;
			}
		}else{
			context.lineWidth = 1;
			context.beginPath();
			context.moveTo(this.x, this.y);
			context.lineTo(this.x + this.length * Math.cos(this.theta), this.y + this.length * Math.sin(this.theta));
			context.stroke();
			context.restore();
			
			this.x += this.vx;
			this.y += this.vy;
			this.vy += this.ay;
		}
		return true;
	}
};
var HUMAN = function(width, height, renderer){
	this.width = width;
	this.height = height;
	this.renderer = renderer;
	
	this.init();
};
HUMAN.prototype = {
	HORIZONTAL_OFFSET : 30,
	VERTICAL_OFFSET_RATE_RANGE : {min : 0.05, max : 0.45},
	VELOCITY_OFFSET : {min : 0.3, max : 0.6},
	SHADOW_SCALE : -0.6,
	
	init : function(){
		this.setParameters(true);
	},
	setParameters : function(toInit){
		this.direction = (Math.random() < 0.5) ? 1 : -1;
		
		if(toInit){
			this.x = this.renderer.getRandomValue({min : 0, max : this.width});
		}else{
			this.x = (this.direction > 0) ? -this.HORIZONTAL_OFFSET : (this.width + this.HORIZONTAL_OFFSET);
		}
		var position = this.renderer.getRandomValue(this.VERTICAL_OFFSET_RATE_RANGE) * this.height,
			verticalPosition = this.height * this.VERTICAL_OFFSET_RATE_RANGE.max;
			
		this.rate = 10 * (verticalPosition - position) / verticalPosition | 0;
		this.y = this.height - position;
		this.vx = this.renderer.getRandomValue(this.VELOCITY_OFFSET) * this.direction;
		this.vxRate = (0.5 + 0.5 * (verticalPosition - position) / verticalPosition);
		this.theta = 0;
	},
	renderSubstance : function(context){
		context.save();
		context.translate(this.x, this.y);
		context.scale(this.direction * this.vxRate, 1 * this.vxRate);
		this.renderHuman(context, 30);
		context.restore();
		
		this.theta += this.vx / 5;
		this.theta %= Math.PI * 2;
		this.x += this.vx * this.vxRate;
		
		if(this.x < -this.HORIZONTAL_OFFSET && this.direction < 0 || this.x > this.width + this.HORIZONTAL_OFFSET && this.direction > 0){
			this.setParameters(false);
		}
	},
	renderShadow : function(context){
		context.save();
		context.translate(this.x, this.y);
		context.scale(this.direction * this.vxRate, this.SHADOW_SCALE * this.vxRate);
		this.renderHuman(context, 35);
		context.restore();
	},
	renderHuman : function(context, baseColor){
		var color = this.renderer.color.replace('%luminance', baseColor - this.rate);
		
		context.fillStyle = color;
		context.strokeStyle = color;
		context.lineWidth = 2;
		
		context.beginPath();
		context.moveTo(-30, -60);
		context.quadraticCurveTo(-20, -75, 0, -75);
		context.quadraticCurveTo(20, -75, 30, -60);
		context.closePath();
		context.fill();
		
		context.beginPath();
		context.moveTo(0, -80);
		context.lineTo(0, -35);
		context.stroke();
		
		context.beginPath();
		context.arc(-10, -55, 5, 0, Math.PI * 2, false);
		context.fill();
		
		context.beginPath();
		context.moveTo(-15, -50);
		context.quadraticCurveTo(-20, -40, -15, -30);
		context.lineTo(-5, -30);
		context.quadraticCurveTo(0, -40, -5, -50);
		context.closePath();
		context.fill();
		
		for(var i = -1; i <= 1; i += 2){
			context.beginPath();
			context.moveTo(-13, -30);
			context.lineTo(-13 + 7 * Math.sin(this.theta) * i, -5);
			context.lineTo(-3 + 7 * Math.sin(this.theta) * i, -5);
			context.lineTo(-7 + 7 * Math.sin(this.theta) * i, -10);
			context.lineTo(-7, -30);
			context.closePath();
			context.fill();
		}
	}
};
$(function(){
	RENDERER.init();
});

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js