Skip to content

Commit 51f6782

Browse files
committed
feat: initial ditherwave 0.1.0
- <Dither> React component wrapping img/video/canvas with WebGL2 dithering - <DitheredWaves> animated fBm-noise background, palette-quantised - Four modes: bayer, floyd (Riemersma approx), dots (15 deg halftone), ascii - Vanilla /vanilla entry — zero React imports for plain HTML/CDN consumers - ESM + CJS + UMD outputs, ~8 kb gz index, ~7 kb gz vanilla - Next.js 14 demo at apps/demo
0 parents  commit 51f6782

44 files changed

Lines changed: 4345 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
node_modules
2+
dist
3+
.next
4+
.turbo
5+
out
6+
.DS_Store
7+
*.log
8+
.env*
9+
!.env.example
10+
.vercel
11+
coverage
12+
.pnpm-store
13+
*.tsbuildinfo

CHANGELOG.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Changelog
2+
3+
## 0.1.0 — initial release
4+
5+
### Components
6+
7+
- **`<Dither>`** — wraps an `<img>`, `<video>`, or `<canvas>` and re-renders it through a WebGL2 dither pass every frame. Live video sampling via `requestVideoFrameCallback` when available. Optional `fallback` prop for browsers without WebGL2.
8+
- **`<DitheredWaves>`** — full-bleed animated fBm-noise background, dithered and palette-quantised. Drop-in hero. Inspired by reactbits' Dither background, reimplemented in raw WebGL2 to skip the three.js dependency.
9+
10+
### Modes
11+
12+
- `bayer` — ordered dithering, configurable 2×2 / 4×4 / 8×8 matrices.
13+
- `floyd` — Riemersma-style Hilbert-walk approximation of Floyd-Steinberg (single-pass GPU-friendly; not true FS).
14+
- `dots` — newspaper halftone, rotated 15° for authentic print feel.
15+
- `ascii` — luminance-indexed glyph atlas, customisable charset.
16+
17+
### Distribution
18+
19+
- ESM, CJS, and UMD outputs, all with sourcemaps.
20+
- Separate `ditherwave/vanilla` entry — zero React imports for plain HTML / CDN consumers.
21+
- React is an **optional** peer dependency (`peerDependenciesMeta.react.optional`).
22+
- Bundle size: ~8 kb gzipped (index), ~7 kb gzipped (vanilla).
23+
24+
### Other
25+
26+
- Imperative API: `createDither`, `createDitheredWaves`, `dither` alias.
27+
- React hook: `useDither`.
28+
- Pauses offscreen via `IntersectionObserver` (opt-out via `pauseOffscreen={false}`).
29+
- Demo site at `apps/demo`.

