How I Made an Icon System Out of CSS Custom Properties

SVG is the best format for icons on a website, there is no doubt about that. It allows you to have sharp icons no matter the screen pixel density, you can change the styles of the SVG on hover and you can even animate the icons with CSS or JavaScript.

There are many ways to include an SVG on a page and each technique has its own advantages and disadvantages. For the last couple of years, I have been using a Sass function to import directly my icons in my CSS and avoid having to mess up my HTML markup.

I have a Sass list with all the source codes of my icons. Each icon is then encoded into a data URI with a Sass function and stored in a custom property on the root of the page.

TL;DR

What I have for you here is a Sass function that creates a SVG icon library directly in your CSS.

The SVG source code is compiled with the Sass function that encodes them in data URI and then stores the icons in CSS custom properties. You can then use any icon anywhere in your CSS like as if it was an external image.

This is an example pulled straight from the code of my personal site:

.c-filters__summary h2:after {
  content: var(--svg-down-arrow);
  position: relative;
  top: 2px;
  margin-left: auto;
  animation: closeSummary .25s ease-out;
}

Demo

Sass structure

/* All the icons source codes */
$svg-icons: (
  burger: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0...'
);

/* Sass function to encode the icons */
@function svg($name) {
  @return url('data:image/svg+xml, #{$encodedSVG} ');
}

/* Store each icon into a custom property */
:root {
  @each $name, $code in $svg-icons {
    --svg-#{$name}: #{svg($name)};
  }
}

/* Append a burger icon in my button */
.menu::after {
  content: var(--svg-burger);
}		

This technique has both pros and cons, so please take them into account before implementing this solution on your project:

Pros

  • There are no HTTP requests for the SVG files.
  • All of the icons are stored in one place.
  • If you need to update an icon, you don’t have to go over each HTML templates file.
  • The icons are cached along with your CSS.
  • You can manually edit the source code of the icons.
  • It does not pollute your HTML by adding extra markup.
  • You can still change the color or some aspect of the icon with CSS.

Cons

  • You cannot animate or update a specific part of the SVG with CSS.
  • The more icons you have, the heavier your CSS compiled file will be.

I mostly use this technique for icons rather than logos or illustrations. An encoded SVG is always going to be heavier than its original file, so I still load my complex SVG with an external file either with an <img> tag or in my CSS with url(path/to/file.svg).

Encoding SVG into data URI

Encoding your SVG as data URIs is not new. In fact Chris Coyier wrote a post about it over 10 years ago to explain how to use this technique and why you should (or should not) use it.

There are two ways to use an SVG in your CSS with data URI:

  • As an external image (using background-image,border-image,list-style-image,…)
  • As the content of a pseudo element (e.g. ::before or ::after)

Here is a basic example showing how you how to use those two methods:

The main issue with this particular implementation is that you have to convert the SVG manually every time you need a new icon and it is not really pleasant to have this long string of unreadable code in your CSS.

This is where Sass comes to the rescue!

Using a Sass function

By using Sass, we can make our life simpler by copying the source code of our SVG directly in our codebase, letting Sass encode them properly to avoid any browser error.

This solution is mostly inspired by an existing function developed by Threespot Media and available in their repository.

Here are the four steps of this technique:

  • Create a variable with all your SVG icons listed.
  • List all the characters that needs to be skipped for a data URI.
  • Implement a function to encode the SVGs to a data URI format.
  • Use your function in your code.

1. Icons list

/**
* Add all the icons of your project in this Sass list
*/
$svg-icons: (
  burger: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24.8 18.92" width="24.8" height="18.92"><path d="M23.8,9.46H1m22.8,8.46H1M23.8,1H1" fill="none" stroke="#000" stroke-linecap="round" stroke-width="2"/></svg>'
);

2. List of escaped characters

/**
* Characters to escape from SVGs
* This list allows you to have inline CSS in your SVG code as well
*/
$fs-escape-chars: (
  ' ': '%20',
  '\'': '%22',
  '"': '%27',
  '#': '%23',
  '/': '%2F',
  ':': '%3A',
  '(': '%28',
  ')': '%29',
  '%': '%25',
  '<': '%3C',
  '>': '%3E',
  '\\': '%5C',
  '^': '%5E',
  '{': '%7B',
  '|': '%7C',
  '}': '%7D',
);

3. Encode function

/**
* You can call this function by using `svg(nameOfTheSVG)`
*/
@function svg($name) {
  // Check if icon exists
  @if not map-has-key($svg-icons, $name) {
    @error 'icon “#{$name}” does not exists in $svg-icons map';
    @return false;
  }

  // Get icon data
  $icon-map: map-get($svg-icons, $name);

  $escaped-string: '';
  $unquote-icon: unquote($icon-map);
  // Loop through each character in string
  @for $i from 1 through str-length($unquote-icon) {
    $char: str-slice($unquote-icon, $i, $i);

    // Check if character is in symbol map
    $char-lookup: map-get($fs-escape-chars, $char);

    // If it is, use escaped version
    @if $char-lookup != null {
        $char: $char-lookup;
    }

    // Append character to escaped string
    $escaped-string: $escaped-string + $char;
  }

  // Return inline SVG data
  @return url('data:image/svg+xml, #{$escaped-string} ');
}		

4. Add an SVG in your page

button {
  &::after {
    /* Import inline SVG */
    content: svg(burger);
  }
}

If you have followed those steps, Sass should compile your code properly and output the following:

button::after {
  content: url("data:image/svg+xml, %3Csvg%20xmlns=%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox=%270%200%2024.8%2018.92%27%20width=%2724.8%27%20height=%2718.92%27%3E%3Cpath%20d=%27M23.8,9.46H1m22.8,8.46H1M23.8,1H1%27%20fill=%27none%27%20stroke=%27%23000%27%20stroke-linecap=%27round%27%20stroke-width=%272%27%2F%3E%3C%2Fsvg%3E ");
}		

Custom properties

The now-implemented Sass svg() function works great. But its biggest flaw is that an icon that is needed in multiple places in your code will be duplicated and could increase your compiled CSS file weight by a lot!

To avoid this, we can store all our icons into CSS variables and use a reference to the variable instead of outputting the encoded URI every time.

We will keep the same code we had before, but this time we will first output all the icons from the Sass list into the root of our webpage:

/**
  * Convert all icons into custom properties
  * They will be available to any HTML tag since they are attached to the :root
  */

:root {
  @each $name, $code in $svg-icons {
    --svg-#{$name}: #{svg($name)};
  }
}

Now, instead of calling the svg() function every time we need an icon, we have to use the variable that was created with the --svg prefix.

button::after {
  /* Import inline SVG */
  content: var(--svg-burger);
}

Optimizing your SVGs

This technique does not provide any optimization on the source code of the SVG you are using. Make sure that you don’t leave unnecessary code; otherwise they will be encoded as well and will increase your CSS file size.

