• About Us
  • Privacy Policy
  • Disclaimer
  • Contact Us
AimactGrow
  • Home
  • Technology
  • AI
  • SEO
  • Coding
  • Gaming
  • Cybersecurity
  • Digital marketing
No Result
View All Result
  • Home
  • Technology
  • AI
  • SEO
  • Coding
  • Gaming
  • Cybersecurity
  • Digital marketing
No Result
View All Result
AimactGrow
No Result
View All Result

Letting the Artistic Course of Form a WebGL Portfolio

Admin by Admin
November 28, 2025
Home Coding
Share on FacebookShare on Twitter


This case research walks by means of your complete artistic course of I went by means of whereas constructing my web site.

It’s organized into 5 chronological steps, every representing a key stage within the challenge’s growth and the selections that formed the ultimate end result. You’ll see how what I initially thought can be the centerpiece nearly grew to become optionally available, whereas a number of options disappeared solely as a result of they didn’t match the visible course that emerged.

Essentially the most thrilling a part of this course of was watching issues reveal themselves organically, guiding the artistic journey somewhat than dictating it from the beginning. I needed to keep affected person and attentive to grasp what ambiance was taking form in entrance of me.

I recorded movies alongside the best way to remind myself, on tough days, the place I got here from and to understand the evolution. I wish to be clear and share these captures with you, though they’re fully work-in-progress and much from polished. I discover it extra attention-grabbing to see how issues evolve.

At first, I needed so as to add a WebGL fold impact as a result of it was one thing I’d tried to grasp throughout my first mission 5 years in the past with Inconceivable Bureau however couldn’t handle effectively. I remembered there was already a case research by Davide Perozzi on Codrops who did an ideal walkthrough to clarify the impact, so based mostly on that, I replicated the impact and added the power to fold alongside each axes.

To make the fold occur in any course, we now have to use vector projection to redistribute the curl impact alongside an arbitrary course:

The curlPlane perform transforms a linear place right into a curved place utilizing round arc arithmetic. By projecting every vertex place onto the specified fold course utilizing the dot product, making use of the curl perform to that one-dimensional worth, after which redistributing the end result again alongside the course vector, the impact gained full directional freedom.

To make the fold impact extra sensible, I added a refined pretend shadow based mostly on the curvature quantity. The concept is straightforward: the extra the floor curls, the darker it turns into.

This straightforward approach provides a refined sense of depth with out requiring advanced lighting calculations, making the fold really feel extra three-dimensional and bodily grounded.

That is how I believed I’d have my first major function within the portfolio, however at this level, I didn’t know I used to be flawed and the journey was simply getting began…

Whereas taking part in with this new fold impact for various layouts and animations on my homepage, I additionally constructed a diffraction impact on textual content (which finally didn’t make it into the ultimate portfolio, I’ll write a separate tutorial about it). As I experimented with these two results, I out of the blue needed to make a display screen seem within the middle. And I nonetheless don’t know precisely why, however I additionally needed a personality inside.

For a very long time, I’d needed to play with 3D characters, bones, and animations. That is how my 3D character first appeared within the journey. Initially, I needed to make him discuss, so I added subtitles and totally different actions. The character and animations got here from Mixamo.

For embedding the 3D scene inside a bounded space, I used the MeshPortal approach. As an alternative of rendering to the total canvas, I created a separate scene that renders to a render goal (FBO), then displayed that texture on a aircraft mesh with a customized masks shader.

The portal shader makes use of a uMask uniform (a vec4 representing left, proper, backside, high bounds) to clip the rendered texture, creating that exact “display screen inside a display screen” impact:

Whereas taking part in with this setup, I noticed it could be attention-grabbing to animate the display screen between part transitions, altering its placement, measurement, and the 3D scene (digital camera place, character actions, dice measurement…).
However this required fixing three linked challenges:

Let me stroll you thru how I constructed each bit.

The Problem: The portal wanted to adapt to totally different positions and sizes on every web page whereas staying responsive.

The Resolution: I created a system that tracks DOM aspect bounds and converts them to WebGL coordinates. In every part, I positioned a reference

that defines the place the portal needs to be:

Then I created a hook to trace this div’s bounds and normalize them to viewport coordinates (0-1 vary):

