The creative coder’s dream is to rule pixels on their screen. To arrange them in beautiful patterns and do whatever you want with them. Well, this is exactly what we are going to do with this demo. Let’s distort and rule pixels with the power of our mouse cursor, just like the developers of the amazing Infinite Bad Guy website did!
Setup
The scene is the usual, we just create a fullscreen image on a screen, so it preserves the aspect ratio, and has its “background-size: cover” applied through the glsl shader. In the end, we have a geometry stretched for the whole viewport, and a little shader like this:
The whole thing just shows the image, no distortions yet.
The Magnificent Data Texture
I hope by this time you know that any texture in WebGL is basically just numbers corresponding to each pixel’s color.
Three.js has a specific API to create your own textures pixel by pixel. It is called, no surprise, DataTexture. So let’s create another texture for our demo, with random numbers:
const size = rows * columns;
const data = new Float32Array(3 * size);
for(let i = 0; i < size; i++) {
const stride = i * 3;
let r = Math.random() * 255 ;
let r1 = Math.random() * 255 ;
data[stride] = r; // red, and also X
data[stride + 1] = r1; // green, and also Y
data[stride + 2] = 0; // blue
}
this.texture = new THREE.DataTexture(data, width, height, THREE.RGBFormat, THREE.FloatType);
This is heavily based on the default example from the documentation. The only difference is, we are using FloatType texture, so we are not bound to only integer numbers. One of the interesting things is, that numbers should be between 0 and 255, even though, in the GLSL it will be 0..1 range anyway. You should just keep that in mind, so you are using correct number ranges.
What is also an interesting idea, is that GLSL doesn’t really care what the numbers mean in your data structures. It could be both color.rgb, and color.xyz. And that’s precisely what we will use here, we don’t care about exact color of this texture, we will use it as a distortion for our demo! Just as a nice data structure for GLSL.
But, just to understand better, this is what the texture will look like when you want to preview it:
You see those big rectangles because i picked something like 25×35 DataTexture size, which is really low-res. Also, it has colors because im using two different random numbers for XY(Red-Green) variables, which results in this.
So now, we could already use this texture as a distortion in our fragment shader:
vec4 color = texture2D(uTexture,newUV);
vec4 offset = texture2D(uDataTexture,vUv);
// we are distorting UVs with new texture values
gl_FragColor = texture2D(uTexture,newUV - 0.02*offset.rg);
The Mouse and its power
So now, let’s make it dynamic! We will need a couple of things. First, we need the mouse position and speed. And also, the mouse radius, meaning, at what distance would the mouse distort our image.
A short explanation: On each step of the animation, I will loop through my grid cells aka pixels of DataTexture. And assign some values based on mouse position and speed. Second, im going to relax the distortion. This needs to be done, if the user stops moving mouse, the distortion should come to 0.
So, now the code looks like this, simplified a bit, for better understanding the concept:
let data = DataTexture.image.data;
// loop through all the pixels of DataTexture
for (let i = 0; i < rows; i++) {
for (let j = 0; j < cols; j++) {
// get distance between mouse, and current DataTexture pixel
let distance = distanceBetween(mouse, [i,j])
if (distance < maxDistance) {
let index = 3 * (i + this.size * j); // get the pixel coordinate on screen
data[index] = this.mouse.vX ; // mouse speed
data[index + 1] = this.mouse.vY ; // mouse speed
}
}
// slowly move system towards 0 distortion
for (let i = 0; i < data.length; i += 3) {
data[i] *= 0.9
data[i + 1] *= 0.9
}
DataTexture.needsUpdate = true;
A couple of things are added to make it look better, but the concept is here. If you ever worked with particle systems, this is exactly that concept, except our particles never move, we just change some values of the particles (distortion inside each big pixel).
Result
I left the settings open in the last demo, so you can play with parameters and come up with your own unique feel of the animation. Let me know what it inspired you to create!
In this ALL YOUR HTML coding session you will learn how to recreate the interesting ripple effect seen on the homunculus.jp website with Three.js. We’ll have a look at render targets and use a little bit of math.
This coding session was streamed live on November 21, 2021.
Today I’d like to show you how to code a special cursor effect. Custom cursors have been very popular and there’s so many creative possibilities that can enhance a certain design and an interaction logic.
Let’s have a look how to create a fullscreen crosshair cursor in SVG and how to distort the cursors’s lines with an SVG filter when hovering over links. We’ll also add a nice hover animation for some menu items using Splitting.js.
The Markup
For the SVG cursor we want a singe SVG for each line that has enough of space to get the distortion effect applied to it:
First, we need to hide our cursor by default, and we just want to show it if the user has a pointing device, like a mouse. So we add a media query with the any-pointer media feature:
Let’s create our custom cursor. So we have two SVGs, one for each line. As we saw in the markup earlier, each one of the SVGs will include a filter that we’ll apply to the respective line when hovering over a menu link.
Let’s start coding the entry JavaScript file (index.js):
import { Cursor } from './cursor';
// initialize custom cursor
const cursor = new Cursor(document.querySelector('.cursor'));
// mouse effects on all links
[...document.querySelectorAll('a')].forEach(link => {
link.addEventListener('mouseenter', () => cursor.enter());
link.addEventListener('mouseleave', () => cursor.leave());
});
Now, let’s create a class for the cursor (in cursor.js):
import { gsap } from 'gsap';
import { getMousePos } from './utils';
// Track the mouse position and update it on mouse move
let mouse = {x: 0, y: 0};
window.addEventListener('mousemove', ev => mouse = getMousePos(ev));
export class Cursor {
constructor(el) {
}
// hovering over a link
enter() {
}
// hovering out a link
leave() {
}
// create the turbulence animation timeline on the cursor line elements
createNoiseTimeline() {
}
// render styles and loop
render() {
// ...
requestAnimationFrame(() => this.render());
}
}
What we do here is to update the mouse position as we move the mouse around. For that, we use the getMousePos function (in utils.js).
Let’s move on to the next interesting part:
...
constructor(el) {
// main DOM element which includes the 2 svgs, each for each line
this.DOM = {el: el};
// both horizontal and vertical lines
this.DOM.lines = this.DOM.el.children;
[this.DOM.lineHorizontal, this.DOM.lineVertical] = this.DOM.lines;
// hide initially
gsap.set(this.DOM.lines, {opacity: 0});
...
}
...
We initialize the line DOM elements and hide them initially.
We want to update the lines transform (translation values) as we move the mouse. For that, let’s create an object that stores the translation state:
...
constructor(el) {
...
// style properties that will change as we move the mouse (translation)
this.renderedStyles = {
tx: {previous: 0, current: 0, amt: 0.15},
ty: {previous: 0, current: 0, amt: 0.15}
};
...
}
...
With interpolation, we can achieve a smooth animation effect when moving the mouse. The “previous” and “current” values are the values we’ll be interpolating. The current value of one of these “animatable” properties will be one between these two values at a specific increment. The value of “amt” is the amount to interpolate. As an example, the following formula calculates our current translationX value:
...
constructor(el) {
...
// svg filters (ids)
this.filterId = {
x: '#filter-noise-x',
y: '#filter-noise-y'
};
// the feTurbulence elements per filter
this.DOM.feTurbulence = {
x: document.querySelector(`${this.filterId.x} > feTurbulence`),
y: document.querySelector(`${this.filterId.y} > feTurbulence`)
}
// turbulence current value
this.primitiveValues = {turbulence: 0};
// create the gsap timeline that will animate the turbulence value
this.createNoiseTimeline();
}
...
Next, we initialize the filter ids, the feTurbulence elements for each SVG filter (one per line) and also the current turbulence value. Then we create the GSAP timeline that will take care of updating the baseFrequency of each filter with the current turbulence value. Here’s how that timeline and the methods that start/stop it look like:
...
createNoiseTimeline() {
// turbulence value animation timeline:
this.tl = gsap.timeline({
paused: true,
onStart: () => {
// apply the filters for each line element
this.DOM.lineHorizontal.style.filter = `url(${this.filterId.x}`;
this.DOM.lineVertical.style.filter = `url(${this.filterId.y}`;
},
onUpdate: () => {
// set the baseFrequency attribute for each line with the current turbulence value
this.DOM.feTurbulence.x.setAttribute('baseFrequency', this.primitiveValues.turbulence);
this.DOM.feTurbulence.y.setAttribute('baseFrequency', this.primitiveValues.turbulence);
},
onComplete: () => {
// remove the filters once the animation completes
this.DOM.lineHorizontal.style.filter = this.DOM.lineVertical.style.filter = 'none';
}
})
.to(this.primitiveValues, {
duration: 0.5,
ease: 'power1',
// turbulence start value
startAt: {turbulence: 1},
// animate to 0
turbulence: 0
});
}
enter() {
// start the turbulence timeline
this.tl.restart();
}
leave() {
// stop the turbulence timeline
this.tl.progress(1).kill();
}
...
The animation will change the turbulence value (which starts at 1 and ends at 0) and apply it to each feTurbulence’s baseFrequency attribute. The filters get applied to the lines in the beginning and removed once the animation is completed.
To finalize the constructor method we fade in the lines and start updating the translation values the first time we move the mouse:
...
constructor(el) {
...
import { lerp, getMousePos } from './utils';
...
// on first mousemove fade in the lines and start the requestAnimationFrame rendering function
this.onMouseMoveEv = () => {
this.renderedStyles.tx.previous = this.renderedStyles.tx.current = mouse.x;
this.renderedStyles.ty.previous = this.renderedStyles.ty.previous = mouse.y;
gsap.to(this.DOM.lines, {duration: 0.9, ease: 'Power3.easeOut', opacity: 1});
requestAnimationFrame(() => this.render());
window.removeEventListener('mousemove', this.onMouseMoveEv);
};
window.addEventListener('mousemove', this.onMouseMoveEv);
}
...
Now we’re only missing the actual method that updates the lines’ translation values as we move the mouse:
...
render() {
// update the current translation values
this.renderedStyles['tx'].current = mouse.x;
this.renderedStyles['ty'].current = mouse.y;
// use linear interpolation to delay the translation animation
for (const key in this.renderedStyles ) {
this.renderedStyles[key].previous = lerp(this.renderedStyles[key].previous, this.renderedStyles[key].current, this.renderedStyles[key].amt);
}
// set the new values
gsap.set(this.DOM.lineVertical, {x: this.renderedStyles['tx'].previous});
gsap.set(this.DOM.lineHorizontal, {y: this.renderedStyles['ty'].previous});
// loop this until the end of time
requestAnimationFrame(() => this.render());
}
As an extra, let’s add a little glitch effect to the menu items texts when we hover over them. We’ll use the Splitting library to split the menu texts into spans/chars so we can animate each one individually. We want to change the translation values of each character and also it’s color. Let’s update our index.js file:
import { Cursor } from './cursor';
import { MenuItem } from './menuItem';
// Splitting (used to split the menu item texts to spans/characters)
import 'splitting/dist/splitting.css';
import 'splitting/dist/splitting-cells.css';
import Splitting from 'splitting';
// initialize Splitting
const splitting = Splitting();
// initialize custom cursor
const cursor = new Cursor(document.querySelector('.cursor'));
// Menu Items
[...document.querySelectorAll('.menu > a')].forEach(el => new MenuItem(el));
// mouse effects on all links
[...document.querySelectorAll('a')].forEach(link => {
link.addEventListener('mouseenter', () => cursor.enter());
link.addEventListener('mouseleave', () => cursor.leave());
});
Assuming we have data-splitting set in all elements we want to split into chars, then we only need to call Splitting() and we then have each text split into a bunch of spans, for every letter of the text.
Let’s now have a look at our MenuItem class:
import { gsap } from 'gsap';
export class MenuItem {
constructor(el) {
this.DOM = {el};
// all text chars (Splittingjs)
this.DOM.titleChars = this.DOM.el.querySelectorAll('span.char');
// initial and final colors for each span char (before and after hovering)
const bodyComputedStyle = getComputedStyle(document.body);
this.colors = {
initial: bodyComputedStyle.getPropertyValue('--color-menu'),
final: bodyComputedStyle.getPropertyValue('--color-link')
};
this.initEvents();
}
...
}
We get a reference to all the characters of the menu item text and also it’s initial and final colors for the hover animation.
We initialize the mouseenter/mouseleave events which will triggger the animation on the characters.
When hovering over a menu item we will randomly change its characters position and color, and when hovering out we reset the original color:
onMouseEnter() {
if ( this.leaveTimeline ) {
this.leaveTimeline.kill();
}
// let's try to do an animation that resembles a glitch effect on the characters
// we randomly set new positions for the translation and rotation values of each char and also set a new color
// and repeat this for 3 times
this.enterTimeline = gsap.timeline({
defaults: {
duration: 0.05,
ease: 'power3',
x: () => gsap.utils.random(-15, 15),
y: () => gsap.utils.random(-20, 10),
rotation: () => gsap.utils.random(-5, 5),
color: () => gsap.utils.random(0, 3) < 0.5 ? this.colors.final : this.colors.initial
}
})
// repeat 3 times (repeatRefresh option will make sure the translation/rotation values will be different for each iteration)
.to(this.DOM.titleChars, {
repeat: 3,
repeatRefresh: true
}, 0)
// reset translation/rotation and set final color
.to(this.DOM.titleChars, {
x: 0,
y: 0,
rotation: 0,
color: this.colors.final
}, '+=0.05');
}
onMouseLeave() {
// set back the initial color for each char
this.leaveTimeline = gsap.timeline()
.to(this.DOM.titleChars, {
duration: 0.4,
ease: 'power3',
color: this.colors.initial
});
}
And that is all!
I hope this has not been too difficult to follow and that you have gained some insight into constructing this fancy effect.
Editor’s note: We want to share more of the web dev and design community directly here on Codrops, so we’re very happy to start featuring Yuriy’s newest live coding sessions plus the demo!
In this episode of ALL YOUR HTML, I decompile the particles effect from Mark Appleby’s website and show you how you can create a particle system from scratch, using no libraries at all. I also address some performance tips, and ideas about SDF in the 2D world.
This coding session was streamed live on Oct 11, 2020.