We did a cursor trailing effect on our website. Learned how to do holiday effects the right way, experimented with Canvas and Transition, built a NPM library cursor-effect.
Here's our story...
How it started
I saw an ancient HTML spell the other day:
<MARQUEE><BLINK>
How would you suppose this element behave?
</BLINK></MARQUEE>
Surprised by the fact it still works
It reminds me of the old days where cursor trail is a sexy effect. So I went googling but most result are telling you how to set cursor trail on Windows
Luckily there's Cursor Effects (tholman.com) , which seem to be used on StackOverflow before. So we implemented the same effect on our site: Yourator, with some customization tweaks. This post is what we've learned from implementing this effect, and we also published the effect as an npm library:
Cursor Trails: https://github.com/yourator/cursor-trails
Learn from the original cursor-effect library
src: https://github.com/tholman/cursor-effects/blob/master/src/snowflakeCursor.js
We only needed snowflake effect, so this is what we'll be talking about. The main flow of starts from init
, which contains the basic working flow of this effect:
- Initialize environment (canvas) for snowflakes
- Draw emoji character (possibleEmoji) on canvas
-
bindEvents
listens for mouse & touch event -
loop
updates snowflake continuously
bindEvents
listens for mouse & touch event
We're doing the same thing in onMouseMove
and onTouchMove
: call addParticle
upon the event and create a snowflake at where cursor is.
onWindowResize
is responsible for adjusting canvas size.
Why not use CSS width: 100%, height: 100% ?
Since canvas is a canvas with assigned resolution, CSS can only adjust its visual size. If you create a canvas with 100px x 100px and stretch it to 200px x 1000px, then you'll have a 100px x 100px canvas (and pixels in it) which it stretched 2x wide and 10x long. So we need to adjust size of canvas according to window size.
The last line of init
was a loop()
call, to create an infinite loop using requestAnimationFrame. This loop is responsible to update snowflake position and behaviors, it will call update
on each snowflake (particles), or clean up expiring snowflakes. This is the most CPU intensive part.
More detail on particle update: manage its own lifespan, position, rotating and draw snowflake emoji on canvas accordingly
Improvements
The original cursor-effect repo is a effect we need. But to be used on our site we have to add some improvements:
- Use image arrays to render customizable images
- We want to have more control over snowflake's behavior such as: appearing frequency, it's speed and life, etc.
- Touch event on touch device will trigger mousemove and touchstart at the same time, generating two (almost overlapping) snowflakes at once.
- We want to import this library through npm for easier maintainence
What we do
Use image arrays to render customizable images
Change the fillText
with drawImage
, also add calculation for snowflake opacity: globalAlpha
. Since there are several canvas context manipulation, we use save
& restore
to prevent polluting original context.
And since image loading is async, we need loadImage
handles the image url array
With the help of promise all (or Promise.allSettled) , to load image before init()
call
Control snowflake's behavior over initialization options
Main benefit is this boosts prototyping and discussion productivity, you can live-tweak and quickly show the result, or even hand the prototype to stakeholders and let them decide the behaviors.
This part is simple, just don't forget the option defaults
Touch event on touch device will trigger mousemove and touchstart
bindEvent
method listens for mousemove
touchstart
touchmove
, but Touch event on touch device will trigger mousemove and touchstart on user touch, causing excess particle creation, you can see touch event order it on MDN. To prevent this we need to detect if this device is touch device or not
NPM-ify for easier maintainence
Uses ESM and published on NPM, use when in need.
npm install cursor-trails
Something learned about Canvas and Transition
Which we adjust cursor-effect to deal with image loading, FPS drops significantly, thought it was because we throw too many image creating at a short time. Even rewrite one version using CSS Transition, just to find out it was because mass creating of SVG element consumes a lot of CPU.
Canvas is very effective with drawing bitmap image in a fixed space. While CSS Transition suits animating DOM elements on the page. So creating lots of image element on canvas is more smooth then creating and transform it.
I must highlight chrome's devtool's "rendering" tab (edge has it also), especially two checked in this image
it shows fps and paint areas, as gif below
Other considerations
The effect is sexy (in a retro 90s way), but we shouldn't forgot this is not the main purpose of users on our site (they are here for job searching and career development). So after some discussion we decided to let this sexy feature reside only in the main search section at home page. It's spacey, it is what users first sees, it wont interfere with other thing users want to do. Hope this gets some balance with Christmas ambient and user's opration.
We we're planning to use prefers-reduced-motion
to deal with low-end devices, but due to time limitation, that will be put on the roadmap.
This library now only have snow falling effect, hope we can have more strategies on particle behavior. Maybe even customizable strategy, ex: fixed, floating, fadeout of cursor trailing effects.
That's about it.
🎄❄️🧑🎄 Merry Christmas 🎄❄️🧑🎄
Here's our repo (again): https://github.com/yourator/cursor-trails
References
- stackoverflow's april fools day effect
- Mouse Trail - Noah Yamamoto (archive.org)
- The technology of nostalgia (humphd.org)
- When to Use SVG vs. When to Use Canvas - CSS-Tricks
Top comments (0)