Scrollbars on Hover

First, scrollbars are a usability and accessibility thing. Second, a rule of thumb: if an area scrolls, it should have a visible scrollbar. But the web is a big place and I like tricks, so I’m going to cover the idea of only revealing them on hover. Even macOS itself¹ hides scrollbars by default, revealing them contextually and on interaction. Same on iOS, leading to confusing momements.

All that aside, here’s a way to hide scrollbars by default, only revealing them when the element is hovered. It was created by Thomas Gladdines, who also emailed me about it:

In quick testing on my machine, it works across Chrome, Firefox, and Safari, regardless of my macOS settings. So pretty robust.

The trick is that mask covers the scrollbar! So, if you create a mask that is exactly as wide as the scrollbar (here, I’m just guessing that 17px will cover it) and super duper tall (both of which should probably be calculated by a script), it can perfectly cover the scrollbar. You can even transition the position of the mask, faking a fading in/out effect. Very clever.

Notably, this is the real scrollbar of the element, and not a faked one. Faking one could be another approach. Ben Nadel covered how Slack does that. Their trick is to force the scrollbar to render in an area hidden by overflow, and make a virtual scrollbar that mimics the native one (which you’d then have more direct control over). It’s not forcing the scrollbar either, which is something else you can do if so motivated. And nothing about this prevents you from styling the scrollbar, which might actually have some benefits like specifying the exact width of it.

  1. As I write: If your device allows gestures, scroll bars are hidden until you start scrolling. Otherwise, they’re visible. ↩️

The post Scrollbars on Hover appeared first on CSS-Tricks.

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

Inline Menu Layout with Gallery Panel

Sometimes, sh*t happens. Some days ago it was one of those days for me, you know, one of those spilling coffee on your mac day without having made a backup for a month. What was I thinking? Well, I did finish up this design but lost the link to the Dribbble shot I got inspired from, so if you are the original designer, I apologize for posting this without a reference, so please let me know, I’ll fill the credits in the demo asap.

Anyhoo, this layout is just a little play with hover animations and transitioning to a new panel with a (dummy) gallery layout. I really like the trend of squeezing and squooshing images and words slightly and making them fly in and out as it gives a special dynamic to the design.

The initial view is an “inline” style menu with a hover effect that shows an image for each item.

When we click on the menu item, all items disappear with a cut-off animation and the images from the gallery appear from below, imitating that same motion.

I hope you like this little layout and the animations!

The post Inline Menu Layout with Gallery Panel appeared first on Codrops.

Using a brightness() filter to generically highlight content

Rick Strahl:

I can’t tell you how many times over the years I’ve implemented a custom ‘button’ like CSS implementation. Over the years I’ve used images, backgrounds, gradients, and opacity to effectively ‘highlight’ a control. All that works of course, but the problem with most of these approaches is that one way or the other you’re hard coding a color value, image, or gradient.

You certainly have a lot more control if you specify exact colors, but if you can pull off brightening, darkening, or even a hue-shift in a way that feels cohesive on your site, it’s certainly a lot less code to maintain,

.button.specific-button {
  background: #4CAF50;
}
.button.specific-button:focus,
.button.specific-button:hover {
  background: #A5D6A7;
}

/* vs. */
.button:focus,
.button:hover {
  filter: brightness(120%);
}

/* or maybe you're super hardcore and do it everywhere */
:focus,
:hover {
  filter: brightness(120%) saturate(120%);
}

Direct Link to ArticlePermalink


The post Using a brightness() filter to generically highlight content appeared first on CSS-Tricks.

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

Magnetic Buttons

It has been a long time since we explored some button styles here on Codrops! But after seeing a really nice one on Cuberto, I wanted to explore some ideas and share them with you.

The main idea of these buttons is that they are magnetic and follow the mouse pointer. Along with that, there’s lots of room to play with some interesting hover animations. A very nice thing to explore is the motion of an additional element like a shadow or another line. The parallax effect created by moving the button’s elements differently, gives a nice twist to the animation.

Here we play with a border animation.
This circular button fills with a solid color.
A shadow element creates some depth.

I really hope you like them! Can’t wait to see your ideas 🙂 Share them with us @codrops or @crnacura.

The post Magnetic Buttons appeared first on Codrops.

Exploring Animations for Menu Hover Effects

Last week I showed you how we can create a fancy menu hover animation with images. Today I’d like to share some of the interesting effects we can achieve with that technique. Tuning some variables, timings and filters, the possibilities are really endless, so I invite you to explore some of the things we can come up with in the these couple of demos. Don’t forget to move your mouse along the link to see some jazzy motion and filters in action 🙂

I really hope you enjoy these playful animations!

The post Exploring Animations for Menu Hover Effects appeared first on Codrops.

Creating a Menu Image Animation on Hover

At Codrops, we love experimenting with playful hover effects. Back in 2018, we explored a set of fun hover animations for links. We called that Image Reveal Hover Effects and it shows how to make images appear with a fancy animation when hovering items of a menu. After seeing the fantastic portfolio of Marvin Schwaibold, I wanted to try this effect again on a larger menu and add that beautiful swing effect when moving the mouse. Using some filters, this can also be made more dramatic.

If you are interested in other similar effect, have a look at these:

So, today we’ll have a look at how to create this juicy image hover reveal animation:

Some Markup and Styling

We’ll use a nested structure for each menu item because we’ll have several text elements that will appear on page load and hover.

But we’ll not go into the text animation on load or the hover effect so what we are interested in here is how we’ll make the image appear for each item. The first thing I do when I want to make a certain effect is to write up the structure that I need using no JavaScript. So let’s take a look at that:

<a class="menu__item">
    <span class="menu__item-text">
        <span class="menu__item-textinner">Maria Costa</span>
    </span>
    <span class="menu__item-sub">Style Reset 66 Berlin</span>
    <!-- Markup for the image, inserted with JS -->
    <div class="hover-reveal">
        <div class="hover-reveal__inner">
            <div class="hover-reveal__img" style="background-image: url(img/1.jpg);"></div>
        </div>
    </div>
</a>

In order to construct this markup for the image, we need to save the source somewhere. We’ll use a data attribute on the menu__item, e.g. data-img="img/1.jpg". We’ll go into more detail later on.

Next, we’ll have some styling for it:

.hover-reveal {
    position: absolute;
    z-index: -1;
    width: 220px;
    height: 320px;
    top: 0;
    left: 0;
    pointer-events: none;
    opacity: 0;
}

.hover-reveal__inner {
    overflow: hidden;
}

.hover-reveal__inner,
.hover-reveal__img {
    width: 100%;
    height: 100%;
    position: relative;
}

.hover-reveal__img {
    background-size: cover;
    background-position: 50% 50%;
}

Any other styles that are specific to our effect (like the transforms) we’ll add dynamically.

Let’s take a look at the JavaScript.

The JavaScript

We’ll use GSAP and besides our hover animation, we’ll also use a custom cursor and smooth scrolling. For that we’ll use the smooth scroll library from the amazing folks of Locomotive, the Agency of the year. Since those are both optional and out of the scope of the menu effect we want to showcase, we’ll not be covering it here.

First things first: let’s preload all the images. For the purpose of this demo we are doing this on page load, but that’s optional.

Once that’s done, we can initialize the smooth scroll instance, the custom cursor and our Menu instance.

Here’s how the entry JavaScript file (index.js) looks like:

import Cursor from './cursor';
import {preloader} from './preloader';
import LocomotiveScroll from 'locomotive-scroll';
import Menu from './menu';

const menuEl = document.querySelector('.menu');

preloader('.menu__item').then(() => {
    const scroll = new LocomotiveScroll({el: menuEl, smooth: true});
    const cursor = new Cursor(document.querySelector('.cursor'));
    new Menu(menuEl);
});

Now, let’s create a class for the Menu (in menu.js):

import {gsap} from 'gsap';
import MenuItem from './menuItem';

export default class Menu {
    constructor(el) {
        this.DOM = {el: el};
        this.DOM.menuItems = this.DOM.el.querySelectorAll('.menu__item');
        this.menuItems = [];
        [...this.DOM.menuItems].forEach((item, pos) => this.menuItems.push(new MenuItem(item, pos, this.animatableProperties)));

        ...
    }
    ...
}

So far we have a reference to the main element (the menu <nav>
element) and the menu item elements. We’ll also create an array of our MenuItem instances. But let’s cover that bit in a moment.

What we’ll want to do now is to update the transform (both, X and Y translate) value as we move the mouse over the menu items. But we might as well want to update other properties. In our case we will additionally be updating the rotation and the CSS filter value (brightness). For that, let’s create an object that stores this configuration:

constructor(el) {
    ...

    this.animatableProperties = {
        tx: {previous: 0, current: 0, amt: 0.08},
        ty: {previous: 0, current: 0, amt: 0.08},
        rotation: {previous: 0, current: 0, amt: 0.08},
        brightness: {previous: 1, current: 1, amt: 0.08}
    };
}

With interpolation, we can achieve the 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:

this.animatableProperties.tx.previous = MathUtils.lerp(this.animatableProperties.tx.previous, this.animatableProperties.tx.current, this.animatableProperties.tx.amt);

