From c806a772e3b24b749a174cf4fb3f4ab3d1c69437 Mon Sep 17 00:00:00 2001 From: Damien Date: Tue, 25 Feb 2025 08:18:21 -0500 Subject: [PATCH] feat: typing animation landing page intro --- src/app/page.tsx | 45 ++++- src/components/TypingAnimation.tsx | 263 +++++++++++++++++++++++++++++ 2 files changed, 304 insertions(+), 4 deletions(-) create mode 100644 src/components/TypingAnimation.tsx diff --git a/src/app/page.tsx b/src/app/page.tsx index f917476..be56b05 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,9 +1,46 @@ -import * as React from 'react' +'use client'; + +import * as React from 'react'; +import { useState } from 'react'; import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; +import Fade from '@mui/material/Fade'; +import TypingAnimation from '@/components/TypingAnimation'; export default function HomePage() { + const [showContent, setShowContent] = useState(false); + return ( - - - ) + <> + {!showContent && ( + setShowContent(true)} + /> + )} + + + + + Damien's Website + + + This is a test sentence. + + {/* Add more content here */} + + + + ); } diff --git a/src/components/TypingAnimation.tsx b/src/components/TypingAnimation.tsx new file mode 100644 index 0000000..391d826 --- /dev/null +++ b/src/components/TypingAnimation.tsx @@ -0,0 +1,263 @@ +import React, { useState, useEffect } from 'react'; +import { Box, Typography, Fade, Stack } from '@mui/material'; + +interface TypingAnimationProps { + titleText: string; + subtitleText: string; + typingSpeed?: number; + delayBeforeRemoval?: number; + onComplete?: () => void; +} + +const TypingAnimation: React.FC = ({ + titleText, + subtitleText, + typingSpeed = 100, + delayBeforeRemoval = 2000, + onComplete +}) => { + const [displayedTitle, setDisplayedTitle] = useState(''); + const [displayedSubtitle, setDisplayedSubtitle] = useState(''); + const [showCursor, setShowCursor] = useState(true); + const [isTitleComplete, setIsTitleComplete] = useState(false); + const [isSubtitleComplete, setIsSubtitleComplete] = useState(false); + const [isVisible, setIsVisible] = useState(true); + const [isFadingOut, setIsFadingOut] = useState(false); + const [cursorPosition, setCursorPosition] = useState('title'); // 'title' or 'subtitle' + + // Start cursor blinking immediately + useEffect(() => { + const cursorInterval = setInterval(() => { + setShowCursor(prev => !prev); + }, 500); + + return () => clearInterval(cursorInterval); + }, []); + + // Handle the title typing animation + useEffect(() => { + if (!isTitleComplete) { + setCursorPosition('title'); + if (displayedTitle.length < titleText.length) { + const timeout = setTimeout(() => { + setDisplayedTitle(titleText.substring(0, displayedTitle.length + 1)); + }, typingSpeed); + + return () => clearTimeout(timeout); + } else { + setIsTitleComplete(true); + } + } + }, [displayedTitle, titleText, typingSpeed, isTitleComplete]); + + // Handle the subtitle typing animation (starts after title is complete) + useEffect(() => { + if (isTitleComplete && !isSubtitleComplete) { + setCursorPosition('subtitle'); + if (displayedSubtitle.length < subtitleText.length) { + const timeout = setTimeout(() => { + setDisplayedSubtitle(subtitleText.substring(0, displayedSubtitle.length + 1)); + }, typingSpeed); + + return () => clearTimeout(timeout); + } else { + setIsSubtitleComplete(true); + } + } + }, [displayedSubtitle, subtitleText, typingSpeed, isTitleComplete, isSubtitleComplete]); + + // Handle fade out after both animations are complete + useEffect(() => { + if (isSubtitleComplete) { + // Start fade out animation + const fadeOutTimeout = setTimeout(() => { + setIsFadingOut(true); + }, delayBeforeRemoval); + + // Set a timeout to remove the animation after fade out + const removalTimeout = setTimeout(() => { + setIsVisible(false); + if (onComplete) { + onComplete(); + } + }, delayBeforeRemoval + 1000); // Add 1000ms for fade out duration + + return () => { + clearTimeout(fadeOutTimeout); + clearTimeout(removalTimeout); + }; + } + }, [isSubtitleComplete, delayBeforeRemoval, onComplete]); + + if (!isVisible) { + return null; + } + + // Cursor component that can be reused + const Cursor = () => ( + + | + + ); + + // Default text shadow and glow styles + const defaultTextEffects = { + textShadow: '0 0 10px rgba(255, 255, 255, 0.25), 0 0 20px rgba(255, 255, 255, 0.15)', + filter: 'drop-shadow(0 2px 4px rgba(0, 0, 0, 0.25))' + }; + + // Blue glow for "Damien" + const blueGlowEffects = { + textShadow: '0 0 10px rgba(25, 118, 210, 0.35), 0 0 20px rgba(25, 118, 210, 0.25)', + filter: 'drop-shadow(0 2px 4px rgba(0, 0, 0, 0.25))' + }; + + // Red glow for exclamation mark + const redGlowEffects = { + textShadow: '0 0 10px rgba(244, 67, 54, 0.35), 0 0 20px rgba(244, 67, 54, 0.25)', + filter: 'drop-shadow(0 2px 4px rgba(0, 0, 0, 0.25))' + }; + + // Function to render the title with "Damien" in blue + const renderStyledTitle = () => { + // The full text we're typing + const fullTitleText = titleText; + + // Find where "Damien" starts in the full text + const damienStartIndex = fullTitleText.toLowerCase().indexOf('damien'); + + // If "Damien" isn't in the text (shouldn't happen, but just in case) + if (damienStartIndex === -1) { + return ( + <> + {displayedTitle} + {cursorPosition === 'title' && } + + ); + } + + // The end index of "Damien" in the full text + const damienEndIndex = damienStartIndex + 6; // "Damien" is 6 characters + + // Now we need to check what parts of the displayed text correspond to each section + const result = []; + + // Add each character with appropriate styling + for (let i = 0; i < displayedTitle.length; i++) { + // If this character is part of "Damien" + if (i >= damienStartIndex && i < damienEndIndex) { + result.push( + + {displayedTitle[i]} + + ); + } else { + // Regular character + result.push(displayedTitle[i]); + } + } + + return ( + <> + {result} + {cursorPosition === 'title' && } + + ); + }; + + // Function to render the subtitle with red exclamation mark + const renderStyledSubtitle = () => { + // If there's no exclamation mark yet in the displayed text + if (!displayedSubtitle.includes('!')) { + return ( + <> + {displayedSubtitle} + {cursorPosition === 'subtitle' && } + + ); + } + + // Find the exclamation mark + const exclamationIndex = displayedSubtitle.indexOf('!'); + + // Split the displayed text into parts + const beforeExclamation = displayedSubtitle.substring(0, exclamationIndex); + const exclamationMark = displayedSubtitle.substring(exclamationIndex, exclamationIndex + 1); // "!" + + return ( + <> + {beforeExclamation} + + {exclamationMark} + + {cursorPosition === 'subtitle' && } + + ); + }; + + return ( + + + + + {renderStyledTitle()} + + + + {renderStyledSubtitle()} + + + + + ); +}; + +export default TypingAnimation; \ No newline at end of file