You can check this great list of tools and information on how to optimize properly your SVG. My favorite tool is Jake Archibald’s SVGOMG — simply drag your file in there and copy the outputted code.

Bonus: Updating the icon on hover

With this technique, we cannot select with CSS specific parts of the SVG. For example, there is no way to change the fill color of the icon when the user hovers the button. But there are a few tricks we can use with CSS to still be able to modify the look of our icon.

For example, if you have a black icon and you want to have it white on hover, you can use the invert() CSS filter. We can also play with the hue-rotate() filter.

That’s it!

I hope you find this little helper function handy in your own projects. Let me know what you think of the approach — I’d be interested to know how you’d make this better or tackle it differently!


How I Made an Icon System Out of CSS Custom Properties originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Animate Anything Along an SVG Path

SVG is a very neat format to display any illustration, icon or logo on a website. Furthermore, they can be animated in CSS or JavaScript to make them more attractive. But SVG can also be used for their data only, without the visual! Let me explain…

An SVG is a vector image format, which means it is not made of coloured pixels, but math functions that, once interpreted, can be rendered on screen. Since the browser must convert the file from functions to actual pixels, it also let us access a wide variety of methods to either manipulate, or retrieve data from the math.

In today’s article, we will explore the function getPointAtLength() and see how we can use the data of an SVG path for creative use cases such as the demo below.

See the Pen Random dots along path – CodePen Challenge by Louis Hoebregts (@Mamboleoo) on CodePen.

⚠ If you are not familiar with SVG, this CSS-Tricks article is a good start!

The method getPointAtLength()

If we take a look at the MDN documentation page about the method it says:
The SVGGeometryElement.getPointAtLength() method returns the point at a given distance along the path.

The method will give us the coordinates of a point that is precisely along the path at a specific distance that we send as a parameter.

For example path.getPointAtLength(10) will return an SVGPoint (an object) with x & y coordinates.

Since we need to give the distance of our point, it means we will most likely need to know how long is our path. Luckily, the SVG API has a method getTotalLength() available to any SVGGeometryElement that returns the total length of our element!

⚠ The SVGGeometryElement variable refers to all SVG tags that are created from a geometry (path, rect, circle,…) so this does not include image, filter, clip-path,… tags.

Let’s see how we can then use this function to animate a circle along a given path using the GreenSock library.

To do so, we need a JavaScript object that will contain the animated values (as gsap cannot animate number variables directly) and set a property distance to zero.
We then create a tween that will update the distance value from 0 to the total length of our path.
Finally on each frame, we retrieve a point along the path based on the animated distance value, and we update the cx and cy attributes of our circle to make it move ✨

// Create an object that gsap can animate
const val = { distance: 0 };
// Create a tween
gsap.to(val, {
  // Animate from distance 0 to the total distance
  distance: path.getTotalLength(),
  // Function call on each frame of the animation
  onUpdate: () => {
    // Query a point at the new distance value
    const point = path.getPointAtLength(val.distance);
    // Update the circle coordinates
    circle.setAttribute('cx', point.x);
    circle.setAttribute('cy', point.y);
  }
});

See the Pen Animate single element along path by Louis Hoebregts (@Mamboleoo) on CodePen.

⚠ If the effect you want to achieve is just animating one element along an SVG path such as in the demo above, you could check the MotionPathPlugin by GreenSock. It will let you animate easily any DOM element from a path you provide. (plus it’s free!)

Using the points coordinates for particles

I love particles, it’s no breaking news. Which is why, when I learn a new technique I always try to implement something with them!
Let’s see how instead of a single circle moving along a path, we could make many more circles exploding like a bomb fuse 💣

The overall logic of this animation is exactly the same as before, except that on each frame we will create a new circle element and animate it. As you can see, the setup is very similar.

const svg = document.querySelector('svg');
const fuse = svg.querySelector('.fuse');

const val = { distance: 0 };
gsap.to(val, {
  distance: fuse.getTotalLength(),
  repeat: -1,
  duration: 5,
  onUpdate: () => {
    // Query a point at the new distance value
    const point = fuse.getPointAtLength(val.distance);
    // Create a new particle
    createParticle(point);
  }
});

The createParticle function will be called on each frame to make a new particle pop and fade out. Here are the steps of the animation:

  1. Create a new circle element and append it to the SVG
  2. Set the coordinates from the point we calculated with getPointAtLength
  3. Define a random radius and color for each
  4. Animate that particle cx & cy attributes to a random position
  5. Once the animation is complete, remove the particle from the DOM
function createParticle (point) {
  // Create a new circle element
  const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
  // Prepend the element to the SVG
  svg.prepend(circle);
  // Set the coordinates of that circle
  circle.setAttribute('cx', point.x);
  circle.setAttribute('cy', point.y);
  // Define a random radius for each circle
  circle.setAttribute('r', (Math.random() * 2) + 0.2);
  // Define a random color
  circle.setAttribute('fill', gsap.utils.random(['#ff0000', '#ff5a00', '#ff9a00', '#ffce00', '#ffe808']));
  
  // Animate the circle
  gsap.to(circle, {
    // Random cx based on its current position
    cx: '+=random(-20,20)',
    // Random cy based on its current position
    cy: '+=random(-20,20)',
    // Fade out
    opacity: 0,
    // Random duration for each circle
    duration: 'random(1, 2)',
    // Prevent gsap from rounding the cx & cy values
    autoRound: false,
    // Once the animation is complete
    onComplete: () => {
      // Remove the SVG element from its parent
      svg.removeChild(circle);
    }
  });
}

See the Pen Bomb fuse particles by Louis Hoebregts (@Mamboleoo) on CodePen.

⚠ To make the animation prettier I’m also animating the stroke-dashoffset of the fuse to make it more realistic. You can check this article for more details on such animation.

Using the coordinates in WebGL

So far we have only animated SVG elements next to the path. But sometimes all we need are the raw coordinates from the path, but not the path itself. For example, if we want to animate particles in a 2D Canvas or in WebGL like in the animation below.

See the Pen Make it POP! by Louis Hoebregts (@Mamboleoo) on CodePen.

⚠ The following chapter expects you to know how to setup and run a three.js scene.

Here are the key concepts of this animation:

  1. Get the path and its total length
  2. Loop along the path until you reach its length
  3. Get the point that exists on the index distance
  4. On each iteration, create a Vector3 at the point’s coordinates
  5. Push the vector into an array of vertices
  6. Create a geometry from the vertices
  7. Create a Points mesh and add it into your scene
// Get the Path in the DOM
const path = document.querySelector("path");
// Store the total length of the path
const length = path.getTotalLength();