Finally, we can show the menu items, which are hidden by default. This was just a little extra, and totally optional, but it’s definitely a nice add-on to reveal each item with a delay on page load.

constructor(el) {
    ...

    this.showMenuItems();
}
showMenuItems() {
    gsap.to(this.menuItems.map(item => item.DOM.textInner), {
        duration: 1.2,
        ease: 'Expo.easeOut',
        startAt: {y: '100%'},
        y: 0,
        delay: pos => pos*0.06
    });
}

That’s it for the Menu class. What we’ll be looking into next is how to create the MenuItem class together with some helper variables and functions.

So, let’s start by importing the GSAP library (which we will use to show and hide the images), some helper functions and the images inside our images folder.

Next, we need to get access to the mouse position at any given time, since the image will follow along its movement. We can update this value on “mousemove” . We will also cache its position so we can calculate its speed and movement direction for both, the X and Y axis.

Hence, that’s what we’ll have so far in the menuItem.js file:

import {gsap} from 'gsap';
import { map, lerp, clamp, getMousePos } from './utils';
const images = Object.entries(require('../img/*.jpg'));

let mousepos = {x: 0, y: 0};
let mousePosCache = mousepos;
let direction = {x: mousePosCache.x-mousepos.x, y: mousePosCache.y-mousepos.y};

window.addEventListener('mousemove', ev => mousepos = getMousePos(ev));

export default class MenuItem {
    constructor(el, inMenuPosition, animatableProperties) {
        ...
    }
    ...
}

An item will be passed its position/index in the menu (inMenuPosition) and the animatableProperties object described before. The fact that the “animatable” property values are shared and updated among the different menu items will make the movement and rotation of the images continuous.

Now, in order to be possible to show and hide the menu item image in a fancy way, we need to create that specific markup we’ve shown in the beginning and append it to the item. Remember, our menu item is this by default:

<a class="menu__item" data-img="img/3.jpg">
    <span class="menu__item-text"><span class="menu__item-textinner">Franklin Roth</span></span>
    <span class="menu__item-sub">Amber Convention London</span>
</a>

Let’s append the following structure to the item:

<div class="hover-reveal">
    <div class="hover-reveal__inner" style="overflow: hidden;">
        <div class="hover-reveal__img" style="background-image: url(pathToImage);">
        </div>
    </div>
</div>

The hover-reveal element will be the one moving as we move the mouse.
The hover-reveal__inner element together with the hover-reveal__img (the one with the background image) will be the ones that we can animate together to create fancy animations like reveal/unreveal effects.

layout() {
    this.DOM.reveal = document.createElement('div');
    this.DOM.reveal.className = 'hover-reveal';
    this.DOM.revealInner = document.createElement('div');
    this.DOM.revealInner.className = 'hover-reveal__inner';
    this.DOM.revealImage = document.createElement('div');
    this.DOM.revealImage.className = 'hover-reveal__img';
    this.DOM.revealImage.style.backgroundImage = `url(${images[this.inMenuPosition][1]})`;
    this.DOM.revealInner.appendChild(this.DOM.revealImage);
    this.DOM.reveal.appendChild(this.DOM.revealInner);
    this.DOM.el.appendChild(this.DOM.reveal);
}

And the MenuItem constructor completed:

constructor(el, inMenuPosition, animatableProperties) {
    this.DOM = {el: el};
    this.inMenuPosition = inMenuPosition;
    this.animatableProperties = animatableProperties;
    this.DOM.textInner = this.DOM.el.querySelector('.menu__item-textinner');
    this.layout();
    this.initEvents();
}

The last step is to initialize some events. We need to show the image when hovering the item and hide it when leaving the item.

Also, when hovering it we need to update the animatableProperties object properties, and make the image move, rotate and change its brightness as the mouse moves:

initEvents() {
    this.mouseenterFn = (ev) => {
        this.showImage();
        this.firstRAFCycle = true;
        this.loopRender();
    };
    this.mouseleaveFn = () => {
        this.stopRendering();
        this.hideImage();
    };
    
    this.DOM.el.addEventListener('mouseenter', this.mouseenterFn);
    this.DOM.el.addEventListener('mouseleave', this.mouseleaveFn);
}

Let’s now code the showImage and hideImage functions.

We can create a GSAP timeline for this. Let’s start by setting the opacity to 1 for the reveal element (the top element of that structure we’ve just created). Also, in order to make the image appear on top of all other menu items, let’s set the item’s z-index to a high value.

Next, we can animate the appearance of the image. Let’s do it like this: the image gets revealed to the right or left, depending on the mouse x-axis movement direction (which we have in direction.x). For this to happen, the image element (revealImage) needs to animate its translationX value to the opposite side of its parent element (revealInner element).
That’s basically it:

showImage() {
    gsap.killTweensOf(this.DOM.revealInner);
    gsap.killTweensOf(this.DOM.revealImage);
    
    this.tl = gsap.timeline({
        onStart: () => {
            this.DOM.reveal.style.opacity = this.DOM.revealInner.style.opacity = 1;
            gsap.set(this.DOM.el, {zIndex: images.length});
        }
    })
    // animate the image wrap
    .to(this.DOM.revealInner, 0.2, {
        ease: 'Sine.easeOut',
        startAt: {x: direction.x < 0 ? '-100%' : '100%'},
        x: '0%'
    })
    // animate the image element
    .to(this.DOM.revealImage, 0.2, {
        ease: 'Sine.easeOut',
        startAt: {x: direction.x < 0 ? '100%': '-100%'},
        x: '0%'
    }, 0);
}

To hide the image we just need to reverse this logic:

hideImage() {
    gsap.killTweensOf(this.DOM.revealInner);
    gsap.killTweensOf(this.DOM.revealImage);

    this.tl = gsap.timeline({
        onStart: () => {
            gsap.set(this.DOM.el, {zIndex: 1});
        },
        onComplete: () => {
            gsap.set(this.DOM.reveal, {opacity: 0});
        }
    })
    .to(this.DOM.revealInner, 0.2, {
        ease: 'Sine.easeOut',
        x: direction.x < 0 ? '100%' : '-100%'
    })
    .to(this.DOM.revealImage, 0.2, {
        ease: 'Sine.easeOut',
        x: direction.x < 0 ? '-100%' : '100%'
    }, 0);
}

Now we just need to update the animatableProperties object properties so the image can move around, rotate and change its brightness smoothly. We do this inside a requestAnimationFrame loop. In every cycle we interpolate the previous and current values so things happen with an easing.

We want to rotate the image and change its brightness depending on the x-axis speed (or distance traveled from the previous cycle) of the mouse. Therefore we need to calculate that distance for every cycle which we can get by subtracting the mouse position from the cached mouse position.

We also want to know in which direction we move the mouse since the rotation will be dependent on it. When moving to the left the image rotates negatively, and when moving to the right, positively.

Next, we want to update the animatableProperties values. For the translationX and translationY, we want the center of the image to be positioned where the mouse is. Note that the original position of the image element is on the left side of the menu item.

The rotation can go from -60 to 60 degrees depending on the speed/distance of the mouse and its direction. Finally the brightness can go from 1 to 4, also depending on the speed/distance of the mouse.

In the end, we take these values together with the previous cycle values and use interpolation to set up a final value that will then give us that smooth feeling when animating the element.

This is how the render function looks like:

render() {
    this.requestId = undefined;
    
    if ( this.firstRAFCycle ) {
        this.calcBounds();
    }

    const mouseDistanceX = clamp(Math.abs(mousePosCache.x - mousepos.x), 0, 100);
    direction = {x: mousePosCache.x-mousepos.x, y: mousePosCache.y-mousepos.y};
    mousePosCache = {x: mousepos.x, y: mousepos.y};

    this.animatableProperties.tx.current = Math.abs(mousepos.x - this.bounds.el.left) - this.bounds.reveal.width/2;
    this.animatableProperties.ty.current = Math.abs(mousepos.y - this.bounds.el.top) - this.bounds.reveal.height/2;
    this.animatableProperties.rotation.current = this.firstRAFCycle ? 0 : map(mouseDistanceX,0,100,0,direction.x < 0 ? 60 : -60);
    this.animatableProperties.brightness.current = this.firstRAFCycle ? 1 : map(mouseDistanceX,0,100,1,4);

    this.animatableProperties.tx.previous = this.firstRAFCycle ? this.animatableProperties.tx.current : lerp(this.animatableProperties.tx.previous, this.animatableProperties.tx.current, this.animatableProperties.tx.amt);
    this.animatableProperties.ty.previous = this.firstRAFCycle ? this.animatableProperties.ty.current : lerp(this.animatableProperties.ty.previous, this.animatableProperties.ty.current, this.animatableProperties.ty.amt);
    this.animatableProperties.rotation.previous = this.firstRAFCycle ? this.animatableProperties.rotation.current : lerp(this.animatableProperties.rotation.previous, this.animatableProperties.rotation.current, this.animatableProperties.rotation.amt);
    this.animatableProperties.brightness.previous = this.firstRAFCycle ? this.animatableProperties.brightness.current : lerp(this.animatableProperties.brightness.previous, this.animatableProperties.brightness.current, this.animatableProperties.brightness.amt);
    
    gsap.set(this.DOM.reveal, {
        x: this.animatableProperties.tx.previous,
        y: this.animatableProperties.ty.previous,
        rotation: this.animatableProperties.rotation.previous,
        filter: `brightness(${this.animatableProperties.brightness.previous})`
    });

    this.firstRAFCycle = false;
    this.loopRender();
}