// Normalize DOM bounds to viewport coordinates (0-1 vary)
export perform normalizeBounds(bounds, dimensions) {
  return {
    x: bounds.x / dimensions.width,
    y: bounds.y / dimensions.top,
    width: bounds.width / dimensions.width,
    top: bounds.top / dimensions.top,
  };
}

// Observe the reference div and register its bounds for every part
useResizeObserver(() => {
  const dimensions = { width: window.innerWidth, top: window.innerHeight };
  const bounds = redSquareRef.present.getBoundingClientRect();
  const normalizedBounds = normalizeBounds(bounds, dimensions);

  setRedSquareSize(sectionName, {
    x: normalizedBounds.x,
    y: normalizedBounds.y,
    width: normalizedBounds.width,
    top: normalizedBounds.top,
  });
});

These normalized bounds are then transformed to match the shader’s vec4 uMask format (left, proper, backside, high):

perform calculateMaskValues(measurement) {
  return {
    x: measurement.x,                          // left
    y: measurement.width + measurement.x,             // proper
    z: 1 - (measurement.top + measurement.y),      // backside
    w: 1 - measurement.y,                      // high
  };
}

B. Hash-Primarily based Part Navigation with Clean Transitions

Now that I might monitor portal positions, I wanted a technique to navigate between sections easily.

Since my portfolio has just a few major areas (House, Tasks, About, Contact), I made a decision to construction them as sections somewhat than separate pages, utilizing hash-based routing to navigate between them.

Why hash-based navigation?

  • Retains your complete expertise in a single web page load
  • Permits easy crossfade transitions between sections
  • Maintains browser historical past for again/ahead navigation
  • Stays accessible with correct URL states

The Setup: Every part has a singular id attribute that corresponds to its hash route:

...
...
...
...

The useSectionTransition hook handles this routing whereas orchestrating simultaneous in/out animations:

export perform useSectionTransition({ onEnter, onExiting, information } = {}) {
  const ref = useRef(null);
  const { currentSection, isLoaded } = useStore();
  const prevSectionRef = useRef(currentSection);
  const hasEnteredOnce = useRef(false);

  useEffect(() => {
    if (!isLoaded) return;
    const sectionId = ref.present?.id;
    if (!sectionId) return;

    const isCurrent = currentSection === sectionId;
    const wasCurrent = prevSectionRef.present === sectionId;

    // Transition in
    if (isCurrent && !wasCurrent && hasEnteredOnce.present) {
      onEnter?.({ from: prevSectionRef.present, to: currentSection, information });
      ref.present.fashion.pointerEvents = "auto";
    }

    // Transition out (simultaneous, non-blocking)
    if (!isCurrent && wasCurrent) {
      onExiting?.({ from: sectionId, to: currentSection, performed: () => {}, information });
      ref.present.fashion.pointerEvents = "none";
    }

    prevSectionRef.present = currentSection;
  }, [currentSection, isLoaded]);

  return ref;
}

The way it works: Once you navigate from part A to B, part A’s onExiting callback fires instantly whereas part B’s onEnter fires on the similar time, creating easy crossfaded transitions.

The hash modifications are pushed to the browser historical past, so the again/ahead buttons work as anticipated, preserving the navigation accessible and according to customary net conduct.

C. Bringing It All Collectively: Animating the Portal

With each the bounds monitoring system and part navigation in place, animating the portal between sections grew to become easy:

updatePortal = (measurement, period = 1, ease = "power3.out", delay = 0) => {
  const maskValues = calculateMaskValues(measurement);
  gsap.to(portalMeshRef.present.materials.uniforms.uMask.worth, {
    ...maskValues,
    period,
    ease,
    delay,
  });
};

// In my part transition callbacks:
onEnter: () => {
  updatePortal(redSquareSize.about, 1.2, "expo.inOut");
}

This technique permits the portal to seamlessly transition between totally different sizes and positions on every part whereas staying completely aware of window resizing.

The Consequence

As soon as I had this method in place, it grew to become simple to experiment with transitions and discover the best layouts for every web page. By means of testing, I eliminated the subtitles and saved a void ambiance with simply the character alone in an enormous area, animating just a few parts to make it really feel stranger and extra contemplative. I might even experiment with a second portal for break up display screen results (which you’ll see didn’t keep…)

