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

pack-opener-js

v0.5.9

Published

Interactive pack-opening animation rendered by PixiJS, for React Native and the web

Readme

pack-opener-js

Interactive pack-opening animation rendered by PixiJS. One scene, two hosts: the web and React Native (inside a WebView).

The pack hangs there floating, light crossing its foil, a comet mimes the gesture, and the seal is cut by an actual finger — the tear follows the path the finger took, not a straight line between two points, glowing and throwing sparks as it goes. The lid flies off, light pours out of the pack, the card rises face down, gathers itself in its rarity colour and turns over in perspective onto its stand.

npm install pack-opener-js
# web: also install pixi.js
# React Native: also install react-native-webview react-native-reanimated

Web

import {createPackOpener} from 'pack-opener-js';

const opener = await createPackOpener(document.querySelector('#stage'), {
  assets: {pack: {url: pack.url}, card: {url: card.url}},
  theme: {glow: rarityColor},
}, {
  onEvent: event => console.log(event.type, event),
});

opener.autoSlice();   // open it without a gesture
opener.shuffle();     // carousel: spin the ring on
opener.reset();       // put it back together
opener.setOptions({assets: {pack: {url}}, motion: {speed: 2}});   // retune live
opener.destroy();

The engine is imperative on purpose — any framework can call it. For React there is a component over it:

import PackOpener from 'pack-opener-js/react';

<PackOpener
  options={{assets: {pack: {url: pack.url}}, theme: {glow: rarityColor}}}
  onOpenComplete={callOpenApi}
  onRevealComplete={showResult}
/>;

Input goes through pointer events, so the cut follows a finger, a stylus or a mouse with no separate code path.

React Native

import PackOpener from 'pack-opener-js/native';

<PackOpener
  ref={openerRef}
  options={{
    assets: {pack: {url: pack.url}, card: {url: card.url}},
    theme: {glow: rarityColor},
  }}
  disabled={busy}
  hint={<Text>Slice the seal to open</Text>}
  topSlot={title}
  bottomSlot={actions}
  renderFallback={() => <MyImage uri={pack.url} />}
  onHaptic={intent => Haptics.trigger(intent)}
  onInteractionStart={hideChrome}
  onOpenComplete={callOpenApi}
  onRevealComplete={showResult}
/>;

ref exposes autoSlice(), shuffle() and reset(); disabled covers ignoring input.

The host owns everything around the animation. The package plays no haptics and ships no typography: it reports intents (light / heavy / success) and renders whatever hint, topSlot and bottomSlot are given, positioned against the geometry the scene reports back — so slots sit on the revealed card on any screen size.

Artwork is fetched by the WebView, so the pack and card URLs must be reachable from it. Cross-origin images need CORS headers, or the host can hand over data URLs instead.

Options

Options can change while the animation is on screen. Pass a new object (React Native and React) or call setOptions on the handle (web) and the running scene picks it up — durations, colours and gesture thresholds take effect at once, and anything baked into a texture, such as the card's colours or its size, is rebuilt on the next reset() rather than mid-ceremony. Two things do need a new scene and cannot be retuned: a different variant and different pack artwork. The wrappers remount by themselves in that case; setOptions returns false so a direct caller knows.

Anything left out falls back to the chosen preset, field by field: a theme with only glow set keeps the preset's card colours. PackOpenerOptions is the full, commented surface — in short:

Well over two hundred fields, in the groups below — enough to build a noticeably different ceremony out of the same mechanic:

