The goal of this animation was to bring to life each of the three key marketing points of Angular v17, “Works at any scale”, “Loved by millions” and “Build for everyone”, with the vibrant gradients of the new Angular branding.
We decided to try something new with WebGL and 2D signed distance fields for the 2D animation, giving us the freedom to explore more advanced effects and the ability to move elements in 3D space.
“No Build” setup for rapid prototyping
The scroll animation was originally prototyped as a standard GSAP ScrollTrigger animation (static site, no build, for rapid prototyping), with SVGs and the MorphSVG plugin, and then later converted to a WebGL animation using the same SVGs and signed distance fields for the gradient masks and 3D effects.
You can see a “no build” version of the animation on our CodeSandbox.
OGL for a minimal WebGL library
OGL is a minimal WebGL library with similar API and conventions as three.js, and is a great alternative for developers comfortable writing their own GLSL shaders, while keeping the bundle size very low.
OGL is a pure ES6 module library and fully tree-shakeable, however at the time, lacked official TypeScript definitions needed for Angular.
Simple MVC design pattern
In this Model–view–controller (MVC) pattern, the top-level app creates the views first, and then passes the views down through the controllers.
Note there isn’t the Model-part in this specific pattern as it wasn’t needed, so it’s really just a View–controller (VC) pattern?
Its top-down tree-like structure works well with tree-shaking, and without any circular dependencies.
Custom View base class for GSAP animations
Each of the original SVG elements were replaced with View classes, GSAP can animate any DOM element or object, and in this case we animate transform, rotate, scale, and alpha (TRSA) properties of the Views.
GSAP updates the properties, and the update() method of the View applies the values to the WebGL scene graph.
You can see the View base class in the Angular GitHub repo.
2D signed distance fields with anti-aliasing
OGL comes with a Multi-channel signed distance field (MSDF) example, commonly used in video games to display text for heads-up displays (HUDs) among many other uses.
Here we’ve used msdfgen, which supports advanced SVG decoding, to generate MSDF textures of the original SVGs.
Multi-channel signed distance fields (MSDFs), compared to signed distance fields/functions (SDFs), apply the red, green, and blue (RGB) channels of the texture for more accurate distance calculations to the edge, with sharp corners at all angles and sizes.
To animate the individual letters of the Angular wordmark, 8 textures are generated at the same size so they can be animated separately with their own transform origins.
Anti-aliasing is then applied to the edge using the GLSL functions fwidth() and smoothstep().
float msdf(sampler2D image, vec2 uv) {
vec3 tex = texture2D(image, uv).rgb;
float signedDist = max(min(tex.r, tex.g), min(max(tex.r, tex.g), tex.b)) - 0.5;
float d = fwidth(signedDist);
return smoothstep(-d, d, signedDist);
}
2D polygon animation of the Angular logo
Using the same anti-aliased edges of the MSDF technique above, the vertices of SDF polygons are animated between start and end points.
Positions for each of the vertices/points were taken right out of Figma, and normalized for the SDF polygon animation.
The Angular gradient
Created in Adobe Illustrator as a 4-point freeform gradient, it was simplest just to load a single 512x512 texture.
As part of the branding, the movement of the gradient has a specific path swaying from left to right to limit the amount of red displayed in the logo.
The same gradient is used throughout the animation, changing size and shape between the Angular logo and the “Build for everyone” heading.
The Angular.dev homepage includes two query string debug parameters for the gradient, which you can see yourself by adding ?debug or ?gradient to the homepage URL.
Delta-based linear interpolation
If your animation includes some kind of linear interpolation, or in our case GSAP’s interpolate() utility function, the timing of those animations will be different at different frame rates, for example 30fps on a device in low power mode, or a high refresh rate display at 120fps.
To ensure the speed of the animation is always the same regardless of frame rate, we’re using the deltaTime parameter (in milliseconds) of GSAP’s ticker() callback to calculate the frame rate, and in-turn adjust the playback speed based on a standard 60fps display.
For example, 30fps would be 2x speed, and 120fps would be 0.5x speed, which may seem backwards at first, but what this calculation is doing is making the values the same regardless of speed when compared to 60fps.
At a refresh rate of 60, the update() handler is called 60 times, and at a refresh rate of 30 you would have to double the values to be at the same point in time, because the values are interpolated on each frame, if that makes sense.
Hopefully the diagram below makes it a bit clearer.
And the calculation used in the update() handler.
private logoProgress = 0;
private logoProgressTarget = 0;
private lerpSpeed = 0.1;
private refreshRate = 60;
private playbackRate = 1;
private onUpdate: gsap.TickerCallback = (time: number, deltaTime: number, frame: number) => {
this.playbackRate = this.refreshRate / (1000 / deltaTime);
this.logoProgress = gsap.utils.interpolate(
this.logoProgress,
this.logoProgressTarget,
this.lerpSpeed * this.playbackRate,
);
this.logoAnimation.progress(this.logoProgress);
this.canvas.update(time, deltaTime, frame, this.progress);
};
Responsive sizing
And finally, making sure the size of the WebGL elements look good on both mobile and desktop, at all sizes.
Here we’re using a single breakpoint of 1000px for mobile/desktop to keep things simple, with a scale parameter that is passed down through the controllers to the views as part of the resize() handler.
Similar to the delta-based linear interpolation above with a base frame rate of 60fps, the scale calculation for desktop is based on a laptop size of 1470px wide, generating a scale from 0.5x to 2x given the size of your display.
private onResize: void = () => {
const width = this.window.innerWidth;
const height = this.window.innerHeight;
const dpr = this.window.devicePixelRatio;
if (width < 1000) {
this.scale = 0.5;
} else {
this.scale = width / 1470;
}
this.canvas.resize(width, height, dpr, this.scale);
};
We love open source
Checkout the code yourself, it’s all in the Angular GitHub repo!
Other articles in the series
Continue to Crafting Interactivity: How Monogram Built the Engaging Animations for Angular.dev's Homepage (Part 2)