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

@instantphoto/react

v0.4.3

Published

React components for instant film photo effects using WebGL

Readme

@instantphoto/react

CI Storybook npm Coverage Bundle size License: MIT TypeScript React

React component library for applying authentic Polaroid 600 and Fujifilm Instax instant-film effects to images — entirely in the browser via WebGL.

  • Zero server dependency — runs 100% client-side
  • 300 DPI export for print-ready output
  • Interactive pan, pinch-to-zoom, and scroll zoom
  • Keyboard shortcuts (arrow keys, +/-, R, Ctrl+Z)
  • Undo/redo transform history
  • Batch processing API
  • TypeScript-first, strict types, no any

Installation

bun add @instantphoto/react

Peer dependencies — install separately if not already present:

bun add react react-dom   # ^18 or ^19

Live demo

Browse the component stories on GitHub Pages.


Quick start

import { InstantPhotoFrame } from '@instantphoto/react'
import '@instantphoto/react/styles.css'

export function App() {
  return <InstantPhotoFrame src="/my-photo.jpg" />
}

Components

<InstantPhotoFrame>

Static display component — renders a photo with film effects on a Polaroid/Instax card. No interactive pan/zoom.

import { useState } from 'react'
import { InstantPhotoFrame, type CaptureFn } from '@instantphoto/react'
import '@instantphoto/react/styles.css'

function PhotoCard() {
  const [capture, setCapture] = useState<CaptureFn>()

  return (
    <>
      <InstantPhotoFrame
        src="/photo.jpg"
        frameType="polaroid_600"
        filmType="polaroid"
        width={320}
        onRender={fn => setCapture(() => fn)}
        onError={err => console.error(err)}
      />

      <button onClick={() => capture?.().then(blob => saveAs(blob, 'photo.png'))}>
        Download image
      </button>

      <button onClick={() => capture?.({ target: 'frame' }).then(blob => saveAs(blob, 'frame.png'))}>
        Download with frame
      </button>
    </>
  )
}

<InstantPhotoImageEditor>

Interactive editor with pan, pinch-to-zoom, scroll zoom, keyboard shortcuts, and undo/redo.

import { useState } from 'react'
import { InstantPhotoImageEditor, type CaptureFn, type InstantPhotoSettings } from '@instantphoto/react'
import '@instantphoto/react/styles.css'

function Editor() {
  const [src, setSrc] = useState<string>()
  const [capture, setCapture] = useState<CaptureFn>()
  const [settings, setSettings] = useState<InstantPhotoSettings>()

  return (
    <>
      <input type="file" onChange={e => setSrc(URL.createObjectURL(e.target.files![0]))} />

      <InstantPhotoImageEditor
        src={src}
        frameType="polaroid_600"
        filmType="polaroid"
        width={480}
        onRender={fn => setCapture(() => fn)}
        onSettingsChange={setSettings}
        onUndo={t => console.log('Undo to', t)}
        onRedo={t => console.log('Redo to', t)}
      />

      <button disabled={!capture} onClick={() => capture?.()}>
        Export
      </button>

      {settings && (
        <pre>{JSON.stringify(settings, null, 2)}</pre>
      )}
    </>
  )
}

<InstantPhotoEditor>

Higher-level convenience component that wraps InstantPhotoImageEditor and wires up a built-in upload button (shown when no image is loaded) and a delete button (shown as an overlay when an image is loaded).

import { useState } from 'react'
import { InstantPhotoEditor } from '@instantphoto/react'
import '@instantphoto/react/styles.css'

function App() {
  const [src, setSrc] = useState<string>()

  return (
    <InstantPhotoEditor
      src={src}
      frameType="polaroid_600"
      width={400}
      onUpload={file => setSrc(URL.createObjectURL(file))}
      onDelete={() => setSrc(undefined)}
    />
  )
}
  • When onUpload is provided and src is not set, a camera-icon upload button is centred inside the frame.
  • When onDelete is provided and src is set, a circular × button appears in the top-right corner of the image.
  • All InstantPhotoImageEditor props (except emptyState/imageOverlay) are forwarded as-is.

