1import gsap from 'gsap'; 2import { boxes, positions } from './boxes.js'; 3import { hasLaunchedConfetti, launchConfettiFromBoxes, setHasLaunchConfetti } from './confettis-effect.js'; 4 5let allSectionTypes = []; 6 7async function loadYearsData() { 8 try { 9 const types = [{ type: 'intro', isMainYearSection: true }]; 10 const scriptElement = document.getElementById('years-data'); 11 if (!scriptElement) { 12 throw new Error('Years data script element not found'); 13 } 14 const data = JSON.parse(scriptElement.textContent); 15 data.forEach((year) => { 16 types.push({ type: 'year', isMainYearSection: true }); 17 year.dates?.forEach(() => 18 types.push({ 19 type: 'date', 20 isMainYearSection: false, 21 }), 22 ); 23 }); 24 types.push({ type: 'final', isMainYearSection: true }); 25 allSectionTypes = types; 26 } catch (error) { 27 console.error('Error loading years.json data:', error); 28 return null; 29 } 30} 31 32// Define GSAP ease types for randomized animations. 33const eases = ['power1.inOut', 'power2.inOut', 'power3.inOut']; 34const getRandomEase = () => eases[Math.floor(Math.random() * eases.length)]; 35 36// Shuffle box positions and mirror them horizontally. 37const shuffledBoxPositions = gsap.utils 38 .shuffle([...positions]) 39 .map((pair) => pair.map((pos) => ({ ...pos, left: 100 - pos.left }))); 40 41/** 42 * Creates and initializes two div elements for each box in the DOM. 43 */ 44export function createBoxes() { 45 const vh = window.innerHeight / 100; 46 const vw = window.innerWidth / 100; 47 const centerX = 50 * vw; 48 const centerY = 50 * vh; 49 50 boxes.forEach((box) => { 51 const mainBoxDiv = document.createElement('div'); 52 Object.assign(mainBoxDiv.style, { 53 backgroundColor: box.bgcolor, 54 width: box.width, 55 height: box.height, 56 position: 'fixed', 57 willChange: 'transform, opacity', 58 pointerEvents: 'none', 59 }); 60 mainBoxDiv.classList.add(box.name); 61 document.body.appendChild(mainBoxDiv); 62 63 gsap.set(`.${box.name}`, { 64 xPercent: -50, 65 yPercent: -50, 66 left: centerX, 67 top: centerY, 68 x: 0, 69 y: 0, 70 scale: 0, 71 opacity: 0, 72 }); 73 74 const bisBoxDiv = mainBoxDiv.cloneNode(); 75 bisBoxDiv.classList.remove(box.name); 76 bisBoxDiv.classList.add(`${box.name}bis`); 77 document.body.appendChild(bisBoxDiv); 78 79 // Initial position for the 'bis' box. 80 const [startPosition] = positions[boxes.indexOf(box)]; 81 const dx = startPosition.left * vw - centerX; 82 const dy = startPosition.top * vh - centerY; 83 gsap.set(bisBoxDiv, { 84 xPercent: -50, 85 yPercent: -50, 86 left: centerX, 87 top: centerY, 88 x: dx, 89 y: dy, 90 scale: startPosition.scale, 91 opacity: 1, 92 }); 93 }); 94} 95 96/** 97 * Sets up the main GSAP timeline for box animations based on scroll. 98 */ 99export async function setupBoxesTimeline() { 100 // Fetch years data once 101 if (!allSectionTypes?.length) await loadYearsData(); 102 const vh = window.innerHeight / 100; 103 const vw = window.innerWidth / 100; 104 const centerX = 50 * vw; 105 const centerY = 50 * vh; 106 const sectionCount = document.querySelectorAll('.section').length - 1; 107 108 // Utility function that converts a position defined in viewport units (left/top in %) 109 // into pixel-based transform values (x/y), relative to the screen center. 110 // This allows GPU-accelerated animations using transform rather than layout-affecting properties. 111 function computeDelta(pos) { 112 return { 113 x: pos.left * vw - centerX, 114 y: pos.top * vh - centerY, 115 scale: pos.scale, 116 opacity: pos.opacity, 117 }; 118 } 119 120 const tl = gsap.timeline({ 121 scrollTrigger: { 122 trigger: '.container', 123 start: 'top top', 124 end: `+=${100 * sectionCount * vh}`, // End trigger after all sections. 125 scrub: true, // Links timeline progress to scroll. 126 fastScrollEnd: true, 127 onUpdate: ({ progress }) => { 128 // Launch confetti when scroll reaches the end of the timeline. 129 if (progress >= 0.999 && !hasLaunchedConfetti) { 130 setHasLaunchConfetti(true); 131 launchConfettiFromBoxes(); 132 } else if (progress < 0.999) { 133 setHasLaunchConfetti(false); // Reset flag if scrolling back up. 134 } 135 }, 136 }, 137 }); 138 139 // Define box animations for each scroll step (section transition). 140 for (let step = 0; step < sectionCount; step++) { 141 const { isMainYearSection } = allSectionTypes[step]; 142 const isNextMainYear = step < sectionCount - 1 ? allSectionTypes[step + 1].isMainYearSection : true; // Treat last section as a main year for animation purposes. 143
144 boxes.forEach((box, i) => { 145 const [p1, p1bis] = shuffledBoxPositions[i]; 146 const [p2, p3] = positions[i]; 147 148 const p1Delta = computeDelta(p1); 149 const p1bisDelta = computeDelta(p1bis); 150 const p2Delta = computeDelta(p2); 151 const p3Delta = computeDelta(p3); 152 153 // Animation for the main box. 154 tl.to( 155 `.${box.name}`, 156 { 157 ease: getRandomEase(), 158 keyframes: [ 159 // Start: hidden at center 160 { x: 0, y: 0, scale: 0, autoAlpha: 0, duration: 0 }, 161 // Mid-point 1: moves to shuffled position 1 162 { 163 x: p1Delta.x, 164 y: p1Delta.y, 165 scale: p1Delta.scale, 166 autoAlpha: isNextMainYear ? p1Delta.opacity : i % 3 === 0 ? 0.3 : 0, 167 duration: 0.6, 168 }, 169 // Mid-point 2: moves to shuffled position 2 170 { 171 x: p1bisDelta.x, 172 y: p1bisDelta.y, 173 scale: p1bisDelta.scale, 174 autoAlpha: isNextMainYear ? p1bisDelta.opacity : i % 3 === 0 ? 0.3 : 0, 175 duration: 0.4, 176 }, 177 // End: hidden at center 178 { x: 0, y: 0, scale: 0, autoAlpha: 0, duration: 0 }, 179 ], 180 }, 181 step, // Position this animation within the main timeline. 182 ); 183 184 // Animation for the 'bis' box. 185 tl.to( 186 `.${box.name}bis`, 187 { 188 ease: getRandomEase(), 189 keyframes: [ 190 // Start: at its original position 191 { 192 x: p2Delta.x, 193 y: p2Delta.y, 194 scale: p2Delta.scale, 195 autoAlpha: isMainYearSection ? p2Delta.opacity : i % 3 === 0 ? 0.3 : i % 4 === 0 ? 0.6 : 0, // Dynamic opacity 196 duration: 0, 197 }, 198 // Mid-point: moves to a new position, fades out 199 { 200 x: p3Delta.x, 201 y: p3Delta.y, 202 scale: p3Delta.scale, 203 autoAlpha: 0, 204 duration: 0.4, 205 }, 206 // Reset: hidden at center 207 { x: 0, y: 0, scale: 0, autoAlpha: 0, duration: 0 }, 208 // Re-enter: moves back to original position 209 { 210 x: p2Delta.x, 211 y: p2Delta.y, 212 scale: p2Delta.scale, 213 autoAlpha: isNextMainYear ? p2Delta.opacity : i % 3 === 0 ? 0.3 : i % 4 === 0 ? 0.6 : 0, // Dynamic opacity 214 duration: 0.6, 215 }, 216 ], 217 }, 218 step, // Position this animation within the main timeline. 219 ); 220 }); 221 } 222}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.