npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@tsogtoodev/particle-glock

v0.2.1

Published

Turn any SVG into an interactive particle mark for Vue 3 — assembles from a scattered cloud, repels from the cursor, fires comet bullets on click, morphs between shapes, and renders HTTP status codes for error pages.

Readme

@tsogtoodev/particle-glock

An interactive particle-mark for any SVG logo, for Vue 3.

Every opaque pixel of your SVG becomes a particle. The mark flies in from a scattered cloud, repels away from the cursor, fires comet "bullets" on click (with an impact shockwave), auto-shoots on an idle timer, and can morph between two shapes. No WebGL, no dependencies beyond Vue.

Particles render on a full-viewport, click-through canvas appended to <body> — never inside your layout — so no ancestor's overflow, size, or box can ever crop the effect. The mark is drawn at the host element's on-screen position and follows it as the page scrolls or resizes.

Install

npm i @tsogtoodev/particle-glock

Usage

<script setup lang="ts">
import { ParticleLogo } from '@tsogtoodev/particle-glock';
import logo from './logo.svg?raw'; // or a URL string, or inline markup
</script>

<template>
  <ParticleLogo :svg="logo" :width="37" :height="29" color="#1a191d" idle />
</template>

svg accepts inline SVG markup, a data: URI, or a URL to an .svg file (fetched at runtime). With Vite, import logo from './logo.svg?raw' gives you the markup string directly.

Register globally (optional)

import { createApp } from 'vue';
import ParticleLogoPlugin from '@tsogtoodev/particle-glock';

createApp(App).use(ParticleLogoPlugin); // <ParticleLogo> available everywhere

Props

| Prop | Type | Default | Description | | ---------------------- | ------------------------------------------- | -------------- | ----------- | | svg (required) | string | — | Inline markup, data URI, or URL to the mark. | | altSvg | string | — | A second shape to morph into (contain-fit, centered). | | width / height | number | SVG intrinsic | Display size in CSS px. | | color | string (any CSS color) | inherited color | Ink color of the particles. | | padding | number | 60 | Minimum cursor reach (px) beyond the mark. The hover zone is always at least as large as the repulsion field, so raising this only widens it further. Particles are never clipped. | | hover | boolean | auto | Repel particles from the cursor. Off by default on touch devices. | | shootOnClick | boolean | true | Fire a bullet at the click point. | | idle | boolean \| { minMs?, maxMs? } | false | Auto-shoot on a random interval (default 12–28s). | | autostart | boolean | true | Assemble on mount. | | params | Partial<AssemblyParams> | — | Engine tuning (see below). | | respectReducedMotion | boolean | true | Render a static SVG when the user prefers reduced motion. | | maxDpr | number | 2 | Clamp device pixel ratio. | | quality | 'auto' \| 'high' \| 'low' | 'auto' | Performance profile — see Performance. | | zIndex | number | 2147483000 | z-index of the click-through overlay canvas. | | sound | boolean \| { volume?: number } | false | Play synthesized UI sounds on interaction (see below). | | as | string | 'span' | Wrapper tag (e.g. 'a', 'button'). |

Events

@assembled · @impact · @ready

Exposed methods (via template ref)

<ParticleLogo ref="logo" :svg="mark" :alt-svg="arrow" />
logo.value.shoot(clientX?, clientY?); // fire a bullet (defaults to mark center)
logo.value.morphTo(1);                // 0 = mark, 1 = alt shape
logo.value.toggleMorph();
logo.value.setColor('#e5484d');

Engine tuning (params)

| Key | Default | Meaning | | -------------- | ------- | ------- | | assembleMs | 1100 | Fly-in duration per particle. | | staggerMs | 700 | Max random start delay (the staggered gather). | | gatherSpread | 2.6 | Scatter radius, as a multiple of mark size. Affects the fly-in only. | | followLag | 10 | How snappily particles chase a moved origin. | | jitter | 0.65 | Sub-pixel randomisation of resting positions (device px). See below. | | maxParticles | 40000 | Hard cap on particle count. See Performance. | | streakDetail | 3 | Nested layers per motion streak (3 = comet tail, 1 = single line). |

About jitter — particles are sampled one per source pixel, so they rest on a perfectly regular lattice. Warping that lattice (cursor repulsion, blasts) makes neighbours fall in and out of phase, which shows up as concentric moiré rings. Jittering each resting position by roughly the lattice spacing breaks the regularity and roughly halves the ringing; past ~0.65 there's no further gain, only a softer shape. Set 0 for a mathematically exact lattice. The value scales automatically with the sampling stride, so it always stays proportional to the actual particle spacing.

