I'm trying to make a cursor that leaves a trail of magic dust like in that intro to any Disney film: example. So the way I see it is it's split into two parts. 1. the trail and 2. similar trail that falls and fades out. So far I have made the basic trail work quite well, the code below is for the falling trail and essentially a copy of it with a css animation..
The problem I'm having is it's jittering a lot. I'm guessing the css animation is not great for performance and is causing this to really jitter on my page. I've just read up on requestAnimationFrame but am new to this so not sure how to implement it.. How could I use requestAnimationFrame instead of css here?
I also think creating the animation custom in js would allow there to also be a random offset in the animation of the falling particles.. much more like in the video.
window.addEventListener('mousemove', function (e) {
//trail
[1, .9, .8, .5, .25, .6, .4, .3, .2].forEach(function (i) {
var j = (1 - i) * 50;
var elem = document.createElement('div');
var size = Math.ceil(Math.random() * 10 * i) + 'px';
elem.style.position = 'fixed';
elem.style.zIndex = 6;
elem.style.top = e.pageY - window.scrollY + Math.round(Math.random() * j - j / 2) + 'px';
elem.style.left = e.pageX + Math.round(Math.random() * j - j / 2) + 'px';
elem.style.width = size;
elem.style.opacity = "0.5";
elem.style.height = size;
elem.style.background = 'hsla(' +
Math.round(Math.random() * 160) + ', ' +
'60%, ' +
'90%, ' +
i + ')';
elem.style.borderRadius = size;
elem.style.pointerEvents = 'none';
document.body.appendChild(elem);
window.setTimeout(function () {
document.body.removeChild(elem);
}, Math.round(Math.random() * i * 1000));
});
// falling trail
[1, .9, .8, .5, .25, .6, .3, .2].forEach(function (i) {
var j = (1 - i) * 50;
var elem = document.createElement('div');
var size = Math.ceil(Math.random() * 10 * i) + 'px';
elem.style.position = 'fixed';
elem.style.zIndex = 6;
elem.style.top = e.pageY - window.scrollY + Math.round(Math.random() * j - j / 2) + 'px';
elem.style.left = e.pageX + Math.round(Math.random() * j - j / 2) + 'px';
elem.style.width = size;
elem.style.opacity = "0.5";
elem.style.height = size;
elem.style.animation = "fallingsparkles 1s";
elem.style.background = 'hsla(' +
Math.round(Math.random() * 160) + ', ' +
'60%, ' +
'90%, ' +
i + ')';
elem.style.borderRadius = size;
elem.style.pointerEvents = 'none';
document.body.appendChild(elem);
window.setTimeout(function () {
document.body.removeChild(elem);
}, Math.round(Math.random() * i * 1000));
});
}, false);
body {
width:100%;
height:100%;
background-color: #000;
}
@keyframes fallingsparkles {
from {
transform: translateY(0);
}
to {
transform: translateY(50px);
}
}
<body></body>
I'm trying to make a cursor that leaves a trail of magic dust like in that intro to any Disney film: example. So the way I see it is it's split into two parts. 1. the trail and 2. similar trail that falls and fades out. So far I have made the basic trail work quite well, the code below is for the falling trail and essentially a copy of it with a css animation..
The problem I'm having is it's jittering a lot. I'm guessing the css animation is not great for performance and is causing this to really jitter on my page. I've just read up on requestAnimationFrame but am new to this so not sure how to implement it.. How could I use requestAnimationFrame instead of css here?
I also think creating the animation custom in js would allow there to also be a random offset in the animation of the falling particles.. much more like in the video.
window.addEventListener('mousemove', function (e) {
//trail
[1, .9, .8, .5, .25, .6, .4, .3, .2].forEach(function (i) {
var j = (1 - i) * 50;
var elem = document.createElement('div');
var size = Math.ceil(Math.random() * 10 * i) + 'px';
elem.style.position = 'fixed';
elem.style.zIndex = 6;
elem.style.top = e.pageY - window.scrollY + Math.round(Math.random() * j - j / 2) + 'px';
elem.style.left = e.pageX + Math.round(Math.random() * j - j / 2) + 'px';
elem.style.width = size;
elem.style.opacity = "0.5";
elem.style.height = size;
elem.style.background = 'hsla(' +
Math.round(Math.random() * 160) + ', ' +
'60%, ' +
'90%, ' +
i + ')';
elem.style.borderRadius = size;
elem.style.pointerEvents = 'none';
document.body.appendChild(elem);
window.setTimeout(function () {
document.body.removeChild(elem);
}, Math.round(Math.random() * i * 1000));
});
// falling trail
[1, .9, .8, .5, .25, .6, .3, .2].forEach(function (i) {
var j = (1 - i) * 50;
var elem = document.createElement('div');
var size = Math.ceil(Math.random() * 10 * i) + 'px';
elem.style.position = 'fixed';
elem.style.zIndex = 6;
elem.style.top = e.pageY - window.scrollY + Math.round(Math.random() * j - j / 2) + 'px';
elem.style.left = e.pageX + Math.round(Math.random() * j - j / 2) + 'px';
elem.style.width = size;
elem.style.opacity = "0.5";
elem.style.height = size;
elem.style.animation = "fallingsparkles 1s";
elem.style.background = 'hsla(' +
Math.round(Math.random() * 160) + ', ' +
'60%, ' +
'90%, ' +
i + ')';
elem.style.borderRadius = size;
elem.style.pointerEvents = 'none';
document.body.appendChild(elem);
window.setTimeout(function () {
document.body.removeChild(elem);
}, Math.round(Math.random() * i * 1000));
});
}, false);
body {
width:100%;
height:100%;
background-color: #000;
}
@keyframes fallingsparkles {
from {
transform: translateY(0);
}
to {
transform: translateY(50px);
}
}
<body></body>
Share
Improve this question
edited Oct 1, 2020 at 18:26
AGrush
asked Oct 1, 2020 at 16:42
AGrushAGrush
1,16716 silver badges33 bronze badges
5
- 1 Can you link to a youtube video for reference? – Raphael Serota Commented Oct 1, 2020 at 16:52
-
Could it be related to the fact that that the
i
value is based on a uneven progression from 1 down to 0.2?[1, .9, .8, .5, .25, .6, .3, .2]
Testing it in a snippet with[1, .9, .8, .7, .6, .5, .4, .3, .2]
in both places, it runs a bit smoother. – ATD Commented Oct 1, 2020 at 17:18 - youtube./watch?v=kzYfRLMtP9c#t=0m21s – AGrush Commented Oct 1, 2020 at 18:24
- i think in the preview it runs ok.. but on a real site animates in sections then goes blank then animates the next section.. then freezes etc.. – AGrush Commented Oct 1, 2020 at 18:25
-
A possible solution, because javascript is single-threaded, is to use a service worker to unload the "main thread". To do that, however, you need to do the animation in javascript. Another idea to try out is to use
transform: translate3d
to activate hardware acceleration. – Rickard Elimää Commented Oct 8, 2020 at 8:22
3 Answers
Reset to default 7 +50What a cool little animation. I think it will be annoying in the long run, but still: cool.
What I don't like about this solution is that I had to do a continuous recursive loop, that goes on even if there are no elements to animate. There are probably ways to avoid that, but it would be too plex for just to showcase how to implement requestAnimationFrame
. As you can see, it's not that hard to use requestAnimationFrame. You just call the same method that you do the calculations in.
Apart from adding each element to the body, I also added the element into sparkleArr
with a few more properties through addAnimationProperties
. I finally added a whole new method - moveSparkles
- for calculating movement and removing the elements. calculateInterpolation
calculates the percentage of how long the element should move, based on when the element was created, when it will disappear and the current time.
I think the code is self-explanatory. I had to do a ment on one place, where I don't think it would be possible to explain the code through variables or method names.
let trailArr = [1, .9, .8, .5, .25, .6, .4, .3, .2];
var sparklesArr = [];
function trailAnimation(e, i, maxYTranslation) {
let elem = document.createElement('div');
elem = styleSparkle(elem, e, i);
elem.classList.add("sparkle");
document.body.appendChild(elem);
elem = addAnimationProperties(elem, i, maxYTranslation);
sparklesArr.push(elem);
}
function styleSparkle(elem, e, i) {
let j = (1 - i) * 50;
let size = Math.ceil(Math.random() * 10 * i) + 'px';
elem.style.top = e.pageY - window.scrollY + Math.round(Math.random() * j - j / 2) + 'px';
elem.style.left = e.pageX + Math.round(Math.random() * j - j / 2) + 'px';
elem.style.width = size;
elem.style.height = size;
elem.style.borderRadius = size;
elem.style.background = 'hsla(' +
Math.round(Math.random() * 160) + ', ' +
'60%, ' +
'90%, ' +
i + ')';
return elem;
}
function addAnimationProperties(elem, i, maxYTranslation) {
const ANIMATION_SPEED = 1100;
let lifeExpectancy = Math.round(Math.random() * i * ANIMATION_SPEED);
elem.maxYTranslation = maxYTranslation;
elem.animationSpeed = ANIMATION_SPEED;
elem.created = Date.now();
elem.diesAt = elem.created + lifeExpectancy;
return elem;
}
function moveSparkles() {
let remove = false;
let moveIndex = 0;
let sparkle;
for (let i = 0; i < sparklesArr.length; i++) {
sparkle = sparklesArr[i];
remove = sparkle.diesAt <= Date.now();
if (remove) {
document.body.removeChild(sparkle);
} else {
if (sparkle.maxYTranslation) {
let interpolation = calculateInterpolation(sparkle);
sparkle.style.transform = `translateY(${interpolation}px)`;
}
sparklesArr[moveIndex++] = sparkle; // faster than array.splice()
}
}
sparklesArr.length = moveIndex;
requestAnimationFrame(moveSparkles);
}
function calculateInterpolation(sparkle) {
let currentMillis = Date.now();
let lifeProgress = (currentMillis - sparkle.created) / sparkle.animationSpeed;
let interpolation = sparkle.maxYTranslation * lifeProgress;
return interpolation;
}
window.addEventListener('mousemove', function (e) {
trailArr.forEach((i) => {trailAnimation(e, i)});
let maxYTranslation = '80';
trailArr.forEach((i) => {trailAnimation(e, i, maxYTranslation)});
}, false);
moveSparkles(); // starts the recursive loop
body {
width:100%;
height:100%;
background-color: #000;
}
.sparkle {
position: fixed;
z-index: 6;
opacity: 0.5;
pointer-events: none;
}
<body></body>
I made another version thats quite fun.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
body {
width: 100%;
height: 100%;
background-color: #000;
}
.star-five {
background: transparent;
margin: 50px 0;
position: relative;
display: block;
color: #ffffff;
width: 0px;
height: 0px;
border-right: 100px solid transparent;
border-bottom: 70px solid #ffffff;
border-left: 100px solid transparent;
/* transform: rotate(35deg) scale(0.1) translate(-1450px, -250px); */
}
.star-five:before {
border-bottom: 80px solid #ffffff;
border-left: 30px solid transparent;
border-right: 30px solid transparent;
background-color: transparent;
position: absolute;
height: 0;
width: 0;
top: -45px;
left: -65px;
display: block;
content: '';
transform: rotate(-35deg);
}
.star-five:after {
background-color: transparent;
position: absolute;
display: block;
color: #ffffff;
top: 3px;
left: -105px;
width: 0px;
height: 0px;
border-right: 100px solid transparent;
border-bottom: 70px solid #ffffff;
border-left: 100px solid transparent;
transform: rotate(-70deg);
content: '';
}
</style>
</head>
<body>
<script>
window.addEventListener('mousemove', function (e) {
//trail
[.7, .9, .8, .5, .25, .6, .4, .3, .2].forEach(function (i) {
var j = (1 - i) * 50;
var elem = document.createElement('div');
var size = Math.ceil(Math.random() * 10 * i) + 'px';
//ramdom number between 0 and 1
var precision = 50; // 2 decimals
var randomnum = Math.floor(Math.random() * (10 * precision - 1 * precision) + 1 * precision) / (1*precision);
var rOpacity = randomnum/10;
var rSize = randomnum/120;
elem.style.position = 'fixed';
elem.classList.add('star-five')
elem.style.zIndex = 6;
elem.style.transform = `rotate(35deg) scale(${rSize})`
//elem.style.transform = `rotate(35deg) scale(${rSize}) translate(-1450px, -250px)`
elem.style.top = e.pageY - window.scrollY + Math.round(Math.random() * j - j / 2) - 100 + 'px';
elem.style.left = e.pageX + Math.round(Math.random() * j - j / 2) - 100 + 'px';
//elem.style.width = size;
//console.log(rSize);
elem.style.opacity = rOpacity;
//elem.style.height = size;
// elem.style.background = 'hsla(' +
// Math.round(Math.random() * 160) + ', ' +
// '60%, ' +
// '100%, ' +
// i + ')';
//elem.style.borderRadius = size;
elem.style.pointerEvents = 'none';
document.body.appendChild(elem);
window.setTimeout(function () {
document.body.removeChild(elem);
}, Math.round(Math.random() * i * 1000));
});
////
}, false);
</script>
</body>
</html>
I'm mostly posting this for others to use, where I refactored the code a bit. Nothing has changed, but it's easier to follow. I will follow up with a real answer.
let trailArr = [1, .9, .8, .5, .25, .6, .4, .3, .2];
function trailAnimation(e, i, callbackFn) {
var elem = document.createElement('div');
elem = styleSparkle(elem, e, i);
if (typeof callbackFn == 'function') {
elem = callbackFn(elem);
}
elem.classList.add("sparkle");
document.body.appendChild(elem);
window.setTimeout(function () {
document.body.removeChild(elem);
}, Math.round(Math.random() * i * 1000));
}
function styleSparkle(elem, e, i) {
let j = (1 - i) * 50;
let size = Math.ceil(Math.random() * 10 * i) + 'px';
elem.style.top = e.pageY - window.scrollY + Math.round(Math.random() * j - j / 2) + 'px';
elem.style.left = e.pageX + Math.round(Math.random() * j - j / 2) + 'px';
elem.style.width = size;
elem.style.height = size;
elem.style.borderRadius = size;
elem.style.background = 'hsla(' +
Math.round(Math.random() * 160) + ', ' +
'60%, ' +
'90%, ' +
i + ')';
return elem;
}
window.addEventListener('mousemove', function (e) {
trailArr.forEach((i) => {trailAnimation(e, i)});
trailArr.forEach((i) => {trailAnimation(e, i, (elem) => {
elem.style.animation = "fallingsparkles 1s";
return elem;
})});
}, false);
body {
width:100%;
height:100%;
background-color: #000;
}
.sparkle {
position: fixed;
z-index: 6;
opacity: 0.5;
pointer-events: none;
}
@keyframes fallingsparkles {
from {
transform: translateY(0);
}
to {
transform: translateY(50px);
}
}
<body></body>