Keyboard shortcuts (click/focus the editor first):

| Key | Action | |-----|--------| | Arrow keys | Pan | | + / = | Zoom in | | - | Zoom out | | r / R / 0 | Reset pan & zoom | | Ctrl+Z / Cmd+Z | Undo | | Ctrl+Y / Ctrl+Shift+Z | Redo |


Props

Shared props (InstantPhotoFrame and InstantPhotoImageEditor)

| Prop | Type | Default | Description | |------|------|---------|-------------| | src | string \| HTMLImageElement \| ImageBitmap | — | Image source. InstantPhotoImageEditor makes this optional. | | frameType | FrameType | 'polaroid_600' | Frame format. | | filmType | FilmType | 'polaroid' | Film emulsion profile. | | grainAmount | number | film default | Grain intensity 0–1. | | grainSizePx | number | 2.08 | Grain particle size in output pixels. | | grainColorAmount | number | 1.0 | Colored grain: 0 = monochrome, 1 = subtle color. | | halationAmount | number | film default | Bloom/halation intensity 0–1. | | vignetteIntensity | number | film default | Vignette intensity 0–1. | | chromaticShift | number | film default | Chromatic aberration in source pixels. | | saturationDelta | number | film default | Saturation shift –100 to +100. | | filmCurveAmount | number | 1.0 | Film color curve blend 0–1. | | shadowWideIntensity | number | 0.31 | Large inbound shadow intensity. | | seed | number | 0 | Grain seed. 0 = random each render. | | width | number \| string | '100%' | CSS width of the frame. | | className | string | — | Extra CSS class on the frame div. | | style | CSSProperties | — | Inline styles on the frame div. | | onRender | (capture: CaptureFn) => void | — | Called after each render with a capture function. | | onError | (error: Error) => void | — | Called on WebGL or image loading errors. |

InstantPhotoImageEditor-only props

| Prop | Type | Default | Description | |------|------|---------|-------------| | emptyState | React.ReactNode | — | Rendered centred inside the image area when src is not set. | | imageOverlay | React.ReactNode | — | Rendered on top of the photo when src is set. Wrapper has pointer-events: none; add pointer-events: auto to interactive children. | | maxZoom | number | 5 | Maximum zoom factor. | | onRenderDelay | number | 600 | Debounce ms before onRender fires after a gesture. | | liveUpdateDuringGesture | boolean | true | Full effects during drag (true) or raw preview (false). | | onTransformChange | (transform: ImageTransform) => void | — | Called on every gesture event with updated pan/zoom. | | onSettingsChange | (settings: InstantPhotoSettings) => void | — | Called when transform or effect params change. | | onUndo | (transform: ImageTransform) => void | — | Called after keyboard undo is applied. | | onRedo | (transform: ImageTransform) => void | — | Called after keyboard redo is applied. |

InstantPhotoEditor-only props

Accepts all InstantPhotoImageEditor props (except emptyState/imageOverlay) plus:

| Prop | Type | Default | Description | |------|------|---------|-------------| | onUpload | (file: File) => void | — | Called when the user picks a file. Shows an upload button when set. | | onDelete | () => void | — | Called when the delete overlay is clicked. Shows a delete button when set and src is provided. | | accept | string | 'image/*' | accept attribute forwarded to the hidden file input. |


Frame types

| FrameType | Physical format | Canvas size | |-------------|-----------------|-------------| | polaroid_600 | Polaroid 600 (79×79 mm) | 933×933 px | | instax_mini | Fujifilm Instax Mini (46×62 mm) | 543×732 px | | instax_square | Fujifilm Instax Square (62×62 mm) | 732×732 px | | instax_wide | Fujifilm Instax Wide (99×62 mm) | 1169×732 px |

