Trendveris
Live Coverage
Sign in Sign up
Trending: Champions League Transfer News Premier League World Cup
Trendveris
AI & ML

Leveraging GSAP for Dynamic Portfolio Development

Thibault Guignand explores the technical integration of GSAP in building a dynamic portfolio, showcasing the application of shader uniforms and clip-path wipes for enhanced visual effects.

May 06, 2026 | 3 min read
Sign in to save

From Shader Uniforms to Clip-Path Wipes: How GSAP Drives My Portfolio

This article explores the meticulous construction of my portfolio, showcasing how GSAP acts as the maestro that brings various elements—from shader uniforms to dynamic text reveals—together seamlessly.

I’m Thibault Guignand, and I balance my work between freelancing and a full-time role. This dual approach enriches my experience, exposing me to a diverse array of projects—from corporate clients to bold, artistic endeavors. Ultimately, I aim to devote myself entirely to the realm of creativity.

This portfolio overhaul served as both an experimental platform and a self-assessment of my standing within the creative web ecosystem. My earlier portfolio hinted at the design choices I wanted to amplify, and I set out to refine this vision, aligning it with the considerable experience I've garnered.

Daily inspiration emerges from a mix of artists I admire—like Aristide Benoist, Cathy Dole, and Corentin Bernabou—and prominent creative sites such as Awwwards. I don't replicate their work; instead, I gauge my progress against their high standards.

The entire process spanned several weeks. While the primary structure came together quickly, the real challenge lay in ensuring that all formats—desktop, tablet, and mobile—functioned flawlessly.

I initiated the WebGL component using Three.js, but midway through, I migrated to OGL. This trade-off proved beneficial, resulting in a lighter bundle and a more streamlined API that allowed me to own the codebase completely.

Tech Stack & Tools

The choice of technologies was deliberate; opting for widely-used tools enables me to demonstrate proficiency in the foundational aspects that production studios rely on.

Vite + React 18 + TypeScript. This setup is my go-to for speed and type safety. It ensures clarity for anyone who reads my code.

GSAP. I've been a fan since the beginning, and with its recent transition to a fully free model, it's an obvious choice. Its features, including SplitText, ScrollTrigger, and the timeline API, are unparalleled for creating dynamic motion in web projects.

OGL. The switch from Three.js stems from a desire for a more lightweight and controllable WebGL environment.

Lenis. The native scroll behavior is too erratic for precise ScrollTrigger animations, so Lenis offers smooth scrolling and a consistent reference point that I can sync with GSAP's ticker.

SCSS + BEM. This is both a habit and a personal preference. When crafting shaders and custom layouts, having a systematic naming convention is crucial for maintaining order in my code.

i18n (FR/EN). With international juries assessing creative work on awards platforms, being bilingual ensures my website reaches the widest audience possible.

Design tooling. I laid out the grid structure in Figma to control proportions and rhythm, integrating it directly into the production CSS. Press Cmd/Ctrl + G anywhere on the site to see the grid alignment, ensuring that content fits precisely. The rest—typography, animations, transitions—was crafted directly in code, minimizing handoffs and enhancing the feedback loop.

Feature Breakdowns

Video Carousel Transition

The homepage features a full-screen video carousel. Hover over any project to see the current video melt into the next through a block-reveal transition, enhanced by a noise distortion effect and chromatic aberration.

How it works. A full-screen triangle in normalized device coordinates runs the fragment shader, executing three parallel operations:

  1. Block reveal mask: The UV coordinates are pixelated and sampled against a static noise texture, creating a binary mask that expands block by block based on the progress uniform. This eliminates any linear wipe effect, as pixels switch abruptly instead of gradually.
  2. Displacement: A separate noise sample, animated over time, warps the UV coordinates in two dimensions, peaking in intensity at 50% progress before returning to normal.
  3. Chromatic aberration: Both textures are sampled with opposing offsets for the red and blue channels, following the same easing curve.
float dt = parabola(progress, 2.);

// Block reveal: static noise pixelated, compared to progress
vec2 blockUv = floor(vUv * uNoisePixelSize) / uNoisePixelSize;
float noiseVal = texture2D(displacement, blockUv).g;
float intpl = step(noiseVal, progress);

// Warp
vec2 displaceDir = (noise.rg - 0.5) * 2.0;
vec2 warpedUv = uv + displaceDir * dt * uDisplaceIntensity;

// Chromatic aberration
float shift = dt * uRGBShift;
t1.r = texture2D(texture1, warpedUv + vec2(shift, 0.0)).r;
t1.g = texture2D(texture1, warpedUv).g;
t1.b = texture2D(texture1, warpedUv - vec2(shift, 0.0)).b;

// same for t2…
gl_FragColor = mix(t1, t2, intpl);

