Sparkler Light Painting
Canvas buffer faded each frame → persistent long-exposure trail. Your cursor becomes a sparkler: incandescent halo, jetting sparks, and an automatic parametric heart loop.
You'll get access to the interactive demo with a free account.
This effect is part of Effect.Labs — 811 vanilla effects, some free, some premium. Atmosphere has 57 effects, including 10 free. Explore the category →
3 usage examples



How it works
The effect uses two stacked canvases: an off-screen accumulation buffer (this.acc) that receives trail strokes, and the display canvas (this.cv). On every frame the buffer is faded by a fillRect in destination-out mode with globalAlpha = 0.012 — this rate yields roughly 3 seconds of persistence, equivalent to a long-exposure photograph.
Each trail segment is painted to the buffer in three passes: a wide orange glow (lw=14, α=0.10), a mid amber stroke (lw=5, α=0.35), and a near-opaque white core thread (lw=1.5, α=0.90). globalCompositeOperation = 'lighter' causes overlapping zones to bloom toward white.
The live head (contact point) is a pre-computed off-screen sprite: a radial gradient from white through amber to transparent, drawn at 20 px, never written to the buffer. It is composited onto cv each frame with a sinusoidal pulse and three flickering rays at rotating angles. Sparks form a pool of up to 100 particles: random radial velocity, gravity +0.055/frame, colour (yellow→orange) indexed to remaining life — also on cv only.
When idle, an automatic mode traverses two shapes in turn (parametric heart, then 5-point star) via arc-length tables at N_SAMPLES = 300 — guaranteeing constant traversal speed regardless of local curvature. If the pointer stays still for more than 2.5 s (RESUME_MS), auto mode resumes. An IntersectionObserver cuts the rAF when the scene scrolls off-screen: zero wasted CPU.
Accessibility
- prefers-reduced-motion: checked once at init via
matchMedia. If active,drawStatic()draws the heart in a single pass (same 3 stroke layers, no rAF). No animation runs. Caveat: the check is not a live listener — an OS change mid-session takes effect only on page reload. - The
<canvas>carriesaria-hidden="true"— canvas content is not exposed to screen readers. - The
.spk-scenecontainer carriesrole="img"and anaria-labeldescribing the effect in plain language. - The '✕ Clear' button has an explicit
aria-labeland an orange (#fb923c)focus-visiblering meeting WCAG AA. It is hidden in CSS whenprefers-reduced-motion: reduceis active. - The light trail is driven by mouse or touch only — no keyboard path triggers it. Overlay text (headings, CTAs) remains in the normal tab order.
Browser compatibility
Requires Canvas 2D Context, requestAnimationFrame, and 'lighter' / 'destination-out' composite modes — supported across all modern browsers since 2016.
If <code>canvas</code> is unsupported (IE 8−), the <code>.spk-scene</code> container renders with a dark background (#050510) — no JS errors, the page stays functional. On browsers without <code>IntersectionObserver</code> (Safari pre-12.1), the rAF runs continuously.
The code
HTML structure to paste into your page (CSS + JS available with a premium account):
<div class="spk-scene" role="img" aria-label="Sparkler drawing a persistent light trail — automatic parametric heart trace, interactive with cursor">
<canvas class="spk-canvas" aria-hidden="true"></canvas>
<!-- Optional: clear button -->
<div class="spk-ui">
<button class="spk-clear-btn" type="button" aria-label="Clear the light trail">✕ Clear</button>
</div>
<!-- Optional: scrim + overlay content -->
<div class="spk-scrim" aria-hidden="true"></div>
<div class="spk-hero-content">
<p class="spk-hero-eyebrow">Eyebrow</p>
<h1 class="spk-hero-title">Title</h1>
<p class="spk-hero-sub">Subtitle</p>
<button class="spk-btn">CTA</button>
</div>
</div>
Full HTML + CSS + JS, copy-paste ready — with hundreds of premium effects.
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
| SparklerPainting.setFade(v) | 0.012 | Buffer fade rate per frame. 0.006 = ~6 s trail (long exposure), 0.025 = ~1 s trail (quick flash). Hot-swappable without reload. |
| SparklerPainting.setSparks(v) | 5 | Sparks emitted per frame during pointer or auto-trace movement. 1 = subtle, 12 = dense shower. Hot-swappable. |
| SparklerPainting.clear() | — | Clears the accumulation buffer and resets sparks and auto-trace position — use it to start a fresh sequence on a business event. |
| GLOW_R (JS const, line 6) | 10 | Live-head sprite radius in canvas pixels (displayed at ~20 px). Raise to 18–22 for a larger, more imposing halo. |
| SPARKS_IDLE (JS const, line 11) | 1 | Sparks emitted per frame when the pointer is still. 0 = sparks only while moving; 3 = permanent ember glow. |
| RESUME_MS (JS const, line 10) | 2500 | Delay (ms) before auto-trace resumes after pointer stillness. 0 = instant resume; 5000 = leave full control to the user. |
| .spk-scene { background } | #050510 | Scene background colour, repainted by ctx.fillStyle before each composite. A deep blue (#030820) shifts the orange trail toward red. |
FAQ
STILL_MS = 80 sets the threshold: if the last mousemove is older than 80 ms, the pointer is considered still. The code keeps rendering the live head and emits one spark per frame (SPARKS_IDLE = 1), but adds no new segment to the buffer. After RESUME_MS = 2500 ms of stillness, automatic mode resumes.Scene instance has an autoIdx property (0 = heart, 1 = star). In the source, at the line this.autoIdx = 1-this.autoIdx inside the 'pausing' block, replace it with this.autoIdx = 0 to loop the heart indefinitely. The public SparklerPainting API does not expose this — it requires editing the JS source.touchmove events are listened with { passive: false } to allow preventDefault() and suppress scroll interference. On iOS, add touch-action: none to .spk-scene in CSS if a long press triggers the native context menu.