Creating WebGL Effects with CurtainsJS

This article focuses adding WebGL effects to <image> and <video> elements of an already “completed” web page. While there are a few helpful resources out there on this subject (like these two), I hope to help simplify this subject by distilling the process into a few steps: 

  • Create a web page as you normally would.
  • Render pieces that you want to add WebGL effects to with WebGL.
  • Create (or find) the WebGL effects to use.
  • Add event listeners to connect your page with the WebGL effects.

Specifically, we’ll focus on the connection between regular web pages and WebGL. What are we going to make? How about a draggle image slider with an interactive mouse hover!

We won’t cover the core functionality of slider or go very far into the technical details of WebGL or GLSL shaders. However, there are plenty of comments in the demo code and links to outside resources if you’d like to learn more. 

We’re using the latest version of WebGL (WebGL2) and GLSL (GLSL 300) which currently do not work in Safari or in Internet Explorer. So, use Firefox or Chrome to view the demos. If you’re planning to use any of what we’re covering in production, you should load both the GLSL 100 and 300 versions of the shaders and use the GLSL 300 version only if curtains.renderer._isWebGL2 is true. I cover this in the demo above.

First, create a web page as you normally would

You know, HTML and CSS and whatnot. In this case, we’re making an image slider but that’s just for demonstration. We’re not going to go full-depth on how to make a slider (Robin has a nice post on that). But here’s what I put together:

  1. Each slide is equal to the full width of the page.
  2. After a slide has been dragged, the slider continues to slide in the direction of the drag and gradually slow down with momentum.
  3. The momentum snaps the slider to the nearest slide at the end point. 
  4. Each slide has an exit animation that’s fired when the drag starts and an enter animation that’s fired when the dragging stops.
  5. When hovering the slider, a hover effect is applied similar to this video.

I’m a huge fan of the GreenSock Animation Platform (GSAP). It’s especially useful for us here because it provides a plugin for dragging, one that enables momentum on drag, and one for splitting text by line . If you’re uncomfortable creating sliders with GSAP, I recommend spending some time getting familiar with the code in the demo above.

Again, this is just for demonstration, but I wanted to at least describe the component a bit. These are the DOM elements that we will keep our WebGL synced with. 

Next, use WebGL to render the pieces that will contain WebGL effects

Now we need to render our images in WebGL. To do that we need to:

  1. Load the image as a texture into a GLSL shader.
  2. Create a WebGL plane for the image and correctly apply the image texture to the plane.
  3. Position the plane where the DOM version of the image is and scale it correctly.

The third step is particularly non-trivial using pure WebGL because we need to track the position of the DOM elements we want to port into the WebGL world while keeping the DOM and WebGL parts in sync during scroll and user interactions.

There’s actually a library that helps us do all of this with ease: CurtainsJS! It’s the only library I’ve found that easily creates WebGL versions of DOM images and videos and syncs them without too many other features (but I’d love to be proven wrong on that point, so please leave a comment if you know of others that do this well).

With Curtains, this is all the JavaScript we need to add:

// Create a new curtains instance
const curtains = new Curtains({ container: "canvas", autoRender: false });
// Use a single rAF for both GSAP and Curtains
function renderScene() {
  curtains.render();
}
gsap.ticker.add(renderScene);
// Params passed to the curtains instance
const params = {
  vertexShaderID: "slider-planes-vs", // The vertex shader we want to use
  fragmentShaderID: "slider-planes-fs", // The fragment shader we want to use
  
 // Include any variables to update the WebGL state here
  uniforms: {
    // ...
  }
};
// Create a curtains plane for each slide
const planeElements = document.querySelectorAll(".slide");
planeElements.forEach((planeEl, i) => {
  const plane = curtains.addPlane(planeEl, params);
  // const plane = new Plane(curtains, planeEl, params); // v7 version
  // If our plane has been successfully created
  if(plane) {
    // onReady is called once our plane is ready and all its texture have been created
    plane.onReady(function() {
      // Add a "loaded" class to display the image container
      plane.htmlElement.closest(".slide").classList.add("loaded");
    });
  }
});

We also need to update our updateProgress function so that it updates our WebGL planes.

function updateProgress() {
  // Update the actual slider
  animation.progress(wrapVal(this.x) / wrapWidth);
  
  // Update the WebGL slider planes
  planes.forEach(plane => plane.updatePosition());
}

We also need to add a very basic vertex and fragment shader to display the texture that we’re loading. We can do that by loading them via <script> tags, like I do in the demo, or by using backticks as I show in the final demo.  

Again, this article will not go into a lot of detail on the technical aspects of these GLSL shaders. I recommend reading The Book of Shaders and the WebGL topic on Codrops as starting points.

If you don’t know much about shaders, it’s sufficient to say that the vertex shader positions the planes and the fragment shader processes the texture’s pixels. There are also three variable prefixes that I want to point out:

  • ins are passed in from a data buffer. In vertex shaders, they come from the CPU (our program). In fragment shaders, they come from the vertex shader.
  • uniforms are passed in from the CPU (our program).
  • outs are outputs from our shaders. In vertex shaders, they are passed into our fragment shader. In fragment shaders, they are passed to the frame buffer (what is drawn to the screen).

