@its-thepoe/dither-react
v0.2.0
Published
React canvas dither effect matching the Dither Playground (pointer, shockwaves, presets).
Maintainers
Readme
@its-thepoe/dither-react
React component that reproduces the Dither Playground canvas: image processing, ordered / error-diffusion dither, pointer parallax, tap/click shockwaves, sampled or solid dots, and mobile dot scaling.
Install
npm install @its-thepoe/dither-reactPeer dependencies: react and react-dom ^18 or ^19.
Usage
"use client";
import { useState, useCallback, useRef } from "react";
import {
DitherCanvas,
DEFAULT_PLAYGROUND_PARAMS,
mergeDitherParams,
LOGO_PRESET_URLS,
} from "@its-thepoe/dither-react";
export function Hero() {
const [imageSrc, setImageSrc] = useState(LOGO_PRESET_URLS.linear);
const inputRef = useRef<HTMLInputElement>(null);
const onFile = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
if (!/^image\/(jpeg|png|webp)$/i.test(file.type)) return;
setImageSrc(URL.createObjectURL(file));
}, []);
return (
<div className="fixed inset-0">
<DitherCanvas
imageSrc={imageSrc}
params={mergeDitherParams({ scale: 0.5 })}
className="absolute inset-0 h-full w-full touch-none"
warmPresetLogosOnMount
/>
<input
ref={inputRef}
type="file"
accept="image/jpeg,image/jpg,image/png,image/webp"
className="hidden"
onChange={onFile}
/>
</div>
);
}Props (DitherCanvas)
| Prop | Type | Default | Description |
| ----------------------------- | ----------------------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| imageSrc | string | (required) | Any URL the browser can load: https:, same-origin path, blob:, or data:image/.... |
| params | Partial<DitherCanvasParams> | — | Merged with DEFAULT_PLAYGROUND_PARAMS (algorithm, scale, colours, dither, etc.). |
| className / style | — | — | Passed to the <canvas> after built-in cursor/background. Your style wins on duplicate keys. |
| transparentCanvas | boolean | false | Skips the default CSS background on the canvas (use on layered or gradient pages). |
| syncPageBackground | boolean | false | When true, sets document.documentElement and body background to the effective canvas colour. Ignored if transparentCanvas is true. |
| layoutInsetPx | number | 0 | Insets the dot field from the canvas edges (CSS px), reducing clipping when dots move with pointer/shockwaves. |
| interactionScale | number | 1 | Scales pointer radius, pointer force, and shockwave strength (clamped ~0.05–4). Lower on small embeds. |
| respectPrefersReducedMotion | boolean | false | Listens for prefers-reduced-motion: reduce and disables pointer + shockwave motion (static dither). |
| fallbackImageSrc | string | — | If imageSrc fails to load or process, the canvas rebuilds once from this URL. |
| warmPresetLogosOnMount | boolean | false | Prewarms bundled built-in logo requests (LOGO_PRESET_URLS). |
| onLoadError | (err: unknown) => void | — | Called on load/process failure (including if the fallback also fails). |
Embedding small fixed-size canvases
Pointer and shockwave motion are computed in canvas pixel space. Dots that move outside the bitmap are clipped, which shows up as harsh edges on tight fixed-size heroes or cards.
Mitigations (combine as needed):
**layoutInsetPx** — Pulls the laid-out dot grid inward so motion has room before hitting the clip rect (same idea as oversized wrapper + lower scale, but explicit).**interactionScale**— Use values like0.5–0.85to soften radius and forces on small surfaces.- Larger canvas — Give the element more layout size or overflow room if your design allows.
Default physics constants are exported if you need to document or mirror them: DEFAULT_MOUSE_RADIUS, DEFAULT_MOUSE_FORCE_PEAK, DEFAULT_SHOCKWAVE_SPEED, DEFAULT_SHOCKWAVE_WIDTH, DEFAULT_SHOCKWAVE_STRENGTH.
Built-in logos
LOGO_PRESET_URLS maps linear | cursor | dispensary | mockhaus to bundler-resolved URLs (works in Vite, Next, etc.). Use them for imageSrc or to sync a logo picker.
Next.js App Router
The canvas requires the DOM. If you import it from a Server Component, load it on the client:
import dynamic from "next/dynamic";
const DitherCanvas = dynamic(
() =>
import("@its-thepoe/dither-react").then((m) => m.DitherCanvas),
{ ssr: false }
);Add transpilePackages: ["@its-thepoe/dither-react"] in next.config.ts when consuming the package from node_modules.
Bundled logo URLs (LOGO_PRESET_URLS): the package uses new URL("./assets/…", import.meta.url) and ships PNGs under dist/assets/. If your bundler still resolves those URLs incorrectly, alias the package to its source entry (this monorepo’s playground does this for both Turbopack and webpack):
import path from "path";
const ditherSrcAbs = path.join(__dirname, "node_modules/@its-thepoe/dither-react/src/index.ts");
// Workspace monorepo: path.join(__dirname, "packages/dither-react/src/index.ts")
// next.config.ts — Turbopack must use a path **relative to turbopack.root** (not an absolute
// filesystem path), or resolution breaks with `./Users/...` style errors.
turbopack: {
root: __dirname,
resolveAlias: {
"@its-thepoe/dither-react": "./packages/dither-react/src/index.ts",
},
},
webpack: (config) => {
config.resolve.alias["@its-thepoe/dither-react"] = ditherSrcAbs;
return config;
},CORS
loadImage uses crossOrigin = "anonymous". Remote imageSrc URLs must respond with CORS headers that allow your origin, or use same-origin / blob: / data: URLs.
Custom uploads
Validate types before createObjectURL (e.g. image/jpeg, image/png, image/webp). Revoke old blob URLs when replacing the file to avoid leaks.
Versioning (semver)
- Patch: bug fixes, no visual change for the same
params+ image. - Minor: new optional props or exports; default behaviour unchanged.
- Major: changed default params, dither output, or public type shapes.
Publishing from this monorepo
# from repository root
npm run build -w @its-thepoe/dither-react
cd packages/dither-react && npm publish --access publicBefore publish
**npm whoami** — must show your username. 401 means run**npm login** (token expired or never logged in).- Scope (fixes
404 Not FoundonPUT …/@its-thepoe/dither-react) — npm only lets you publish@its-thepoe/…if you are logged in as the npm user namedits-thepoeor your account is in the**its-thepoeorg** with publish access. If your GitHub isits-thepoebut your npm username is different, create the org on npmjs.com and add yourself, or change thenamein thispackage.jsonto@<your-npm-username>/dither-reactand update imports everywhere. - Registry — From the repo root, run
npm config get registry(expecthttps://registry.npmjs.org/). If you are insidepackages/dither-react, npm may error with**ENOWORKSPACES**; usenpm config get registry --workspaces=falseorcdto the monorepo root first. - Optional: run
**npm pkg fix**in this folder if npm warns aboutpackage.jsonnormalization (repository URL is alreadygit+https://…).
List packages you maintain: npm access list packages (not ls-packages).
Adjust name / --access if you use a private registry or non-scoped package.
Exports
Besides DitherCanvas, the package re-exports core helpers (processImage, createDotSystem, updateDots, renderDots, UpdateDotsOptions, default physics constants, etc.) for advanced integrations.