I hope this has been not too difficult to follow and that you have gained some insight into constructing this fancy effect.

Please let me know if you have any question @codrops or @crnacura.

Thank you for reading!

The images used in the demo are by Andrey Yakovlev and Lili Aleeva. All images used are licensed under CC BY-NC-ND 4.0

The post Creating a Menu Image Animation on Hover appeared first on Codrops.

A Glitchy Grid Layout Slideshow

I love experimenting with irregular layouts. I wanted to try to take this custom “background” grid one level further and add a stack-like navigation effect. I liked how it turned out but there was some Jazz missing. It was calling for some dramatic effect, like a glitch animation! So I added it and it was the missing bit indeed.

So, here it is: a glitchy grid layout slideshow with a magnetic link effect and a quick, stack-like animation when navigating.

Here’s the initial view:

When clicking on one of the navigation buttons, there’s a quick stacking animation:

When hovering over the “Enter” button, a glitch animation happens on all the images and texts. This is how it all comes together:

I really hope this comes in handy as a starting point for your ideas! Let me know what you think @codrops or @crnacura 🙂

The post A Glitchy Grid Layout Slideshow appeared first on Codrops.

How to Create a Motion Hover Effect for a Background Image Grid

If you follow our UI Interactions & Animations Roundups, you might have spotted this beautiful grid designed by the folks of tubik:

Previously, Zhenya Rynzhuk also designed this wonderful layout with a similar interaction:

It’s not too complicated to implement this. I wanted to try it and in the following I’ll walk you through the relevant markup and code.

The markup and style for the grid

The markup is simply a grid of items that have background images. I like to use this structure because it allows me to control the sizes of the images by setting their position in the grid.

<div class="grid">
    <div class="grid__item pos-1">
        <div class="grid__item-img" style="background-image:url(img/1.jpg);"></div>
    </div>
    <div class="grid__item pos-2">
        <div class="grid__item-img" style="background-image:url(img/2.jpg);"></div>
    </div>
    <div class="grid__item pos-3">
        <div class="grid__item-img" style="background-image:url(img/3.jpg);"></div>
    </div>
    ...
</div>

The grid is stretched to be a bit bigger than its parent because we want to move the items and create the illusion of an infinite plane of images.

.grid {
	pointer-events: none;
	position: absolute;
	width: 110%;
	height: 110%;
	top: -5%;
	left: -5%;
	display: grid;
	grid-template-columns: repeat(50,2%);
	grid-template-rows: repeat(50,2%);
}

.grid__item {
	position: relative;
}

.grid__item-img {
	position: relative;
	width: 100%;
	height: 100%;
	background-size: cover;
	background-position: 50% 50%;
}

The grid is divided into 50 cells for the rows and columns. With this layout density, the position of each image element can be set precisely.

/* Shorthand grid-area: grid-row-start / grid-column-start / grid-row-end / grid-column-end */

.pos-1 {
	grid-area: 10 / 1 / 26 / 7;
}

.pos-2 {
	grid-area: 1 / 18 / 9 / 27;
}

.pos-3 {
	grid-area: 1 / 36 / 14 / 42;
}

...

Note that I use the double division structure for the possibility of moving the inner element with the background image to create the motion effect seen in demo 3. For that case, I define some extra styles:

/* If we want to move the inner image */

.grid--img .grid__item {
	overflow: hidden;
	display: flex;
	align-items: center;
	justify-content: center;
	will-change: transform;
}

.grid--img .grid__item-img {
	flex: none;
	width: calc(100% + 100px);
	height: calc(100% + 100px);
	will-change: transform;
}

The JavaScript

Now, let’s have a look at the JavaScript part. I’m using GSAP by GreenSock. We start by creating a Grid class to represent the grid of pictures:

export default class Grid {
    constructor(el) {
        this.DOM = {el: el};
        this.gridItems = [];
        this.items = [...this.DOM.el.querySelectorAll('.grid__item')];
        this.items.forEach(item => this.gridItems.push(new GridItem(item)));
       
        this.showItems();
    }
    ...
}

const grid = new Grid(document.querySelector('.grid'));

There should be an initial animation where the grid items scale up and fade in. We can add a method to the class for that. We also want the items to start at different times and for that we use the GSAP stagger option. The items will start animating from the center of the grid:

showItems() {
    gsap.timeline()
    .set(this.items, {scale: 0.7, opacity: 0}, 0)
    .to(this.items, {
        duration: 2,
        ease: 'Expo.easeOut',
        scale: 1,
        stagger: {amount: 0.6, grid: 'auto', from: 'center'}
    }, 0)
    .to(this.items, {
        duration: 3,
        ease: 'Power1.easeOut',
        opacity: 0.4,
        stagger: {amount: 0.6, grid: 'auto', from: 'center'}
    }, 0);
}

Now, let’s make the items move as we move the mouse around. Each grid item will be represented by a GridItem class:

class GridItem {
    constructor(el) {
        this.DOM = {el: el};
        this.move();
    }
    ...
}

The position of each item in both axes should be mapped with the mouse position. So, the mouse can move from position 0 to the width or height of the window. As for the item, it’ll move in a range of [start, end] that we need to specify. We’ll be assigning random values for the start/end value so that each item moves differently from each other.

Let’s add the move method to the GridItem class:

move() {
    // amount to move in each axis
    let translationVals = {tx: 0, ty: 0};
    // get random start and end movement boundaries
    const xstart = getRandomNumber(15,60);
    const ystart = getRandomNumber(15,60);
   
    // infinite loop
    const render = () => {
        // Calculate the amount to move.
        // Using linear interpolation to smooth things out.
        // Translation values will be in the range of [-start, start] for a cursor movement from 0 to the window's width/height
        translationVals.tx = lerp(translationVals.tx, map(mousepos.x, 0, winsize.width, -xstart, xstart), 0.07);
        translationVals.ty = lerp(translationVals.ty, map(mousepos.y, 0, winsize.height, -ystart, ystart), 0.07);
       
        gsap.set(this.DOM.el, {x: translationVals.tx, y: translationVals.ty});  
        requestAnimationFrame(render);
    }
    requestAnimationFrame(render);
}

And that’s it!

I hope you find this helpful and please let me know your feedback via @codrops. Thank you for reading!

The post How to Create a Motion Hover Effect for a Background Image Grid appeared first on Codrops.

Interactive WebGL Hover Effects

I love WebGL, and in this article I will explain one of the cool effects you can make if you master shaders. The effect I want to recreate is originally from Jesper Landberg’s website. He’s a really cool dude, make sure to check out his stuff:

So let’s get to business! Let’s start with this simple HTML:

<div class="item">
    <img src="img.jpg" class="js-image" alt="">
    <h2>Some title</h2>
    <p>Lorem ipsum.</p>
</div>
<script src="app.js"></script>

Couldn’t be any easier! Let’s style it a bit to look prettier:

All the animations will happen in a Canvas element. So now we need to add a bit of JavaScript. I’m using Parcel here, as it’s quite simple to get started with. I’ll use Three.js for the WebGL part.

So let’s add some JavaScript and start with a basic Three.js setup from the official documentation:

import * as THREE from "three";

var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera( 75, window.innerWidth/window.innerHeight, 0.1, 1000 );

var renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );


camera.position.z = 5;

var animate = function () {
	requestAnimationFrame( animate );

	cube.rotation.x += 0.01;
	cube.rotation.y += 0.01;

	renderer.render( scene, camera );
};

animate();

Let’s style the Canvas element:

body { margin: 0; }

canvas { 
	display: block; 
	position: fixed;
	z-index: -1; // put it to background
	left: 0; // position it to fill the whole screen
	top: 0; // position it to fill the whole screen
}

Once you have all this in place, you can just run it with `parcel index.html`. Now, you wouldn’t see much, its an empty 3D scene so far. Let’s leave the HTML for a moment, and concentrate on the 3D scene for now.

Let’s create a simple PlaneBufferGeometry object with an image on it. Just like this:

let TEXTURE = new TextureLoader().load('supaAmazingImage.jpg'); 
let mesh = new Mesh(
	new PlaneBufferGeometry(), 
	new MeshBasicMaterial({map: TEXTURE})
)

And now we’ll see the following:

Obviously we are not there yet, we need that color trail following our mouse. And of course, we need shaders for that. If you are interested in shaders, you’ve probably come across some tutorials on how to displace images, like displacing on hover or liquid distortion effects.

But we have a problem: we can only use shaders on (and inside) that image from the example above. But the effect is not constrained to any image borders, but rather, it’s fluid, covering more area, like the whole screen.

Postprocessing to the rescue

It turns out that the output of the Three.js renderer is just another image. We can make use of that and apply the shader displacement on that output!

Here is the missing part of the code:

// set up post processing
let composer = new EffectComposer(renderer);
let renderPass = new RenderPass(scene, camera);
// rendering our scene with an image
composer.addPass(renderPass);

// our custom shader pass for the whole screen, to displace previous render
let customPass = new ShaderPass({vertexShader,fragmentShader});
// making sure we are rendering it.
customPass.renderToScreen = true;
composer.addPass(customPass);

// actually render scene with our shader pass
composer.render()
// instead of previous
// renderer.render(scene, camera);

There are a bunch of things happening here, but it’s pretty straightforward: you apply your shader to the whole screen.

So let’s do that final shader with the effect:

// get small circle around mouse, with distances to it
float c = circle(uv, mouse, 0.0, 0.2);
// get texture 3 times, each time with a different offset, depending on mouse speed:
float r = texture2D(tDiffuse, uv.xy += (mouseVelocity * .5)).x;
float g = texture2D(tDiffuse, uv.xy += (mouseVelocity * .525)).y;
float b = texture2D(tDiffuse, uv.xy += (mouseVelocity * .55)).z;
// combine it all to final output
color = vec4(r, g, b, 1.);

You can see the result of this in the first demo.

Applying the effect to several images

A screen has its size, and so do images in 3D. So what we need to do now is to calculate some kind of relation of those two.

Just like I did in my previous article, we can make a plane with a width of 1, and fit it exactly to the screen width. So practically, we have WidthOfPlane=ScreenSize.

For our Three.js scene, this means that if want an image with a width of 100px on the screen, we will make a Three.js object with width of 100*(WidthOfPlane/ScreenSize). That’s it! With this kind of math we can also set some margins and positions easily.

When the page loads, I will loop through all the images, get their dimensions, and add them to my 3D world:

let images = [...document.querySelectorAll('.js-image')];
images.forEach(image=>{
	// and we have the width, height and left, top position of the image now!
	let dimensions = image.getBoundingClientRect();
	// hide original image
	image.style.visibility = hidden;
	// add 3D object to your scene, according to its HTML brother dimensions
	createMesh(dimensions);
})

Now it’s quite straightforward to make this HTML-3D hybrid.

Another thing that I added here is mouseVelocity. I used it to change the radius of the effect. The faster the mouse moves, the bigger the radius.

To make it scrollable, we would just need to move the whole scene, the same amount that the screen was scrolled. Using that same formula I mentioned before: NumberOfPixels*(WidthOfPlane/ScreenSize).

Sometimes it’s even easier to make WidthOfPlane equal to ScreenSize. That way, you end up with exactly the same numbers in both worlds!

Exploring different effects

With different shaders you can come up with any kind of effect with this approach. So I decided to play a little bit with the parameters.

Instead of separating the image in three color layers, we could simply displace it depending on the distance to the mouse:

vec2 newUV = mix(uv, mouse, circle); 
color = texture2D(tDiffuse,newUV);

And for the last effect I used some randomness, to get a pixelated effect around the mouse cursor.

In this last demo you can switch between effects to see some modifications you can make. With the “zoom” effect, I just use a displacement, but in the last one, I also randomize the pixels, which looks kinda cool to me!

I’d be happy to see your ideas for this animation. What kind of effect would you do with this technique?

Interactive WebGL Hover Effects was written by Yuriy Artyukh and published on Codrops.

CSS-Only Marquee Effect

Some time ago I encountered this great Dribbble shot by Francesco Zagami. It has a really nice marquee animation when hovering a menu item (you have to wait a couple of seconds to see the menu).

I really love this effect and I have seen it in more designs recently. So I wanted to try and implement it using CSS only, without any JavaScript, and share it with you. After some searching, I found an interesting solution on StackOverflow and one by Alvin Kobie on Codepen.

For this demo, I needed to adjust the styles a bit to create the exact effect seen in Francesco’s Dribbble shot, like offsetting the marquee text and fading it in on hover. The marquee requires text repetition so that the illusion works. The main idea is to animate the marquee infinitely, restarting it seamlessly.

For that we can use the following markup:

<div class="marquee">
	<div class="marquee__inner" aria-hidden="true">
		<span>Showreel</span>
		<span>Showreel</span>
		<span>Showreel</span>
		<span>Showreel</span>
	</div>
</div>

… and these styles:

.marquee {
    position: relative;
    overflow: hidden;
    --offset: 20vw;
    --move-initial: calc(-25% + var(--offset));
    --move-final: calc(-50% + var(--offset));
}

.marquee__inner {
    width: fit-content;
    display: flex;
    position: relative;
    transform: translate3d(var(--move-initial), 0, 0);
    animation: marquee 5s linear infinite;
    animation-play-state: paused;
}

.marquee span {
    font-size: 10vw;
    padding: 0 2vw;
}

.marquee:hover .marquee__inner {
    animation-play-state: running;
}

@keyframes marquee {
    0% {
        transform: translate3d(var(--move-initial), 0, 0);
    }

    100% {
        transform: translate3d(var(--move-final), 0, 0);
    }
}

For the marquee to have an offset (i.e. we want to show the first item, cut off at the beginning), it basically needs to be pulled back. So let’s use four repeated items, like this:

The amount that we want the items to be pulled back is defined in the variable --move-initial. So -25% makes it move back the exact length of one item (as we have four in total).

And the --offset lets us adjust this a bit, so that we see some of the text. --move-final is the end position of the animation, where we can seamlessly start a new loop. It’s half of the way (two items now), again with one item on the left being cut off the same amount like in the initial position. By setting an adequate font size (in vw), we can make sure that three repetitions are visible in the viewport. This is important for the “illusion” to work (i.e. start the next loop).

For the demo, I’ve added some more transitions and images with a blend mode. Have a look at the code if you’d like to see how that all works together.

I really hope you like this demo and find it useful!

Credits

CSS-Only Marquee Effect was written by Mary Lou and published on Codrops.

Animated Custom Cursor Effects

So I have been playing with distortion effects using SVG filters recently and wanted to now try and apply these to a custom cursor. Imagine animating a circular custom cursor with those distortions when hovering over links (or any other element). Here are four demos that explore this idea.

The effects are done by applying SVG filters to a custom cursor element which is an SVG. Besides animating the cursor itself (scaling it up), the SVG filter is animated when hovering over anchors (or any other element you’d like this to interact with).

If you are interested in more of these kind of effects, have a look some previous related experiments:

I really hope you enjoy these and can make use of them! As always, feel free to use the designs in your projects.

Show me what you come up with and ping me @codrops!

Credits

Animated Custom Cursor Effects was written by Mary Lou and published on Codrops.

Ideas for Distorted Link Effects on Menus

After exploring some distortion effects on line elements for links, I couldn’t wait but try them on some big style menus. So here is a little set of demos that shows some ideas for how to use those distortion effects. They were a great excuse to play with some of my favorite typefaces available on Adobe Fonts including Bely Display, Freight and Rigatoni.

How about a tiny squiggly line under an outlined text:

Or some distortion on a thick line with a gradient:

Or maybe two lines hugging the lovely Rigatoni typeface:

Let’s put that swirly line in front of a sexy serif:

Or be bold and make a strong contrasty box with a wave inside:

Check out all the demos here.

I really hope you enjoy these styles and find them useful!

Please share your creations with me on Twitter @codrops!

Ideas for Distorted Link Effects on Menus was written by Mary Lou and published on Codrops.

Background Scale Hover Effect with CSS Clip-path

Today I’d like to share a simple hover effect with you. It’s a recreation of the hover effect seen in the menu on the DDD Hotel website by Garden Eight. The idea is to scale down the background image and “fitting” it to a clip shape which contains the same background image. The shape is visible because the opacity of the background is a bit lower:

When I saw the effect on the DDD Hotel website, I wanted to try to do it using the clip-path property and explore different shapes.

It’s very straightforward: one layer has the background image and a second layer has the additional clip path with a basic shape.

For the last demo, where I wanted to show two circles, I simply stacked two clip-path layers. But for more complex paths, one could also use an SVG instead.

Other interesting things that could be done here is to animate the clip-path (scale it/move it) or change the shape for each link. What do you think?

Background Scale Hover Effect with CSS Clip-path was written by Mary Lou and published on Codrops.

Distorted Link Effects with SVG Filters

Today we’d like to share some ideas for distorted link effects with you. Inspired by Adrien Denat’s Distorted Button Effects with SVG Filters, we wanted to explore some effects on links, specifically lines, using SVG Filters. There’s lots to explore here, so we’d made a simple demo that shows a couple of these effects.

If you’d like to understand the underlying mechanism of how an SVG filter effect works, we recommend checking out Adrien’s article. And as always, we highly recommend Sara’s series on SVG filters.

Using underlines, circles and squares creates fun results. You can apply the SVG filter to any HTML elements, including the link text itself (see the last example).

We hope you enjoy these effects and get inspired!

Distorted Link Effects with SVG Filters was written by Mary Lou and published on Codrops.

4 Ways to Animate the Color of a Text Link on Hover

Let’s create a pure CSS effect that changes the color of a text link on hover… but slide that new color in instead of simply swapping colors.

There are four different techniques we can use to do this. Let’s look at those while being mindful of important things, like accessibility, performance, and browser support in mind.