3. Let’s Make Issues Dance

Whereas testing totally different character actions, I lastly determined to make the character dance all through the navigation. This gave me the concept to create dynamic movement results to accompany this dance.

For the Tasks web page, I needed one thing easy that highlights the purchasers I’ve labored with and provides an natural scroll impact. By rendering challenge titles as WebGL textures, I experimented with a number of parameters and shortly created this easy but dynamic stretch impact that responds to scroll velocity.

Velocity-Primarily based Stretch Shader

The vertex shader creates a sine-wave distortion based mostly on scroll velocity:

uniform vec2 uViewportSizes;
uniform float uVelocity;
uniform float uScaleY;

void major() {
  vec3 newPosition = place;
  newPosition.x *= uScaleX;
  vec4 finalPosition = modelViewMatrix * vec4(newPosition, 1.0);

  // Calculate stretch based mostly on place in viewport
  float ampStretch = 0.009 * uScaleY;
  float M_PI = 3.1415926535897932;

  vProgressVisible = sin(finalPosition.y / uViewportSizes.y * M_PI + M_PI / 2.0)
                     * abs(uVelocity * ampStretch);

  // Apply vertical stretch
  finalPosition.y *= 1.0 + vProgressVisible;

  gl_Position = projectionMatrix * finalPosition;
}

The sine wave creates easy distortion the place:

  • Textual content in the midst of the display screen stretches most
  • Textual content at high/backside stretches much less
  • Impact depth scales with uVelocity

The rate naturally decays after scrolling stops, making a easy ease-out impact that feels natural.

Including Depth with Velocity-Pushed Strains

To enrich the textual content stretch and improve the sense of depth within the dice containing the character, I added animated traces to the fragment shader that additionally reply to scroll velocity. These traces create a parallax-like impact that reinforces the sensation of depth as you scroll.

The shader creates infinite repeating traces utilizing a modulo operation on normalized depth:

void major() {
    float normalizedDepth = clamp((vPosition.z - minDepth) / (maxDepth - minDepth), 0.0, 1.0);
    
    vec3 baseColor = combine(backColor, frontColor, normalizedDepth);

    // Create repeating sample with scroll-driven offset
    float adjustedDepth = normalizedDepth + lineOffset;
    float repeatingPattern = mod(adjustedDepth, lineSpacing);
    float normalizedPattern = repeatingPattern / lineSpacing;

    // Generate line with uneven smoothing for directionality
    float lineIntensity = asymmetricLine(
        normalizedPattern, 
        0.2, 
        lineWidth * lineSpread * 0.2, 
        lineEdgeSmoothingBack * 0.2, 
        lineEdgeSmoothingFront * 0.2
    ) * 0.4;

    vec3 amplifiedLineColor = lineColor * 3.0;
    vec3 finalColor = combine(baseColor, amplifiedLineColor, clamp(lineIntensity, 0.0, 1.0));

    ...
}

The magic occurs on the JavaScript facet, the place I animate the `lineOffset` uniform based mostly on scroll place and modify the blur based mostly on velocity:

const updateLinesOnScroll = (scroll, velocity) => {
  // Animate line offset with scroll
  lineOffsetRef.present.worth = (scroll * scrollConfig.factorOffsetLines) % lineSpacingRef.present.worth;

  // Add movement blur based mostly on velocity
  lineEdgeSmoothingBackRef.present.worth = 0.2 + Math.abs(velocity) * 0.2;
};

The way it works:

  • The `lineOffset` uniform strikes in sync with scroll place, making traces seem to movement by means of the dice
  • The `lineEdgeSmoothingBack` will increase with scroll velocity, creating movement blur on quick scrolls
  • The modulo operation creates infinite repeating traces with out efficiency overhead
  • Uneven smoothing (totally different blur on entrance/again edges) offers the traces directionality

4. Assume Exterior the Sq.

At this level, I had my portal system, my character, and the infinite scroll working effectively. However I struggled to seek out one thing authentic and shocking for the contact and about pages. Merely altering the display screen portal’s place felt too boring, I needed to seek out one thing new. For a number of days, I attempted to suppose “outdoors the dice.”

