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 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 (
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
<>
|
||||
{!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