◉ Pulse
dannystuart avatar
#12 today

dannystuart/particle-hero

TypeScript Library

Turn a photo or short clip into an interactive field of light-specks, and export it as an embeddable hero. Framework-free canvas engine, no 3D or particle library.

Created Jul 23, 2026
Stars
15
Today
+15
Forks
3
Quality
90/100
Momentum
20

README

Fetched from GitHub on Oct 30, 2026, 11:03 PM UTC · README.md

View original on GitHub

Third-party content — Open Source Pulse is not the owner of this repository.

This README belongs to dannystuart/particle-hero and its contributors. We fetch it directly from the project’s public GitHub repository and display it for reference only. All text, code, images and trademarks remain the property of their respective authors. See the repository for its license terms. Copyright / takedown policy.

Particle Hero

Turn a photo — or a short video clip — into a field of drifting light-specks that part around your cursor. Tune it live, then export it as a self-contained script you can drop into any website.

No 3D library, no particle library, no WebGL. It's hand-written canvas code in one file, and the whole thing installs in about ten seconds.

Move your cursor across the portrait, then open the panel and start pulling sliders. Best on a desktop, but it works on a phone too.


Run it locally

pnpm install && pnpm dev

Then open http://localhost:3000.

Everything is in the panel on the right — on a phone it's a sheet at the bottom.


Use your own image

This is the fun part, and it's a two-step change.

1. Drop your file into public/.

2. Add one entry to PORTRAITS in app/portraits.ts:

{
  id: "my-portrait",
  name: "My Portrait",
  src: "/my-portrait.png",
  bg: { mode: "alpha" },
}

Save. It appears in the switcher at the bottom of the screen automatically.

Which bg mode do I need?

The engine has to know which pixels are subject and which are background, so it only spends particles on the thing you care about.

Your image Use Why
A cutout PNG (transparent background) { mode: "alpha" } Easiest and best. Any background remover gives you this.
Subject on a flat colour { mode: "chroma", color: [255, 255, 255], tolerance: 0.12 } Flood-fills that colour inward from the edges. color is RGB 0–255. Raise tolerance if bits of background survive; lower it if parts of your subject vanish.
A video clip omit bg, set kind: "video" Video works out its own subject from the light in each frame.

Cutout PNGs give by far the best results. If a photo looks muddy, that's usually the reason — the engine is spending particles on the background.

Video clips

{
  id: "my-clip",
  name: "My Clip",
  src: "/my-clip.mp4",
  kind: "video",
  defaults: { count: 50000, contrast: 0.45 },
}

Keep clips short (2–5s), looping, and ideally a bright subject on a dark background — that's what reads best as light.

Per-portrait defaults

defaults overrides any control for that portrait only:

defaults: { count: 50000, contrast: 0.7, size: 1.5 }

Handy when one image needs more contrast than the rest.


What the controls do

Style — the switch at the top of the panel. Dots is the classic dense grain; Constellation rebuilds the portrait from a few thousand larger stars joined by thin lines that trace the subject — a star-map look that uses the same cursor interaction and ships through the export just like dots. Each style remembers its own settings.

Source — flip between the particles and the untouched original, to compare.

Frame — the preview box size (16:9, 21:9, square…). Fit and Scale decide how the subject sits in the frame, and unlike the preview size, those two travel with your export.

Background — Mode switches between light specks on a dark backdrop and dark "ink" specks on a light one. Looks are one-click pairings of backdrop and speck colour. Backdrop is the manual version.

Particles

  • Count — how many specks. Exactly the number you set, no hidden scaling.
  • Speck size — how big each one is. Small changes here matter a lot.
  • Contrast — how hard the light falls off. 0 is flat; higher makes highlights blaze and shadows recede.
  • Colour / Fill — one flat colour, or a gradient revealed through the specks.
  • Cursor size / style — how far your cursor pushes the field, and what the pointer looks like.

Mockup — drops a fake hero layout over the top (nav, headline, sub, button) so you can see it working as a real page. Preview only; it doesn't ship with the export.


Put it on your own site

Hit Export. You get:

  • a config block matching exactly what's on screen
  • a <script> snippet to paste
  • a download for embed.js (~18KB, no dependencies)
<div id="hero" style="width:100%;height:600px"></div>
<script src="/embed.js"></script>
<script>
  window.ParticlePortrait.mount("#hero", {
    src: "/your-image.png",
    bg: { mode: "alpha" },
    settings: {
      /* … whatever you tuned … */
    },
  });
</script>

It fills whatever box you give it. Host embed.js and your image on your own site.

If you change anything in app/engine/, rebuild the bundle:

pnpm build:embed

Performance notes

The cost per frame is roughly particle count × speck size × screen pixels. If it feels sluggish:

  1. Drop Count first — it's the biggest lever.
  2. Then Speck size — going from 1.25 to 3 is nearly 6× the pixels to paint.
  3. Very large, very dense displays are the hardest case.

Phones automatically start at a lower count (the panel shows the real number — there's no hidden multiplier anywhere).


How it works, briefly

  1. The source is analysed once into a weighted probability map — brighter areas and edges get more weight, so particles land where the detail is.
  2. Particles are drawn from that map and given a home position.
  3. Every frame they spring back toward home, drift gently, and are pushed away by the cursor.
  4. For video, step 1 runs across the whole clip so particles cover everywhere the subject ever goes, then each frame's brightness decides which ones light up.

The engine (app/engine/core.ts) is plain canvas and DOM with no framework imports — which is why the same code runs both this playground and the exported embed. No drift between what you tune and what you ship.


Licence

Code is MIT — see LICENSE.

The sample media (statue, flower, parrot, glass) is included so the project runs out of the box. Please swap in your own for anything you publish.

Rendered by GitHub from the repository’s README. Scroll to read the full file.
First trending

Jul 24, 2026

first spotted on the list

Best rank

#12

highest position reached

Appearances

1

times on the trending list

Quality score

90

out of 100

Momentum

20

trend strength indicator

Star velocity

0.15

stars per day since creation

Activity — last 60 days

daily counts, collected every 6 hours
  • Stars
  • Forks
  • Merged PRs
  • Issues
  • Closed issues

Collecting activity data since launch — check back in a few days.

Star history

total stars per snapshot

Not enough snapshots yet — check back soon.

Rank history

position on trending · lower is better

Not enough snapshots yet — check back soon.

3D card for your website

Live HTML card with a glass surface and pointer tilt. Paste the iframe on any website.

<iframe src="https://www.notionpulse.cc/embed/dannystuart/particle-hero" width="420" height="240" style="border:0;max-width:100%;background:transparent" loading="lazy" title="dannystuart/particle-hero — trending on Open Source Pulse"></iframe>

Stars badge

Show current stars in your README or profile. Auto-updates every 6 hours.

dannystuart/particle-hero stars badge
Markdown
[![dannystuart/particle-hero](https://www.notionpulse.cc/api/badge/dannystuart/particle-hero.svg)](https://www.notionpulse.cc/repo/dannystuart/particle-hero)
HTML
<a href="https://www.notionpulse.cc/repo/dannystuart/particle-hero"><img src="https://www.notionpulse.cc/api/badge/dannystuart/particle-hero.svg" alt="dannystuart/particle-hero stars badge" /></a>

Repository of the Day

#12 Repository of the Day

Repository of the Week

#12 Repository of the Week

Repository of the Month

#12 Repository of the Month