Once we’ve added all of that to our project, we have the same exact thing before but our slider is now being displayed via WebGL! Neat.

CurtainsJS easily converts images and videos to WebGL. As far as adding WebGL effects to text, there are several different methods but perhaps the most common is to draw the text to a <canvas> and then use it as a texture in the shader (e.g. 1, 2). It’s possible to do most other HTML using html2canvas (or similar) and use that canvas as a texture in the shader; however, this is not very performant.

Create (or find) the WebGL effects to use

Now we can add WebGL effects since we have our slider rendering with WebGL. Let’s break down the effects seen in our inspiration video:

  1. The image colors are inverted.
  2. There is a radius around the mouse position that shows the normal color and creates a fisheye effect.
  3. The radius around the mouse animates from 0 when the slider is hovered and animates back to 0 when it is no longer hovered.
  4. The radius doesn’t jump to the mouse’s position but animates there over time.
  5. The entire image translates based on the mouse’s position in reference to the center of the image.

When creating WebGL effects, it’s important to remember that shaders don’t have a memory state that exists between frames. It can do something based on where the mouse is at a given time, but it can’t do something based on where the mouse has been all by itself. That’s why for certain effects, like animating the radius once the mouse has entered the slider or animating the position of the radius over time, we should use a JavaScript variable and pass that value to each frame of the slider. We’ll talk more about that process in the next section.

Once we modify our shaders to invert the color outside of the radius and create the fisheye effect inside of the radius, we’ll get something like the demo below. Again, the point of this article is to focus on the connection between DOM elements and WebGL so I won’t go into detail about the shaders, but I did add comments to them.

But that’s not too exciting yet because the radius is not reacting to our mouse. That’s what we’ll cover in the next section.

I haven’t found a repository with a lot of pre-made WebGL shaders to use for regular websites. There’s ShaderToy and VertexShaderArt (which have some truly amazing shaders!), but neither is aimed at the type of effects that fit on most websites. I’d really like to see someone create a repository of WebGL shaders as a resource for people working on everyday sites. If you know of one, please let me know.

Add event listeners to connect your page with the WebGL effects

Now we can add interactivity to the WebGL portion! We need to pass in some variables (uniforms) to our shaders and affect those variables when the user interacts with our elements. This is the section where I’ll go into the most detail because it’s the core for how we connect JavaScript to our shaders.

First, we need to declare some uniforms in our shaders. We only need the mouse position in our vertex shader:

// The un-transformed mouse position
uniform vec2 uMouse;

We need to declare the radius and resolution in our fragment shader:

uniform float uRadius; // Radius of pixels to warp/invert
uniform vec2 uResolution; // Used in anti-aliasing

Then let’s add some values for these inside of the parameters we pass into our Curtains instance. We were already doing this for uResolution! We need to specify the name of the variable in the shader, it’s type, and then the starting value:

const params = {
  vertexShaderID: "slider-planes-vs", // The vertex shader we want to use
  fragmentShaderID: "slider-planes-fs", // The fragment shader we want to use
  
  // The variables that we're going to be animating to update our WebGL state
  uniforms: {
    // For the cursor effects
    mouse: { 
      name: "uMouse", // The shader variable name
      type: "2f",     // The type for the variable - https://webglfundamentals.org/webgl/lessons/webgl-shaders-and-glsl.html
      value: mouse    // The initial value to use
    },
    radius: { 
      name: "uRadius",
      type: "1f",
      value: radius.val
    },
    
    // For the antialiasing
    resolution: { 
      name: "uResolution",
      type: "2f", 
      value: [innerWidth, innerHeight] 
    }
  },
};

Now the shader uniforms are connected to our JavaScript! At this point, we need to create some event listeners and animations to affect the values that we’re passing into the shaders. First, let’s set up the animation for the radius and the function to update the value we pass into our shader:

const radius = { val: 0.1 };
const radiusAnim = gsap.from(radius, { 
  val: 0, 
  duration: 0.3, 
  paused: true,
  onUpdate: updateRadius
});
function updateRadius() {
  planes.forEach((plane, i) => {
    plane.uniforms.radius.value = radius.val;
  });
}

If we play the radius animation, then our shader will use the new value each tick.

We also need to update the mouse position when it’s over our slider for both mouse devices and touch screens. There’s a lot of code here, but you can walk through it pretty linearly. Take your time and process what’s happening.

const mouse = new Vec2(0, 0);
function addMouseListeners() {
  if ("ontouchstart" in window) {
    wrapper.addEventListener("touchstart", updateMouse, false);
    wrapper.addEventListener("touchmove", updateMouse, false);
    wrapper.addEventListener("blur", mouseOut, false);
  } else {
    wrapper.addEventListener("mousemove", updateMouse, false);
    wrapper.addEventListener("mouseleave", mouseOut, false);
  }
}


