I used to be lucky final 12 months to get approached by Shawn Wang (swyx) about performing some work for Temporal. The concept was to forged my artistic eye over what was on the positioning and give you some concepts that may give the positioning a bit of “one thing” additional. This was fairly a neat problem as I take into account myself extra of a developer than a designer. However I really like studying and leveling up the design facet of my sport.
One of many concepts I got here up with was this interactive starry backdrop. You possibly can see it working on this shared demo:
Blockquote idea utilizing a bit of perspective and CSS customized properties 😎
Been having fun with the artistic freedom to give you issues @temporalio 🤓
Including a splash of caprice to supplies 🎉
⚒️ @reactjs && @tailwindcss (Web site is NextJS)
👉 https://t.co/GHtUEcl674 through @CodePen pic.twitter.com/s9xP2tRrOx
— Jhey 🔨🐻✨ (@jh3yy) July 2, 2021
The neat factor about this design is that it’s constructed as a drop-in React part. And it’s tremendous configurable within the sense that when you’ve put collectively the foundations for it, you can also make it utterly your personal. Don’t need stars? Put one thing else in place. Don’t need randomly positioned particles? Place them in a constructed means. You might have complete management of what to bend it to your will.
So, let’s have a look at how we will create this drop-in part to your website! As we speak’s weapons of selection? React, GreenSock and HTML <canvas>. The React half is completely optionally available, after all, however, having this interactive backdrop as a drop-in part makes it one thing you’ll be able to make use of on different tasks.
Let’s begin by scaffolding a fundamental app
import React from ‘https://cdn.skypack.dev/react’
import ReactDOM from ‘https://cdn.skypack.dev/react-dom’
import gsap from ‘https://cdn.skypack.dev/gsap’
const ROOT_NODE = doc.querySelector(‘#app’)
const Starscape = () => <h1>Cool Thingzzz!</h1>
const App = () => <Starscape/>
ReactDOM.render(<App/>, ROOT_NODE)
Very first thing we have to do is render a <canvas> component and seize a reference to it that we will use inside React’s useEffect. For these not utilizing React, retailer a reference to the <canvas> in a variable as a substitute.
const Starscape = () => {
const canvasRef = React.useRef(null)
return <canvas ref={canvasRef} />
}
Our <canvas> goes to want some kinds, too. For starters, we will make it so the canvas takes up the total viewport measurement and sits behind the content material:
canvas {
place: mounted;
inset: 0;
background: #262626;
z-index: -1;
top: 100vh;
width: 100vw;
}
Cool! However not a lot to see but.
We’d like stars in our sky
We’re going to “cheat” a bit of right here. We aren’t going to attract the “basic” pointy star form. We’re going to make use of circles of differing opacities and sizes.
Draw a circle on a <canvas> is a case of grabbing a context from the <canvas> and utilizing the arc perform. Let’s render a circle, err star, within the center. We are able to do that inside a React useEffect:
const Starscape = () => {
const canvasRef = React.useRef(null)
const contextRef = React.useRef(null)
React.useEffect(() => {
canvasRef.present.width = window.innerWidth
canvasRef.present.top = window.innerHeight
contextRef.present = canvasRef.present.getContext(‘2nd’)
contextRef.present.fillStyle = ‘yellow’
contextRef.present.beginPath()
contextRef.present.arc(
window.innerWidth / 2, // X
window.innerHeight / 2, // Y
100, // Radius
0, // Begin Angle (Radians)
Math.PI * 2 // Finish Angle (Radians)
)
contextRef.present.fill()
}, [])
return <canvas ref={canvasRef} />
}
So what we have now is a giant yellow circle:
It is a good begin! The remainder of our code will happen inside this useEffect perform. That’s why the React half is kinda optionally available. You possibly can extract this code out and use it in whichever type you want.
We’d like to consider how we’re going to generate a bunch of “stars” and render them. Let’s create a LOAD perform. This perform goes to deal with producing our stars in addition to the overall <canvas> setup. We are able to additionally transfer the sizing logic of the <canvas> sizing logic into this perform:
const LOAD = () => {
const VMIN = Math.min(window.innerHeight, window.innerWidth)
const STAR_COUNT = Math.ground(VMIN * densityRatio)
canvasRef.present.width = window.innerWidth
canvasRef.present.top = window.innerHeight
starsRef.present = new Array(STAR_COUNT).fill().map(() => ({
x: gsap.utils.random(0, window.innerWidth, 1),
y: gsap.utils.random(0, window.innerHeight, 1),
measurement: gsap.utils.random(1, sizeLimit, 1),
scale: 1,
alpha: gsap.utils.random(0.1, defaultAlpha, 0.1),
}))
}
Our stars at the moment are an array of objects. And every star has properties that outline their traits, together with:
x: The star’s place on the x-axisy: The star’s place on the y-axismeasurement: The star’s measurement, in pixelsscale: The star’s scale, which can come into play once we work together with the partalpha: The star’s alpha worth, or opacity, which will even come into play throughout interactions
We are able to use GreenSock’s random() technique to generate a few of these values. You might also be questioning the place sizeLimit, defaultAlpha, and densityRatio got here from. These at the moment are props we will cross to the Starscape part. We’ve offered some default values for them:
const Starscape = ({ densityRatio = 0.5, sizeLimit = 5, defaultAlpha = 0.5 }) => {
A randomly generated star Object would possibly appear to be this:
{
“x”: 1252,
“y”: 29,
“measurement”: 4,
“scale”: 1,
“alpha”: 0.5
}
However, we have to see these stars and we do this by rendering them. Let’s create a RENDER perform. This perform will loop over our stars and render every of them onto the <canvas> utilizing the arc perform:
const RENDER = () => {
contextRef.present.clearRect(
0,
0,
canvasRef.present.width,
canvasRef.present.top
)
starsRef.present.forEach(star => {
contextRef.present.fillStyle = `hsla(0, 100%, 100%, ${star.alpha})`
contextRef.present.beginPath()
contextRef.present.arc(star.x, star.y, star.measurement / 2, 0, Math.PI * 2)
contextRef.present.fill()
})
}
Now, we don’t want that clearRect perform for our present implementation as we’re solely rendering as soon as onto a clean <canvas>. However clearing the <canvas> earlier than rendering something isn’t a foul behavior to get into, And it’s one we’ll want as we make our canvas interactive.
Think about this demo that reveals the impact of not clearing between frames.
Our Starscape part is beginning to take form.
See the code
const Starscape = ({ densityRatio = 0.5, sizeLimit = 5, defaultAlpha = 0.5 }) => {
const canvasRef = React.useRef(null)
const contextRef = React.useRef(null)
const starsRef = React.useRef(null)
React.useEffect(() => {
contextRef.present = canvasRef.present.getContext(‘2nd’)
const LOAD = () => {
const VMIN = Math.min(window.innerHeight, window.innerWidth)
const STAR_COUNT = Math.ground(VMIN * densityRatio)
canvasRef.present.width = window.innerWidth
canvasRef.present.top = window.innerHeight
starsRef.present = new Array(STAR_COUNT).fill().map(() => ({
x: gsap.utils.random(0, window.innerWidth, 1),
y: gsap.utils.random(0, window.innerHeight, 1),
measurement: gsap.utils.random(1, sizeLimit, 1),
scale: 1,
alpha: gsap.utils.random(0.1, defaultAlpha, 0.1),
}))
}
const RENDER = () => {
contextRef.present.clearRect(
0,
0,
canvasRef.present.width,
canvasRef.present.top
)
starsRef.present.forEach(star => {
contextRef.present.fillStyle = `hsla(0, 100%, 100%, ${star.alpha})`
contextRef.present.beginPath()
contextRef.present.arc(star.x, star.y, star.measurement / 2, 0, Math.PI * 2)
contextRef.present.fill()
})
}
LOAD()
RENDER()
}, [])
return <canvas ref={canvasRef} />
}
Have a mess around with the props on this demo to see how they have an effect on the the best way stars are rendered.
Earlier than we go additional, you could have observed a quirk within the demo the place resizing the viewport distorts the <canvas>. As a fast win, we will rerun our LOAD and RENDER features on resize. Usually, we’ll wish to debounce this, too. We are able to add the next code into our useEffect name. Word how we additionally take away the occasion listener within the teardown.
// Naming issues is difficult…
const RUN = () => {
LOAD()
RENDER()
}
RUN()
// Arrange occasion dealing with
window.addEventListener(‘resize’, RUN)
return () => {
window.removeEventListener(‘resize’, RUN)
}
Cool. Now once we resize the viewport, we get a brand new generated starry.
Interacting with the starry backdrop
Now for the enjoyable half! Let’s make this factor interactive.
The concept is that as we transfer our pointer across the display, we detect the proximity of the celebs to the mouse cursor. Relying on that proximity, the celebs each brighten and scale up.
We’re going to want so as to add one other occasion listener to tug this off. Let’s name this UPDATE. It will work out the space between the pointer and every star, then tween every star’s scale and alpha values. To verify these tweeted values are appropriate, we will use GreenSock’s mapRange() utility. Actually, inside our LOAD perform, we will create references to some mapping features in addition to a measurement unit then share these between the features if we have to.
Right here’s our new LOAD perform. Word the brand new props for scaleLimit and proximityRatio. They’re used to restrict the vary of how huge or small a star can get, plus the proximity at which to base that on.
const Starscape = ({
densityRatio = 0.5,
sizeLimit = 5,
defaultAlpha = 0.5,
scaleLimit = 2,
proximityRatio = 0.1
}) => {
const canvasRef = React.useRef(null)
const contextRef = React.useRef(null)
const starsRef = React.useRef(null)
const vminRef = React.useRef(null)
const scaleMapperRef = React.useRef(null)
const alphaMapperRef = React.useRef(null)
React.useEffect(() => {
contextRef.present = canvasRef.present.getContext(‘2nd’)
const LOAD = () => {
vminRef.present = Math.min(window.innerHeight, window.innerWidth)
const STAR_COUNT = Math.ground(vminRef.present * densityRatio)
scaleMapperRef.present = gsap.utils.mapRange(
0,
vminRef.present * proximityRatio,
scaleLimit,
1
);
alphaMapperRef.present = gsap.utils.mapRange(
0,
vminRef.present * proximityRatio,
1,
defaultAlpha
);
canvasRef.present.width = window.innerWidth
canvasRef.present.top = window.innerHeight
starsRef.present = new Array(STAR_COUNT).fill().map(() => ({
x: gsap.utils.random(0, window.innerWidth, 1),
y: gsap.utils.random(0, window.innerHeight, 1),
measurement: gsap.utils.random(1, sizeLimit, 1),
scale: 1,
alpha: gsap.utils.random(0.1, defaultAlpha, 0.1),
}))
}
}
And right here’s our UPDATE perform. It calculates the space and generates an acceptable scale and alpha for a star:
const UPDATE = ({ x, y }) => {
starsRef.present.forEach(STAR => {
const DISTANCE = Math.sqrt(Math.pow(STAR.x – x, 2) + Math.pow(STAR.y – y, 2));
gsap.to(STAR, {
scale: scaleMapperRef.present(
Math.min(DISTANCE, vminRef.present * proximityRatio)
),
alpha: alphaMapperRef.present(
Math.min(DISTANCE, vminRef.present * proximityRatio)
)
});
})
};
However wait… it doesn’t do something?
Nicely, it does. However, we haven’t set our part as much as present updates. We have to render new frames as we work together. We are able to attain for requestAnimationFrame usually. However, as a result of we’re utilizing GreenSock, we will make use of gsap.ticker. That is sometimes called “the heartbeat of the GSAP engine” and it’s is an effective substitute for requestAnimationFrame.
To make use of it, we add the RENDER perform to the ticker and ensure we take away it within the teardown. One of many neat issues about utilizing the ticker is that we will dictate the variety of frames per second (fps). I prefer to go along with a “cinematic” 24fps:
// Take away RUN
LOAD()
gsap.ticker.add(RENDER)
gsap.ticker.fps(24)
window.addEventListener(‘resize’, LOAD)
doc.addEventListener(‘pointermove’, UPDATE)
return () => {
window.removeEventListener(‘resize’, LOAD)
doc.removeEventListener(‘pointermove’, UPDATE)
gsap.ticker.take away(RENDER)
}
Word how we’re now additionally operating LOAD on resize. We additionally want to ensure our scale is being picked up in that RENDER perform when utilizing arc:
const RENDER = () => {
contextRef.present.clearRect(
0,
0,
canvasRef.present.width,
canvasRef.present.top
)
starsRef.present.forEach(star => {
contextRef.present.fillStyle = `hsla(0, 100%, 100%, ${star.alpha})`
contextRef.present.beginPath()
contextRef.present.arc(
star.x,
star.y,
(star.measurement / 2) * star.scale,
0,
Math.PI * 2
)
contextRef.present.fill()
})
}
It really works! 🙌
It’s a really refined impact. However, that’s intentional as a result of, whereas it’s is tremendous neat, we don’t need this form of factor to distract from the precise content material. I’d advocate enjoying with the props for the part to see totally different results. It is sensible to set all the celebs to low alpha by default too.
The next demo permits you to play with the totally different props. I’ve gone for some fairly standout defaults right here for the sake of demonstration! However keep in mind, this text is extra about displaying you the methods so you’ll be able to go off and make your personal cool backdrops — whereas being conscious of the way it interacts with content material.
Refinements
There may be one concern with our interactive starry backdrop. If the mouse cursor leaves the <canvas>, the celebs keep shiny and upscaled however we would like them to return to their unique state. To repair this, we will add an additional handler for pointerleave. When the pointer leaves, this tweens the entire stars all the way down to scale 1 and the unique alpha worth set by defaultAlpha.
const EXIT = () => {
gsap.to(starsRef.present, {
scale: 1,
alpha: defaultAlpha,
})
}
// Arrange occasion dealing with
window.addEventListener(‘resize’, LOAD)
doc.addEventListener(‘pointermove’, UPDATE)
doc.addEventListener(‘pointerleave’, EXIT)
return () => {
window.removeEventListener(‘resize’, LOAD)
doc.removeEventListener(‘pointermove’, UPDATE)
doc.removeEventListener(‘pointerleave’, EXIT)
gsap.ticker.take away(RENDER)
}
Neat! Now our stars reduce down and return to their earlier alpha when the mouse cursor leaves the scene.
Bonus: Including an Easter egg
Earlier than we wrap up, let’s add a bit of Easter egg shock to our interactive starry backdrop. Ever heard of the Konami Code? It’s a well-known cheat code and a cool means so as to add an Easter egg to our part.
We are able to virtually do something with the backdrop as soon as the code runs. Like, we may make all the celebs pulse in a random means for instance. Or they might come to life with further colours? It’s a chance to get artistic with issues!
We’re going hear for keyboard occasions and detect whether or not the code will get entered. Let’s begin by making a variable for the code:
const KONAMI_CODE =
‘arrowup,arrowup,arrowdown,arrowdown,arrowleft,arrowright,arrowleft,arrowright,keyb,keya’;
Then we create a second impact inside our the starry backdrop. It is a good method to preserve a separation of issues in that one impact handles all of the rendering, and the opposite handles the Easter egg. Particularly, we’re listening for keyup occasions and test whether or not our enter matches the code.
const codeRef = React.useRef([])
React.useEffect(() => {
const handleCode = e => {
codeRef.present = […codeRef.current, e.code]
.slice(
codeRef.present.size > 9 ? codeRef.present.size – 9 : 0
)
if (codeRef.present.be part of(‘,’).toLowerCase() === KONAMI_CODE) {
// Get together in right here!!!
}
}
window.addEventListener(‘keyup’, handleCode)
return () => {
window.removeEventListener(‘keyup’, handleCode)
}
}, [])
We retailer the person enter in an Array that we retailer inside a ref. As soon as we hit the get together code, we will clear the Array and do no matter we would like. For instance, we might create a gsap.timeline that does one thing to our stars for a given period of time. If so, we don’t wish to permit Konami code to enter whereas the timeline is lively. As a substitute, we will retailer the timeline in a ref and make one other test earlier than operating the get together code.
const partyRef = React.useRef(null)
const isPartying = () =>
partyRef.present &&
partyRef.present.progress() !== 0 &&
partyRef.present.progress() !== 1;
For this instance, I’ve created a bit of timeline that colours every star and strikes it to a brand new place. This requires updating our LOAD and RENDER features.
First, we want every star to now have its personal hue, saturation and lightness:
// Producing stars! ⭐️
starsRef.present = new Array(STAR_COUNT).fill().map(() => ({
hue: 0,
saturation: 0,
lightness: 100,
x: gsap.utils.random(0, window.innerWidth, 1),
y: gsap.utils.random(0, window.innerHeight, 1),
measurement: gsap.utils.random(1, sizeLimit, 1),
scale: 1,
alpha: defaultAlpha
}));
Second, we have to take these new values into consideration when rendering takes place:
starsRef.present.forEach((star) => {
contextRef.present.fillStyle = `hsla(
${star.hue},
${star.saturation}%,
${star.lightness}%,
${star.alpha}
)`;
contextRef.present.beginPath();
contextRef.present.arc(
star.x,
star.y,
(star.measurement / 2) * star.scale,
0,
Math.PI * 2
);
contextRef.present.fill();
});
And right here’s the enjoyable little bit of code that strikes all the celebs round:
partyRef.present = gsap.timeline().to(starsRef.present, {
scale: 1,
alpha: defaultAlpha
});
const STAGGER = 0.01;
for (let s = 0; s < starsRef.present.size; s++) {
partyRef.present
.to(
starsRef.present[s],
{
onStart: () => {
gsap.set(starsRef.present[s], {
hue: gsap.utils.random(0, 360),
saturation: 80,
lightness: 60,
alpha: 1,
})
},
onComplete: () => {
gsap.set(starsRef.present[s], {
saturation: 0,
lightness: 100,
alpha: defaultAlpha,
})
},
x: gsap.utils.random(0, window.innerWidth),
y: gsap.utils.random(0, window.innerHeight),
length: 0.3
},
s * STAGGER
);
}
From there, we generate a brand new timeline and tween the values of every star. These new values get picked up by RENDER. We’re including a stagger by positioning every tween within the timeline utilizing GSAP’s place parameter.
That’s it!
That’s one method to make an interactive starry backdrop to your website. We mixed GSAP and an HTML <canvas>, and even sprinkled in some React that makes it extra configurable and reusable. We even dropped an Easter egg in there!
The place can you are taking this part from right here? How would possibly you apply it to a website? The mix of GreenSock and <canvas> is numerous enjoyable and I’m trying ahead to seeing what you make! Listed below are a pair extra concepts to get your artistic juices flowing…
An Interactive Starry Backdrop for Content material initially printed on CSS-Tips. You need to get the e-newsletter.
Subscribe to MarketingSolution.
Receive web development discounts & web design tutorials.
Now! Lets GROW Together!