Mastering Web Performance: The Fusion of GSAP, Three.js, Lenis, and Web Audio in Trionn

Jul 15, 2026 982 views

Decoding Trionn: The Integration of GSAP, Three.js, Lenis, and Web Audio

A closer look at the intricate coordination of multiple animation and rendering techniques to create a fluid web experience.

The creation of Trionn wasn't just a design quest; it was an ambitious experiment focused on merging animation, WebGL, and interactive elements into one cohesive web presence. After comprehensive iterations that spanned several months, the project materialized into a sophisticated interplay of technologies including GSAP, Three.js, Lenis, and customized Web Audio features, all tailored to deliver a responsive digital experience where each section functions as part of an integrated ecosystem.

What’s striking is how the project transitioned from several initial concepts. Elements like an interactive hero, scroll-driven narrative techniques, and dynamic real-time graphics didn't spring forth fully formed; they gradually merged into a unified framework for animation, showcasing an evolutionary design process.

Developed over a four-month period, Trionn has received accolades from prestigious platforms such as FWA, GSAP, Orpetron, CSS Design Awards, Web Design Awards, and CSS Winner. Yet, beyond the accolades, the creative process unearthed significant technical hurdles related to performance optimization, synchronization challenges, rendering issues, and interaction design intricacies.

In this case study, we’ll dissect the foundation of Trionn, focusing on the underlying architecture, the animation systems utilized, the WebGL techniques employed, and the strategies adopted to optimize performance.

Technical Insights

Creating a site with such intricate animations requires a delicate balance between creativity and technical performance. Each technology in this stack serves a distinctive role—driving animations, managing scroll interactions, rendering captivating WebGL scenes, and generating real-time audio.

Key technologies integrated into the project include:

  • GSAP + @gsap/react for managing timelines, user interactions, and fine-tuned page transitions.
  • ScrollTrigger for implementing scroll-related interactions, like pinned sections and reveal sequences.
  • SplitText for creating customizable text animations at various granular levels.
  • Three.js as the backbone for the hero graphics, featured sections, and the interactive portfolio grid.
  • Lenis for enhancing smooth scroll effects, in sync with GSAP.
  • Web Audio API for real-time sound generation and audio interactions.
  • Next.js and React for application structuring and framework support.
  • Tailwind CSS for design aesthetics.
  • Swiper for dynamic testimonial and award showcases.

At the heart of this vibrant animation ecosystem lies GSAP, orchestrating a myriad of animations—from page transitions to scroll-triggered sequences and component-specific animations—all meticulously managed through GSAP timelines and the <a href="https://gsap.com/resources/React/">useGSAP</a> hook, streamlining component management throughout Next.js.

ScrollTrigger plays a pivotal role in directing the majority of scroll interactions across the site. This includes everything from pinned narrative sections to scrubbed animations that conjoin user actions with visual drama. Additionally, gsap.matchMedia() is heavily leveraged, permitting distinct animation logics for desktop and mobile experiences, avoiding a one-size-fits-all approach.

For animating text, we developed an adaptable BlurTextReveal component using SplitText, which supports character, word, and line-based animations. It streamlines functionality, handling reduced motion settings and GPU layer management efficiently.

Three.js facilitates the site’s unique WebGL elements, diverging from using React Three Fiber to maintain precision over the render loop and resource handling, especially for the intricately animated hero symbol.

Lenis coordinates with gsap.ticker to synchronize scrolling mechanisms site-wide, ensuring a cohesive user experience.

To amplify interactivity, sound effects—such as those for the hero's hover, explosion, and welding effects—are generated using the Web Audio API, enabling a dynamic audio landscape rather than relying on static sound files.

Hero Section Dynamics

The development of the hero section was especially dynamic, integrating a range of technologies—WebGL, GSAP, SplitText, and the Web Audio API—into a singular interactive scheme, making it one of the most complex components of the site.