CONTRIBUTING.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# contributing
2+
3+
Open an issue or a PR. Keep the library core under 10kb gzipped — features that push it past that will be declined or moved to the demo package.
4+
5+
```sh
6+
pnpm install
7+
pnpm --filter ditherwave build # build library
8+
pnpm --filter dither-demo dev # run demo at :3100
9+
pnpm --filter ditherwave size # check gzipped size
10+
```
11+
12+
Shaders live in `packages/dither/src/shaders/`. The engine (`src/engine.ts`) owns texture lifecycle, render loop, and `IntersectionObserver`. The React surface is thin on purpose.

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 sahilsaini5
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
<div align="center">
2+
3+
# ditherwave
4+
5+
**A tiny WebGL2 dithering primitive for React.**
6+
Wrap any image, video, or canvas in `<Dither>` — or drop in `<DitheredWaves>` for an animated grainy background. Bayer, halftone, ASCII, or error-diffusion. Live, animated, **under 8kb gzipped**.
7+
8+
[![npm](https://img.shields.io/npm/v/ditherwave?style=flat-square)](https://www.npmjs.com/package/ditherwave)
9+
[![bundle](https://img.shields.io/bundlephobia/minzip/ditherwave?style=flat-square)](https://bundlephobia.com/package/ditherwave)
10+
[![license](https://img.shields.io/npm/l/ditherwave?style=flat-square)](./LICENSE)
11+
12+
</div>
13+
14+
---
15+
16+
## install
17+
18+
```sh
19+
npm install ditherwave
20+
# or: pnpm add ditherwave · yarn add ditherwave · bun add ditherwave
21+
```
22+
23+
React is an **optional** peer dep — only needed if you import the React components. Zero runtime dependencies otherwise.
24+
25+
### use without React (plain HTML / CSS / JS)
26+
27+
```html
28+
<canvas id="bg" style="width:100%;height:100vh"></canvas>
29+
30+
<script type="module">
31+
import { createDitheredWaves } from 'https://esm.sh/ditherwave/vanilla';
32+
createDitheredWaves(document.getElementById('bg'), {
33+
waveColor: '#39ff14',
34+
baseColor: '#050605',
35+
});
36+
</script>
37+
```
38+
39+
Or via npm + a `<script>` tag (no bundler):
40+
41+
```html
42+
<canvas id="bg"></canvas>
43+
<script src="./node_modules/ditherwave/dist/vanilla.umd.global.js"></script>
44+
<script>
45+
Dither.createDitheredWaves(document.getElementById('bg'), { /* ...opts */ });
46+
</script>
47+
```
48+
49+
The `ditherwave/vanilla` entry contains only the framework-free helpers (`createDither`, `createDitheredWaves`, `dither`) and pulls in zero React code.
50+
51+
## use
52+
53+
Two primitives. Pick one.
54+
55+
### `<Dither>` — dither existing content
56+
57+
```tsx
58+
import { Dither } from 'ditherwave';
59+
60+
<Dither mode="bayer" palette={['#0f380f', '#9bbc0f']} animate>
61+
<img src="https://picsum.photos/800" alt="" />
62+
</Dither>
63+
```
64+
65+
Drop an `<img>`, `<video>`, or `<canvas>` inside and it'll be dithered into the output canvas every frame. Sources update live — video textures via `requestVideoFrameCallback`, canvases every frame.
66+
67+
### `<DitheredWaves>` — animated noise background
68+
69+
```tsx
70+
import { DitheredWaves } from 'ditherwave';
71+
72+
<DitheredWaves
73+
waveColor="#9bbc0f"
74+
baseColor="#0f380f"
75+
pixelSize={3}
76+
colorNum={4}
77+
/>
78+
```
79+
80+
A full-bleed animated fBm-noise pattern, bayer-dithered, quantised to `colorNum` levels per channel. Drop-in hero background.
81+
82+
## api
83+
84+
### `<Dither>` props
85+
86+
| Prop | Type | Default | Notes |
87+
| ---------------- | ------------------------------------------------- | -------------- | ----- |
88+
| `mode` | `'bayer' \| 'floyd' \| 'dots' \| 'ascii'` | `'bayer'` | |
89+
| `resolution` | `number` | `256` | output pixel grid on the short edge |
90+
| `palette` | `string[]` | `['#0d0c0a','#ece8df']` | 2–8 hex colors |
91+
| `intensity` | `number` | `1` | 0–1, strength of the pattern |
92+
| `animate` | `boolean` | `false` | slow drift on the threshold pattern |
93+
| `matrixSize` | `2 \| 4 \| 8` | `8` | bayer only |
94+
| `charset` | `string` | `' .:-=+*#%@'` | ascii only, density ramp low→high |
95+
| `pauseOffscreen` | `boolean` | `true` | uses IntersectionObserver |
96+
| `fallback` | `ReactNode` | `children` | rendered when WebGL2 isn't available |
97+
98+
### `<DitheredWaves>` props
99+
100+
| Prop | Type | Default |
101+
| ------------------------ | --------- | ----------- |
102+
| `mode` | `'bayer' \| 'floyd' \| 'dots' \| 'ascii'` | `'bayer'` |
103+
| `waveColor` | hex | `'#7e7e7e'` |
104+
| `baseColor` | hex | `'#000000'` |
105+
| `waveSpeed` | `number` | `0.05` |
106+
| `waveFrequency` | `number` | `3` |
107+
| `waveAmplitude` | `number` | `0.3` |
108+
| `pixelSize` | `number` | `2` |
109+
| `colorNum` | `number` | `4` |
110+
| `matrixSize` | `2 \| 4 \| 8` | `8` |
111+
| `enableMouseInteraction` | `boolean` | `true` |
112+
| `mouseRadius` | `number` | `1` |
113+
| `disableAnimation` | `boolean` | `false` |
114+
115+
Also exported:
116+
117+
- `useDither(canvasRef, sourceRef, options)` — imperative hook.
118+
- `createDither(target, source, options)` — framework-agnostic; returns `{ destroy, setOptions, render }`.
119+
- `createDitheredWaves(target, options)` — same, for the noise background.
120+
- `dither(source, target, options)` — alias for non-React users.
121+
122+
## modes
123+
124+
**bayer** — ordered dithering. Cheap, GPU-friendly, parallelizes perfectly. The Obra Dinn / Gameboy look.
125+
126+
**floyd** — error diffusion. True Floyd-Steinberg is sequential and can't run in a single fragment pass. This mode ships a **Riemersma-style Hilbert-walk approximation** that reads neighbours and mixes a low-discrepancy threshold — it's *not* true FS, and we won't pretend it is. Looks close enough that you'll only notice in A/B.
127+
128+
**dots** — newspaper halftone. Rotated 15° for authentic print feel. Dot radius scales with local darkness. The pretty one.
129+
130+
**ascii** — luminance-indexed glyph atlas built at init. Uses the system monospace stack for ~0 bundle cost; pass your own `charset` for a denser ramp.
131+
132+
## performance
133+
134+
- 60fps on a 2020 MacBook Air at 1080p / resolution 256.
135+
- Zero per-frame allocations after init.
136+
- `devicePixelRatio` capped at 2 — dithering at 3× is wasteful.
137+
- Pauses when scrolled offscreen (IntersectionObserver, on by default).
138+
- Listens for `webglcontextlost`; reinit on `webglcontextrestored` is your call.
139+
140+
## size
141+
142+
Library core ships ESM, CJS, and UMD with sourcemaps. The full public surface tree-shakes down to under 8kb gzipped. Use `pnpm --filter ditherwave size` to check locally.
143+
144+
## credits
145+
146+
`<DitheredWaves>` is heavily inspired by [reactbits' Dither background](https://reactbits.dev/backgrounds/dither) — same domain-warped fBm noise + Bayer post-process, reimplemented in raw WebGL2 to keep the bundle small and avoid the three.js / postprocessing dependency. If you only need the background and you're already using `@react-three/fiber`, use theirs.
147+
148+
## license
149+
150+
MIT.

0 commit comments

Comments
 (0)