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

@hippolink/ng-collector-vision

v0.2.0

Published

> Angular 22 component library for real-time, on-device TCG card scanning — powered by [CollectorVision](https://github.com/HanClinto/CollectorVision).

Readme

ng-collector-vision

Angular 22 component library for real-time, on-device TCG card scanning — powered by CollectorVision.

Point a camera at a Magic: The Gathering, Pokémon, Yu-Gi-Oh!, Lorcana, One Piece, or other supported card. Get back a card identity in under a second. No server. No API key. No cloud.

<cv-card-scanner game="magic" (cardDetected)="onCard($event)" />

npm version license: AGPL-3.0

Live demo →


What is CollectorVision?

This library is an Angular wrapper around CollectorVision — an open-source card-recognition library created by @HanClinto.

CollectorVision solves a hard computer vision problem: given a hand-held photo of a trading card (arbitrary angle, lighting, sleeve), identify the exact printing. It does this with two custom-trained ONNX neural networks:

| Model | Role | Input | Output | | ------------- | --------------- | ----------------------- | ------------------------------------- | | Cornelius | Corner detector | 384 × 384 video frame | 4 corner points + presence confidence | | Milo | Card embedder | 448 × 448 dewarped crop | 128-dimensional fingerprint |

The fingerprint is matched against a catalog of up to ~115 k reference embeddings (cosine similarity, <10 ms on device). Catalogs come from the Catalog v2 feed (FP16 base snapshot + incremental updates) and are cached in IndexedDB after the first download.

The full pipeline runs end-to-end in the browser, in a Web Worker, with no data ever leaving the device.

Try the original web demo: https://hanclinto.github.io/CollectorVision/


How the Angular wrapper adds value

CollectorVision ships a plain JavaScript Web Worker and applet. ng-collector-vision wraps it in a production-ready Angular component that:

  • Manages the camera stream lifecycle (permissions, play/pause, teardown)
  • Drives the scan loop at a configurable interval
  • Runs the confirmation bucket (N consecutive frames must agree before emitting)
  • Handles the full async init sequence: manifest → getUserMedia → worker → ready
  • Exposes everything as typed Angular signals and outputs
  • Synthesises audio feedback via WebAudio (no asset files required by default)
  • Provides CSS custom properties for theming
  • Emits cardDetected with SSR safety, abort handling, and correct cleanup on destroy

Installation

npm install @hippolink/ng-collector-vision

1 — Copy runtime assets (angular.json)

The package ships the CollectorVision Web Worker, ONNX WASM runtime, and a Cross-Origin Isolation service worker. Add them to your application's angular.json assets:

"assets": [
  {
    "glob": "**/*",
    "input": "node_modules/@hippolink/ng-collector-vision/collectorvision",
    "output": "/collectorvision"
  },
  {
    "glob": "coi-serviceworker.js",
    "input": "node_modules/@hippolink/ng-collector-vision/collectorvision",
    "output": "/"
  }
]

Why? The inference pipeline runs in a Web Worker using multi-threaded WASM (SharedArrayBuffer). SharedArrayBuffer requires Cross-Origin Isolation — the included service worker injects the required COOP/COEP headers on every same-origin response so this works on any static host.

2 — Register the service worker (index.html)

Add this before the Angular bootstrap <script> tag:

<script>
  if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('coi-serviceworker.js').then((reg) => {
      if (reg.installing) {
        reg.installing.addEventListener('statechange', (e) => {
          if (e.target.state === 'activated') location.reload();
        });
      }
    });
  }
</script>

On the first page load the service worker installs, claims the page, and triggers one reload so the page runs under the isolation headers. All subsequent visits are seamless and instant.

Note: Use a relative path (coi-serviceworker.js, no leading /) so the registration works both at the root domain and under a subdirectory (e.g. GitHub Pages).

3 — Add the model manifest

The ONNX models are shared by every game, so one manifest covers them all:

public/collectorvision/assets/manifest.json
{
  "version": "2026-09-20",
  "models": {
    "detector": "https://huggingface.co/HanClinto/cornelius/resolve/9280009f5a66f75f952820d9dadb894909b759b7/cornelius-2.12.onnx",
    "milo": "https://huggingface.co/HanClinto/milo/resolve/9bcc5e809e936b8c5630d1e7101aae1de1e76621/model.onnx"
  },
  "model_hashes": {
    "detector": "650da3cc3e9ac778c6951de631f824ec1e63bdabf3aaa39a35d7435af625612e",
    "milo": "bd13d8d60383c69da04dce261f32e93fdaeaa8fd618fbc991e7385f71b3d45df"
  },
  "model_sizes": { "detector": 4407545, "milo": 5191100 },
  "detector": {
    "family": "cornelius",
    "input_size": 384,
    "preprocess": "imagenet-rgb",
    "outputs": { "corners": "corners", "presence": "presence", "sharpness": "sharpness" }
  },
  "catalog": { "dims": 128 }
}

model_hashes are used as the IndexedDB cache key, so a new model file always busts the cache. Pin the model URLs to a Hugging Face revision as above (the CollectorVision model registry lists the current ones) — resolve/main/… can change under you.

Catalogs are not configured here. The worker loads them by game (and source) from the Catalog v2 feed — a FP16 base snapshot plus incremental updates, cached in IndexedDB. Supported game values:

| game | Source(s) | Card ID | | ----------------------------------------------------------------------------- | ----------------------- | ---------------------------- | | magic | tcgplayer, scryfall | TCGplayer id / Scryfall UUID | | pokemon, pokemon-japan, yugioh, lorcana, onepiece, fab, digimon | tcgplayer | TCGplayer id | | swu, union-arena, gundam, riftbound | tcgplayer | TCGplayer id |

Set the source input to scryfall (Magic only) to get Scryfall UUIDs and an oracle ID for grouping printings. The default is tcgplayer.

Models (~10 MB) and the catalog (~1–30 MB depending on game) download once and are cached in IndexedDB. Subsequent launches are instant, even offline.

4 — Provide HTTP client

// app.config.ts
import { provideHttpClient } from '@angular/common/http';

export const appConfig: ApplicationConfig = {
  providers: [provideHttpClient()],
};

Quick start

// my-scanner.component.ts
import { Component } from '@angular/core';
import { CardScannerComponent, type CardDetection } from '@hippolink/ng-collector-vision';

@Component({
  selector: 'app-my-scanner',
  imports: [CardScannerComponent],
  template: `
    <cv-card-scanner
      game="magic"
      (cardDetected)="onCard($event)"
      (scannerClosed)="isOpen = false"
    />
  `,
})
export class MyScannerComponent {
  isOpen = true;

  onCard(detection: CardDetection): void {
    // detection.cardId is a TCGplayer product ID (source 'tcgplayer', the default)
    // or a Scryfall UUID (source 'scryfall', Magic only)
    console.log(detection.cardId, detection.score.toFixed(3));
  }
}

Component API

Selector

<cv-card-scanner … />

Inputs

| Input | Type | Default | Description | | --------------------- | ------------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | game | CardScannerGame | required | Active game. Changing this value restarts the scanner. | | minCornerConfidence | number | 0.02 | Cornelius gate threshold [0–1]. Raise to reduce false positives in cluttered scenes. Updated live — no restart required. | | minAcceptanceScore | number | 0.5 | Minimum Milo cosine similarity before a frame is passed to the confirmation bucket [0–1]. Updated live. | | consecutiveMatches | number | 2 | Number of consecutive matching frames required before cardDetected fires. Higher = more certain, slower. Updated live. | | cooldownMs | number | 3500 | Cooldown (ms) before the same card can fire cardDetected again. Updated live. | | source | CardScannerSource | 'tcgplayer' | Catalog source ('scryfall' is Magic only). Read when the scanner starts. | | groupBySecondaryId | boolean | true | When true, alternate printings of the same card (same oracle/secondary ID) are grouped together for the confirmation streak. Updated live. | | scanIntervalMs | number | 900 | Interval between frame captures (ms). Minimum 100 recommended. Updated live. | | playSounds | boolean | true | Enable synthesized audio feedback. | | confidentSoundUrl | string \| null | null | WAV (or any Web Audio–decodable format) for a confident detection. null → synthesized two-note chime. | | uncertainSoundUrl | string \| null | null | Sound for a low-confidence detection. null → synthesized blip. |