// Empty array to store all vertices
const vertices = [];
// Loop along the path
for (let i = 0; i < length; i += 0.2) {
  // Get the coordinates of a point based on the index value
  const point = path.getPointAtLength(i);
  // Create a new vector at the coordinates
  const vector = new THREE.Vector3(point.x, -point.y, 0);
  // Randomize a little bit the point to make the heart fluffier
  vector.x += (Math.random() - 0.5) * 30;
  vector.y += (Math.random() - 0.5) * 30;
  vector.z += (Math.random() - 0.5) * 70;
  // Push the vector into the array
  vertices.push(vector);
}
// Create a new geometry from the vertices
const geometry = new THREE.BufferGeometry().setFromPoints(vertices);
// Define a pink material
const material = new THREE.PointsMaterial( { color: 0xee5282, blending: THREE.AdditiveBlending, size: 3 } );
// Create a Points mesh based on the geometry and material
const particles = new THREE.Points(geometry, material);
// Offset the particles in the scene based on the viewbox values
particles.position.x -= 600 / 2;
particles.position.y += 552 / 2;
// Add the particles in to the scene
scene.add(particles);

See the Pen Untitled by Louis Hoebregts (@Mamboleoo) on CodePen.

Creating a heart of particles is fun, but animating those particles is even FUNNIER 🥳

First we setup a global timeline so that all tweens will be grouped together and we’ll be able to repeat all of them once the last animation is complete.

// Create a global gsap timeline that contains all tweens
const tl = gsap.timeline({
  repeat: -1,
  yoyo: true
});

While creating the vector, we attach a gsap tween on it so that it animates from the center of the heart. We can calculate the x & y coordinates for the start based on the SVG viewBox attributes viewBox="0 0 600 552".
Since y axis are in the other direction in SVG, we apply a negative value. (y is going up in WebGL, while in CSS & SVG it’s going down).

Each vector’s animation will have a delay that is calculated from its own distance along the path so that will create a nice flow of particles going round.

for (let i = 0; i < length; i += 0.1) {
  [...]
  // Create a tween for that vector
  tl.from(vector, {
      x: 600 / 2, // Center X of the heart
      y: -552 / 2, // Center Y of the heart
      z: 0, // Center of the scene
      ease: "power2.inOut",
      duration: "random(2, 5)" // Random duration
    },
    i * 0.002 // Delay calculated from the distance along the path
  );
}

Finally, we need to update the geometry of the Points mesh on each frame as the Vector3 objects are being animated but the geometry is not aware of that.

function render() {
  requestAnimationFrame(render);
  // Update the geometry from the animated vertices
  geometry.setFromPoints(vertices);
}

And voilà 💖

See the Pen Create word from SVG Path – WebGL by Louis Hoebregts (@Mamboleoo) on CodePen.

What’s next?

You tell me! Now that you can extract the coordinates of points along an SVG path, try to apply those data anywhere else 🚀
What about animating lines instead of particles? Or create a new path based on random points that you pick along another one?

Explore this technique and share your results with me on Twitter, I can’t wait to see what you’ll come up with 💙

See you another time,
Mamboleoo

The post Animate Anything Along an SVG Path appeared first on Codrops.

Surface Sampling in Three.js

One day I got lost in the Three.js documentation and I came across something called “MeshSurfaceSampler“. After reading the little information on the page, I opened the provided demo and was blown away!

What exactly does this class do? In short, it’s a tool you attach to a Mesh (any 3D object) then you can call it at any time to get a random point along the surface of your object.

The function works in two steps:

  1. Pick a random face from the geometry
  2. Pick a random point on that face

In this tutorial we will see how you can get started with the MeshSurfaceSampler class and explore some nice effects we can build with it.

💡 If you are the kind of person who wants to dig right away with the demos, please do! I’ve added comments in each CodePen to help you understand the process.

⚠ This tutorial assumes basic familiarity with Three.js

Creating a scene

The first step in (almost) any WebGL project is to first setup a basic scene with a cube.
In this step I will not go into much detail, you can check the comments in the code if needed.

We are aiming to render a scene with a wireframe cube that spins. This way we know our setup is ready.

⚠ Don’t forget to also load OrbitControls as it is not included in Three.js package.

// Create an empty scene, needed for the renderer
const scene = new THREE.Scene();
// Create a camera and translate it
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(1, 1, 2);

// Create a WebGL renderer and enable the antialias effect
const renderer = new THREE.WebGLRenderer({ antialias: true });
// Define the size and append the <canvas> in our document
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

// Add OrbitControls to allow the user to move in the scene
const controls = new THREE.OrbitControls(camera, renderer.domElement);

// Create a cube with basic geometry & material
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshBasicMaterial({
  color: 0x66ccff,
  wireframe: true
});
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);

/// Render the scene on each frame
function render () {  
  // Rotate the cube a little on each frame
  cube.rotation.y += 0.01;
  
  renderer.render(scene, camera);
}
renderer.setAnimationLoop(render);

See the Pen by Louis Hoebregts (@Mamboleoo) on CodePen.

Creating a sampler

For this step we will create a new sampler and use it to generate 300 spheres on the surface of our cube.

💡 Note that MeshSurfaceSampler is not built-in with Three.js. You can find it in the official repository, in the ‘examples’ folder.

Once you have added the file in your imported scripts, we can initiate a sampler for our cube.

const sampler = new THREE.MeshSurfaceSampler(cube).build();

This needs to be done only once in our code. If you want to get random coordinates on multiple meshes, you will need to store a new sampler for each object.

Because we will be displaying hundreds of the same geometry, we can use the InstancedMesh class to achieve better performance. Juste like a regular Mesh, we define the geometry (SphereGeometry for the demo) and a material (MeshBasicMaterial). After to have those two, you can pass them to a new InstancedMesh and define how many objects you need (300 in this case).

const sphereGeometry = new THREE.SphereGeometry(0.05, 6, 6);
const sphereMaterial = new THREE.MeshBasicMaterial({
 color: 0xffa0e6
});
const spheres = new THREE.InstancedMesh(sphereGeometry, sphereMaterial, 300);
scene.add(spheres);	

Now that our sampler is ready to be used, we can create a loop to define a random position and scale for each of our spheres.

Before we loop, we need two dummy variables for this step:

  • tempPosition is a 3D Vector that our sampler will update with the random coordinates
  • tempObject is a 3D Object used to define the position and scale of a sphere and generate a matrix from it

Inside the loop, we start by sampling a random point on the surface of our cube and store it into tempPosition.
Those coordinates are then applied to our tempObject.
We also define a random scale for the dummy object so that not every sphere will look the same.
Because we need the Matrix of the dummy object, we ask Three.js to update it.
Finally we add the updated Matrix of the object into our InstancedMesh’s own Matrix at the index of the sphere we want to move.

const tempPosition = new THREE.Vector3();
const tempObject = new THREE.Object3D();
for (let i = 0; i < 300; i++) {
  sampler.sample(tempPosition);
  tempObject.position.set(tempPosition.x, tempPosition.y, tempPosition.z);
  tempObject.scale.setScalar(Math.random() * 0.5 + 0.5);
  tempObject.updateMatrix();
  spheres.setMatrixAt(i, tempObject.matrix);
}	

