Skip to main content
Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.

Making Snow Animations In Angular 11.0.5

By Ben Nadel on

Way back in the day, when I was a developer at Koko Interactive, we used to send out Macromedia-Flash-based Christmas cards. These cards often had some sort of snow-flake animations in which lots of little dots danced their way down the screen using ActionScript and timeline sprites. As a fun code kata, I thought it would be interesting to try and recreate this kind of technique using CSS animations in Angular 11.0.5.

Run this demo in my JavaScript Demos project on GitHub.

View this code in my JavaScript Demos project on GitHub.

When I used to create these snowscapes back in Macromedia Flash, each Flake was an instance of a movie clip that existed on the "stage". Then, as the timeline moved forward, each individual clip was responsible for updating its own X (horizontal) and Y (vertical) coordinates. Back in the day, these values were calculated using trigonometry (some such Sin / CoSin nonsense which I have long-since forgotten how to do); but, with today's modern CSS capabilities, I am going to achieve the same effect using the animation properties.

When a snow flake floats from the top of the screen down to the bottom of the screen, it's actually moving in two different directions at the same time: as it moves vertical downward, it's also moving back and forth horizontally. These two movements together make for a decently natural look-and-feel.

To animate one Element in two parallel directions, all we have to do is wrap the Element in two parents and then animate each parent element independently. Imagine this structure:

<div class="vertical-track">
	<div class="horizontal-track">
		<span class="flake"> . </span>
	</div>
</div>

With this structure, the "flake" itself doesn't actually move - it's the "horizontal-track" parent that is moving back-and-forth horizontally; and, it's the "vertical-track" parent that is moving from top-to-bottom. Under the hood, each of these "track" elements has its own animation property that is driven by its own set of @keyframes.

If all snowflakes moved with the same speed and timing, it would look very artificial. So, in order to introduce some natural variance, we can use inline style properties to override the animation-duration and animation-delay properties. The animation-duration affects the overall speed of the movements while the animation-delay affects where in the timeline the animation starts. This latter effect is used to make sure that all snowflakes don't start at the top of the screen.

To get this working, I created an App component that generates 150 snowflakes with random speeds, sizes, and offsets:

// Import the core angular services.
import { Component } from "@angular/core";

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

interface SnowFlakeConfig {
	depth: number ;
	left: number ;
	speed: number ;
}

@Component({
	selector: "app-root",
	styleUrls: [ "./app.component.less" ],
	templateUrl: "./app.component.html"
})
export class AppComponent {

	public snowFlakes: SnowFlakeConfig[];

	// I initialize the app component.
	constructor() {

		this.snowFlakes = [];

		for ( var i = 1 ; i <= 150 ; i++ ) {

			this.snowFlakes.push({
				depth: this.randRange( 1, 5 ),
				left: this.randRange( 0, 100 ),
				speed: this.randRange( 1, 5 )
			});

		}

	}

	// ---
	// PRIVATE METHODS.
	// ---

	// I generate a random integer between the given range, inclusive.
	private randRange( min: number, max: number ) : number {

		var range = ( max - min );

		return( min + Math.round( Math.random() * range ) );


	}

}

In this case, depth and speed are just abstractions that the <snow-flake> component is going to use to create variance. The left property is just used to position each snowflake horizontally on the screen (using viewport units):

<h1>
	Making Snow Animations In Angular 11.0.5
</h1>

<ng-template ngFor let-config [ngForOf]="snowFlakes">

	<snow-flake
		[depth]="config.depth"
		[speed]="config.speed"
		[style.left.vw]="config.left"
	></snow-flake>

</ng-template>

<div class="note">
	Photo from
	<a href="https://wallpaperaccess.com/winter-mountain" target="_blank">wallpaperaccess.com</a>
</div>

The <snow-flake> component is a pure (OnPush) component takes the depth and speed inputs and translates them into CSS animations and pixel sizes. Since the HTML template for the flake is fairly small, I've included it right in the component code-behind. Notice that I'm using inline styles to change the duration and delay of each animation:

// Import the core angular services.
import { ChangeDetectionStrategy } from "@angular/core";
import { Component } from "@angular/core";

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

@Component({
	selector: "snow-flake",
	inputs: [ "depth", "speed" ],
	changeDetection: ChangeDetectionStrategy.OnPush,
	styleUrls: [ "./snow-flake.component.less" ],
	template:
	`
		<div
			[style.animation-duration.s]="verticalDuration"
			[style.animation-delay.s]="verticalDelay"
			class="vertical-track">

			<div
				[style.animation-duration.s]="horizontalDuration"
				[style.animation-delay.s]="horizontalDelay"
				class="horizontal-track">

				<span
					[style.opacity]="flakeOpacity"
					[style.width.px]="flakeSize"
					[style.height.px]="flakeSize"
					class="flake">
				</span>

			</div>

		</div>
	`
})
export class SnowFlakeComponent {

