PageSourceSearch

https://symfony.com/assets/twenty_years/scroll-boxes-cnHzl4Y.js

js symfony.com collected 2026-09-24 06:40:21 UTC 8,485 bytes, 222 lines download raw bytes

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.