2024-10-31 Web Development, Video Production, Programming
Building a Typing Code Animation Component
By O. Wolfson
This article will guide you through building a typing code animation component in React/Next.js. This component types out code with syntax highlighting, providing an engaging way to display code snippets for tutorials, presentations, or educational content. Incidentally, this setup is perfect for video recording, as the component is sized at 720p to maintain consistent formatting and high resolution for tutorial videos or presentations.
Here below is a simple example of how to output text one character at a time. This can be run as a node script to see the effect.
javascriptconst content = `Hello, App Router!
Hello World`;
let sentence = "";
const typeTest = async () => {
for (let index = 0; index < content.length; index++) {
sentence = content.slice(0, index + 1);
await new Promise((resolve) => setTimeout(resolve, 50));
console.log(sentence);
}
};
typeTest();
Overview of the Component
The component takes an array of code blocks and animates the typing of each block one character at a time. It includes an elapsed time display to track how long the animation has been running and provides an estimated total time for completion.
Core Features
- Typing Animation: Each code block is revealed character by character with a specified delay.
- Elapsed Time HUD: Displays the elapsed time and the estimated total time for the animation.
- Syntax Highlighting: Uses the
react-syntax-highlighter
library with a customizable theme for highlighting code.
Code Walkthrough
Let's dive into the code and explain how each function and component works:
typescript"use client";
import { useState, useRef } from "react";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { darcula } from "react-syntax-highlighter/dist/esm/styles/prism";
// Type definition for a code block
interface CodeBlock {
time: number;
content: string;
delay: number;
}
// Code block configuration
const codeBlocks: CodeBlock[] = [
{
time: 1000,
content: `
import { createApiClient } from "./apiClient";
import { getSession } from "./sessionHandler";
export async function initializeClient() {
const session = await getSession();
return createApiClient(
process.env.API_BASE_URL ?? "https://default.api.url",
process.env.API_KEY ?? "default_api_key",
{
sessionData: {
getAll: () => session.getAllSessionData(),
setAll: (dataToSet) => {
for (const { key, value, options } of dataToSet) {
session.setSessionData(key, value, options);
}
},
clear: () => session.clearAllData(),
},
}
);
}
`,
delay: 50,
},
];
// Function to type each character in the content
async function typeCode(
content: string,
delay: number,
onUpdate: (newText: string) => void
): Promise<void> {
for (let index = 0; index < content.length; index++) {
onUpdate(content.slice(0, index + 1));
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
// Helper function to calculate the total estimated time for animation
function calculateTotalAnimationTime(codeBlocks: CodeBlock[]): number {
const totalCharacters = codeBlocks.reduce(
(sum, block) => sum + block.content.length,
0
);
const totalDelay = codeBlocks.reduce((sum, block) => sum + block.time, 0);
const typingTime = totalCharacters * codeBlocks[0].delay;
return (totalDelay + typingTime) / 1000; // Convert to seconds
}
export default function CodeAnimation() {
const [displayedCode, setDisplayedCode] = useState<{ text: string }[]>([]);
const [started, setStarted] = useState<boolean>(false);
const [elapsedTime, setElapsedTime] = useState<number>(0);
const timerRef = useRef<NodeJS.Timeout | null>(null);
const totalEstimatedTime = calculateTotalAnimationTime(codeBlocks);
// Timer management functions
const startTimer = () => {
timerRef.current = setInterval(
() => setElapsedTime((prev) => prev + 1),
1000
);
};
const stopTimer = () => {
if (timerRef.current) {
clearInterval(timerRef.current);
}
};
// Function to start the animation
const startAnimation = () => {
setStarted(true);
setElapsedTime(0);
startTimer();
codeBlocks.forEach((block, idx) => {
setTimeout(async () => {
setDisplayedCode((prev) => [...prev, { text: "" }]);
await typeCode(block.content, block.delay, (newText) =>
setDisplayedCode((prev) => {
const updatedBlocks = [...prev];
updatedBlocks[idx].text = newText;
return updatedBlocks;
})
);
if (idx === codeBlocks.length - 1) stopTimer();
}, block.time);
});
};
return (
<div>
<div className="text-white p-2 rounded text-sm text-center">
Elapsed Time: {elapsedTime} seconds / Estimated Total Time:{" "}
{totalEstimatedTime.toFixed(2)} seconds
</div>
<div className="flex border items-center border-gray-800 h-[720px] w-[1280px] mx-auto">
<div className="code-animation-container w-full font-mono p-5 rounded-lg max-w-[1000px] mx-auto text-center text-xl">
{started ? (
displayedCode.map((block, idx) => (
<SyntaxHighlighter
key={idx}
language="javascript"
style={darcula}
className="custom-syntax-highlighter"
>
{block.text}
</SyntaxHighlighter>
))
) : (
<button
type="button"
onClick={startAnimation}
className="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600"
>
Start Animation
</button>
)}
<style jsx>{`
:global(.custom-syntax-highlighter pre),
:global(.custom-syntax-highlighter code),
:global(.custom-syntax-highlighter) {
background: none !important;
}
`}</style>
</div>
</div>
</div>
);
}
Explanation of the Code
1. typeCode
Function
This function handles the animation of typing out the code block one character at a time. It iterates through the content
string and calls the onUpdate
callback with the updated string at each step, adding a delay between characters using setTimeout
.
2. calculateTotalAnimationTime
Function
Calculates the estimated total time for the entire animation by summing up:
- The number of characters across all code blocks multiplied by the delay per character.
- The initial delay (
time
) for each code block before it starts typing.
This is displayed in the HUD to give users an idea of how long the animation will take.
3. Timer Management Functions
startTimer
: Starts the timer and updateselapsedTime
every second.stopTimer
: Stops the timer when the animation is complete.
4. startAnimation
Function
The main function that controls the animation flow:
- Resets and starts the timer.
- Iterates through each code block and starts typing after a specified initial
time
. - Updates
displayedCode
to reflect the currently typed characters.
5. Component Structure
- The component displays a button to start the animation.
- Once started, it shows the typing animation of the code blocks using
react-syntax-highlighter
with thedarcula
theme for syntax highlighting. - A HUD at the top displays the elapsed time and the total estimated time for the animation.
Styling
Most of the styling is handled using Tailwind CSS classes for consistency and maintainability. Custom styles are added using the :global
rule to remove the background of the syntax highlighter.
Sound Effects
To add a bit of realism, I added a sound effect that plays when the animation starts. This is done using the useEffect
hook to initialize the audio object and the play
method to start the sound.
typescript"use client";
import { useState, useRef, useEffect } from "react";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { darcula } from "react-syntax-highlighter/dist/esm/styles/prism";
// Type definition for a code block
interface CodeBlock {
time: number;
content: string;
delay: number;
}
// Code block configuration
const codeBlocks: CodeBlock[] = [
{
time: 1000,
content: `
import { createApiClient } from "./apiClient";
import { getSession } from "./sessionHandler";
export async function initializeClient() {
const session = await getSession();
return createApiClient(
process.env.API_BASE_URL ?? "https://default.api.url",
process.env.API_KEY ?? "default_api_key",
{
sessionData: {
getAll: () => session.getAllSessionData(),
setAll: (dataToSet) => {
for (const { key, value, options } of dataToSet) {
session.setSessionData(key, value, options);
}
},
clear: () => session.clearAllData(),
},
}
);
}
`,
delay: 50,
},
];
// Function to type each character in the content
async function typeCode(
content: string,
delay: number,
onUpdate: (newText: string) => void
): Promise<void> {
for (let index = 0; index < content.length; index++) {
onUpdate(content.slice(0, index + 1));
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
// Helper function to calculate the total estimated time for animation
function calculateTotalAnimationTime(codeBlocks: CodeBlock[]): number {
const totalCharacters = codeBlocks.reduce(
(sum, block) => sum + block.content.length,
0
);
const totalDelay = codeBlocks.reduce((sum, block) => sum + block.time, 0);
const typingTime = totalCharacters * codeBlocks[0].delay;
return (totalDelay + typingTime) / 1000; // Convert to seconds
}
export default function CodeAnimation() {
const [displayedCode, setDisplayedCode] = useState<{ text: string }[]>([]);
const [started, setStarted] = useState<boolean>(false);
const [elapsedTime, setElapsedTime] = useState<number>(0);
const timerRef = useRef<NodeJS.Timeout | null>(null);
const totalEstimatedTime = calculateTotalAnimationTime(codeBlocks);
const typingSoundRef = useRef<HTMLAudioElement | null>(null);
useEffect(() => {
// Initialize the audio object
typingSoundRef.current = new Audio("/typing.mp3");
}, []);
// Timer management functions
const startTimer = () => {
timerRef.current = setInterval(
() => setElapsedTime((prev) => prev + 1),
1000
);
};
const stopTimer = () => {
if (timerRef.current) {
clearInterval(timerRef.current);
}
};
// Function to start the animation
const startAnimation = () => {
setStarted(true);
setElapsedTime(0);
startTimer();
// Play typing sound
if (typingSoundRef.current) {
typingSoundRef.current.loop = true; // Loop the sound while typing
typingSoundRef.current.play().catch((err) => {
console.error("Error playing sound:", err);
});
}
codeBlocks.forEach((block, idx) => {
setTimeout(async () => {
setDisplayedCode((prev) => [...prev, { text: "" }]);
await typeCode(block.content, block.delay, (newText) =>
setDisplayedCode((prev) => {
const updatedBlocks = [...prev];
updatedBlocks[idx].text = newText;
return updatedBlocks;
})
);
if (idx === codeBlocks.length - 1) {
// Stop the typing sound when the animation completes
if (typingSoundRef.current) {
typingSoundRef.current.pause();
typingSoundRef.current.currentTime = 0; // Reset for next play
}
stopTimer();
}
}, block.time);
});
};
return (
<div>
<div className="text-white p-2 rounded text-sm text-center">
Elapsed Time: {elapsedTime} seconds / Estimated Total Time:{" "}
{totalEstimatedTime.toFixed(2)} seconds
</div>
<div className="flex border items-center border-gray-800 h-[720px] w-[1280px] mx-auto">
<div className="code-animation-container w-full font-mono p-5 rounded-lg max-w-[1000px] mx-auto text-center text-xl">
{started ? (
displayedCode.map((block, idx) => (
<SyntaxHighlighter
// biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
key={idx}
language="javascript"
style={darcula}
className="custom-syntax-highlighter"
>
{block.text}
</SyntaxHighlighter>
))
) : (
<button
type="button"
onClick={startAnimation}
className="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600"
>
Start Animation
</button>
)}
<style jsx>{`
:global(.custom-syntax-highlighter pre),
:global(.custom-syntax-highlighter code),
:global(.custom-syntax-highlighter) {
background: none !important;
}
`}</style>
</div>
</div>
</div>
);
}