|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>Matrix Text Animation Demo</title> |
|
<style> |
|
/* Reset and base styles */ |
|
* { |
|
margin: 0; |
|
padding: 0; |
|
box-sizing: border-box; |
|
} |
|
|
|
html, body { |
|
font-family: 'Arial', sans-serif; |
|
line-height: 1.6; |
|
color: #333; |
|
background-color: #f5f5f5; |
|
height: 100%; |
|
scroll-behavior: smooth; |
|
} |
|
|
|
/* Section styles */ |
|
section { |
|
min-height: 100vh; |
|
padding: 4rem 2rem; |
|
display: flex; |
|
flex-direction: column; |
|
justify-content: center; |
|
align-items: center; |
|
opacity: 0.7; |
|
transition: opacity 0.5s ease; |
|
} |
|
|
|
section.active { |
|
opacity: 1; |
|
} |
|
|
|
/* Matrix text animation styles */ |
|
[class^="matrixtext"] { |
|
opacity: 0; |
|
visibility: hidden; |
|
transition: opacity 0.3s ease, visibility 0.3s ease; |
|
/* backdrop-filter: blur(2px); */ |
|
} |
|
|
|
[class^="matrixtext"].animate { |
|
opacity: 1; |
|
visibility: visible; |
|
} |
|
|
|
[class^="matrixtext"].animate span { |
|
opacity: 0; |
|
animation: charReveal 0.3s forwards; |
|
animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1); |
|
} |
|
|
|
.matrixtext1.animate span { |
|
animation-delay: calc(var(--char-index) * 0.015s); |
|
} |
|
|
|
.matrixtext2.animate span { |
|
animation-delay: calc(var(--char-index) * 0.02s); |
|
} |
|
|
|
.matrixtext3.animate span { |
|
animation-delay: calc(var(--char-index) * 0.03s); |
|
} |
|
|
|
@keyframes charReveal { |
|
0% { |
|
opacity: 0; |
|
} |
|
100% { |
|
opacity: 1; |
|
} |
|
} |
|
|
|
/* Additional styling for the demo */ |
|
.container { |
|
max-width: 1200px; |
|
margin: 0 auto; |
|
text-align: center; |
|
} |
|
|
|
h1 { |
|
font-size: 3rem; |
|
margin-bottom: 1rem; |
|
} |
|
|
|
h2 { |
|
font-size: 2.5rem; |
|
margin-bottom: 1rem; |
|
} |
|
|
|
p { |
|
font-size: 1.2rem; |
|
margin-bottom: 2rem; |
|
max-width: 800px; |
|
} |
|
|
|
.matrix-group { |
|
margin: 2rem 0; |
|
} |
|
|
|
.nav-buttons { |
|
position: fixed; |
|
bottom: 2rem; |
|
right: 2rem; |
|
display: flex; |
|
gap: 1rem; |
|
} |
|
|
|
button { |
|
padding: 0.5rem 1rem; |
|
background-color: #333; |
|
color: white; |
|
border: none; |
|
border-radius: 4px; |
|
cursor: pointer; |
|
transition: background-color 0.3s ease; |
|
} |
|
|
|
button:hover { |
|
background-color: #555; |
|
} |
|
|
|
/* Color variations for sections */ |
|
.herosection { |
|
background-color: #f0f8ff; |
|
} |
|
|
|
.contentsection { |
|
background-color: #fff0f5; |
|
} |
|
|
|
.aboutmesection { |
|
background-color: #f0fff0; |
|
} |
|
|
|
.gallerysection { |
|
background-color: #fff8dc; |
|
} |
|
|
|
.blogsection { |
|
background-color: #f5f5dc; |
|
} |
|
|
|
.contactsection { |
|
background-color: #f0ffff; |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<section class="herosection active"> |
|
<div class="container"> |
|
<div class="matrix-group"> |
|
<h1 class="matrixtext1">Matrix Text Animation</h1> |
|
<p class="matrixtext2">Watch as text reveals itself character by character with a smooth animation.</p> |
|
<p class="matrixtext3 stall">This text will wait a bit before animating.</p> |
|
</div> |
|
</div> |
|
</section> |
|
|
|
<section class="contentsection"> |
|
<div class="container"> |
|
<div class="matrix-group"> |
|
<h2 class="matrixtext1">Features</h2> |
|
<p class="matrixtext2">Character-by-character reveal with customizable speeds</p> |
|
<p class="matrixtext2 stall1">Configurable delay times (this has a 1-second delay)</p> |
|
<p class="matrixtext3 stall2">Even longer delays are possible (this has a 2-second delay)</p> |
|
</div> |
|
</div> |
|
</section> |
|
|
|
<section class="aboutmesection"> |
|
<div class="container"> |
|
<div class="matrix-group"> |
|
<h2 class="matrixtext1">About</h2> |
|
<p class="matrixtext2">This animation system works with intersection observers</p> |
|
<p class="matrixtext3">Elements animate when they come into view</p> |
|
</div> |
|
</div> |
|
</section> |
|
|
|
<section class="gallerysection"> |
|
<div class="container"> |
|
<div class="matrix-group"> |
|
<h2 class="matrixtext1">Gallery</h2> |
|
<p class="matrixtext2">Different sections can have different animation speeds</p> |
|
<p class="matrixtext3">Try scrolling between sections to see animations trigger</p> |
|
</div> |
|
</div> |
|
</section> |
|
|
|
<section class="blogsection"> |
|
<div class="container"> |
|
<div class="matrix-group"> |
|
<h2 class="matrixtext1">Blog</h2> |
|
<p class="matrixtext2">Animations only trigger when sections become active</p> |
|
<p class="matrixtext3">This helps control when animations happen</p> |
|
</div> |
|
</div> |
|
</section> |
|
|
|
<section class="contactsection"> |
|
<div class="container"> |
|
<div class="matrix-group"> |
|
<h2 class="matrixtext1">Contact</h2> |
|
<p class="matrixtext2">Text animations can create engaging user experiences</p> |
|
<p class="matrixtext3">Perfect for revealing content dramatically</p> |
|
</div> |
|
</div> |
|
</section> |
|
|
|
<div class="nav-buttons"> |
|
<button id="prev-section">Previous Section</button> |
|
<button id="next-section">Next Section</button> |
|
</div> |
|
|
|
<script> |
|
// MatrixText class implementation |
|
class MatrixText { |
|
constructor(group) { |
|
this.instanceId = Math.random().toString(36).substring(2, 9); |
|
this.group = group; |
|
this.elements = [...group.querySelectorAll('[class^="matrixtext"]')].sort( |
|
(a, b) => { |
|
const aNumMatch = a.className.match(/matrixtext(\d+)/); |
|
const bNumMatch = b.className.match(/matrixtext(\d+)/); |
|
const aNum = aNumMatch ? parseInt(aNumMatch[1]) : 0; |
|
const bNum = bNumMatch ? parseInt(bNumMatch[1]) : 0; |
|
return aNum - bNum; |
|
} |
|
); |
|
|
|
// Store original content for each element |
|
this.originalContents = new Map(); |
|
this.setupElements(); |
|
this.currentIndex = 0; |
|
this.hasStarted = false; |
|
this.isAnimating = false; |
|
this.isComplete = false; |
|
} |
|
|
|
setupElements() { |
|
this.elements.forEach((el) => { |
|
const textContent = el.textContent; |
|
// Store original content and tag name |
|
this.originalContents.set(el, { |
|
text: textContent, |
|
className: el.className |
|
}); |
|
|
|
el.innerHTML = ""; |
|
|
|
[...textContent].forEach((char, i) => { |
|
const span = document.createElement("span"); |
|
span.textContent = char; |
|
span.style.setProperty("--char-index", i); |
|
el.appendChild(span); |
|
}); |
|
|
|
// Ensure elements are hidden BEFORE first IntersectionObserver check |
|
el.style.opacity = "0"; |
|
el.style.visibility = "hidden"; |
|
}); |
|
} |
|
|
|
revertToOriginal(element) { |
|
if (!this.originalContents.has(element)) return; |
|
|
|
const original = this.originalContents.get(element); |
|
const spans = element.querySelectorAll("span"); |
|
|
|
// Ensure all spans are fully visible first |
|
spans.forEach(span => { |
|
span.style.opacity = "1"; |
|
}); |
|
|
|
// Small delay to ensure all spans are visible before reverting |
|
setTimeout(() => { |
|
// Replace with original content in one operation to avoid flicker |
|
element.innerHTML = original.text; |
|
// Make sure we maintain the same class and visibility |
|
element.className = original.className; |
|
element.classList.add('animate'); // Keep the animate class |
|
element.style.opacity = "1"; |
|
element.style.visibility = "visible"; |
|
}, 50); |
|
} |
|
|
|
async animateNext() { |
|
if (this.isComplete) { |
|
return; |
|
} |
|
if (this.currentIndex >= this.elements.length || this.isAnimating) { |
|
if(this.currentIndex >= this.elements.length && !this.isComplete) { |
|
this.isComplete = true; |
|
} |
|
return; |
|
} |
|
|
|
this.isAnimating = true; |
|
const element = this.elements[this.currentIndex]; |
|
|
|
// Handle different stall durations |
|
if (element.classList.contains("stall2")) { |
|
await new Promise((resolve) => setTimeout(resolve, 2000)); // 2 seconds |
|
} else if (element.classList.contains("stall1")) { |
|
await new Promise((resolve) => setTimeout(resolve, 1000)); // 1 second |
|
} else if (element.classList.contains("stall")) { |
|
await new Promise((resolve) => setTimeout(resolve, 500)); // 0.5 seconds |
|
} |
|
|
|
element.style.setProperty("opacity", "1"); |
|
element.style.setProperty("visibility", "visible"); |
|
element.classList.add("animate"); |
|
|
|
const spans = element.querySelectorAll("span"); |
|
if (spans.length === 0) { |
|
this.currentIndex++; |
|
this.isAnimating = false; |
|
if (this.currentIndex >= this.elements.length) { |
|
this.isComplete = true; |
|
} else { |
|
this.animateNext(); |
|
} |
|
return; |
|
} |
|
|
|
// Calculate proper animation duration based on the element class |
|
let animationDelayFactor = 0.008; // Default for matrixtext1 |
|
if (element.classList.contains('matrixtext2')) animationDelayFactor = 0.02; |
|
if (element.classList.contains('matrixtext3')) animationDelayFactor = 0.03; |
|
|
|
// Calculate total animation time including the delay for the last character |
|
const lastIndex = spans.length - 1; |
|
const baseAnimationTime = 300; // 0.3s duration from CSS |
|
const lastCharDelay = lastIndex * animationDelayFactor * 1000; |
|
const totalAnimationTime = baseAnimationTime + lastCharDelay + 100; // Add small buffer |
|
|
|
// Set up proper timeout that matches the CSS animation timing |
|
const timeoutPromise = new Promise((resolve) => setTimeout(resolve, totalAnimationTime)); |
|
|
|
// Animation end detection can still be included as a race |
|
const animationEndPromise = new Promise((resolve) => { |
|
const lastSpan = spans[lastIndex]; |
|
lastSpan.addEventListener("animationend", resolve, { once: true }); |
|
}); |
|
|
|
await Promise.race([animationEndPromise, timeoutPromise]); |
|
|
|
// Revert to original element after animation completes |
|
this.revertToOriginal(element); |
|
|
|
this.currentIndex++; |
|
this.isAnimating = false; |
|
|
|
if (this.currentIndex >= this.elements.length) { |
|
this.isComplete = true; |
|
} else { |
|
this.animateNext(); |
|
} |
|
} |
|
} |
|
|
|
function isInActiveSection(element) { |
|
const section = element.closest('.herosection, .contentsection, .aboutmesection, .gallerysection, .blogsection, .contactsection'); |
|
if (!section) { |
|
return true; |
|
} |
|
return section.classList.contains('active'); |
|
} |
|
|
|
let isInitialized = false; |
|
const activeAnimations = new WeakMap(); |
|
|
|
function initializeMatrixAnimations() { |
|
if (isInitialized) { |
|
return; |
|
} |
|
|
|
isInitialized = true; |
|
|
|
const intersectionObserverOptions = { |
|
threshold: 0.1, |
|
rootMargin: "0px 0px -5% 0px", |
|
}; |
|
|
|
const observer = new IntersectionObserver( |
|
(entries) => { |
|
entries.forEach((entry) => { |
|
const group = entry.target; |
|
|
|
let matrix = activeAnimations.get(group); |
|
if (!matrix) { |
|
matrix = new MatrixText(group); |
|
activeAnimations.set(group, matrix); |
|
} |
|
|
|
if (entry.isIntersecting) { |
|
if (!isInActiveSection(group)) { |
|
return; |
|
} |
|
|
|
if (!matrix.hasStarted && !matrix.isAnimating && !matrix.isComplete) { |
|
matrix.hasStarted = true; |
|
matrix.animateNext(); |
|
} |
|
} |
|
}); |
|
}, |
|
intersectionObserverOptions |
|
); |
|
|
|
const matrixGroups = document.querySelectorAll(".matrix-group"); |
|
matrixGroups.forEach((group) => { |
|
observer.observe(group); |
|
}); |
|
|
|
const sectionObserver = new MutationObserver((mutations) => { |
|
mutations.forEach((mutation) => { |
|
if (mutation.type === 'attributes' && mutation.attributeName === 'class') { |
|
const section = mutation.target; |
|
|
|
if (section.classList.contains('active')) { |
|
const groupsInActivatedSection = section.querySelectorAll('.matrix-group'); |
|
|
|
groupsInActivatedSection.forEach((group) => { |
|
let matrix = activeAnimations.get(group); |
|
|
|
if (!matrix) { |
|
matrix = new MatrixText(group); |
|
activeAnimations.set(group, matrix); |
|
observer.observe(group); |
|
} else if (!matrix.isComplete) { |
|
observer.unobserve(group); |
|
observer.observe(group); |
|
} |
|
}); |
|
} |
|
} |
|
}); |
|
}); |
|
|
|
const sectionsToWatch = document.querySelectorAll('.herosection, .contentsection, .aboutmesection, .gallerysection, .blogsection, .contactsection'); |
|
sectionsToWatch.forEach((section) => { |
|
sectionObserver.observe(section, { |
|
attributes: true, |
|
attributeFilter: ['class'], |
|
childList: false, |
|
subtree: false |
|
}); |
|
}); |
|
} |
|
|
|
// Ensure DOM is fully ready before initializing |
|
if (document.readyState === 'loading') { |
|
document.addEventListener('DOMContentLoaded', initializeMatrixAnimations); |
|
} else { |
|
initializeMatrixAnimations(); |
|
} |
|
|
|
// Navigation functionality for the demo |
|
document.addEventListener('DOMContentLoaded', function() { |
|
const sections = document.querySelectorAll('section'); |
|
const prevButton = document.getElementById('prev-section'); |
|
const nextButton = document.getElementById('next-section'); |
|
let currentSectionIndex = 0; |
|
|
|
// Initialize first section as active |
|
sections[0].classList.add('active'); |
|
|
|
function updateActiveSection() { |
|
sections.forEach(section => section.classList.remove('active')); |
|
sections[currentSectionIndex].classList.add('active'); |
|
sections[currentSectionIndex].scrollIntoView({ behavior: 'smooth' }); |
|
} |
|
|
|
prevButton.addEventListener('click', function() { |
|
if (currentSectionIndex > 0) { |
|
currentSectionIndex--; |
|
updateActiveSection(); |
|
} |
|
}); |
|
|
|
nextButton.addEventListener('click', function() { |
|
if (currentSectionIndex < sections.length - 1) { |
|
currentSectionIndex++; |
|
updateActiveSection(); |
|
} |
|
}); |
|
|
|
// Handle scroll events to update active section |
|
window.addEventListener('scroll', function() { |
|
const scrollPosition = window.scrollY + window.innerHeight / 2; |
|
|
|
sections.forEach((section, index) => { |
|
const sectionTop = section.offsetTop; |
|
const sectionBottom = sectionTop + section.offsetHeight; |
|
|
|
if (scrollPosition >= sectionTop && scrollPosition < sectionBottom) { |
|
if (currentSectionIndex !== index) { |
|
currentSectionIndex = index; |
|
sections.forEach(s => s.classList.remove('active')); |
|
section.classList.add('active'); |
|
} |
|
} |
|
}); |
|
}); |
|
}); |
|
</script> |
|
</body> |
|
</html> |