// Update the stored mouse position along with WebGL "mouse"
function updateMouse(e) {
  radiusAnim.play();
  
  if (e.changedTouches && e.changedTouches.length) {
    e.x = e.changedTouches[0].pageX;
    e.y = e.changedTouches[0].pageY;
  }
  if (e.x === undefined) {
    e.x = e.pageX;
    e.y = e.pageY;
  }
  
  mouse.x = e.x;
  mouse.y = e.y;
  
  updateWebGLMouse();
}


// Updates the mouse position for all planes
function updateWebGLMouse(dur) {
  // update the planes mouse position uniforms
  planes.forEach((plane, i) => {
    const webglMousePos = plane.mouseToPlaneCoords(mouse);
    updatePlaneMouse(plane, webglMousePos, dur);
  });
}


// Updates the mouse position for the given plane
function updatePlaneMouse(plane, endPos = new Vec2(0, 0), dur = 0.1) {
  gsap.to(plane.uniforms.mouse.value, {
    x: endPos.x,
    y: endPos.y,
    duration: dur,
    overwrite: true,
  });
}


// When the mouse leaves the slider, animate the WebGL "mouse" to the center of slider
function mouseOut(e) {
  planes.forEach((plane, i) => updatePlaneMouse(plane, new Vec2(0, 0), 1) );
  
  radiusAnim.reverse();
}

We should also modify our existing updateProgress function to keep our WebGL mouse synced.

// Update the slider along with the necessary WebGL variables
function updateProgress() {
  // Update the actual slider
  animation.progress(wrapVal(this.x) / wrapWidth);
  
  // Update the WebGL slider planes
  planes.forEach(plane => plane.updatePosition());
  
  // Update the WebGL "mouse"
  updateWebGLMouse(0);
}

Now we’re cooking with fire! Our slider now mets all of our requirements.

Two additional benefits of using GSAP for your animations is that it provides access to callbacks, like onComplete, and GSAP keeps everything perfectly synced no matter the refresh rate (e.g. this situation).

You take it from here!

This is, of course, just the tip of the iceberg when it comes to what we can do with the slider now that it is in WebGL. For example,  common effects like turbulence and displacement can be added to the images in WebGL. The core concept of a displacement effect is to move pixels around based on a gradient lightmap that we use as an input source. We can use this texture (that I pulled from this displacement demo by Jesper Landberg — you should give him a follow) as our source and then plug it into our shader. 

To learn more about creating textures like these, see this article, this tweet, and this tool. I am not aware of any existing repositories of images like these, but if you know of one please, let me know.

If we hook up the texture above and animate the displacement power and intensity so that they vary over time and based on our drag velocity, then it will create a nice semi-random, but natural-looking displacement effect:

It’s also worth noting that Curtains has its own React version if that’s how you like to roll.

That’s all I’ve got for now. If you create something using what you’ve learned from this article, I’d love to see it! Connect with me via Twitter.


The post Creating WebGL Effects with CurtainsJS appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

An Overview of Scroll Technologies

Scroll-related animations have been used on the web for years. In recent years, they’ve started to become more common, perhaps in part due to devices being higher-performing and thus able to handle more animation. 

There are a number of scroll related technologies out there, so this article’s aim is to provide an overview of them and tools to help choose the one that’s right for you. I’d argue that these technologies can be broken down into two broad categories: ones for specific scroll-related behaviors and ones for more generic scroll-related behaviors.

Technologies for specific scroll-related behaviors

There are a few simple native CSS scroll effects that are supported by modern browsers. In some limited use cases they can be sufficient for your scroll animation needs.

position: sticky;

If all you need is for an element to stay in the same place on scroll for a portion of the page, using position: sticky is a good option. It’s straightforward and built into modern browsers. That said, a polyfill is required for IE support and some mobile browsers. For a solid overview, check out this article by Preethi.

CSS parallax

This isn’t a technology as much as a technique, but it’s pretty handy for simple parallax effects where you want different pieces of the page to move at different speeds on scroll. There’s a good write up of the technique on Alligator.io and a bunch of examples on CodePen, like this Firewatch header. The biggest downside for me is that it’s difficult to understand what values to use to set the perspective and transforms in order to get the parallax effect exactly right.

CSS scroll snap points

Scroll snap points allow the browser to snap to particular scroll positions that you set after a user is done with their normal scrolling. This can be helpful for keeping certain elements in view. However, the API is still in flux so be careful to use the most up to date API and be careful about relying on it in production. This CSS-Tricks article by Max Kohler is a good place to learn about it right now.

Smooth scrolling

Smooth scrolling is supported natively when jumping from section to section within a page using window.scrollTo() in JavaScript or even the scroll-behavior property in CSS. Generic smooth scrolling that smooths out mouse wheel actions is not supported natively in all browsers at this time. There are various JavaScript libraries that attempt to add smooth scroll support for the mousewheel action, but I’ve yet to find one that is bug-free and plays nicely with all other scroll technologies. Plus, smooth scrolling isn’t always good in the first place.

