Skip to main content
Ben Nadel at cf.Objective() 2011 (Minneapolis, MN) with: Eddie Ballisty
Ben Nadel at cf.Objective() 2011 (Minneapolis, MN) with: Eddie Ballisty ( @eballisty )

Making Snow Animations In Angular 11.0.5

By 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!

Want to use code from this post? Check out the license.

Reader Comments

21 Comments

Personally, I have found that creating a lot of instances of directives reduces the performances drastically. My guess is that the fans are turning on due to large number of instances of directives rather than css.

You can also try using transform: translateZ(0) trick into moving css rendering to gpu for performance improvements.

Also check out perlin noise for randomly moving the snow flake down.

15,688 Comments

@Hassam,

When I first put this together, I actually tried to use the will-change CSS property to hint that the browser was going to need to do some "layer work". But, adding the will-change actually crushed the performance. I was thinking of going back and trying out the translateZ() stuff, but I was discouraged by the will-change issues.

As far as the number of directives having an impact on performance, I could understand that on first render. But, once the page was rendered, I'm not changing any view-models. As such, there should be no change-detection running. Which means, Angular really shouldn't be performing any work - there's nothing to process, no views to reconcile. That's why I think it was strictly the CSS that was causing the fans to run.

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel