• 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

From SplitText to MorphSVG: 5 Artistic Demos Utilizing Free GSAP Plugins

Admin by Admin
May 14, 2025
Home Coding
Share on FacebookShare on Twitter


for a masks impact later
linesClass: “line”,
wordsClass: “phrase”,
charsClass: “letter”
});

masks: "traces" wraps every line in its personal container so you are able to do masked reveals with out additional markup.

3. Hook up the buttons

Since it is a showcase, we’ve added three buttons. One every for “Strains”, “Phrases” and “Letters”—to let customers set off every model on demand. In an actual challenge you would possibly hearth these tweens on scroll, on web page load, or when one other interplay happens.

To maintain our code a bit cleaner, we outline a config object that maps every cut up kind to its ultimate period and stagger. As a result of traces, phrases, and letters have vastly completely different counts, matching your timing to the variety of parts ensures every animation feels tight and responsive.

Should you used the identical stagger for letters as you do for traces, animating dozens (or a whole lot) of chars would take eternally. Tailoring the stagger to the ingredient rely retains the reveal snappy.

// 1. Outline per-type timing
const config = {
  traces: { period: 0.8, stagger: 0.08 },
  phrases: { period: 0.6, stagger: 0.06 },
  letters: { period: 0.4, stagger: 0.008 }
};

Subsequent, our animate(kind) perform:

perform animate(kind) {
  // 1) Clear up any working tween so clicks “restart” cleanly
  if (currentTween) {
    currentTween.kill();
    gsap.set(currentTargets, { yPercent: 0 });
  }

  // 2) Pull the appropriate timing from our config
  const { period, stagger } = config[type];

  // 3) Match the button’s data-split-type to the CSS class
  // Our SplitText name used linesClass="line", wordsClass="phrase", charsClass="letter"
  const selector = kind === "traces" ? ".line"
                 : kind === "phrases" ? ".phrase"
                                    : ".letter";

  // 4) Question the proper parts and animate
  currentTargets = heading.querySelectorAll(selector);
  currentTween = gsap.fromTo(
    currentTargets,
    { yPercent: 110 },
    { yPercent: 0, period, stagger, ease: "osmo-ease" }
  );
}

Discover how kind (the button’s data-split-type) straight aligns with our config keys and the category names we set on every slice. This tidy mapping means you possibly can add new sorts (or swap class names) with out rewriting your logic—simply replace config (and your SplitText choices) and the perform auto-adapts.

Lastly, tie all of it along with occasion listeners:

const buttons = doc.querySelectorAll('[data-split="button"]');

buttons.forEach(btn =>
  btn.addEventListener("click on", () =>
    animate(btn.dataset.splitType)
  )
);

4. Placing all of it collectively

Let’s put all of our JS collectively in a single neat perform, and name it as quickly as our fonts are loaded. This fashion we keep away from splitting textual content whereas a fallback font is seen, and with that, we keep away from any surprising line breaks.

// JavaScript (guarantee GSAP, SplitText & CustomEase are loaded)
gsap.registerPlugin(SplitText, CustomEase);
CustomEase.create("osmo-ease", "0.625, 0.05, 0, 1");

perform initSplitTextDemo() {
  const heading = doc.querySelector('[data-split="heading"]');
  SplitText.create(heading, {
    kind: "traces, phrases, chars",
    masks: "traces",
    linesClass: "line",
    wordsClass: "phrase",
    charsClass: "letter"
  });

  const config = {
    traces: { period: 0.8, stagger: 0.08 },
    phrases: { period: 0.6, stagger: 0.06 },
    letters: { period: 0.4, stagger: 0.008 }
  };

  let currentTween, currentTargets;

  perform animate(kind) {
    if (currentTween) {
      currentTween.kill();
      gsap.set(currentTargets, { yPercent: 0 });
    }

    const { period, stagger } = config[type];
    const selector = kind === "traces" ? ".line"
                   : kind === "phrases" ? ".phrase"
                                      : ".letter";

    currentTargets = heading.querySelectorAll(selector);
    currentTween = gsap.fromTo(
      currentTargets,
      { yPercent: 110 },
      { yPercent: 0, period, stagger, ease: "osmo-ease" }
    );
  }

  doc.querySelectorAll('[data-split="button"]').forEach(btn =>
    btn.addEventListener("click on", () =>
      animate(btn.dataset.splitType)
    )
  );
}

doc.fonts.prepared.then(initSplitTextDemo);

5. Assets & hyperlinks

Give it a spin your self! Discover this demo on CodePen and seize the Webflow cloneable beneath. For a deep dive into each out there choice, try the official SplitText docs, and head over to the CustomEase documentation to discover ways to craft your personal easing curves.

→ Webflow Cloneable

→ CodePen

We’ll proceed subsequent with the Physics2D Textual content Smash demo—combining SplitText with one other GSAP plugin for a very completely different impact.

Physics2D Textual content Smash Demo

Should you weren’t conscious already, with the latest Webflow × GSAP bulletins, SplitText obtained a serious overhaul—filled with highly effective new choices, accessibility enhancements, and a dramatically smaller bundle dimension. Take a look at the SplitText docs for all the small print.

In contrast to our earlier demo (which was extra of an interactive playground with buttons), this impact is quite a bit nearer to a real-world utility; as you scroll, every heading “breaks” into characters and falls off of your viewport prefer it’s hit a roof—because of ScrollTrigger and Physics2DPlugin.

Earlier than we dive into code, a pair notes:

  • Plugins wanted: GSAP core, SplitText, ScrollTrigger, and Physics2DPlugin.
  • Property used: We’re utilizing some squiggly, enjoyable, 3D objects from a free pack on wannathis.one. Positively try their stuff, they’ve extra enjoyable issues!
  • Demo objective: We’re combining SplitText + Physics2D on scroll so your headings shatter into characters and “fall” off the highest of the viewport, as in the event that they hit a ‘roof’.

HTML & CSS Setup

  
physique {
  coloration: #efeeec;
  background-color: #340824;
}

.drop-wrapper {
  width: 100%;
  min-height: 350vh;
}

.drop-section {
  show: flex;
  justify-content: middle;
  align-items: middle;
  min-height: 100vh;
  place: relative;
}

.drop-heading {
  max-width: 40rem;
  margin: 0;
  font-size: 4rem;
  font-weight: 500;
  line-height: 1;
  text-align: middle;
}

.drop-heading-img {
  show: inline-block;
  place: relative;
  width: 1.4em;
  z-index: 2;
}

.drop-heading-img.is--first {
  remodel: rotate(-20deg) translate(.15em, -.2em);
}

.drop-heading-img.is--second {
  remodel: translate(-.15em) rotate(10deg);
}

.drop-heading-img.is--third {
  remodel: translate(-.05em, .1em) rotate(50deg);
  margin: 0 .1em;
}

1. Register plugins

Begin by registering all of our vital plugins

gsap.registerPlugin(ScrollTrigger, SplitText, Physics2DPlugin);

2. SplitText setup

We’re utilizing aria: true right here to routinely add an aria-label on the wrapper and conceal cut up spans from display screen readers. For the reason that newest replace, aria: true is the default, so that you don’t essentially have so as to add it right here—however we’re highlighting it for the article.

We cut up the textual content as quickly because the code runs, in order that we are able to connect a callback to the brand new onSplit perform, however extra on that in step 3.

new SplitText("[data-drop-text]", {
  kind: "traces, chars",
  autoSplit: true,  // re-split if the ingredient resizes and it is cut up by traces
  aria: true, // default now, however price highlighting!
  linesClass: "line",
});

With the latest SplitText replace, there’s additionally a brand new choice known as autoSplit—which takes care of resize occasions, and re-splitting your textual content.

An essential caveat for the autoSplit choice; it is best to at all times create your animations within the (additionally new!) onSplit() callback in order that in case your textual content re-splits (when the container resizes or a font masses in), the ensuing animations have an effect on the freshly-created line/phrase/character parts as an alternative of those from the earlier cut up. Should you’re planning on utilizing a non-responsive font-size or simply need to study extra about this (superior) new function that takes care of responsive line splitting, try the documentation right here.

3. Set off on scroll

In our onSplit callback, we loop over every line within the heading, within a context. This context, which we return on the finish, makes certain GSAP can clear up this animation every time the textual content re-splits.

In our loop, we create a ScrollTrigger for every line, and we set as soon as: true, so our animation solely fires as soon as. In step 4 we’ll add our animation!

It’s price taking part in round with the begin values to actually nail the second the place your textual content visually ‘touches’ the highest of the window. For our font, dimension, and line-height combo, an offset of 10px labored nice.

new SplitText("[data-drop-text]", {
  kind: "traces, chars",
  autoSplit: true,
  aria: true,
  linesClass: "line",
  onSplit(self) {
    // use a context to gather up all of the animations
    let ctx = gsap.context(() => {
      self.traces.forEach((line) => { // loop across the traces          
        gsap.timeline({
          scrollTrigger: {
            as soon as: true, // solely hearth as soon as
            set off: line, // use the road as a set off
            begin: "high top-=10" // modify the set off level to your liking
          }
        })
      });
    });

    return ctx; // return our animations so GSAP can clear them up when onSplit fires
  }
});

4. Drop the letters with Physics2D

Now, let’s add 2 tweens to our timeline. The primary one, utilizing the Physics2D plugin, sends every youngster ingredient of the road, flying straight down with randomized velocity, angle, and gravity. A second tween makes certain the weather are pale out in direction of the top.

new SplitText("[data-drop-text]", {
  kind: "traces, chars",
  autoSplit: true,
  aria: true,
  linesClass: "line",
  onSplit(self) {
    // use a context to gather up all of the animations
    let ctx = gsap.context(() => {
      self.traces.forEach((line) => { // loop across the traces          
        gsap.timeline({
          scrollTrigger: {
            as soon as: true, // solely hearth as soon as
            set off: line, // use the road as a set off
            begin: "high top-=10" // modify the set off level to your liking
          }
        })
        .to(line.youngsters, { // goal the youngsters
          period: "random(1.5, 3)", // Use randomized values for a extra dynamic animation
          physics2D: {
            velocity: "random(500, 1000)",
            angle: 90,
            gravity: 3000
          },
          rotation: "random(-90, 90)",
          ease: "none"
        })
        .to(line.youngsters,{ // Begin fading them out
          autoAlpha: 0,
          period: 0.2
         }, "-=.2");
      });
    });

    return ctx; // return our animations so GSAP can clear them up when onSplit fires
  }
});

Tip: use gsap.utils.random()! Giving every char and picture a barely completely different pace and spin creates a joyful, and extra pure feeling to all of it.

5. Placing all of it collectively

gsap.registerPlugin(ScrollTrigger, SplitText, Physics2DPlugin);

perform initDroppingText() {
  new SplitText("[data-drop-text]", {
    kind: "traces, chars",
    autoSplit: true,
    aria: true,
    linesClass: "line",
    onSplit(self) {
      // use a context to gather up all of the animations
      let ctx = gsap.context(() => {
        self.traces.forEach((line) => {         
          gsap
            .timeline({
              scrollTrigger: {
                as soon as: true,
                set off: line,
                begin: "high top-=10"
              }
            })
            .to(line.youngsters, { // goal the youngsters
              period: "random(1.5, 3)", // Use randomized values for a extra dynamic animation
              physics2D: {
                velocity: "random(500, 1000)",
                angle: 90,
                gravity: 3000
              },
              rotation: "random(-90, 90)",
              ease: "none"
            })
            .to(
              line.youngsters,
              {
                autoAlpha: 0,
                period: 0.2
              },
              "-=.2"
            );
        });
      });

      return ctx; // return our animations so GSAP can clear them up when onSplit fires
    }
  });
}

doc.addEventListener("DOMContentLoaded", initDroppingText);

6. Assets & hyperlinks

→ Webflow Cloneable

→ CodePen

Subsequent up: an interactive Inertia Dot Grid that springs and flows together with your cursor!

Glowing Interactive Dot Grid

InertiaPlugin (previously ThrowPropsPlugin) means that you can easily glide any property to a cease, honoring an preliminary velocity in addition to making use of elective restrictions on the top worth. It brings real-world momentum to your parts, letting them transfer with an preliminary velocity and easily sluggish beneath configurable resistance. You merely specify a beginning velocity and resistance worth, and the plugin handles the physics.

On this demo, we’re utilizing a quick-to-prototype grid of

dots that glow as your cursor approaches, spring away on speedy mouse actions, and ripple outward on clicks. Whereas a Canvas or WebGL method would scale extra effectively for 1000’s of particles and ship larger frame-rates, our div-based resolution retains the code easy and accessible—good for spotlighting InertiaPlugin’s capabilities.

Earlier than we dive in:

  • Plugins wanted: GSAP core and InertiaPlugin.
  • Demo objective: Construct a responsive grid of dots that glow with proximity and spring away on quick mouse strikes or clicks—showcasing how the InertiaPlugin can add playful, physics-based reactions to a format.

HTML & CSS Setup



physique {
  overscroll-behavior: none;
  background-color: #08342a;
  coloration: #efeeec;
}

.dots-container {
  place: absolute;
  inset: 4em;
  show: flex;
  flex-flow: wrap;
  hole: 2em;
  justify-content: middle;
  align-items: middle;
  pointer-events: none;
}

.dot {
  place: relative;
  width: 1em;
  peak: 1em;
  border-radius: 50%;
  background-color: #245e51;
  transform-origin: middle;
  will-change: remodel, background-color;
  remodel: translate(0);
  place-self: middle;
}

.section-resource {
  coloration: #efeeec;
  justify-content: middle;
  align-items: middle;
  show: flex;
  place: absolute;
  inset: 0;
}

.osmo-icon-svg {
  width: 10em;
}

.osmo-icon__link {
  coloration: currentColor;
  text-decoration: none;
}

1. Register plugins

gsap.registerPlugin(InertiaPlugin);

2. Construct your grid & elective middle gap

First, wrap every little thing in an initGlowingInteractiveDotsGrid() perform and declare your tweakable parameters—colours, glow distance, pace thresholds, shockwave settings, max pointer pace, and whether or not to carve out a middle gap for a emblem. We additionally arrange two arrays, dots and dotCenters, to trace the weather and their positions.

perform initGlowingInteractiveDotsGrid() {
  const container = doc.querySelector('[data-dots-container-init]');
  const colours = { base: "#245E51", energetic: "#A8FF51" };
  const threshold = 200;
  const speedThreshold = 100;
  const shockRadius = 325;
  const shockPower = 5;
  const maxSpeed = 5000;
  const centerHole = true;
  let dots = [];
  let dotCenters = [];

  // buildGrid(), mousemove & click on handlers outlined subsequent…
}

With these in place, buildGrid() figures out what number of columns and rows match based mostly in your container’s em sizing, then optionally carves out a wonderfully centered block of 4 or 5 columns/rows (relying on whether or not the grid dimensions are even or odd) if centerHole is true. That gap offers house to your emblem; set centerHole = false to fill each cell.

Inside buildGrid(), we:

  1. Filter any current dots and reset our arrays.
  2. Learn the container’s fontSize to get dotPx (in px) and derive gapPx.
  3. Calculate what number of columns and rows match, plus the overall cells.
  4. Compute a centered “gap” of 4 or 5 columns/rows if centerHole is true, so you possibly can place a emblem or focal ingredient.
perform buildGrid() {
  container.innerHTML = "";
  dots = [];
  dotCenters = [];

  const model = getComputedStyle(container);
  const dotPx = parseFloat(model.fontSize);
  const gapPx = dotPx * 2;
  const contW = container.clientWidth;
  const contH = container.clientHeight;
  const cols = Math.ground((contW + gapPx) / (dotPx + gapPx));
  const rows = Math.ground((contH + gapPx) / (dotPx + gapPx));
  const whole = cols * rows;

  const holeCols = centerHole ? (cols % 2 === 0 ? 4 : 5) : 0;
  const holeRows = centerHole ? (rows % 2 === 0 ? 4 : 5) : 0;
  const startCol = (cols - holeCols) / 2;
  const startRow = (rows - holeRows) / 2;

  // …subsequent: loop by every cell to create dots…
}

Now loop over each cell index. Inside that loop, we conceal any dot within the gap area and initialize the seen ones with GSAP’s set(). Every dot is appended to the container and pushed into our dots array for monitoring.

For every dot:

  • If it falls within the gap area, we conceal it.
  • In any other case, we place it at { x: 0, y: 0 } with the bottom coloration and mark it as not but sprung.
  • Append it to the container and monitor it in dots.
// ... add this to the buildGrid() perform

for (let i = 0; i < whole; i++) {
  const row = Math.ground(i / cols);
  const col = i % cols;
  const isHole =
    centerHole &&
    row >= startRow &&
    row < startRow + holeRows &&
    col >= startCol &&
    col < startCol + holeCols;

  const d = doc.createElement("div");
  d.classList.add("dot");

  if (isHole) {
    d.model.visibility = "hidden";
    d._isHole = true;
  } else {
    gsap.set(d, { x: 0, y: 0, backgroundColor: colours.base });
    d._inertiaApplied = false;
  }

  container.appendChild(d);
  dots.push(d);
}

// ... extra code added beneath

Lastly, as soon as the DOM is up to date, measure every seen dot’s middle coordinate—together with any scroll offset—so we are able to calculate distances later. Wrapping in requestAnimationFrame ensures the format is settled.

// ... add this to the buildGrid() perform

requestAnimationFrame(() => {
  dotCenters = dots
    .filter(d => !d._isHole)
    .map(d => {
      const r = d.getBoundingClientRect();
      return {
        el: d,
        x: r.left + window.scrollX + r.width / 2,
        y: r.high + window.scrollY + r.peak / 2
      };
    });
});

// that is the top of the buildGrid() perform

By now, the entire buildGrid() perform will appear to be the next:

perform buildGrid() {
  container.innerHTML = "";
  dots = [];
  dotCenters = [];

  const model = getComputedStyle(container);
  const dotPx = parseFloat(model.fontSize);
  const gapPx = dotPx * 2;
  const contW = container.clientWidth;
  const contH = container.clientHeight;
  const cols = Math.ground((contW + gapPx) / (dotPx + gapPx));
  const rows = Math.ground((contH + gapPx) / (dotPx + gapPx));
  const whole = cols * rows;

  const holeCols = centerHole ? (cols % 2 === 0 ? 4 : 5) : 0;
  const holeRows = centerHole ? (rows % 2 === 0 ? 4 : 5) : 0;
  const startCol = (cols - holeCols) / 2;
  const startRow = (rows - holeRows) / 2;

  for (let i = 0; i < whole; i++) {
    const row = Math.ground(i / cols);
    const col = i % cols;
    const isHole = centerHole &&
      row >= startRow && row < startRow + holeRows &&
      col >= startCol && col < startCol + holeCols;

    const d = doc.createElement("div");
    d.classList.add("dot");

    if (isHole) {
      d.model.visibility = "hidden";
      d._isHole = true;
    } else {
      gsap.set(d, { x: 0, y: 0, backgroundColor: colours.base });
      d._inertiaApplied = false;
    }

    container.appendChild(d);
    dots.push(d);
  }

  requestAnimationFrame(() => {
    dotCenters = dots
      .filter(d => !d._isHole)
      .map(d => {
        const r = d.getBoundingClientRect();
        return {
          el: d,
          x: r.left + window.scrollX + r.width / 2,
          y: r.high + window.scrollY + r.peak / 2
        };
      });
  });
}

On the finish of initGlowingInteractiveDotsGrid(), we connect a resize listener and invoke buildGrid() as soon as to kick issues off:

window.addEventListener("resize", buildGrid);
buildGrid();

3. Deal with mouse transfer interactions

Because the consumer strikes their cursor, we calculate its velocity by evaluating the present e.pageX/e.pageY to the final recorded place over time (dt). We clamp that pace to maxSpeed to keep away from runaway values. Then, on the following animation body, we loop by every dot’s middle:

  • Compute its distance to the cursor and derive t = Math.max(0, 1 - dist / threshold).
  • Interpolate its coloration from colours.base to colours.energetic.
  • If pace > speedThreshold and the dot is inside threshold, mark it _inertiaApplied and hearth an inertia tween to push it away earlier than it springs again.

All this nonetheless goes within our initGlowingInteractiveDotsGrid() perform:

let lastTime = 0
let lastX = 0
let lastY = 0

window.addEventListener("mousemove", e => {
  const now = efficiency.now()
  const dt = now - lastTime || 16
  let dx = e.pageX - lastX
  let dy = e.pageY - lastY
  let vx = (dx / dt) * 1000
  let vy = (dy / dt) * 1000
  let pace = Math.hypot(vx, vy)

  if (pace > maxSpeed) {
    const scale = maxSpeed / pace
    vx = vx * scale
    vy = vy * scale
    pace = maxSpeed
  }

  lastTime = now
  lastX = e.pageX
  lastY = e.pageY

  requestAnimationFrame(() => {
    dotCenters.forEach(({ el, x, y }) => {
      const dist = Math.hypot(x - e.pageX, y - e.pageY)
      const t = Math.max(0, 1 - dist / threshold)
      const col = gsap.utils.interpolate(colours.base, colours.energetic, t)
      gsap.set(el, { backgroundColor: col })

      if (pace > speedThreshold && dist < threshold && !el._inertiaApplied) {
        el._inertiaApplied = true
        const pushX = (x - e.pageX) + vx * 0.005
        const pushY = (y - e.pageY) + vy * 0.005

        gsap.to(el, {
          inertia: { x: pushX, y: pushY, resistance: 750 },
          onComplete() {
            gsap.to(el, {
              x: 0,
              y: 0,
              period: 1.5,
              ease: "elastic.out(1, 0.75)"
            })
            el._inertiaApplied = false
          }
        })
      }
    })
  })
})