See the Pen #1 Surface Sampling by Louis Hoebregts (@Mamboleoo) on CodePen.

Amazing isn’t it? With only a few steps we already have a working scene with random meshes along a surface.

Phew, let’s just take a breath before we move to more creative demos ✨

Playing with particles

Because everybody loves particles (I know you do), let’s see how we can generate thousands of them to create the feeling of volume only from tiny dots. For this demo, we will be using a Torus knot instead of a cube.

This demo will work with a very similar logic as for the spheres before:

  • Sample 15000 coordinates and store them in an array
  • Create a geometry from the coordinates and a material for Points
  • Combine the geometry and material into a Points object
  • Add them to the scene
/* Sample the coordinates */
const vertices = [];
const tempPosition = new THREE.Vector3();
for (let i = 0; i < 15000; i ++) {
  sampler.sample(tempPosition);
  vertices.push(tempPosition.x, tempPosition.y, tempPosition.z);
}

/* Create a geometry from the coordinates */
const pointsGeometry = new THREE.BufferGeometry();
pointsGeometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));

/* Create a material */
const pointsMaterial = new THREE.PointsMaterial({
  color: 0xff61d5,
  size: 0.03
});
/* Create a Points object */
const points = new THREE.Points(pointsGeometry, pointsMaterial);

/* Add the points into the scene */
scene.add(points);		

Here is the result, a 3D Torus knot only made from particles ✨
Try adding more particles or play with another geometry!

See the Pen #3 Surface Sampling by Louis Hoebregts (@Mamboleoo) on CodePen.

💡 If you check the code of the demo, you will notice that I don’t add the torus knot into the scene anymore. MeshSurfaceSampler requires a Mesh, but it doesn’t even have to be rendered in your scene!

Using a 3D Model

So far we have only been playing with native geometries from Three.js. It was a good start but we can take a step further by using our code with a 3D model!

There are many websites that provide free or paid models online. For this demo I will use this elephant from poly.pizza.

See the Pen #4 Surface Sampling by Louis Hoebregts (@Mamboleoo) on CodePen.

#1 Loading the .obj file

Three.js doesn’t have built-in loaders for OBJ models but there are many loaders available on the official repository.

Once the file is loaded, we will update its material with wireframe activated and reduce the opacity so we can see easily through.

/* Create global variable we will need for later */
let elephant = null;
let sampler = null;
/* Load the .obj file */
new THREE.OBJLoader().load(
  "path/to/the/model.obj",
  (obj) => {
    /* The loaded object with my file being a group, I need to pick its first child */
    elephant = obj.children[0];
    /* Update the material of the object */
    elephant.material = new THREE.MeshBasicMaterial({
      wireframe: true,
      color: 0x000000,
      transparent: true,
      opacity: 0.05
    });
    /* Add the elephant in the scene */
    scene.add(obj);
    
    /* Create a surface sampler from the loaded model */
    sampler = new THREE.MeshSurfaceSampler(elephant).build();

    /* Start the rendering loop */ 
    renderer.setAnimationLoop(render);
  }
);	

#2 Setup the Points object

Before sampling points along our elephant we need to setup a Points object to store all our points.

This is very similar to what we did in the previous demo, except that this time we will define a custom color for each point. We are also using a texture of a circle to make our particles rounded instead of the default square.

/* Used to store each particle coordinates & color */
const vertices = [];
const colors = [];
/* The geometry of the points */
const sparklesGeometry = new THREE.BufferGeometry();
/* The material of the points */
const sparklesMaterial = new THREE.PointsMaterial({
  size: 3,
  alphaTest: 0.2,
  map: new THREE.TextureLoader().load("path/to/texture.png"),
  vertexColors: true // Let Three.js knows that each point has a different color
});
/* Create a Points object */
const points = new THREE.Points(sparklesGeometry, sparklesMaterial);
/* Add the points into the scene */
scene.add(points);	

#3 Sample a point on each frame

It is time to generate the particles on our model! But you know what? It works the same way as on a native geometry 😍

Since you already know how to do that, you can check the code below and notice the differences:

  • On each frame, we add a new point
  • Once the point is sampled, we update the position attribute of the geometry
  • We pick a color from an array of colors and add it to the color attribute of the geometry
/* Define the colors we want */
const palette = [new THREE.Color("#FAAD80"), new THREE.Color("#FF6767"), new THREE.Color("#FF3D68"), new THREE.Color("#A73489")];
/* Vector to sample a random point */
const tempPosition = new THREE.Vector3();

function addPoint() {
  /* Sample a new point */
  sampler.sample(tempPosition);
  /* Push the point coordinates */
  vertices.push(tempPosition.x, tempPosition.y, tempPosition.z);
  /* Update the position attribute with the new coordinates */
  sparklesGeometry.setAttribute("position", new THREE.Float32BufferAttribute(vertices, 3)  );
  
  /* Get a random color from the palette */
  const color = palette[Math.floor(Math.random() * palette.length)];
  /* Push the picked color */
  colors.push(color.r, color.g, color.b);
  /* Update the color attribute with the new colors */
  sparklesGeometry.setAttribute("color", new THREE.Float32BufferAttribute(colors, 3));
}

function render(a) {
  /* If there are less than 10,000 points, add a new one*/
  if (vertices.length < 30000) {
    addPoint();
  }
  renderer.render(scene, camera);
}		

Animate a growing path

A cool effect we can create using the MeshSurfaceSampler class is to create a line that will randomly grow along the surface of our mesh. Here are the steps to generate the effect:

  1. Create an array to store the coordinates of the vertices of the line
  2. Pick a random point on the surface to start and add it to your array
  3. Pick another random point and check its distance from the previous point
    1. If the distance is short enough, go to step 4
    2. If the distance is too far, repeat step 3 until you find a point close enough
  4.  Add the coordinates of the new point in the array
  5. Update the line geometry and render it
  6. Repeat steps 3-5 to make the line grow on each frame

The key here is the step 3 where we will pick random points until we find one that is close enough. This way we won’t have two points across the mesh. This could work for a simple object (like a sphere or a cube) as all the lines will stay inside the object. But think about our elephant, what if we have a point connected from the trunk to one of the back legs. You will end up with lines where there should be ’empty’ spaces.

Check the demo below to see the line coming to life!

See the Pen #5 Surface Sampling by Louis Hoebregts (@Mamboleoo) on CodePen.

For this animation, I’m creating a class Path as I find it a cleaner way if we want to create multiple lines. The first step is to setup the constructor of that Path. Similar to what we have done before, each path will require 4 properties:

  1. An array to store the vertices of the line
  2. The final geometry of the line
  3. A material specific for Line objects
  4. A Line object combining the geometry and the material
  5. The previous point Vector