CardScannerGame:

type CardScannerGame =
  | 'magic'
  | 'pokemon'
  | 'pokemon-japan'
  | 'yugioh'
  | 'lorcana'
  | 'onepiece'
  | 'fab'
  | 'digimon'
  | 'swu'
  | 'union-arena'
  | 'gundam'
  | 'riftbound';

type CardScannerSource = 'tcgplayer' | 'scryfall';

Outputs

| Output | Payload | When | | --------------- | --------------- | ----------------------------------------------------------------------------------------- | | cardDetected | CardDetection | Every confirmed detection (after streak + cooldown). | | scannerClosed | void | When close() is called, or when the component is destroyed while the scanner is active. |

Public signals (read-only)

| Signal | Type | Description | | ------------------ | ------------------------------------ | ------------------------------------------------------------------------ | | status | ScannerStatus | Current lifecycle state (see below). | | downloadProgress | number | Download progress [0–100] during the 'downloading' phase. | | errorMessage | string | Human-readable error message when status() === 'error'. | | cornerConfidence | number | Latest Cornelius confidence value from the worker (updated every frame). | | viewfinderFlash | 'confident' \| 'uncertain' \| null | Active viewfinder ring animation. | | isScanning | computed | true when status() === 'scanning'. |

Methods

| Method | Description | | --------------- | --------------------------------------------------------------------- | | openScanner() | Start (or restart) the scanner. Called automatically on mount. | | close() | Stop camera and worker, set status to 'idle', emit scannerClosed. |

ScannerStatus

type ScannerStatus =
  | 'idle' // Not started, or after close()
  | 'requesting-permission' // Waiting for the browser camera permission prompt
  | 'downloading' // Fetching models + catalog (shows download progress)
  | 'scanning' // Live — will emit cardDetected
  | 'error'; // Unrecoverable. Read errorMessage(). Call openScanner() to retry.

CardDetection

interface CardDetection {
  /** Raw card identifier from the catalog.
   *  source 'tcgplayer' → integer string  e.g. "507371"
   *  source 'scryfall'  → UUID            e.g. "abc123-..." */
  cardId: string;

  /** Card name from the catalog record, if present. */
  cardName: string | null;

  /** Oracle ID (Scryfall source only). */
  secondaryId: string | null;

  /** Name of the secondaryId field, e.g. "scryfallOracleId". */
  secondaryIdField: string | null;

  /** Cosine similarity score [0, 1]. Values ≥ 0.8 are high-confidence. */
  score: number;

  /** Cornelius corner-detector confidence for this frame. */
  confidence: number;

  /** Normalised corner coordinates [[x, y], ...] in [0, 1]. */
  corners: [number, number][];

  /** Milo sharpness estimate, if the model provides it. */
  sharpness: number | null;

  /** Card orientation detected by Milo. */
  orientation: 'upright' | 'rotated_180' | null;

  /** True when score < 0.8. Consumer should flag the card for manual review. */
  needsReview: boolean;

  /** ISO 8601 timestamp of the detection. */
  detectedAt: string;
}

Theming

The component renders only the raw viewfinder (video + canvas overlay). Status text, error messages, flash rings, and all other chrome are the consumer's responsibility — use the public signals to drive your own UI.

Adding a flash ring

Use viewfinderFlash (read via a template reference) to apply your own animation:

<div
  class="scanner-wrap"
  [class.flash-confident]="scanner.viewfinderFlash() === 'confident'"
  [class.flash-uncertain]="scanner.viewfinderFlash() === 'uncertain'"
>
  <cv-card-scanner #scanner game="magic" (cardDetected)="onCard($event)" />
</div>
@keyframes ring-confident {
  0% {
    box-shadow: 0 0 0 4px rgba(34, 197, 94, 0.9);
  }
  100% {
    box-shadow: 0 0 0 4px rgba(34, 197, 94, 0);
  }
}
@keyframes ring-uncertain {
  0% {
    box-shadow: 0 0 0 4px rgba(245, 158, 11, 0.9);
  }
  100% {
    box-shadow: 0 0 0 4px rgba(245, 158, 11, 0);
  }
}
.scanner-wrap {
  border-radius: 12px;
}
.flash-confident {
  animation: ring-confident 0.9s ease-out forwards;
}
.flash-uncertain {
  animation: ring-uncertain 0.9s ease-out forwards;
}

Handling detections

Deduplication and quantity tracking

cardDetected fires once per confirmation streak — the built-in cooldown (default 3.5 s) prevents rapid re-fires. Consumer code is responsible for maintaining a list:

protected readonly cards = signal<CardDetection[]>([]);

onCard(detection: CardDetection): void {
  this.cards.update(list => {
    const i = list.findIndex(c => c.cardId === detection.cardId);
    // Re-scan of the same card after cooldown — increment quantity in your own model
    if (i >= 0) return list; // or increment a counter
    return [detection, ...list];
  });
}

Enriching with Scryfall (Magic)

For Magic cards scanned with the tcgplayer source, cardId is a TCGplayer integer ID. Scryfall can resolve it:

fetch(`https://api.scryfall.com/cards/tcgplayer/${detection.cardId}`)
  .then((r) => r.json())
  .then((card) => console.log(card.name, card.image_uris?.small));

For the scryfall source ([source]="'scryfall'"), cardId is directly a Scryfall UUID:

fetch(`https://api.scryfall.com/cards/${detection.cardId}`)
  .then((r) => r.json())
  .then((card) => console.log(card.name));

Handling the needsReview flag

@if (detection.needsReview) {
<span class="warning">Low confidence — please verify this card visually.</span>
}

Showing the scanner status

@switch (scanner.status()) { @case ('requesting-permission') {
<p>Please allow camera access in your browser.</p>
} @case ('downloading') {
<p>Loading… {{ scanner.downloadProgress() }}%</p>
} @case ('error') {
<p class="error">{{ scanner.errorMessage() }}</p>
<button (click)="scanner.openScanner()">Retry</button>
} }

Where scanner is a viewChild(CardScannerComponent) reference.


Advanced configuration

Asset base path

CV_ASSET_BASE_PATH controls where the model manifest and sound files are fetched from. Override to serve these from your own platform or CDN:

import { CV_ASSET_BASE_PATH } from '@hippolink/ng-collector-vision';

providers: [{ provide: CV_ASSET_BASE_PATH, useValue: 'https://platform.example.com' }];

Note: scanner.worker.mjs and its bundled vendor files always load from the local origin — cross-origin workers require explicit Access-Control-Allow-Origin headers that most servers don't provide. CV_ASSET_BASE_PATH does not control the worker path.

Cloudflare Pages and hosts with a per-file size limit

The bundled ONNX Runtime WASM binary (ort-wasm-simd-threaded.asyncify.wasm) is ~27 MB, which exceeds Cloudflare Pages' per-file limit. Redirect it to a CDN using the pre-built constant:

import { CV_WASM_BASE_PATH, ORT_CDN_WASM_PATH } from '@hippolink/ng-collector-vision';

providers: [{ provide: CV_WASM_BASE_PATH, useValue: ORT_CDN_WASM_PATH }];

ORT_CDN_WASM_PATH points to [email protected] on jsDelivr. Alternatively, use your own R2 bucket or any URL that serves the WASM with correct Content-Type: application/wasm headers:

providers: [{ provide: CV_WASM_BASE_PATH, useValue: 'https://pub-xxx.r2.dev/wasm/' }];

The WASM file remains in the npm package for hosts that can serve it locally — CV_WASM_BASE_PATH is purely opt-in.

Custom sounds

Replace the synthesized WebAudio tones with your own WAV files:

<cv-card-scanner
  game="magic"
  confidentSoundUrl="/sounds/pickup_high.wav"
  uncertainSoundUrl="/sounds/scan.wav"
  (cardDetected)="onCard($event)"
/>

Silent mode

<cv-card-scanner game="magic" [playSounds]="false" (cardDetected)="onCard($event)" />

Tuning the scanner

All tuning parameters update live without restarting the worker or camera:

<cv-card-scanner
  game="magic"
  [minCornerConfidence]="0.05"
  [minAcceptanceScore]="0.6"
  [consecutiveMatches]="3"
  [cooldownMs]="5000"
  [scanIntervalMs]="500"
  [groupBySecondaryId]="false"
  (cardDetected)="onCard($event)"
/>

Architecture

┌─────────────────────────────────────────────────────┐
│ cv-card-scanner (Angular component)                  │
│                                                       │
│  Camera (getUserMedia)                               │
│    │ every scanIntervalMs                            │
│    ▼                                                  │
│  canvas.drawImage(video)                             │
│    │ createImageBitmap()  [transferred]              │
│    ▼                                                  │
│  scanner.worker.mjs  ◄─── manifest.json             │
│  ┌─────────────────┐       models + catalog         │
│  │ Cornelius (ONNX)│  384×384 → corners             │
│  │ Dewarp          │  perspective transform          │
│  │ Milo (ONNX)     │  448×448 → 128-d embedding     │
│  │ Cosine search   │  embedding × catalog            │
│  └────────┬────────┘                                 │
│           │ WorkerResultMsg { cardId, score, ... }   │
│           ▼                                          │
│  ConfirmationBucket (N consecutive matches)          │
│           │ confirmed                                │
│           ▼                                          │
│  cardDetected.emit(CardDetection)                    │
└─────────────────────────────────────────────────────┘

First-launch latency: models download once (~10 MB) and catalog downloads once (1–30 MB depending on game), both cached in IndexedDB. Cold start is the only slow path.

Steady-state latency: <100 ms per inference on a mid-range laptop CPU (WASM SIMD, multi-threaded).


Browser support

Requires:

  • getUserMedia (camera access)
  • Web Workers with ES modules
  • createImageBitmap
  • SharedArrayBuffer + Cross-Origin Isolation (provided by the bundled service worker)
  • IndexedDB (for model/catalog caching)

Tested on: Chrome 120+, Firefox 121+, Safari 17+, Android Chrome.


Upgrading from 0.1.x

0.2.0 moves to CollectorVision Catalog v2 (the models are unchanged):

  • Replace the per-game assets/<game>/manifest.json files with a single assets/manifest.json (see setup step 3). Catalogs are no longer named in the manifest.
  • Copy collectorvision/lib/ next to scanner.worker.mjs (the worker imports the Catalog v2 client from it). The default **/* glob does this; a hand-picked asset list must include lib/**.
  • Allow https://hanclinto.github.io in your CSP connect-src (the catalog feed) alongside Hugging Face.
  • CardDetection gains cardName. cardId is unchanged for the default tcgplayer source; use [source]="'scryfall'" for Magic Scryfall UUIDs. Oracle-id grouping only applies to the Scryfall source.
  • CardScannerGame gains pokemon-japan, yugioh, fab, digimon, swu, union-arena, gundam, riftbound.

License

AGPL-3.0 — see LICENSE.

This package bundles assets from CollectorVision (AGPL-3.0, by @HanClinto). Commercial use of the underlying models may require a separate license — see the CollectorVision repository for details.

The bundled onnxruntime-web WASM runtime is licensed under the MIT License.