Case Research: Gabriel Contassot’s Portfolio — 2024

Gabriel Contassot's portfolio splash screen

Working with Gabriel on his new portfolio has been an awesome expertise. He initiated the undertaking with a minimalist but well-conceived design, incorporating animation concepts and sustaining an open-minded method. This flexibility fostered intensive experimentation all through the event course of, which, in my expertise, yields the most effective outcomes.

The core of the web site encompasses a two-page “loop,” transitioning from a predominant gallery on the homepage to an in depth undertaking view. The target was to make sure cohesive animations and supply hanging, colourful transitions when navigating from the dark-themed homepage to the brighter case research. As it is a portfolio, the first focus was on showcasing the content material successfully.

There’s a commented demo of the primary impact on the finish of this case research.

Construction / Stack

Every time doable, I desire to work with vanilla JavaScript and easy instruments, and this undertaking introduced the proper alternative to make the most of my present favourite stack. I used Astro for static web page era, Taxi to create a single-page-app-like expertise with clean web page transitions, and Gsap Tweens for animation results. Twgl gives WebGL helpers, whereas Lenis handle the scrolling.

All content material is delivered by means of Sanity, with the only exception of case research movies, that are hosted on Cloudflare and streamed utilizing Hls.

The web site is statically generated and deployed on Vercel, each by way of CI/CD and from Sanity to rebuild when the content material updates.

The CMS construction is sort of easy, only a assortment for the work, one for pages like /about, and a gaggle for generic information (which on this case is barely contact data). On this occasion the web site is fairly easy and this configuration shouldn’t be actually wanted, however contemplating the headless nature of this setup this was the easiest way to make sure the content material aspect of issues might outlive the web site, and for a subsequent model we might (or whoever will work on it) doubtlessly construct on prime.

The official integration for Astro/Sanity got here proper in the course of the undertaking, enhancing the interplay between the 2. We’re additionally leveraging the Vercel Deploy plugin, so who makes use of the CMS can freely deploy a brand new model when wanted.

The entire repo appears one thing like this:

	(fonts and recordsdata)

	(sanity setup)


		[Website Components as .astro files]

		/modules (all of the js)
		/gl (all of the webgl)
        app.js (js entrypoint)

Astro + Sanity

On this case we’re utilizing Astro at a ten% of it’s potential, simply with.astro recordsdata (no frameworks). Principally as a templating language for static website era. We’re largely leveraging the element method, that finally ends up being compiled right into a single, statically generated html doc.

For example, the homepage appears like this. On the prime, in between the --- there may be what Astro calls frontmatter, which is solely the server aspect of issues that on this case executes at construct time since we’re not in SSR mode. Right here you’ll be able to see an instance if this.

<!-- pages/Residence.astro -->

import Merchandise from "./Merchandise.astro";
import ScrollToExp from "../ScrollToExp.astro";
import ScrollUp from "../ScrollUp.astro";

import { getWork } from "../content material/sanity.js";

import Nav from "../Nav.astro";

const gadgets = await getWork(true);
const sorted = gadgets.type((a, b) => a.props.order - b.props.order);

<Nav />

    class="h-[120vh] flex items-end justify-center pb-[20vh]"
    <ScrollToExp />
  { => <Merchandise information={merchandise} />)}
    class="h-[180vh] flex items-end justify-center pb-[3vh]"
    <ScrollUp />

You’ll be able to try my starters here, the place you’ll discover each the Astro and Sanity starters that I used to spin up this undertaking.


I take advantage of a single entry level for all my javascript (app.js) at a format stage as a element, and the attention-grabbing half begins from there.

In my entry level I initialise the entire predominant parts of of the app.

<!-- parts/Canvas.js  -->