4. Deal with click on ‘shockwave’ impact

On every click on, we ship a radial ‘shockwave’ by the grid. We reuse the identical inertia + elastic return logic, however scale the push by a distance-based falloff in order that dots nearer to the press transfer additional, then all spring again in unison.

window.addEventListener("click on", e => {
  dotCenters.forEach(({ el, x, y }) => {
    const dist = Math.hypot(x - e.pageX, y - e.pageY)
    if (dist < shockRadius && !el._inertiaApplied) {
      el._inertiaApplied = true
      const falloff = Math.max(0, 1 - dist / shockRadius)
      const pushX = (x - e.pageX) * shockPower * falloff
      const pushY = (y - e.pageY) * shockPower * falloff

      gsap.to(el, {
        inertia: { x: pushX, y: pushY, resistance: 750 },
        onComplete() {
          gsap.to(el, {
            x: 0,
            y: 0,
            period: 1.5,
            ease: "elastic.out(1, 0.75)"
          })
          el._inertiaApplied = false
        }
      })
    }
  })
})

5. Placing all of it collectively

By now, all of our items dwell inside one initGlowingInteractiveDotsGrid() perform. Right here’s an abbreviated view of your remaining JS setup:

gsap.registerPlugin(InertiaPlugin);

perform initGlowingInteractiveDotsGrid() {
  // buildGrid(): creates and positions dots
  // window.addEventListener("mousemove", …): glow & spring logic
  // window.addEventListener("click on", …): shockwave logic
}

doc.addEventListener("DOMContentLoaded", initGlowingInteractiveDotsGrid);

6. Assets & hyperlinks

→ Webflow Cloneable

→ CodePen

Subsequent up: DrawSVG Scribbles Demo — let’s draw some playful, randomized underlines on hover!

DrawSVG Scribbles Demo

GSAP’s DrawSVGPlugin animates the stroke of an SVG path by tweening its stroke-dasharray and stroke-dashoffset, making a ‘drawing’ impact. You possibly can management begin/finish percentages, period, easing, and even stagger a number of paths. On this demo, we’ll connect a randomized scribble underline to every hyperlink on hover—good for including a playful contact to your navigation or call-to-actions.

  • Plugins wanted: GSAP core and DrawSVGPlugin
  • Demo objective: On hover, inject a random SVG scribbles beneath your hyperlink textual content and animate it from 0% to 100% draw, then erase it on hover-out.

HTML & CSS Setup

Branding

Design

Improvement

physique {
  background-color: #fefaee;
}
.section-resource {
  show: flex;
  justify-content: middle;
  align-items: middle;
  min-height: 100vh;
  font-size: 1.5vw;
}
.text-draw {
  coloration: #340824;
  cursor: pointer;
  margin: 0 1em;
  font-size: 2em;
  text-decoration: none;
}
.text-draw__p {
  margin-bottom: 0;
  font-size: 1.5em;
  font-weight: 500;
  line-height: 1.1;
}
.text-draw__box {
  place: relative;
  width: 100%;
  peak: .625em;
  coloration: #e55050;
}
.text-draw__box-svg {
  place: absolute;
  high: 0;
  left: 0;
  width: 100%;
  peak: 100%;
  overflow: seen !essential;
}

1. Register the plugin

gsap.registerPlugin(DrawSVGPlugin);

2. Put together your SVG variants

We outline an array of tangible SVG scribbles. Every string is a standalone with its . Once we inject it, we run decorateSVG() to make sure it scales to its container and makes use of currentColor for theming.

We’ve drawn these scribbles ourselves in figma utilizing the pencil. We advocate drawing (and thus creating the trail coordinates) within the order of which you need to draw them.

const svgVariants = [
    ``,
    ``,
    ``,
    ``,
    ``,
    ``
  ];
  
perform decorateSVG(svgEl) {  
  svgEl.setAttribute('class', 'text-draw__box-svg');
  svgEl.setAttribute('preserveAspectRatio', 'none');
  svgEl.querySelectorAll('path').forEach(path => {
    path.setAttribute('stroke', 'currentColor');
  });
}

3. Arrange hover animations

For every hyperlink, we hear for mouseenter and mouseleave. On hover-in, we:

  • Forestall restarting if the earlier draw-in tween remains to be energetic.
  • Kill any ongoing draw-out tween.
  • Choose the following SVG variant (biking by the array).
  • Inject it into the field, enhance it, set its preliminary drawSVG to “0%”, then tween to “100%” in 0.5s with an ease of power2.inOut.