That’s when it hit me: the display screen is only a aircraft. Let’s play with it as a aircraft, not as a display screen.

Morphing the Airplane into Textual content

This concept led me to the impact for the about web page: reworking the aircraft into massive letter shapes that act as a masks.

Within the video above, you possibly can see black traces representing the 2 SVG paths used for the morph impact: the beginning rectangle and the goal textual content masks. Right here’s how this impact is constructed:

The Idea

The approach makes use of GSAP’s MorphSVG to transition between two SVG paths:

  1. Beginning path (rectPath): A easy rectangle with a 1px stroke define, with intermediate factors alongside every edge for easy morphing
  2. Goal path (rectWithText): A crammed rectangle with the textual content lower out as a “gap” utilizing SVG’s fill-rule: evenodd

Each paths are mechanically sized to match the textual content dimensions, guaranteeing a seamless morph.

Changing Textual content to SVG and Producing the Beginning Path

I created a customized hook utilizing the text-to-svg library to generate the textual content as an SVG path, together with an oblong border path:

const { svgElement, regenerateSvg } = useTextToSvg(
  "WHO",
  {
    fontPath: "/fonts/Anton-Common.ttf",
    addRect: true,                    // Generate the rectangle border path
  },
  titleRef
);

The hook mechanically:

  • Converts the textual content into an SVG path
  • Matches the DOM aspect’s computed types (fontSize, lineHeight) for pixel-perfect alignment

Creating the Goal Path: Rectangle with Textual content Gap

After the hook generates the essential paths, I create the goal path by combining the rectangle define with the textual content path utilizing SVG’s fill-rule: evenodd:

// Get the textual content path from the generated SVG
const textPath = svgRef.present.querySelector("#textual content");
const textD = textPath.getAttribute("d");

// Get dimensions from the textual content bounding field
const bbox = textPath.getBBox();

// Mix: outer rectangle + interior textual content (which creates a gap)
const combinedPath = [
  `M ${bbox.x} ${bbox.y}`,       // Move to top-left
  `h ${bbox.width}`,              // Horizontal line to top-right
  `v ${bbox.height}`,             // Vertical line to bottom-right
  `h -${bbox.width}`,             // Horizontal line to bottom-left
  'Z',                            // Close rectangle path
  textD                           // Add text path (creates the hole)
].be part of(' ');

rectWithTextRef.present = doc.createElementNS("http://www.w3.org/2000/svg", "path");
rectWithTextRef.present.setAttribute('d', combinedPath);
rectWithTextRef.present.setAttribute('fill', '#000');
rectWithTextRef.present.setAttribute('fill-rule', 'evenodd');  // Important for creating the opening

The fill-rule: evenodd is the important thing right here, it treats overlapping paths as holes. When the rectangle path and textual content path overlap, the textual content space turns into clear, creating that “cut-out” impact.

Why GSAP’s MorphSVG Plugin?

Morphing between a easy rectangle and complicated textual content shapes is notoriously tough. The paths have fully totally different level counts and constructions. GSAP’s MorphSVG plugin handles this intelligently by:

  1. Analyzing each paths and discovering optimum level correspondences
  2. Utilizing the intermediate factors to create easy transitions
  3. Utilizing kind: "rotational" to create a pure, spiraling morph animation

The Efficiency Problem
As soon as the morph was working, I hit a efficiency wall. Morphing massive SVG paths with many factors precipitated seen body drops, particularly on lower-end units. The SVG morph was easy in idea, however rendering advanced textual content paths by continuously updating SVG DOM parts was costly. I wanted 60fps, not 30fps with stutters.

The Resolution: Canvas Rendering
GSAP’s MorphSVG has a robust however lesser-known function: the render callback. As an alternative of updating the SVG DOM on each body, I might render the morphing path on to an HTML5 canvas:

gsap.to(rectPathRef.present, {
  morphSVG: {
    form: rectWithTextRef.present,
    render: draw,                    // Customized canvas renderer
    updateTarget: false,             // Do not replace the SVG DOM
    kind: "rotational"
  },
  period: about.to.period,
  ease: about.to.ease
});