Let’s get started!

Technique 1: Using background-clip: text

At the time of writing, the background-clip: text property is an experimental feature and is not supported in Internet Explorer 11 and below.

This technique involves creating knockout text with a hard stop gradient. The markup consists of a single HTML link (<a>) element to create a hyperlink:

<a href="#">Link Hover</a>

We can start adding styles to the hyperlink. Using overflow: hidden will clip any content outside of the hyperlink during the hover transition:

a {
  position: relative;
  display: inline-block;
  font-size: 2em;
  font-weight: 800;
  color: royalblue;
  overflow: hidden;
}

We will need to use a linear gradient with a hard stop at 50% to the starting color we want the link to be as well as the color that it will change to:

a {
  /* Same as before */
  background: linear-gradient(to right, midnightblue, midnightblue 50%, royalblue 50%);
}

Let’s use background-clip to clip the gradient and the text value to display the text. We will also use the background-size and background-position properties to have the starting color appear:

a {
  /* Same as before */
  background-clip: text;
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-size: 200% 100%;
  background-position: 100%;
}

Finally, let’s add the transition CSS property and :hover CSS pseudo-class to the hyperlink. To have the link fill from left to right on hover, use the background-position property:

a {
  /* Same as before */
  transition: background-position 275ms ease;
}
a:hover {
  background-position: 0 100%;
}

While this technique does achieve the hover effect, Safari and Chrome will clip text decorations and shadows, meaning they won’t be displayed. Applying text styles, such as an underline, with the text-decoration CSS property will not work. Perhaps consider using other approaches when creating underlines.

Technique 2: Using width/height

This works by using a data attribute containing the same text as the one in the <a> tag and setting the width (filling the text from left-to-right or right-to-left) or height (filling the text from top-to-bottom or bottom-to-top), from 0% to 100% on hover.

Here is the markup:

<a href="#" data-content="Link Hover">Link Hover</a>

The CSS is similar to the previous technique minus the background CSS properties. The text-decoration property will work here:

a {
  position: relative;
  display: inline-block;
  font-size: 2em;
  color: royalblue;
  font-weight: 800;
  text-decoration: underline;
  overflow: hidden;
}

This is when we need to use the content from the data-content attribute. It will be positioned above the content in the <a> tag. We get to use the nice little trick of copying the text in the data attribute and displaying it via the attr() function on the content property of the element’s ::before pseudo-element.

a::before {
  position: absolute;
  content: attr(data-content); /* Prints the value of the attribute */
  top: 0;
  left: 0;
  color: midnightblue;
  text-decoration: underline;
  overflow: hidden;
  transition: width 275ms ease;
}

To keep the text from wrapping to the next line, white-space: nowrap will be applied. To change the link fill color, set the value for the color CSS property using the ::before pseudo-element and having the width start at 0:

a::before {
  /* Same as before */
  width: 0;
  white-space: nowrap;
}

Increase the width to 100% to the ::before pseudo element to complete the text effect on hover:

a:hover::before {
  width: 100%;
}

While this technique does the trick, using the width or height properties will not produce a performant CSS transition. It is best to use either the transform or opacity properties to achieve a smooth, 60fps transition.

Using the text-decoration CSS property can allow for different underline styles to appear in the CSS transition. I created a demo showcasing this using the next technique: the clip-path CSS property.

Technique 3: Using clip-path

For this technique, we will be using the clip-path CSS property with a polygon shape. The polygon will have four vertices, with two of them expanding to the right on hover:

The markup is the same as the previous technique. We will use a ::before pseudo-element again, but the CSS is different:

a::before {
  position: absolute;
  content: attr(data-content);
  color: midnightblue;
  text-decoration: underline;
  clip-path: polygon(0 0, 0 0, 0% 100%, 0 100%);
  transition: clip-path 275ms ease;
}

Unlike the previous techniques, text-decoration: underline must be declared to the ::before pseudo-element for the color to fill the underline on hover.

Now let’s look into the CSS for the clip-path technique:

clip-path: polygon(0 0, 0 0, 0% 100%, 0 100%);

The polygon’s vertices of the clip-path property are set in percentages to define coordinates by the order written:

  • 0 0 = top left
  • 0 0 = top right
  • 100% 0 = bottom right
  • 0 100% = bottom left

The direction of the fill effect can be changed by modifying the coordinates. Now that we have an idea for the coordinates, we can make the polygon expand to the right on hover:

a:hover::before {
  clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%);
}

This technique works pretty well, but note that support for the clip-path property varies between browsers. Creating a CSS transition with clip-path is a better alternative than using the width/height technique; however, it does affect the browser paint.

Technique 4: Using transform

The markup for this technique uses a masking method with a <span> element. Since we will be using duplicated content in a separate element, we will use aria-hidden="true" to improve accessibility — that will hide it from screen readers so the content isn’t read twice:

<a href="#"><span data-content="Link Hover" aria-hidden="true"></span>Link Hover</a>

The CSS for the <span> element contains a transition that will be starting from the left:

span {
  position: absolute;
  top: 0;
  left: 0;
  overflow: hidden;
  transform: translateX(-100%);
  transition: transform 275ms ease;
}

Next, we need to get the <span> to slide the right like this:

To do this, we will use the translateX() CSS function and set it to 0:

a:hover span {
  transform: translateX(0);
}

Then, we will use the ::before pseudo-element for the <span>, again using the data-content attribute we did before. We’ll set the position by translating it 100% along the x-axis.

span::before { 
  display: inline-block;
  content: attr(data-content);
  color: midnightblue;
  transform: translateX(100%);
  transition: transform 275ms ease;
  text-decoration: underline;
}

Much like the <span> element, the position of the ::before pseudo-element will also be set to  translateX(0):

a:hover span::before {
  transform: translateX(0);
}

While this technique is the the most cross-browser compatible of the bunch, it requires more markup and CSS to get there. That said, using the transform CSS property is great for performance as it does not trigger repaints and thus produces smooth, 60fps CSS transitions.

There we have it!

We just looked at four different techniques to achieve the same effect. Although each has its pros and cons, you can see that it’s totally possible to slide in a color change on text. It’s a neat little effect that makes links feel a little more interactive.

The post 4 Ways to Animate the Color of a Text Link on Hover appeared first on CSS-Tricks.

Solving Sticky Hover States with @media (hover: hover)

Mezo Istvan does a good job of covering the problem and a solution to it in a blog post on Medium¹.

If you tap on something that has a :hover state but you don't leave the page then, on a mobile device, there is a chance that :hover state "sticks." You'll see this with stuff like jump-links used as tabs or buttons that trigger on-page functionality.

button:hover {
  border: 3px solid green; /* might stick! */
}

The solution, or trick, is a new(ish) "CSS4" media query that allows you only to apply styles on devices with hover capability.

@media (hover: hover) {
  button:hover {
    border: 3px solid green; /* solves sticky problem */
  }
}

Your typical touch screen mobile device will fail that media query, the style won't apply, and you'll avoid the sticky problem.

Support is solid, so not much worry there.

  1. It almost feels like we have to apologize to linking to things on Medium lately. I have no idea what you're going to experience when you get there. Will you just be able to read it? Will it be a teaser where you have to log in to read more? Will it be behind a paywall? I have no idea. In this case, hopefully, this link post has enough info in it that isn't not blocking you from learning anything.


Direct Link to ArticlePermalink

The post Solving Sticky Hover States with @media (hover: hover) appeared first on CSS-Tricks.

Case Study: Akaru 2019

In 2019, a new version of our Akaru studio website has been released.

After long discussions between developers and designers, we found the creative path we wanted to take for the redesign. The idea was to create a connection between our name Akaru and the graphic style. Meaning “to highlight” in Japanese, we wanted Akaru to transmit the light spectrum, the iridescence and reflections that light can have on some surfaces. 

The particularity of the site is the mixing of regular content in the DOM/CSS and interactive background content in WebGL. We’ll have a look at how we planned and decided on the visuals of the main effect, and in the second part will share a technical overview and show how the “iridescent oil” effect was coded.

Design of the liquid effect

In the following, we will go through our iteration process between design and implementation and how we decided on the visuals of the interactive liquid/oil effect.

Visual Search

After some in-depth research, we built an inspirational mood board inspired by 3D artists and photographers. We have therefore selected several colors, and used all the details present in the images of liquids we considered: the mixture of fluids, streaks of colors and lights.

Processus

We started to create our first texture tests in Photoshop using vector shapes, brushes, distortions, and blurs. After several tests we were able to make our first environmental test with an interesting graphic rendering. The best method was to first draw the waves and shapes, then paint over and mix the colors with the different fusion modes.

Challenges

The main challenge was to “feel” a liquid effect on the textures. It was from this moment that the exchange between designers and developers became essential. To achieve this effect, the developers created a parametric tool where the designers could upload a texture, and then decide on the fluid movements using a Flowmap. From there, we could manage amplitudes, noise speed, scale and a multitude of options.

Implementing the iridescent oil effect

Now we will go through the technical details of how the iridescent oil effect was implemented on every page. We are assuming some basic knowledge of WebGL with Three.js and the GLSL language so we will skip over commonly used code, like scene initialization.

