Create Stunning Rain & Snow Effects with a Canvas Drop Component
This article walks through building a reusable canvas‑based drop component that renders realistic rain and snow animations, explains the underlying JavaScript architecture, and provides complete source code with usage examples and future enhancement ideas.
Article Origin
The author needed rain and snow animation effects for a recent project, so a reusable canvasDrop component was created to demonstrate common falling‑object animations on an HTML5 canvas.
Demo and Usage
Include a canvas element and the script, then initialize the component:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<style>#canvas{width:100%;height:100%;}</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script src="canvasDrop.js"></script>
<script>
canvasDrop.init({
type: "rain",
speed: [0.4,2.5],
size_range: [0.5,1.5],
hasBounce: true,
wind_direction: -105,
hasGravity: true
});
</script>
</body>
</html>Two screenshots show the rain and snow results.
Using canvas for many moving objects is simpler and more performant than creating DOM elements.
Source Code Explanation
First, several global variables are defined, including wind direction, gravity, and a requestAnimFrame polyfill.
Core Objects
Vector : stores horizontal ( x ) and vertical ( y ) speed values and provides add and copy methods.
The Drop object represents a raindrop or snowflake. Its constructor randomly sets the initial position, radius, speed, and velocity based on the configured wind angle.
var Drop = function() {
var randomEdge = Math.random()*2;
if(randomEdge > 1){
this.pos = new Vector(50 + Math.random() * canvas.width, -80);
}else{
this.pos = new Vector(canvas.width, Math.random() * canvas.height);
}
this.radius = (OPTS.size_range[0] + Math.random() * OPTS.size_range[1]) * DPR;
this.speed = (OPTS.speed[0] + Math.random() * OPTS.speed[1]) * DPR;
this.prev = this.pos;
var eachAnger = 0.017453293;
wind_anger = OPTS.wind_direction * eachAnger;
speed_x = this.speed * Math.cos(wind_anger);
speed_y = -this.speed * Math.sin(wind_anger);
this.vel = new Vector(wind_x, wind_y);
};The update method applies gravity (if enabled) and moves the drop, while the draw method renders a line for rain (using a Bézier curve) or a circle for snow.
Drop.prototype.update = function(){
this.prev = this.pos.copy();
if(OPTS.hasGravity){ this.vel.y += gravity; }
this.pos.add(this.vel);
};
Drop.prototype.draw = function(){
ctx.beginPath();
if(OPTS.type == "rain"){
ctx.moveTo(this.prev.x, this.prev.y);
var ax = Math.abs(this.radius * Math.cos(wind_anger));
var ay = Math.abs(this.radius * Math.sin(wind_anger));
ctx.bezierCurveTo(this.pos.x+ax, this.pos.y+ay, this.prev.x+ax, this.prev.y+ay, this.pos.x, this.pos.y);
ctx.stroke();
}else{
ctx.arc(this.pos.x, this.pos.y, this.radius, 0, Math.PI*2);
ctx.fill();
}
};Bounce : simple bounce objects created when a drop hits the ground; they have their own update and draw methods.
External Interface
update
Runs the animation loop: clears the canvas, updates and draws all drops and bounces, creates new drops when needed, and schedules the next frame with requestAnimFrame.
function update(){
var d = new Date;
ctx.clearRect(0,0,canvas.width,canvas.height);
var i = drops.length;
while(i--){
var drop = drops[i];
drop.update();
if(drop.pos.y >= canvas.height){
if(OPTS.hasBounce){
var n = Math.round(4 + Math.random()*4);
while(n--) bounces.push(new Bounce(drop.pos.x, canvas.height));
}
drops.splice(i,1);
}
drop.draw();
}
if(OPTS.hasBounce){
var i = bounces.length;
while(i--){
var bounce = bounces[i];
bounce.update();
bounce.draw();
if(bounce.pos.y > canvas.height) bounces.splice(i,1);
}
}
if(drops.length < OPTS.maxNum && Math.random() < drop_chance){
for(var j=0, len=OPTS.numLevel; j<len; j++) drops.push(new Drop());
}
requestAnimFrame(update);
}init
Initializes the canvas, handles high‑DPI screens, sets canvas dimensions, and calls setStyle to configure line width and colors based on the animation type.
function init(opts){
OPTS = opts;
canvas = document.getElementById(opts.id);
ctx = canvas.getContext("2d");
DPR = window.devicePixelRatio;
canvasWidth = canvas.clientWidth * DPR;
canvasHeight = canvas.clientHeight * DPR;
canvas.width = canvasWidth;
canvas.height = canvasHeight;
drop_chance = 0.4;
setStyle();
}
function setStyle(){
if(OPTS.type == "rain"){
ctx.lineWidth = 1 * DPR;
ctx.strokeStyle = 'rgba(223,223,223,0.6)';
ctx.fillStyle = 'rgba(223,223,223,0.6)';
}else{
ctx.lineWidth = 2 * DPR;
ctx.strokeStyle = 'rgba(254,254,254,0.8)';
ctx.fillStyle = 'rgba(254,254,254,0.8)';
}
}Conclusion
The simple drop component is now functional, but it has limitations: the public API is minimal, style customization is basic, and there is no support for pausing, speeding up, or slowing down the animation. Future work could expose more parameters, allow custom update / draw logic for drops and bounces, and add pause/acceleration controls.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Tencent IMWeb Frontend Team
IMWeb Frontend Community gathering frontend development enthusiasts. Follow us for refined live courses by top experts, cutting‑edge technical posts, and to sharpen your frontend skills.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
