Every week, we’ll give you an overview of the best deals for designers, make sure you don’t miss any by subscribing to our deals feed. You can also follow the recently launched website Type Deals if you are looking for free fonts or font deals.
Front End Developer eLearning Bundle, 20 Course/120+ Hours
Your lifetime access gives you so much learning spread out over 20 courses and 120+ teaching hours. This collection is great for you beginners looking to really digest all you can about the world of JavaScript.
This classic font family is loaded with style. An All-Caps typeface, this collection comes in Regular and Rough styles, as well as regular or slanted fonts. With 300+ glyphs and plenty of extras, your design problems are solved with this family that’s a great choice for everything from book covers to packaging.
This bundle contains various styles/themes suitable for every business. Insanely well-designed, fully customizable, and available in multiple different visual styles, this templates can help turn your audiences into loyal clients. What’s more, if you act now, you can save 92% off the regular price!
HalftonePro is a powerful and easy-to-use tool for creating vector halftone, low poly, or stripe patterns. You can use an existing image as the source or create your own gradient. Play around to customize a pattern as you see fit and then export it for use in programs like Adobe Illustrator, GIMP or Inkscape.
Business Booster Bundle of 200+ Professional Vectors
Whether you’re rebranding your business or just looking for a fresh marketing take, the Business Booster Bundle is your answer! Loaded with more than 200 professional vectors, you’ll get everything you need to market your business via all sorts of resources from infographics to presentations. It’s time for a fresh new look!
Your family of fonts is about to expand big time. With this magnificent Mighty Deal you’ll get a whopping 49 unique typefaces, made up from 3 fabulous font families. Multiple weights and loads of OpenType Features make for an easy choice when it comes to everything from branding to magazine covers.
While browsing the latest award-winning websites, you may notice a lot of fancy image distortion animations or neat 3D effects. Most of them are created with WebGL, an API allowing GPU-accelerated image processing effects and animations. They also tend to use libraries built on top of WebGL such as three.js or pixi.js. Both are very powerful tools to create respectively 2D and 3D scenes.
But, you should keep in mind that those libraries were not originally designed to create slideshows or animate DOM elements. There is a library designed just for that, though, and we’re going to cover how to use it here in this post.
WebGL, CSS Positioning, and Responsiveness
Say you’re working with a library like three.js or pixi.js and you want to use it to create interactions, like mouseover and scroll events on elements. You might run into trouble! How do you position your WebGL elements relative to the document and other DOM elements? How would handle responsiveness?
This is exactly what I had in mind when creating curtains.js.
Curatins.js allows you to create planes containing images and videos (in WebGL we will call them textures) that act like plain HTML elements, with position and size defined by CSS rules. But these planes can be enhanced with the endless possibilities of WebGL and shaders.
Wait, shaders?
Shaders are small programs written in GLSL that will tell your GPU how to render your planes. Knowing how shaders work is mandatory here because this is how we will handle animations. If you’ve never heard of them, you may want to learn the basics first. There are plenty of good websites to start learning them, like The Book of Shaders.
Now that you get the idea, let’s create our first plane!
Setup of a basic plane
To display our first plane, we will need a bit of HTML, CSS, and some JavaScript to create the plane. Then our shaders will animate it.
HTML
The HTML will be really simple here. We will create a <div> that will hold our canvas, and a div that will hold our image.
<body>
<!-- div that will hold our WebGL canvas -->
<div id="canvas"></div>
<!-- div used to create our plane -->
<div class="plane">
<!-- image that will be used as a texture by our plane -->
<img src="path/to/my-image.jpg" />
</div>
</body>
CSS
We will will use CSS to make sure the <div> that wraps the canvas will be bigger than our plane, and apply any size to the plane div. (Our WebGL plane will have the exact same size and positions of this div.)
body {
/* make the body fit our viewport */
position: relative;
width: 100%;
height: 100vh;
margin: 0;
/* hide scrollbars */
overflow: hidden;
}
#canvas {
/* make the canvas wrapper fit the document */
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
.plane {
/* define the size of your plane */
width: 80%;
max-width: 1400px;
height: 80vh;
position: relative;
top: 10vh;
margin: 0 auto;
}
.plane img {
/* hide the img element */
display: none;
}
JavaScript
There’s a bit more work in the JavaScript. We need to instantiate our WebGL context, create a plane with uniform parameters, and use it.
window.onload = function() {
// pass the id of the div that will wrap the canvas to set up our WebGL context and append the canvas to our wrapper
var webGLCurtain = new Curtains("canvas");
// get our plane element
var planeElement = document.getElementsByClassName("plane")[0];
// set our initial parameters (basic uniforms)
var params = {
vertexShaderID: "plane-vs", // our vertex shader ID
fragmentShaderID: "plane-fs", // our framgent shader ID
uniforms: {
time: {
name: "uTime", // uniform name that will be passed to our shaders
type: "1f", // this means our uniform is a float
value: 0,
},
}
}
// create our plane mesh
var plane = webGLCurtain.addPlane(planeElement, params);
// use the onRender method of our plane fired at each requestAnimationFrame call
plane.onRender(function() {
plane.uniforms.time.value++; // update our time uniform value
});
}
Shaders
We need to write the vertex shader. It won’t be doing much except position our plane based on the model view and projection matrix and pass varyings to the fragment shader:
<!-- vertex shader -->
<script id="plane-vs" type="x-shader/x-vertex">
#ifdef GL_ES
precision mediump float;
#endif
// those are the mandatory attributes that the lib sets
attribute vec3 aVertexPosition;
attribute vec2 aTextureCoord;
// those are mandatory uniforms that the lib sets and that contain our model view and projection matrix
uniform mat4 uMVMatrix;
uniform mat4 uPMatrix;
// if you want to pass your vertex and texture coords to the fragment shader
varying vec3 vVertexPosition;
varying vec2 vTextureCoord;
void main() {
// get the vertex position from its attribute
vec3 vertexPosition = aVertexPosition;
// set its position based on projection and model view matrix
gl_Position = uPMatrix * uMVMatrix * vec4(vertexPosition, 1.0);
// set the varyings
vTextureCoord = aTextureCoord;
vVertexPosition = vertexPosition;
}
</script>
Now our fragment shader. This is where we will add a little displacement effect based on our time uniform and the texture coordinates.
<!-- fragment shader -->
<script id="plane-fs" type="x-shader/x-fragment">
#ifdef GL_ES
precision mediump float;
#endif
// get our varyings
varying vec3 vVertexPosition;
varying vec2 vTextureCoord;
// the uniform we declared inside our javascript
uniform float uTime;
// our texture sampler (this is the lib default name, but it could be changed)
uniform sampler2D uSampler0;
void main() {
// get our texture coords
vec2 textureCoord = vTextureCoord;
// displace our pixels along both axis based on our time uniform and texture UVs
// this will create a kind of water surface effect
// try to comment a line or change the constants to see how it changes the effect
// reminder : textures coords are ranging from 0.0 to 1.0 on both axis
const float PI = 3.141592;
textureCoord.x += (
sin(textureCoord.x * 10.0 + ((uTime * (PI / 3.0)) * 0.031))
+ sin(textureCoord.y * 10.0 + ((uTime * (PI / 2.489)) * 0.017))
) * 0.0075;
textureCoord.y += (
sin(textureCoord.y * 20.0 + ((uTime * (PI / 2.023)) * 0.023))
+ sin(textureCoord.x * 20.0 + ((uTime * (PI / 3.1254)) * 0.037))
) * 0.0125;
gl_FragColor = texture2D(uSampler0, textureCoord);
}
</script>
Et voilà! You’re all done, and if everything went well, you should be seeing something like this.
Alright, that’s pretty cool so far, but we started this post talking about 3D and interactions, so let’s look at how we could add those in.
About vertices
To add a 3D effect we would have to change the plane vertices position inside the vertex shader. However in our first example, we did not specify how many vertices our plane should have, so it was created with a default geometry containing six vertices forming two triangles :
In order to get decent 3D animations, we would need more triangles, thus more vertices:
This plane has five segments along its width and five segments along its height. As a result, we have 50 triangles and 150 total vertices.
Refactoring our JavaScript
Fortunately, it is easy to specify our plane definition as it could be set inside our initial parameters.
We are also going to listen to mouse position to add a bit of interaction. To do it properly, we will have to wait for the plane to be ready, convert our mouse document coordinates to our WebGL clip space coordinates and send them to the shaders as a uniform.
// we are using window onload event here but this is not mandatory
window.onload = function() {
// track the mouse positions to send it to the shaders
var mousePosition = {
x: 0,
y: 0,
};
// pass the id of the div that will wrap the canvas to set up our WebGL context and append the canvas to our wrapper
var webGLCurtain = new Curtains("canvas");
// get our plane element
var planeElement = document.getElementsByClassName("plane")[0];
// set our initial parameters (basic uniforms)
var params = {
vertexShaderID: "plane-vs", // our vertex shader ID
fragmentShaderID: "plane-fs", // our framgent shader ID
widthSegments: 20,
heightSegments: 20, // we now have 20*20*6 = 2400 vertices !
uniforms: {
time: {
name: "uTime", // uniform name that will be passed to our shaders
type: "1f", // this means our uniform is a float
value: 0,
},
mousePosition: { // our mouse position
name: "uMousePosition",
type: "2f", // notice this is a length 2 array of floats
value: [mousePosition.x, mousePosition.y],
},
mouseStrength: { // the strength of the effect (we will attenuate it if the mouse stops moving)
name: "uMouseStrength", // uniform name that will be passed to our shaders
type: "1f", // this means our uniform is a float
value: 0,
},
}
}
// create our plane mesh
var plane = webGLCurtain.addPlane(planeElement, params);
// once our plane is ready, we could start listening to mouse/touch events and update its uniforms
plane.onReady(function() {
// set a field of view of 35 to exagerate perspective
// we could have done it directly in the initial params
plane.setPerspective(35);
// listen our mouse/touch events on the whole document
// we will pass the plane as second argument of our function
// we could be handling multiple planes that way
document.body.addEventListener("mousemove", function(e) {
handleMovement(e, plane);
});
document.body.addEventListener("touchmove", function(e) {
handleMovement(e, plane);
});
}).onRender(function() {
// update our time uniform value
plane.uniforms.time.value++;
// continually decrease mouse strength
plane.uniforms.mouseStrength.value = Math.max(0, plane.uniforms.mouseStrength.value - 0.0075);
});
// handle the mouse move event
function handleMovement(e, plane) {
// touch event
if(e.targetTouches) {
mousePosition.x = e.targetTouches[0].clientX;
mousePosition.y = e.targetTouches[0].clientY;
}
// mouse event
else {
mousePosition.x = e.clientX;
mousePosition.y = e.clientY;
}
// convert our mouse/touch position to coordinates relative to the vertices of the plane
var mouseCoords = plane.mouseToPlaneCoords(mousePosition.x, mousePosition.y);
// update our mouse position uniform
plane.uniforms.mousePosition.value = [mouseCoords.x, mouseCoords.y];
// reassign mouse strength
plane.uniforms.mouseStrength.value = 1;
}
}
Now that our JavaScript is done, we have to rewrite our shaders so that they’ll use our mouse position uniform.
Refactoring the shaders
Let’s look at our vertex shader first. We have three uniforms that we could use for our effect:
the time which is constantly increasing
the mouse position
our mouse strength, which is constantly decreasing until the next mouse move
We will use all three of them to create a kind of 3D ripple effect.
<script id="plane-vs" type="x-shader/x-vertex">
#ifdef GL_ES
precision mediump float;
#endif
// those are the mandatory attributes that the lib sets
attribute vec3 aVertexPosition;
attribute vec2 aTextureCoord;
// those are mandatory uniforms that the lib sets and that contain our model view and projection matrix
uniform mat4 uMVMatrix;
uniform mat4 uPMatrix;
// our time uniform
uniform float uTime;
// our mouse position uniform
uniform vec2 uMousePosition;
// our mouse strength
uniform float uMouseStrength;
// if you want to pass your vertex and texture coords to the fragment shader
varying vec3 vVertexPosition;
varying vec2 vTextureCoord;
void main() {
vec3 vertexPosition = aVertexPosition;
// get the distance between our vertex and the mouse position
float distanceFromMouse = distance(uMousePosition, vec2(vertexPosition.x, vertexPosition.y));
// this will define how close the ripples will be from each other. The bigger the number, the more ripples you'll get
float rippleFactor = 6.0;
// calculate our ripple effect
float rippleEffect = cos(rippleFactor * (distanceFromMouse - (uTime / 120.0)));
// calculate our distortion effect
float distortionEffect = rippleEffect * uMouseStrength;
// apply it to our vertex position
vertexPosition += distortionEffect / 15.0;
gl_Position = uPMatrix * uMVMatrix * vec4(vertexPosition, 1.0);
// varyings
vTextureCoord = aTextureCoord;
vVertexPosition = vertexPosition;
}
</script>
As for the fragment shader, we are going to keep it simple. We are going to fake lights and shadows based on each vertex position:
<script id="plane-fs" type="x-shader/x-fragment">
#ifdef GL_ES
precision mediump float;
#endif
// get our varyings
varying vec3 vVertexPosition;
varying vec2 vTextureCoord;
// our texture sampler (this is the lib default name, but it could be changed)
uniform sampler2D uSampler0;
void main() {
// get our texture coords
vec2 textureCoords = vTextureCoord;
// apply our texture
vec4 finalColor = texture2D(uSampler0, textureCoords);
// fake shadows based on vertex position along Z axis
finalColor.rgb -= clamp(-vVertexPosition.z, 0.0, 1.0);
// fake lights based on vertex position along Z axis
finalColor.rgb += clamp(vVertexPosition.z, 0.0, 1.0);
// handling premultiplied alpha (useful if we were using a png with transparency)
finalColor = vec4(finalColor.rgb * finalColor.a, finalColor.a);
gl_FragColor = finalColor;
}
</script>
With these two simple examples, we’ve seen how to create a plane and interact with it.
Videos and displacement shaders
Our last example will create a basic fullscreen video slideshow using a displacement shader to enhance the transitions.
Displacement shader concept
The displacement shader will create a nice distortion effect. It will be written inside our fragment shader using a grayscale picture and will offset the pixel coordinates of the videos based on the texture RGB values. Here’s the image we will be using:
The effect will be calculated based on each pixel RGB value, with a black pixel being [0, 0, 0] and a white pixel [1, 1, 1] (GLSL equivalent for [255, 255, 255]). To simplify, we will use only the red channel value, as with a grayscale image red, green and blue are always equal.
You can try to create your own grayscale image (it works great with geometric shape ) to get your unique transition effect.
Multiple textures and videos
A plane can have more than one texture simply by adding multiple image tags. This time, instead of images we want to use videos. We just have to replace the <img /> tags with a <video /> one. However there are two things to know when it comes to video:
The video will always fit the exact size of the plane, which means your plane has to have the same width/height ratio as your video. This is not a big deal tho because it is easy to handle with CSS.
On mobile devices, we can’t autoplay videos without a user gesture, like a click event. It is therefore safer to add a „enter site” button to display and launch our videos.
HTML
The HTML is still pretty straightforward. We will create our canvas div wrapper, our plane div containing the textures and a button to trigger the video autoplay. Just notice the use of the data-sampler attribute on the image and video tags—it will be useful inside our fragment shader.
<body>
<div id="canvas"></div>
<!-- this div will handle the fullscreen video sizes and positions -->
<div class="plane-wrapper">
<div class="plane">
<!-- notice here we are using the data-sampler attribute to name our sampler uniforms -->
<img src="path/to/displacement.jpg" data-sampler="displacement" />
<video src="path/to/video.mp4" data-sampler="firstTexture"></video>
<video src="path/to/video-2.mp4" data-sampler="secondTexture"></video>
</div>
</div>
<div id="enter-site-wrapper">
<span id="enter-site">
Click to enter site
</span>
</div>
</body>
CSS
The stylesheet will handle a few things: display the button and hide the canvas before the user has entered the site, size and position our plane-wrapper div to handle fullscreen responsive videos.
@media screen {
body {
margin: 0;
font-size: 18px;
font-family: 'PT Sans', Verdana, sans-serif;
background: #212121;
line-height: 1.4;
height: 100vh;
width: 100vw;
overflow: hidden;
}
/*** canvas ***/
#canvas {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 10;
/* hide the canvas until the user clicks the button */
opacity: 0;
transition: opacity 0.5s ease-in;
}
/* display the canvas */
.video-started #canvas {
opacity: 1;
}
.plane-wrapper {
position: absolute;
/* center our plane wrapper */
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
z-index: 15;
}
.plane {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
/* tell the user he can click the plane */
cursor: pointer;
}
/* hide the original image and videos */
.plane img, .plane video {
display: none;
}
/* center the button */
#enter-site-wrapper {
display: flex;
justify-content: center;
align-items: center;
align-content: center;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 30;
/* hide the button until everything is ready */
opacity: 0;
transition: opacity 0.5s ease-in;
}
/* show the button */
.curtains-ready #enter-site-wrapper {
opacity: 1;
}
/* hide the button after the click event */
.curtains-ready.video-started #enter-site-wrapper {
opacity: 0;
pointer-events: none;
}
#enter-site {
padding: 20px;
color: white;
background: #ee6557;
max-width: 200px;
text-align: center;
cursor: pointer;
}
}
/* fullscreen video responsive */
@media screen and (max-aspect-ratio: 1920/1080) {
.plane-wrapper {
height: 100vh;
width: 177vh;
}
}
@media screen and (min-aspect-ratio: 1920/1080) {
.plane-wrapper {
width: 100vw;
height: 56.25vw;
}
}
JavaScript
As for the JavaScript, we will go like this:
Set a couple variables to store our slideshow state
Create the Curtains object and add the plane to it
When the plane is ready, listen to a click event to start our videos playback (notice the use of the playVideos() method). Add another click event to switch between the two videos.
Update our transition timer uniform inside the onRender() method
window.onload = function() {
// here we will handle which texture is visible and the timer to transition between images
var activeTexture = 1;
var transitionTimer = 0;
// set up our WebGL context and append the canvas to our wrapper
var webGLCurtain = new Curtains("canvas");
// get our plane element
var planeElements = document.getElementsByClassName("plane");
// some basic parameters
var params = {
vertexShaderID: "plane-vs",
fragmentShaderID: "plane-fs",
imageCover: false, // our displacement texture has to fit the plane
uniforms: {
transitionTimer: {
name: "uTransitionTimer",
type: "1f",
value: 0,
},
},
}
var plane = webGLCurtain.addPlane(planeElements[0], params);
// create our plane
plane.onReady(function() {
// display the button
document.body.classList.add("curtains-ready");
// when our plane is ready we add a click event listener that will switch the active texture value
planeElements[0].addEventListener("click", function() {
if(activeTexture == 1) {
activeTexture = 2;
}
else {
activeTexture = 1;
}
});
// click to play the videos
document.getElementById("enter-site").addEventListener("click", function() {
// display canvas and hide the button
document.body.classList.add("video-started");
// play our videos
plane.playVideos();
}, false);
}).onRender(function() {
// increase or decrease our timer based on the active texture value
// at 60fps this should last one second
if(activeTexture == 2) {
transitionTimer = Math.min(60, transitionTimer + 1);
}
else {
transitionTimer = Math.max(0, transitionTimer - 1);
}
// update our transition timer uniform
plane.uniforms.transitionTimer.value = transitionTimer;
});
}
Shaders
This is where all the magic will occur. Like in our first example, the vertex shader won’t do much and you’ll have to focus on the fragment shader that will create a “dive in” effect:
<script id="plane-vs" type="x-shader/x-vertex">
#ifdef GL_ES
precision mediump float;
#endif
// default mandatory variables
attribute vec3 aVertexPosition;
attribute vec2 aTextureCoord;
uniform mat4 uMVMatrix;
uniform mat4 uPMatrix;
// varyings
varying vec3 vVertexPosition;
varying vec2 vTextureCoord;
// custom uniforms
uniform float uTransitionTimer;
void main() {
vec3 vertexPosition = aVertexPosition;
gl_Position = uPMatrix * uMVMatrix * vec4(vertexPosition, 1.0);
// varyings
vTextureCoord = aTextureCoord;
vVertexPosition = vertexPosition;
}
</script>
<script id="plane-fs" type="x-shader/x-fragment">
#ifdef GL_ES
precision mediump float;
#endif
varying vec3 vVertexPosition;
varying vec2 vTextureCoord;
// custom uniforms
uniform float uTransitionTimer;
// our textures samplers
// notice how it matches our data-sampler attributes
uniform sampler2D firstTexture;
uniform sampler2D secondTexture;
uniform sampler2D displacement;
void main( void ) {
// our texture coords
vec2 textureCoords = vec2(vTextureCoord.x, vTextureCoord.y);
// our displacement texture
vec4 displacementTexture = texture2D(displacement, textureCoords);
// our displacement factor is a float varying from 1 to 0 based on the timer
float displacementFactor = 1.0 - (cos(uTransitionTimer / (60.0 / 3.141592)) + 1.0) / 2.0;
// the effect factor will tell which way we want to displace our pixels
// the farther from the center of the videos, the stronger it will be
vec2 effectFactor = vec2((textureCoords.x - 0.5) * 0.75, (textureCoords.y - 0.5) * 0.75);
// calculate our displaced coordinates to our first video
vec2 firstDisplacementCoords = vec2(textureCoords.x - displacementFactor * (displacementTexture.r * effectFactor.x), textureCoords.y - displacementFactor * (displacementTexture.r * effectFactor.y));
// opposite displacement effect on the second video
vec2 secondDisplacementCoords = vec2(textureCoords.x - (1.0 - displacementFactor) * (displacementTexture.r * effectFactor.x), textureCoords.y - (1.0 - displacementFactor) * (displacementTexture.r * effectFactor.y));
// apply the textures
vec4 firstDistortedColor = texture2D(firstTexture, firstDisplacementCoords);
vec4 secondDistortedColor = texture2D(secondTexture, secondDisplacementCoords);
// blend both textures based on our displacement factor
vec4 finalColor = mix(firstDistortedColor, secondDistortedColor, displacementFactor);
// handling premultiplied alpha
finalColor = vec4(finalColor.rgb * finalColor.a, finalColor.a);
// apply our shader
gl_FragColor = finalColor;
}
</script>
Here’s our little video slideshow with a cool transition effect:
This example is a great way to show you how to create a slideshow with curtains.js: you might want to use images instead of videos, change the displacement texture, modify the fragment shader or even add more slides…
Going deeper
We’ve just scraped the surface of what’s possible with curtains.js. You could try to create multiple planes with a cool mouse over effect for your article thumbs for example. The possibilities are almost endless.
If you want to see more examples covering all those basics usages, you can check the library website or the GitHub repo.
In this hands-on course, Adobe guru Daniel Walter Scott will take you through examples of typical creative challenges you will encounter as you learn to master Adobe Photoshop. You’ll learn about adjusting colors, masking, retouching, and manipulating images. Whether you are a photographer, marketer, or designer, this is the perfect course to gain core Adobe Photoshop skills.
You can take our new course straight away with a subscription to Envato Elements. For a single low monthly fee, you get access not only to this course, but also to our growing library of over 1,000 video courses and industry-leading eBooks on Envato Tuts+.
Plus you now get unlimited downloads from the huge Envato Elements library of 550,000+ creative assets. Create with unique fonts, photos, graphics and templates, and deliver better projects faster.
In this tutorial, you will learn how to create a sparkle photo effect. I will explain everything in so much detail that everyone can create it, even those who have just opened Photoshop for the first time.
The effect shown above is the one I will show you how to create in this tutorial. If you would like to create the even more advanced magic dust effects below, using just a single click and in only a few minutes, then check out my Magic Dust 2 Photoshop Action.
What You’ll Need
To recreate the design above, you will need the following resources:
First, open the photo that you want to work with. To open your photo, go to File > Open, choose your photo, and click Open. Now, before we get started, just check a couple of things:
Your photo should be in RGB Color mode, 8 Bits/Channel. To check this, go to Image > Mode.
For best results, your photo size should be 2000–3000 px wide/high. To check this, go to Image > Image Size.
Your photo should be the Background layer. If it is not, go to Layer > New > Background from Layer.
2. How to Create the Sparkles Brush
Step 1
In this section, we are going to create the brush that we will need for this effect. Choose the Brush Tool (B) and pick a soft brush. Then, go to Window > Brush and in the Brush window use the settings below:
Step 2
Now to define this brush with new settings as a new brush, click on the top right icon in the bottom right corner of the Brush panel, and name it Sparkles.
3. How to Create the Sparkles
Step 1
In this section we are going to create the sparkles. Go to Layer > New > Layer to create a new layer and name it Sparkles_Small_1.
Step 2
Now choose the Brush Tool (B), select the Sparkles brush, and set the Diameter of the brush to around 10 px. Then, set the foreground color to #ffffff and brush as shown below (decrease the Diameter of the brush as you brush from the edges of the photo towards the center):
Step 3
Go to Layer > New > Layer to create a new layer and name it Sparkles_Small_2. Then, drag this layer just below the Sparkles_Small_1 layer in the Layers panel.
Step 4
Now choose the Brush Tool (B), select the Sparkles brush, and set the Diameter of the brush to around 10 px. Then, set the foreground color to #ffffff and brush as shown below (decrease the Diameter of the brush as you brush from the edges of the photo towards the center):
Step 5
Go to Layer > New > Layer to create a new layer and name it Sparkles_Medium_1. Then, drag this layer just below the Sparkles_Small_2 layer in the Layers panel.
Step 6
Now choose the Brush Tool (B), select the Sparkles brush, and set the Diameter of the brush to around 35 px. Then, set the foreground color to #ffffff and brush as shown below (decrease the Diameter of the brush as you brush from the edges of the photo towards the center):
Step 7
Go to Layer > New > Layer to create a new layer and name it Sparkles_Medium_2. Then, drag this layer just below the Sparkles_Medium_1 layer in the Layers panel.
Step 8
Now choose the Brush Tool (B), select the Sparkles brush, and set the Diameter of the brush to around 35 px. Then, set the foreground color to #ffffff and brush as shown below (decrease the Diameter of the brush as you brush from the edges of the photo towards the center):
Step 9
Go to Layer > New > Layer to create a new layer and name it Sparkles_Large_1. Then, drag this layer just below the Sparkles_Medium_2 layer in the Layers panel.
Step 10
Now choose the Brush Tool (B), select the Sparkles brush, and set the Diameter of the brush to around 50 px. Then, set the foreground color to #ffffff and brush as shown below (decrease the Diameter of the brush as you brush from the edges of the photo towards the center):
Step 11
Go to Layer > New > Layer to create a new layer and name it Sparkles_Large_1. Then, drag this layer just below the Sparkles_Large_1 layer in the Layers panel.
Step 12
Now choose the Brush Tool (B), select the Sparkles brush, and set the Diameter of the brush to around 75 px. Then, set the foreground color to #ffffff and brush as shown below (decrease the Diameter of the brush as you brush from the edges of the photo towards the center):
Step 13
Select the Sparkles_Small_1 layer, go to Layer > New Fill Layer > Solid Color to create a new solid color fill layer, name it SS_1_Color, and choose the color #eea815 as shown below:
Step 14
Now press Control-Alt-G on your keyboard to create a clipping mask. Then, select the Sparkles_Small_1 layer and change the Blending Mode of this layer to Linear Dodge (Add).
Step 15
Select the SS_1_Color layer and press Control-J on your keyboard to duplicate it. Then, drag this layer just above the Sparkles_Medium_1 layer in the Layers panel. After that, press Control-Alt-G on your keyboard to create a clipping mask.
Step 16
Now name this layer SM_1_Color. Then, select the Sparkles_Medium_1 layer and change the Blending Mode of this layer to Linear Dodge (Add).
Step 17
Select the SM_1_Color layer and press Control-J on your keyboard to duplicate it. Then, drag this layer just above the Sparkles_Large_1 layer in the Layers panel. After that, press Control-Alt-G on your keyboard to create a clipping mask.
Step 18
Now name this layer SL_1_Color. Then, select the Sparkles_Large_1 layer
and change the Blending Mode of this layer to Linear Dodge (Add).
Step 19
Select the SS_1_Color layer and Shift-click on the Sparkles_Large_2 layer to select all layers between. Then, go to Layer > New > Group from Layers to create a new group from the selected layers and name it Sparkles.
4. How to Create the Focus
Step 1
In this section, we are going to create the focus. Select the Background layer, go to Layer > New Adjustment Layer > Levels to create a new levels adjustment layer, and name it Focus_1.
Step 2
Now Double-click on this layer thumbnail and, in the Properties panel, use the settings below:
Step 3
Choose the Brush Tool (B), pick a soft brush, set the foreground color to #000000, and brush as shown below:
Step 4
Now select the Background layer and press Control-J on your keyboard to duplicate it. Then, go to Filter > Blur > Gaussian Blur and set the Radius to 10 px.
Step 5
Go to Layer > Layer Mask > Reveal All to add a layer mask that reveals the whole layer.
Step 6
Choose the Brush Tool (B), pick a soft brush, set the foreground color to #000000, and brush as shown below:
Step 7
Now name this layer Focus_2.
5. How to Make the Final Adjustments
Step 1
In this section, we are going to make final adjustments to the design. Select the Sparkles folder, go to Layer > New Adjustment Layer > Curves to create a new curves adjustment layer, and name it Color Look.
Step 2
Now Double-click on this layer thumbnail and in the Properties panel enter the settings below:
Step 3
Press D on your keyboard to reset the swatches. Then, go to Layer > New Adjustment Layer > Gradient Map to create a new gradient map adjustment layer and name it Overall Contrast.
Step 4
Now change the Blending Mode of this layer to Overlay and set the Opacity to 10%.
Step 5
Go to Layer > New Adjustment Layer > Vibrance to create a new vibrance adjustment layer and name it Overall Vibrance/Saturation.
Step 6
Now Double-click on this layer thumbnail and in the Properties panel, set the Vibrance to +10 and the Saturation to +5.
Step 7
Go to Layer > New Adjustment Layer > Levels to create a new levels adjustment layer and name it Overall Brightness.
Step 8
Now Double-click on this layer thumbnail and in the Properties panel enter the settings below:
Step 9
Press Control-Alt-Shift-E on your keyboard to make a screenshot, and then press Control-Shift-U to desaturate this layer. Then, go to Filter > Other > High Pass and set the Radius to 2 px.
Step 10
Change the Blending Mode of this layer to Vivid Light and set the Opacity to 50%. Then, name this layer Overall Sharpening.
You Made It!
Congratulations, you have succeeded! Here is our final result:
If you would like to create the even more advanced magic dust effects
below, using just a single click and in only a few minutes, then check
out my Magic Dust 2 Photoshop Action.
The
action works so you simply draw a path over your photo where you want the effect to appear and just
play the action. It’s really that simple! The sparkles remain fully layered, and you have full control over them to customize them—you can move, scale, or rotate them, remove them, or
duplicate them to create even more sparkles! The action
will also create 15 preset color looks that you can choose from.
The action comes with a detailed video tutorial that demonstrates how to use the action and customize the results to get the most out of the effect.
Gradient effects are super easy to achieve in InDesign, and they’re an instant way of bringing your layouts bang up-to-date.
Read on to discover five high-impact gradient effects, from duotone to neon-pastel, that would look fantastic on posters, flyers, stationery, or magazine designs.
If you want to make your gradient effects truly unique, you can easily add cool typography over the top of your design. Head over to GraphicRiver or Envato Elements to source great fonts for your next project.
What You’ll Need to Create Your Gradient Effects
All you need is access to Adobe InDesign to create the majority of these effects.
This is the simplest gradient effect to achieve, and it looks amazing set as a background or as a fill color for shapes or text.
Step 1
Open InDesign and go to File > New > Document. You can set the page to any size you prefer.
If you’re creating your gradient effects for print (as I am here), make sure the Intent is set to Print. For online-friendly images, set the Intent to Web.
With your document created, expand the Swatches panel (Window > Color > Swatches).
Choose New Color Swatch from the panel’s drop-down menu (at top-right). Set the Color Mode to CMYK for print or RGB for web, and set the levels to C=0 M=82 Y=24 K=0 or R=234 G=76 B=125.
Click Add and OK.
Create a second new swatch, C=0 M=17 Y=85 K=0 or R=255 G=211 B=50.
Step 2
Choose New Gradient Swatch from the Swatches panel’s menu.
Name the swatch Duotone Gradient, set the Type to Linear, and choose Swatches from the Stop Color menu.
Click on the left-hand stop on the Gradient Ramp and set this to your first pink swatch, C=0 M=82 Y=24 K=0. Set the right-hand stop to the yellow swatch, C=0 M=17 Y=85 K=0.
Click Add and OK.
Step 3
Create a shape on the page you’d like to apply the gradient to. Use the Rectangle Tool (M) to create a background shape. Here I’ve used the Ellipse Tool (L) to create a circle (holding Shift to keep the proportions).
Apply the Duotone Gradient swatch to the Fill Color of the shape from the Swatches panel.
You can use the Gradient panel (Window > Color > Gradient) to adjust the Angle of the gradient.
2. Blackout Gradient Effect
This is a high-impact gradient effect that gives an eclipse effect. This works best as a standalone effect using a circle design. Why not try placing text or an image inside the inner circle for extra impact?
Step 1
From the Swatches panel (Window > Color > Swatches), choose Create New Swatch. Create a hot pink swatch, C=0 M=87 Y=2 K=0 or R=232 G=59 B=140.
Create two more new swatches:
C=1 M=14 Y=63 K=0 or R=254 G=219 B=117
C=0 M=92 Y=99 K=0 or R=229 G=45 B=20
Step 2
Choose New Gradient Swatch from the Swatches panel’s menu.
Name the swatch Blackout Gradient, set the Type to Linear and choose Swatches from the Stop Color menu.
Click on the left-hand stop on the Gradient Ramp and set this to your hot pink swatch, C=0 M=87 Y=2 K=0. Set the right-hand stop to the pale yellow swatch, C=0 M=92 Y=99 K=0.
Click on the ramp about a third across from the left to add a third stop, setting this to your red swatch, C=0 M=92 Y=99 K=0.
Step 3
Take the Ellipse Tool (L) and drag onto the page to create a small circle. Set the Fill Color to [Black].
Then create a second larger circle over the top, setting the Fill to Blackout Gradient.
Right-Click on this larger circle and choose Arrange > Send to Back.
Step 4
With the Blackout Gradient circle selected, go to Object on the top main menu and choose Effects > Gradient Feather.
Set the Type to Radial, and move the dark stop on the ramp about two-thirds of the way along.
Click OK to exit the window.
Step 5
Edit > Copy and Edit > Paste the gradient circle, sending it to the back as before. Scale it up to a slightly larger size and position it towards the top-left of the design.
Edit > Paste again, moving this circle to the bottom-right of the design.
3. Pride Gradient Effect
Show your support of Pride this summer (LGBT Pride Month takes place in June) with this gradient set in the colors of the Pride flag.
Step 1
From the Swatches panel (Window > Color > Swatches), choose Create New Swatch. Create a red swatch, C=0 M=93 Y=86 K=0 or R=230 G=42 B=40.
Create five more new swatches:
C=0 M=60 Y=83 K=0 or R=240 G=127 B=54
C=8 M=6 Y=93 K=0 or R=245 G=222 B=0
C=80 M=3 Y=86 K=0 or R=12 G=164 B=81
C=82 M=63 Y=0 K=0 or R=66 G=96 B=169
C=38 M=93 Y=0 K=0 or R=171 G=45 B=136
Step 2
Choose New Gradient Swatch from the Swatches panel’s menu.
Name the swatch Pride Gradient, set the Type to Linear, and choose Swatches from the Stop Color menu.
Click on the left-hand stop on the Gradient Ramp and set this to your new red swatch, C=0 M=93 Y=86 K=0. Set the right-hand stop to the violet swatch, C=38 M=93 Y=0 K=0.
Click on the ramp to add four more stops, setting them to (from left to right) orange, yellow, green, and blue from your palette of new swatches.
Then click Add and OK.
Step 3
Create a shape on the page you’d like to apply the gradient to. Here I’ve used the Ellipse Tool (L) to create a circle (holding Shift to keep the proportions).
Apply the Pride Gradient swatch to the Fill Color of the shape from the Swatches panel.
You can use the Gradient panel (Window > Color > Gradient) to adjust the Angle of the gradient.
Step 4
You can set the shape against a dark background for impact.
Use the Rectangle Tool (M) to create a frame across the whole page and set the Fill Color to [Black]. Right-Click on the shape and choose Arrange > Send to Back.
4. Pastel and Neon Double Gradient Effect
This is a popular take on the gradient trend that looks incredible on posters and magazine covers. It’s simple to create, and you can switch up colors to create an almost endless range of results.
Step 1
From the Swatches panel (Window > Color > Swatches), choose Create New Swatch. Create a lavender swatch, C=14 M=65 Y=0 K=0 or R=215 G=119 B=174.
Create three more new swatches:
C=59 M=0 Y=6 K=0 or R=98 G=198 B=233
C=0 M=33 Y=22 K=0 or R=248 G=192 B=186
C=0 M=86 Y=7 K=0 or R=233 G=62 B=136
Step 2
Choose New Gradient Swatch from the Swatches panel’s menu.
Name the swatch Background Gradient, set the Type to Linear and choose Swatches from the Stop Color menu.
Click on the left-hand stop on the Gradient Ramp and set this to your new lavender swatch, C=14 M=65 Y=0 K=0. Set the right-hand stop to the blue swatch, C=59 M=0 Y=6 K=0.
Then click Add and OK.
Step 3
Create a second New Gradient Swatch. Name this swatch Foreground Gradient, set the Type to Linear, and choose Swatches from the Stop Color menu.
Click on the left-hand stop on the Gradient Ramp and set this to your pale pink swatch, C=0 M=33 Y=22 K=0. Set the right-hand stop to the bright pink swatch, C=0 M=86 Y=7 K=0. Click Add and OK.
Step 4
Create a shape on the page you’d like to apply the gradient to. Here I’ve used the Ellipse Tool (L) to create a circle.
Apply the Foreground Gradient swatch to the Fill Color of the shape from the Swatches panel.
Step 5
Create a shape across the whole page using the Rectangle Tool (M) and Right-Click > Arrange > Send to Back.
Apply the Background Gradient swatch to the Fill Color of the shape.
You can adjust the Angle of the gradient and strength of each of the colors from the Gradient panel (Window > Color > Gradient).
5. Retro Gradient Effect
This funky effect takes inspiration from 1960s and 70s graphic design. It’s a more chunky, dissected take on the gradient trend, and it’s great for giving typography a retro flavor.
From the Swatches panel (Window > Color > Swatches), choose Create New Swatch from the panel’s main menu.
Create a yellow swatch, C=3 M=6 Y=91 K=0 or R=255 G=226 B=9.
Create ten more new swatches, to create a full rainbow palette plus one pale swatch and one dark swatch:
C=0 M=44 Y=93 K=0 or R=246 G=159 B=25
C=0 M=59 Y=94 K=0 or R=240 G=128 B=26
C=0 M=92 Y=92 K=0 or R=230 G=45 B=32
C=0 M=93 Y=0 K=0 or R=231 G=36 B=134
C=77 M=100 Y=0 K=0 or R=98 G=36 B=131
C=100 M=90 Y=22 K=5 or R=39 G=52 B=117
C=73 M=9 Y=4 K=0 or R=6 G=173 B=224
C=77 M=4 Y=47 K=0 or R=0 G=170 B=155
C=7 M=3 Y=17 K=0 or R=242 G=241 B=222
C=72 M=67 Y=59 K=79 or R=35 G=31 B=32
Step 2
Use the Type Tool (T) to create a large text frame across the bottom of the page.
Type in a single word, such as ‘Hey’, and from either the top Controls panel or the Character panel (Window > Type & Tables > Character), set the Font to Charlevoix Pro ExtraBold. Blow up the Font Size so that the text extends across the whole page, crossing the trim edge on both sides.
Set your Type Tool cursor between each of the letters and reduce the Kerning (letter-spacing) from the Character panel, so that the letters are pushed closer together.
Set the Font Color, from the Swatches panel, to your new yellow swatch, C=3 M=6 Y=91 K=0.
Step 3
Select the text frame and Edit > Copy, Edit > Paste in Place, using the arrow keys to move this second frame upwards slightly.
Adjust the Font Color to your orange swatch, C=0 M=44 Y=93 K=0.
Step 4
Continue to Edit > Paste in Place more text frames, shifting them upwards and adjusting the Font Color until you have created a gradual rainbow effect, as shown below.
For your final text frame, set the Font Color to your off-black swatch, C=72 M=67 Y=59 K=79.
Step 5
To add a final retro vibe to your effect, create a shape across the whole page using the Rectangle Tool (M) and Right-Click on it and Arrange > Send to Back.
Set the Fill Color to the beige swatch, C=7 M=3 Y=17 K=0.
Hey! Awesome Work!
And there we have it! Five quick and effective gradient effects that can transform any lacklustre layout.
Why not add cool typography to your gradient designs to transform them into posters or flyers? Head over to GraphicRiver or Envato Elements to source great fonts for your next project.
Looking for more InDesign text effect tutorials? Don’t miss these:
Want to give your type an edgy, Mad Max makeover? This super-quick glitch effect only takes 10 minutes to put together, and it makes a good substitute for…
Everyone needs a bit of optimism in their lives, and this neon sign poster is the perfect pick-me-up. Pop it on the office noticeboard, set it as your phone…
In this quick and simple tutorial, you’ll learn how to create a paper cut-out effect which you can apply to any text in Adobe InDesign. This type trend would…
After four hours and some twenty minutes, of which over four hours were spent on tweaking positioning, edges and highlights… I finally had the result below:
My version of the rainbow gradient infinity.
The gradient doesn’t look like in the original illustration, as I chose to generate the rainbow logically instead of using the Dev Tools picker or something like that, but other than that, I think I got pretty close—let’s see how I did that!
Markup
As you’ve probably already guessed from the title, the HTML is just one element:
<div class='∞'></div>
Styling
Deciding on the approach
The first idea that might come to mind when seeing the above would be using conic gradients as border images. Unfortunately, border-image and border-radius don’t play well together, as illustrated by the interactive demo below:
Whenever we set a border-image, border-radius just gets ignored, so using the two together is sadly not an option.
So the approach we take here is using conic-gradient() backgrounds and then getting rid of the part in the middle with the help of a mask. Let’s see how that works!
Creating the two ∞ halves
We first decide on an outer diameter.
$do: 12.5em;
We create the two halves of the infinity symbol using the ::before and ::after pseudo-elements of our .∞ element. In order to place these two pseudo-elements next to one another, we use a flex layout on their parent (the infinity element .∞). Each of these has both the width and the height equal to the outer diameter $do. We also round them with a border-radius of 50% and we give them a dummy background so we can see them.
In order to create the conic-gradient() backgrounds for the two haves, we must first understand how the conic-gradient() function works.
If inside the conic-gradient() function we have a list of stops without explicit positions, then the first is taken to be at 0% (or 0deg, same thing), the last is taken to be at 100% (or 360deg), while all those left are distributed evenly in the [0%, 100%] interval.
If we have just 2 stops, it’s simple. The first is at 0%, the second (and last) at 100% and there are no other stops in between.
If we have 3 stops, the first is at 0%, the last (third) at 100%, while the second is dead in the middle of the [0%, 100%] interval, at 50%.
If we have 4 stops, the first is at 0%, the last (fourth) at 100%, while the second and third split the [0%, 100%] interval into 3 equal intervals, being positioned at 33.(3)% and 66.(6)% respectively.
If we have 5 stops, the first is at 0%, the last (fifth) at 100%, while the second, third and fourth split the [0%, 100%] interval into 4 equal intervals being positioned at 25%, 50% and 75% respectively.
If we have 6 stops, the first is at 0%, the last (sixth) at 100%, while the second, third, fourth and fifth split the [0%, 100%] interval into 5 equal intervals being positioned at 20%, 40%, 60% and 80% respectively.
In general, if we have n stops, the first is at 0%, the last at 100%, while the ones in between split the [0%, 100%] interval into n-1 eqial intervals spanning 100%/(n-1) each. If we give the stops 0-based indices, then each one of them is positioned at i*100%/(n-1).
For the first one, i is 0, which gives us 0*100%/(n-1) = 0%.
For the last (n-th) one, i is n-1, which gives us (n-1)*100%/(n-1) = 100%.
Here, we choose to use 9 stops which means we split the [0%, 100%] interval into 8 equal intervals.
Alright, but how do we get the stop list?
The hsl() stops
Well, for simplicity, we choose to generate it as a list of HSL values. We keep the saturation and the lightness fixed and we vary the hue. The hue is an angle value that goes from 0 to 360, as we can see here:
Visual representation of the hue scale from 0 to 360 (saturation and lightness being kept constant).
With this in mind, we can construct a list of hsl() stops with fixed saturation and lightness and varying hue if we know the start hue$hue-start, the hue range$hue-range (this is the end hue minus the start hue) and the number of stops$num-stops.
Let’s say we keep the saturation and the lightness fixed at 85% and 57%, respectively (arbitrary values that can probably be tweaked for better results) and, for example, we might go from a start hue of 240 to an end hue of 300 and use 4 stops.
In order to generate this list of stops, we use a get-stops() function that takes these three things as arguments:
We create the list of stops $list which is originally empty (and which we’ll return at the end after we populate it). We also compute the span of one of the equal intervals our stops split the full start to end interval into ($unit).
@function get-stops($hue-start, $hue-range, $num-stops) {
$list: ();
$unit: $hue-range/($num-stops - 1);
/* populate the list of stops $list */
@return $list
}
In order to populate our $list, we loop through the stops, compute the current hue, use the current hue to generate the hsl() value at that stop and then then add it to the list of stops:
@for $i from 0 to $num-stops {
$hue-curr: $hue-start + $i*$unit;
$list: $list, hsl($hue-curr, 85%, 57%);
}
We can now use the stop list this function returns for any kind of gradient, as it can be seen from the usage examples for this function shown in the interactive demo below (navigation works both by using the previous/next buttons on the sides as well as the arrow keys and the PgDn/ PgUp keys):
Note how, when our range passes one end of the [0, 360] interval, it continues from the other end. For example, when the start hue is 30 and the range is -210 (the fourth example), we can only go down to 0, so then we continue going down from 360.
Conic gradients for our two halves
Alright, but how do we determine the $hue-start and the $hue-range for our particular case?
In the original image, we draw a line in between the central points of the two halves of the loop and, starting from this line, going clockwise in both cases, we see where we start from and where we end up in the [0, 360] hue interval and what other hues we pass through.
We start from the line connecting the central points of the two halves and we go around them in the clockwise direction.
To simplify things, we consider we pass through the whole [0, 360] hue scale going along our infinity symbol. This means the range for each half is 180 (half of 360) in absolute value.
Keywords to hue values correspondence for saturation and lightness fixed at 100% and 50% respectively.
On the left half, we start from something that looks like it’s in between some kind of cyan (hue 180) and some kind of lime (hue 120), so we take the start hue to be the average of the hues of these two (180 + 120)/2 = 150.
The plan for the left half.
We get to some kind of red, which is 180 away from the start value, so at 330, whether we subtract or add 180:
So… do we go up or down? Well, we pass through yellows which are around 60 on the hue scale, so that’s going down from 150, not up. Going down means our range is negative (-180).
The plan for the right half.
On the right half, we also start from the same hue in between cyan and lime (150) and we also end at the same kind of red (330), but this time we pass through blues, which are around 240, meaning we go up from our start hue of 150, so our range is positive in this case (180).
As far as the number of stops goes, 9 should suffice.
Now update our code using the values for the left half as the defaults for our function:
@function get-stops($hue-start: 150, $hue-range: -180, $num-stops: 9) {
/* same as before */
}
.∞ {
display: flex;
&:before, &:after {
/* same as before */
background: conic-gradient(get-stops());
}
&:after {
background: conic-gradient(get-stops(150, 180));
}
}
And now our two discs have conic-gradient() backgrounds:
However, we don’t want these conic gradients to start from the top.
For the first disc, we want it to start from the right—that’s at 90° from the top in the clockwise (positive) direction. For the second disc, we want it to start from the left—that’s at 90° from the top in the other (negative) direction, which is equivalent to 270° from the top in the clockwise direction (because negative angles don’t appear to work from some reason).
Angular offsets from the top for our two halves.
Let’s modify our code to achieve this:
.∞ {
display: flex;
&:before, &:after {
/* same as before */
background: conic-gradient(from 90deg, get-stops());
}
&:after {
background: conic-gradient(from 270deg, get-stops(150, 180));
}
}
The next step is to cut holes out of our two halves. We do this with a mask or, more precisely, with a radial-gradient() one. This cuts out Edge support for now, but since it’s something that’s in development, it’s probably going to be a cross-browser solution at some point in the not too far future.
Remember that CSS gradient masks are alpha masks by default (and only Firefox currently allows changing this via mask-mode), meaning that only the alpha channel matters. Overlaying the mask over our element makes every pixel of this element use the alpha channel of the corresponding pixel of the mask. If the mask pixel is completely transparent (its alpha value is 0), then so will the corresponding pixel of the element.
In order to create the mask, we compute the outer radius $ro (half the outer diameter $do) and the inner radius $ri (a fraction of the outer radius $ro).
$ro: .5*$do;
$ri: .52*$ro;
$m: radial-gradient(transparent $ri, red 0);
We then set the mask on our two halves:
.∞ {
/* same as before */
&:before, &:after {
/* same as before */
mask: $m;
}
}
This looks perfect in Firefox, but the edges of radial gradients with abrupt transitions from one stop to another look ugly in Chrome and, consequently, so do the inner edges of our rings.
Close-up of the inner edge of the right half in Chrome.
The fix here would be not to have an abrupt transition between stops, but spread it out over a small distance, let’s say half a pixel:
$m: radial-gradient(transparent calc(#{$ri} - .5px), red $ri);
Close-up of the inner edge of the right half in Chrome after spreading out the transition between stops over half a pixel.
The following step is to offset the two halves such that they actually form an infinity symbol. The visible circular strips both have the same width, the difference between the outer radius $ro and the inner radius $ri. This means we need to shift each laterally by half this difference $ri - $ri.
.∞ {
/* same as before */
&:before, &:after {
/* same as before */
margin: 0 (-.5*($ro - $ri));
}
}
We’re getting closer, but we still have a very big problem here. We don’t want the right part of the loop to be completely over the left one. Instead, we want the top half of the right part to be over that of the left part and the bottom half of the left part to be over that of the right part.
So how do we achieve that?
We take a similar approach to that presented in an older article: using 3D!
In order to better understand how this works, consider the two card example below. When we rotate them around their x axes, they’re not in the plane of the screen anymore. A positive rotation brings the bottom forward and pushes the top back. A negative rotation brings the top forward and pushes the bottom back.
So if we give the left one a positive rotation and the right one a negative rotation, then the top half of the right one appears in front of the top half of the left one and the other way around for the bottom halves.
Addiing perspective makes what’s closer to our eyes appears bigger and what’s further away appears smaller and we use way smaller angles. Without it, we have the 3D plane intersection without the 3D appearance.
Note that both our halves need to be in the same 3D context, something that’s achieved by setting transform-style: preserve-3d on the .∞ element.
.∞ {
/* same as before */
transform-style: preserve-3d;
&:before, &:after {
/* same as before */
transform: rotatex(1deg);
}
&:after {
/* same as before */
transform: rotatex(-1deg);
}
}
This still isn’t perfect though. Since the inner edges of our two rings are a bit blurry, the transition in between them and the crisp outer ones looks a bit odd, so maybe we can do better there:
A quick fix here would be to add a radial-gradient() cover on each of the two halves. This cover is transparent white for most of the unmasked part of the two halves and goes to solid white along both their inner and outer edges such that we have nice continuity:
$gc: radial-gradient(#fff $ri, rgba(#fff, 0) calc(#{$ri} + 1px),
rgba(#fff, 0) calc(#{$ro} - 1px), #fff calc(#{$ro} - .5px));
.∞ {
/* same as before */
&:before, &:after {
/* same as before */
background: $gc, conic-gradient(from 90deg, get-stops());
}
&:after {
/* same as before */
background: $gc, conic-gradient(from 270deg, get-stops(150, 180));
}
}
The benefit becomes more obvious once we add a dark background to the body:
No more sharp contrast between inner and outer edges.
The final result
Finally, we add some prettifying touches by layering some more subtle radial gradient highlights over the two halves. This was the part that took me the most because it involved the least amount of logic and the most amount of trial and error. At this point, I just layered the original image underneath the .∞ element, made the two halves semi-transparent and started adding gradients and tweaking them until they pretty much matched the highlights. And you can see when I got sick of it because that’s when the position values become rougher approximations with fewer decimals.
Another cool touch would be drop shadows on the whole thing using a filter on the body. Sadly, this breaks the 3D intersection effect in Firefox, which means we cannot add it there, too.
When I first shared this demo, I got asked about animating it. I initially thought this would be complicated, but then it hit me that, thanks to Houdini, it doesn’t have to be!
As mentioned in my previous article, we can animate in between stops, let’s say from a red to a blue. In our case, the saturation and lightness components of the hsl() values used to generate the rainbow gradient stay constant, all that changes is the hue.
For each and every stop, the hue goes from its initial value to its initial value plus 360, thus passing through the whole hue scale in the process. This is equivalent to keeping the initial hue constant and varying an offset. This offset --off is the custom property we animate.
Sadly, this means support is limited to Blink browsers with the Experimental Web Platform features flag enabled.
The Experimental Web Platform features flag enabled in Chrome.
Still, let’s see how we put it all into code!
For starters, we modify the get-stops() function such that the current hue at any time is the initial hue of the current stop $hue-curr plus our offset --off:
If you work with video, your life just got a little easier. Instead of having to create everything from scratch or chase around the web looking for stock footage, motion graphics and After Effects templates, you can now access more than 60,000 top-quality video items, all in one place.