On hover-out, we tween drawSVG from “100% 100%” to erase it, then clear the SVG when full.

let nextIndex = null;

doc.querySelectorAll('[data-draw-line]').forEach(container => {
  const field = container.querySelector('[data-draw-line-box]');
  if (!field) return;
  let enterTween = null;
  let leaveTween = null;

  container.addEventListener('mouseenter', () => {
    if (enterTween && enterTween.isActive()) return;
    if (leaveTween && leaveTween.isActive()) leaveTween.kill();

    if (nextIndex === null) {
      nextIndex = Math.ground(Math.random() * svgVariants.size);
    }

    field.innerHTML = svgVariants[nextIndex];
    const svg = field.querySelector('svg');
    if (svg) {
      decorateSVG(svg);
      const path = svg.querySelector('path');
      gsap.set(path, { drawSVG: '0%' });
      enterTween = gsap.to(path, {
        period: 0.5,
        drawSVG: '100%',
        ease: 'power2.inOut',
        onComplete: () => { enterTween = null; }
      });
    }

    nextIndex = (nextIndex + 1) % svgVariants.size;
  });

  container.addEventListener('mouseleave', () => {
    const path = field.querySelector('path');
    if (!path) return;

    const playOut = () => {
      if (leaveTween && leaveTween.isActive()) return;
      leaveTween = gsap.to(path, {
        period: 0.5,
        drawSVG: '100% 100%',
        ease: 'power2.inOut',
        onComplete: () => {
          leaveTween = null;
          field.innerHTML = '';
        }
      });
    };

    if (enterTween && enterTween.isActive()) {
      enterTween.eventCallback('onComplete', playOut);
    } else {
      playOut();
    }
  });
});

4. Initialize on web page load

Wrap the above setup in your initDrawRandomUnderline() perform and name it as soon as the DOM is prepared:

perform initDrawRandomUnderline() {
  // svgVariants, decorateSVG, and all occasion listeners…
}

doc.addEventListener('DOMContentLoaded', initDrawRandomUnderline);

5. Assets & hyperlinks

→ Webflow Cloneable

→ CodePen

And now on to the ultimate demo: MorphSVG Toggle Demo—see the way to morph one icon into one other in a single tween!

MorphSVG Toggle Demo

MorphSVGPlugin helps you to fluidly morph one SVG form into one other—even after they have completely different numbers of factors—by intelligently mapping anchor factors. You possibly can select the morphing algorithm (dimension, place or complexity), management easing, period, and even add rotation to make the transition really feel additional easy. On this demo, we’re toggling between a play ► and pause ❚❚ icon on button click on, then flipping again. Excellent for video gamers, music apps, or any interactive management.

We extremely advocate diving into the docs for this plugin, as there are a complete bunch of choices and potentialities.

  • Plugins wanted: GSAP core and MorphSVGPlugin
  • Demo objective: Construct a play/pause button that seamlessly morphs its SVG path on every click on.

HTML & CSS Setup

physique {
  background-color: #0e100f;
  coloration: #fffce1;
  show: flex;
  flex-direction: column;
  align-items: middle;
  justify-content: middle;
  peak: 100vh;
  margin: 0;
}

.play-pause-button {
  background: clear;
  border: none;
  width: 10rem;
  peak: 10rem;
  show: flex;
  align-items: middle;
  justify-content: middle;
  coloration: currentColor;
  cursor: pointer;
}

.play-pause-icon {
  width: 100%;
  peak: 100%;
}

1. Register the plugin

gsap.registerPlugin(MorphSVGPlugin);

2. Outline paths & toggle logic

We retailer two path definitions: playPath and pausePath, then seize our button and the ingredient inside it. A easy isPlaying boolean tracks state. On every click on, we name gsap.to() on the SVG path, passing morphSVG choices:

  • kind: “rotational” to easily rotate factors into place
  • map: “complexity” to match by variety of anchors for pace
  • form set to the alternative icon’s path

Lastly, we flip isPlaying so the following click on morphs again.