<canvas data-gl="c"></canvas>
  import "../js/app.js";
  • Pages — which is Taxi setup in a manner so it returns guarantees. This manner I can simply await web page animations and make my life a bit simpler with holding all the pieces in sync (which I find yourself by no means doing correctly and manually syncing values as a result of I get messy and the supply is arising).
  • Scroll — which is solely a small lenis wrapper. Fairly normal tbh, just a few utilities and helper features in addition to the setup code. I even have the logic to subscribe and unsubscrube features from different parts that want the scroll, so I’m positive all the pieces is all the time in sync.
  • Dom — holds all of the DOM associated code, each purposeful and animation associated.
  • Gl — that holds all of the WebGl issues, on this case fairly easy because it’s only a full display screen quad that I take advantage of to alter the background color with good and clean values
// app.js

class App {
  constructor() {

	// ...

  init() {
    this.pages = new Pages();
    this.scroll = new Scroll();
    this.dom = new Dom(); = new Gl();;


	// ...

  // ...

In right here there are my predominant (and solely) resize() and render() features.
This manner I’m positive I solely name requestAnimationFrame()as soon as render loop and have a single supply of fact for my time worth, and that listening and firing a single occasion for dealing with resize.


The animation framework depends on two major JavaScript lessons: an Observer and a Observe.

An Observer, constructed utilizing the IntersectionObserver API, triggers every time a DOM node turns into seen or hidden within the viewport. This class is designed to be versatile, permitting builders to simply lengthen it and add customized performance as wanted.

In the meantime, the Observe class builds upon the Observer. It routinely listens to scroll and resize occasions, calculating a worth between 0 and 1 that displays the on-screen place of a component. This class is configurable, permitting you to set the beginning and finish factors of the monitoring—successfully functioning as a bespoke ScrollTrigger. One in every of its key options is that it solely renders content material when the factor is in view, leveraging its foundational Observer structure to optimize efficiency.

// observe.js

export class Observe {
  constructor({ factor, config, addClass }) {
    this.factor = factor;
    this.config =  "10px",
      threshold: config?.threshold ;

    if (addClass !== undefined) this.addClass = addClass;
	// ....

// observe.js

import { clientRect } from "./clientRect.js";
import { map, clamp, lerp, scale } from "./math.js";
import { Observe } from "./observe.js";

export class Observe extends Observe {
	worth = 0;
	inview = true;

	constructor({ factor, config }) {
		tremendous({ factor, config })
		this.factor = factor;
		this.config = {
		  bounds: [0, 1],
		  prime: "backside",
		  backside: "prime",

	// ...

A sensible demonstration of how these lessons perform is clear on the case research pages.

On this setup, photos and movies seem on the display screen, activated by the Observer class. On the identical time, the scaling results utilized to photographs on the prime and backside of the web page are simple transformations pushed by a Observe on the guardian factor.

The web page transition includes a easy factor that adjustments shade based mostly on the hyperlinks clicked. This factor then wipes upwards and away, successfully signaling a change within the web page.


The preloader on our web site is extra of a stylistic function than a purposeful one—it doesn’t really monitor loading progress, primarily as a result of there isn’t a lot content material to load. We launched it as a artistic enhancement as a result of simplicity of the positioning.

Functionally, it consists of a textual content block that shows altering numbers. This textual content block is animated throughout the display screen utilizing a transformX property. The motion is managed by a setInterval perform, which triggers at progressively shorter intervals to simulate the loading course of.

// loader.js

import Tween from "gsap";

export class Loader {
  constructor({ factor }) {
    this.el = factor;
    this.quantity = this.el.youngsters[0];

  animate() {
    let depend = 0;

    const totalDuration = 2.8;
    const values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 20, 23, 30, 50, 70, 80, 100];
    const splitDuration = totalDuration / values.size;

    return new Promise((resolve) => {
      const destroy = () => {, {
          autoAlpha: 0,
          length: 0.8,
          ease: "",
          onComplete: () => {
            setTimeout(() => {
              this.el.take away();
            }, 800);

      let interval = setInterval(() => {
        if (values[count] === undefined) {
      }, splitDuration * 1000);

  step(val) {
    let v = val;
    if (v === 100 && window.mobileCheck()) {
      v = 95;
    this.quantity.textContent = val;
    this.quantity.type.rework = `translateX(${v}%)`;

Scrambled Textual content

The textual content animation function is predicated on GSAP’s ScrambleText plugin, enhanced with further utilities for higher management and stability.

Initially, we tried to recreate the performance from scratch to reduce textual content motion—given the massive dimension of the textual content—however this proved difficult. We managed to stabilize the scrambling impact considerably by reusing the unique characters of every phrase solely, minimizing variations throughout every shuffle.

We additionally refined the interactive components, similar to making certain that the hover impact doesn’t activate throughout an ongoing animation. This was significantly vital as some unintended mixtures generated inappropriate phrases in French throughout the scrambles.

For the homepage, we changed the hover-trigger with an onload activation for the menu/navigation centerpiece. We hardcoded the durations to synchronize completely with the specified timing of the visible results.

Moreover, we built-in CSS animations to handle the visibility of components, setting {merchandise}.type.animationDelay immediately in JavaScript. A Observe object was employed to dynamically alter the dimensions of components based mostly on their scroll place, enhancing the interactive visible expertise.

// nav.js

this.values = {
  length: [1.2, 1.5, 0.4, 0.2, 1, 0.6, 0.6],
  del: [0, 0.4, 1.3, 1.4, 1.5, 1.6, 2.1],
  lined: [0, 0.3, 1.1, 1.5],

// ...

animateIn() {
    this.el.classList.add("anim");, { autoAlpha: 1, length: 1 });

    this.texts.forEach((line, i) => {
      line.type.fontKerning = "none"; // probs doesnt do something, {
        length: this.values.length[i],
        delay: this.values.del[i],
        ease: "expo.out",
        scrambleText: {
          textual content: "{authentic}",
          chars: [
          revealDelay: this.values.length[i] * 0.5,
          velocity: 1,

Homepage photos impact

That is in all probability probably the most attention-grabbing piece of it, and I wanted a few tries to grasp how you can make it work, earlier than realising which are actually simply absolute positioned photos with a clip-path inset mixed with a Observe to sync it with the scroll that additionally controls the scaling of the interior picture.

// scrollImage.js

constructor() {
    // ...
    this.picture.type.rework = `scale(1)`;
    this.imagWrap.type.clipPath = "inset(100% 0 0 0)";
    // ...

 render() {
    if (!this.inView) return;

    this.picture.type.rework = `scale(${1.2 + this.observe.worth*-0.2})`;

    this.imagWrap.type.clipPath = `
      inset(${this.observe.value2 * 100}% 
        ${this.observe.value1 * 100}% 

Colour Change

It’s the one WebGl piece of this complete web site.

Initially, the idea concerned altering colours based mostly on scroll interactions, however this was ultimately moderated because of considerations about it turning into overly distracting. The implementation now includes a full-screen quad, constructed from a single triangle with remapped UV coordinates, which permits for a extra versatile and responsive visible show.

The colour values are dynamically retrieved from attributes specified within the DOM, which could be freely adjusted by way of the CMS. This setup includes changing shade values from hexadecimal to vec3 format. Moreover, a few GSAP Tweens are employed to handle the animations for transitioning the colours out and in easily.

This use of WebGL ensures that the colour transitions usually are not solely clean and visually interesting but additionally performant, avoiding the lag and choppiness that may happen with heavy CSS animations.


This can be a minimal rebuild of the primary homepage impact. Aside from some CSS to make it features, 90% of it occurs within the observe.js file, whereas all the pieces is initialised from predominant.js.

The Observe class is used as the bottom to create the ImageTransform one, which extends the performance and transforms the picture.

There’s just a few helper features to calculate the bounds on resize and to try to maximise efficiency it’s solely referred to as by lenis when a scroll is going on. Ideally must be wrapped by an Observer so it solely calculates when is in view, however I saved it less complicated for the demo.


It’s a easy web site, however was a enjoyable and attention-grabbing problem for us nonetheless.
Hit me up on Twitter when you have any questions or need to know extra!


Leave a Reply

Your email address will not be published. Required fields are marked *