Technologies for generic scroll behaviors

Currently, there is no way to create or fire generic animations based on the scroll position using just CSS (though there is a proposal that could support some form of generic scroll based animations in CSS in the distant future) or to scrub through part of an animation. As such, if you want to animate elements on scroll, you’ll need to use at least some JavaScript to create the effect you want. There are two broad methods of using JavaScript to fire animations on scroll: using intersection observers and using the scroll event.

IntersectionObserver

Intersection observers are great if all you need for your animation is information related to whether or not and how much of an element is visible in the viewport. This makes them a good option for reveal animations. Even then, some things are difficult (though not impossible) using intersection observers, such as firing different animations depending on the direction an element enters the viewport. Intersection observers also aren’t very helpful if you want to do any scroll animations when an element is in between and not overlapping with the start and end points. 

Using the scroll event

Using the scroll event will give you the most freedom in controlling animations on scroll. It allows you to affect an element on scroll no matter where it is in terms of the viewport and set up starting and ending points exactly as you need for your project. 

With that being said, it can also be intense on performance if it isn’t throttled correctly and doesn’t have a convenient API to create particular behaviors. This is why it’s oftentimes helpful to use a good scrolling library that can handle the throttling for you and give you a more handy API to work with. Some can even handle a lot of the resizing issues for you!

Tools to create generic scroll behaviors

There are a few holistic scrolling libraries that attempt to give you full control over animations on scroll without you having to perform all of the calculations yourself. 

ScrollMagic

ScrollMagic provides a relatively simple API to create various effects on scroll and can be hooked into different animation libraries like GSAP and Velocity.js. However, it has become less maintained over the past few years, which lead to the creation of ScrollScene.

ScrollScene

ScrollScene is essentially a wrapper to try and make ScrollMagic and/or the intersection observer more usable. It uses a custom, more maintained version of ScrollMagic and adds additional features like video playback, scene init breakpoints, and scene duration breakpoints. It also makes use of GSAP

ScrollTrigger

ScrollTrigger is an official GreenSock plugin for GSAP. It has a long list of features and has the most easy to use API of any scroll library (at least to me). Using it, you can have complete control to define where your scroll animations start and end, animate anything (WebGL, canvas, SVG, DOM, whatever) on scroll, pin elements in place while the animation is running, and more. Plus it has the support of GreenSock and the GreenSock forums

Notable mention: Locomotive Scroll

While it doesn’t attempt to be as comprehensive of a scrolling library as the other libraries mentioned above, Locomotive Scroll is focused on providing custom smooth scrolling. You can also animate certain properties of DOM objects by adding data attributes or hook into the onscroll event to animate other types of objects. 

In summary

For some particular scroll animation effects, like sticky positioning and parallax, CSS technologies may be sufficient, at least when using a polyfill for browsers that don’t support those properties.

I generally recommend using GSAP’s ScrollTrigger because it can do everything that CSS properties can do, plus much more. ScrollTrigger will handle the browser support and calculations so that you can focus on animating!

Here’s a table covering which tools you can use to create particular effects:

position: stickyCSS parallaxCSS scroll snap pointsSmooth ScrollingIntersection observersScrollMagicScrollSceneLocomotive ScrollScrollTrigger
Pinning⚪️
Parallax effects
Scrubbing through animation with easing⚪️⚪️⚪️⚪️⚪️
Snaps scroll position⚪️⚪️⚪️⚪️
Dynamic Batching / Staggering
Supports horizontal scroll effects

Here’s a table comparing various other aspects of scroll technology:

position: stickyCSS parallaxCSS scroll snap pointsSmooth scrollingIntersection observersScrollMagicScrollSceneLocomotive ScrollScrollTrigger
Usable in production (good browser support)⚪️⚪️⚪️⚪️⚪️
Complete freedom in animation⚪️
Maintainedn/an/an/an/an/a
Works with DOM, Canvas, WebGl, SVG⚪️
Works easily with resizing⚪️
Restricts animation to relevant section⚪️⚪️
Directionally aware⚪️⚪️
Native technology
✅ = Yes
⚪️ = Partial support
❌ = No

The post An Overview of Scroll Technologies appeared first on CSS-Tricks.

Tips for Writing Animation Code Efficiently

I’ve been coding web animations and helping others do the same for years now. However, I have yet to see a concise list of tips focused on how to efficiently build animations, so here you go!

I will be using the GreenSock Animation Platform (GSAP). It provides a simple, readable API and solves cross-browser inconsistencies so that you can focus on animating. The code and concepts should be understandable even if you’ve never used GSAP. If you’d like to familiarize yourself with the basics of GSAP first so that you can get the most out of this article, the best place to begin is GSAP’s getting started page (includes a video). 

Tip #1: Use an animation library