// Canvas rendering perform known as on each body
perform draw(rawPath, goal) {
  // Clear canvas
  ctx.save();
  ctx.setTransform(1, 0, 0, 1, 0, 0);
  ctx.clearRect(0, 0, canvas.width, canvas.top);
  ctx.restore();

  // Draw the morphing path
  ctx.fillStyle = "#000";
  ctx.beginPath();

  for (let j = 0; j < rawPath.size; j++) {
    const section = rawPath[j];
    ctx.moveTo(section[0], section[1]);

    // Draw bezier curves from the morphing path information
    for (let i = 2; i < section.size; i += 6) {
      ctx.bezierCurveTo(
        section[i], section[i + 1],
        section[i + 2], section[i + 3],
        section[i + 4], section[i + 5]
      );
    }

    if (section.closed) ctx.closePath();
  }

  ctx.fill("evenodd");  // Use evenodd fill rule on canvas too
}

The draw callback receives the interpolated path information at every body and renders it onto canvas utilizing bezier curves. This strategy:

  • Bypasses costly SVG DOM manipulation
  • Leverages canvas’s hardware-accelerated rendering
  • Maintains 60fps even with 100+ level paths

Key Lesson: The render callback in MorphSVG is extremely highly effective for optimization. Canvas rendering gave me easy efficiency with out sacrificing the attractive morph impact.

Don’t hesitate to examine MorphSVG’s superior choices within the documentation there are numerous helpful ideas and tips.

Timeline with Video Illustrations

The impact seemed good, however one thing was nonetheless lacking, it didn’t spotlight the web page’s content material and the textual content on the About web page was too simple to skip. It didn’t make you wish to learn

I went on vacation, and once I got here again, I had a ‘little’ revelation. In my life earlier than coding, I used to be a theater director, and earlier than that, I labored in cinema. Since I began coding 5 years in the past, I’ve at all times introduced myself as a developer with a background in theater and cinema. But when I’m actually sincere with myself, I really love working in all three fields, and I’m satisfied many bridges may be constructed between these three arts.

So I instructed myself: I’m all three. That is how the about web page must be constructed.

I created a timeline divided into three elements: Cinema, Theater, and Code. Once you hover over a step, a corresponding video seems contained in the letters, like silhouettes in a miniature shadow puppet theater.

I additionally positioned the digital camera so the characters seem as silhouettes + shifting slowly contained in the “who” letters.

5. Closing the Display / Closing the Loop

Lastly, for the contact web page, I utilized the identical “suppose outdoors the sq.” precept. I needed to shut the display screen and rework it into letters spelling “MEET ME.”

The important thing was syncing the portal masks measurement with DOM parts utilizing normalized bounds:

// Normalize DOM bounds to viewport coordinates (0-1 vary)
export perform normalizeBounds(bounds, dimensions) {
  return {
    x: bounds.x / dimensions.width,
    y: bounds.y / dimensions.top,
    width: bounds.width / dimensions.width,
    top: bounds.top / dimensions.top,
  };
}

// Observe the "MEET ME" textual content measurement and sync it with the portal
const letsMeetRef = useResizeObserver(() => {
  const dimensions = { width: window.innerWidth, top: window.innerHeight };
  const bounds = letsMeetRef.present.getBoundingClientRect();
  const normalizedBounds = normalizeBounds(bounds, dimensions);

  setRedSquareSize("contact", {
    x: normalizedBounds.x,
    y: normalizedBounds.y,
    width: normalizedBounds.width,
    top: normalizedBounds.top,
  });
});

By giving the portal the precise normalized measurement of my DOM letters, I might orchestrate an ideal phantasm with cautious timing. The true magic occurred with GSAP’s sequencing and easing:

const animToContact = async (from) => {
  // First: zoom digital camera dramatically
  updateCameraFov(otherCameraRef, 150, contact.present.period * 0.5, "power3.in");

  // Then: develop portal to fullscreen
  await gsap.to(portalMeshRef.present.materials.uniforms.uMask.worth, {
    x: 0, y: 1, z: 0, w: 1,  // fullscreen
    period: contact.present.period * 0.6,
    ease: "power2.out",
  });

  // Lastly: morph into letter shapes with bouncy ease
  updatePortal(redSquareSize.contact, contact.present.period * 0.4, "again.out(1.2)");

  // Disguise portal masks to disclose DOM textual content beneath
  gsap.set(portalMeshRef.present.materials.uniforms.uMask.worth, {
    x: 0.5, y: 0.5, z: 0.5, w: 0.5,  // collapsed
    delay: contact.present.period * 0.4,
  });
};