Creating the plane

For this project, we use the OrthographicCamera of Three.js. This camera removes all perspective so we can create our plane without caring about the depth of it.

We will create our plane with a geometry which has the width of the viewport, and we get the height by multiplying the width of the plane by the aspect ratio of our texture:

const PLANE_ASPECT_RATIO = 9 / 16;

const planeWidth = window.innerWidth;
const planeHeight = planeWidth * PLANE_ASPECT_RATIO;

const geometry = new PlaneBufferGeometry(planeWidth, planeHeight);

We could keep the number of segments by default since this effect runs on the fragment shader. By doing so, we reduce the amount of vertices we have to render, which is always good for performance.

Then, in the shader we use the UVs to sample our texture:

vec3 color = texture2D(uTexture, vUv).rgb;

gl_FragColor = vec4(color, 1.0);

Oil motion

Now that our texture is rendered on our plane, we need to make it flow.

To create some movement, we sampled the texture with an offset two times with a different offset:

float phase1 = fract(uTime * uFlowSpeed + 0.5);
float phase2 = fract(uTime * uFlowSpeed + 1.0);

// mirroring phase
phase1 = 1.0 - phase1;
phase2 = 1.0 - phase2;

vec3 color1 = texture2D(
    uTexture,
    uv + vec2(0.5, 0.0) * phase1).rgb;

vec3 color2 = texture2D(
    uTexture,
    uv + vec2(0.5, 0.0) * phase2).rgb;

Then we blend our two textures together:

float flowLerp = abs((0.5 - phase1) / 0.5);
vec3 finalColor = mix(color1, color2, flowLerp);

return finalColor;

But we don’t want our texture to always flow in the same direction, we want some areas to flow up, some others to flow to the right, and so on. To achieve this, we used Flow Map or Vector Map, which look like this:

Example Flow Map

A flow map is a texture in which every pixel contains a direction represented as a 2d vector x and y. In this texture, the red component stores the direction on the x axis, while the green component stores the direction on the y axis. Areas where the liquid is stationary are mid red and mid green (you can find those areas on top of the map). In fact, the direction could be in two ways, for example on the x axis the liquid could go to the left or to the right. To store this information a red value of 0 will make the texture go to the left and a red value of 255 will make the texture go to the right. In the shader, we implement this logic like this:

vec2 flowDir = texture2D(uFlowMap, uv).rg;
// make mid red and mid green the "stationary flow" values
flowDir -= 0.5;

// mirroring phase
phase1 = 1.0 - phase1;
phase2 = 1.0 - phase2;

vec3 color1 = texture2D(
    uTexture,
    uv + flowDir * phase1).rgb;

vec3 color2 = texture2D(
    uTexture,
    uv + flowDir * phase2).rgb;

We painted this map using Photoshop and unfortunately, with all exports (jpeg, png, etc.), we always got some weird artefacts. We found out that using PNG resulted in the least “glitchy” exports we could obtain. We guess that it comes from the compression algorithm for exports in Photoshop. These artefacts are invisible to the eye and can only be seen when we use it as a map. To fix that, we blurred the texture two times with glsl-fast-gaussian-blur (one vertically and one horizontally) and blended them together:

vec4 horizontalBlur = blur(
    uFlowMap,
    uv,
    uResolution,
    vec2(uFlowMapBlurRadius, 0.0)
  );
vec4 verticalBlur = blur(
    uFlowMap,
    uv,
    uResolution,
    vec2(0.0, uFlowMapBlurRadius)
  );
vec4 texture = mix(horizontalBlur, verticalBlur, 0.5);

As you can see, we used glslify to import glsl modules hosted on npm; it’s very useful to keep you shader’s code split and as simple as possible.

Make it feel more natural

Now that our liquid flows, we can clearly see when the liquid is repeating. But liquid doesn’t flow this way in real life. To create a better illusion of a realistic flow movement, we added some turbulence to distort our textures. 

To create this turbulence we use glsl-noise to compute a 3D Noise in which x and y will be the UV downscaled a bit to create a large distortion, and Z will be the time elapsed since the first frame, this will create an animated seamless noise:

float x = uv.x * uNoiseScaleX;
float y = uv.y * uNoiseScaleY;

float n = cnoise3(vec3(x, y, uTime * uNoiseSpeed));

Then, instead of sampling our flow with the default UV, we distort them with the noise:

vec2 distordedUv = uv + applyNoise(uv);

vec3 color1 = texture2D(
    uTexture,
    distordedUv + flowDir * phase1).rgb;

...

On top of that, we use a uniform called uNoiseAmplitude to control the noise strength.

To observe how the noise influences the rendering, you can tweak it inside the “Noise” folder in the GUI at the top right of the screen. For example, try to tweak the “amplitude” value:

Adding the mouse trail

To add some user interaction, we wanted to create a trail inside the oil, like a finger pushing the oil, where the finger would be the user’s pointer. This effect consists of three things:

1. Computing the mouse trail

To achieve this we used a Frame Buffer (or FBO). I will not go very deep into what frame buffers are here but if you want to you can learn everything about it here

Basically, it will: 

  1. Draw a circle in the current mouse position
  2. Render this as a texture
  3. Store this texture
  4. Use this texture the next frame to draw on top of it the new mouse position
  5. Repeat

By doing so, we have a trail drawn by the mouse and everything run on the GPU! For this kind of simulations, running them on the GPU is way more performant than running them on the CPU.

2. Blending the trail with the flow map

We can use the frame buffer as texture. It will be a black texture, with a white trail painted by the mouse. So we pass our trail texture via uniform to our Oil shader and we can compute it like this:

float trail = texture2D(uTrailTexture, uv).r;

We use only the red component of our texture since it’s a grayscale map and all colors are equal.

Then inside our flow function we use our trail to change the direction our liquid texture flow:

flowDir.x -= trail;
flowDir.y += trail * 0.075;

3. Adding the mouse acceleration

When we move our finger in a liquid, the trail it will create depends on the speed our finger moves. To recreate this feeling we make the radius of the trail depending on the mouse speed: the faster the mouse will go, the bigger the trail will be.

To find the mouse speed we compute the difference between the damped and the current mouse position:

const deltaMouse = clamp(this.mouse.distanceTo(this.smoothedMouse), 0.0, 1.0) * 100;

Then we normalize it and apply an easing to this value with the easing functions provided by TweenMax to avoid creating a linear acceleration.

const normDeltaMouse = norm(deltaMouse, 0.0, this.maxRadius);
const easeDeltaMouse = Circ.easeOut.getRatio(normDeltaMouse);

The Tech Stack

Here’s an overview of the technologies we’ve used in our project:

  • three.js for the WebGL part
  • Vue.js for the DOM part, it allows us to wrap up the WebGL inside a component which communicate easily with the rest of the UI 
  • GSAP is the tweening library we love and use in almost every project as it is well optimized and performant
  • Nuxt.js to pre-render during deployment and serve all our pages as static files

Prismic is a really easy to use headless CMS with a nice API for image formatting and a lot of others useful features.

Conclusion

We hope you liked this Case Study, if you have any questions, feel free to ask us on Twitter (@lazyheart and @colinpeyrat), we would be very happy to receive your feedback!

Case Study: Akaru 2019 was written by Jeremy Franzese and published on Codrops.

Creating a Distorted Mask Effect on an Image with Babylon.js and GLSL

Nowadays, it’s really hard to navigate the web and not run into some wonderful website that has some stunning effects that seem like black magic.

Well, many times that “black magic” is in fact WebGL, sometimes mixed with a bit of GLSL. You can find some really nice examples in this Awwwards roundup, but there are many more out there.

Recently, I stumbled upon the Waka Waka website, one of the latest works of Ben Mingo and Aristide Benoist, and the first thing I noticed was the hover effect on the images.

It was obvious that it’s WebGL, but my question was: “How did Aristide do that?”

Since I love to deconstruct WebGL stuff, I tried to replicate it, and in the end I’ve made it.

In this tutorial I’ll explain how to create an effect really similar to the one in the Waka Waka website using Microsoft’s BabylonJS library and some GLSL.

This is what we’ll do.

The setup

The first thing we have to do is create our scene; it will be very basic and will contain only a plane to which we’ll apply a custom ShaderMaterial.

I won’t cover how to setup a scene in BabylonJS, for that you can check its comprehensive documentation.

Here’s the code that you can copy and paste:

import { Engine } from "@babylonjs/core/Engines/engine";
import { Scene } from "@babylonjs/core/scene";
import { Vector3 } from "@babylonjs/core/Maths/math";
import { ArcRotateCamera } from "@babylonjs/core/Cameras/arcRotateCamera";
import { ShaderMaterial } from "@babylonjs/core/Materials/shaderMaterial";
import { Effect } from "@babylonjs/core/Materials/effect";
import { PlaneBuilder } from "@babylonjs/core/Meshes/Builders/planeBuilder";

class App {
  constructor() {
    this.canvas = null;
    this.engine = null;
    this.scene = null;
  }

  init() {
    this.setup();
    this.addListeners();
  }

