PageSourceSearch

https://belfastbookfestival.com/js/animations.js

js belfastbookfestival.com collected 2026-10-03 23:54:52 UTC 15,690 bytes, 688 lines download raw bytes

1
2
3
4/* ================================
5   GSAP ANIMATIONS
6   ================================ */
7
8
9/* ================================
10   0. GSAP REGISTER & SETUP. REMEMBER YOU NEED TO LOAD THE GSAP AND SCROLLTRIGGER SCRIPTS FIRST
11================================ */
12gsap.registerPlugin(ScrollTrigger, ScrollSmoother);
13
14
15
16/* ---------------------------------
17   1. Shrink Banner on Scroll
18--------------------------------- */
19function shrinkBannerContents({ target, start = 100 } = {}) {
20  const banner = document.querySelector(target);
21  if (!banner) return;
22
23  gsap.registerPlugin(ScrollTrigger);
24
25  ScrollTrigger.create({
26    trigger: document.body,
27    start: `${start} top`,
28    endTrigger: document.body,
29    end: "bottom top", // ensures trigger runs through full page
30    toggleClass: { targets: banner, className: "is-shrunk" },
31    onLeaveBack: () => banner.classList.remove("is-shrunk"),
32  });
33}
34
35/* ---------------------------------
36   2. Card Fade + Hover Animation
37--------------------------------- */
38function animateCardImages({
39  cardSelector = ".card",
40  imgSelector = ".card-img-top",
41  btnSelector = ".btn",
42  scrollY = 0,
43  fadeDuration = 1,
44  hoverScale = 1.01,
45  hoverDuration = 1.2,
46  btnLiftY = -0.5,
47  btnShadowFrom = "0 2px 6px rgba(0,0,0,0.10)",
48  btnShadowTo = "0 8px 18px rgba(0,0,0,0.14)",
49  shimmerDuration = 0.3
50} = {}) {
51  const cards = document.querySelectorAll(cardSelector);
52  if (!cards.length) return;
53
54  cards.forEach(card => {
55    const img = card.querySelector(imgSelector);
56    const btn = card.querySelector(btnSelector);
57
58    // IMAGE ANIMATION
59    if (img) {
60		gsap.set(img, {
61			opacity: 1,
62			y: 0,
63			clearProps: "transform"
64		});
65
66		gsap.fromTo(
67			img,
68			{
69			opacity: 0,
70			y: scrollY
71			},
72			{
73			opacity: 1,
74			y: 0,
75			duration: fadeDuration,
76			ease: "power2.out",
77			immediateRender: false,
78			clearProps: "opacity,transform",
79			scrollTrigger: {
80				trigger: card,
81				start: "top 85%",
82				toggleActions: "play none none none",
83				once: true,
84				invalidateOnRefresh: true
85			}
86			}
87		);
88
89		const hoverTween = gsap.to(img, {
90			scale: hoverScale,
91			duration: hoverDuration,
92			paused: true,
93			ease: "power2.out",
94			transformOrigin: "center center"
95		});
96
97		card.addEventListener("mouseenter", () => hoverTween.play());
98		card.addEventListener("mouseleave", () => hoverTween.reverse());
99		}
100
101    // BUTTON ENHANCEMENT
102    if (btn) {
103      btn.style.position = "relative";
104      btn.style.overflow = "hidden";
105
106      // set a soft resting shadow
107      gsap.set(btn, {
108        boxShadow: btnShadowFrom
109      });
110
111      // add shimmer layer once
112      let shimmer = btn.querySelector(".btn-shimmer");
113      if (!shimmer) {
114        shimmer = document.createElement("span");
115        shimmer.className = "btn-shimmer";
116        btn.appendChild(shimmer);
117      }
118
119      // make sure shimmer starts hidden off to the left
120      gsap.set(shimmer, {
121        xPercent: -140,
122        opacity: 1
123      });
124
125      // button lift/shadow tween
126      const liftTween = gsap.to(btn, {
127        y: btnLiftY,
128        boxShadow: btnShadowTo,
129        duration: 0.25,
130        ease: "power2.out",
131        paused: true
132      });
133
134      // shimmer sweep tween
135      const shimmerTween = gsap.fromTo(
136        shimmer,
137        {
138          xPercent: -140,
139          opacity: 1
140        },
141        {
142          xPercent: 140,
143          opacity: 1,
144          duration: shimmerDuration,
145          ease: "power2.out",
146          paused: true
147        }
148      );
149
150      	// ENTER
151		card.addEventListener("mouseenter", () => {
152		liftTween.play();
153
154		// kill anything currently running on shimmer
155		gsap.killTweensOf(shimmer);
156
157		// hard reset before starting again
158		gsap.set(shimmer, {
159			xPercent: -140,
160			opacity: 1
161		});
162
163		// fresh shimmer
164		gsap.to(shimmer, {
165			xPercent: 140,
166			duration: shimmerDuration,
167			ease: "power2.out"
168		});
169		});
170
171		// LEAVE
172		card.addEventListener("mouseleave", () => {
173		liftTween.reverse();
174
175		// stop any in-progress shimmer cleanly
176		gsap.killTweensOf(shimmer);
177
178		// soft fade out, then reset
179		gsap.to(shimmer, {
180			opacity: 0,
181			duration: 0.15,
182			ease: "power1.out",
183			onComplete: () => {
184			gsap.set(shimmer, {
185				xPercent: -140,
186				opacity: 1
187			});
188			}
189		});
190		});
191    }
192  });
193}
194
195/* ---------------------------------
196   3. Parallax for Carousel Images
197--------------------------------- */
198function parallaxCarouselImage({
199  selector,
200  y = 100,
201  start = "top top",
202  end = "bottom top"
203}) {
204  const imgs = document.querySelectorAll(selector);
205  if (!imgs.length) return;
206
207  imgs.forEach(img => {
208    gsap.to(img, {
209      y,
210      ease: "none",
211      scrollTrigger: {
212        trigger: img.closest("section"),
213        start,
214        end,
215        scrub: true,
216        invalidateOnRefresh: true
217      }
218    });
219  });
220}
221
222/* ---------------------------------
223   4. Parallax Backgrounds (.parallax)
224--------------------------------- */
225function parallaxBackgrounds() {
226  document.querySelectorAll(".parallax").forEach(el => {
227    gsap.to(el, {
228      backgroundPositionY: "5%",
229      ease: "none",
230      scrollTrigger: {
231        trigger: el,
232        start: "top top",
233        end: "bottom bottom",
234        scrub: 0.5,
235        invalidateOnRefresh: true
236      }
237    });
238  });
239}
240
241
242/* ---------------------------------
243   5. Smooth Scroll Setup
244--------------------------------- */
245function smoothScrollSetup() {
246  // Initialize smooth scrolling
247  ScrollSmoother.create({
248    wrapper: ".smoothScrollContain",  // your main smooth scroll container
249    content: ".smoothContent",           // adjust if needed
250    smooth: 1.2,                      // scroll smoothing strength
251    effects: true,                    // enable effects (parallax etc)
252    normalizeScroll: true,            // handles browser quirks
253    ignoreMobileResize: true,          // improves mobile stability
254	fixedElements: ".sticky-top, .offcanvas, .cc-window, .cc-revoke"
255  });
256}
257
258
259/* ---------------------------------
260   6. Animate Slide
261--------------------------------- */
262function animateRowIn({
263  targetRow = ".animateRow",   // class or selector for rows to animate
264  distance = 80,               // slide distance (px)
265  duration = 0.4,                // animation duration (seconds)
266  delay = 0,                   // delay before animation starts (seconds)
267  direction = "lr",            // slide direction: "lr" = left→right, "rl" = right→left, "tb" = top→bottom, "bt" = bottom→top
268  stagger = false              // staggered fade-in inside each column (false or number)
269} = {}) {
270  if (!gsap || !ScrollTrigger) {
271    console.warn("GSAP or ScrollTrigger not loaded");
272    return;
273  }
274
275  gsap.registerPlugin(ScrollTrigger);
276
277  const rows = document.querySelectorAll(targetRow);
278  if (!rows.length) return;
279
280  rows.forEach((row) => {
281    const cols = row.querySelectorAll(".col-md-6, .col-lg-6");
282    if (!cols.length) return;
283
284    const tl = gsap.timeline({
285      scrollTrigger: {
286        trigger: row,
287        start: "top 85%",
288        toggleActions: "play none none none",
289        once: true
290      },
291      delay: delay
292    });
293
294    let leftVars = {}, rightVars = {};
295
296    // Set animation directions
297    switch (direction) {
298      case "lr": // left-right
299        leftVars = { x: -distance };
300        rightVars = { x: distance };
301        break;
302      case "rl": // right-left
303        leftVars = { x: distance };
304        rightVars = { x: -distance };
305        break;
306      case "tb": // top-bottom
307        leftVars = { y: -distance };
308        rightVars = { y: distance };
309        break;
310      case "bt": // bottom-top
311        leftVars = { y: distance };
312        rightVars = { y: -distance };
313        break;
314      default:
315        leftVars = { x: -distance };
316        rightVars = { x: distance };
317    }
318
319    // Animate both columns (or single col gracefully)
320    tl.from(cols[0], {
321      ...leftVars,
322      opacity: 0,
323      duration: duration,
324      ease: "power3.out"
325    });
326
327    if (cols[1]) {
328      tl.from(cols[1], {
329        ...rightVars,
330        opacity: 0,
331        duration: duration,
332        ease: "power3.out"
333      }, `-=${duration / 2}`); // overlap slightly
334    }
335
336    // Optional staggered fade-ins inside the row
337    if (stagger) {
338      const fadeTargets = row.querySelectorAll("p, h1, h2, h3, h4, h5, h6, img");
339      if (fadeTargets.length) {
340        tl.from(fadeTargets, {
341          y: 20,
342          opacity: 0,
343          duration: 0.6,
344          stagger: stagger,
345          ease: "power2.out"
346        }, "-=0.3");
347      }
348    }
349  });
350}
351
352
353function animatePanels({
354  selectors = [".leadIns .leadIn", ".navImageGrid .navCard"],
355  revealY = 24,
356  revealScale = 0.985,
357  revealDuration = 0.8,
358  stagger = 0.12,
359  start = "top 85%"
360} = {}) {
361  gsap.registerPlugin(ScrollTrigger);
362
363  selectors.forEach(selector => {
364    document.querySelectorAll(selector).forEach(panel => {
365      gsap.set(panel, {
366        transformOrigin: "center center",
367        willChange: "transform, opacity"
368      });
369    });
370
371    const groups = new Map();
372
373    document.querySelectorAll(selector).forEach(panel => {
374      const parentRow = panel.closest(".row, .navImageGrid");
375      const key = parentRow || panel.parentNode;
376
377      if (!groups.has(key)) groups.set(key, []);
378      groups.get(key).push(panel);
379    });
380
381    groups.forEach((panels, triggerEl) => {
382      gsap.fromTo(
383        panels,
384        {
385          opacity: 0,
386          y: revealY,
387          scale: revealScale
388        },
389        {
390          opacity: 1,
391          y: 0,
392          scale: 1,
393          duration: revealDuration,
394          stagger: stagger,
395          ease: "power2.out",
396          scrollTrigger: {
397            trigger: triggerEl,
398            start: start,
399            toggleActions: "play none none none"
400          }
401        }
402      );
403    });
404
405    document.querySelectorAll(selector).forEach(panel => {
406      const img = panel.querySelector("img");
407
408      panel.addEventListener("mouseenter", () => {
409        gsap.to(panel, {
410          y: -4,
411          scale: 1.01,
412          boxShadow: "0 12px 28px rgba(0,0,0,0.14)",
413          duration: 0.3,
414          ease: "power2.out",
415          overwrite: true
416        });
417
418        if (img) {
419          gsap.to(img, {
420            scale: 1.04,
421            duration: 0.6,
422            ease: "power2.out",
423            overwrite: true
424          });
425        }
426      });
427
428      panel.addEventListener("mouseleave", () => {
429        gsap.to(panel, {
430          y: 0,
431          scale: 1,
432          boxShadow: "0 6px 18px rgba(0,0,0,0.08)",
433          duration: 0.3,
434          ease: "power2.out",
435          overwrite: true
436        });
437
438        if (img) {
439          gsap.to(img, {
440            scale: 1,
441            duration: 0.6,
442            ease: "power2.out",
443            overwrite: true
444          });
445        }
446      });
447    });
448  });
449}
450
451
452/* ---------------------------------
453   7. Event Intro Hero Animation
454--------------------------------- */
455function animateEventIntroHero({
456  panelSelector = ".eventDetailIntroPanel",
457  imageSelector = ".eventHeroImage, .eventDetailIntroPanel img",
458  textSelector = ".summaryInfo > *",
459  imageRevealY = 2,
460  imageRevealScale = 1.1,
461  imageDuration = 0.2,
462  textRevealY = 22,
463  textDuration = 0.3,
464  textStagger = 0.1,
465  parallaxYPercent = 6,
466  parallaxStart = "top bottom",
467  parallaxEnd = "bottom top"
468} = {}) {
469  const panels = document.querySelectorAll(panelSelector);
470  if (!panels.length) return;
471
472  panels.forEach(panel => {
473    const image = panel.querySelector(imageSelector);
474    const textItems = panel.querySelectorAll(textSelector);
475
476    if (image) {
477      gsap.set(image, {
478        autoAlpha: 0,
479        y: imageRevealY,
480        scale: imageRevealScale,
481        transformOrigin: "center center",
482        willChange: "transform, opacity"
483      });
484    }
485
486    if (textItems.length) {
487      gsap.set(textItems, {
488        autoAlpha: 0,
489        y: textRevealY,
490        willChange: "transform, opacity"
491      });
492    }
493
494    const tl = gsap.timeline({
495      defaults: {
496        ease: "power2.out"
497      }
498    });
499
500    if (image) {
501      tl.to(image, {
502        autoAlpha: 1,
503        y: 0,
504        scale: 1,
505        duration: imageDuration
506      });
507    }
508
509    if (textItems.length) {
510      tl.to(textItems, {
511        autoAlpha: 1,
512        y: 0,
513        duration: textDuration,
514        stagger: textStagger
515      }, image ? "-=0.7" : 0);
516    }
517
518    if (image) {
519      gsap.to(image, {
520        yPercent: parallaxYPercent,
521        ease: "none",
522        scrollTrigger: {
523          trigger: panel,
524          start: parallaxStart,
525          end: parallaxEnd,
526          scrub: 1,
527          invalidateOnRefresh: true
528        }
529      });
530    }
531  });
532}
533
534
535
536/* ---------------------------------
537   8. Banner Gradient Intro Animation
538--------------------------------- */
539function animateBannerGradientIntro({
540  selector = ".bannerSection",
541  duration = 1.8,
542  delay = 0.1,
543  angleFrom = 0,
544  angleTo = -50,
545  stop1From = 0,
546  stop1To = 20,
547  stop2From = 10,
548  stop2To = 30,
549  alpha1From = 0.8,
550  alpha1To = 0.7,
551  alpha2From = 0.05,
552  alpha2To = 0.2
553} = {}) {
554  const banners = document.querySelectorAll(selector);
555  if (!banners.length) return;
556
557  banners.forEach(banner => {
558    gsap.fromTo(
559      banner,
560      {
561        "--banner-angle": angleFrom + "deg",
562        "--banner-stop-1": stop1From + "%",
563        "--banner-stop-2": stop2From + "%",
564        "--banner-alpha-1": alpha1From,
565        "--banner-alpha-2": alpha2From
566      },
567      {
568        "--banner-angle": angleTo + "deg",
569        "--banner-stop-1": stop1To + "%",
570        "--banner-stop-2": stop2To + "%",
571        "--banner-alpha-1": alpha1To,
572        "--banner-alpha-2": alpha2To,
573        duration: duration,
574        delay: delay,
575        ease: "power2.out"
576      }
577    );
578  });
579}
580
581
582
583/* ---------------------------------
584   Initialization & Helpers
585--------------------------------- */
586function initGSAPAnimations() {
587	// Banner shrink
588	shrinkBannerContents({ target: ".topBannerContainer" });
589
590
591	// Card fade & hover
592	animateCardImages({
593	cardSelector: ".card",
594	imgSelector: ".card-img-top",
595	hoverScale: 1.04
596	});
597
598	// Carousel parallax
599	parallaxCarouselImage({
600	selector: "#fpImageDiv .carousel-item img",
601	y: 180
602	});
603
604	// Background parallax
605	parallaxBackgrounds();
606
607	animatePanels();
608
609	animateBannerGradientIntro({
610		selector: ".bannerSection",
611		duration: 1,
612		angleFrom: 30,
613		angleTo: 45,
614		stop1From: 0,
615		stop1To: 30,
616		stop2From: 20,
617		stop2To: 40,
618		alpha2From: 0.1,
619		alpha2To: 0
620	});
621
622	animateEventIntroHero({
623		panelSelector: ".introSection",
624		imageSelector: ".introSection img",
625		textSelector: ".introSection > *",
626		parallaxYPercent:2
627	});
628
629	animateEventIntroHero({
630		panelSelector: ".eventDetailIntroPanel",
631		imageSelector: ".eventHeroImage, .eventDetailIntroPanel img",
632		textSelector: ".summaryInfo > *",
633		parallaxYPercent:2
634	});
635
636	//Smooth Scroll
637	//smoothScrollSetup();
638
639	/* animateRowIn({
640		targetRow: ".animateSlide",
641		direction: "rl",
642		distance: 20,
643		duration: 0.3,
644		stagger: 0.1
645	}); */
646
647	// Final refresh to ensure sync
648	ScrollTrigger.refresh(true);
649  
650}
651
652//CUSTOM BBF
653
654
655
656
657
658
659
660
661/* ---------------------------------
662   Global Event Listeners
663--------------------------------- */
664
665// Wait until everything is ready (avoids Chrome flicker)
666window.addEventListener("load", () => {
667  requestAnimationFrame(initGSAPAnimations);
668});
669
670// Debounced resize refresh
671let resizeTimer;
672window.addEventListener("resize", () => {
673  clearTimeout(resizeTimer);
674  resizeTimer = setTimeout(() => ScrollTrigger.refresh(), 250);
675});
676
677// Recalculate if page is scrolled to top after reload
678window.addEventListener(
679  "scroll",
680  () => {
681    if (window.scrollY === 0) ScrollTrigger.refresh();
682  },
683  { passive: true }
684);
685
686
687
688

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.