The hero is constructed from two interconnected layers: a Three.js scene that forms the backdrop, rendering the brand symbol and its various interactions—such as idle motion, hover effects, and explosive interactions—and a foreground of standard DOM elements, including the headline and rotating words, which are animated using GSAP and SplitText. This combination allows the text to remain accessible while effectively blending with the animated canvas.

Both layers operate under a synchronized transitionReady mechanism, which triggers animations only after a page transition is completed, using requestIdleCallback to manage performance efficiently and keep the initial loading snappy.

The Hero Headline Animation

The introduction of the hero headline—“Designed to”—is executed through a staggered animation style, where each character appears one at a time, emphasizing a more engaging viewer experience compared to conventional fade-ins.

// components/Sections/Home/Banner.tsx — usage
<BlurTextReveal
 as="h1"
 text="Designed to"
 animationType="chars" 
 stagger={0.08}
 delay={1.2} 
/>

// components/TextAnimation/BlurTextReveal.tsx — the underlying engine
const split = new SplitText(textRef.current, {
 type: "chars, words, lines",
 smartWrap: true,
});

const targets = split.chars; 

gsap.set([textRef.current, targets], {
 autoAlpha: 0,
 filter: "blur(12px)",
 willChange: "filter, opacity", 
});

const tl = gsap.timeline({
 paused: manual,
});

tl.to(textRef.current, {
    autoAlpha: 1,
    filter: "blur(0px)",
    duration: 0.5,
  }, delay)
  .to(targets, {
    autoAlpha: 1,
    filter: "blur(0px)",
    duration: 0.8,
    stagger: {
      each: 0.08,
      from: "random",
    }, 
    ease: "power2.out",
  }, delay);

By using filter: blur() along with opacity modulation, the text appears to come alive instead of merely fading in. After the animation concludes, we eliminate will-change, freeing up GPU layers for other elements.

This same BlurTextReveal component is employed across the site for various textual elements, each time tailored to fit specific animation requirements.

Idle Behavior of the Hero Symbol

In its rest state, the hero symbol exhibits a gentle rotation while its three arms undulate in a sine-wave pattern with offset phases, creating a fluid and organic visual rhythm.

// hooks/useTrionnSymbolScene.ts — ongoing update loop

// Automatic rotation adjusts to mouse position
if (!st.dragging) {
  st.rotY += prefersReducedMotion ? 0.0015 : 0.0042; 
  st.rotX = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, st.rotX));

  group.rotation.x +=
    (st.rotX + mouse.y * 0.22 - group.rotation.x) * 0.06; 
  group.rotation.y +=
    (st.rotY + mouse.x * 0.22 - group.rotation.y) * 0.06;
}

// Each arm has independent movement for enhanced realism
particles.forEach((p) => {
  const phase = p.shapeIdx * (Math.PI * 2 / 3); 

  const armDriftX =
    Math.sin(t * 0.4 + phase) * 0.012 * (1 - explodeAmt);
  const armDriftY =
    Math.cos(t * 0.35 + phase) * 0.008 * (1 - explodeAmt);
  const armDriftZ =
    Math.sin(t * 0.3 + phase * 1.5) * 0.006 * (1 - explodeAmt);
});

When the reduced motion preference is active, we simply adjust the speed instead of disabling motion entirely. Mouse movement is smoothed using linear interpolation, giving the symbol a magnetic, organic feel as it reacts to user input. The ambient drift also scales back as other interaction states become active.

Magnetic Hover Effects

Upon hovering over the symbol with the cursor, the corresponding panel “charges,” visibly brightening and reflecting light, accompanied by a distinct beep for the first interaction. This hover detection is accomplished via raycasting, allowing the interaction to map precisely to the symbol’s geometry as it moves.

// hooks/useTrionnSymbolScene.ts — raycasting for hover detection

const raycaster = new THREE.Raycaster();