  setup() {
    this.canvas = document.querySelector("#app");
    this.engine = new Engine(this.canvas, true, null, true);
    this.scene = new Scene(this.engine);

    // Adding the vertex and fragment shaders to the Babylon's ShaderStore
    Effect.ShadersStore["customVertexShader"] = require("./shader/vertex.glsl");
    Effect.ShadersStore[
      "customFragmentShader"
    ] = require("./shader/fragment.glsl");

    // Creating the shader material using the `custom` shaders we added to the ShaderStore
    const planeMaterial = new ShaderMaterial("PlaneMaterial", this.scene, {
      vertex: "custom",
      fragment: "custom",
      attributes: ["position", "normal", "uv"],
      uniforms: ["worldViewProjection"]
    });
    planeMaterial.backFaceCulling = false;

    // Creating a basic plane and adding the shader material to it
    const plane = new PlaneBuilder.CreatePlane(
      "Plane",
      { width: 1, height: 9 / 16 },
      this.scene
    );
    plane.scaling = new Vector3(7, 7, 1);
    plane.material = planeMaterial;

    // Camera
    const camera = new ArcRotateCamera(
      "Camera",
      -Math.PI / 2,
      Math.PI / 2,
      10,
      Vector3.Zero(),
      this.scene
    );

    this.engine.runRenderLoop(() => this.scene.render());
  }

  addListeners() {
    window.addEventListener("resize", () => this.engine.resize());
  }
}

const app = new App();
app.init();

As you can see, it’s not that different from other WebGL libraries like Three.js: it sets up a scene, a camera, and it starts the render loop (otherwise you wouldn’t see anything).

The material of the plane is a ShaderMaterial for which we’ll have to create its respective shader files.

// /src/shader/vertex.glsl

precision highp float;

// Attributes
attribute vec3 position;
attribute vec3 normal;
attribute vec2 uv;

// Uniforms
uniform mat4 worldViewProjection;

// Varyings
varying vec2 vUV;

void main(void) {
    gl_Position = worldViewProjection * vec4(position, 1.0);
    vUV = uv;
}
// /src/shader/fragment.glsl

precision highp float;

// Varyings
varying vec2 vUV;

void main() {
  vec3 color = vec3(vUV.x, vUV.y, 0.0);
  gl_FragColor = vec4(color, 1.0);
}

You can forget about the vertex shader since for the purpose of this tutorial we’ll work only on the fragment shader.

Here you can see it live:

Good, we’ve already written 80% of the JavaScript code we need for the purpose of this tutorial.

The logic

GLSL is cool, it allows you to create stunning effects that would be impossible to do with HTML, CSS and JS alone. It’s a completely different world, and if you’ve always done “web” stuff you’ll get confused at the beginning, because when working with GLSL you have to think in a completely different way to achieve any effect.

The logic behind the effect we want to achieve is pretty simple: we have two overlapping images, and the image that overlaps the other one has a mask applied to it.

Simple, but it doesn’t work like SVG masks for instance.

Adjusting the fragment shader

Before going any further we need to tweak the fragment shader a little bit.

As for now, it looks like this:

// /src/shader/fragment.glsl

precision highp float;

// Varyings
varying vec2 vUV;

void main() {
  vec3 color = vec3(vUV.x, vUV.y, 0.0);
  gl_FragColor = vec4(color, 1.0);
}

Here, we’re telling the shader to assign each pixel a color whose channels are determined by the value of the x coordinate for the Red channel and the y coordinate for the Green channel.

But we need to have the origin at the center of the plane, not the bottom-left corner. In order to do so we have to refactor the declaration of uv this way:

// /src/shader/fragment.glsl

precision highp float;

// Varyings
varying vec2 vUV;

void main() {
  vec2 uv = vUV - 0.5;
  vec3 color = vec3(uv.x, uv.y, 0.0);
  gl_FragColor = vec4(color, 1.0);
}

This simple change will result into the following:

This is becase we moved the origin from the bottom left corner to the center of the plane, so uv‘s values go from -0.5 to 0.5. Since you cannot assign negative values to RGB channels, the Red and Green channels fallback to 0.0 on the whole bottom left area.

Creating the mask

First, let’s change the color of the plane to complete black:

// /src/shader/fragment.glsl

precision highp float;

// Varyings
varying vec2 vUV;

void main() {
  vec2 uv = vUV - 0.5;
  vec3 color = vec3(0.0);
  gl_FragColor = vec4(color, 1.0);
}

Now let’s add a rectangle that we will use as the mask for the foreground image.

Add this code outside the main() function:

vec3 Rectangle(in vec2 size, in vec2 st, in vec2 p, in vec3 c) {
  float top = step(1. - (p.y + size.y), 1. - st.y);
  float right = step(1. - (p.x + size.x), 1. - st.x);
  float bottom = step(p.y, st.y);
  float left = step(p.x, st.x);
  return top * right * bottom * left * c;
}

(How to create shapes is beyond of the scope of this tutorial. For that, I suggest you to read this chapter of “The Book of Shaders”)

The Rectangle() function does exactly what its name says: it creates a rectangle based on the parameters we pass to it.

Then, we redeclare the color using that Rectangle() function:

vec2 maskSize = vec2(0.3, 0.3);

// Note that we're subtracting HALF of the width and height to position the rectangle at the center of the scene
vec2 maskPosition = vec2(-0.15, -0.15);
vec3 maskColor =  vec3(1.0);

color = Rectangle(maskSize, uv, maskPosition, maskColor);

Awesome! We now have our black plane with a beautiful white rectangle at the center.

But, wait! That’s not supposed to be a rectangle; we set its size to be 0.3 on both the width and the height!

That’s because of the ratio of our plane, but it can be easily fixed in two simple steps.

First, add this snippet to the JS file:

this.scene.registerBeforeRender(() => {
  plane.material.setFloat("uPlaneRatio", plane.scaling.x / plane.scaling.y);
});

And then, edit the shader by adding this line at the top of the file:

uniform float uPlaneRatio;

…and this line too, right below the line that sets the uv variable

uv.x *= uPlaneRatio;

Short explanation

In the JS file, we’re sending a uPlaneRatio uniform (one of the GLSL data type) to the fragment shader, whose value is the ratio between the plane width and height.

We made the fragment shader wait for that uniform by declaring it at the top of the file, then the shader uses it to adjust the uv.x value.


Here you can see the final result: a black plane with a white square at the center; nothing too fancy (yet), but it works!

Adding the foreground image

Displaying an image in GLSL is pretty simple. First, edit the JS code and add the following lines:

// Import the `Texture` module from BabylonJS at the top of the file
import { Texture } from '@babylonjs/core/Materials/Textures/texture'
// Add this After initializing both the plane mesh and its material
const frontTexture = new Texture('src/images/lantern.jpg')
plane.material.setTexture("u_frontTexture", frontTexture)

This way, we’re passing the foreground image to the fragment shader as a Texture element.

Now, add the following lines to the fragment shader:

// Put this at the beginninng of the file, outside of the `main()` function
uniform sampler2D u_frontTexture;
// Put this at the bottom of the `main()` function, right above `gl_FragColor = ...`
vec3 frontImage = texture2D(u_frontTexture, uv * 0.5 + 0.5).rgb;

A bit of explaining:

We told BabylonJS to pass the texture to the shader as a sampler2D with the setTexture() method, and then, we made the shader know that we will pass that sampler2D whose name is u_frontTexture.

Finally, we created a new variable of type vec3 named frontImage that contains the RGB values of our texture.

By default, a texture2D is a vec4 variable (it contains the r, g, b and a values), but we don’t need the alpha channel so we declare frontImage as a vec3 variable and explicitly get only the .rgb channels.

Please also note that we’ve modified the UVs of the texture by first multiplying it by 0.5 and then adding 0.5 to it. This is because at the beginning of the main() function I’ve remapped the coordinate system to -0.5 -> 0.5, and also because of the fact that we had to adjust the value of uv.x.


If you now add this to the GLSL code…

color = frontImage;

…you will see our image, rendered by a GLSL shader:

Masking

Always keep in mind that, for shaders, everything is a number (yes, even images), and that 0.0 means completely hidden while 1.0 stands for fully visible.

We can now use the mask we’ve just created to hide the parts of our image where the value of the mask equals 0.0.

With that in mind, it’s pretty easy to apply our mask. The only thing we have to do is multiply the color variable by the value of the mask:

// The mask should be a separate variable, not set as the `color` value
vec3 mask = Rectangle(maskSize, uv, maskPosition, maskColor);

// Some super magic trick
color = frontImage * mask;

Et voilà, we now have a fully functioning mask effect:

Let’s enhance it a bit by making the mask follow a circular path.

In order to do that we must go back to our JS file and add a couple of lines of code.

// Add this to the class constructor
this.time = 0
// This goes inside the `registerBeforeRender` callback
this.time++;
plane.material.setFloat("u_time", this.time);

In the fragment shader, first declare the new uniform at the top of the file:

uniform float u_time;

Then, edit the declaration of maskPosition like this:

vec2 maskPosition = vec2(
  cos(u_time * 0.05) * 0.2 - 0.15,
  sin(u_time * 0.05) * 0.2 - 0.15
);