The again.out(1.2) easing from GSAP was essential, it creates that satisfying bounce that makes the letters really feel like they’re popping into place organically, somewhat than simply showing mechanically.
If you happen to haven’t but, check out GSAP’s easing web page, it’s a useful device for locating the right movement curve.
Taking part in with the digital camera’s FOV and place additionally helped construct an actual sense of area and depth.

And with that, the loop was full. My preliminary sticker (the one which began all of it) had lengthy been put aside. Despite the fact that it not had a practical objective, I felt it deserved a spot within the portfolio. So I positioned it on the Contact web page, as a small, private wink. 😉

The humorous half is that what took me probably the most time on this web page wasn’t the code, however discovering the best movies, ones that weren’t too literal, but remained coherent and evocative. They add a way of elsewhere, a quiet invitation to flee into one other world.
Similar to earlier than, the true breakthrough got here from working with what already existed, somewhat than including extra.
And above all, from utilizing visuals to inform a narrative, to disclose in movement who I'm.

Conclusion

At that second, I felt the portfolio had discovered its kind.

I’m very pleased with what it grew to become, even with its small imperfections. The about web page could possibly be higher designed, however this website appears like me, and I really like returning to it.

I hope this case research offers you motivation to create new private types. It’s a really distinctive pleasure as soon as the shape reveals itself.

Don’t hesitate to contact me when you have technical questions. I'll come again with smaller tutorials on particular elements intimately.

And thanks, Manoela and GSAP, for giving me the chance to replicate on this lengthy journey!

Tags: CreativelettingPortfolioProcessshapeWebGL
Admin

Admin

Next Post
Save Massive on TCG, Video games, Plushies & Extra

Save Massive on TCG, Video games, Plushies & Extra

Leave a Reply Cancel reply

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

Recommended.

Bitcoin value $8.6 billion moved for the primary time since 2011, purchased for simply $210K

Bitcoin value $8.6 billion moved for the primary time since 2011, purchased for simply $210K

July 7, 2025
The use (and design) of instruments

Tales and hope | Seth’s Weblog

April 27, 2025

Trending.

How you can open the Antechamber and all lever places in Blue Prince

How you can open the Antechamber and all lever places in Blue Prince

April 14, 2025
The most effective methods to take notes for Blue Prince, from Blue Prince followers

The most effective methods to take notes for Blue Prince, from Blue Prince followers

April 20, 2025
Exporting a Material Simulation from Blender to an Interactive Three.js Scene

Exporting a Material Simulation from Blender to an Interactive Three.js Scene

August 20, 2025
AI Girlfriend Chatbots With No Filter: 9 Unfiltered Digital Companions

AI Girlfriend Chatbots With No Filter: 9 Unfiltered Digital Companions

May 18, 2025
Constructing a Actual-Time Dithering Shader

Constructing a Actual-Time Dithering Shader

June 4, 2025

AimactGrow

Welcome to AimactGrow, your ultimate source for all things technology! Our mission is to provide insightful, up-to-date content on the latest advancements in technology, coding, gaming, digital marketing, SEO, cybersecurity, and artificial intelligence (AI).

Categories

  • AI
  • Coding
  • Cybersecurity
  • Digital marketing
  • Gaming
  • SEO
  • Technology

Recent News

Forest Frolic Problem Information And Walkthrough

Forest Frolic Problem Information And Walkthrough

January 11, 2026
The 5 Finest Account-Based mostly Promoting Software program I Belief

The 5 Finest Account-Based mostly Promoting Software program I Belief

January 11, 2026
  • About Us
  • Privacy Policy
  • Disclaimer
  • Contact Us

© 2025 https://blog.aimactgrow.com/ - All Rights Reserved

No Result
View All Result
  • Home
  • Technology
  • AI
  • SEO
  • Coding
  • Gaming
  • Cybersecurity
  • Digital marketing

© 2025 https://blog.aimactgrow.com/ - All Rights Reserved