perform initMorphingPlayPauseToggle() {
  const playPath =
    "M3.5 5L3.50049 3.9468C3.50049 3.177 4.33382 2.69588 5.00049 3.08078L20.0005 11.741C20.6672 12.1259 20.6672 13.0882 20.0005 13.4731L17.2388 15.1412L17.0055 15.2759M3.50049 8L3.50049 21.2673C3.50049 22.0371 4.33382 22.5182 5.00049 22.1333L14.1192 16.9423L14.4074 16.7759";
  const pausePath =
    "M15.5004 4.05859V5.0638V5.58691V8.58691V15.5869V19.5869V21.2549M8.5 3.96094V10.3721V17V19L8.5 21";

  const buttonToggle = doc.querySelector('[data-play-pause="toggle"]');
  const iconPath = buttonToggle.querySelector('[data-play-pause="path"]');
  let isPlaying = false;

  buttonToggle.addEventListener("click on", () => {
    gsap.to(iconPath, {
      period: 0.5,
      ease: "power4.inOut",
      morphSVG: {
        kind: "rotational",
        map: "complexity",
        form: isPlaying ? playPath : pausePath
      }
    });
    isPlaying = !isPlaying;
  });
}

doc.addEventListener("DOMContentLoaded", initMorphingPlayPauseToggle);

4. Assets & hyperlinks

  • MorphSVGPlugin docs
  • Bonus: We additionally added a confetti impact on click on utilizing the Physics2DPlugin for the beneath Webflow and CodePen assets!

→ Webflow Cloneable

→ CodePen

And that wraps up our MorphSVG Toggle!

Closing ideas

Thanks for making it this far down the web page! We all know it’s a somewhat lengthy learn, so we hope there’s some inspiring stuff in right here for you. Each Dennis and I are tremendous stoked with all of the GSAP Plugins being free now, and might’t wait to create extra assets with them.

As a notice, we’re absolutely conscious that every one the HTML and markup within the article is somewhat concise, and positively less than commonplace with all finest practices for accessibility. To make these assets production-ready, positively search for steering on the requirements at w3.org! Consider the above ones as your launch-pad. Able to tweak and make your personal.

Have a beautiful remainder of your day, or evening, wherever you’re. Glad animating!

Entry a rising library of assets

Constructed by two award-winning artistic builders Dennis Snellenberg and Ilja van Eck, our vault offers you entry to the strategies, parts, code, and instruments behind our tasks. All neatly packed in a custom-built dashboard. Construct, tweak, and make them your personal—for Webflow and non-Webflow customers.

Grow to be a member at this time to unlock our rising set of parts and be a part of a group of greater than 850 artistic builders worldwide!

→ Grow to be a member

Osmo

Begin constructing web sites individuals keep in mind. Weekly new assets. A platform by Dennis Snellenberg & Ilja van Eck.

gsap morphing svg typography

Integrating Rive right into a React Challenge: Behind the Scenes of Valley Adventures

Tags: CreativeDemosFreeGSAPMorphSVGPluginsSplitText
Admin

Admin

Next Post
Jessica Jones returns in Daredevil: Born Once more season 2

Jessica Jones returns in Daredevil: Born Once more season 2

Leave a Reply Cancel reply

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

Recommended.

Bungie caught utilizing stolen artwork belongings in Marathon, guarantees it is not going to occur once more

Bungie caught utilizing stolen artwork belongings in Marathon, guarantees it is not going to occur once more

May 17, 2025
Brinker Named Amongst “10 Most Promising Protection Tech Startups of 2025”

Brinker Named Amongst “10 Most Promising Protection Tech Startups of 2025”

April 3, 2025

Trending.

Industrial-strength April Patch Tuesday covers 135 CVEs – Sophos Information

Industrial-strength April Patch Tuesday covers 135 CVEs – Sophos Information

April 10, 2025
Expedition 33 Guides, Codex, and Construct Planner

Expedition 33 Guides, Codex, and Construct Planner

April 26, 2025
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
Important SAP Exploit, AI-Powered Phishing, Main Breaches, New CVEs & Extra

Important SAP Exploit, AI-Powered Phishing, Main Breaches, New CVEs & Extra

April 28, 2025
Wormable AirPlay Flaws Allow Zero-Click on RCE on Apple Units by way of Public Wi-Fi

Wormable AirPlay Flaws Allow Zero-Click on RCE on Apple Units by way of Public Wi-Fi

May 5, 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

Yoast AI Optimize now out there for Basic Editor • Yoast

Replace on Yoast AI Optimize for Traditional Editor  • Yoast

June 18, 2025
You’ll at all times keep in mind this because the day you lastly caught FamousSparrow

You’ll at all times keep in mind this because the day you lastly caught FamousSparrow

June 18, 2025
  • 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