All canvas dimensions are at 300 DPI, so exported images are print-ready at the authentic physical size.

Film types

| FilmType | Description | |------------|-------------| | polaroid | Polaroid 600 emulation — warm tones, prominent grain, strong vignette | | instax | Fujifilm Instax emulation — cooler tones, lighter grain and vignette | | original | No film emulation — only inbound shadow applied |


Capture API

onRender provides a stable CaptureFn that can be called at any time:

type CaptureFn = (options?: CaptureOptions) => Promise<Blob | null>

interface CaptureOptions {
  target?: 'image' | 'frame'   // 'image' = canvas only; 'frame' = full frame with white border
  format?: 'image/png' | 'image/jpeg' | 'image/webp'
  quality?: number              // 0–1, for JPEG/WebP only
}
// Export just the photo at 300 DPI:
const blob = await capture()

// Export the full frame (white border included):
const blob = await capture({ target: 'frame', format: 'image/jpeg', quality: 0.9 })

Batch processing

Process multiple images off-screen with a single shared WebGL pipeline:

import { batchProcess } from '@instantphoto/react'

const blobs = await batchProcess(
  files.map(f => ({ src: URL.createObjectURL(f) })),
  {
    frameType: 'instax_mini',
    glOptions: { filmType: 'instax' },
    captureOptions: { target: 'frame', format: 'image/jpeg', quality: 0.92 },
    onProgress: (done, total) => setProgress(done / total),
  }
)

Settings export

InstantPhotoSettings is a serialisable snapshot of all editor parameters at a point in time. Use it to persist or restore editor state, or embed it alongside an exported image:

import type { InstantPhotoSettings } from '@instantphoto/react'

<InstantPhotoImageEditor
  onSettingsChange={(settings: InstantPhotoSettings) => {
    localStorage.setItem('editor-settings', JSON.stringify(settings))
  }}
/>

Browser support

| Feature | Chrome | Firefox | Safari | Edge | |---------|--------|---------|--------|------| | WebGL 1.0 | 8+ | 4+ | 5.1+ | 12+ | | createImageBitmap | 50+ | 42+ | 10.1+¹ | 79+ | | ResizeObserver | 64+ | 69+ | 13.1+ | 79+ | | CSS aspect-ratio | 88+ | 89+ | 15+ | 88+ | | Pointer Events | 55+ | 59+ | 13+ | 12+ |

¹ imageOrientation: 'flipY' is not supported in Safari < 17. The library automatically falls back to a canvas-based Y-flip, so rendering is correct on all Safari versions.

Minimum recommended versions: Chrome 88, Firefox 89, Safari 15, Edge 88.


Performance tips

  • Set liveUpdateDuringGesture={false} on low-end or mobile devices for smooth 60fps pan/zoom (uses a lightweight raw-crop preview during gesture, renders full effects on release).
  • Use seed > 0 for deterministic grain (avoids visual jitter during interactions).
  • Use batchProcess instead of mounting multiple editors when processing many images.
  • The canvas always renders at 300 DPI. On high-DPI displays the CSS scale factor handles visual sharpness — no need to double the canvas size.

Development

# Install dependencies
bun install

# Run unit tests (watch mode)
bun test

# Run unit tests once with coverage report
bun run test:coverage

# Run Storybook (component explorer)
bun run storybook

# Run E2E tests against a built Storybook
bun run test:e2e

# Full local CI parity (lint, typecheck, coverage, build, storybook, e2e)
bun run test:ci

# Build the package
bun run build

# Lint and typecheck
bun run lint
bun run typecheck

Releasing

This package uses Changesets for versioning and publishing.

# Document your change (creates a changeset file)
bun run changeset

# Apply pending changesets and bump the version
bun run version

# Build and publish to npm (done automatically by CI on merge to main)
bun run release

On every push to main, the release workflow either opens a version-bump PR (if there are pending changesets) or publishes to npm when that PR is merged. Add an NPM_TOKEN secret in the repository settings for publishing to work.