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

@rallycry/card-tech

v0.1.1

Published

Shared card motion, lighting, effects, React shells, and WebGL foil rendering for Rally Cry products.

Readme

@rallycry/card-tech

The shared card rendering stack: motion, lighting, the effect catalogue, the card shells, and the WebGL foil path.

Install

bun add @rallycry/card-tech

React components and hooks require React 19 or newer. The WebGL entrypoint also requires three and @react-three/fiber; both remain optional for DOM-only consumers.

Quick start

Import the shared styles once at the app boundary, then import only the paths the product uses:

import "@rallycry/card-tech/card.css"
import "@rallycry/card-tech/effects.css"

import { FlipCard, HoloTiltLayer } from "@rallycry/card-tech/components"
import { resolveEffectBudget } from "@rallycry/card-tech/effects"

It exists because three codebases had each built a different two-thirds of this and none of them had all of it:

| | what it had | what it lacked | | --- | --- | --- | | universal-rally-cry | a 27-preset effect library, flip/tilt/hand/modal shells | tested motion maths; any answer to N-up cost beyond two hardcoded name lists | | full-build | headless, tested physics; spring, fan, breathe, velocity, wheel | any effect library; a consumer for half of what it had built | | conveyor | a GPU path: foil shaders, one canvas anchored to DOM rects, tier detection | the DOM effect library; anything not shaped like its own product's card |

Conveyor drag wobble

useDragGhost is the reusable lift-and-throw motion used by Conveyor's card overlay. It samples pointer velocity over a rolling window, then applies the same speed-dependent damped spring, axis limits, perspective, and lift scale as Conveyor's DragOverlayCard.

import { useRef } from "react"
import { useDragGhost } from "@rallycry/card-tech/react"

function DragGhost({ isDragging }: { isDragging: boolean }) {
  const deltaRef = useRef({ x: 0, y: 0 })
  const ghostRef = useRef<HTMLDivElement>(null)

  useDragGhost({ active: isDragging, deltaRef, ghostRef })

  return <div ref={ghostRef}>Card content</div>
}

Update deltaRef.current from a drag library or raw pointer events. The hook writes the transform in one animation frame loop without React rerenders and respects reduced-motion preferences.

Layout

The root entrypoint is headless; maths, the custom-property contract, and capability detection. No React, no design system. Everything else is a subpath so a consumer only pays for what it uses.

@rallycry/card-tech              motion maths, lighting/parallax, capabilities
@rallycry/card-tech/effects      the preset catalogue, cost budget, overlays
@rallycry/card-tech/effects.css  the preset stylesheet
@rallycry/card-tech/card.css     the shell stylesheet
@rallycry/card-tech/react        hooks: mouse/device tilt, drag flip, fan nav
@rallycry/card-tech/components   FlipCard, CardHand, HoloTiltLayer
@rallycry/card-tech/gl           the WebGL foil path (three + R3F optional peers)

three and @react-three/fiber are optional peers: importing /gl is what pulls them in.

Cross-repo consumption

Published releases are regular public npm packages, so another Rally Cry repo does not need this monorepo or a workspace link:

bun add @rallycry/card-tech@^0.1.0

The WebGL renderer remains opt-in through @rallycry/card-tech/gl. Its Three.js dependencies are optional peers, so DOM-only consumers do not install or bundle them.

To publish a release, update version and push a matching card-tech-vX.Y.Z tag. The release workflow checks, compiles, and publishes the package. Conveyor can then install that version and map its product-specific card faces onto these shared shells and effects.

The load-bearing decisions

One lighting contract, ten properties. Every effect rule reads only --rct-mx, --rct-posx, --rct-hyp, --rct-o and friends, and nothing else. That is why the pointer, drag-rotation and device-orientation drivers are interchangeable; add a driver and every downstream effect keeps working; add an effect and no driver changes.

Units in that contract are load-bearing. The percent-carrying properties feed calc(var(--rct-posx) * -2.5 + 175%) inside effect rules; a bare number invalidates the whole calc() and the layer silently falls back to 0 0. --rct-hyp and --rct-o must stay unitless because they multiply into filter() factors. Both are asserted in src/__tests__/lighting.test.ts.

One position serves both faces. A flipped card's back carries its own rotateY(180deg), which cancels the parent's flip, so the back sees the highlight exactly where the front does. Only the per-face strength differs. The web app previously wrote the same value into a separate front and back variable; mirroring instead would put the glare on the wrong shoulder after every flip.

Cost belongs to the effect. The web app encoded it as two hand-maintained lists of effect names (INTENSE_*, DENSE_VIEW_HEAVY_*) sitting in a package that knows nothing about rendering, so adding an effect meant remembering to edit a list elsewhere. Here each effect carries a cost, the host declares its environment, and resolveEffectBudget decides. That immediately surfaced a real bug: spotlight runs a window-level pointermove listener and an rAF that never settles, but was missing from the heavy list, so roster grids ran one such pair per card.

Reduced motion keeps still gradients. A budget of static still renders foil and galaxy, because a still gradient is not motion. Suppressing the effect entirely would make identity cosmetics vanish for those readers.

Unaffordable effects degrade to none, never to a cheaper lookalike. A user who picked plasma and silently got foil would think their setting had been changed.

One rAF owns the transform. Idle breathing and hover deflect are competing targets inside a single loop, not competing loops. Every loop stops at a settle epsilon and clears its inline properties; on a full table that is the difference between idle and a pegged CPU.

Tilt moves decoration, never interactive content. The tilt applies to a pointer-events: none sublayer while controls sit on a flat sibling. Tilting the whole face makes buttons drift out from under the pointer and flicker at element boundaries.

WebKit gets a different composite. Inside a deep preserve-3d subtree WebKit falls blend modes back to normal. The white spotlight still lightens, so it survives as plus-lighter; the tinted prismatic band would darken the card, so it is dropped. WebKit also refuses to apply a rounded clip to a 3D-composited layer, which is what the opaque -webkit-mask and the whole flat rendering path are for.

Theming

The package carries no design-system dependency. Everything a host can theme arrives as a custom property, documented at the top of src/components/card.css

  • surface, border, radius, shadows, and the two holo tints. A host that sets nothing still gets a legible card.

In this repo the bridge is apps/web/src/components/organisms/flip-card/cardTechTheme.ts, which maps the org's MUI palette onto the contract once at :root.

What deliberately stayed behind

  • Card faces. full-build's role/twist/event frames and the web app's seven profile layouts are game and product domain, not card tech.
  • RcCardModal. It is an MUI Modal + Slide wrapper; reimplementing a modal primitive here would mean re-solving focus trapping and portalling for no gain.
  • Conveyor's snapDOM binding, FPS bench, and card-anatomy mapping. The reusable capture queue, texture caches, and ShowcaseCard live in /gl; Conveyor supplies its product-specific rasterizer, slot content, and masks.
  • getRarity(storyPointValue). The host decides what makes a card rare and passes a resolved tier/intensity.

Storybook

bun run --cwd packages/card-tech storybook

Stories use a neutral full-bleed specimen face labelled with the effect under test. Its color and contrast make blend behavior visible without coupling the package to a product or game theme.

This repo's Storybook exercises the DOM effects and card shells. URC does not import /gl; the complete shared-canvas, DOM-capture, and ShowcaseCard integration runs in Conveyor and is covered by rallycry/conveyor#3858. Package tests cover the framework-independent WebGL registry, frame step, materials, icon textures, capture ownership, and texture-cache behavior.