if (
  st.mouseScreenX !== -9999 &&
  st.scrollProgress < 0.08 &&
  st.clickBurst < 0.05 &&
  st.introAmt < 0.08
) {
  raycaster.setFromCamera(mouse, camera);

  const hits = raycaster.intersectObjects(
    particles
      .filter((p) => !p.isEdge)
      .map((p) => p.mesh as THREE.Mesh),
    false,
  );

  const nowHit = hits.length > 0 ? hits[0].object : null;

  if (nowHit !== st.hoveredMesh) {
    if (nowHit) {
      const hm = nowHit as THREE.Mesh & {
        _flash?: number;
        _flashActive?: boolean;
      };

      hm._flash = 1.0; // triggers visual feedback
      hm._flashActive = true;

      audio.playHoverBeep(); // sound triggers only for new panel interaction
    }

    st.hoveredMesh = nowHit;
  }
}

// Decay the highlight effect
mesh._flash = (mesh._flash || 0) * 0.92;  

const f = mesh._flash;

mat.envMapIntensity = 3.0 + f * 1.6; 
mat.clearcoatRoughness = Math.max(0.01, 0.05 - f * 0.035);
mat.transmission = 0.35 + f * 0.32; 
});

Raycasting ensures that the hover effects are closely aligned with the symbol’s actual form and movement. Each panel’s highlight decays autonomously, avoiding the complexity and performance costs of multiple GSAP tweens.

Weld Spark Visuals

As the page loads, three guiding lines extend from the symbol. Once this animation wraps up, hovering over any of these lines triggers a lively burst of spark animations, visually emphasizing the invitation to “Dare ⚡ to touch the lines.”

// hooks/useTrionnSymbolScene.ts

const baseLinesReadyForSpark =
  inS1 &&
  undrawAmt < 0.02 &&
  st.lineState.every((s) => s.prog >= 0.995);

if (baseLinesReadyForSpark) {
  const allLinePts = [ptsL, ptsR, ptsB];

  let hitResult: { x: number; y: number } | null = null;
  let hitLineIdx = -1;

  for (let li = 0; li < allLinePts.length; li++) {
    const h = mouseNearLine(allLinePts[li], 14);

    if (h) {
      hitResult = h;
      hitLineIdx = li;
      break;
    }
  }

  if (hitResult !== null) {
    // Trigger sparks on hover
    if (!st.sparkHoverActive && st.sparkWasAway) {
      st.sparkHoverActive = true;
      st.sparkBurstLeft = 5 + Math.floor(Math.random() * 2); 
      st.sparkWasAway = false;
    }

    if (st.weldCooldown <= 0 && st.sparkBurstLeft > 0) {
      const wp = unproj2(hitResult.x, hitResult.y); 

      const otherIdxs = [0, 1, 2].filter((i) => i !== hitLineIdx);
      const count = Math.random() > 0.5 ? 1 : 2;

      const targetIdxs = otherIdxs
        .sort(() => Math.random() - 0.5)
        .slice(0, count);

      const nearWpts = targetIdxs.map((li) => {
        const pts = allLinePts[li];

        let bestPt: LinePt | null = null;
        let bestD = Infinity;

        for (const pt of pts) {
          const dd =
            (pt.x - hitResult!.x) ** 2 +
            (pt.y - hitResult!.y) ** 2;

          if (dd < bestD) {
            bestD = dd;
            bestPt = pt;
          }
        }

        return unproj2(bestPt!.x, bestPt!.y);
      });

      triggerWeld(wp, nearWpts, !st.sparkSoundPlayed);

      st.sparkBurstLeft--;
      st.weldCooldown = 0.04 + Math.random() * 0.06; 
    }
  }
}

This spark generation routine is activated only once all guiding lines are fully drawn, with checks ensuring that new bursts occur only on fresh hovers, rather than on continuous contact. Each spark burst is unique, involving randomized numbers of visual and auditory elements, ensuring that user interactions remain engaging and never repeat exactly.

The “weld” effect is lean, rendered with THREE.Line geometries to achieve the desired aesthetic without incurring the costs associated with post-processing effects. This artistic touch is complimented by an offscreen 2D <canvas>, which serves as a texture in the Three.js environment, optimizing performance while enhancing visual fidelity.

Hold-to-Blast Interaction

