Snowfall

This is a showcase of a "snowfall" effect; a simple animation of falling snowflakes. The goal is to use relatively little battery power.

This effect isn't really practical yet, it's more of a proof of concept. Future work would involve making it less distracting, and make it not cover up the text.

This website needs some text on it in order to show how the effect would actually look on a real website, not just a uniform dark background. Therefore, all the javascript used for the effect is included here.

(function() {
let element = document.body;

let flakes_per_sec = 1;
let fps = 1;

let img_url = "snowflakes.png";
let img_height = 13;
let img_width = 13;

function randint(from, to) {
	return Math.floor(Math.random() * (to - from) + from);
}

function random(from, to) {
	return Math.random() * (to - from) + from;
}

class ImgFrame {
	constructor(img, width, height, index) {
		this.img = img;
		this.width = width;
		this.height = height;
		this.y = height * index;
	}

	draw(ctx, x, y, scale, rotation) {
		ctx.translate(x, y);
		ctx.rotate(rotation);
		ctx.scale(scale, scale);
		ctx.drawImage(this.img,
			0, this.y, this.width, this.height,
			0, 0, this.width, this.height);
		ctx.setTransform(1, 0, 0, 1, 0, 0);
	}
}

class Snowflake {
	constructor(frame, x, y, scale, rotation) {
		this.frame = frame;
		this.x = x;
		this.y = y;
		this.scale = scale;
		this.rot = rotation;
		this.vx = random(-10, 10);
		this.vy = random(10, 20);
		this.vrot = random(-0.2, 0.2);
		this.dead = false;
	}

	update(canvas, dt) {
		this.x += this.vx * dt;
		this.y += this.vy * dt;
		this.rot += this.vrot * dt;

		this.vx += random(-2, 2) * dt;
		this.vy += random(-2, 2) * dt;
		this.vrot += random(-0.1, 0.1) * dt;

		if (this.vy < -1)
			this.vy = -1;

		if (this.y >= canvas.height + this.frame.height * this.scale)
			this.dead = true;
	}

	draw(ctx) {
		this.frame.draw(ctx, this.x, this.y, this.scale, this.rot);
	}
}

let flakes = [];
let flake_acc = 0;

function update(ctx, dt, frames) {

	// Spawn new flakes
	flake_acc += dt * flakes_per_sec;
	while(flake_acc > 0) {
		let scale = random(1, 2.5);
		let spawn_y = -(img_height * scale);
		flakes.push(new Snowflake(
			frames[randint(0, frames.length)],
			random(-img_width * scale, element.scrollWidth),
			random(spawn_y - 200, spawn_y),
			scale,
			random(-Math.PI, Math.PI)));
		flake_acc -= 1;
	}

	// Clear canvas
	ctx.canvas.width = ctx.canvas.width;
	ctx.webkitImageSmoothingEnabled = false;
	ctx.mozImageSmoothingEnabled = false;
	ctx.imageSmoothingEnabled = false;

	// Update and draw
	flakes.forEach((f, index) => {
		f.update(ctx.canvas, dt);
		if (f.dead)
			flakes.splice(index, 1);
		else
			f.draw(ctx);
	});
}

function resize(canvas, ctx) {
	let bounds = element.getBoundingClientRect();
	canvas.style =
		"position: absolute;" +
		"top: " + bounds.top + "px;" +
		"left: " + bounds.left + "px;" +
		"pointer-events: none;" +
		"image-rendering: -moz-crisp-edges;" +
		"image-rendering: -webkit-crisp-edges;" +
		"image-rendering: pixelated;";
	canvas.width = element.scrollWidth;
	canvas.height = element.scrollHeight;
}

function setup(img) {
	let img_count = Math.floor(img.height / img_height);

	let canvas = document.createElement("canvas");
	document.body.appendChild(canvas);
	let ctx = canvas.getContext("2d");

	resize(canvas, ctx);
	window.addEventListener("resize", resize.bind(null, canvas, ctx));

	let frames = new Array(img_count).fill(null).map((_, index) =>
		new ImgFrame(img, img_width, img_height, index));

	update(ctx, 1 / fps, frames);
	setInterval(update.bind(null, ctx, 1 / fps, frames), (1 / fps) * 1000);
}

let img = new Image();
img.src = img_url;
img.onload = function() {
	setup(img);
}
})();