/* Vector to sample the new point */
const tempPosition = new THREE.Vector3();
class Path {
  constructor () {
    /* The array with all the vertices of the line */
    this.vertices = [];
    /* The geometry of the line */
    this.geometry = new THREE.BufferGeometry();
    /* The material of the line */
    this.material = new THREE.LineBasicMaterial({color: 0x14b1ff});
    /* The Line object combining the geometry & the material */
    this.line = new THREE.Line(this.geometry, this.material);
    
    /* Sample the first point of the line */
    sampler.sample(tempPosition);
    /* Store the sampled point so we can use it to calculate the distance */
    this.previousPoint = tempPosition.clone();
  }
}		

The second step is to create a function we can call on each frame to add a new vertex at the end of our line. Within that function we will execute a loop to find the next point for the path.
When that next point is found, we can store it in the vertices array and in the previousPoint variable.
Finally, we need to update the line geometry with the updated vertices array.

class Path {
  constructor () {...}
  update () {
    /* Variable used to exit the while loop when we find a point */
    let pointFound = false;
    /* Loop while we haven't found a point */
    while (!pointFound) {
      /* Sample a random point */
      sampler.sample(tempPosition);
      /* If the new point is less 30 units from the previous point */
      if (tempPosition.distanceTo(this.previousPoint) < 30) {
        /* Add the new point in the vertices array */
        this.vertices.push(tempPosition.x, tempPosition.y, tempPosition.z);
        /* Store the new point vector */
        this.previousPoint = tempPosition.clone();
        /* Exit the loop */
        pointFound = true;
      }
    }
    /* Update the geometry */
    this.geometry.setAttribute("position", new THREE.Float32BufferAttribute(this.vertices, 3));
  }
}

function render() {
  /* Stop the progression once we have reached 10,000 points */
  if (path.vertices.length < 30000) {
    /* Make the line grow */
    path.update();
  }
  renderer.render(scene, camera);
}		

💡 The value of how short the distance between the previous point and the new one depends on your 3D model. If you have a very small object, that distance could be ‘1’, with the elephant model we are using ’30’.

Now what?

Now that you know how to use MeshSurfaceSampler with particles and lines, it is your turn to create funky demos with it!
What about animating multiple lines together or starting a line from each leg of the elephant, or even popping particles from each new point of the line. The sky is the limit ⛅

See the Pen #6 Surface Sampling by Louis Hoebregts (@Mamboleoo) on CodePen.

This article does not show all the available features from MeshSurfaceSampler. There is still the weight property that allows you to have more or less chance to have a point on some faces. When we sample a point, we could also use the normal or the color of that point for other creative ideas. This could be part of a future article one day… 😊

Until next time, I hope you learned something today and that you can’t wait to use that new knowledge!

If you have questions, let me know on Twitter.

The post Surface Sampling in Three.js appeared first on Codrops.

Perfect Tooltips With CSS Clipping and Masking

Clipping and masking have been around for a while now in CSS and even have pretty decent browser support. I recently worked on a project that needed to use a clipping technique for tooltips showing above links in text.

Those tooltips have two designs based on their content:

One design is a tooltip that contains plain text against a solid background.
The other design allows an image to cover the entire space.

You might not think the text tooltip requires any clipping at all. A pseudo-element can be positioned at the bottom to add the little notch, right? You are indeed absolutely right! Because the background of the tooltip is a a plain color, there’s really no need for CSS trickery and whatnot.

But clipping the image in the second design is where things get interesting…

Here’s the thought process my mind followed when I started the task.

Idea 1: clip-path & polygon

The CSS clip-path property allows us to define a custom polygon with percentage values to make the path we want.
This solution is often enough if the shape of your path is simple enough. In the demo below, I’m using calc() values to make sure the clip is fully responsive, while the little triangle stays the same size no matter how stretched the parent is.

.tooltip {
  clip-path: polygon(
    0% 0%, // Top left point
    100% 0%, // Top right point
    100% calc(100% - 10px), // Bottom right point
    calc(50% + 10px) calc(100% - 10px), // Center right of the triangle
    50% 100%, // Tip of the triangle
    calc(50% - 10px) calc(100% - 10px), // Center left of the triangle
    0% calc(100% - 10px) // Bottom left point
  );
}

This solution is very clean but, in my case, not good enough as I don’t have a straight triangle notch, but rather a custom shape.

Idea 2: clip-path and SVG