| Group | What it covers | | --- | --- | | variant | Which animation mechanic runs: slice, burst or carousel. | | preset | Named set of numbers for that mechanic: classic for slice, charged for burst, showcase for carousel. | | assets | Pack and card artwork, how long to wait for the card, and — for carousel — the card's facts ({label, value} pairs) and its tier badge ({label, color}), which it tells before the card is shown. | | theme | Background, rarity glow, rim, bloom, beam, sparks, the light of the cut, hint, the card back's colours and print, corner radius. | | motion | Every shared duration, plus speed as one multiplier over all of them. How the lid is thrown, tumbled and faded; how far the emptied wrapper sinks; how thin the card goes edge-on in a flat turn and how it is shaded; the beam's width, glow, tip and tail; the opacity of all three glows; the card's own flourish. | | interaction | slice only. What counts as a swipe: activation distance, commit fraction, trail sampling, cut gap and raggedness, the band it may travel through, how the blade runs out, how often progress is reported, and the arc a programmatic cut follows. | | layout | Where the pack sits and how much of the stage it takes; the card's size and widest allowed ratio; the size and softness of the three glows. | | rest | Where the revealed card and its stand come to rest, given outright in css px (card box, pedestal left/width/bottom) — for a host whose own layout decides; absent, layout decides. | | hint | slice only. The comet that mimes the swipe: position, sweep, head, tail, tail opacity and falloff, cadence. | | slice | slice only. The pack's float, the light crossing its foil and the glow round its outline; the seam, the light under the blade and the sparks off it; the flare, the spray and the jolt when the seal gives, and the light pouring out; the card's charge — beam laps, halo, printed star, sway, tremble — its turn in perspective, the landing with its push, knock and glints, and the embers after it. | | charge | burst only. How long a full charge takes and how fast it bleeds away; the shudder and squeeze of a pressured pack; the heat, halo, bloom and progress arc it lights up with; the sparks pulled in from outside; the breath of an untouched pack. | | carousel | carousel only. The ring: how many copies, its radius, how far they turn with it and how they gather toward the front, the lens and the eye's height, how a copy fades and shades as it turns away, the hover; the idle drift, the shuffle, the finger's turn and the settle; the drop, the rise and how large the held pack grows; the float; the dissolve, the pacing of the facts, the banner's slide, hold and tilt, the flip; the floor reflection. | | burst | burst only. The blast: flash, the grid the foil is torn into and how uneven that cut is, when the shards start burning as sparks and when they fade, plus the spikes, shock rings, debris streaks and glitter thrown off with them. Then the beat of quiet, and the card's arrival: the dust cloud, the grid the artwork is cut into, how far out its pieces start and how they are staggered, when the finished card comes up underneath them, the landing push and the ring that goes out with it. | | performance | Frame caps for moving, idling and sleeping; ceiling on the device pixel ratio; multisampling. | | sound | The ceremony's sound, off unless enabled: its volume, the player's own muted, the pull's badge, which decides how hard the landing hits, and the host's own samples laid over the baked cues. See Sound. |

What is deliberately not in here: easing curves, and the order of the phases themselves. A different sequence of events is a different mechanic — that is what a variant is for, and adding one does not touch this shape.

Two levels on purpose. A variant is a mechanic with its own scene module; a preset is a set of numbers for one variant. New styles arrive as variants, new looks as presets, and neither changes the shape above. Groups marked above as belonging to one mechanic are only filled in when that mechanic runs, so a host cannot spend an afternoon tuning knobs the running scene never reads.

classic is the animation as it shipped in the app this was built for — every number in it was measured against the reference recording, so it is the baseline other presets are judged against.

The three mechanics

slice — the seal is cut with a finger. The cut follows the trail and glows behind the blade, sparks flying off it; when it gives the lid tears away, light pours out of the pack, the emptied wrapper sinks and the card rises out past the lip face down, a plain card of white light. It gathers itself — a beam in its colour lapping its edge, the halo behind it filling with that colour — and turns over in perspective onto its stand, glints catching on the artwork as it lands. It asks for a small piece of skill and rewards a confident swipe.

burst — the pack is held under a finger and the pressure builds: it shudders, squeezes, heats up from the inside and a line closes around it showing how far the charge has come. At full charge it goes off: a flash, spikes of light, shock rings, and the wrapper torn into a few hundred shards that burn out as they fly, with debris streaking out and glitter left hanging in the air. Then a beat of quiet — held, not empty, the glitter is still drifting. The card does not slide in: dust gathers where it will be, resolves into pieces of the artwork, and they fly into place until the card is whole. What marks the landing is the card itself — its outline swells and a highlight runs across the artwork — rather than anything thrown at it. It asks for nerve rather than aim, and the wait is the point.

Both halves are cut by the same knife. The artwork — wrapper first, card after — is re-cut into a mosaic: one grid of nodes pushed off true, every edge torn along points both of its cells share. Pieces that interlock are what let the wrapper look whole until it bursts and the card arrive as a card rather than as a grid with gaps in it.

