fast-pixelizer 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 handsupmin
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.
package/README.md ADDED
@@ -0,0 +1,197 @@
1
+ # fast-pixelizer
2
+
3
+ Fast, zero-dependency image pixelation library. Works in **browser** and **Node.js**.
4
+
5
+ [![npm](https://img.shields.io/npm/v/fast-pixelizer)](https://www.npmjs.com/package/fast-pixelizer)
6
+ [![license](https://img.shields.io/badge/license-MIT-blue)](./LICENSE)
7
+ [![bundle size](https://img.shields.io/bundlephobia/minzip/fast-pixelizer)](https://bundlephobia.com/package/fast-pixelizer)
8
+
9
+ ---
10
+
11
+ ## Overview
12
+
13
+ | | Original | `clean` | `detail` |
14
+ | :-------: | :------------------------------: | :------------------------------------------: | :--------------------------------------------: |
15
+ | **32×32** | ![original](./docs/original.png) | ![clean-32](./examples/example-32-clean.png) | ![detail-32](./examples/example-32-detail.png) |
16
+ | **64×64** | ![original](./docs/original.png) | ![clean-64](./examples/example-64-clean.png) | ![detail-64](./examples/example-64-detail.png) |
17
+
18
+ **`clean`** — picks the most frequent color in each cell. Sharp, graphic pixel art look.
19
+
20
+ **`detail`** — averages all colors in each cell. Smoother gradients, more texture.
21
+
22
+ ---
23
+
24
+ ## Install
25
+
26
+ ```bash
27
+ npm install fast-pixelizer
28
+ ```
29
+
30
+ ---
31
+
32
+ ## Usage
33
+
34
+ ```ts
35
+ import { pixelate } from 'fast-pixelizer'
36
+
37
+ const result = pixelate(imageData, { resolution: 32 })
38
+ // → { data: Uint8ClampedArray, width: number, height: number }
39
+ ```
40
+
41
+ The input accepts a browser `ImageData`, a `node-canvas` image data object, or any plain `{ data: Uint8ClampedArray, width: number, height: number }`.
42
+
43
+ ### Browser
44
+
45
+ ```ts
46
+ import { pixelate } from 'fast-pixelizer'
47
+
48
+ const canvas = document.querySelector('canvas')
49
+ const ctx = canvas.getContext('2d')
50
+ ctx.drawImage(myImage, 0, 0)
51
+
52
+ const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
53
+ const result = pixelate(imageData, { resolution: 32 })
54
+
55
+ // Draw back
56
+ const out = new ImageData(result.data, result.width, result.height)
57
+ ctx.putImageData(out, 0, 0)
58
+ ```
59
+
60
+ ### Node.js (with [sharp](https://sharp.pixelplumbing.com))
61
+
62
+ ```ts
63
+ import sharp from 'sharp'
64
+ import { pixelate } from 'fast-pixelizer'
65
+
66
+ const { data, info } = await sharp('./photo.png')
67
+ .ensureAlpha()
68
+ .raw()
69
+ .toBuffer({ resolveWithObject: true })
70
+
71
+ const result = pixelate(
72
+ { data: new Uint8ClampedArray(data.buffer), width: info.width, height: info.height },
73
+ { resolution: 32 },
74
+ )
75
+
76
+ await sharp(Buffer.from(result.data), {
77
+ raw: { width: result.width, height: result.height, channels: 4 },
78
+ })
79
+ .png()
80
+ .toFile('./output.png')
81
+ ```
82
+
83
+ ---
84
+
85
+ ## API
86
+
87
+ ### `pixelate(input, options): PixelateResult`
88
+
89
+ #### `input: ImageLike`
90
+
91
+ ```ts
92
+ interface ImageLike {
93
+ data: Uint8ClampedArray
94
+ width: number
95
+ height: number
96
+ }
97
+ ```
98
+
99
+ Compatible with the browser's built-in `ImageData`, `node-canvas`, and raw pixel buffers.
100
+
101
+ #### `options: PixelateOptions`
102
+
103
+ | Option | Type | Default | Description |
104
+ | ------------ | ------------------------- | ------------ | ------------------------------------------------------------------------------------------- |
105
+ | `resolution` | `number` | **required** | Grid size. `32` means a 32×32 cell grid. Clamped to image dimensions automatically. |
106
+ | `mode` | `'clean' \| 'detail'` | `'clean'` | `'clean'` = most-frequent color per cell. `'detail'` = average color per cell. |
107
+ | `output` | `'original' \| 'resized'` | `'original'` | `'original'` = same dimensions as input. `'resized'` = output is `resolution × resolution`. |
108
+
109
+ #### `PixelateResult`
110
+
111
+ ```ts
112
+ interface PixelateResult {
113
+ data: Uint8ClampedArray
114
+ width: number
115
+ height: number
116
+ }
117
+ ```
118
+
119
+ ---
120
+
121
+ ## Examples
122
+
123
+ ```ts
124
+ // Defaults: clean mode, original size
125
+ pixelate(img, { resolution: 32 })
126
+
127
+ // Average color, output as resolution × resolution
128
+ pixelate(img, { resolution: 64, mode: 'detail', output: 'resized' })
129
+
130
+ // Very blocky, 8×8 grid
131
+ pixelate(img, { resolution: 8 })
132
+ ```
133
+
134
+ ### Try it locally
135
+
136
+ Clone the repo and run the library against the sample image to see the output yourself:
137
+
138
+ ```bash
139
+ git clone https://github.com/handsupmin/fast-pixelizer.git
140
+ cd fast-pixelizer
141
+ npm install
142
+ npm run examples
143
+ ```
144
+
145
+ Output images will be written to `examples/`. Replace `docs/original.png` with any image to try your own.
146
+
147
+ ---
148
+
149
+ ## Performance
150
+
151
+ | Resolution | Image size | clean | detail |
152
+ | ---------- | ---------- | ----- | ------ |
153
+ | 32 | 512×512 | ~1ms | ~0.5ms |
154
+ | 128 | 512×512 | ~3ms | ~1ms |
155
+ | 256 | 1024×1024 | ~12ms | ~5ms |
156
+
157
+ - **`clean` mode** uses a pre-allocated `Uint16Array(32768)` bucket table — no `Map`, no per-call heap allocations.
158
+ - **`detail` mode** is a single accumulation pass with no allocations.
159
+ - Cell boundaries use `Math.round` to eliminate pixel gaps and overlaps between adjacent cells.
160
+ - Both modes iterate in row-major order for CPU cache locality.
161
+ - Zero runtime dependencies.
162
+
163
+ ---
164
+
165
+ ## Web Worker (browser)
166
+
167
+ For large images, run `pixelate` inside a Worker to keep the main thread unblocked:
168
+
169
+ ```ts
170
+ // pixelate.worker.ts
171
+ import { pixelate } from 'fast-pixelizer'
172
+
173
+ self.onmessage = (e) => {
174
+ const { input, options } = e.data
175
+ const result = pixelate(input, options)
176
+ self.postMessage(result, [result.data.buffer]) // transfer buffer, no copy
177
+ }
178
+ ```
179
+
180
+ ```ts
181
+ // main thread
182
+ const worker = new Worker(new URL('./pixelate.worker.ts', import.meta.url), { type: 'module' })
183
+ worker.postMessage({ input, options }, [input.data.buffer])
184
+ worker.onmessage = (e) => console.log(e.data) // PixelateResult
185
+ ```
186
+
187
+ ---
188
+
189
+ ## Contributing
190
+
191
+ Contributions are welcome! See [CONTRIBUTING.md](./CONTRIBUTING.md).
192
+
193
+ ---
194
+
195
+ ## License
196
+
197
+ [MIT](./LICENSE)
package/dist/index.cjs ADDED
@@ -0,0 +1,120 @@
1
+ 'use strict';
2
+
3
+ // src/algorithms.ts
4
+ var _freq = new Uint16Array(32768);
5
+ var _touched = [];
6
+ function getFrequentColor(data, width, x0, y0, x1, y1) {
7
+ for (let i = 0; i < _touched.length; i++) _freq[_touched[i]] = 0;
8
+ _touched.length = 0;
9
+ let maxCount = 0;
10
+ let bestKey = 0;
11
+ let transparentCount = 0;
12
+ let totalPixels = 0;
13
+ for (let py = y0; py < y1; py++) {
14
+ const row = py * width * 4;
15
+ for (let px = x0; px < x1; px++) {
16
+ const i = row + px * 4;
17
+ const a = data[i + 3];
18
+ totalPixels++;
19
+ if (a < 128) {
20
+ transparentCount++;
21
+ continue;
22
+ }
23
+ const key = data[i] >> 3 << 10 | data[i + 1] >> 3 << 5 | data[i + 2] >> 3;
24
+ if (_freq[key] === 0) _touched.push(key);
25
+ const c = ++_freq[key];
26
+ if (c > maxCount) {
27
+ maxCount = c;
28
+ bestKey = key;
29
+ }
30
+ }
31
+ }
32
+ if (totalPixels === 0 || transparentCount * 2 > totalPixels) return [0, 0, 0, 0];
33
+ return [(bestKey >> 10 & 31) << 3, (bestKey >> 5 & 31) << 3, (bestKey & 31) << 3, 255];
34
+ }
35
+ function getAverageColor(data, width, x0, y0, x1, y1) {
36
+ let rSum = 0, gSum = 0, bSum = 0, aSum = 0;
37
+ let transparentCount = 0;
38
+ let totalPixels = 0;
39
+ for (let py = y0; py < y1; py++) {
40
+ const row = py * width * 4;
41
+ for (let px = x0; px < x1; px++) {
42
+ const i = row + px * 4;
43
+ const a = data[i + 3];
44
+ totalPixels++;
45
+ aSum += a;
46
+ if (a < 128) {
47
+ transparentCount++;
48
+ continue;
49
+ }
50
+ rSum += data[i];
51
+ gSum += data[i + 1];
52
+ bSum += data[i + 2];
53
+ }
54
+ }
55
+ if (totalPixels === 0 || transparentCount * 2 > totalPixels) return [0, 0, 0, 0];
56
+ const visible = totalPixels - transparentCount;
57
+ return [
58
+ visible > 0 ? rSum / visible + 0.5 | 0 : 0,
59
+ visible > 0 ? gSum / visible + 0.5 | 0 : 0,
60
+ visible > 0 ? bSum / visible + 0.5 | 0 : 0,
61
+ aSum / totalPixels + 0.5 | 0
62
+ ];
63
+ }
64
+
65
+ // src/index.ts
66
+ function pixelate(input, options) {
67
+ const { data, width, height } = input;
68
+ const { mode = "clean", output = "original" } = options;
69
+ const resolution = Math.max(1, Math.min(Math.floor(options.resolution), width, height));
70
+ const getColor = mode === "clean" ? getFrequentColor : getAverageColor;
71
+ const cellW = width / resolution;
72
+ const cellH = height / resolution;
73
+ const cellColors = new Uint8ClampedArray(resolution * resolution * 4);
74
+ for (let row = 0; row < resolution; row++) {
75
+ for (let col = 0; col < resolution; col++) {
76
+ const x0 = Math.round(col * cellW);
77
+ const y0 = Math.round(row * cellH);
78
+ const x1 = Math.round((col + 1) * cellW);
79
+ const y1 = Math.round((row + 1) * cellH);
80
+ const [r, g, b, a] = getColor(data, width, x0, y0, x1, y1);
81
+ const idx = (row * resolution + col) * 4;
82
+ cellColors[idx] = r;
83
+ cellColors[idx + 1] = g;
84
+ cellColors[idx + 2] = b;
85
+ cellColors[idx + 3] = a;
86
+ }
87
+ }
88
+ if (output === "resized") {
89
+ return { data: cellColors, width: resolution, height: resolution };
90
+ }
91
+ const out = new Uint8ClampedArray(width * height * 4);
92
+ for (let row = 0; row < resolution; row++) {
93
+ for (let col = 0; col < resolution; col++) {
94
+ const idx = (row * resolution + col) * 4;
95
+ const r = cellColors[idx];
96
+ const g = cellColors[idx + 1];
97
+ const b = cellColors[idx + 2];
98
+ const a = cellColors[idx + 3];
99
+ const x0 = Math.round(col * cellW);
100
+ const y0 = Math.round(row * cellH);
101
+ const x1 = Math.round((col + 1) * cellW);
102
+ const y1 = Math.round((row + 1) * cellH);
103
+ for (let py = y0; py < y1; py++) {
104
+ const rowBase = py * width * 4;
105
+ for (let px = x0; px < x1; px++) {
106
+ const i = rowBase + px * 4;
107
+ out[i] = r;
108
+ out[i + 1] = g;
109
+ out[i + 2] = b;
110
+ out[i + 3] = a;
111
+ }
112
+ }
113
+ }
114
+ }
115
+ return { data: out, width, height };
116
+ }
117
+
118
+ exports.pixelate = pixelate;
119
+ //# sourceMappingURL=index.cjs.map
120
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/algorithms.ts","../src/index.ts"],"names":[],"mappings":";;;AAKA,IAAM,KAAA,GAAQ,IAAI,WAAA,CAAY,KAAK,CAAA;AACnC,IAAM,WAAqB,EAAC;AAMrB,SAAS,iBACd,IAAA,EACA,KAAA,EACA,EAAA,EACA,EAAA,EACA,IACA,EAAA,EACkC;AAElC,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,QAAA,CAAS,MAAA,EAAQ,KAAK,KAAA,CAAM,QAAA,CAAS,CAAC,CAAC,CAAA,GAAI,CAAA;AAC/D,EAAA,QAAA,CAAS,MAAA,GAAS,CAAA;AAElB,EAAA,IAAI,QAAA,GAAW,CAAA;AACf,EAAA,IAAI,OAAA,GAAU,CAAA;AACd,EAAA,IAAI,gBAAA,GAAmB,CAAA;AACvB,EAAA,IAAI,WAAA,GAAc,CAAA;AAElB,EAAA,KAAA,IAAS,EAAA,GAAK,EAAA,EAAI,EAAA,GAAK,EAAA,EAAI,EAAA,EAAA,EAAM;AAC/B,IAAA,MAAM,GAAA,GAAM,KAAK,KAAA,GAAQ,CAAA;AACzB,IAAA,KAAA,IAAS,EAAA,GAAK,EAAA,EAAI,EAAA,GAAK,EAAA,EAAI,EAAA,EAAA,EAAM;AAC/B,MAAA,MAAM,CAAA,GAAI,MAAM,EAAA,GAAK,CAAA;AACrB,MAAA,MAAM,CAAA,GAAI,IAAA,CAAK,CAAA,GAAI,CAAC,CAAA;AACpB,MAAA,WAAA,EAAA;AACA,MAAA,IAAI,IAAI,GAAA,EAAK;AACX,QAAA,gBAAA,EAAA;AACA,QAAA;AAAA,MACF;AAEA,MAAA,MAAM,GAAA,GAAQ,IAAA,CAAK,CAAC,CAAA,IAAK,KAAM,EAAA,GAAQ,IAAA,CAAK,CAAA,GAAI,CAAC,KAAK,CAAA,IAAM,CAAA,GAAM,IAAA,CAAK,CAAA,GAAI,CAAC,CAAA,IAAK,CAAA;AACjF,MAAA,IAAI,MAAM,GAAG,CAAA,KAAM,CAAA,EAAG,QAAA,CAAS,KAAK,GAAG,CAAA;AACvC,MAAA,MAAM,CAAA,GAAI,EAAE,KAAA,CAAM,GAAG,CAAA;AACrB,MAAA,IAAI,IAAI,QAAA,EAAU;AAChB,QAAA,QAAA,GAAW,CAAA;AACX,QAAA,OAAA,GAAU,GAAA;AAAA,MACZ;AAAA,IACF;AAAA,EACF;AAEA,EAAA,IAAI,WAAA,KAAgB,CAAA,IAAK,gBAAA,GAAmB,CAAA,GAAI,WAAA,SAAoB,CAAC,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAC,CAAA;AAG/E,EAAA,OAAO,CAAA,CAAG,OAAA,IAAW,EAAA,GAAM,EAAA,KAAO,CAAA,EAAA,CAAK,OAAA,IAAW,CAAA,GAAK,EAAA,KAAO,CAAA,EAAA,CAAI,OAAA,GAAU,EAAA,KAAO,CAAA,EAAG,GAAG,CAAA;AAC3F;AAKO,SAAS,gBACd,IAAA,EACA,KAAA,EACA,EAAA,EACA,EAAA,EACA,IACA,EAAA,EACkC;AAClC,EAAA,IAAI,OAAO,CAAA,EACT,IAAA,GAAO,CAAA,EACP,IAAA,GAAO,GACP,IAAA,GAAO,CAAA;AACT,EAAA,IAAI,gBAAA,GAAmB,CAAA;AACvB,EAAA,IAAI,WAAA,GAAc,CAAA;AAElB,EAAA,KAAA,IAAS,EAAA,GAAK,EAAA,EAAI,EAAA,GAAK,EAAA,EAAI,EAAA,EAAA,EAAM;AAC/B,IAAA,MAAM,GAAA,GAAM,KAAK,KAAA,GAAQ,CAAA;AACzB,IAAA,KAAA,IAAS,EAAA,GAAK,EAAA,EAAI,EAAA,GAAK,EAAA,EAAI,EAAA,EAAA,EAAM;AAC/B,MAAA,MAAM,CAAA,GAAI,MAAM,EAAA,GAAK,CAAA;AACrB,MAAA,MAAM,CAAA,GAAI,IAAA,CAAK,CAAA,GAAI,CAAC,CAAA;AACpB,MAAA,WAAA,EAAA;AACA,MAAA,IAAA,IAAQ,CAAA;AACR,MAAA,IAAI,IAAI,GAAA,EAAK;AACX,QAAA,gBAAA,EAAA;AACA,QAAA;AAAA,MACF;AACA,MAAA,IAAA,IAAQ,KAAK,CAAC,CAAA;AACd,MAAA,IAAA,IAAQ,IAAA,CAAK,IAAI,CAAC,CAAA;AAClB,MAAA,IAAA,IAAQ,IAAA,CAAK,IAAI,CAAC,CAAA;AAAA,IACpB;AAAA,EACF;AAEA,EAAA,IAAI,WAAA,KAAgB,CAAA,IAAK,gBAAA,GAAmB,CAAA,GAAI,WAAA,SAAoB,CAAC,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAC,CAAA;AAE/E,EAAA,MAAM,UAAU,WAAA,GAAc,gBAAA;AAE9B,EAAA,OAAO;AAAA,IACL,OAAA,GAAU,CAAA,GAAK,IAAA,GAAO,OAAA,GAAU,MAAO,CAAA,GAAI,CAAA;AAAA,IAC3C,OAAA,GAAU,CAAA,GAAK,IAAA,GAAO,OAAA,GAAU,MAAO,CAAA,GAAI,CAAA;AAAA,IAC3C,OAAA,GAAU,CAAA,GAAK,IAAA,GAAO,OAAA,GAAU,MAAO,CAAA,GAAI,CAAA;AAAA,IAC1C,IAAA,GAAO,cAAc,GAAA,GAAO;AAAA,GAC/B;AACF;;;AC9CO,SAAS,QAAA,CAAS,OAAkB,OAAA,EAA0C;AACnF,EAAA,MAAM,EAAE,IAAA,EAAM,KAAA,EAAO,MAAA,EAAO,GAAI,KAAA;AAChC,EAAA,MAAM,EAAE,IAAA,GAAO,OAAA,EAAS,MAAA,GAAS,YAAW,GAAI,OAAA;AAEhD,EAAA,MAAM,UAAA,GAAa,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,IAAA,CAAK,GAAA,CAAI,IAAA,CAAK,KAAA,CAAM,OAAA,CAAQ,UAAU,CAAA,EAAG,KAAA,EAAO,MAAM,CAAC,CAAA;AAEtF,EAAA,MAAM,QAAA,GAAW,IAAA,KAAS,OAAA,GAAU,gBAAA,GAAmB,eAAA;AACvD,EAAA,MAAM,QAAQ,KAAA,GAAQ,UAAA;AACtB,EAAA,MAAM,QAAQ,MAAA,GAAS,UAAA;AAGvB,EAAA,MAAM,UAAA,GAAa,IAAI,iBAAA,CAAkB,UAAA,GAAa,aAAa,CAAC,CAAA;AAEpE,EAAA,KAAA,IAAS,GAAA,GAAM,CAAA,EAAG,GAAA,GAAM,UAAA,EAAY,GAAA,EAAA,EAAO;AACzC,IAAA,KAAA,IAAS,GAAA,GAAM,CAAA,EAAG,GAAA,GAAM,UAAA,EAAY,GAAA,EAAA,EAAO;AACzC,MAAA,MAAM,EAAA,GAAK,IAAA,CAAK,KAAA,CAAM,GAAA,GAAM,KAAK,CAAA;AACjC,MAAA,MAAM,EAAA,GAAK,IAAA,CAAK,KAAA,CAAM,GAAA,GAAM,KAAK,CAAA;AACjC,MAAA,MAAM,EAAA,GAAK,IAAA,CAAK,KAAA,CAAA,CAAO,GAAA,GAAM,KAAK,KAAK,CAAA;AACvC,MAAA,MAAM,EAAA,GAAK,IAAA,CAAK,KAAA,CAAA,CAAO,GAAA,GAAM,KAAK,KAAK,CAAA;AACvC,MAAA,MAAM,CAAC,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAC,CAAA,GAAI,QAAA,CAAS,IAAA,EAAM,KAAA,EAAO,EAAA,EAAI,EAAA,EAAI,EAAA,EAAI,EAAE,CAAA;AACzD,MAAA,MAAM,GAAA,GAAA,CAAO,GAAA,GAAM,UAAA,GAAa,GAAA,IAAO,CAAA;AACvC,MAAA,UAAA,CAAW,GAAG,CAAA,GAAI,CAAA;AAClB,MAAA,UAAA,CAAW,GAAA,GAAM,CAAC,CAAA,GAAI,CAAA;AACtB,MAAA,UAAA,CAAW,GAAA,GAAM,CAAC,CAAA,GAAI,CAAA;AACtB,MAAA,UAAA,CAAW,GAAA,GAAM,CAAC,CAAA,GAAI,CAAA;AAAA,IACxB;AAAA,EACF;AAGA,EAAA,IAAI,WAAW,SAAA,EAAW;AACxB,IAAA,OAAO,EAAE,IAAA,EAAM,UAAA,EAAY,KAAA,EAAO,UAAA,EAAY,QAAQ,UAAA,EAAW;AAAA,EACnE;AAGA,EAAA,MAAM,GAAA,GAAM,IAAI,iBAAA,CAAkB,KAAA,GAAQ,SAAS,CAAC,CAAA;AAEpD,EAAA,KAAA,IAAS,GAAA,GAAM,CAAA,EAAG,GAAA,GAAM,UAAA,EAAY,GAAA,EAAA,EAAO;AACzC,IAAA,KAAA,IAAS,GAAA,GAAM,CAAA,EAAG,GAAA,GAAM,UAAA,EAAY,GAAA,EAAA,EAAO;AACzC,MAAA,MAAM,GAAA,GAAA,CAAO,GAAA,GAAM,UAAA,GAAa,GAAA,IAAO,CAAA;AACvC,MAAA,MAAM,CAAA,GAAI,WAAW,GAAG,CAAA;AACxB,MAAA,MAAM,CAAA,GAAI,UAAA,CAAW,GAAA,GAAM,CAAC,CAAA;AAC5B,MAAA,MAAM,CAAA,GAAI,UAAA,CAAW,GAAA,GAAM,CAAC,CAAA;AAC5B,MAAA,MAAM,CAAA,GAAI,UAAA,CAAW,GAAA,GAAM,CAAC,CAAA;AAE5B,MAAA,MAAM,EAAA,GAAK,IAAA,CAAK,KAAA,CAAM,GAAA,GAAM,KAAK,CAAA;AACjC,MAAA,MAAM,EAAA,GAAK,IAAA,CAAK,KAAA,CAAM,GAAA,GAAM,KAAK,CAAA;AACjC,MAAA,MAAM,EAAA,GAAK,IAAA,CAAK,KAAA,CAAA,CAAO,GAAA,GAAM,KAAK,KAAK,CAAA;AACvC,MAAA,MAAM,EAAA,GAAK,IAAA,CAAK,KAAA,CAAA,CAAO,GAAA,GAAM,KAAK,KAAK,CAAA;AAEvC,MAAA,KAAA,IAAS,EAAA,GAAK,EAAA,EAAI,EAAA,GAAK,EAAA,EAAI,EAAA,EAAA,EAAM;AAC/B,QAAA,MAAM,OAAA,GAAU,KAAK,KAAA,GAAQ,CAAA;AAC7B,QAAA,KAAA,IAAS,EAAA,GAAK,EAAA,EAAI,EAAA,GAAK,EAAA,EAAI,EAAA,EAAA,EAAM;AAC/B,UAAA,MAAM,CAAA,GAAI,UAAU,EAAA,GAAK,CAAA;AACzB,UAAA,GAAA,CAAI,CAAC,CAAA,GAAI,CAAA;AACT,UAAA,GAAA,CAAI,CAAA,GAAI,CAAC,CAAA,GAAI,CAAA;AACb,UAAA,GAAA,CAAI,CAAA,GAAI,CAAC,CAAA,GAAI,CAAA;AACb,UAAA,GAAA,CAAI,CAAA,GAAI,CAAC,CAAA,GAAI,CAAA;AAAA,QACf;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,EAAA,OAAO,EAAE,IAAA,EAAM,GAAA,EAAK,KAAA,EAAO,MAAA,EAAO;AACpC","file":"index.cjs","sourcesContent":["/**\n * Pre-allocated frequency table for frequent-color sampling.\n * 5 bits per channel (>> 3) → 32^3 = 32,768 buckets.\n * Safe because JS is single-threaded per context.\n */\nconst _freq = new Uint16Array(32768)\nconst _touched: number[] = []\n\n/**\n * Returns the most-frequent quantized color in the cell.\n * Uses a typed-array bucket table instead of Map for speed.\n */\nexport function getFrequentColor(\n data: Uint8ClampedArray,\n width: number,\n x0: number,\n y0: number,\n x1: number,\n y1: number,\n): [number, number, number, number] {\n // Reset only buckets touched in the previous call\n for (let i = 0; i < _touched.length; i++) _freq[_touched[i]] = 0\n _touched.length = 0\n\n let maxCount = 0\n let bestKey = 0\n let transparentCount = 0\n let totalPixels = 0\n\n for (let py = y0; py < y1; py++) {\n const row = py * width * 4\n for (let px = x0; px < x1; px++) {\n const i = row + px * 4\n const a = data[i + 3]\n totalPixels++\n if (a < 128) {\n transparentCount++\n continue\n }\n // Pack 5-bit quantized channels into a single 15-bit key\n const key = ((data[i] >> 3) << 10) | ((data[i + 1] >> 3) << 5) | (data[i + 2] >> 3)\n if (_freq[key] === 0) _touched.push(key)\n const c = ++_freq[key]\n if (c > maxCount) {\n maxCount = c\n bestKey = key\n }\n }\n }\n\n if (totalPixels === 0 || transparentCount * 2 > totalPixels) return [0, 0, 0, 0]\n\n // Decode key back to RGB (multiply by 8 to restore approximate original range)\n return [((bestKey >> 10) & 31) << 3, ((bestKey >> 5) & 31) << 3, (bestKey & 31) << 3, 255]\n}\n\n/**\n * Returns the average color of visible (non-transparent) pixels in the cell.\n */\nexport function getAverageColor(\n data: Uint8ClampedArray,\n width: number,\n x0: number,\n y0: number,\n x1: number,\n y1: number,\n): [number, number, number, number] {\n let rSum = 0,\n gSum = 0,\n bSum = 0,\n aSum = 0\n let transparentCount = 0\n let totalPixels = 0\n\n for (let py = y0; py < y1; py++) {\n const row = py * width * 4\n for (let px = x0; px < x1; px++) {\n const i = row + px * 4\n const a = data[i + 3]\n totalPixels++\n aSum += a\n if (a < 128) {\n transparentCount++\n continue\n }\n rSum += data[i]\n gSum += data[i + 1]\n bSum += data[i + 2]\n }\n }\n\n if (totalPixels === 0 || transparentCount * 2 > totalPixels) return [0, 0, 0, 0]\n\n const visible = totalPixels - transparentCount\n // (x + 0.5) | 0 ≡ Math.round(x) for non-negative values — avoids function call overhead\n return [\n visible > 0 ? (rSum / visible + 0.5) | 0 : 0,\n visible > 0 ? (gSum / visible + 0.5) | 0 : 0,\n visible > 0 ? (bSum / visible + 0.5) | 0 : 0,\n (aSum / totalPixels + 0.5) | 0,\n ]\n}\n","import { getAverageColor, getFrequentColor } from './algorithms'\n\n// ─── Types ───────────────────────────────────────────────────────────────────\n\n/**\n * Any object with `data`, `width`, `height` — compatible with the browser's\n * built-in `ImageData` as well as node-canvas and raw buffers.\n */\nexport interface ImageLike {\n data: Uint8ClampedArray\n width: number\n height: number\n}\n\nexport interface PixelateOptions {\n /**\n * Number of pixel cells along each axis (e.g. 32 → 32×32 grid).\n * Any positive integer. Values larger than the image dimension are clamped.\n */\n resolution: number\n\n /**\n * Color sampling algorithm per cell.\n * - `'clean'` — most-frequent color (sharp, graphic look) [default]\n * - `'detail'` — average color (smoother gradients, more texture)\n */\n mode?: 'clean' | 'detail'\n\n /**\n * Output dimensions.\n * - `'original'` — same size as input, cells filled with uniform color [default]\n * - `'resized'` — output is `resolution × resolution` pixels\n */\n output?: 'original' | 'resized'\n}\n\nexport interface PixelateResult {\n data: Uint8ClampedArray\n width: number\n height: number\n}\n\n// ─── Core ────────────────────────────────────────────────────────────────────\n\n/**\n * Pixelates an image synchronously.\n *\n * Works in both browser and Node.js (no DOM required).\n *\n * @example\n * ```ts\n * const result = pixelate(imageData, { resolution: 32 })\n * const result = pixelate(imageData, { resolution: 64, mode: 'detail', output: 'resized' })\n * ```\n */\nexport function pixelate(input: ImageLike, options: PixelateOptions): PixelateResult {\n const { data, width, height } = input\n const { mode = 'clean', output = 'original' } = options\n\n const resolution = Math.max(1, Math.min(Math.floor(options.resolution), width, height))\n\n const getColor = mode === 'clean' ? getFrequentColor : getAverageColor\n const cellW = width / resolution\n const cellH = height / resolution\n\n // Sample one color per cell\n const cellColors = new Uint8ClampedArray(resolution * resolution * 4)\n\n for (let row = 0; row < resolution; row++) {\n for (let col = 0; col < resolution; col++) {\n const x0 = Math.round(col * cellW)\n const y0 = Math.round(row * cellH)\n const x1 = Math.round((col + 1) * cellW)\n const y1 = Math.round((row + 1) * cellH)\n const [r, g, b, a] = getColor(data, width, x0, y0, x1, y1)\n const idx = (row * resolution + col) * 4\n cellColors[idx] = r\n cellColors[idx + 1] = g\n cellColors[idx + 2] = b\n cellColors[idx + 3] = a\n }\n }\n\n // Build output\n if (output === 'resized') {\n return { data: cellColors, width: resolution, height: resolution }\n }\n\n // output === 'original': paint each cell back at full size\n const out = new Uint8ClampedArray(width * height * 4)\n\n for (let row = 0; row < resolution; row++) {\n for (let col = 0; col < resolution; col++) {\n const idx = (row * resolution + col) * 4\n const r = cellColors[idx]\n const g = cellColors[idx + 1]\n const b = cellColors[idx + 2]\n const a = cellColors[idx + 3]\n\n const x0 = Math.round(col * cellW)\n const y0 = Math.round(row * cellH)\n const x1 = Math.round((col + 1) * cellW)\n const y1 = Math.round((row + 1) * cellH)\n\n for (let py = y0; py < y1; py++) {\n const rowBase = py * width * 4\n for (let px = x0; px < x1; px++) {\n const i = rowBase + px * 4\n out[i] = r\n out[i + 1] = g\n out[i + 2] = b\n out[i + 3] = a\n }\n }\n }\n }\n\n return { data: out, width, height }\n}\n"]}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Any object with `data`, `width`, `height` — compatible with the browser's
3
+ * built-in `ImageData` as well as node-canvas and raw buffers.
4
+ */
5
+ interface ImageLike {
6
+ data: Uint8ClampedArray;
7
+ width: number;
8
+ height: number;
9
+ }
10
+ interface PixelateOptions {
11
+ /**
12
+ * Number of pixel cells along each axis (e.g. 32 → 32×32 grid).
13
+ * Any positive integer. Values larger than the image dimension are clamped.
14
+ */
15
+ resolution: number;
16
+ /**
17
+ * Color sampling algorithm per cell.
18
+ * - `'clean'` — most-frequent color (sharp, graphic look) [default]
19
+ * - `'detail'` — average color (smoother gradients, more texture)
20
+ */
21
+ mode?: 'clean' | 'detail';
22
+ /**
23
+ * Output dimensions.
24
+ * - `'original'` — same size as input, cells filled with uniform color [default]
25
+ * - `'resized'` — output is `resolution × resolution` pixels
26
+ */
27
+ output?: 'original' | 'resized';
28
+ }
29
+ interface PixelateResult {
30
+ data: Uint8ClampedArray;
31
+ width: number;
32
+ height: number;
33
+ }
34
+ /**
35
+ * Pixelates an image synchronously.
36
+ *
37
+ * Works in both browser and Node.js (no DOM required).
38
+ *
39
+ * @example
40
+ * ```ts
41
+ * const result = pixelate(imageData, { resolution: 32 })
42
+ * const result = pixelate(imageData, { resolution: 64, mode: 'detail', output: 'resized' })
43
+ * ```
44
+ */
45
+ declare function pixelate(input: ImageLike, options: PixelateOptions): PixelateResult;
46
+
47
+ export { type ImageLike, type PixelateOptions, type PixelateResult, pixelate };
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Any object with `data`, `width`, `height` — compatible with the browser's
3
+ * built-in `ImageData` as well as node-canvas and raw buffers.
4
+ */
5
+ interface ImageLike {
6
+ data: Uint8ClampedArray;
7
+ width: number;
8
+ height: number;
9
+ }
10
+ interface PixelateOptions {
11
+ /**
12
+ * Number of pixel cells along each axis (e.g. 32 → 32×32 grid).
13
+ * Any positive integer. Values larger than the image dimension are clamped.
14
+ */
15
+ resolution: number;
16
+ /**
17
+ * Color sampling algorithm per cell.
18
+ * - `'clean'` — most-frequent color (sharp, graphic look) [default]
19
+ * - `'detail'` — average color (smoother gradients, more texture)
20
+ */
21
+ mode?: 'clean' | 'detail';
22
+ /**
23
+ * Output dimensions.
24
+ * - `'original'` — same size as input, cells filled with uniform color [default]
25
+ * - `'resized'` — output is `resolution × resolution` pixels
26
+ */
27
+ output?: 'original' | 'resized';
28
+ }
29
+ interface PixelateResult {
30
+ data: Uint8ClampedArray;
31
+ width: number;
32
+ height: number;
33
+ }
34
+ /**
35
+ * Pixelates an image synchronously.
36
+ *
37
+ * Works in both browser and Node.js (no DOM required).
38
+ *
39
+ * @example
40
+ * ```ts
41
+ * const result = pixelate(imageData, { resolution: 32 })
42
+ * const result = pixelate(imageData, { resolution: 64, mode: 'detail', output: 'resized' })
43
+ * ```
44
+ */
45
+ declare function pixelate(input: ImageLike, options: PixelateOptions): PixelateResult;
46
+
47
+ export { type ImageLike, type PixelateOptions, type PixelateResult, pixelate };
package/dist/index.js ADDED
@@ -0,0 +1,118 @@
1
+ // src/algorithms.ts
2
+ var _freq = new Uint16Array(32768);
3
+ var _touched = [];
4
+ function getFrequentColor(data, width, x0, y0, x1, y1) {
5
+ for (let i = 0; i < _touched.length; i++) _freq[_touched[i]] = 0;
6
+ _touched.length = 0;
7
+ let maxCount = 0;
8
+ let bestKey = 0;
9
+ let transparentCount = 0;
10
+ let totalPixels = 0;
11
+ for (let py = y0; py < y1; py++) {
12
+ const row = py * width * 4;
13
+ for (let px = x0; px < x1; px++) {
14
+ const i = row + px * 4;
15
+ const a = data[i + 3];
16
+ totalPixels++;
17
+ if (a < 128) {
18
+ transparentCount++;
19
+ continue;
20
+ }
21
+ const key = data[i] >> 3 << 10 | data[i + 1] >> 3 << 5 | data[i + 2] >> 3;
22
+ if (_freq[key] === 0) _touched.push(key);
23
+ const c = ++_freq[key];
24
+ if (c > maxCount) {
25
+ maxCount = c;
26
+ bestKey = key;
27
+ }
28
+ }
29
+ }
30
+ if (totalPixels === 0 || transparentCount * 2 > totalPixels) return [0, 0, 0, 0];
31
+ return [(bestKey >> 10 & 31) << 3, (bestKey >> 5 & 31) << 3, (bestKey & 31) << 3, 255];
32
+ }
33
+ function getAverageColor(data, width, x0, y0, x1, y1) {
34
+ let rSum = 0, gSum = 0, bSum = 0, aSum = 0;
35
+ let transparentCount = 0;
36
+ let totalPixels = 0;
37
+ for (let py = y0; py < y1; py++) {
38
+ const row = py * width * 4;
39
+ for (let px = x0; px < x1; px++) {
40
+ const i = row + px * 4;
41
+ const a = data[i + 3];
42
+ totalPixels++;
43
+ aSum += a;
44
+ if (a < 128) {
45
+ transparentCount++;
46
+ continue;
47
+ }
48
+ rSum += data[i];
49
+ gSum += data[i + 1];
50
+ bSum += data[i + 2];
51
+ }
52
+ }
53
+ if (totalPixels === 0 || transparentCount * 2 > totalPixels) return [0, 0, 0, 0];
54
+ const visible = totalPixels - transparentCount;
55
+ return [
56
+ visible > 0 ? rSum / visible + 0.5 | 0 : 0,
57
+ visible > 0 ? gSum / visible + 0.5 | 0 : 0,
58
+ visible > 0 ? bSum / visible + 0.5 | 0 : 0,
59
+ aSum / totalPixels + 0.5 | 0
60
+ ];
61
+ }
62
+
63
+ // src/index.ts
64
+ function pixelate(input, options) {
65
+ const { data, width, height } = input;
66
+ const { mode = "clean", output = "original" } = options;
67
+ const resolution = Math.max(1, Math.min(Math.floor(options.resolution), width, height));
68
+ const getColor = mode === "clean" ? getFrequentColor : getAverageColor;
69
+ const cellW = width / resolution;
70
+ const cellH = height / resolution;
71
+ const cellColors = new Uint8ClampedArray(resolution * resolution * 4);
72
+ for (let row = 0; row < resolution; row++) {
73
+ for (let col = 0; col < resolution; col++) {
74
+ const x0 = Math.round(col * cellW);
75
+ const y0 = Math.round(row * cellH);
76
+ const x1 = Math.round((col + 1) * cellW);
77
+ const y1 = Math.round((row + 1) * cellH);
78
+ const [r, g, b, a] = getColor(data, width, x0, y0, x1, y1);
79
+ const idx = (row * resolution + col) * 4;
80
+ cellColors[idx] = r;
81
+ cellColors[idx + 1] = g;
82
+ cellColors[idx + 2] = b;
83
+ cellColors[idx + 3] = a;
84
+ }
85
+ }
86
+ if (output === "resized") {
87
+ return { data: cellColors, width: resolution, height: resolution };
88
+ }
89
+ const out = new Uint8ClampedArray(width * height * 4);
90
+ for (let row = 0; row < resolution; row++) {
91
+ for (let col = 0; col < resolution; col++) {
92
+ const idx = (row * resolution + col) * 4;
93
+ const r = cellColors[idx];
94
+ const g = cellColors[idx + 1];
95
+ const b = cellColors[idx + 2];
96
+ const a = cellColors[idx + 3];
97
+ const x0 = Math.round(col * cellW);
98
+ const y0 = Math.round(row * cellH);
99
+ const x1 = Math.round((col + 1) * cellW);
100
+ const y1 = Math.round((row + 1) * cellH);
101
+ for (let py = y0; py < y1; py++) {
102
+ const rowBase = py * width * 4;
103
+ for (let px = x0; px < x1; px++) {
104
+ const i = rowBase + px * 4;
105
+ out[i] = r;
106
+ out[i + 1] = g;
107
+ out[i + 2] = b;
108
+ out[i + 3] = a;
109
+ }
110
+ }
111
+ }
112
+ }
113
+ return { data: out, width, height };
114
+ }
115
+
116
+ export { pixelate };
117
+ //# sourceMappingURL=index.js.map
118
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/algorithms.ts","../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Pre-allocated frequency table for frequent-color sampling.\n * 5 bits per channel (>> 3) → 32^3 = 32,768 buckets.\n * Safe because JS is single-threaded per context.\n */\nconst _freq = new Uint16Array(32768)\nconst _touched: number[] = []\n\n/**\n * Returns the most-frequent quantized color in the cell.\n * Uses a typed-array bucket table instead of Map for speed.\n */\nexport function getFrequentColor(\n data: Uint8ClampedArray,\n width: number,\n x0: number,\n y0: number,\n x1: number,\n y1: number,\n): [number, number, number, number] {\n // Reset only buckets touched in the previous call\n for (let i = 0; i < _touched.length; i++) _freq[_touched[i]] = 0\n _touched.length = 0\n\n let maxCount = 0\n let bestKey = 0\n let transparentCount = 0\n let totalPixels = 0\n\n for (let py = y0; py < y1; py++) {\n const row = py * width * 4\n for (let px = x0; px < x1; px++) {\n const i = row + px * 4\n const a = data[i + 3]\n totalPixels++\n if (a < 128) {\n transparentCount++\n continue\n }\n // Pack 5-bit quantized channels into a single 15-bit key\n const key = ((data[i] >> 3) << 10) | ((data[i + 1] >> 3) << 5) | (data[i + 2] >> 3)\n if (_freq[key] === 0) _touched.push(key)\n const c = ++_freq[key]\n if (c > maxCount) {\n maxCount = c\n bestKey = key\n }\n }\n }\n\n if (totalPixels === 0 || transparentCount * 2 > totalPixels) return [0, 0, 0, 0]\n\n // Decode key back to RGB (multiply by 8 to restore approximate original range)\n return [((bestKey >> 10) & 31) << 3, ((bestKey >> 5) & 31) << 3, (bestKey & 31) << 3, 255]\n}\n\n/**\n * Returns the average color of visible (non-transparent) pixels in the cell.\n */\nexport function getAverageColor(\n data: Uint8ClampedArray,\n width: number,\n x0: number,\n y0: number,\n x1: number,\n y1: number,\n): [number, number, number, number] {\n let rSum = 0,\n gSum = 0,\n bSum = 0,\n aSum = 0\n let transparentCount = 0\n let totalPixels = 0\n\n for (let py = y0; py < y1; py++) {\n const row = py * width * 4\n for (let px = x0; px < x1; px++) {\n const i = row + px * 4\n const a = data[i + 3]\n totalPixels++\n aSum += a\n if (a < 128) {\n transparentCount++\n continue\n }\n rSum += data[i]\n gSum += data[i + 1]\n bSum += data[i + 2]\n }\n }\n\n if (totalPixels === 0 || transparentCount * 2 > totalPixels) return [0, 0, 0, 0]\n\n const visible = totalPixels - transparentCount\n // (x + 0.5) | 0 ≡ Math.round(x) for non-negative values — avoids function call overhead\n return [\n visible > 0 ? (rSum / visible + 0.5) | 0 : 0,\n visible > 0 ? (gSum / visible + 0.5) | 0 : 0,\n visible > 0 ? (bSum / visible + 0.5) | 0 : 0,\n (aSum / totalPixels + 0.5) | 0,\n ]\n}\n","import { getAverageColor, getFrequentColor } from './algorithms'\n\n// ─── Types ───────────────────────────────────────────────────────────────────\n\n/**\n * Any object with `data`, `width`, `height` — compatible with the browser's\n * built-in `ImageData` as well as node-canvas and raw buffers.\n */\nexport interface ImageLike {\n data: Uint8ClampedArray\n width: number\n height: number\n}\n\nexport interface PixelateOptions {\n /**\n * Number of pixel cells along each axis (e.g. 32 → 32×32 grid).\n * Any positive integer. Values larger than the image dimension are clamped.\n */\n resolution: number\n\n /**\n * Color sampling algorithm per cell.\n * - `'clean'` — most-frequent color (sharp, graphic look) [default]\n * - `'detail'` — average color (smoother gradients, more texture)\n */\n mode?: 'clean' | 'detail'\n\n /**\n * Output dimensions.\n * - `'original'` — same size as input, cells filled with uniform color [default]\n * - `'resized'` — output is `resolution × resolution` pixels\n */\n output?: 'original' | 'resized'\n}\n\nexport interface PixelateResult {\n data: Uint8ClampedArray\n width: number\n height: number\n}\n\n// ─── Core ────────────────────────────────────────────────────────────────────\n\n/**\n * Pixelates an image synchronously.\n *\n * Works in both browser and Node.js (no DOM required).\n *\n * @example\n * ```ts\n * const result = pixelate(imageData, { resolution: 32 })\n * const result = pixelate(imageData, { resolution: 64, mode: 'detail', output: 'resized' })\n * ```\n */\nexport function pixelate(input: ImageLike, options: PixelateOptions): PixelateResult {\n const { data, width, height } = input\n const { mode = 'clean', output = 'original' } = options\n\n const resolution = Math.max(1, Math.min(Math.floor(options.resolution), width, height))\n\n const getColor = mode === 'clean' ? getFrequentColor : getAverageColor\n const cellW = width / resolution\n const cellH = height / resolution\n\n // Sample one color per cell\n const cellColors = new Uint8ClampedArray(resolution * resolution * 4)\n\n for (let row = 0; row < resolution; row++) {\n for (let col = 0; col < resolution; col++) {\n const x0 = Math.round(col * cellW)\n const y0 = Math.round(row * cellH)\n const x1 = Math.round((col + 1) * cellW)\n const y1 = Math.round((row + 1) * cellH)\n const [r, g, b, a] = getColor(data, width, x0, y0, x1, y1)\n const idx = (row * resolution + col) * 4\n cellColors[idx] = r\n cellColors[idx + 1] = g\n cellColors[idx + 2] = b\n cellColors[idx + 3] = a\n }\n }\n\n // Build output\n if (output === 'resized') {\n return { data: cellColors, width: resolution, height: resolution }\n }\n\n // output === 'original': paint each cell back at full size\n const out = new Uint8ClampedArray(width * height * 4)\n\n for (let row = 0; row < resolution; row++) {\n for (let col = 0; col < resolution; col++) {\n const idx = (row * resolution + col) * 4\n const r = cellColors[idx]\n const g = cellColors[idx + 1]\n const b = cellColors[idx + 2]\n const a = cellColors[idx + 3]\n\n const x0 = Math.round(col * cellW)\n const y0 = Math.round(row * cellH)\n const x1 = Math.round((col + 1) * cellW)\n const y1 = Math.round((row + 1) * cellH)\n\n for (let py = y0; py < y1; py++) {\n const rowBase = py * width * 4\n for (let px = x0; px < x1; px++) {\n const i = rowBase + px * 4\n out[i] = r\n out[i + 1] = g\n out[i + 2] = b\n out[i + 3] = a\n }\n }\n }\n }\n\n return { data: out, width, height }\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "fast-pixelizer",
3
+ "version": "1.0.0",
4
+ "description": "Fast, zero-dependency image pixelation. Works in browser and Node.js.",
5
+ "keywords": [
6
+ "pixelate",
7
+ "pixel-art",
8
+ "image",
9
+ "canvas",
10
+ "imagedata"
11
+ ],
12
+ "license": "MIT",
13
+ "author": "handsupmin",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/handsupmin/fast-pixelizer.git"
17
+ },
18
+ "type": "module",
19
+ "main": "./dist/index.cjs",
20
+ "module": "./dist/index.js",
21
+ "types": "./dist/index.d.ts",
22
+ "exports": {
23
+ ".": {
24
+ "import": {
25
+ "types": "./dist/index.d.ts",
26
+ "default": "./dist/index.js"
27
+ },
28
+ "require": {
29
+ "types": "./dist/index.d.cts",
30
+ "default": "./dist/index.cjs"
31
+ }
32
+ }
33
+ },
34
+ "files": [
35
+ "dist",
36
+ "README.md",
37
+ "LICENSE"
38
+ ],
39
+ "scripts": {
40
+ "build": "tsup",
41
+ "dev": "tsup --watch",
42
+ "lint": "eslint src",
43
+ "lint:fix": "eslint src --fix",
44
+ "format": "prettier --write .",
45
+ "examples": "npm run build && node scripts/generate-examples.mjs",
46
+ "prepublishOnly": "npm run build"
47
+ },
48
+ "devDependencies": {
49
+ "@eslint/js": "^9.0.0",
50
+ "@types/sharp": "^0.31.1",
51
+ "eslint": "^9.0.0",
52
+ "eslint-config-prettier": "^10.0.0",
53
+ "eslint-plugin-prettier": "^5.0.0",
54
+ "globals": "^15.0.0",
55
+ "prettier": "^3.0.0",
56
+ "sharp": "^0.34.5",
57
+ "tsup": "^8.0.0",
58
+ "typescript": "~5.5.0",
59
+ "typescript-eslint": "^8.0.0"
60
+ }
61
+ }