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

safelight

v0.1.0

Published

Ordered-dither surfaces, grain effects, and duotone image pipelines for the web. The darkroom, as a library.

Readme

safelight

Ordered-dither surfaces, grain effects, and duotone image pipelines for the web. The darkroom, as a library.

Everything here obeys one law: a pixel flips when an exposure field e(t) crosses its fixed threshold T(x, y) in an 8×8 Bayer matrix. Images develop darkest-first from blank paper, hovers bloom outward as square grain, pages wipe cell-by-cell through view transitions, theme flips develop the new theme over the old — all from the same matrix, the same law, so every effect on a page reads as one material. Nothing wipes, slides, or fades.

Extracted from (and battle-tested on) shannadige.com, where every surface is a live demo.

Install

npm install safelight

Zero runtime dependencies. sharp is an optional peer dependency, needed only by the build-time CLI.

Setup

Safelight paints in exactly two colors — ink and paper — resolved from CSS custom properties on your root element and re-resolved whenever its class or data-theme attribute changes:

:root { --ink: #d63a00; --background: #f0eeeb; }
.dark { --ink: #f0521a; --background: #16110d; }
import { configure, astroPreset } from 'safelight';

configure({
  tokens: { ink: '--ink', paper: '--background' }, // these are the defaults
  ...astroPreset, // on Astro ClientRouter: teardown/reattach across page swaps
});

On a plain multi-page site, configure() is optional. With any client-side router that swaps the DOM, set teardownEvents/reattachEvents to its lifecycle events (astroPreset covers Astro's ClientRouter).

Runtime behaviors

All canvas behaviors share one runtime contract: internal resolution upscaled with image-rendering: pixelated (grain stays square), rAF loops that pause offscreen via IntersectionObserver and settle to zero idle cost, ink/paper re-read on theme change, and a single static frame under prefers-reduced-motion.

Develop — images that develop like prints

import { createSurface, mountDevelop, holdOnPaper } from 'safelight';

const release = holdOnPaper(canvas); // rest on blank paper until the pass runs
const surface = await createSurface('/photo.jpg'); // precomputed buffers, one per image
release();
mountDevelop(canvas, surface, { settle: 'crisp' }); // or 'dither' to rest on grain

Ink condenses darkest-first from blank paper, then pixels resolve crisp behind a one-band ink frontier. A drop-in replacement for blur-up placeholders.

Bloom — hover reveal

import { mountBloom } from 'safelight';

const bloom = mountBloom(canvas, stage, surface, {
  mask: true, // reveal the DOM beneath instead of the surface's own pixels
  onSettled: (revealed) => revealed && video.play(),
});

A spring-driven boundary expands from the cursor in Chebyshev distance, quantized to the cell grid — square and grid-aligned, never circular.

Ambience & Scanbed — idle fields

import { mountAmbience, mountScanbed, timeOfDayDensity } from 'safelight';

mountAmbience(canvas, { density: timeOfDayDensity() }); // two-sine interference cloud
mountAmbience(canvas, { pattern: 'specimen' }); // raster sweep over procedural noise
mountScanbed(canvas, { onFrame: ({ flare }) => syncDomLayers(flare) }); // photocopier pass

Screen & Plate — panel entrances

import { mountScreen, mountPlate, hidePlate, warmPlates } from 'safelight';

requestIdleCallback(() => warmPlates()); // pre-rasterize mask tiles at idle

hidePlate(panel); // before layout: panel rests masked-out
mountScreen(cover, { onComplete }); // canvas cover dissolves off the content
mountPlate(panel); // panel's own surface carves in with the same field

Theme wash — light/dark developed, not faded

import { mountThemeWash } from 'safelight';

mountThemeWash(() => document.documentElement.classList.toggle('dark'));

The theme flips on frame zero; a cover in the old theme's paper dissolves off it, with a frontier in the old theme's ink (override with frontier: [r, g, b]).

Dissolve — page transitions as a grain wipe

import { installDissolve } from 'safelight';

installDissolve({ pin: ['site-header'] }); // pin chrome identical on both pages

Generated CSS over the View Transitions API — no per-navigation JS. Works with Astro's <ClientRouter /> or any document.startViewTransition flow.

Server-side: safelight/planes

import { ditherPlanes } from 'safelight/planes';

const rgb = ditherPlanes(tonePlane, maskPlane, { width, height, ink, paper, graphite });
// → Uint8Array of raw RGB, straight into sharp/canvas

Composites two grayscale planes (coverage + ink selection) through the same Bayer matrix — for OG images that carry identical grain to the page they open.

Build-time CLI

npx safelight photo.jpg out.png --mode atkinson --width 900 --ink '#d63a00'
npx safelight --batch   # reads safelight.config.json: themes × jobs → PNGs + manifest.json

Pre-renders 1-bit duotone placeholders (per theme) with atkinson, bayer, or scanline screening. The bayer mode uses the runtime's own 8×8 matrix, so pre-rendered placeholders and live passes land on identical grain. Config shape is documented in cli/duotone.mjs.

License

MIT © Shan Nadige