carousel — copies of the pack stand on a turntable, each turned with the ring and facing out, seen from in front and a little above and mirrored in the floor: the front one lit, the ones beside it turned away in perspective, the two round the back seen from behind between them. A shuffle spins the ring round, settles it on another copy and chooses it; a finger turns it, and let go it coasts and settles on the nearest. A tap on a copy chooses it too: it drops off the ring as its neighbours fade and rises alone to centre stage, where it floats until it is tapped again — the host opens the pack server-side in between, and the float lasts as long as that takes, nothing looping. The second tap dissolves the pack and the pull is told before it is shown: its facts fade in one under the other, then a pill in the tier's colour, and for a tier worth shouting about a banner slides across the stage with the tier's name running along it. Then the blank card flips over into the artwork. It asks for nothing but a choice, and the telling is the point: the reveal is read before it is seen.

The mechanics share no part of their finish, on purpose: a second animation that ends in the same spinning card back is not a second animation. slice charges the card face down and turns it over in perspective; burst tears the wrapper into pieces and then builds the card out of pieces; carousel tells the card in words and then flips it over. What they do share is the card object, its stand and the events, so a host swaps one for another with a single option and changes nothing else.

Three numbers in charged carry most of its feel: charge.holdMs, the length of the anticipation; burst.beatMs, the quiet between the blast and the dust — borrowed from slot design, where the gap before the last reel stops is what makes a result feel considered rather than rushed; and burst.assembleStagger, which decides whether the card fills in from the middle outwards or arrives as one flat sheet.

Performance

A WebGL canvas the size of a phone screen, redrawn sixty times a second, is what makes a device hot — and the scene spends most of its life with nothing new to draw: waiting for a finger, or holding a card that has already settled. So it throttles itself in three steps, all of them yours to change:

performance: {maxFps: 60, idleFps: 30, sleepFps: 1, resolutionCap: 2, antialias: true}

maxFps applies while something moves, idleFps while the pack just hangs there looping the hint, sleepFps once nothing changes at all. Commands and the first touch always restore the full rate immediately, so a sleeping scene never feels stuck. resolutionCap is the ceiling on the device pixel ratio the canvas is drawn at — phones report 3 and up, and each step multiplies the pixels the GPU shades for a scene made of soft glows, where the difference is hard to see.

Turn the caps up if you are on a desktop and want it perfectly smooth; turn them down further on low-end phones.

Sound

The ceremony can be heard: the foil tearing under the finger, the lid coming off, the sweep and the impact as the card lands, the burst's blast, the carousel's clicks, whooshes and chime. Each mechanic has its own score, played on the scene's own events with Web Audio wherever the scene runs — the browser on the web, the WebView on React Native — so a host wires nothing. It is off by default; turn it on with an option:

sound: {enabled: true, volume: 0.8, muted: false, badge: 'Big Hit'}

volume is the master level (0.8 when left out). muted is for the player's own mute button: pass the new options and what is playing fades out, while the score keeps its place — lift the mute mid-ceremony and the next cue is heard. badge is the pull's badge as the platform names it (Grail, Chase, Big Hit, Epic, Rare); the bigger the pull, the harder the landing hits. It may arrive late, with the card, and falls back to assets.card.badge's label.

A host that already scores the ceremony itself from the events (onPhase, onProgress, onTimeline and the rest) keeps sound off, or drops its own score when it turns this one on — with both, every cue plays twice.

The sounds are baked into the package, so there is nothing to host or fetch. On the web they sit in a chunk of their own that is only loaded once sound is on; the React Native wrapper lets its WebView start audio without a touch inside the page, so a pack opened with autoSlice is heard too. On iOS the session is ambient where the browser lets it be set: the ringer switch silences the ceremony and the player's music keeps playing. The files and their licences are listed in assets/sounds/CREDITS.md; replace one there and run pnpm build:sounds.

A host with sounds of its own lays them over the baked ones by cue name, a URL or a data: URI each:

sound: {
  enabled: true,
  samples: {'reveal-sweep': '/sounds/sweep.mp3', impact: '/sounds/impact.mp3'},
}

The cues are the keys of assets/sounds/manifest.json. The files are read once, when sound is first on; one that will not load leaves the baked cue in place. In the React Native WebView only data: URIs are sure to load. The score's timing is tuned to the baked files — reveal-sweep starts 2 s before the card lands, impact 0.94 s before it hits, blast 0.57 s before the flash — so a replacement with a different run-up lands off the beat.

