JS
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof gsap === 'undefined' || typeof ScrollTrigger === 'undefined' || typeof Physics2DPlugin === 'undefined') {
console.error("A GSAP plugin is missing!"); return;
}
gsap.registerPlugin(ScrollTrigger, Physics2DPlugin);
const fallingArea = document.getElementById('falling-letters-area');
const sentenceStates = {};
const bodyEl = document.getElementById('scrolling-body');
const storySection = document.getElementById('story-section'); // Used for text color
const downwardLook = { bg: '#F5F5DC', text: '#00008B' }; // Light Tan BG, Dark Blue Text
const upwardLook = { bg: '#00008B', text: '#F5F5DC' }; // Dark Blue BG, Light Tan Text
let currentGlobalLook = downwardLook; // Start with downward look
// Tween to apply colors smoothly
function transitionToLook(look, duration = 0.4) {
if (bodyEl.style.backgroundColor !== look.bg || storySection.style.color !== look.text) {
gsap.to(bodyEl, { backgroundColor: look.bg, duration: duration, ease: "sine.out" });
// Target .sentence directly for text color to ensure it overrides inheritance if needed
gsap.to('.sentence', { color: look.text, duration: duration, ease: "sine.out" });
currentGlobalLook = look; // Update the global state
}
}
function getLetterDataForFall(sentenceEl, sentenceId) { /* ... (same as before) ... */
const letterDataArray = [];
const originalStyles = { visibility: sentenceEl.style.visibility, opacity: sentenceEl.style.opacity };
sentenceEl.style.visibility = 'hidden'; sentenceEl.style.opacity = '1';
if(sentenceEl.parentElement) void sentenceEl.parentElement.offsetHeight;
void sentenceEl.offsetHeight;
const originalHTML = sentenceEl.innerHTML;
let tempHTML = ''; let letterIndex = 0;
Array.from(sentenceEl.childNodes).forEach(node => {
if (node.nodeType === Node.TEXT_NODE) {
node.textContent.split('').forEach(char => {
tempHTML += `<span class="measure-span" style="display:inline-block; position:relative;">${char}</span>`;
});
} else if (node.nodeName === 'BR') { tempHTML += '<br>'; }
else { tempHTML += node.outerHTML || ''; }
});
sentenceEl.innerHTML = tempHTML;
sentenceEl.querySelectorAll('.measure-span').forEach(span => {
const char = span.textContent;
if (char.trim() !== '' || char === ' ') {
const rect = span.getBoundingClientRect();
if (rect.width > 0 || char === ' ') {
letterDataArray.push({
char, id: `letter-${sentenceId}-fall-${letterIndex++}`,
rect: rect, width: rect.width, height: rect.height
});
}
}
});
sentenceEl.innerHTML = originalHTML;
sentenceEl.style.visibility = originalStyles.visibility;
sentenceEl.style.opacity = originalStyles.opacity;
return letterDataArray;
}
function createFallingLetterDiv(letterData, sentenceStyles, letterTextColor) { /* ... (same as before, takes letterTextColor) ... */
const letterDiv = document.createElement('div');
letterDiv.classList.add('falling-letter');
letterDiv.id = letterData.id;
letterDiv.textContent = letterData.char;
letterDiv.style.fontSize = sentenceStyles.fontSize;
letterDiv.style.lineHeight = sentenceStyles.lineHeight;
letterDiv.style.color = letterTextColor; // Apply specific color
gsap.set(letterDiv, {
x: letterData.rect.left, y: letterData.rect.top,
width: letterData.width > 0 ? letterData.width : 'auto',
opacity: 1
});
fallingArea.appendChild(letterDiv);
return letterDiv;
}
gsap.utils.toArray('.sentence').forEach((sentenceEl) => {
const sentenceId = sentenceEl.dataset.sentenceId;
sentenceStates[sentenceId] = {
letterDataForFall: null,
fallingLetterDivs: [],
isFallen: false,
fallAnimationTweens: []
// No complex reform timeline needed, just state and fade
};
ScrollTrigger.create({
trigger: sentenceEl,
start: "top top",
end: "bottom top",
// markers: true,
onEnter: () => { // SENTENCE HITS TOP (SCROLLING DOWN) -> FALL
if (sentenceStates[sentenceId].isFallen) return;
sentenceStates[sentenceId].isFallen = true;
transitionToLook(downwardLook); // Ensure downward look
gsap.to(sentenceEl, {opacity: 0, duration: 0.2, onComplete: () => {
gsap.set(sentenceEl, {visibility: 'hidden'});
}});
const sentenceComputedStyle = window.getComputedStyle(sentenceEl);
const sentenceFontSize = sentenceComputedStyle.fontSize;
let letterEffectiveHeight = parseFloat(sentenceComputedStyle.lineHeight === 'normal' ? parseFloat(sentenceFontSize) * 1.2 : sentenceComputedStyle.lineHeight);
if (isNaN(letterEffectiveHeight) || letterEffectiveHeight <= 0) letterEffectiveHeight = parseFloat(sentenceFontSize) * 1.2;
const sentenceStyles = { fontSize: sentenceFontSize, lineHeight: letterEffectiveHeight + 'px' };
if (!sentenceStates[sentenceId].letterDataForFall) {
sentenceStates[sentenceId].letterDataForFall = getLetterDataForFall(sentenceEl, sentenceId);
}
sentenceStates[sentenceId].fallingLetterDivs.forEach(div => div.remove());
sentenceStates[sentenceId].fallingLetterDivs = [];
sentenceStates[sentenceId].fallAnimationTweens = [];
sentenceStates[sentenceId].letterDataForFall.forEach((data) => {
// Letters fall with the "downwardLook" text color
const letterDiv = createFallingLetterDiv(data, sentenceStyles, downwardLook.text);
sentenceStates[sentenceId].fallingLetterDivs.push(letterDiv);
const boundsFloor = window.innerHeight - letterEffectiveHeight;
const tween = gsap.to(letterDiv, {
physics2D: { /* ... physics params ... */
gravity: gsap.utils.random(4000, 6000), angle: 270,
velocity: gsap.utils.random(200, 500), friction: 0.2,
bounds: { top:0,left:0,width:window.innerWidth,height:boundsFloor },
restitution: gsap.utils.random(0.0, 0.1)
},
duration: gsap.utils.random(2.5, 4.5), ease: "power1.in",
});
sentenceStates[sentenceId].fallAnimationTweens.push(tween);
});
},
onLeaveBack: () => { // SENTENCE LEAVES TOP GOING UP -> REFORM
if (!sentenceStates[sentenceId].isFallen) return;
sentenceStates[sentenceId].isFallen = false;
if (sentenceStates[sentenceId].fallAnimationTweens) {
sentenceStates[sentenceId].fallAnimationTweens.forEach(tween => tween.kill());
sentenceStates[sentenceId].fallAnimationTweens = [];
}
transitionToLook(upwardLook); // Switch to upward look
// Fade out the fallen letters (they are already at the bottom)
// Their color is not changed here, they just disappear.
// The background change makes them effectively "inverse".
gsap.to(sentenceStates[sentenceId].fallingLetterDivs, {
opacity: 0,
duration: 0.3, // Faster fade out
delay: 0, // Start fading immediately as colors change
onComplete: () => {
sentenceStates[sentenceId].fallingLetterDivs.forEach(div => div.remove());
sentenceStates[sentenceId].fallingLetterDivs = [];
}
});
// Fade in the original sentence. It will inherit the new text color from .sentence
// which was updated by transitionToLook.
gsap.set(sentenceEl, {
color: upwardLook.text, // Explicitly set color before fade for safety
visibility: 'visible'
});
gsap.to(sentenceEl, {
opacity: 1,
duration: 0.4,
delay: 0.1 // Delay slightly for BG change to start
});
}
});
});
// Optional: A global ScrollTrigger to detect scroll direction for proactive color change.
// This is more advanced and might be overly sensitive. The onEnter/onLeaveBack might be enough.
// let lastScrollTop = 0;
// ScrollTrigger.create({
// trigger: document.documentElement, // or window
// start: "top top",
// end: "bottom bottom",
// onUpdate: (self) => {
// let st = self.scrollTop;
// if (st > lastScrollTop) { // Scrolling Down
// if (currentGlobalLook !== downwardLook) {
// // console.log("Global down, setting downwardLook");
// // transitionToLook(downwardLook, 0.2); // Faster transition
// }
// } else { // Scrolling Up
// if (currentGlobalLook !== upwardLook) {
// // console.log("Global up, setting upwardLook");
// // transitionToLook(upwardLook, 0.2);
// }
// }
// lastScrollTop = st <= 0 ? 0 : st; // For Mobile or negative scrolling
// }
// });
window.addEventListener('resize', () => {
ScrollTrigger.refresh();
for (const id in sentenceStates) {
sentenceStates[id].letterDataForFall = null;
}
});
});
</script>