Next stop, Stranger Things.
Stranger Things might be the most dissected and celebrated title sequence around! Let's break it down and see if we can emulate it in most of its glory. It is a 52 second sequence, so I will not recreate it entirely!
TLDR
Here is the finished animation.
There are toggles to turn on audio and shadows. The shadows are too demanding for Firefox.
Give it ❤️❤️❤️ on Codepen!
About the title sequence
Here is a video of the title sequence:
The Art of the Title did a fascinating write-up on the title sequence. They interviewed Michelle Doughtery, the Creative Director of Imaginary Forces, who created the title sequence. It is interesting to see the ideas they considered, and how different iterations of the ideas led to the final result.
As Michelle points out the title sequence is not shown first, rather it shown after an opening scene from the episode. This gives it more of a stake in the flow of the story.
What the Duffers did that was really brilliant was the placement of the title sequence right after a very dramatic moment. It’s almost the palate cleanser, or moment of breath. I think part of the reason these particular titles feel fresh is because it’s become integral to the storytelling.
Also, Vox did a short "making of" video of the title sequence that features Michelle also.
The motif of the title is quite simple. It is a closeup of the scattered letters of the title slowly been drawn together. It is backed by a moody synth soundtrack produced by Michael Stein and Kyle Dixon. The letters are a kind of a neon red and it all references the style of the 1980s.
The font used for the title is ITC Benguiat. The font is free for personal use. Most notably, the font was used on the cover of countless Stephen King novels, which probably contributed to its selection for the title. Some alterations were made to some of the letters to give them a more nuanced appearance.
What we are making
I will focus on the second half of the title sequence, from the 25th second to the end. This is where the vantage point becomes fixed and the title is zoomed in closely, with just a few of the letters in view. It is slowly zooming out to reveal the complete title, while some of the letters that are in offset positions fall into their final positions.
Since we need to edit the shapes of some of the letters, we will create a SVG (Scalable Vector Graphic). The alternative is to customize the font, which is licensed, and this is beyond my skillset. So, let's not go there!
Maxwell Ridgeway did a 4-part tutorial in Adobe Aftereffects if you want to explore creating this as a video instead.
Preparation of the SVG
Below is a screenshot of the final state of the title.
Let's create a basic version of this as a SVG in Inkscape.
We want a landscape SVG, a 3:2 aspect ratio would be suitable.
On the main menu, select File, then choose Document properties... to open the tab below.
Enter 1200 as Width, 800 as Height, and select "px" as Units. I picked 1200x800 as it is the most convenient size with the desired aspect ratio. The width
and height
of the SVG can be changed later without issue.
We want a black rectangle that will fill the entire canvas. We add a rect
element. Give it the same width
and height
as the canvas. The default fill
is black.
We add a text
element for each word. We set the font-family
to "ITC Benguiat". We want it to be outline text -- so we give it a fill="none"
and pick a reddish color for the stroke
.
To get the sizing right, we can try out some font sizes until we have the text cover a good portion of the canvas. Adjust the stroke-width
until we get the right thickness, a valud of 4 looks about right to me.
We add 3 rect
elements for the decorative boxes that surround the text.
We want to center our elements vertically and horizontally. To do this, we open the Align and Distribute tab. On the menu, click on Object, then select Align and Distribute... . It opens the tab as per screenshot below.
We want to align our text element relative to the page, and center on both axes:
- Check it that "Page" is selected in the dropdown box
- In the Align section, click the "Align on vertical axis" button. This is the third button on the first row, as circled in green in the screenshot above.
- Now, click the "Align on horizontal" button. This is the third button on the second row, as circled in green in the screenshot above.
Below is the SVG markup tidied up. I removed the width
and height
, I will set this in CSS later. I removed the unnecessary tspan
elements and just had a text
element to represent each word.
<svg viewBox="0 0 1200 800" xmlns="http://www.w3.org/2000/svg">
<rect id="bg" x="0" y="0" width="1200" height="800"/>
<rect x="925.24" y="449.54" width="168.95" height="14" fill="none" stroke="#a3280e" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/>
<text x="139.79877" y="401.03232" fill="#000000" font-family="'ITC Benguiat'" font-size="160px" stroke="#a3280e" stroke-width="4">STRANGER</text>
<text x="293.13965" y="536.1662" font-family="'ITC Benguiat'" font-size="160px" stroke="#a3280e" stroke-width="4">THINGS</text>
<rect x="105.81" y="263.09" width="965.55" height="14" fill="none" stroke="#a3280e" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/>
<rect x="111.29" y="452.78" width="168.95" height="14" fill="none" stroke="#a3280e" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/>
</svg>
This is what it looks like:
There are changes we need to make to get it to match the original title! Below is the screenshot with the differences highlighted and annotated.
We need to change the following:
- All of the letters are stretched in the title. The 'T' has been made slightly taller than the other letters too.
- The first and last letters of the word 'STRANGER' are bigger than the rest.
- Some serifs have been modified. The first 'S' has had some alterations, the 'T' loses the serifs on the top. Also, the back of the 'G' has been flattened.
- There is some kerning to connect some letters. It looks a bit like magic that the corners vanish!
- The elements are colored to appear florescent with some areas brighter than others. They are a bit blurry.
Editing the letters
What we need to do is convert the text
elements to paths. We want a path
for each letter.
Select the the 2 words (text
elements). You can hold down the [[Shift]] key and click on them both.
On the main menu, select Path, and select Object to Path. Now the text
elements has become a group (g
) of path
elements, one path
for each letter.
We want to ungroup these now, so we can see each path individually. Select the group and right-click, select Ungroup from the context menu. Now select all elements on the canvas, and you should see that each letter is now selectable (they have dotted lines around them), as below.
Now, we can manipulate the shape and size of each of the letters. We use the edit path by nodes tool. You can select by hitting the [[N]] key, or selecting it from the tool bar as highlighted below.
Now, when you click on a letter, you will see a controls points that you can drag to change the shape of the letter.
For stretching the letters, you can select groups of these controls points and move them in a direction. It is easiest to see in this video demonstration. Here I make the letter T thinner, and longer.
If you want scale the size up proportionally, like we do with the first 'S', you can change the width
and height
. However, we want to do it proportionally, so it does not look odd. To do this we must make sure to select the padlock to lock the porportions. See screenshot below for this.
Now, when you increase one, the other will grow at a proportional rate with it. You can see me scaling up the 'S' in the video below.
To edit the serifs is more finicky. You need to edit the controls points and adjust the curvative of some segements. I hate this part!
It takes a while to get it all done. It took me maybe 2 hours to get it the way I liked it as I was identifying the differences and executing them accurately! Accuracy is important here because the text will be zoomed in a lot in the animation. If we are off by a small fraction here or there, it will stick out in a very apparent way! In other situations, you can get away with being less precise.
Below is the title with the letters edited.
The one thing that is questionable is the joining of letters that overlap. When the 'R' and the 'A' in the word "Stranger" come together, the strokes morph together to create an unified shape of the 2 letters.
I will do some black magic in the animation to achieve this!
Getting the right colours for the florescent look
We will need to experiment with gradients to get the colors right.
Let's take a letter and see if we can identify a pattern to the colours. Let's look at the first 'S' of the title.
Below I circled the areas that have much lighter colours. There are some areas with midtones too.
It's not a fixed, predictable pattern if you look at it letter by letter.
Let's try to get a color palette from the screenshot to see how many colours we are working with. I took the same cropped screenshot with the big 'S' and uploaded it to https://colordesigner.io/color-palette-from-image to get a color palette for me. See screenshot below.
It identifies a 5 colour palette: 4 reddish colours and 1 black colour. I am not seeing a super bright red colour there.
I can move the slider to make a palette with more colours. When I move it to 7, I get the result in the screenshot below. I can see that the second last colour looks more like the brightest colour I was expecting.
So, I will take one bright (#E05E44) , one midtone (#721209), and one darker red colour (#4A0604) from this palette to make my own minimal palette. I will play with some gradients to see if I can get an organic looking combination of these colours.
Maybe it is just me, but I really do not like editing gradients in Inkscape! It feels so clumsy to me. I will do the experimentation myself, and show you the results of the experiments.
The first thing is that that my mini color palette does not work. It is too mellow. Bin that!
Secondly, I think a radialGradient
captures the quality of having brightly illuminated areas better than a linearGradient
. A linearGradient
works well for letters that are illuminated on particular sides.
I experimented with blending modes as well and it did not make any noticeable improvement.
Lets see how one of the radial gradients looks on the title. Below is "radial3" applied to the entire title.
It looks too uniform and it sticks out as a jarring pattern. Maybe, just maybe, if you apply blur and shadows, it looks less harsh. Though, I think using a couple of different gradients would be a better idea.
Let's try using both "radial2" and "radial3" gradients applying them to random elements.
Wayy better. It could be brighter and the gradients could be smoother by using more colours.
It is easy to get bogged down working on coloring and asthetic tweaks. We can let it be it for now, and improve it further once the animation is complete.
What about adding shadows and blur for that hazy neon vibe?
For now, we will just preview the shadows and blur. Why?
If you want add drop shadows or blur to elements in SVG, you need to use filters. Filters are expensive to animate.
Even though, we will not be changing the values of a filter in our animation, if we move a letter that has a filter applied to it, the CPU/GPU has to do more intensive calculation and rendering. You will find that if you add a filter to an element you are animating, the frame-rate will decrease significantly! We can explore this later when we optimize and polish the animation.
I will just do a quick preview to see if we are on the right track and that blur and drop shadow will complete the appearance we are after.
This is what it would look like if you add blur of 7.5% to the stroke:
And if we add a drop shadow (Filter > Shadows and Glow > Drop Shadow..), it looks like this:
Let's do a side-by-side comparison of my version with the original to see how far off I am:
It is not too far away! My version needs to have smoother, brighter gradients for sure. The shadows need to be worked on to make it more neon, I probably overdid the blur here.
Version to animate
I will take the version without the blur and drop shadow filters. I will go into the SVG markup and add an id
to each path
and rect
, so we can reference them in the animation. Something like this, omitting many attribute and elements for brevity:
<svg id="titleSvg" viewBox="0 0 1200 800">
<!--gradients are here -->
<rect id="bg" width="1200" height="800"></rect>
<g id="title">
<rect id="top-box"/>
<path id="word1-s" />
<path id="word1-t" />
<path id="word1-r1" />
<!-- and so on -->
</g>
</svg>
We will copy the SVG markup and plop it in our index.html inline.
Animation time
The central part of the animation is the slow zooming out to reveal the title. This zoom acts like a camera focusing on different parts of the title. We need to move the letters into different starting positions and move them into their final positions in unison. While the animations are all simple transforms, to synchronize the movement in a performant is a ballet.
We have 4 parts to the animation that will last 20 seconds:
- The zooming out of the title - This beings from 0 until 18 seconds.
- The movement of the letters - The letters are moving from the beginning, but we only see some of the letters when it is zoomed out far enough. All the letters are in place by 14 seconds.
- The expansion of the decorative boxes - At second 14, the boxes expand. The top box is first with the smaller second and thirds boxes delayed by a few hundreds of a seconds.
- Slow fade out of the title - The title fades to black over the last 2 seconds of the animation.
We will try to tie labels in our animation to make these parts clear in our code.
Part 1: The zooming out of the title
We 2 broads options, we can use transform: scale()
or transform: translateZ()
. We want to the zooming to reveal the title at a constant rate, so we want to have a linear easing.
An important difference between the two options is that transform: scale()
is a 2D operation, and transform: translateZ()
is a 3D operation. In theory, transform: translateZ()
has the potential to be offloaded more easily to the GPU. This may be better for performance. It is hard to tell if it will in fact be better, since we will be doing another transform on many of the letters. So, let's try to get the same result with both options and see how it works with the other parts of the animation later.
Method 1: Using scale transformation
The tricky thing about scaling is that it behaves like an exponential operation. It's an interesting phenomena that occurs when you animate an object's scale that makes it appear to change speed even with a linear ease. GSAP created an EasePack that includes ExpoScaleEase
that compensates by bending the easing curve accordingly. This is the secret sauce for silky-smooth zooming/scaling animations.
To include the EasePack in a project, you can use include it as a script in your HTML file (index.html):
<html>
<!--head-->
<body>
<!--other stuff-->
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.10.4/gsap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.10.4/EasePack.min.js"></script>
<script src="main.js"></script> <!--animation code in here -->
</body>
</html>
Or you can import it inside main.js as below:
// if you have the gsap package locally
import { gsap } from "gsap";
import { EasePack } from "gsap/EasePack";
gsap.registerPlugin(EasePack);
// using webpack or rollup
import { gsap, EasePack } from "gsap/all;
gsap.registerPlugin(EasePack);
//animation code here
The docs for ExpoScaleEase
have a very nice explanation with a walk-through of the example below.
Let's apply this to our scaling version.
let tl = gsap.timeline();
tl.fromTo(
"#titleSvg",
{ scale: 17 },
{ duration : 18, scale: 1, ease: ExpoScaleEase.config(17, 1) }
);
Here, we scale from 17 to 1 over 18 seconds. The config function has the following signature: config( startingScale:Number, endingScale:Number, ease:Ease [optional] )
, so we provide our scale values as the first 2 paramaters.
Trying this out, it animates how we want to, but it is quite janky!
We can review this by turning on the "Frame Rendering Stats" in Chrome devtools. You will find this option on the Rendering tab on the very bottom of the devtools, as per screenshot below. I am not a big fan of the layout of the Chrome devtools. Sometimes the bottom pane with additional tabs such as the Rendering tab is not visible. If you don't see this, you must click on the overflow button (3 vertically-aligned dots) in the devtool menu bar, then select the option More Tools, then select Rendering.
As you can see below, we are getting 23.4 frames per second. Ideally this bar is totally green. If frames are dropped, then you see red lines. We have a lot of red lines! 🥵
Let's see if it makes a difference if we target the title (letters and decorative boxes), rather than the entire SVG!
Now, we are getting nearly 60 frames per second (fps). No red lines! 🏆
Great, but wait, now the animation starts from the top left corner of the title text. This is because transformations use a transform origin of the top left of a canvas.
To fix this, I used the set()
method to set the transform origin to be the center of the canvas.
I used the to()
method instead of fromTo()
for the animation.
tl.set("#title", { transformOrigin: "50% 50%", scale: 17 });
tl.to("#title", { duration: 18, scale: 1, ease: ExpoScaleEase.config(17, 1) });
Now, we have 60fps (more or less) from the correct starting point.
This is the intial state we want to be in, excluding the "Executive Producer" credit.
And this is what we have:
We are zoomed in way too much, and our focal point looks off. Ignore the spacing between letters, we tend to this in the next step. We are just concerned with the scale and focal point for now.
Let's try different values for scale
to get the right zoom level. Trying some smaller values, 5 looks to be in the ballpark.
Now, let's adjust transform-origin
to change the focal point. We want to go left a bit, so we will decrease the first number (x-origin). And then, we want to go down a bit, so will increase the second number (y-origin).
tl.set("#title", { transformOrigin: "48% 70%", scale: 5 });
tl.to("#title", { duration: 18, scale: 1, ease: ExpoScaleEase.config(5, 1) });
This looks better.
In part 2, we may need to tweak the values when we move the initial position of the letters.
Method 2: Using Z translation
One gotcha with using translateZ()
is that you cannot do 3D transformations with SVG elements (see StackOverflow question How can I get translateZ to work in svg groups?) . Therefore, we can only perform our translateZ()
transformation on the SVG itself.
We need to set the perspective
property on the parent of the SVG to control the perceived distance of the Z translation. It is preferable to wrap our SVG with a div
, rather than have the body
as the parent to have more control of the layout. Now, we will animate the z
property of the svg
to move the SVG away from the viewer. We set the initial value of z
on the SVG to be a positive value, so that it begins close to the viewer. This is JavaScript:
tl.set(".wrapper", {
perspective: 800,
});
tl.set("#titleSvg", {
z: 700,
});
gsap.to("#titleSvg", {
duration: 18,
z: 0,
ease: Power0.easeNone,
});
This works, but it is quite janky! It starts off with a splutter, and then evens out to 35 fps.
Hmm, how about the mysterious will-change
property?
This property gives the browser a heads-up that we will animate a particular property, so it can optimize and possibly push it to the GPU.
tl.set("#titleSvg", {
z: 700,
willChange: "transform",
});
And this addition gets us to nearly 60 fps.
However, we don't want it to move so fast! The linear ease, ease: Power0.easeNone
, actually does not yield a constant rate that we want. We are experiencing a similar issue to scale
where the easing does not control the progression of the animation in the manner we would hope for.
Changing the values of perspective
on the wrapper div
along with the values of z
on the SVG does not make much of a difference. I couldn't hit on the right set of values to get the desired result.
Unfortunately, it does seems like a long-shot to get this the way we want with a custom easing and playing with some values!
Another issue I noticed is that it looks kind of fuzzy in Firefox at the beginning of the animation! See screenshot below! 🙈
Let's park it for now, and use the scale
solution with part 2 to see if we can the result we want.
Part 2 - The movement of the letters
We need to experiment here. The process here is to position the letters offset to their final position, and then start moving them when they come into view.
Outline timeline
Let's outline our timeline into tweens! This is what I extrapolated from the title sequence:
- 0.0s -> 8.0s: The letters 'A' and 'N' of the first word come into view and are moving towards each horizontally.
- 0.2s -> 9.2s: The letter 'I' of the second word come into view at 0.25s, moving in from opposite directions. The 'N' that is in between these letters, is positioned offset below, out of view.
- 1.0s -> 12.0s: The letter 'G' of the first word and the letter 'G' of the second word start to shift left very slowly. This is subtle and possibly could be skipped. This back of the G's straighten out as they touch the 'N'. I would need to do more work to incorporate this aspect of it.
- 4.0s -> 10.0s : The letter 'R' of the first word comes in from the left. The serif on the leading leg kind of combines with the adjacent 'A'. Some black magic is done there!
- 5.0s -> 10.0s: The letter 'N' of the second word comes in from below.
- 9.0s -> 15.0s : The letter 'T' of the first word drops down from above.
- 9.5s -> 14.0s: The 'H' of the second word. The distances they are positioned outside are equal.
- 10.0s -> 17.0s: The letter 'E' of the first word drops down from above. The letter 'S' of the second word comes from below.
- 11.0s -> 17.0s: The last 'R' of the first word starts shifting left.
- 12.75s -> 17.0s : The 'S' of the first word starts shifting right.
Organize code for the first tweens
Let's consider how we do this.
We will create a separate timeline for each part. We will name our timeline from part 1 as zoomTimeline
.
let zoomTimeline = gsap.timeline();
zoomTimeline.set("#title", { transformOrigin: "48% 70%", scale: 5 });
zoomTimeline.to("#title", {
duration: 18,
scale: 1,
ease: ExpoScaleEase.config(5, 1),
});
We will name our timeline for this part as lettersTimeline
.
The first letters that come into view are the 'A' and 'N' of the first word. They are coming together. So, we need to reposition the 'A' to the left (negative X translation), and the 'N' to the right (positive X translation).
let lettersTimeline = gsap.timeline();
lettersTimeline.set("#word1-a", { x: -20 });
lettersTimelin.set("#word1-n", { x: 20 });
If we comment out the scaleTimeline
code, it looks something like this:
Now, we need to add an animation to change the value of x
to zero.
lettersTimeline.to("#word1-a, #word1-n", { x: 0, duration: 8 });
Now, we can uncomment the scaleTimeline
code and see how it looks.
We can adjust the values until we are happy with those 2 letters. We need to move them a bit further apart, x
of 30 for #word1-a
, and x
of -30 for #word1-n
looks better.
We need to repeat this process for the other letters. It is not complicated, but it takes time and patience.
Positioning all letters
It might be easier to position all of the letters before we go further. This way, they are out of the way of the others that will animate, one by one. It does not need to exact for now. We will refine the positions when we animate them.
I commented out the scaleTimeline
and picked some values for each of the letters. This is how I set them initially:
//first word: STRANGER
lettersTimeline.set("#word1-s", { x: -150 }); //prob should have same absolute value as #word2-r2
lettersTimeline.set("#word1-t", { y: -180 }); //prob should be same absolute value as #word2-h
lettersTimeline.set("#word1-r1", { x: -50 });
lettersTimeline.set("#word1-a", { x: -20 });
lettersTimeline.set("#word1-n", { x: 20 });
lettersTimeline.set("#word1-g", { x: 50 }); //prob should be same value as #word2-g
lettersTimeline.set("#word1-e", { y: -180 });
lettersTimeline.set("#word1-r2", { x: 150 }); //prob should have same absolute value as #word1-s
//second word: THINGS
// 'T' is static
lettersTimeline.set("#word2-h", { y: 180 }); //prob should be same absolute value as #word1-t
lettersTimeline.set("#word2-i", { x: -100 });
lettersTimeline.set("#word2-n", { y: 100 });
lettersTimeline.set("#word2-g", { x: 50 }); //prob should be same value as #word1-g
lettersTimeline.set("#word2-s", { y: 180 });
This should be OK to read as the letters are in order they appear!
The only thing is, as you can see with comments, the values of some letters should be offset by the same absolute value. For example, for the first 2 letters we animate, we moved the 'A' minus 30 on the X axis, and the 'N' plus 30 on the X axis. They are moved equidistantly.
It might make sense to have variables for these values, so we can tweak them later as we are writing each tween.
So, this would be it with variables, and without the comments:
let batch1Distance = 30;
let batch3Distance = 50;
let batch6and8Distance = 180;
let batch9and10Distance = 150;
//first word: STRANGER
lettersTimeline.set("#word1-s", { x: `-${batch9and10Distance}` });
lettersTimeline.set("#word1-t", { y: `-${batch6and8Distance}` });
lettersTimeline.set("#word1-r1", { x: -50 });
lettersTimeline.set("#word1-a", { x: `-${batch1Distance}` });
lettersTimeline.set("#word1-n", { x: `${batch1Distance}` });
lettersTimeline.set("#word1-g", { x: `${batch3Distance}` });
lettersTimeline.set("#word1-e", { y: -180 });
lettersTimeline.set("#word1-r2", { x: `${batch9and10Distance}` });
//second word: THINGS
// 'T' is static
lettersTimeline.set("#word2-h", { y: `${batch6and8Distance}` });
lettersTimeline.set("#word2-i", { x: -130 });
lettersTimeline.set("#word2-n", { y: 100 });
lettersTimeline.set("#word2-g", { x: `${batch3Distance}` });
lettersTimeline.set("#word2-s", { y: 180 });
It looks something like this zoomed out now!
Second batch and adding labels
Let's do the next letter now. It is the 'I' of the second word.
I will use labels to group the tweens to keep track of what I am doing. You can use a label instead of using raw numbers for the position
parameter (last parameter) in the .to()
function.
lettersTimeline
.addLabel("batch1", 0)
.addLabel("batch2", 0.2)
.to("#word1-a, #word1-n", { x: 0, duration: 8 }, "batch1")
.to("#word2-i", { x: 0, duration: 9 }, "batch2");
This can make it easier to read, if you can come up with good names. I can't in this case!
Previously, we did the hard work in using set()
to position our letters. The values we need to change for the animaiton are quite straightforward now. We are setting the x
or y
value to zero, and picking the appropriate duration
.
The letter is appearing a bit earlier than we'd like, so I decreased the value from -100 to 130, so it comes onscreen at the right time, and amount.
Here is the full code for the first 2 batches:
A process for moving through timeline to focus on individual tweens
It can be nice to include a little dashboard to control the timeline, so you can create a shorter feedback loop of the portion of the timeline you are working on. You do not have to do this, but it can make your life easier.
Without some controls, you can find yourself commenting out code and using temporary values to hone in on particular portion of the timeline. I can cover this idea in a separate topic another time.
A good alternative is to write a bit of code to set the starting point in the timeline. We can create a currentPoint
variable to set our interim start point. We use pause()
to pause our timelines initially, then use the seek()
function to move the timelines to currentPoint
, and then use the play()
function to start at that point. Something like this:
let currentPoint = 1; //in seconds (decimal allowed)
let zoomTimeline = gsap.timeline();
zoomTimeline.pause();
//setting of values and tweens
let lettersTimeline = gsap.timeline();
lettersTimeline.pause();
//setting of values and tweens
zoomTimeline.seek(currentPoint);
lettersTimeline.seek(currentPoint);
zoomTimeline.play();
lettersTimeline.play();
The above code will start the timelines at the 1 second mark, which is where we want to our third batch to begin. So, adding the code that batch, which is the letter 'R' of the first word, look like this:
Letter batches 4 to 10
I won't cover the rest of letters. They are a variation on everything we discussed in the section. I hope that I explained things clearly enough that you could finish this yourself if you had to!
Part 3 - Expansion of the decorative boxes
We can use a scaleX()
transformation to expand the boxes horizontally.
To determine the direction of the expansion, we set the transformOrigin
:
- If you want it to expand from the center, you can the
transformOrigin: 50% 50%
. We want the top box to do this. - If you want it to expand left to right, you can set
transformOrigin: 0% 50%
. We want the bottom right box to do this. - If you want it to expand right to left, you can set
transformOrigin: 100% 50%
. We want the bottom left box to do this.
The sequence is that the top box expands first, the duration is 1 second. Then, shortly before it is completely expanded, the bottom 2 boxes expand, taking about three quarters of a second. They overlap by about a half a second.
The code is below.
In this version, the animation runs immediately, but in the final version this sequence runs approximately 15 seconds into the complete title sequence.
Part 4 - Fade out
The final part is the fading out of the title.
We will call this timeline -- the visibilityTimeline
! Your boy can name things can't he? 🤣
To fade out the title is simple, right? We create a tween that sets opacity
to zero.
let visibilityTimeline = gsap.timeline();
visibilityTimeline.to("#title", { opacity: 0, duration: 1.5 });
You can, but there is one extra element that may go unnoticed in this case! There is a vignette effect, it is not a uniform fade. As you can see in the screenshot below, actually the outside of the title becomes darker in a graduated way.
We can use a mask
containing an ellipse
shape to achieve this.
What I find easiest to do to find the correct placement and size is to add an ellipse
element as the last element to the inline SVG in the page. I give it the attributes: fill=white
and opacity=0.5
. This way I can see the title underneath.
I choose white for the fill
because for a mask, white is the transparent part, darker colours are semi-opaque.
Now, I try out values for the attributes of the ellipse
such as: cx
, cy
, rx
, and ry
so that I can get the size and shape the way I want. To give a slightly darker band around the edge of the mask, we add a stroke
and use a light grey color to make it slightly opaque.
Once we are happy with the outcome, we can remove opacity="0.5"
from the ellipse
and put it inside a mask
element. Something like this:
<svg>
<defs>
<mask id="spotReveal">
<ellipse
fill="white"
stroke="#d6d6d6"
cx="600"
rx="600"
stroke-width="80"
ry="300"
cy="380"
></ellipse>
</mask>
<!--our gradients are in here too-->
</defs>
<rect id="bg" width="1200" height="800" />
<g id="title" mask="url(#spotReveal)">
<!--boxes and letters here-->
</g>
</svg>
The id
of the mask is used as reference. We apply the mask to the "title" group ( g
element with an id
of "title) with mask="url(#spotReveal)"
.
In the JavaScript, we can animate the ellipse
to shrink it in size through scale
, and reduce the opacity
to create the vignette fadeout effect.
To make it easier to visualize the effect of the mask, here is a tween without a 2 second duration. I am not animating opacity
in order to show the dimensions of the ellipse clearly.
Notice that we set transformOrigin: "50% 50%"
for the ellipse also. The default transform-origin
for SVGs is to start for the top left corner. We want it to scale from the center.
Now, let's change the tween to get the result we want. We need change the tween to reduce the opacity
to zero to make the entire title fadeout. We need to make the duration
shorter too. Now, it looks like this:
It is surprising sometimes, how some details can elevate something, and sometimes they may go unappreciated. When you speed some thing up like this it is easy to miss the details!
In the final version, this happens 18 seconds into the animation.
Putting it all together
I noticed a sizeable degradation in performance when I added the 4 parts together! 😥
Since it happened from the beginning, I focused on the first 2 part of the animation to see what the issue could be. That lead me to look at things that were applied from parts 3 and part 4 but were not relevant. I removed the mask used in part 4 from the "title" group, and that was the bad guy!
We will set the mask on the title only when we need it. In the visibilityTimeline
, we can set that attribute when the timeline begins using the onStart
property.
visibilityTimeline.to("#spotReveal ellipse", {
scale: 0.6,
opacity: 0,
duration: 2,
onStart: () => {
document.querySelector("#title").setAttribute("mask", "url(#spotReveal)");
},
});
Should I combine the 4 timelines into a single timeline?
As a first effort, it is easier to nest timelines within a master timeline. This way we do not need to change our code and we can retain the info we get from the naming.
To nest a timeline, you can wrap it in a function, and then call that function in the add()
function of the master timeline. This is the skeleton of our code:
function part1() {
// scaleTimeline stuff
}
function part2(){
//letterTimeline stuff
}
function part3(){
//letterTimeline stuff
}
function part4(){
//visibilityTimeline stuff
}
const masterTimeline = gsap.timeline();
masterTimeline
.add(part1(), 0)
.add(part2(), 0)
.add(part3(), 15) // call at 15 second mark
.add(part4(), 19);
And with some tweaking of the values to get things tighter, this is the outcome:
It is pretty good! It can improved in appearance but the performance of the animation is very good. It is averaging approximately 55 fps in Chrome. I recorded the performance in Chrome devtools and it shows that very few frames are dropped. See the red bars on the same line as the word Frames in the screenshot below.
Here it is in the Stats Rendering display in Chrome..
There are a couple of points that the framerate dips. As above, when a lot of the letters are moving at once is when it dips. This could be reviewed to see if some improvements could be made.
It is not quite the same without the synth soundtrack, so next we will add that.
Adding the audio
The accompanying soundtrack complements the sequence really well. I think it is not quite the same without it. Let's add the song to plays in sync with the animation.
Initially the song is muted. I added a mute toggle button (a checkbox technically) to the top right corner to enable the song. You will probably need to the unmute toggle and then click the title to restart the animation to hear the audio, this is because some browsers block audio by default that is initiated by a user.
It is the same code as I used in the Schitt's Creek title sequence of this series, you can visit the "Adding audio" section of that post to learn more.
The spit and polish
Let's try to polish it up and make it that extra 5% to 10% better. The top priority is the gradients.
Polish the gradients
This is the part I hate the most as I find editing gradients in Inkscape to be clumsy!
I went through each letter and gave each an individal gradient. I kept the saturation and lightness of the colors high. I looked to use colors that have smoother transitions and use different hues for the more illuminated edges. You can see a side by side comparison of the before and after of this work below.
The biggest win is that the jarring pattern is gone. I think using more nuanced gradients gives it a more reflective quality.
This took me quite a while to get right! I had to go against my natural inclination to reduce the saturation and lightness of certain colors. It needs to be bright and bold all over with gradual transitions.
Add the black magic morphing of serifs
The second polish we can add is the "black magic" that results in the joining of the overlapping serifs of some of the adjacent letters in the first word (R, A, and N).
To achieve this I added some black boxes (rect
elements) to the SVG and placed them at the point where the letters eventually overlap -- I colored them yellow as per screenshot below for identification.
The idea is to initially hide the boxes, and as the letters reach that overlapping point, I will animate them in through the opacity
property, or maybe through a scale transformation. I added another timeline for this and you can see it in the final version as the polish
function! It does add an extra bit to the final appearance, especially when zoomed out.
Can we add shadows?
Using filters to add some shadow, noise, and blur for the glowing neon effect is probably too much of a burden to animate with everything else going. Let's see rather than speculate! I will optimize the SVG and see how close I can get.
After optimizing the SVG, I added a "drop shadow" filter in Inkscape, again through the menu - choose Filter > Shadows and Glow > Drop Shadow... I added a shadow with a semi-transparent reddish color and a blur radius of 3. This is how it looks:
In this version, you can appreciate the joined serifs of the letters of the first word that we achieved earlier!
Anyway, I used this version in the animation and I was surprised to find that Chrome can handle it very well and runs at 55 fps. However, Firefox cannot handle it, the animation breaks down really!
In the final version, I added a checkbox to toggle the shadow on and off if you want to play with it, and see the degradation of performance in real time.
The final animation
For your convenience, here is the final version again.
Give it ❤️❤️❤️ on Codepen!
Wrapping up
It was quite a journey! I am proud that I got the animation to a high level.
I learned a few things along the way, which makes it worthwhile. It required a lot of patience to do the detailed work that to elevate it from good to great. I was able to do this by leaving it alone for a few weeks, and then coming back with a fresh head to tackle some tedious parts again.
Throughout this series, I have explored if it is possible to do an animation as a CSS animation. In this case, no further investigation is required! It is not possible. We need JavaScript to provide the special easing for the smooth zooming of the title, that is everything that I covered in part 1. There is no way in CSS to provide that kind of interpolated easing to a scale transformation as far as I know.
This type of demanding animation demonstrates the upper limits of animating HTML or SVG. If you want more photo-realistic effects for the noisy neon glow of the letters, animating SVG filters is too taxing for the browser to do in unison with other transformations. To make a perfect duplicate of the video, you would need to do the animation with canvas or Web GL. Maybe I can try that another time. Web GL is something I would like to learn.
Thanks for reading!
You can subscribe to my RSS feed to get my latest articles.
Top comments (9)
Wow! This is both comprehensive and just all around awesome. I love Stranger Things!
By the way, this is a super fun series... appreciate ya sharing it!
Thanks Michael. Glad you are enjoying the series!
It takes a while to get the desired results! It can be enlightening to see the struggle and pain and some sort of process emerge, and hopefully at the end, to appreciate how you can achieve a great result.
I won't be writing one this size for some time 😅 I will do something easier next time!
Props for this awesome work! Great job 💪
Thanks Lars!
Well done!
Thanks Quentin :-)
Some comments may only be visible to logged-in visitors. Sign in to view all comments. Some comments have been hidden by the post's author - find out more