Events

Both hosts see the same sequence, as callbacks on the web and as props on React Native: the gesture starts, the pack commits to being cut, the lid comes off (onOpenComplete — the moment to call an open API, the reveal that follows buys the request its time), and the whole ceremony ends (onRevealComplete). The carousel commits on the first tap and opens on the second, so a host that wants the open API called at the commit — and its facts and badge passed in with setOptions before the second tap — has the whole float to do it in.

Requirements

  • Web: pixi.js ^8.3 (slice turns its card and carousel draws its ring with PerspectiveMesh) and a WebGL-capable browser. react ^18 or ^19 for the /react entry.
  • React Native: 0.79 or newer (the package is resolved through exports), plus react-native-webview and react-native-reanimated. Pixi is not needed — it is compiled into the scene bundle and never reaches the host's node_modules.

Adding an animation style

A variant is a mechanic — how the pack opens and what the reveal looks like. A preset is a named set of numbers for one variant. Hosts pick both:

createPackOpener(el, {variant: 'slice', preset: 'classic', assets: {…}});
createPackOpener(el, {variant: 'burst', preset: 'charged', assets: {…}});

A variant declares itself and nothing else in the package changes — not the engine, not either wrapper:

// src/core/variants/tear/index.ts
export const tear: VariantModule = {
  id: 'tear',
  presets: PRESETS,          // its own numbers; `resolveOptions` reads them
  defaultPreset: 'classic',
  create({app, texture, options, emit}) {
    return new TearScene(app, texture, options, emit);
  },
};

create gets a live Pixi app, the loaded pack texture, options with every field already filled in, and emit. It returns the instance the engine drives: update(deltaMS), the gesture (onDown / onMove / onUp), autoSlice, reset, setEnabled, setCardTexture, a rect telling hosts where the pack was drawn, and an optional destroy. Full definitions with comments are in src/core/variants/types.ts.

Register it in src/core/variants/index.ts and add its name to VariantName in src/core/config/types.ts. Defaults resolve through the variant, so a new mechanic never inherits numbers that were tuned for a different one.

What comes out of the pack is not part of a mechanic: the card, its glows and the beam around it live in variants/shared, and a variant only picks how the card enters — place + emerge for a blast, park + slide for a cut. Option groups that belong to one mechanic are declared optional in ResolvedOptions and resolved only when its preset carries them.

Working on the package

src/core/        the engine: createPackOpener(element, options) → handle
  config/        the public option surface: types, resolve, bridge protocol
  runtime/       shared helpers (colour parsing)
  variants/      one folder per animation style
    types.ts     what a variant must provide
    index.ts     the registry
    shared/      what every mechanic reveals: geometry, textures, the card
    slice/       cut the seal: declaration, presets, scene, wrapper, hint
    burst/       charge and detonate: declaration, presets, scene, pack,
                 shreds, and the card assembled out of particles
    carousel/    choose and be told: declaration, presets, scene, the ring
                 of copies (turntable) and the story told in text
src/react/       React component for the web, over the engine
src/native/      React Native wrapper: WebView + bridge + geometry-driven slots
  webviewEntry.js  what gets bundled: engine + bridge
  sceneBundle.js   GENERATED — that bundle, with Pixi, as a JS string
playground/      Vite demo: every option as a live control
scripts/         build tooling

Both hosts run the same engine. The scene itself knows nothing about where it lives: it draws, reads the gesture it is handed, and reports through emit — which becomes a callback in a browser and a postMessage across the WebView bridge on React Native.

The scene cannot go through Metro, so it is bundled ahead of time. The committed bundle must match the sources:

pnpm build           # scene bundle, then dist
pnpm verify:scene    # fails when the committed bundle is stale
pnpm typecheck
pnpm playground      # http://localhost:5173 — /index.html and /react.html

The playground runs on built-in placeholder artwork, so it works with no assets at hand, and most of the schema is a live control there, grouped the way this README groups it — the fastest way to see what a number does. Sliders retune the running scene instead of restarting it.

Not done yet

The slice scene is plain JS, not TypeScript: it is split into modules and is covered by the variant interface, but typing it would make a second variant cheaper to write.

License

MIT for the code. The sounds carry their own licences, listed in assets/sounds/CREDITS.md.