When the hero symbol is clicked and held, it initiates a multi-faceted interaction: surrounding interface components begin to vibrate, signaling a build-up of energy. After approximately half a second, the symbol disassembles into its individual panels, each dispersing along distinct trajectories while an explosion visual and sound amplify the event. Releasing the click reverses this energetic display, gracefully reassembling the symbol back to its original form.

Engaging the Charge-Up

On pressing the symbol, the interaction enters its charging phase, resetting timers and activating vibration feedback prior to the resulting blast sequence.

const onMouseDown = (e: MouseEvent) => {
  // ... hit-test logic omitted ...

  st.holding = true;
  st.holdTime = 0;
  st.vibrateAmt = 1.0;
  st.vibratePhase = 0;
  st.clickBurst = 0;
  st.joinPlayed = false;
};

window.addEventListener("mousedown", onMouseDown);

Charging and Detonating Sequence

When the mouse button is pressed, the interaction unfolds through two key stages. The initial half-second focuses on a charge-up animation, laying the groundwork for what's to come. Once that preliminary duration elapses, the visual representation shatters, with each panel disengaging and culminating in an explosive sequence of motion.

if (st.holding) {
  st.holdTime += 1 / 60;
  st.vibrateAmt = 1.0;

  if (st.holdTime < 0.5) {
    st.clickBurst = 0; // still charging
  } else {
    if (st.clickBurst === 0) {
      audio.stopVibrateSound();
      audio.playExplodeSound();
      audio.startWooshSound();
    }

    st.vibrateAmt *= 0.88;
    st.clickBurst = Math.min(1.0, st.clickBurst + 0.02); // ramps from 0 → 1 over ~50 frames
  }
} else {
  // Released — both values ease back down instead of snapping to 0
  st.vibrateAmt = Math.max(0, st.vibrateAmt - 0.08);
  st.clickBurst = Math.max(0, st.clickBurst - 0.025);
}

The Role of clickBurst in the Blast Effect

The clickBurst parameter dictates how far each panel moves from its starting location. As this value transitions from 0 to 1, the panels are animated independently in their specific directions according to predefined trajectories, resulting in a highly controlled explosion effect that feels coherent yet dynamic.

const burstContrib =
  st.scrollProgress < 0.15 ? st.clickBurst : 0;

const explodeAmt = Math.max(
  st.scrollProgress,
  st.hoverAmt,
  burstContrib,
  st.introAmt,
);

particles.forEach((p) => {
  const amt = Math.max(0, explodeAmt - p.delay); // staggered by each panel's delay
  const burst = amt * 5.5;

  p.mesh.position.set(
    p.explodeDir.x * burst + 0,
    p.explodeDir.y * burst,
    p.explodeDir.z * burst,
  );

  p.mesh.rotation.x =
    p.spinAxis.x * p.spinSpeed * amt * Math.PI;
});

Reactive UI Elements During the Charge

As the charging animation unfolds, adjacent UI elements, including navigation buttons and headings, take on a slight vibration effect that corresponds with the ongoing interaction. This synchronized movement ensures a cohesive experience, with these elements smoothly returning to their original positions through CSS transitions after the interaction concludes.

vibrateEls.forEach((el) => {
  el.style.transition = "none";
  el.style.transform = `translate(${sx}px, ${sy}px)`; // sx/sy based on sine wave
});

// On release:
el.style.transition =
  "transform 0.7s cubic-bezier(0.25, 0.46, 0.45, 0.94)";
el.style.transform =
  "perspective(600px) translate(0px, 0px) rotateX(0deg)";

The half-second charge-up introduces a thoughtful delay before the explosion occurs, enhancing the sense of anticipation and intentionality in the interaction. A singular explodeAmt variable integrates the effects from various states—scrolling, hovering, and the hold-to-blast mechanism—using Math.max(). This unified approach simplifies the animation logic, allowing for smooth reversals without the need for additional animation paths whenever the button is released.

Scroll Animation in the Services Section

