feat: typing animation landing page intro
This commit is contained in:
parent
688c77d90a
commit
c806a772e3
@ -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 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() {
|
export default function HomePage() {
|
||||||
|
const [showContent, setShowContent] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: 'flex' }}>
|
<>
|
||||||
</Box>
|
{!showContent && (
|
||||||
)
|
<TypingAnimation
|
||||||
|
titleText="My name is Damien."
|
||||||
|
subtitleText="Welcome to my website!"
|
||||||
|
typingSpeed={80}
|
||||||
|
delayBeforeRemoval={3000}
|
||||||
|
onComplete={() => setShowContent(true)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Fade in={showContent} timeout={1000} style={{ transitionDelay: showContent ? '500ms' : '0ms' }}>
|
||||||
|
<Box sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
minHeight: '100vh',
|
||||||
|
p: 4,
|
||||||
|
visibility: showContent ? 'visible' : 'hidden'
|
||||||
|
}}>
|
||||||
|
<Typography variant="h3" component="h1" gutterBottom>
|
||||||
|
Damien's Website
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" paragraph>
|
||||||
|
This is a test sentence.
|
||||||
|
</Typography>
|
||||||
|
{/* Add more content here */}
|
||||||
|
</Box>
|
||||||
|
</Fade>
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
263
src/components/TypingAnimation.tsx
Normal file
263
src/components/TypingAnimation.tsx
Normal file
@ -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<TypingAnimationProps> = ({
|
||||||
|
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 = () => (
|
||||||
|
<Box
|
||||||
|
component="span"
|
||||||
|
sx={{
|
||||||
|
opacity: showCursor ? 1 : 0,
|
||||||
|
transition: 'opacity 0.5s',
|
||||||
|
ml: 0.5,
|
||||||
|
display: 'inline-block'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
|
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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' && <Cursor />}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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(
|
||||||
|
<Box
|
||||||
|
key={i}
|
||||||
|
component="span"
|
||||||
|
sx={{
|
||||||
|
color: '#1976d2', /* Material UI primary blue */
|
||||||
|
...blueGlowEffects
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{displayedTitle[i]}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Regular character
|
||||||
|
result.push(displayedTitle[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{result}
|
||||||
|
{cursorPosition === 'title' && <Cursor />}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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' && <Cursor />}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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}
|
||||||
|
<Box
|
||||||
|
component="span"
|
||||||
|
sx={{
|
||||||
|
color: '#f44336', /* Material UI red */
|
||||||
|
...redGlowEffects
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{exclamationMark}
|
||||||
|
</Box>
|
||||||
|
{cursorPosition === 'subtitle' && <Cursor />}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fade in={!isFadingOut} timeout={1000}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'fixed',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
backgroundColor: 'background.default',
|
||||||
|
zIndex: 1000
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack spacing={1} alignItems="center">
|
||||||
|
<Typography
|
||||||
|
variant="h2"
|
||||||
|
component="div"
|
||||||
|
sx={{
|
||||||
|
fontWeight: 'bold',
|
||||||
|
...defaultTextEffects
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{renderStyledTitle()}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Typography
|
||||||
|
variant="h5"
|
||||||
|
component="div"
|
||||||
|
sx={{
|
||||||
|
color: 'text.secondary',
|
||||||
|
...defaultTextEffects
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{renderStyledSubtitle()}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
</Fade>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TypingAnimation;
|
Loading…
x
Reference in New Issue
Block a user