After our little draggable image strip experiment, we wanted to explore using the dragging functionality on a menu. The idea is to show a large inline menu with some scattered thumbnails. The menu can be dragged and while doing so, the thumbnails get moved with an animation. Each menu item also changes the letters to show an outlined version. When clicking on the “explore” link under a menu item, the thumbnails move and enlarge to form a grid.
The animations are powered by TweenMax and we use Dave DeSandro’s Draggabilly.
Attention: Note that the demo is experimental and that we use modern CSS properties that might not be supported in older browsers.
The initial view looks as follows:
When clicking on the explore link, we animate all thumbnails to their place in a custom grid.
Today we’d like to share an experimental slideshow with you. The main idea is to show three slides of a slideshow that is slightly rotated. The titles of each slide, which serve as a decorative element, overlay the images and are rotated in an opposing angle. This creates an interesting look, especially when animated. When clicking on one of the lateral slides, the whole thing moves, and when we click on the middle slide, we move everything up and reveal a content area.
Attention: Note that the demo is experimental and that we use modern CSS properties that might not be supported in older browsers. Edge has a problem with SVG data-uri cursors, so you won’t see the custom cursors in the demo there.
The initial view of the slideshow looks as follows:
When we click on the lateral slides, we can navigate. When clicking on the middle one, we open the respective content view for that item:
We also have a dark mode option:
Here’s how the animations look:
We hope you enjoy this slideshow and find it useful!
Custom cursors certainly were a big trend in web development in 2018. In the following tutorial we’ll take a look at how to create a magnetic noisy circle cursor for navigation elements as shown in Demo 4. We’ll be using Paper.js with Simplex Noise.
The Cursor Markup
The markup for the cursor will be split up into two elements. A simple <div> for the small white dot and a <Canvas> element to draw the red noisy circle using Paper.js.
Basically both cursor elements have a fixed position. To be exactly at the tip of the mouse pointer, we adjust left and top of the small cursor. The canvas will simply fill the whole viewport.
Because we’re building our own cursor, we need to make sure to not show the system’s cursor in its normal state and when hovering links.
.page, .page a {
cursor: none;
}
Animating the Small Dot Cursor
In order to have smooth performance we use a requestAnimationFrame()-loop.
// set the starting position of the cursor outside of the screen
let clientX = -100;
let clientY = -100;
const innerCursor = document.querySelector(".cursor--small");
const initCursor = () => {
// add listener to track the current mouse position
document.addEventListener("mousemove", e => {
clientX = e.clientX;
clientY = e.clientY;
});
// transform the innerCursor to the current mouse position
// use requestAnimationFrame() for smooth performance
const render = () => {
innerCursor.style.transform = `translate(${clientX}px, ${clientY}px)`;
// if you are already using TweenMax in your project, you might as well
// use TweenMax.set() instead
// TweenMax.set(innerCursor, {
// x: clientX,
// y: clientY
// });
requestAnimationFrame(render);
};
requestAnimationFrame(render);
};
initCursor();
Setting up the Circle on Canvas
The following is the basis for the red circle part of the cursor. In order to move the red circle around we’ll use a technique called linear interpolation.
let lastX = 0;
let lastY = 0;
let isStuck = false;
let showCursor = false;
let group, stuckX, stuckY, fillOuterCursor;
const initCanvas = () => {
const canvas = document.querySelector(".cursor--canvas");
const shapeBounds = {
width: 75,
height: 75
};
paper.setup(canvas);
const strokeColor = "rgba(255, 0, 0, 0.5)";
const strokeWidth = 1;
const segments = 8;
const radius = 15;
// we'll need these later for the noisy circle
const noiseScale = 150; // speed
const noiseRange = 4; // range of distortion
let isNoisy = false; // state
// the base shape for the noisy circle
const polygon = new paper.Path.RegularPolygon(
new paper.Point(0, 0),
segments,
radius
);
polygon.strokeColor = strokeColor;
polygon.strokeWidth = strokeWidth;
polygon.smooth();
group = new paper.Group([polygon]);
group.applyMatrix = false;
const noiseObjects = polygon.segments.map(() => new SimplexNoise());
let bigCoordinates = [];
// function for linear interpolation of values
const lerp = (a, b, n) => {
return (1 - n) * a + n * b;
};
// function to map a value from one range to another range
const map = (value, in_min, in_max, out_min, out_max) => {
return (
((value - in_min) * (out_max - out_min)) / (in_max - in_min) + out_min
);
};
// the draw loop of Paper.js
// (60fps with requestAnimationFrame under the hood)
paper.view.onFrame = event => {
// using linear interpolation, the circle will move 0.2 (20%)
// of the distance between its current position and the mouse
// coordinates per Frame
lastX = lerp(lastX, clientX, 0.2);
lastY = lerp(lastY, clientY, 0.2);
group.position = new paper.Point(lastX, lastY);
}
}
initCanvas();
Handling the Hover State
const initHovers = () => {
// find the center of the link element and set stuckX and stuckY
// these are needed to set the position of the noisy circle
const handleMouseEnter = e => {
const navItem = e.currentTarget;
const navItemBox = navItem.getBoundingClientRect();
stuckX = Math.round(navItemBox.left + navItemBox.width / 2);
stuckY = Math.round(navItemBox.top + navItemBox.height / 2);
isStuck = true;
};
// reset isStuck on mouseLeave
const handleMouseLeave = () => {
isStuck = false;
};
// add event listeners to all items
const linkItems = document.querySelectorAll(".link");
linkItems.forEach(item => {
item.addEventListener("mouseenter", handleMouseEnter);
item.addEventListener("mouseleave", handleMouseLeave);
});
};
initHovers();
Making the Circle “Magnetic” and “Noisy”
The following snipped is the extended version of the above-mentioned paper.view.onFrame method.
// the draw loop of Paper.js
// (60fps with requestAnimationFrame under the hood)
paper.view.onFrame = event => {
// using linear interpolation, the circle will move 0.2 (20%)
// of the distance between its current position and the mouse
// coordinates per Frame
if (!isStuck) {
// move circle around normally
lastX = lerp(lastX, clientX, 0.2);
lastY = lerp(lastY, clientY, 0.2);
group.position = new paper.Point(lastX, lastY);
} else if (isStuck) {
// fixed position on a nav item
lastX = lerp(lastX, stuckX, 0.2);
lastY = lerp(lastY, stuckY, 0.2);
group.position = new paper.Point(lastX, lastY);
}
if (isStuck && polygon.bounds.width < shapeBounds.width) {
// scale up the shape
polygon.scale(1.08);
} else if (!isStuck && polygon.bounds.width > 30) {
// remove noise
if (isNoisy) {
polygon.segments.forEach((segment, i) => {
segment.point.set(bigCoordinates[i][0], bigCoordinates[i][1]);
});
isNoisy = false;
bigCoordinates = [];
}
// scale down the shape
const scaleDown = 0.92;
polygon.scale(scaleDown);
}
// while stuck and big, apply simplex noise
if (isStuck && polygon.bounds.width >= shapeBounds.width) {
isNoisy = true;
// first get coordinates of large circle
if (bigCoordinates.length === 0) {
polygon.segments.forEach((segment, i) => {
bigCoordinates[i] = [segment.point.x, segment.point.y];
});
}
// loop over all points of the polygon
polygon.segments.forEach((segment, i) => {
// get new noise value
// we divide event.count by noiseScale to get a very smooth value
const noiseX = noiseObjects[i].noise2D(event.count / noiseScale, 0);
const noiseY = noiseObjects[i].noise2D(event.count / noiseScale, 1);
// map the noise value to our defined range
const distortionX = map(noiseX, -1, 1, -noiseRange, noiseRange);
const distortionY = map(noiseY, -1, 1, -noiseRange, noiseRange);
// apply distortion to coordinates
const newX = bigCoordinates[i][0] + distortionX;
const newY = bigCoordinates[i][1] + distortionY;
// set new (noisy) coodrindate of point
segment.point.set(newX, newY);
});
}
polygon.smooth();
};
General Remarks
I hope you enjoyed this tutorial and have fun playing around with it in your own projects. Of course this is just a starting point and you can go crazier with animations, shapes, colors etc. If you have any questions, please feel free to reach out or shoot me a tweet!
This tutorial is going to demonstrate how to build a wave animation effect for a grid of building models using three.js and TweenMax (GSAP).
Attention: This tutorial assumes you already have a some understanding of how three.js works.
If you are not familiar, I highly recommend checking out the official documentation and examples .
The idea is to create a grid of random buildings, that reveal based on their distance towards the camera. The motion we are trying to get is like a wave passing through, and the farthest elements will be fading out in the fog.
We also modify the scale of each building in order to create some visual randomness.
Getting started
First we have to create the markup for our demo. It’s a very simple boilerplate since all the code will be running inside a canvas element:
createScene() {
this.scene = new THREE.Scene();
this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.renderer.shadowMap.enabled = true;
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(this.renderer.domElement);
// this is the line that will give us the nice foggy effect on the scene
this.scene.fog = new THREE.Fog(this.fogConfig.color, this.fogConfig.near, this.fogConfig.far);
}
Camera
Let’s add a camera for to scene:
createCamera() {
const width = window.innerWidth;
const height = window.innerHeight;
this.camera = new THREE.PerspectiveCamera(20, width / height, 1, 1000);
// set the distance our camera will have from the grid
// this will give us a nice frontal view with a little perspective
this.camera.position.set(3, 16, 111);
this.scene.add(this.camera);
}
Ground
Now we need to add a shape to serve as the scene’s ground
addFloor() {
const width = 200;
const height = 200;
const planeGeometry = new THREE.PlaneGeometry(width, height);
// all materials can be changed according to your taste and needs
const planeMaterial = new THREE.MeshStandardMaterial({
color: '#fff',
metalness: 0,
emissive: '#000000',
roughness: 0,
});
const plane = new THREE.Mesh(planeGeometry, planeMaterial);
planeGeometry.rotateX(- Math.PI / 2);
plane.position.y = 0;
this.scene.add(plane);
}
Load 3D models
Before we can build the grid, we have to load our models.
loadModels(path, onLoadComplete) {
const loader = new THREE.OBJLoader();
loader.load(path, onLoadComplete);
}
onLoadModelsComplete(model) {
// our buildings.obj file contains many models
// so we have to traverse them to do some initial setup
this.models = [...model.children].map((model) => {
// since we don't control how the model was exported
// we need to scale them down because they are very big
// scale model down
const scale = .01;
model.scale.set(scale, scale, scale);
// position it under the ground
model.position.set(0, -14, 0);
// allow them to emit and receive shadow
model.receiveShadow = true;
model.castShadow = true;
return model;
});
// our list of models are now setup
}
Ambient Light
addAmbientLight() {
const ambientLight = new THREE.AmbientLight('#fff');
this.scene.add(ambientLight);
}
Grid Setup
Now we are going to place those models in a grid layout.
createGrid() {
// define general bounding box of the model
const boxSize = 3;
// define the min and max values we want to scale
const max = .009;
const min = .001;
const meshParams = {
color: '#fff',
metalness: .58,
emissive: '#000000',
roughness: .18,
};
// create our material outside the loop so it performs better
const material = new THREE.MeshPhysicalMaterial(meshParams);
for (let i = 0; i < this.gridSize; i++) {
for (let j = 0; j < this.gridSize; j++) {
// for every iteration we pull out a random model from our models list and clone it
const building = this.getRandomBuiding().clone();
building.material = material;
building.scale.y = Math.random() * (max - min + .01);
building.position.x = (i * boxSize);
building.position.z = (j * boxSize);
// add each model inside a group object so we can move them easily
this.group.add(building);
// store a reference inside a list so we can reuse it later on
this.buildings.push(building);
}
}
this.scene.add(this.group);
// center our group of models in the scene
this.group.position.set(-this.gridSize - 10, 1, -this.gridSize - 10);
}
Spot Light
We also add a SpotLight to the scene for a nice light effect.
Before we animate the models into the scene, we want to sort them according to their z distance to the camera.
sortBuildingsByDistance() {
this.buildings.sort((a, b) => {
if (a.position.z > b.position.z) {
return 1;
}
if (a.position.z < b.position.z) {
return -1;
}
return 0;
}).reverse();
}
Animate Models
This is the function where we go through our buildings list and animate them. We define the duration and the delay of the animation based on their position in the list.
Today we’d like to share yetanother CSS Grid-powered slideshow with you. The idea is to show and hide images with a reveal effect and add a parallax like effect to the main image and the title. For the title we’ve added two copied layers with an outline style which creates an interesting motion effect. For the animations we use TweenMax.
Attention: Note that we use modern CSS properties that might not be supported in older browsers.
The initial view of the slideshow looks as follows:
For each slide we have a custom grid layout with one main image that spans the full height of the page. When we go next, the images will be hidden with a sliding motion and the title letters disappear randomly. The new slide will reveal its images and the title in a similar fashion.
When moving the mouse, we move copied layers of the main image and the title to create a trail-like effect.
Once the plus after the excerpt is clicked, we show the content of the slide and change the background color:
We hope you like this slideshow and find it useful!
Today we’d like to share a little speedy motion effect with you. The idea is based on the Dribbble shot Ping Pong Slow Motion by Gal Shir where you can see a ping pong ball being shot from one racket to the other. The motion in the animation is enhanced by squeezing the ball and making some background stripes’ height pulsate. This is exactly what we want to do in a slideshow transition: we’ll squeeze the image and add some background effect. Additionally, we’ll make the letters of the title fly away consecutively.
The grain texture for the background was generated using Grained by Sarath Saleem.
Attention: Note that we use modern CSS properties like CSS Grid and CSS Custom Properties that are not supported in older browsers.
The slideshow shows a image in the center of the page:
When clicking on “next” or “previous”, the image will move away, being squeezed to create the illusion of a fast acceleration. The letters will fly away and the background shapes will start “pulsating”.
We hope you enjoy this little effect and find it useful!