GSAP × WebGL: bridging with a single uniform. The pinpoint for these animations is a single progress value, ranging from 0 to 1, managed by a GSAP timeline. Each animation frame, this value updates in the shader, demonstrating how GSAP effectively controls motion without needing to modify the GLSL codebase. This method is universally applied across all effects in my project.

Minimal per-frame upload. Video textures require re-uploading every frame since the browser delivers new pixels. I limit updates only to the two textures currently transitioning (source + destination), streamlining the process. Outside transitions, the carousel defaults to native <video> playback without additional GPU overhead.

Flowmap Text Distortion

How it works. The Flowmap utility from OGL tracks the mouse speed and stocks it in an off-screen RG texture for each frame. The shader samples this texture to distort the text's UV coordinates and applies a directional form of chromatic aberration, which shifts the RGB channels based on the vector from the cursor to the pixel.

// Directional chromatic aberration: not centered, guided by cursor
vec2 toMouse = vUv - uMouse;

float influence =
    smoothstep(uRadius, 0.0, length(toMouse)) * uVelo;

vec2 offset =
    normalize(toMouse) * influence * uChromaticIntensity;

// RGB split sampling
float r = texture2D(tWater, baseUV - offset * 1.5).r;
float g = texture2D(tWater, baseUV + offset * 0.5).g;
float b = texture2D(tWater, baseUV + offset * 1.8).b;

// Rainbow kick when velocity is high: sin() with 120° phase offsets
if (uVelo > 0.01) {
    float hueShift =
        uTime * 0.01 + length(toMouse) * 2.0;

    r = mix(
        r,
        sin(hueShift) * 0.5 + 0.5,
        uVelo * uColorShift
    );

    g = mix(
        g,
        sin(hueShift + 2.094) * 0.5 + 0.5,
        uVelo * uColorShift
    );

    b = mix(
        b,
        sin(hueShift + 4.188) * 0.5 + 0.5,
        uVelo * uColorShift
    );
}

The rainbow effect relies on a classic method using three sin() functions, offset by 2π/3 radians. It activates only when the cursor is moving quickly, maintaining visibility during rapid movement while remaining subdued during steady pauses.

One-time mount, texture swapping. This optimization is one of my proudest achievements. My initial version created a new WebGL context for each project title, which was clear in React but disastrous for performance. GPU memory usage spiked, and the rainbow effect became choppy by the fourth hover. The revised approach retains a single FlowmapEffect mounted at the HomePage level, allowing texture swapping without creating new contexts. Coupled with an idle guard that halts the rendering loop after 90 frames of inactivity, this method significantly cuts resource costs during idle moments.

Next-Project Scroll Morph

Scrolling to the bottom of each project page unveils the "Next Project" preview, which expands and reveals a clipped background, while an SVG circle tracks a progression from 0% to 100%. When the circle hits 100%, the page navigates automatically; scrolling back up cancels the navigation.

How it works. A single ScrollTrigger with scrub: 1 orchestrates the entire animation, updating four DOM values each frame through its onUpdate callback—bypassing React's state management to maximize performance.

onUpdate: (self) => {
  const progress = self.progress;
  const percent = Math.round(progress * 100);

  // Counter
  numberEl.textContent = String(percent >= 99 ? 100 : percent);

  // Background morph: scale + inset clip-path
  const bgScale = 1.3 - 0.3 * progress;

  const insetV = Math.max(0, 20 - 20 * progress);
  const insetH = Math.max(0, 40 - 40 * progress);

  bgEl.style.transform = `scale(${bgScale})`;
  bgEl.style.clipPath = `inset(${insetV}% ${insetH}% ${insetV}% ${insetH}%)`;

  // SVG progress circle
  circleEl.style.strokeDashoffset =
    String(CIRCUMFERENCE - progress * CIRCUMFERENCE);

  // Auto-navigation check
  if (percent >= 100 && state === "idle" && hasSeenLowProgress) {
    // trigger page change
  }
};

This direct manipulation of element.style.* avoids the overhead of a full React render for each frame. On a 120 Hz display, this distinction translates to fluid motion versus stuttering.

A state machine to handle scroll unpredictability. The automatic navigation isn't simply based on reaching 100%—users might flick scroll through this section, land on a fragment at 100%, and then change their minds. I implemented a three-state machine (idle → triggered → navigating), complete with guard clauses like hasSeenLowProgress to prevent accidental triggers.

Identical animations, dual triggers. Interestingly, the section doubles as a clickable area. Clicking activates a GSAP tween that transitions from the existing scroll progress to 1 while scrolling the page simultaneously to match the animated states. This maintains consistency in DOM manipulation, yielding the same visual effect from two different triggers.

Page Transitions (GSAP + View Transitions)

Exiting the homepage isn’t jarring; elements such as the WebGL backdrop, grid overlay, side texts, and custom cursor fade out together. After a brief pause, the content layer fades out before the browser's View Transition API takes over for the final effect.

