I made this cursor animation recently, and people seem to like it :)
It is a nice-looking piece, but it's also quite simple and takes only 2KB of JS. Plus, the approach is quite universal and it can be used as a template for other beauties.
So it deserves a step-by-step guide!
Let's go
Step #1: Setup
We're drawing on the <canvas>
element and we need the <canvas>
to take the a full screen.
canvas {
position: fixed;
top: 0;
left: 0;
}
<canvas></canvas>
setupCanvas();
window.addEventListener("resize", setupCanvas);
function setupCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
And, for sure, we need to track the cursor position.
const pointer = {
x: .5 * window.innerWidth,
y: .5 * window.innerHeight,
}
window.addEventListener("click", e => {
updateMousePosition(e.pageX, e.pageY);
});
window.addEventListener("mousemove", e => {
updateMousePosition(e.pageX, e.pageY);
});
window.addEventListener("touchmove", e => {
updateMousePosition(e.targetTouches[0].pageX, e.targetTouches[0].pageY);
});
function updateMousePosition(eX, eY) {
pointer.x = eX;
pointer.y = eY;
}
Step #2: Animation loop
To see the simplest mouse-following animation, we only need to redraw canvas in a loop using the window.requestAnimationFrame()
method, and draw the circle centered as pointer coordinates on each step.
const p = {x: 0, y: 0}; // coordinate to draw
update(0);
function update(t) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// copy cursor position
p.x = poiner.x;
p.y = poiner.y;
// draw a dot
ctx.beginPath();
ctx.arc(p.x, p.y, 5, 0, 2 * Math.PI);
ctx.fill();
window.requestAnimationFrame(update);
}
With the code above, we have a black circle following the mouse.
Step #3: Adding the delay
Now, the circle is following the cursor as fast as it can. Let's add a delay so the dot catches up with the target position in a somewhat elastic way.
const params = {
// ...
spring: .4
};
// p.x = poiner.x;
// p.y = poiner.y;
p.x += (pointer.x - p.x) * params.spring;
p.y += (pointer.y - p.y) * params.spring;
ctx.beginPath();
ctx.arc(p.x, p.y, 5, 0, 2 * Math.PI);
ctx.fill();
The spring
parameter is used to determine how fast the dot will catch up with cursor position. A small value like .1
will make it follow very slowly, while spring = 1
means no delay.
Step #3: Creating mouse trail
Let's create a trail - array of points' data, with each point holding the x
/y
coordinates and dx
/dy
deltas which we use to calculate delay.
const params = {
// ...
pointsNumber: 30
};
// const p = {x: 0, y: 0};
const trail = new Array(params.pointsNumber);
for (let i = 0; i < params.pointsNumber; i++) {
trail[i] = {
x: poiner.x,
y: poiner.y,
dx: 0,
dy: 0,
}
}
Instead of a single dot, we draw now the whole trail where each dot is trying to catch up with the previous one. The first dot catches up with cursor coordinate (pointer
) and delay of this first point is longer - simply because it looks better for me :)
function update(t) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
trail.forEach((p, pIdx) => {
const prev = pIdx === 0 ? pointer : trail[pIdx - 1];
const spring = pIdx === 0 ? .4 * params.spring : params.spring;
p.dx = (prev.x - p.x) * spring;
p.dy = (prev.y - p.y) * spring;
p.x += p.dx;
p.y += p.dy;
ctx.beginPath();
ctx.arc(p.x, p.y, 5, 0, 2 * Math.PI);
ctx.fill();
});
window.requestAnimationFrame(update);
}
Step #4: Turning dots to the line
It's easy to draw a polyline instead of dots.
trail.forEach((p, pIdx) => {
const prev = pIdx === 0 ? pointer : trail[pIdx - 1];
p.dx = (prev.x - p.x) * params.spring;
p.dy = (prev.y - p.y) * params.spring;
p.x += p.dx;
p.y += p.dy;
// ctx.beginPath();
// ctx.arc(p.x, p.y, 5, 0, 2 * Math.PI);
// ctx.fill();
if (pIdx === 0) {
// start the line on the first point
ctx.beginPath();
ctx.moveTo(p.x, p.y);
} else {
// continue with new line segment to the following one
ctx.lineTo(p.x, p.y);
}
});
// draw the thing
ctx.stroke();
Step #5: Accumulating the speed
What makes the cursor animation really nice looking is accumulating the deltas. Let's use dx
/dy
not only for the distance to the neighbour position but accumulate this distance.
To prevent the delta values getting super big super fast, we're also multiplying dx
/dy
with new friction
parameter on each step.
const params = {
// ...
friction: .5
};
...
// ...
// p.dx = (prev.x - p.x) * spring;
// p.dy = (prev.y - p.y) * spring;
p.dx += (prev.x - p.x) * spring;
p.dy += (prev.y - p.y) * spring;
p.dx *= params.friction;
p.dy *= params.friction;
// as before
p.x += p.dx;
p.y += p.dy;
// ...
Step #6: Smooth line
The motion is done! Let's make the stroke look better and replace each line segment with Bézier curve.
trail.forEach((p, pIdx) => {
// calc p.x and p.y
if (pIdx === 0) {
ctx.beginPath();
ctx.moveTo(p.x, p.y);
// } else {
// ctx.lineTo(p.x, p.y);
}
});
for (let i = 1; i < trail.length - 1; i++) {
const xc = .5 * (trail[i].x + trail[i + 1].x);
const yc = .5 * (trail[i].y + trail[i + 1].y);
ctx.quadraticCurveTo(trail[i].x, trail[i].y, xc, yc);
}
ctx.stroke();
Smooth!
Step #7: Play with line width
For this demo, the last step is replacing the default lineWidth
which is 1px to the dynamic value that gets smaller for the each segment.
const params = {
baseWidth: .9,
};
...
for (let i = 1; i < trail.length - 1; i++) {
// ...
ctx.quadraticCurveTo(trail[i].x, trail[i].y, xc, yc);
ctx.lineWidth = params.baseWidth * (params.pointsNumber - i);
}
See the source code on codepen.
Top comments (24)
Nicely done. Especially with the smooth quadratic curve.
You could consider moving a lot of this into a class in order to keep it out of global scope. Also, though it works, I think it's a little awkward to call functions before they are defined. I like to place my event listeners all together at the end of a code block.
Hey Rich! It all makes perfect sense, thank you for the code review! I do have a tendency to concentrate on the visual aspect so it's always nice to have someone to take a look at my JS
Great Project 👍
Thanks man
Nice project
amazing
amazing job
Soooooo satisfying! I can spend hours playing around with this!
@uuuuuulala, so have very tasteful people in your life :)
I discovered your "Damn Satisfying" (and it really is) Curly Cursor creation on CodePen, then had to hunt down my DEV login info, just so I could comment on what beautiful art/code you've made. AND- thank you for also taking the time to write up the tutorial!
Omg @lorenmcclaflin thank you so much!
It took me a bit of time to notice your comment but I'm soooo happy to see it now :) Really made my day, thanks man
Gyansetu's Programming courses are a fantastic way to kickstart your coding journey. With experienced instructors and a variety of programming languages to choose from, it's a great platform for honing your skills and embracing the world of coding. Highly recommended!
For more info:- gyansetu.in/blogs/70-power-bi-mcq-...
My first spam comment here on dev.to! Taste of success 😄
It's so icky when ppl promote themselves using 3rd-person voice.
Very nice, thank you
Look at processing.js...
I don't think processing.js has been actively maintained for quite some time. That was replaced by p5.js but TBH I think they tried too hard to match processing's Java based syntax in js; which I found rather limiting.
For anything canvas based my preference these days is pixiJS :)
PixiJS is indeed a great library
May I also suggest PaperJS
this is the uprgarted version of that code in only html
<!DOCTYPE html>
Interactive Cursor Tutorial
<br>
/* Resetting default styles and adding custom styles <em>/<br>
body,<br>
html {<br>
padding: 0;<br>
margin: 0;<br>
overscroll-behavior: none;<br>
background-color: #111; /</em> Dark background color <em>/<br>
color: #fff; /</em> Text color /<br>
}</p>
<div class="highlight"><pre class="highlight plaintext"><code> / Styling for the tutorial link */
.links {
position: fixed;
bottom: 10px;
right: 10px;
font-size: 18px;
font-family: sans-serif;
background-color: white;
padding: 10px;
}
</style>
</code></pre></div>
<p></head></p>
<p><body><br>
<!-- Canvas element for the interactive cursor animation --><br>
<canvas></canvas></p>
<div class="highlight"><pre class="highlight plaintext"><code><!-- Tutorial link with icon -->
<div class="links">
<a href="dev.to/uuuuuulala/coding-an-intera..." target="_blank">tutorial<img class="icon" src="https://ksenia-k.com/img/icons/link.svg"&gt;&lt;/a&gt;
</div>
<!-- JavaScript for interactive cursor animation -->
<script>
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext('2d');
</script>
</code></pre></div>
<p></body></p>
<p></html></p>
<p>this is only html but fully working try thanks me latter</p>
How would you format this so it is ready to use? I am completely new to code and don't even know where to start!