	public depth: number;
	public speed: number;

	public flakeOpacity: number;
	public flakeSize: number;
	public horizontalDuration: number;
	public horizontalDelay: number;
	public verticalDelay: number;
	public verticalDuration: number;

	// I initialize the snow-flake component.
	constructor() {

		this.depth = 1;
		this.speed = 1;

		this.flakeOpacity = 1;
		this.flakeSize = 1;
		this.verticalDuration = 5;
		this.verticalDelay = 0;
		this.horizontalDuration = 3;
		this.horizontalDelay = 0;

	}

	// ---
	// PUBLIC MEHTODS.
	// ---

	// I get called whenever the input bindings change.
	public ngOnChanges() : void {

		switch ( this.speed ) {
			case 1:
				this.verticalDuration = 5;
				this.horizontalDuration = 3;
			break;
			case 2:
				this.verticalDuration = 6;
				this.horizontalDuration = 3;
			break;
			case 3:
				this.verticalDuration = 8;
				this.horizontalDuration = 3.5;
			break;
			case 4:
				this.verticalDuration = 10;
				this.horizontalDuration = 4;
			break;
			case 5:
				this.verticalDuration = 15;
				this.horizontalDuration = 5;
			break;
		}

		// Choose a random offset for the animation so that we fill the screen with snow
		// flakes rather than having them all start together at the top.
		this.verticalDelay = ( Math.random() * -this.verticalDuration );
		this.horizontalDelay = ( Math.random() * -this.horizontalDuration );

		switch ( this.depth ) {
			case 1:
				this.flakeSize = 1;
				this.flakeOpacity = 1;
			break;
			case 2:
				this.flakeSize = 2;
				this.flakeOpacity = 1;
			break;
			case 3:
				this.flakeSize = 3;
				this.flakeOpacity = 0.9;
			break;
			case 4:
				this.flakeSize = 5;
				this.flakeOpacity = 0.5;
			break;
			case 5:
				this.flakeSize = 10;
				this.flakeOpacity = 0.2;
			break;
		}

	}

}

This component is completely static. Meaning, once the inputs change, the internal properties are compiled and then remain unchanged going forward. And yet, this animates. That's because, under the hood, this component uses CSS animation @keyframes to moves the flake across the screen:

:host {
	display: block ;
	height: 101vh ;
	pointer-events: none ;
	position: fixed ;
	top: -1vh ;
}

.vertical-track {
	animation-delay: 0s ;
	animation-duration: 5s ;
	animation-iteration-count: infinite ;
	animation-name: snow-flake-vertical-animation ;
	animation-timing-function: linear ;
	left: 0px ;
	position: absolute ;
	top: 0px ;
}

.horizontal-track {
	animation-duration: 3s ;
	animation-iteration-count: infinite ;
	animation-name: snow-flake-horizontal-animation ;
	animation-timing-function: ease-in-out ;
	left: 0px ;
	position: absolute ;
	top: 0px ;
}

.flake {
	background-color: #ffffff ;
	border-radius: 20px 20px 20px 20px ;
	display: block ;
	height: 5px ;
	left: 0px ;
	position: absolute ;
	top: 0px ;
	width: 5px ;
}

@keyframes snow-flake-vertical-animation {
	from {
		top: 0% ;
	}
	to {
		top: 100% ;
	}
}

@keyframes snow-flake-horizontal-animation {
	from {
		left: -20px ;
	}
	50% {
		left: 20px ;
	}
	to {
		left: -20px ;
	}
}

As you can see, the vertical track is responsible for moving the flake from the top (0%) to the bottom (100%) of the host element. Then, in parallel, the horizontal track is responsible for moving the flake left-and-right between -20px and 20px. And, since both of these animations use an animation-iteration-count of infinite, the snow flake will animate forever.

And, when we run this Angular 11 application in the browser, we get the following snowy wonderland experience:

Snowscape with snowflake animations using CSS animation and Angular 11.0.5.

Yay! It's a winter wonderland!

I will say, however, that the mood of this is somewhat disturbed by the fact that my MacBook's fan starts to whirrrrr after this app is open for more than a minute. I suppose that animating 150 elements in parallel, even if its purely through CSS, is a lot for the browser to do. Perhaps there's some sort of GPU acceleration technique I could apply? I'm not too familiar with that kind of stuff.

I thought this was a fun little experiment to help kick off another wonderful year of Angular application development. Here's to a 2021 that is an order of magnitude better than 2020!


Reader Comments

What has two thumbs and hopes you leave a comment? This Guy! (Ben Nadel).

Post A Comment

You — Get Out Of My Dreams, Get Into My Blog
Live in the Now
Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.