Preload speeds up the fadeout. Upon clicking a link, two processes initiate simultaneously: the visual fadeout alongside data fetching. The dynamic import() of the next route’s JavaScript chunk, coupled with preloading the hero image, commence before GSAP executes any frames. By the time the timeline completes (approximately 0.6 seconds), both tasks typically finalize.

// Begin preloads immediately: they race GSAP fadeout
const chunkReady = chunkPreloaders[routeChunk]().catch(() => {});
const imageReady = project?.heroImage
  ? preloadImage(project.heroImage)
  : Promise.resolve();

// Sequential fadeout, parallel with network operations
const tl = gsap.timeline();

tl.to(
  [webglBg, gridOverlay, sideTexts, customCursor],
  { opacity: 0, duration: 0.3, ease: "power2.inOut" },
  0
).to(
  contentEl,
  { opacity: 0, duration: 0.35, ease: "power2.inOut" },
  0.25
);

await tl.then();
await Promise.all([chunkReady, imageReady]);

await startPageTransition(() => {
  flushSync(() => {
    navigate(path);
  });
});

flushSync is the key to the View Transition functionality. The method document.startViewTransition allows for capturing the DOM state before making changes. React Router’s navigate() function runs asynchronously, which can interfere with the View Transition process if not handled properly. Wrapping it in flushSync ensures that React renders the new route synchronously within the transition callback, resolving a common bug that can disrupt the experience.

Text Reveal: A Unified Technique

Throughout the site, a consistent text reveal strategy is employed: using GSAP’s SplitText for character or line organization, coupled with a scramble effect to bring characters into view, layered with a clip-path wipe. Centralizing this logic into a single utility allows any adjustments to the animation curve to ripple through the entire site.

Concurrent effects instead of sequential. When a line scrambles from random characters to its final form, the left edge resolves first. Implementing a clip-path reveal from left to right at the same speed maintains visual coherence, so users only see the part of the text that is already readable.

gsap.to(lineEl, {
  duration,
  ease: "none",
  scrambleText: {
    text: lineText,
    chars: SCRAMBLE_CHARS,
    revealDelay,
    speed,
  },
  onStart: () => {
    // The wipe runs concurrently with scrambling
    gsap.to(lineEl, {
      clipPath: "inset(0 0% 0 0)",
      duration: 0.6,
      ease: "power2.out",
    });
  },
});

This strategy ensures a smooth transition without layout disruption during the reveal.

Final Thoughts: Lessons Learned and Future Directions

Reflecting on this project, it’s clear that the effort wasn’t merely about assembling a visually impressive portfolio; it was an exploration of the intersection between technology and design. The intent was to elevate user experience while pushing the boundaries of technical capabilities. If you’re engaged in web development or design, you’ll appreciate that the pursuit of excellence is never a straight path, and every choice counts. Throughout the process, the decision to integrate features like the persistent flowmap and GSAP-driven WebGL effects served not only to enhance aesthetics but also to streamline performance. This technical mindfulness is what sets apart effective web experiences from the chaotic norm. Those who work with WebGL may resonate with my experience of transforming an initially messy implementation into a coherent, polished feature. It demonstrated to me that what begins as a technical challenge can morph into a key design motif when executed thoughtfully. Adopting reduced motion as a parallel design option instead of merely a fallback was a significant realization. This approach underscores the importance of inclusivity in design. Accessibility shouldn’t be an afterthought—if you’re crafting an interactive site, accommodating users with different needs should be integral from the outset. This is a critical takeaway for anyone creating digital experiences today. However, not every aspect of the journey was smooth. I encountered frustrating bugs, particularly with Safari’s handling of clip-path during view transitions. This kind of nuanced issue often falls outside standard documentation, revealing a knowledge gap that developers must navigate through experimentation and creativity. Teasing out specific quirks in browser behavior can feel like searching for needles in a haystack, and yet, these moments lead to valuable insights that foster deeper understanding. In hindsight, if I could change aspects of my approach, I would have embraced OGL sooner, skipping over more cumbersome frameworks like three.js. Sometimes, the most effective way forward is to simplify rather than complicate your tech stack. Similarly, reaching for the right tools like Sanity at the project's inception could have streamlined many of the friction points faced along the way. For projects moving forward, don’t hesitate to integrate tools that address specific needs early on to mitigate future headaches. Ultimately, I pushed myself to create something meaningful, and regardless of the outcome, I emerged with a wealth of knowledge. Moving ahead, the focus shifts to refining these insights into future endeavors, aiming for even better results every time. If you're in the tech space, keep that spirit of learning alive—it's the only way we collectively rise to meet the evolving demands of our audience and our craft.
Source: Thibault Guignand · tympanus.net
Sign in to join the discussion.