Using an SVG path seemed like a good solution. First, you export your SVG clipping path, then use it in your CSS with the url(#clipPathId) value.

Check the demo below. Do you see any issue with the path?

The arrow is stretched based on the image ratio. Since the little notch is part of the whole path shape, it is as stretched as the rectangle part of the path stretches in size.

Idea 3: mask-image

Now here is the thing I discovered with the CSS mask-image property in CSS: You can combine mask layers! Think about it like a background-image in CSS. You can apply multiple gradients or images on a single element. Now, what if you combine all those layers to generate the final mask you need?

This is exactly what we are going to do here with two layers:

  1. A large rectangle that cover the whole block except for a stripe at the bottom (shown in green)
  2. An image of the arrow (shown in pink)

With that solution, the rectangle can stretch according to our tooltip’s dimensions, and the arrow will always keep its fixed size.

All the code and demos below are prefix free and the demos are using Autoprefixer. At the time I’m writing this article, Edge, Chrome & Safari require prefixes.

Just as we would with background properties, we are going to use three different mask properties to define our two layers:

  • mask-image: This property lets us draw the rectangle with a linear background and the arrow with an inline SVG.
  • mask-position: The rectangle doesn’t need a position (as it starts from the top-left), but the arrow needs to be positioned at the center-bottom.
  • mask-repeat: We need to avoid repeating both layers; otherwise, the linear gradient would cover the whole element when it repeats.
.tooltip {
  mask-image:
    linear-gradient(#fff, #fff), /* Rectangle */
    url('data:image/svg+xml;utf8,'); /* Bottom arrow mask-position: */
    0 0, /* Rectangle */
    50% 100%; /* Bottom arrow */
  mask-size:
    100% calc(100% - 18px), /* Rectangle */
    38px 18px; /* Bottom arrow */
  mask-repeat: no-repeat;
}

Tada! Change the tooltip dimensions or replace the image and the bottom arrow will keep its original ratio.

More complex shapes

Let’s get a little fancy and go deeper with this technique. I was inspired by the iMessage app on iOS and tried to reproduce the same tooltips with this masking technique.

I had to draw more layers for my mask to render every rounded corner:

  • four circles, one for each corner (shown in red)
  • one horizontal rectangle (shown in blue)
  • one vertical rectangle (shown in green)
  • one SVG for the arrow (shown in yellow)

The full code is going to be a bit longer as we have more layers to draw, but the logic stays the same. The corners are drawn using four radial gradients. To fill the rectangle, we need two rectangles (one vertical, one horizontal) as shown above. And finally, our little arrow that is using an inline SVG.

.tooltip {
  --radius: 25px;
  mask-image:
    radial-gradient(#fff (var(--radius) - 1), #fff0 var(--radius)), /* Top left corner */
    radial-gradient(#fff (var(--radius) - 1), #fff0 var(--radius)), /* Top right corner */
    radial-gradient(#fff (var(--radius) - 1), #fff0 var(--radius)), /* Bottom left corner */
    radial-gradient(#fff (var(--radius) - 1), #fff0 var(--radius)), /* Bottom right corner */
    linear-gradient(#fff, #fff), /* Horizontal gradient */
    linear-gradient(#fff, #fff), /* Vertical gradient */
    url('data:image/svg+xml;utf8,'); /* Bottom right icon */
  mask-position: 
    0 0, /* Top left corner */
    100% 0, /* Top right corner */
    0 100%, /* Bottom left corner */
    100% 100%, /* Bottom right corner */
    0 var(--radius), /* Horizontal gradient */
    var(--radius) 0, /* Vertical gradient */
    100% 100%; /* Bottom right icon */
  mask-size:
    (var(--radius) * 2) (var(--radius) * 2),  /* Top left corner */
    (var(--radius) * 2) (var(--radius) * 2),  /* Top right corner */
    (var(--radius) * 2) (var(--radius) * 2),  /* Bottom left corner */
    (var(--radius) * 2) (var(--radius) * 2),  /* Bottom right corner */
    100% calc(100% - #{var(--radius) * 2}), /* Horizontal gradient */
    calc(100% - #{var(--radius) * 2}) 100%, /* Vertical gradient */
    (39px / 2) (25px / 2); /* Bottom right icon */
  mask-repeat: no-repeat;
}

As you see, we can create a version with the arrow on the left or right by using a flipped version of the arrow and positioning it in a different corner. The trick is working fine on tooltips without images too. But like I said at the beginning of this article, you probably don’t need that much CSS if you only have a plain background to style.


If you want to learn more about clipping and masking in CSS, there are lots of other great articles right here on CSS-Tricks worth checking out.


The post Perfect Tooltips With CSS Clipping and Masking appeared first on CSS-Tricks.

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

How to Animate the Details Element Using WAAPI

Animating accordions in JavaScript has been one of the most asked animations on websites. Fun fact: jQuery’s slideDown() function was already available in the first version in 2011.

In this article, we will see how you can animate the native <details> element using the Web Animations API.

HTML setup

First, let’s see how we are gonna structure the markup needed for this animation.

The <details> element needs a <summary> element. The summary is the content visible when the accordion is closed.
All the other elements within the <details> are part of the inner content of the accordion. To make it easier for us to animate that content, we are wrapping it inside a <div>.

<details>
  <summary>Summary of the accordion</summary>
  <div class="content">
    <p>
      Lorem, ipsum dolor sit amet consectetur adipisicing elit.
      Modi unde, ex rem voluptates autem aliquid veniam quis temporibus repudiandae illo, nostrum, pariatur quae!
      At animi modi dignissimos corrupti placeat voluptatum!
    </p>
  </div>
</details>

Accordion class

To make our code more reusable, we should make an Accordion class. By doing this we can call new Accordion() on every <details> element on the page.

class Accordion {
  // The default constructor for each accordion
  constructor() {}

  // Function called when user clicks on the summary
  onClick() {}

  // Function called to close the content with an animation
  shrink() {}

  // Function called to open the element after click
  open() {}

  // Function called to expand the content with an animation
  expand() {}

  // Callback when the shrink or expand animations are done
  onAnimationFinish() {}
}

Constructor()

The constructor is the place we save all the data needed per accordion.

constructor(el) {
  // Store the <details> element
  this.el = el;
  // Store the <summary> element
  this.summary = el.querySelector('summary');
  // Store the <div class="content"> element
  this.content = el.querySelector('.content');

  // Store the animation object (so we can cancel it, if needed)
  this.animation = null;
  // Store if the element is closing
  this.isClosing = false;
  // Store if the element is expanding
  this.isExpanding = false;
  // Detect user clicks on the summary element
  this.summary.addEventListener('click', (e) => this.onClick(e));
}

onClick()

In the onClick() function, you’ll notice we are checking if the element is being animated (closing or expanding). We need to do that in case users click on the accordion while it’s being animated. In case of fast clicks, we don’t want the accordion to jump from being fully open to fully closed.

The <details> element has an attribute, [open], applied to it by the browser when we open the element. We can get the value of that attribute by checking the open property of our element using this.el.open.

onClick(e) {
  // Stop default behaviour from the browser
  e.preventDefault();
  // Add an overflow on the <details> to avoid content overflowing
  this.el.style.overflow = 'hidden';
  // Check if the element is being closed or is already closed
  if (this.isClosing || !this.el.open) {
    this.open();
  // Check if the element is being openned or is already open
  } else if (this.isExpanding || this.el.open) {
    this.shrink();
  }
}

shrink()

This shrink function is using the WAAPI .animate() function. You can read more about it in the MDN docs. WAAPI is very similar to CSS @keyframes. We need to define the start and end keyframes of the animation. In this case, we only need two keyframes, the first one being the current height the element, and the second one is the height of the <details> element once it is closed. The current height is stored in the startHeight variable. The closed height is stored in the endHeight variable and is equal to the height of the <summary>.

shrink() {
  // Set the element as "being closed"
  this.isClosing = true;

  // Store the current height of the element
  const startHeight = `${this.el.offsetHeight}px`;
  // Calculate the height of the summary
  const endHeight = `${this.summary.offsetHeight}px`;

  // If there is already an animation running
  if (this.animation) {
    // Cancel the current animation
    this.animation.cancel();
  }

  // Start a WAAPI animation
  this.animation = this.el.animate({
    // Set the keyframes from the startHeight to endHeight
    height: [startHeight, endHeight]
  }, {
    // If the duration is too slow or fast, you can change it here
    duration: 400,
    // You can also change the ease of the animation
    easing: 'ease-out'
  });

  // When the animation is complete, call onAnimationFinish()
  this.animation.onfinish = () => this.onAnimationFinish(false);
  // If the animation is cancelled, isClosing variable is set to false
  this.animation.oncancel = () => this.isClosing = false;
}

open()

The open function is called when we want to expand the accordion. This function does not control the animation of the accordion yet. First, we calculate the height of the <details> element and we apply this height with inline styles on it. Once it’s done, we can set the open attribute on it to make the content visible but hiding as we have an overflow: hidden and a fixed height on the element. We then wait for the next frame to call the expand function and animate the element.

open() {
  // Apply a fixed height on the element
  this.el.style.height = `${this.el.offsetHeight}px`;
  // Force the [open] attribute on the details element
  this.el.open = true;
  // Wait for the next frame to call the expand function
  window.requestAnimationFrame(() => this.expand());
}

expand()

The expand function is similar to the shrink function, but instead of animating from the current height to the close height, we animate from the element’s height to the end height. That end height is equal to the height of the summary plus the height of the inner content.

expand() {
  // Set the element as "being expanding"
  this.isExpanding = true;
  // Get the current fixed height of the element
  const startHeight = `${this.el.offsetHeight}px`;
  // Calculate the open height of the element (summary height + content height)
  const endHeight = `${this.summary.offsetHeight + this.content.offsetHeight}px`;

  // If there is already an animation running
  if (this.animation) {
    // Cancel the current animation
    this.animation.cancel();
  }

  // Start a WAAPI animation
  this.animation = this.el.animate({
    // Set the keyframes from the startHeight to endHeight
    height: [startHeight, endHeight]
  }, {
    // If the duration is too slow of fast, you can change it here
    duration: 400,
    // You can also change the ease of the animation
    easing: 'ease-out'
  });
  // When the animation is complete, call onAnimationFinish()
  this.animation.onfinish = () => this.onAnimationFinish(true);
  // If the animation is cancelled, isExpanding variable is set to false
  this.animation.oncancel = () => this.isExpanding = false;
}

onAnimationFinish()

This function is called at the end of both the shrinking or expanding animation. As you can see, there is a parameter, [open], that is set to true when the accordion is open, allowing us to set the [open] HTML attribute on the element, as it is no longer handled by the browser.

onAnimationFinish(open) {
  // Set the open attribute based on the parameter
  this.el.open = open;
  // Clear the stored animation
  this.animation = null;
  // Reset isClosing & isExpanding
  this.isClosing = false;
  this.isExpanding = false;
  // Remove the overflow hidden and the fixed height
  this.el.style.height = this.el.style.overflow = '';
}

Setup the accordions

Phew, we are done with the biggest part of the code!

All that’s left is to use our Accordion class for every <details> element in the HTML. To do so, we are using a querySelectorAll on the <details> tag, and we create a new Accordion instance for each one.

document.querySelectorAll('details').forEach((el) => {
  new Accordion(el);
});

Notes

To make the calculations of the closed height and open height, we need to make sure that the <summary> and the content always have the same height.

For example, do not try to add a padding on the summary when it’s open because it could lead to jumps during the animation. Same goes for the inner content — it should have a fixed height and we should avoid having content that could change height during the opening animation.

Also, do not add a margin between the summary and the content as it will not be calculated for the heights keyframes. Instead, use a padding directly on the content to add some spacing.

The end

And voilà, we have a nice animated accordion in JavaScript without any library! 🌈



The post How to Animate the Details Element Using WAAPI appeared first on CSS-Tricks.

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

Playing With Particles Using the Web Animations API

When it comes to motion and animations, there is probably nothing I love more than particles. This is why every time I explore new technologies I always end up creating demos with as many particles as I can.

In this post, we'll make even more particle magic using the Web Animations API to create a firework effect when clicking on a button.

Browser support

At the time I'm writing this article, all major browsers — with the exception of Safari and Internet Explorer — at least partially support the Web Animations API. Safari support can be enabled in the "Experimental Features" developer menu.

This browser support data is from Caniuse, which has more detail. A number indicates that browser supports the feature at that version and up.

Desktop

ChromeFirefoxIEEdgeSafari
8376No80TP

Mobile / Tablet

Android ChromeAndroid FirefoxAndroidiOS Safari
80688013.3

If you're interested in reproducing the Twitter heart animation you could also have a look at this cool article by Ana Tudor which is another great example of exploding particles on a button.

HTML setup

We won't need much HTML for this demo. We will use a <button> element but it could be another type of tag element. We could even listen to any click on the page to make particles pop from anywhere if we really wanted to.

<button id="button">Click on me</button>

CSS setup

Since every particle has a few CSS properties in common, we can set them in the global CSS of the page. As you can create custom tag elements in HTML, I will use a <particle> tag name to avoid using semantic tags. But truth is, you could animate <p>, <i> or any tag of your choice.

particle {
  border-radius: 50%;
  left: 0;
  pointer-events: none;
  position: fixed;
  top: 0;
}

A couple thing to note here:

  • The particles should not interact with the layout of our page, so we're setting a fixed position with top and left at 0px each.
  • We're also removing pointer events to avoid any user interaction on the HTML particles while they are on the screen.

Because styling the button and the page layout is not really the purpose of this article I will leave that on the side.

JavaScript setup

Here are the six steps we will follow in our JavaScript:

  1. Listen to click event on the button
  2. Create 30 <particle> elements and append them into the <body>
  3. Set a random width, height and background for every particle
  4. Animate each particle from the mouse position to a random place as they fade out
  5. Remove the <particle> from the DOM when the animation is complete

Step 1: The click event

// We first check if the browser supports the Web Animations API
if (document.body.animate) {
  // If yes, we add a click listener on our button
  document.querySelector('#button').addEventListener('click', pop);
}

Step 2: The particles

// The pop() function is called on every click
function pop(e) { 
  // Loop to generate 30 particles at once
  for (let i = 0; i < 30; i++) {
    // We pass the mouse coordinates to the createParticle() function
    createParticle(e.clientX, e.clientY);
  }
}
function createParticle(x, y) {
  // Create a custom particle element
  const particle = document.createElement('particle');
  // Append the element into the body
  document.body.appendChild(particle);
}

Step 3: Particle width, height and background

function createParticle (x, y) {
  // [...]
  // Calculate a random size from 5px to 25px
  const size = Math.floor(Math.random() * 20 + 5);
  // Apply the size on each particle
  particle.style.width = `${size}px`;
  particle.style.height = `${size}px`;
  // Generate a random color in a blue/purple palette
  particle.style.background = `hsl(${Math.random() * 90 + 180}, 70%, 60%)`;
}

Step 4: Animate each particle

function createParticle (x, y) {
  // [...]
  // Generate a random x & y destination within a distance of 75px from the mouse
  const destinationX = x + (Math.random() - 0.5) * 2 * 75;
  const destinationY = y + (Math.random() - 0.5) * 2 * 75;

  // Store the animation in a variable because we will need it later
  const animation = particle.animate([
    {
      // Set the origin position of the particle
      // We offset the particle with half its size to center it around the mouse
      transform: `translate(${x - (size / 2)}px, ${y - (size / 2)}px)`,
      opacity: 1
    },
    {
      // We define the final coordinates as the second keyframe
      transform: `translate(${destinationX}px, ${destinationY}px)`,
      opacity: 0
    }
  ], {
    // Set a random duration from 500 to 1500ms
    duration: 500 + Math.random() * 1000,
    easing: 'cubic-bezier(0, .9, .57, 1)',
    // Delay every particle with a random value from 0ms to 200ms
    delay: Math.random() * 200
  });
}

Because we have a random delay, the particles waiting to start their animation are visible on the top-left of the screen. To prevent this, we can set a zero opacity on every particle in our global CSS.

particle {
  /* Same as before */
  opacity: 0;
}

Step 5: Remove particles after the animation completes

It is important to remove the particle elements from the DOM. Since we create 30 new elements on every click, the browser memory can fill up pretty quickly and cause things to get janky. Here's how we can do that:

function createParticle (x, y) {
  // Same as before
  // When the animation is finished, remove the element from the DOM
  animation.onfinish = () => {
    particle.remove();
  };
}

Final result

Putting everything together gives us what we're looking for: a colorful explosion of particle goodness.

Not seeing the animation in the demo? Check if your browser supports the Web Animations API. in the support table at the top of the post.

Be creative!

Because all this is using CSS, it's pretty simple to modify the particle styles. Here are five examples using various shapes... and even characters!


Or hey, we can even explode the button itself like Zach Saucier did in this post.

A button with a gradient exploding into particles

The post Playing With Particles Using the Web Animations API appeared first on CSS-Tricks.

Simulating Mouse Movement

If you've ever had to display an interactive animation during a live talk or a class, then you may know that it's not always easy to interact with your slides and while talking.

This happened to me when I needed to show this particles demo to my students. I didn't want to have to stay next to my computer to move my mouse in order to show off the demo.

See the Pen
Particles (on move)
by Louis Hoebregts (@Mamboleoo)
on CodePen.

If you do not interact with the iframe, you will see nothing but a blank space. As soon as you start moving your mouse or your finger, you can see the animation.

For that reason, I created the same demo but I used some extra code to simulate someone interacting with the demo.

See the Pen
Particles (fake)
by Louis Hoebregts (@Mamboleoo)
on CodePen.

Simplex noise

The trick here is to use an algorithm that will generate "smooth" random positions. If we use a classic random function, the fake mouse will be at a purely random position on every frame. What we want is to have a position on every frame that is directly linked to the previous one. Thankfully, there is a technique that does exactly what we need: Simplex noise (or more commonly known as Perlin noise).

Let's take a look at this image where the height of each column is defined with random values on top, and values from Simplex noise algorithm below.

You can quickly notice that the bottom graph seems much smoother because every column height is connected to the previous one. Those graphs are only showing one dimension (the x-axis, from left to right) but with Simplex noise you can get values in multiples dimensions. In our case, we will need two dimensions for the X and Y coordinates of the fake mouse we're simulating.

If you are more interested to know how Simplex noise works, check out the video "I.5: Perlin Noise - The Nature of Code" by Daniel Shiffman

Get noise coordinates

The first thing we need to make our demo work is to implement a script that generates noise. In my case, I'm using this script by Seph.

Once the noise script is loaded, we can start using it on every frame to make our mouse move.

I will be using an image of a mouse for the demos that I put on position: fixed; with a class .mouse, but you could animate anything else for your own projects.

So, let's take a look at the code:

// We retrieve the image from the DOM
const el = document.querySelector('.mouse');

// The render function is called on every frame
function render (a) {
  // The a variable is the amount of milliseconds since we started our script
  
  // Get a noise value based on the elapsed time to get a new value on every frame
  // This noise algorithm is returning values between [-1, 1] so we need to map them to [0, 1] by adding one to the value and dividing it by 2
  const noiseX = (noise.simplex2(0, a*0.0005) + 1) / 2;
  // We get another noise value for the y axis but because we don't want the same value than x, we need to use another value for the first parameter
  const noiseY = (noise.simplex2(1, a*0.0005) + 1) / 2;
  
  // Convert the noise values from [0, 1] to the size of the window
  const x = noiseX * window.innerWidth;
  const y = noiseY * window.innerHeight;
  
  // Apply the x & y coordinates on our element
  el.style.transform = `translate(${x}px, ${y}px)`;
  
  // Call the render function once the browser is ready to make it an infinite loop
  requestAnimationFrame(render);
}

// Ask the browser to call render to start our animation
requestAnimationFrame(render);

Here is the result we get with the above script:

See the Pen
Virtual user 1
by Louis Hoebregts (@Mamboleoo)
on CodePen.

Allow interactivity

With the current code, we are not allowed to interact with our demo anymore. Let's add a bit more code to use our real mouse position when we interact with the demo and switch back to a fake mouse as soon as we stop.

const el = document.querySelector('.mouse');
let lastMove = 0;

// When the mouse is being moved
function onMouseMove (e) {
  // Get the x and y coordinates
  x = e.clientX;
  y = e.clientY;
  
  // Save the last move time
  lastMove = Date.now();
}

// Update the mouse position based on x & y
function updateMouse (x, y) {
  el.style.transform = `translate(${x}px, ${y}px)`;
}

function render (a) {
  // Check if last move was more than 500ms ago
  if (Date.now() - lastMove > 500) {
    // Generate a fake mouse position
    ...
    updateMouse(x, y);
  }
}

// Listen to mouse events
window.addEventListener('mousemove', onMouseMove);

Now, if you move your mouse, the fake mouse will follow yours. If you stop moving for 500ms, the fake mouse will start moving again.

See the Pen
Virtual user 3
by Louis Hoebregts (@Mamboleoo)
on CodePen.

Customized movement

The speed of the mouse can be updated by changing the value of the third parameter. So far, we are setting this value by taking the elapsed time multiplied by 0.0005, which is equal to a/2000.

// Define a speed ratio
const speed = a * 0.0005;
// Use the speed
const noiseX = (noise.simplex3(1, 0, speed) + 1) / 2;

We can also add a bit more randomness in the changes of direction by adding more noise from its position.

let random = 0;
function render (a) {
  ...
  // Update the random value
  random += 0.1;
  // Compute a x random offset based on the window width
  const randX = noise.simplex3(1, 0, random) * window.innerWidth * 0.1;
  // Compute a y random offset based on the window height
  const randY = noise.simplex3(3, 0, random) * window.innerHeight * 0.1;
  
  // Define the x & y values based on (noise * screen) + randomness
  const x = noiseX * innerWidth + randX;
  const y = noiseY * innerHeight + randY;
  ...
}

Play with the inputs to see how speed and randomly calculated values can influence the fake mouse movement.

See the Pen
Virtual user 4
by Louis Hoebregts (@Mamboleoo)
on CodePen.

More mice

Now that we have created one fake mouse, why not create 500 of them?

See the Pen
Virtual user 5
by Louis Hoebregts (@Mamboleoo)
on CodePen.

I now use this trick for almost all my demos. I think it's really cool to be able to display a project without using a video or being forced to keep moving the mouse randomly while trying to talk about the demo.

If you have any questions or remarks, please leave a comment below or ping me on Twitter.

The post Simulating Mouse Movement appeared first on CSS-Tricks.