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

@valantify/runtime

v0.0.7

Published

Client-side runtime for displaying product variations with animated transitions.

Readme

@valantify/runtime

Client-side runtime for displaying product variations with animated transitions.

Purpose

This package provides the browser runtime that consumes assets generated by the Valantify CLI. It displays product images with smooth animated transitions between variations and perspectives.

Installation

npm install @valantify/runtime

Quick Start

import {
  showVariation,
  getVariation,
  initCameraNavigator,
  renderStepOptions,
} from '@valantify/runtime'
import type { Step } from '@valantify/steps'

// Show a variation (automatically initializes everything)
await showVariation('#viewer', 'shoe', 'blue')

// Enable drag-based perspective navigation
initCameraNavigator('#viewer', {
  shoe: {
    cameraPositions: [
      { file: 'front', x: 0, y: 0, z: -1 },
      { file: 'left',  x: 1, y: 0, z: 0 },
      { file: 'right', x: -1, y: 0, z: 0 },
    ]
  }
})

// Get current state
const current = getVariation('#viewer')
// → { group: 'shoe', variation: 'blue', perspective: 'front' }

const STEPS: Step[] = [...]

const rendering = {
  optionsContainer: (optionsHtml: string) => `<div class="options">${optionsHtml}</div>`,
  option: {
    color: (hex: string) => `<button class="option"><span style="background:${hex}"></span></button>`,
    image: (url: string) => `<button class="option"><img src="${url}" alt="" /></button>`,
    text: (label: string) => `<button class="option">${label}</button>`,
  },
}

// Render one step's options into any container
renderStepOptions('#step-options', STEPS, 0, ['#viewer'], rendering)

How It Works

The runtime expects assets in this structure (generated by the CLI):

public/
└── valantify/
    └── shoe/                   # Product group
        ├── product.json        # Product metadata (title, description, perspectives, dimensions)
        ├── red/                # Variation
        │   ├── variant.json    # Variation metadata (features, description)
        │   ├── front.png
        │   ├── side.png
        │   └── transitions/    # Videos between perspectives
        │       └── front-to-side.mp4
        │       └── frames/     # Extracted frames (0-100.webp)
        │           └── front-to-side/0.webp
        └── blue/
            ├── variant.json
            ├── front.png
            └── side.png

Transition Behavior

  • Same perspective, different variation → Instant image swap
  • Same variation, different perspective → Plays transition frames (if available)
  • Both change → Swaps variation first, then animates perspective

API Reference

Core Functions

showVariation(element, group, variation, options?)

Display a product variation with optional animation.

await showVariation('#viewer', 'shoe', 'blue')
await showVariation('#viewer', 'shoe', 'blue', { perspective: 'side' })
await showVariation('#viewer', 'shoe', 'blue', { animated: false })

getVariation(element)

Get the current state from DOM attributes.

const info = getVariation('#viewer')
// → { group: 'shoe', variation: 'blue', perspective: 'front' }

initCameraNavigator(element, options)

Enable drag-based perspective navigation. Returns a cleanup function.

const cleanup = initCameraNavigator('#viewer', {
  shoe: {
    threshold: 30,
    dragToScrub: true,
    maxScrubDistance: 200,
    cameraPositions: [
      { file: 'front', x: 0, y: 0, z: -1 },
      { file: 'left',  x: 1, y: 0, z: 0 },
    ]
  }
})

// Later: remove all listeners
cleanup.destroy()

renderStepOptions(element, steps, stepIndex, viewers, rendering)

Render selectable options for a single step into any container. The runtime handles available options, selection state, cascade updates, and click wiring.

renderStepOptions('#step-options', STEPS, 0, ['#viewer'], rendering)
renderStepOptions('#step-options', STEPS, 1, ['#viewer'], rendering) // replace render for step 1

Behavior:

  • Calls your rendering.option.{color|image|text} for each available option
  • Wraps options with rendering.optionsContainer(...)
  • Marks each option root with data-active="true|false"
  • Replacing the same element is idempotent (no teardown needed)
  • Auto-cleans when element is removed from DOM
  • Auto-cascades across all rendered containers that share any viewer
  • Dispatches valantify:change on viewers after a selection

Selection/cascade event:

viewer.addEventListener('valantify:change', (event) => {
  const { visibleSteps } = (event as CustomEvent).detail
  // update title/counter/prev-next UI
})

Main Exports

The runtime exports:

  • showVariation(element, group, variation, options?) - Display a product variation
  • getVariation(element) - Get current state from DOM attributes
  • initCameraNavigator(element, options) - Enable drag-based perspective navigation
  • renderStepOptions(element, steps, stepIndex, viewers, rendering) - Render step options with auto-cascade

Drag-Based Perspective Navigation

Configuration Options

interface CameraGroupConfig {
  threshold?: number;        // Min drag distance to trigger (default: 30px)
  dragToScrub?: boolean;     // Scrub frames during drag (default: false)
  maxScrubDistance?: number; // Drag distance for full progress (default: 200px)
  cameraPositions: CameraPosition[];
}

interface CameraPosition {
  file: string;  // Perspective filename (without extension)
  x: number;     // Horizontal position on the unit circle
  y: number;     // Vertical elevation (typically 0 for flat rotation)
  z: number;     // Depth position on the unit circle
}