Some developers think that using an animation library is wasteful because they can just use native browser technologies like CSS transitions, CSS animations or the Web Animations API (WAAPI) to accomplish the same thing without loading a library. In some cases, that’s true. However, here are a few other factors to consider:

  • Browser bugs, inconsistencies, and compatibility: An animation library, like GSAP, solves these for you and is universally compatible. You can even use motion paths in IE9! There are many problematic areas when it comes to cross-browser issues, including handling transform-origin on SVG elements, path stroke measurements, 3D origins in Safari, and many more that we don’t have the space to list.
  • Animation workflow: Building even moderately complex animations is much faster and more fun with a tool like GSAP. You can modularize animations, nest them as deeply as you want, and have their timing adjusted automatically. This makes it so much easier to experiment. Trust me: once you try building an animation sequence in CSS and then in GSAP, you’ll see what I mean. Night and day! Future edits are faster too.
  • Animate beyond the DOM: Canvas, WebGL, generic objects, and complex strings can’t be animated with native technologies. Using one consistent tool for all your animations is much cleaner. 
  • Runtime control: Using a good animation library can enable you to pause, resume, reverse, seek through, or even gradually change the speed of an entire animation sequence. You can control each transform component independently (rotation, scale, x, y, skew, etc.). You can also retrieve those values at any time as well. JavaScript animations give you ultimate flexibility.
  • Easing options (bounce, elastic, etc.): CSS only gives you two control points for eases. GSAP’s CustomEase lets you literally create any ease you can imagine. 
  • Lag smoothing: GSAP can prioritize absolute timing or adjust things on the fly to avoid jumps if the CPU gets bogged down.
  • Advanced capabilities: Using GSAP, it’s easy to morph SVGs, add physics/inertia, edit motion paths directly in the browser, use position-aware staggers, and more.

Most of the top animators in the industry use a tool like GSAP because they’ve learned these same things over the years. Once you get beyond very basic animations, a JavaScript library will make your life much, much easier and open up entirely new possibilities. 

Tip #2: Use timelines

A good animation library will provide some way of creating individual animations (called tweens) and a way to sequence animations in a timeline. Think of a timeline like a container for your tweens where you position them in relation to one another. 

const tl = gsap.timeline(); 
tl.to(".box", { duration: 1, x: 100 })
  .to(".box", { duration: 1, backgroundColor: "#f38630" }, "+=0.5") 
  .to(".box", { duration: 1, x: 0, rotation: -360 }, "+=0.5")

By default in GSAP, tweens added to a timeline will wait for the previous tweens to complete before running. The +=0.5 adds an additional offset or delay of a half-second as well, so the second tween will start 0.5 seconds after the first tween finishes no matter how long the first tween’s duration is.

To increase the amount of time between the tween to 1 second, all you need to do is change the +=0.5 to +=1! Super easy. With this approach, you can iterate on your animations quickly without worrying about doing the math to combine previous durations.

Tip #3: Use relative values

By “relative values” I mean three things:

  1. Animate values relative to their current value. GSAP recognizes += and -= prefixes for this. So x: "+=200" will add 200 units (usually pixels) to the current x. And x: "-=200" will subtract 200 from the current value. This is also useful in GSAP’s position parameter when positioning tweens relative to one another.
  2. Use relative units (like vw, vh and, in some cases, %) when values need to be responsive to viewport size changes.
  3. Use methods like .to() and .from() (instead of .fromTo()) whenever possible so that the start or end values are dynamically populated from their current values. That way, you don’t need to declare start and end values in every tween. Yay, less typing! For example, if you had a bunch of differently-colored elements, you could animate them all to black like gsap.to(".class", {backgroundColor: "black" }).

Tip #4: Use keyframes

If you find yourself animating the same target over and over in a row, that’s a perfect time to use keyframes! You do so like this:

gsap.to(".box", { keyframes: [
  { duration: 1, x: 100 },
  { duration: 1, backgroundColor: "#f38630", delay: 0.5 }, 
  { duration: 1, x: 0, rotation: -360, delay: 0.5 }
]});

No timeline necessary! To space out the tweens we just use the delay property in each keyframe. (It can be negative to create overlaps.)

Tip #5: Use smart defaults

GSAP has default values for properties like ease ("power1.out") and duration (0.5 seconds). So, the following is a valid tween that will animate for half a second.

gsap.to(".box", { color: "black" })

To change GSAP’s global defaults, use gsap.defaults():

// Use a linear ease and a duration of 1 instead
gsap.defaults({ ease: "none", duration: 1 });

This can be handy, but it’s more common to set defaults for a particular timeline so that it affects only its children. For example, we can avoid typing duration: 1 for each of the sub-tweens by setting a default on the parent timeline:

const tl = gsap.timeline({ defaults: { duration: 1 } }); 
tl.to(".box", { x: 100 })
  .to(".box", { backgroundColor: "#f38630" }, "+=0.5") 
  .to(".box", { x: 0, rotation: -360 }, "+=0.5")