The Services section stands out as the most intricate scroll-driven animation on the site. Centered around a unified scrollProgressRef value that ranges from 0 to 1, it orchestrates everything—from a 371-frame WebP image animation to the fragmentation of the “OUR SERVICES” headline into animated particle glyphs. It also guides six service cards through designated motion paths, transitions the site's color scheme from dark to light, and concludes with a stripe wipe into the Testimonials section.

Utilizing a Single Scroll Driver

At the core of this sequence is a single, normalized scrollProgressRef value, again spanning from 0 to 1. Rather than crafting multiple timelines for different interactions, this single reference allows various animation states to derive individualized progress ranges. This integration keeps the entire experience synchronized and streamlines adjustments to timing for any effect.

// components/Sections/Home/TrionnServices.tsx

const TOTAL = 371;

const EXPLODE_START = 0.35;
const EXPLODE_END = 0.53;
const CARDS_START = 0.56;
const CARDS_END = 1.0;

// Inside the RAF loop:
const linear = scrollProgressRef.current; // 0 → 1, controlled by parent bridge

s.scrollT = mapServicesScrollProgress(linear, isMobile); // remapped specifically for this section

const targetFrame = s.scrollT * (TOTAL - 1);
s.videoIdx += (targetFrame - s.videoIdx) * 0.12; // ease toward the target frame

drawFrame(s.videoIdx); // refreshes the <img> source

const inZone =
  s.scrollT >= EXPLODE_START &&
  s.scrollT <= EXPLODE_END;

const explodeT = inZone
  ? (s.scrollT - EXPLODE_START) / (EXPLODE_END - EXPLODE_START)
  : 0;

if (inZone && s.gsapTL) {
  s.gsapTL.progress(explodeT);
}

updateCards(s.cardsT); // cards use their own smoothed copy of scrollT

Animating the Image Sequence

The dynamic background features a series of 371 WebP frames, which are presented by updating the src property of a standard <img> element. This method surpasses traditional video or <canvas> rendering by offering a lightweight solution while allowing direct scrubbing by scroll action.

// drawFrame updates a single <img> instead of using <canvas> or <video>
const drawFrame = useCallback((i: number) => {
  const el = imgRef.current;
  const img = stateRef.current.imgs[Math.round(i)];

  if (!img || !img.complete) return;

  // Update the DOM only when the frame changes
  if (el.src !== img.src) {
    el.src = img.src;
  }
}, []);

// Preload all 371 frames in chunks of 20 during idle time
const loadChunk = (start: number) => {
  const end = Math.min(start + CHUNK, TOTAL);

  for (let i = start; i < end; i++) {
    const img = new Image();

    img.src = `/images/stone/frame_${String(i + 1).padStart(4, "0")}.webp`;

    // decode() prevents jank on the first display of the frame
    img.decode().then(checkChunkDone, checkChunkDone);
  }
};

Exploding Headline Particles

The “OUR SERVICES” headline undergoes a transformation as it separates into individual glyphs, each animated with its own trajectory. As the scrolling reaches the designated point, these glyphs disperse, providing visual dynamism just before the service cards make their entrance.

// Positioning each character using the Range API for accurate measurement
const measureChars = useCallback(() => {
  overlay.querySelectorAll("[data-line]").forEach((line) => {
    // ...

    for (let i = 0; i < raw.length; i++) {
      range.setStart(textNode, i);
      range.setEnd(textNode, i + 1);

      const rect = range.getBoundingClientRect();

      results.push({
        ch: display[i],
        x: rect.left + rect.width / 2,
        y: rect.top + rect.height / 2,
        /* font properties */
      });
    }
  });

  return results;
}, []);

// Each measured character transitions into its own <span>, ensuring the original
// typography is preserved before each glyph is animated along its path.
m.forEach((p, i) => {
  const isHero = hi.has(i);

  const angle = rand(-Math.PI, Math.PI);
  const speed = isHero
    ? rand(0.05, 0.15) * maxDim
    : rand(0.4, 0.9) * maxDim;

  s.particles.push({
    el,
    ox: p.x,
    oy: p.y,
    dirX: Math.cos(angle),
    dirY: Math.sin(angle) * rand(-1.0, 0.18),
    speed,
    /* ... */
  });
});

