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

@bunizao/decode-text

v0.1.0

Published

Dependency-free scramble/decode text reveal. Soulwire-style condensing lines or classic pop-in-place, frame-rate independent, layout-shift free.

Downloads

106

Readme

@bunizao/decode-text

Dependency-free scramble/decode text reveal. Zero runtime deps, ~2 KB min+gz.

Two looks:

  • grow — Soulwire-style: the line condenses in from the left while glyphs boil, then settle. Wants a monospace font (scramble and real glyph must share a width).
  • static — classic decrypt: every character slot is locked to its final width up front and glyphs pop in place. Works in any font.

Scheduling keeps Soulwire's fronts but separates the noisy ones from the resolve. A show front (p^0.5) floods cursors in early and a mash front (p^2) graduates them to boiling scramble — both shuffled, both finished before settleStart — and only then does the resolve front sweep left to right at constant speed, one glyph at a time, under easeInOutSine. The scramble pool also absorbs the text's own ASCII glyphs (scrambleFromText), so the mash reads like the sentence shuffling itself.

Why it feels right:

  • Frame-rate independent. Scramble mutation is scheduled in wall time, not per frame — a 120 Hz display boils at the same speed as a 60 Hz one.
  • No layout shift. The host's height is locked, per-frame churn is isolated with contain: layout paint, and visual lines are measured and re-homed into nowrap blocks so a growing line never re-wraps the paragraph.
  • Cheap frames. Settled cells accumulate behind a per-line pointer and are never revisited; in ltr order each frame touches only the active window.
  • Backgrounded tabs resume smoothly (capped-delta clock) instead of snapping to done.
  • Accessible. Screen readers get the full text immediately via a visually-hidden copy; the animated layer is aria-hidden. prefers-reduced-motion skips the animation entirely by default.

Usage

import { decodeText } from '@bunizao/decode-text';

// Prepare + start immediately.
const controller = await decodeText(document.querySelector('.bio')!, {
  layout: 'grow',      // 'grow' | 'static'
  order: 'ltr',        // 'ltr' | 'shuffle'
});
await controller.finished;

To avoid flashing the full text before the reveal, hide the element with CSS, prepare (which blanks every slot), un-hide, then start on your own cue:

import { prepareDecode } from '@bunizao/decode-text';

const el = document.querySelector('.bio')!; // visibility: hidden in CSS
const controller = await prepareDecode(el);
el.style.visibility = 'visible';            // visible but blank — no flash
onHeroReady(() => controller.start());
controller.cancel();                        // restore original markup any time

Inline markup inside the host is flattened (except <br>); color, font-weight and font-style that differ from the host are baked onto each character, so <span class="highlight"> / <b> emphasis survives.

Options

| Option | Default | Meaning | | --- | --- | --- | | charset | __-—/\|<> | Scramble glyph pool | | cursorChar | - | Glyph a cell shows between the show and mash fronts | | layout | grow | grow (condense, monospace) / static (pop in place, any font) | | order | shuffle | Show/mash queue: shuffle (original) or ltr (smooth right-edge growth); final resolution is left to right in both modes | | showPower | 0.5 | Show front exponent — cells turn visible as p^showPower sweeps the queue | | mashPower | 2 | Mash front exponent — cursor graduates to scramble | | settleStart | 0.52 | Progress where the left-to-right resolve front starts; show/mash are packed below it | | settleCurve | 0.8 | Resolve front shape — 1 constant speed, <1 opens fast and savours the tail, >1 hesitates then finishes hard | | scrambleFromText | true | Mix the text's own ASCII glyphs into the scramble pool | | durationPerChar | 0.019 | Seconds per character, clamped to [minLineDuration, maxLineDuration] | | minLineDuration / maxLineDuration | 0.42 / 1.25 | Line duration clamp (seconds) | | lineStagger | 0.2 | Next line starts at this fraction of the summed previous durations | | lineEndGap | 0.07 | Minimum seconds between two line completions — lines always finish in reading order | | mutationHz | 18 | Scramble refresh rate per cell (wall time) | | ease | easeInOutSine | Timeline easing (t: number) => number | | fontTimeout | 400 | Max ms to wait for document.fonts.ready before measuring | | respectReducedMotion | true | Skip animation under prefers-reduced-motion | | onComplete | — | Called when the reveal finishes |

Styling

Cells carry data-state="cursor" / data-state="scramble" while animating and get a default inline opacity (0.3 / 0.55). Override with your own CSS:

.bio [data-state='scramble'] { color: var(--accent); opacity: 1 !important; }

Demo

bun run demo   # vite dev server on the demo/ playground

Publishing

bun run build emits dist/ (ESM + type declarations). Point exports at dist/ before publishing to npm if your consumers do not compile TypeScript from node_modules (this workspace consumes src/ directly via Vite).