Tip #6: Animate multiple elements at once

We mentioned this briefly in the third tip, but it deserves its own tip.

If you have multiple elements that share the same class of .box, the code above will animate all of the elements at the same time!

You can also select multiple elements with different selectors by using a more complex selector string:

gsap.to(".box, .circle", { ... });

Or you can pass an array of variable references as long as the elements are of the same type (selector string, variable reference, generic object, etc.):

var box = document.querySelector(".box");
var circle = document.querySelector(".circle");

// some time later…
gsap.to([box, circle], { ... });

Tip #7: Use function-based values, staggers, and/or loops

Function-based values

Use a function instead of a number/string for almost any property, and GSAP will call that function once for each target when it first renders the tween. Plus, it’ll use whatever gets returned by the function as the property value! This can be really handy for creating a bunch of different animations using a single tween and for adding variance.

GSAP will pass the following parameters into the function:

  1. The index
  2. The specific element being affected
  3. An array of all of the elements affected by the tween

For example, you could set the movement direction based on the index:

Or you could choose items from an array:

Staggers

Make your animations look more dynamic and interesting by offsetting the start times with a stagger. For simple staggered offsets in a single tween, just use stagger: 0.2 to add 0.2 seconds between the start time of each animation.

You can also pass in an object to get more complex stagger effects, including ones that emanate outward from the center of a grid or randomize the timings:

For more information about GSAP’s staggers, check out the stagger documentation.

Loops

It can be helpful to loop through a list of elements to create or apply animations, particularly when they are based on some event, like a user’s interaction (which I’ll discuss later on). 

To loop through a list of items, it’s easiest to use .forEach(). But since this isn’t supported on elements selected with .querySelectorAll() in IE, you can use GSAP’s utils.toArray() function instead.

In the example below, we are looping through each container to add animations to its children that are scoped to that container.

Tip #8: Modularize your animations

Modularization is one of the key principles of programming. It allows you to build small, easy-to-understand chunks that you can combine into larger creations while still keeping things clean, reusable, and easy to modify. It also lets you to use parameters and function scope, increasing the re-usability of your code.

Functions

Use functions to return tweens or timelines and then insert those into a master timeline:

function doAnimation() {
  // do something, like calculations, maybe using arguments passed into the function
  
  // return a tween, maybe using the calculations above
  return gsap.to(".myElem", { duration: 1, color: "red"});
}

tl.add( doAnimation() );

Nesting timelines can truly revolutionize the way you animate. It lets you sequence really complex animations with ease while keeping your code modular and readable.

function doAnimation() {
  const tl = gsap.timeline();
  tl.to(...);
  tl.to(...);
  // ...as many animations as you’d like!

  // When you’re all done, return the timeline
  return tl;
}

const master = gsap.timeline();
master.add( doAnimation() );
master.add( doAnotherAnimation() );
// Add even more timelines!

Here’s a real-world use case modified from Carl Schooff’s “Writing Smarter Animation Code” post.

Here’s a more complex demo showing the same technique using a Star Wars theme by Craig Roblewsky:

Wrapping your animation-building routines inside functions also makes recreating animations (say, on resize) a breeze!

var tl; // keep an accessible reference to our timeline

function buildAnimation() {
  var time = tl ? tl.time() : 0; // save the old time if it exists

  // kill off the old timeline if it exists
  if (tl) {
    tl.kill();
  }

  // create a new timeline
  tl = gsap.timeline();
  tl.to(...)
    .to(...); // do your animation
  tl.time(time); // set the playhead to match where it was
}

buildAnimation(); //kick things off

window.addEventListener("resize", buildAnimation); // handle resize

Effects

With effects, you can turn a custom animation into a named effect that can be called anytime with new targets and configurations. This is especially helpful when you have standards for your animations or if you are going to be calling the same animation from different contexts.

Here’s a super-simple “fade” effect to show the concept:

// register the effect with GSAP:
gsap.registerEffect({
    name: "fade",
    defaults: {duration: 2}, //defaults get applied to the "config" object passed to the effect below
    effect: (targets, config) => {
        return gsap.to(targets, {duration: config.duration, opacity:0});
    }
});

// now we can use it like this:
gsap.effects.fade(".box");
// Or override the defaults:
gsap.effects.fade(".box", {duration: 1});

Tip #9: Use control methods

GSAP provides many methods to control the state of a tween or timeline. They include .play(), .pause(), .reverse(), .progress(), .seek(), .restart(), .timeScale(), and several others. 

Using control methods can make transitions between animations more fluid (such as being able to reverse part way through) and more performant (by reusing the same tween/timeline instead of creating new instances each time). And by giving you finer control over the state of the animation, it can help with debugging as well.

Here’s a simple example:

One amazing use case is tweening the timeScale of a timeline!

Use case: interaction events that trigger animations

Inside of event listeners for user interaction events, we can use control methods to have fine control over our animation’s play state.