u_time is simply one of the uniforms that we pass to our shader from the WebGL program.

The only difference with the u_frontTexture uniform is that we increase its value on each render loop and pass its new value to the shader, so that it updates the mask’s position.

Here’s a live preview of the mask going in a circle:

Adding the background image

In order to add the background image we’ll do the exact opposite of what we did for the foreground image.

Let’s go one step at a time.

First, in the JS class, pass the shader the background image in the same way we did for the foreground image:

const backTexture = new Texture("src/images/lantern-bw.jpg");
plane.material.setTexture("u_backTexture", backTexture);

Then, tell the fragment shader that we’re passing it that u_backTexture and initialize another vec3 variable:

// This goes at the top of the file
uniform sampler2D backTexture;

// Add this after `vec3 frontImage = ...`
vec3 backgroundImage = texture2D(iChannel1, uv * 0.5 + 0.5).rgb;

When you do a quick test by replacing

color = frontImage * mask;

with

color = backImage * mask;

you’ll see the background image.

But for this one, we have to invert the mask to make it behave the opposite way.

Inverting a number is really easy, the formula is:

invertedNumber = 1 - <number>

So, let’s apply the inverted mask to the background image:

backImage *= (1.0 - mask);

Here, we’re applying the same mask we added to the foreground image, but since we inverted it, the effect is the opposite.

Putting it all together

At this point, we can refactor the declaration of the two images by directly applying their masks.

vec3 frontImage = texture2D(u_frontTexture, uv * 0.5 + 0.5).rgb * mask;
vec3 backImage = texture2D(u_backTexture, uv * 0.5 + 0.5).rgb * (1.0 - mask);

We can now display both images by adding backImage to frontImage:

color = backImage + frontImage;

That’s it, here’s a live example of the desired effect:

Distorting the mask

Cool uh? But it’s not over yet! Let’s tweak it a bit by distorting the mask.

To do so, we first have to create a new vec2 variable:

vec2 maskUV = vec2(
  uv.x + sin(u_time * 0.03) * sin(uv.y * 5.0) * 0.15,
  uv.y + cos(u_time * 0.03) * cos(uv.x * 10.0) * 0.15
);

Then, replace uv with maskUV in the mask declaration

vec3 mask = Rectangle(maskSize, maskUV, maskPosition, maskColor);

In maskUV, we’re using some math to add uv values based on the u_time uniform and the current uv.

Try tweaking those values by yourself to see different effects.

Distorting the foreground image

Let’s now distort the foreground image the same way we did for the mask, but with slightly different values.

Create a new vec2 variable to store the foreground image uvs:

vec2 frontImageUV = vec2(
  (uv.x + sin(u_time * 0.04) * sin(uv.y * 10.) * 0.03),
  (uv.y + sin(u_time * 0.03) * cos(uv.x * 15.) * 0.05)
);

Then, use that frontImageUV instead of the default uv when declaring frontImage:

vec3 frontImage = texture2D(u_frontTexture, frontImageUV * 0.5 + 0.5).rgb * mask;

Voilà! Now both the mask and the image have a distortion effect applied.

Again, try tweaking those numbers to see how the effect changes.

10 – Adding mouse control

What we’ve made so far is really cool, but we could make it even cooler by adding some mouse control like making it fade in/out when the mouse hovers/leaves the plane and making the mask follow the cursor.

Adding fade effects

In order to detect the mouseover/mouseleave events on a mesh and execute some code when those events occur we have to use BabylonJS’s actions.

Let’s start by importing some new modules:

import { ActionManager } from "@babylonjs/core/Actions/actionManager";
import { ExecuteCodeAction } from "@babylonjs/core/Actions/directActions";
import "@babylonjs/core/Culling/ray";

Then add this code after the creation of the plane:

this.plane.actionManager = new ActionManager(this.scene);

this.plane.actionManager.registerAction(
  new ExecuteCodeAction(ActionManager.OnPointerOverTrigger, () =>
    this.onPlaneHover()
  )
);

this.plane.actionManager.registerAction(
  new ExecuteCodeAction(ActionManager.OnPointerOutTrigger, () =>
    this.onPlaneLeave()
  )
);

Here we’re telling the plane’s ActionManager to listen for the PointerOver and PointerOut events and execute the onPlaneHover() and onPlaneLeave() methods, which we’ll add right now:

onPlaneHover() {
  console.log('hover')
}

onPlaneLeave() {
  console.log('leave')
}

Some notes about the code above

Please note that I’ve used this.plane instead of just plane; that’s because we’ll have to access it from within the mousemove event’s callback later, so I’ve refactored the code a bit.

ActionManager allows us to listen to certain events on a target, in this case the plane.

ExecuteCodeAction is a BabylonJS action that we’ll use to execute some arbitrary code.

ActionManager.OnPointerOverTrigger and ActionManager.OnPointerOutTrigger are the two events that we’re listening to on the plane. They behave exactly like the mouseenter and mouseleave events for DOM elements.

To detect hover events in WebGL, we need to “cast a ray” from the position of the mouse to the mesh we’re checking; if that ray, at some point, intersects with the mesh, it means that the mouse is hovering it. This is why we’re importing the @babylonjs/core/Culling/ray module; BabylonJS will take care of the rest.


Now, if you test it by hovering and leaving the mesh, you’ll see that it logs hover and leave.

Now, let’s add the fade effect. For this, I’ll use the GSAP library, which is the de-facto library for complex and high-performant animations.

First, install it:

yarn add gsap

Then, import it in our class

import gsap from 'gsap

and add this line to the constructor

this.maskVisibility = { value: 0 };

Finally, add this line to the registerBeforeRender()‘s callback function

this.plane.material.setFloat( "u_maskVisibility", this.maskVisibility.value);

This way, we’re sending the shader the current value property of this.maskVisibility as a new uniform called u_maskVisibility.

Refactor the fragment shader this way:

// Add this at the top of the file, like any other uniforms
uniform float u_maskVisibility;

// When declaring `maskColor`, replace `1.0` with the `u_maskVisibility` uniform
vec3 maskColor = vec3(u_maskVisibility);

If you now check the result, you’ll see that the foreground image is not visible anymore; what happened?

Do you remember when I wrote that “for shaders, everything is a number”? That’s the reason! The u_maskVisibility uniform equals 0.0, which means that the mask is invisible.

We can fix it in few lines of code. Open the JS code and refactor the onPlaneHover() and onPlaneLeave() methods this way:

onPlaneHover() {
  gsap.to(this.maskVisibility, {
    duration: 0.5,
    value: 1
  });
}

onPlaneLeave() {
  gsap.to(this.maskVisibility, {
    duration: 0.5,
    value: 0
  });
}

Now, when you hover or leave the plane, you’ll see that the mask fades in and out!

(And yes, BabylonJS has it’s own animation engine, but I’m way more confident with GSAP, that’s why I opted for it.)

Make the mask follow the mouse cursor

First, add this line to the constructor

this.maskPosition = { x: 0, y: 0 };

and this to the addListeners() method:

window.addEventListener("mousemove", () => {
  const pickResult = this.scene.pick(
    this.scene.pointerX,
    this.scene.pointerY
  );

  if (pickResult.hit) {
    const x = pickResult.pickedPoint.x / this.plane.scaling.x;
    const y = pickResult.pickedPoint.y / this.plane.scaling.y;

    this.maskPosition = { x, y };
  }
});

What the code above does is pretty simple: on every mousemove event it casts a ray with this.scene.pick() and updates the values of this.maskPosition if the ray is intersecting something.

(Since we have only a single mesh we can avoid checking what mesh is being hit by the ray.)

Again, on every render loop, we send the mask position to the shader, but this time as a vec2. First, import the Vector2 module together with Vector3

import { Vector2, Vector3 } from "@babylonjs/core/Maths/math";

Add this in the runRenderLoop callback function

this.plane.material.setVector2(
  "u_maskPosition",
  new Vector2(this.maskPosition.x, this.maskPosition.y)
);

Add the u_maskPosition uniform at the top of the fragment shader

uniform vec2 u_maskPosition;

Finally, refactor the maskPosition variable this way

vec3 maskPosition = vec2(
  u_maskPosition.x * uPlaneRatio - 0.15,
  u_maskPosition.y - 0.15
);

Side note; I’ve adjusted the x using the uPlaneRatio value because at the beginning of the main() function I did the same with the shader’s uvs

And here you can see the result of your hard work:

Conclusion

As you can see, doing these kind of things doesn’t involve too much code (~150 lines of JavaScript and ~50 lines of GLSL, including comments and empty lines); the hard part with WebGL is the fact that it’s complex by nature, and it’s a very vast subject, so vast that many times I don’t even know what to search on Google when I get stuck.

Also, you have to study a lot, way more than with “standard” website development. But in the end, it’s really fun to work with.

In this tutorial, I tried to explain the whole process (and the reasoning behind everything) step by step, just like I want someone to explain it to me; if you’ve reached this point of this tutorial, it means that I’ve reached my goal.

In any case, thanks!

Credits

The lantern image is by Vladimir Fetodov

Creating a Distorted Mask Effect on an Image with Babylon.js and GLSL was written by Francesco Michelini and published on Codrops.