Performance

Particles are sampled one per source pixel, so a full-page number can reach 70k+ particles — far more than a phone can integrate at 60fps. Two things keep that in check:

A particle budget. When a mark exceeds params.maxParticles, the sampler takes every Nth pixel on both axes and draws each dot N times larger — coverage looks the same, the work drops by N². Budgets are set so N stays at 1–2 for typical marks; past that the dots get big enough to read as blocks. Combined with grouping particles by index instead of re-sweeping the array once per alpha bucket, per-frame work falls roughly 7x on desktop and 12x on mobile.

Resolution is deliberately not part of the trade. A settled mark is a blit of the source raster, so lowering the device pixel ratio would make the resting state — the thing people look at most — visibly soft. Particle count is the cheap lever; sharpness is not.

Cheaper motion streaks. During a blast every particle is moving fast, so every particle takes the streak path — at the default 3 nested layers that is 6 Path2D ops per particle per frame, which profiling showed to be the single biggest cost in the heavy case. params.streakDetail: 1 draws one line instead.

A device profile. quality decides the budget, the pixel-ratio cap, streak detail, and whether the cursor field runs at all:

| quality | Budget | maxDpr | Cursor field | Streak layers | | --------- | ------- | -------- | ------------ | ------------- | | 'high' | 40 000 | 2 | on | 3 | | 'low' | 8 000 | 2 | off | 1 | | 'auto' | picks 'low' on a coarse pointer, or 4 or fewer CPU cores | | | |

Measured on an iPhone 16e simulator, error page (code + headline), firing continuously so particles never settle:

| | before | after | | --- | --- | --- | | sustained | 12 fps | 56 fps | | worst 1s average | 11 fps | 37 fps | | peak frame time | 102 ms | 31 ms |

At rest it is 60 fps in both cases — a settled mark is a single cached-raster blit and the animation loop stops entirely.

'auto' is the default. A touch device has no hovering cursor, so the repulsion field there costs per-particle maths every frame and can never be seen — hence off. Any explicit hover, maxDpr or params.maxParticles always wins over the profile:

<!-- force the full-fat version everywhere -->
<ParticleLogo :svg="mark" quality="high" />

<!-- or tune the budget yourself -->
<ParticleLogo :svg="mark" :params="{ maxParticles: 8000 }" />

Other costs worth knowing: the animation loop stops entirely once everything settles (a settled mark is a single cached-raster blit, not a particle sweep), and it stops when the cursor leaves the field's reach rather than spinning.

Composable

For full control, skip the component:

import { ref } from 'vue';
import { useParticleLogo } from '@tsogtoodev/particle-glock';

const host = ref<HTMLElement | null>(null);
const { shoot, morphTo, setColor, ready } = useParticleLogo(host, () => ({
  svg: mark,
  altSvg: arrow,
  width: 37,
  height: 29,
  idle: { minMs: 8000, maxMs: 20000 },
}));
<div ref="host" />

Framework-agnostic core

The engine has no Vue dependency. Use it in vanilla JS or other frameworks:

import { mountParticleLogo } from '@tsogtoodev/particle-glock';

const instance = await mountParticleLogo(document.getElementById('logo')!, {
  svg: markString,
  width: 37,
  height: 29,
  color: '#1a191d',
});
// instance.shoot() / .morphTo(1) / .setColor() / .dispose()

Lower-level still: createAssembly(displayCanvas, sourceCanvas, opts) returns the raw controller (start / setOrigin / setPointer / shoot / setAltShape / morphTo / setInk / dispose). See the exported types.

Error pages (status codes)

<ParticleStatus> renders an HTTP status code as particles — built for 404 / 500 pages. Digit glyphs ship with the package, so no asset wiring is needed:

<script setup>
import { ParticleStatus } from '@tsogtoodev/particle-glock';
</script>

<template>
  <ParticleStatus :code="404" text="Page not found" />
  <ParticleStatus :code="503" text="Серверийн алдаа" :text-size="38" sound />
</template>

Props