How Camera Positions Work

Each perspective is placed at a point on a unit circle. The runtime computes relative directions between positions and matches them to drag direction using dot product. Only relative placement matters — coordinates are normalized internally.

Drag mapping: drag right → +x, drag down → -z

How to determine coordinates: Place perspectives around a circle so adjacent angles are ~45-90° apart. Use cos/sin to convert angles to (x, z) coordinates. The exact values don't matter as long as the relative spacing is correct.

Common Patterns

Simple 4-direction rotation (90° apart):

const CAMERA_POSITIONS = [
  { file: 'front', x: 0, y: 0, z: -1 },
  { file: 'left',  x: 1, y: 0, z: 0 },
  { file: 'back',  x: 0, y: 0, z: 1 },
  { file: 'right', x: -1, y: 0, z: 0 },
]

With diagonal perspectives (45° apart):

const CAMERA_POSITIONS = [
  { file: 'front',      x: 0,     y: 0, z: -1 },
  { file: 'front-left', x: 0.707, y: 0, z: -0.707 },  // 45° between front and left
  { file: 'left',       x: 1,     y: 0, z: 0 },
]

With elevated views:

const CAMERA_POSITIONS = [
  { file: 'front',     x: 0, y: 0,   z: -1 },
  { file: 'front-top', x: 0, y: 0.5, z: -0.866 },  // 30° above front
  { file: 'left',      x: 1, y: 0,   z: 0 },
]

How Drag Navigation Works

  1. On drag start: Captures pointer, stores current perspective
  2. During drag:
    • Converts drag delta to 3D direction vector
    • Uses dot product to find best-matching perspective
    • If dragToScrub enabled, scrubs transition frames to match progress
    • Applies hysteresis to prevent jitter between similar perspectives
  3. On release:
    • Past 50% progress → completes transition to target
    • Before 50% → smoothly animates back to original perspective

Frame Scrubbing

When dragToScrub: true, transition frames scrub based on drag progress:

initCameraNavigator('#viewer', {
  shoe: {
    dragToScrub: true,        // Enable scrubbing
    maxScrubDistance: 200,    // 200px drag = 100% progress
    cameraPositions: [...]
  }
})

The scrubbing:

  • Projects drag distance along the target direction (not raw distance)
  • Handles both forward and reverse frame sequences automatically
  • Falls back to instant swap if no frames exist

Frame URL patterns (resolution based on scrub velocity, with idle upgrades):

/valantify/<group>/<variation>/transitions/frames/<from>-to-<to>/<0-100>.webp        # Low (rapid scrubbing only)
/valantify/<group>/<variation>/transitions/frames/<from>-to-<to>/<0-100>-medium.webp # Medium (normal scrubbing)
/valantify/<group>/<variation>/transitions/frames/<from>-to-<to>/<0-100>-high.webp   # High (slow/stopped)

When scrubbing slows (sustained low velocity) or stops, the runtime automatically upgrades from low → medium → high after a short pause, even if the last drag velocity was high.

Debug Mode

Enable debug mode to log frame resolution statistics:

import { configure } from '@valantify/runtime'

configure({ debug: true })

With debug enabled, the runtime logs the percentage of frames displayed at each resolution tier after 5 seconds of inactivity:

[valantify] Frame resolution stats: low=15.2% (23), medium=61.8% (94), high=23.0% (35) [total=152]

This helps analyze scrubbing behavior and optimize the resolution thresholds.

Scrub Debug Logging

For detailed scrub controller debugging, pass debugScrub in the settings:

initCameraNavigator('#viewer', {
  shoe: {
    dragToScrub: true,
    cameraPositions: [...]
  }
}, { debugScrub: true })

This logs every frame decision with the reason:

[scrub] quality-decision: frame=42 quality=medium reason=velocity-medium velocity=0.00450 target=medium
[scrub] frame-render: frame=42 quality=medium reason=velocity-medium velocity=0.00450
[scrub] upgrade-scheduled: frame=42 quality=medium reason=scheduled-upgrade velocity=0.00120 target=high
[scrub] upgrade-applied: frame=42 quality=high reason=idle-upgrade velocity=0.00000

Quality reasons:

  • velocity-high - Rapid scrubbing, using low quality
  • velocity-medium - Normal scrubbing, using medium quality
  • velocity-low - Slow/stopped, using high quality
  • cached-upgrade - Found higher quality in cache
  • cached-fallback - Fell back to lower cached quality
  • scheduled-upgrade - Scheduled upgrade after delay
  • idle-upgrade - Upgraded after idle timeout

TypeScript

All types are exported:

import type {
  // Core types
  VariationInfo,
  ShowVariationOptions,
  ValantifyConfig,
  Rendering,
  ValantifyChangeEventDetail,
  // Camera navigator types
  CameraPosition,
  CameraGroupConfig,
  CameraNavigatorOptions,
  CameraNavigatorCleanup,
  // Scrub debug types
  ScrubQualityReason,
  ScrubDebugEvent,
  ScrubDebugLogger,
} from '@valantify/runtime'

Development

# Type check
yarn typecheck

Integration

This package is used by:

  • clients/main - Web interface for building visualizers
  • CLI-generated static sites - Embedded via valantify template command (camera positions provided at generation time)

The CLI (clients/cli) generates the assets that this runtime consumes.