Animating Service Card Appearance

As these headline particles separate, the six service cards begin to appear along planned curved trajectories. On larger screens, these cards animate into view in pairs, providing a balanced look while ensuring the scroll sequence remains clear and engaging.

// On desktop: pairs of cards begin 0.2 timeline units apart, following a curved path
const arc =
  frac <= 0.5 ? Math.sin(frac * Math.PI) : 1;

const lX = lStartX + arc * (lPeakX - lStartX);
const lY = lStartY + frac * (lEndY - lStartY); // Vertical transitions from bottom to top

frames.push({
  x: lX,
  y: lY,
  opacity: op,
});

// When a pair reaches the midpoint, initiate the SVG icon stroke animation
if (!s.svgFired.has(lk) && tlTime >= centerTime) {
  s.svgFired.add(lk);

  gsap.fromTo(
    paths,
    {
      drawSVG: "0%",
    },
    {
      drawSVG: "100%",
      duration: 1.5,
      stagger: 0.04,
    },
  );
}

Concluding with a Stripe Wipe Transition

Concluding this section is a stripe wipe effect, which is reused throughout the site for visual consistency across various areas. This uniformity aids in creating a cohesive visual experience while centralizing the implementation into a single, manageable process.

// `applyStripeHold` drives the paused stripe reveal timeline over the final
// scroll range, transitioning to the Testimonials section with a GPU-accelerated `yPercent` transform.
const holdT = Math.max(
  0,
  Math.min(1, (linear - holdStart) / (1 - holdStart)),
);

cache.tl.progress(holdT);

Different from much of the site, this section opts out of using ScrollTrigger for its animations. Instead, every animation is controlled by a single scroll progress value, recalibrated on each frame, which helps maintain unity in the experience without the hassle of coordinating multiple timelines.

To enhance the initial page load, the 371 WebP frames are preloaded in batches during idle periods, using requestIdleCallback for the process. The img.decode() method ensures that each frame is primed for display, while the motion of the service cards is handled differently: the GSAP timeline is constructed when the layout changes and subsequently scrubbed through .progress() during the scrolling interaction, which minimizes the overhead of recalculating each card's motion on every frame.

The Future of Interactive Design: Key Takeaways

As we reflect on the technical feats showcased, it’s clear that using real-time generation for both the visuals and audio profoundly enhances the user experience. The breakthrough here isn’t just about slick graphics or captivating sounds; it’s about creating a genuinely immersive interaction that feels organic and responsive. If you're navigating the world of interactive design, you’ll understand that making digital experiences feel alive is the next frontier. This approach makes user engagement not just a byproduct but an essential element of the experience. What stands out the most is the meticulous craftsmanship behind features like the procedurally generated wave animations and audio-reactive fog. Such attention to detail ensures that every interaction prompts an almost instinctive response from users. The integration of the Web Audio API exemplifies how sound can elevate interactivity, responding dynamically to user inputs—no more static, prerecorded clips. This methodology enhances the emotional connection users feel with the design, making the experience memorable and impactful. And yet, it begs a pertinent question: Can these complex techniques scale? While the initial results are promising, we must consider the implications for broader adoption in web design. The computational demands of real-time audio synthesis and intricate animations could challenge performance on less capable devices. Designers and developers need to weigh the benefits of enhanced interactivity against potential accessibility issues. The key will be finding balance—maximizing engagement without sacrificing performance. In conclusion, as we innovate and push boundaries, we must keep our users at the forefront. What this means for you is a pivotal shift in how you approach design: integrate interactivity that feels intuitive and personal, ensure that your experiences run smoothly across devices, and, importantly, stay curious. The groundwork laid by these technologies sets the stage for a future where digital experiences are not only seen but felt—giving rise to a new era of engaging, interactive design.
Source: Trionn · tympanus.net

Comments

Sign in to comment.
No comments yet. Be the first to comment.

Related Articles

The Architecture Behind Trionn: Coordinating GSAP, Three....