In the example below, we are creating a timeline for each element (so that it doesn’t fire the same animation on all instances), attaching a reference for that timeline to the element itself, and then playing the relevant timeline when the element is hovered, reversing it when the mouse leaves.

Use case: Animating between multiple states of a timeline

You may want a set of animations to affect the same properties of the same elements, but only in certain sequences (e.g. active/inactive states, each with mouseover/mouseout states). It may get tricky to manage. We can simplify it by using states of a timeline and control events. 

Use case: Animating based on the scroll position

We can easily fire animations based on the scroll position by using control methods. For example, this demo plays a full animation once a scroll position has been reached:

You can also attach the progress of an animation to the scroll position for more fancy scroll effects!

But if you’re going to do this, it’s best to throttle the scroll listener for performance reasons:

Hot tip: GreenSock is working on a plugin to make scroll-based animations even easier! You’re in for quite a treat. Keep your eyes peeled for news.

Bonus tip: Use GSAP’s plugins, utility methods, and helper functions

GSAP plugins add extra capabilities to GSAP’s core. Some plugins make it easier to work with rendering libraries, like PixiJS or EaselJS, while other plugins add superpowers like morphing SVG, drag and drop functionality, etc. This keeps the GSAP core relatively small and lets you add features when you need them.

Plugins

MorphSVG morphs between any two SVG shapes, no matter the number of points, and gives you fine control over how the shapes are morphed.

DrawSVG progressively reveals (or hides) the stroke of an SVG element, making it look like it’s being drawn. It works around various browser bugs that affect typical stroke-dashoffset animations.

MotionPath animates anything (SVG, DOM, canvas, generic objects, whatever) along a motion path in any browser. You can even edit the path in-browser using MotionPathHelper!

GSDevTools gives you a visual UI for interacting with and debugging GSAP animations, complete with advanced playback controls, keyboard shortcuts, global synchronization and more.

Draggable provides a surprisingly simple way to make virtually any DOM element draggable, spinnable, tossable, or even flick-scrollable using mouse or touch events. Draggable integrates beautifully (and optionally) with InertiaPlugin so the user can flick and have the motion decelerate smoothly based on momentum.

CustomEase (along with CustomBounce and CustomWiggle) add to GSAP’s already extensive easing capabilities by enabling you to register any ease that you’d like.

SplitText is an easy to use JavaScript utility that allows you to split HTML text into characters, words and lines. It’s easy to use, extremely flexible, works all the way back to IE9, and handles special characters for you.

ScrambleText scrambles the text in a DOM element with randomized characters, refreshing new randomized characters at regular intervals, while gradually revealing your new text (or the original) over the course of the tween. Visually, it looks like a computer decoding a string of text.

Physics2D lets you tween the position of elements based on velocity and acceleration as opposed to going to specific values. PhysicsProps is similar but works with any property, not just 2D coordinates.

Utility methods

GSAP has built-in utility methods that can make some common tasks easier. Most are focused on manipulating values in a particular way, which can be especially helpful when generating or modifying animation values. The ones that I use most often are .wrap(), .random, .interpolate(), .distribute(), .pipe(), and .unitize(), but there are many others you might find helpful.

Helper functions

In a similar light, but not built into GSAP’s core, are some helper functions GreenSock has created over the years to deal with specific use cases. These functions make it easy to FLIP your animations, return a random number based on an ease curve, blend two ease curves, and much more. I highly recommend checking them out!

Conclusion

You’ve made it to the end! Hopefully, you’ve learned a thing or two along the way and this article will continue to be a resource for you in the years to come.

As always, if you have any questions about GSAP, feel free to drop by the GreenSock forums. They’re incredibly helpful and welcoming! As an employee of GreenSock, that’s where I hang out often; I love helping people with animation-related challenges!

The post Tips for Writing Animation Code Efficiently appeared first on CSS-Tricks.

Animate a Blob of Text with SVG and Text Clipping

I came across this neat little animation in a designer newsletter — unfortunately, I lost track of the source, so please give a shout out if you recognize it! In it, a block of text appears to bleed into view with a swirl of colors, then goes out the same way it came in. It’s a slick effect and one I wanted to recreate in code.

The approach I took was to use SVG text as a clip path for an SVG background. We must use SVG text because CSS currently only allows us to animate the background with text clipping if the background is a GIF, which we’re not going to do.

Our first task is to create a background of different SVG shapes. Oval-y blobs work pretty well. The thing to make sure of is to match the size the artboard/canvas in whatever illustration app you’re using to the same dimension as the viewBox you want the final work to be. (In Inkscape, this option is under the Scale section of Document Properties.)

The goal is to cover most of the artboard with a variety of shapes. I've found that it looks best if some of the shapes overlap.

Next, we’ll create some text in a <clipPath>, group the objects that make up the background inside a <g> element, and apply a CSS clip-path on that group. Altogether it looks something like this:

<svg viewbox="0 0 700 225">
  
  <clipPath id="textClip" class="filled-heading">
    <text y="70">We are</text>
    <text y="140">Creators</text>
    <text y="210">+Innovators</text>
  </clipPath>

  
  <g id="background" clip-path="url(#textClip)">
    <path d="m449.78..." />
    
  </g>
</svg>

At this point, all we get is some plain text because we haven’t gotten around to the background animation quite yet.

So what about that animation? We can use a relatively simple CSS animation like this:

/* Animate the background shapes */
#background path {
  animation: pulse 4s cubic-bezier(0.455, 0.030, 0.515, 0.955) infinite;

  /* Necessary to keep the SVG objects in place while scaling */
  transform-origin: 50% 50%;
  transform-box: fill-box;
}

@keyframes pulse {
  /* Rotating it along with the scale makes it a little bit more fancy */
  0%, 100% { transform: scale(0) rotate(33deg); }
  35%, 65% { transform: scale(1) rotate(0deg); }
}

So far, so good.

transform-box: fill-box; is not supported in Internet Explorer or Edge at this point, so if you need to support those browsers, you’ll need to use a JavaScript workaround, like this one.

See the Pen
Animated blob SVG text clipping effect - Pt. 1
by Zach Saucier (@Zeaklous)
on CodePen.

We could start painting things in by hard-coding color values using a text or vector editor, but it's more fun to color the shapes dynamically. Something like this:

// Define an array of colors
const colors = ['#f5a147','#51cad8','#112b39'];
// Select the SVG paths
var blobs = document.querySelectorAll("path");

// Randomly apply colors to the SVG fill property
blobs.forEach(blob => {
  blob.style.fill = colors[Math.floor(Math.random() * colors.length)];
});

In order to change the text values for each iteration, we need to first add them to the SVG clip path.

<clipPath id="text" class="filled-heading">
  <text y="70">We are</text>
  <text y="140">Creators</text>
  <text y="210">+Innovators</text>
  
  <text y="70">We are</text>
  <text y="140">Movers</text>
  <text y="210">+Shakers</text>
  
  <text y="70">We are</text>
  <text y="140">Stylish</text>
  <text y="210">+Techy</text>
</clipPath>

Then we can either use CSS or JavaScript to reveal the lines of text in our preferred order. Unfortunately, we can't surround each section of <text> using a <g> element because <g> elements don't work inside of a clipPath. For this post, we’re going to split things up into three CSS animations, one for each group of three paths:

/* Selects paths 1-3 */
#textClip text:nth-of-type(n + 1):nth-of-type(-n + 3) {
  animation: showFirst 12s infinite;
}

/* Selects paths 4-6 */
#textClip text:nth-of-type(n + 4):nth-of-type(-n + 6) {
  animation: showSecond 12s infinite;
}

/* Selects paths 7-9 */
#textClip text:nth-of-type(n + 7):nth-of-type(-n + 9) {
  animation: showThird 12s infinite;
}

@keyframes showFirst {
  0%, 33% {
    opacity: 1;
  }
  33.0001%, 100% { opacity: 0; }
}

@keyframes showSecond {
  33.0001%, 66% {
    opacity: 1;
  }
  0%, 33%, 66.0001%, 100% { opacity: 0; }
}

@keyframes showThird {
  66.0001%, 99.999% {
    opacity: 1;
  }
  0%, 66%, 100% { opacity: 0; }
}

That does the trick!

See the Pen
Animated blob SVG text clipping effect - Pt. 2
by Zach Saucier (@Zeaklous)
on CodePen.

At this point, we can have a little fun. For example, we can swap backgrounds for a different effect. I used Inkscape's star tool with three to four points to generate some random shapes (using Inkscape’s random parameter) and then colored them using a palette from one of the many color scheme generators (I used Palx) to produce this version:

See the Pen
Animated blob SVG text clipping effect - Pt. 3
by Zach Saucier (@Zeaklous)
on CodePen.

The backgrounds don't even need to fill up the entire background, depending on the effect that we want to create. For example, we could duplicate the text using a element and fill in the text using that as seen in this demo.

Or we could mix it up by rotating the background blobs like this:

See the Pen
Animated blob SVG text clipping effect - Pt. 5
by Zach Saucier (@Zeaklous)
on CodePen.

To make the colors change for every new set of words, we could use either a CSS or JavaScript for the animation. I used JavaScript (and moved the CSS animation that was hiding the text lines to the JavaScript):

See the Pen
Animated blob SVG text clipping effect - Pt. 6
by Zach Saucier (@Zeaklous)
on CodePen.

To center the text horizontally, add x="50%" text-anchor="middle" to each <text> element (Demo). Centering it vertically would take more manual calculation since we’re working with a multi-line format.

One of the nice things about this approach is that, since it uses SVG, it is responsive by default!

P.S. After I made this approach and was looking for the original GIF author, I came across another recreation by Martí Fenosa doing the same effect using a different approach. Check his demo out as well because it’s clever!

The post Animate a Blob of Text with SVG and Text Clipping appeared first on CSS-Tricks.