| Prop | Type | Default | Description | | -------------- | ----------------------------------- | ---------------- | ----------- | | code (req) | string \| number | — | e.g. 404, "500". | | text | string | — | Headline below the code, in the same glyphs. | | textSize | number | 34 | Cap height of the headline in CSS px. | | gap | number | 26 | Space between code and headline in CSS px. | | preset | StatusPreset | picked from code | See below. | | height | number | 160 | Cap height of the code in CSS px; width follows digit count. | | color | string | inherited | Ink color. | | hover · shootOnClick · idle · sound · respectReducedMotion · zIndex | | | Same as <ParticleLogo>. |

height and textSize are both cap heights — the box grows to fit descenders without changing the perceived size. The headline is drawn from the same glyph outlines as the code, so the two read as one typeface at two sizes, with the same particle effects and preset.

Exposes shoot() and setColor(); emits @assembled / @impact.

Presets

| Preset | Feel | Auto-applied to | | ---------- | ----------------------------------------------- | --------------- | | drift | Slow, wide, weightless gather — "lost in space" | 4xx (e.g. 404) | | snap | Fast and tight — "denied" | 401, 403, 429 | | shatter | Assembles, then a comet blows it apart | 5xx | | assemble | The standard logo fly-in | — |

Omit preset and one is chosen from the code; pass it explicitly to override.

Glyph coverage

Glyph outlines are extracted from a real typeface — Geologica (SIL Open Font License, see THIRD-PARTY-NOTICES.md) — so proportions, side bearings and advance widths are the font's own rather than approximations.

The built-in set covers 0–9, A–Z, all 35 Mongolian Cyrillic letters (А–Я, including Ө and Ү) and basic punctuation (. , ! ? - : ' / and space). It is caps-only — input is upper-cased for you, so "Page not found" and "Серверийн алдаа" both work.

Characters outside the set are dropped rather than thrown on the text prop, so an unexpected em dash or emoji can't break an error page. Check coverage up front if you'd rather handle it yourself:

import { canRenderText, missingGlyphs, sanitizeGlyphText } from '@tsogtoodev/particle-glock';

canRenderText('Хуудас олдсонгүй'); // true
missingGlyphs('Oops — 404');       // ['—']
sanitizeGlyphText('Oops — 404');   // 'OOPS 404'

Getting the markup yourself

Glyphs are built in, so nothing needs downloading. To drive <ParticleLogo> directly (or to render a code or headline anywhere else):

import { statusCodeSvg, glyphTextSvg } from '@tsogtoodev/particle-glock';

const code = statusCodeSvg(404);
const line = glyphTextSvg('Хуудас олдсонгүй');

If you want the codes as standalone .svg files (for design tools or non-JS use), generate them into out/status/:

npm run gen:status -- 404 500 418

Using a different typeface

The glyph data in src/core/glyph-data.ts is generated. Point the script at any font you have the rights to use and it re-derives the whole set:

npm run gen:glyphs -- path/to/YourFont.ttf

It normalises to cap height, keeps the font's own advance widths, and fails loudly if a required character is missing. Update THIRD-PARTY-NOTICES.md to match the new font's license.

Sound

Set sound to add audio feedback. Every sound is synthesized live with the Web Audio API — oscillators, a filtered noise burst, and gain envelopes — so there are no audio files to ship or load.

<ParticleLogo :svg="mark" sound />
<ParticleLogo :svg="mark" :sound="{ volume: 0.5 }" />

Cues are wired automatically:

| Interaction | Sound | | ------------------ | -------- | | Cursor enters | hover | | Click / idle shoot | shoot | | Bullet impact | hit | | Morph → alt shape | sort | | Morph → mark | close |

Browsers only allow audio after a user gesture, so the first click unlocks it (hover sounds before any interaction are silently skipped). All logos share one AudioContext.

Drive it directly if you like:

import { getSoundEngine } from '@tsogtoodev/particle-glock';

const audio = getSoundEngine();
audio.play('shoot');        // 'hover' | 'tap' | 'open' | 'close' | 'sort' |
                            // 'grab' | 'drop' | 'confirm' | 'copy' | 'error' |
                            // 'shoot' | 'hit' | ...
audio.setVolume(0.4);
audio.setEnabled(false);

Recipe: back-to-top on scroll

Combine morphTo with an alt "arrow" SVG and drive it from scroll position:

const scrolled = () => window.scrollY > 24;
window.addEventListener('scroll', () => logo.value.morphTo(scrolled() ? 1 : 0));

Notes

  • Particles draw on a position: absolute canvas that overhangs the mark by padding px; the wrapper reserves exactly width × height for layout.
  • requestAnimationFrame pauses while the tab is hidden (browser default), so the animation resumes when the tab is focused.
  • License: MIT.