image-to-pixel 3.0.1 → 3.1.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.
@@ -0,0 +1,62 @@
1
+ // The saved workspace: every open file (its bytes, name and own settings) plus the shared settings,
2
+ // kept in this browser's IndexedDB. Nothing is uploaded. One workspace per browser profile.
3
+
4
+ const DB = 'image-to-pixel';
5
+ const STORE = 'workspace';
6
+ const KEY = 'current';
7
+ const VERSION = 1;
8
+
9
+ function open() {
10
+ return new Promise((resolve, reject) => {
11
+ if (typeof indexedDB === 'undefined') { reject(new Error('This browser has no IndexedDB')); return; }
12
+ const req = indexedDB.open(DB, 1);
13
+ req.onupgradeneeded = () => { if (!req.result.objectStoreNames.contains(STORE)) req.result.createObjectStore(STORE); };
14
+ req.onsuccess = () => resolve(req.result);
15
+ req.onerror = () => reject(req.error);
16
+ });
17
+ }
18
+
19
+ async function run(mode, fn) {
20
+ const db = await open();
21
+ try {
22
+ return await new Promise((resolve, reject) => {
23
+ const tx = db.transaction(STORE, mode);
24
+ const req = fn(tx.objectStore(STORE));
25
+ tx.oncomplete = () => resolve(req && req.result);
26
+ tx.onerror = () => reject(tx.error || (req && req.error));
27
+ tx.onabort = () => reject(tx.error || new Error('Saving was cancelled'));
28
+ });
29
+ } finally {
30
+ db.close();
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Save the workspace.
36
+ * @param {{ shared: object, current: number, items: { name: string, type: 'image'|'video', file: Blob, own: object|null }[] }} ws
37
+ * @returns {Promise<number>} total bytes of the saved files
38
+ */
39
+ export async function saveWorkspace(ws) {
40
+ const record = { version: VERSION, savedAt: Date.now(), ...ws };
41
+ await run('readwrite', s => s.put(record, KEY));
42
+ return ws.items.reduce((a, it) => a + (it.file ? it.file.size : 0), 0);
43
+ }
44
+
45
+ /** The saved workspace, or null. */
46
+ export async function loadWorkspace() {
47
+ try {
48
+ const rec = await run('readonly', s => s.get(KEY));
49
+ return rec && rec.version === VERSION && Array.isArray(rec.items) ? rec : null;
50
+ } catch (_) {
51
+ return null;
52
+ }
53
+ }
54
+
55
+ export async function forgetWorkspace() {
56
+ await run('readwrite', s => s.delete(KEY));
57
+ }
58
+
59
+ /** True when the error means the browser ran out of storage for us. */
60
+ export const isQuotaError = e => !!e && (e.name === 'QuotaExceededError' || /quota|space/i.test(e.message || ''));
61
+
62
+ export const fmtBytes = n => (n >= 1e9 ? `${(n / 1e9).toFixed(1)} GB` : n >= 1e6 ? `${(n / 1e6).toFixed(1)} MB` : `${Math.max(1, Math.round(n / 1e3))} KB`);
package/lib/index.js CHANGED
@@ -1,31 +1,31 @@
1
- // image-to-pixel core: no UI. Works in browsers (WebGL 2 via ShaderMate, CPU fallback) and,
2
- // for { width, height, data } inputs, in Node on the CPU.
3
-
4
- export { createPixelator, pixelate, makeResult } from './core/engine.js';
5
- export {
6
- DEFAULTS, DITHER_METHODS, SETTINGS_VERSION, MAX_SIZE, LEGACY_DITHER,
7
- defaultSettings, normalizeSettings, mergeSettings, resolveSize, ditherKind, cropRect, ASPECTS,
8
- encodeSettings, decodeSettings, compactSettings, settingsToJSON, settingsFromJSON,
9
- } from './core/settings.js';
10
- export { makePlan } from './core/plan.js';
11
- export { runCPU } from './core/cpu/pipeline.js';
12
- export { PALETTES, getPalette } from './core/palette/builtin.js';
13
- export { parsePalette, toGPL, toHexFile, toPaletteJSON } from './core/palette/parse.js';
14
- export { extractPalette } from './core/palette/extract.js';
15
- export { fetchLospec, lospecSlug } from './core/palette/lospec.js';
16
- export { thresholdMap, parseMatrix, bayer } from './core/dither/matrices.js';
17
- export { KERNELS } from './core/dither/kernels.js';
18
- export { PRESETS, getPreset } from './core/presets.js';
19
- export { parseColor, toHex, oklab } from './core/color.js';
20
- export { toFrame, loadImage } from './core/source.js';
21
- export { upscale, resizeNearest, uniqueColors, detectGrid, hasTransparency } from './core/export/scale.js';
22
- export { toCanvas, downloadBlob } from './core/export/canvas.js';
23
- export { encodePNG } from './core/export/png.js';
24
- export { encodeGIF } from './core/export/gif.js';
25
- export { createZip } from './core/export/zip.js';
26
- export { packSpriteSheet, buildTileset } from './core/export/sheet.js';
27
- export { forEachFrame, estimateFps, seekTo, frameTimes, videoReady, mediaError, sniffVideoCodec } from './core/video/frames.js';
28
-
29
- /* global __ITP_VERSION__ */
30
- // The build replaces __ITP_VERSION__ with package.json's version; unbundled source reports 'dev'.
31
- export const VERSION = typeof __ITP_VERSION__ === 'string' ? __ITP_VERSION__ : 'dev';
1
+ // image-to-pixel core: no UI. Works in browsers (WebGL 2 via ShaderMate, CPU fallback) and,
2
+ // for { width, height, data } inputs, in Node on the CPU.
3
+
4
+ export { createPixelator, pixelate, makeResult } from './core/engine.js';
5
+ export {
6
+ DEFAULTS, DITHER_METHODS, SETTINGS_VERSION, MAX_SIZE, LEGACY_DITHER,
7
+ defaultSettings, normalizeSettings, mergeSettings, resolveSize, ditherKind, cropRect, ASPECTS,
8
+ encodeSettings, decodeSettings, compactSettings, settingsToJSON, settingsFromJSON,
9
+ } from './core/settings.js';
10
+ export { makePlan } from './core/plan.js';
11
+ export { runCPU } from './core/cpu/pipeline.js';
12
+ export { PALETTES, getPalette } from './core/palette/builtin.js';
13
+ export { parsePalette, toGPL, toHexFile, toPaletteJSON } from './core/palette/parse.js';
14
+ export { extractPalette } from './core/palette/extract.js';
15
+ export { fetchLospec, lospecSlug } from './core/palette/lospec.js';
16
+ export { thresholdMap, parseMatrix, bayer } from './core/dither/matrices.js';
17
+ export { KERNELS } from './core/dither/kernels.js';
18
+ export { PRESETS, getPreset } from './core/presets.js';
19
+ export { parseColor, toHex, oklab } from './core/color.js';
20
+ export { toFrame, loadImage } from './core/source.js';
21
+ export { upscale, resizeNearest, uniqueColors, detectGrid, hasTransparency } from './core/export/scale.js';
22
+ export { toCanvas, downloadBlob } from './core/export/canvas.js';
23
+ export { encodePNG } from './core/export/png.js';
24
+ export { encodeGIF } from './core/export/gif.js';
25
+ export { createZip } from './core/export/zip.js';
26
+ export { packSpriteSheet, buildTileset } from './core/export/sheet.js';
27
+ export { forEachFrame, estimateFps, seekTo, frameTimes, videoReady, mediaError, sniffVideoCodec } from './core/video/frames.js';
28
+
29
+ /* global __ITP_VERSION__ */
30
+ // The build replaces __ITP_VERSION__ with package.json's version; unbundled source reports 'dev'.
31
+ export const VERSION = typeof __ITP_VERSION__ === 'string' ? __ITP_VERSION__ : 'dev';
package/lib/video.js CHANGED
@@ -1,92 +1,122 @@
1
- // image-to-pixel/video: encode processed frames to MP4 (H.264) or WebM (VP9/VP8) with WebCodecs.
2
- // The muxers are this project's own (lib/core/video/mux-*.js): MIT, no dependencies.
3
-
4
- import { createMp4Muxer } from './core/video/mux-mp4.js';
5
- import { createWebmMuxer } from './core/video/mux-webm.js';
6
- import { upscale } from './core/export/scale.js';
7
-
8
- export { forEachFrame, estimateFps, seekTo, frameTimes, videoReady } from './core/video/frames.js';
9
- export { createMp4Muxer, createWebmMuxer };
10
-
11
- const AVC = ['avc1.640033', 'avc1.4d0033', 'avc1.42003e', 'avc1.640028', 'avc1.42001f'];
12
- const VP = [['vp09.00.10.08', 'vp9'], ['vp8', 'vp8']];
13
-
14
- /** Which formats this browser can encode. */
15
- export async function videoSupport(width = 640, height = 480) {
16
- const out = { mp4: false, webm: false, webcodecs: typeof VideoEncoder !== 'undefined' };
17
- if (out.webcodecs) {
18
- for (const codec of AVC) if ((await VideoEncoder.isConfigSupported({ codec, width, height }).catch(() => ({}))).supported) { out.mp4 = true; break; }
19
- for (const [codec] of VP) if ((await VideoEncoder.isConfigSupported({ codec, width, height }).catch(() => ({}))).supported) { out.webm = true; break; }
20
- }
21
- return out;
22
- }
23
-
24
- const even = n => n + (n & 1);
25
-
26
- /**
27
- * Start an encoder; add frames one at a time (so a long clip never sits in memory), then finish().
28
- * @param {{ format: 'mp4' | 'webm', fps: number, width: number, height: number, scale?: number, bitrate?: number }} opts
29
- * width/height: size of the frames you will add (before scaling)
30
- */
31
- export async function createVideoEncoder(opts) {
32
- const scale = Math.max(1, Math.round(opts.scale || 1));
33
- const W = even(opts.width * scale), H = even(opts.height * scale);
34
- const fps = opts.fps || 30;
35
- const bitrate = opts.bitrate || Math.min(20e6, Math.max(2e6, W * H * fps * 0.15));
36
- if (typeof VideoEncoder === 'undefined') throw new Error('This browser has no WebCodecs VideoEncoder. Export a GIF or PNG sequence instead.');
37
-
38
- let codec = null, muxCodec = null;
39
- const list = opts.format === 'mp4' ? AVC.map(c => [c, 'avc']) : VP;
40
- for (const [c, m] of list) {
41
- const ok = await VideoEncoder.isConfigSupported({ codec: c, width: W, height: H, bitrate, framerate: fps }).catch(() => ({}));
42
- if (ok.supported) { codec = c; muxCodec = m; break; }
43
- }
44
- if (!codec) throw new Error(`No ${opts.format.toUpperCase()} encoder available in this browser at ${W}×${H}.`);
45
-
46
- const muxer = opts.format === 'mp4'
47
- ? createMp4Muxer({ width: W, height: H })
48
- : createWebmMuxer({ width: W, height: H, codec: muxCodec, fps });
49
-
50
- let error = null;
51
- const encoder = new VideoEncoder({
52
- output: (chunk, meta) => muxer.addChunk(chunk, meta),
53
- error: e => { error = e; },
54
- });
55
- // avc.format 'avc': length-prefixed NAL units plus a decoder config record, which MP4 needs
56
- encoder.configure({ codec, width: W, height: H, bitrate, framerate: fps, latencyMode: 'quality', ...(opts.format === 'mp4' ? { avc: { format: 'avc' } } : {}) });
57
-
58
- const canvas = new OffscreenCanvas(W, H);
59
- const ctx = canvas.getContext('2d');
60
- let i = 0;
61
- const frameUs = 1e6 / fps;
62
-
63
- return {
64
- width: W, height: H, codec,
65
- /** @param {{ width, height, data }} img */
66
- async add(img) {
67
- if (error) throw error;
68
- const up = scale > 1 ? upscale(img, scale) : img;
69
- ctx.fillStyle = '#000'; ctx.fillRect(0, 0, W, H);
70
- ctx.putImageData(new ImageData(new Uint8ClampedArray(up.data), up.width, up.height), 0, 0);
71
- const frame = new VideoFrame(canvas, { timestamp: Math.round(i * frameUs), duration: Math.round(frameUs) });
72
- encoder.encode(frame, { keyFrame: i % Math.max(1, Math.round(fps * 2)) === 0 });
73
- frame.close();
74
- i++;
75
- if (encoder.encodeQueueSize > 8) await new Promise(r => setTimeout(r, 0));
76
- },
77
- /** @returns {Promise<Blob>} */
78
- async finish() {
79
- await encoder.flush();
80
- if (error) throw error;
81
- encoder.close();
82
- return new Blob([muxer.finish()], { type: opts.format === 'mp4' ? 'video/mp4' : 'video/webm' });
83
- },
84
- };
85
- }
86
-
87
- /** Convenience: encode an array of frames in one go. */
88
- export async function encodeVideo(frames, opts) {
89
- const enc = await createVideoEncoder({ ...opts, width: frames[0].width, height: frames[0].height });
90
- for (const f of frames) await enc.add(f);
91
- return enc.finish();
92
- }
1
+ // image-to-pixel/video: encode processed frames to MP4 (H.264) or WebM (VP9/VP8) with WebCodecs,
2
+ // optionally with the source's soundtrack (AAC or Opus in MP4, Opus in WebM).
3
+ // The muxers are this project's own (lib/core/video/mux-*.js): MIT, no dependencies.
4
+
5
+ import { createMp4Muxer } from './core/video/mux-mp4.js';
6
+ import { createWebmMuxer } from './core/video/mux-webm.js';
7
+ import { decodeAudio, encodeAudio, audioCodecSupported, findAudioConfig } from './core/video/audio.js';
8
+ import { upscale } from './core/export/scale.js';
9
+
10
+ export { forEachFrame, estimateFps, seekTo, frameTimes, videoReady } from './core/video/frames.js';
11
+ export { createMp4Muxer, createWebmMuxer, decodeAudio, encodeAudio, audioCodecSupported, findAudioConfig };
12
+
13
+ const AVC = ['avc1.640033', 'avc1.4d0033', 'avc1.42003e', 'avc1.640028', 'avc1.42001f'];
14
+ const VP = [['vp09.00.10.08', 'vp9'], ['vp8', 'vp8']];
15
+
16
+ /** Which formats this browser can encode. `aac` / `opus`: audio for MP4 (either) and WebM (Opus). */
17
+ export async function videoSupport(width = 640, height = 480) {
18
+ const out = { mp4: false, webm: false, webcodecs: typeof VideoEncoder !== 'undefined', aac: false, opus: false };
19
+ if (out.webcodecs) {
20
+ for (const codec of AVC) if ((await VideoEncoder.isConfigSupported({ codec, width, height }).catch(() => ({}))).supported) { out.mp4 = true; break; }
21
+ for (const [codec] of VP) if ((await VideoEncoder.isConfigSupported({ codec, width, height }).catch(() => ({}))).supported) { out.webm = true; break; }
22
+ }
23
+ out.aac = await audioCodecSupported('aac');
24
+ out.opus = await audioCodecSupported('opus');
25
+ return out;
26
+ }
27
+
28
+ const even = n => n + (n & 1);
29
+
30
+ /** The audio codec and encoder config for a container, or null when the browser can't encode a suitable one. */
31
+ async function pickAudio(format, pcm, bitrate) {
32
+ const order = format === 'mp4' ? ['aac', 'opus'] : ['opus']; // WebM only holds Opus (or Vorbis)
33
+ for (const codec of order) {
34
+ const config = await findAudioConfig(codec, pcm.sampleRate, pcm.numberOfChannels, bitrate);
35
+ if (config) return { codec, config };
36
+ }
37
+ return null;
38
+ }
39
+
40
+ /**
41
+ * Start an encoder; add frames one at a time (so a long clip never sits in memory), then finish().
42
+ * @param {{ format: 'mp4' | 'webm', fps: number, width: number, height: number, scale?: number, bitrate?: number,
43
+ * audio?: { sampleRate: number, numberOfChannels: number, channels: Float32Array[] } | null,
44
+ * audioBitrate?: number }} opts
45
+ * width/height: size of the frames you will add (before scaling)
46
+ * audio: PCM to add as a soundtrack, e.g. from decodeAudio(file, { start, end }). It is cut to the
47
+ * video's length. When the browser can't encode audio the video is still made, without it
48
+ * (check `encoder.audioCodec`).
49
+ */
50
+ export async function createVideoEncoder(opts) {
51
+ const scale = Math.max(1, Math.round(opts.scale || 1));
52
+ const W = even(opts.width * scale), H = even(opts.height * scale);
53
+ const fps = opts.fps || 30;
54
+ const bitrate = opts.bitrate || Math.min(20e6, Math.max(2e6, W * H * fps * 0.15));
55
+ if (typeof VideoEncoder === 'undefined') throw new Error('This browser has no WebCodecs VideoEncoder. Export a GIF or PNG sequence instead.');
56
+
57
+ let codec = null, muxCodec = null;
58
+ const list = opts.format === 'mp4' ? AVC.map(c => [c, 'avc']) : VP;
59
+ for (const [c, m] of list) {
60
+ const ok = await VideoEncoder.isConfigSupported({ codec: c, width: W, height: H, bitrate, framerate: fps }).catch(() => ({}));
61
+ if (ok.supported) { codec = c; muxCodec = m; break; }
62
+ }
63
+ if (!codec) throw new Error(`No ${opts.format.toUpperCase()} encoder available in this browser at ${W}×${H}.`);
64
+
65
+ const pcm = opts.audio && opts.audio.channels && opts.audio.channels.length ? opts.audio : null;
66
+ const picked = pcm ? await pickAudio(opts.format, pcm, opts.audioBitrate || 128000) : null;
67
+ const audioCodec = picked ? picked.codec : null;
68
+ const audioTrack = picked ? { codec: audioCodec, sampleRate: picked.config.sampleRate, numberOfChannels: picked.config.numberOfChannels, bitrate: picked.config.bitrate || 128000 } : null;
69
+
70
+ const muxer = opts.format === 'mp4'
71
+ ? createMp4Muxer({ width: W, height: H, audio: audioTrack })
72
+ : createWebmMuxer({ width: W, height: H, codec: muxCodec, fps, audio: audioTrack });
73
+
74
+ let error = null;
75
+ const encoder = new VideoEncoder({
76
+ output: (chunk, meta) => muxer.addChunk(chunk, meta),
77
+ error: e => { error = e; },
78
+ });
79
+ // avc.format 'avc': length-prefixed NAL units plus a decoder config record, which MP4 needs
80
+ encoder.configure({ codec, width: W, height: H, bitrate, framerate: fps, latencyMode: 'quality', ...(opts.format === 'mp4' ? { avc: { format: 'avc' } } : {}) });
81
+
82
+ const canvas = new OffscreenCanvas(W, H);
83
+ const ctx = canvas.getContext('2d');
84
+ let i = 0;
85
+ const frameUs = 1e6 / fps;
86
+
87
+ return {
88
+ width: W, height: H, codec,
89
+ /** 'aac' or 'opus' when the soundtrack will be included, null otherwise. */
90
+ audioCodec,
91
+ /** @param {{ width, height, data }} img */
92
+ async add(img) {
93
+ if (error) throw error;
94
+ const up = scale > 1 ? upscale(img, scale) : img;
95
+ ctx.fillStyle = '#000'; ctx.fillRect(0, 0, W, H);
96
+ ctx.putImageData(new ImageData(new Uint8ClampedArray(up.data), up.width, up.height), 0, 0);
97
+ const frame = new VideoFrame(canvas, { timestamp: Math.round(i * frameUs), duration: Math.round(frameUs) });
98
+ encoder.encode(frame, { keyFrame: i % Math.max(1, Math.round(fps * 2)) === 0 });
99
+ frame.close();
100
+ i++;
101
+ if (encoder.encodeQueueSize > 8) await new Promise(r => setTimeout(r, 0));
102
+ },
103
+ /** @returns {Promise<Blob>} */
104
+ async finish() {
105
+ await encoder.flush();
106
+ if (error) throw error;
107
+ encoder.close();
108
+ if (audioCodec && i > 0) {
109
+ // the soundtrack ends with the last frame
110
+ await encodeAudio(pcm, { config: picked.config, maxDuration: i / fps, onChunk: (chunk, meta) => muxer.addAudioChunk(chunk, meta) });
111
+ }
112
+ return new Blob([muxer.finish()], { type: opts.format === 'mp4' ? 'video/mp4' : 'video/webm' });
113
+ },
114
+ };
115
+ }
116
+
117
+ /** Convenience: encode an array of frames in one go. */
118
+ export async function encodeVideo(frames, opts) {
119
+ const enc = await createVideoEncoder({ ...opts, width: frames[0].width, height: frames[0].height });
120
+ for (const f of frames) await enc.add(f);
121
+ return enc.finish();
122
+ }
package/package.json CHANGED
@@ -1,92 +1,92 @@
1
- {
2
- "name": "image-to-pixel",
3
- "version": "3.0.1",
4
- "description": "Turn images and video into pixel art: dithering, palettes, batch and video. A WebGL engine (via ShaderMate) with a CPU fallback, plus a full editor.",
5
- "type": "module",
6
- "license": "MIT",
7
- "author": "Tezumie",
8
- "homepage": "https://tezumie.github.io/Image-to-Pixel/",
9
- "repository": {
10
- "type": "git",
11
- "url": "git+https://github.com/Tezumie/Image-to-Pixel.git"
12
- },
13
- "bugs": {
14
- "url": "https://github.com/Tezumie/Image-to-Pixel/issues"
15
- },
16
- "keywords": [
17
- "pixel-art",
18
- "dithering",
19
- "palette",
20
- "quantization",
21
- "floyd-steinberg",
22
- "bayer",
23
- "webgl",
24
- "glsl",
25
- "shader",
26
- "gif",
27
- "sprite-sheet",
28
- "lospec"
29
- ],
30
- "main": "./dist/image-to-pixel.cjs",
31
- "module": "./dist/image-to-pixel.esm.js",
32
- "types": "./types/index.d.ts",
33
- "unpkg": "./dist/image-to-pixel.min.js",
34
- "jsdelivr": "./dist/image-to-pixel.min.js",
35
- "exports": {
36
- ".": {
37
- "types": "./types/index.d.ts",
38
- "import": "./dist/image-to-pixel.esm.js",
39
- "require": "./dist/image-to-pixel.cjs",
40
- "default": "./dist/image-to-pixel.esm.js"
41
- },
42
- "./video": {
43
- "types": "./types/video.d.ts",
44
- "import": "./dist/video.esm.js",
45
- "default": "./dist/video.esm.js"
46
- },
47
- "./editor": {
48
- "types": "./types/editor.d.ts",
49
- "import": "./dist/editor.esm.js",
50
- "default": "./dist/editor.esm.js"
51
- },
52
- "./editor.css": "./dist/editor.css",
53
- "./dist/*": "./dist/*",
54
- "./package.json": "./package.json"
55
- },
56
- "sideEffects": [
57
- "./dist/editor.css",
58
- "./dist/image-to-pixel.js",
59
- "./dist/image-to-pixel.min.js"
60
- ],
61
- "files": [
62
- "dist",
63
- "lib",
64
- "types",
65
- "LICENSE",
66
- "README.md"
67
- ],
68
- "engines": {
69
- "node": ">=18"
70
- },
71
- "scripts": {
72
- "build": "node esbuild.config.mjs",
73
- "build:site": "npm run build && node scripts/build-site.mjs",
74
- "watch": "node esbuild.config.mjs --watch",
75
- "dev": "node scripts/serve.mjs --dev",
76
- "preview": "node scripts/serve.mjs",
77
- "bluenoise": "node scripts/gen-bluenoise.mjs",
78
- "screenshots": "node scripts/screenshots.mjs",
79
- "clean": "node scripts/clean.mjs",
80
- "test": "npm run build:site && node --test",
81
- "prepublishOnly": "npm test",
82
- "check:runtime": "node scripts/nodepod-check.mjs"
83
- },
84
- "dependencies": {
85
- "shadermate": "^2.0.0"
86
- },
87
- "devDependencies": {
88
- "esbuild": "^0.25.12",
89
- "highlight.js": "^11.12.0",
90
- "marked": "^15.0.12"
91
- }
92
- }
1
+ {
2
+ "name": "image-to-pixel",
3
+ "version": "3.1.0",
4
+ "description": "Turn images and video into pixel art: dithering, palettes, batch and video. A WebGL engine (via ShaderMate) with a CPU fallback, plus a full editor.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Tezumie",
8
+ "homepage": "https://tezumie.github.io/Image-to-Pixel/",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/Tezumie/Image-to-Pixel.git"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/Tezumie/Image-to-Pixel/issues"
15
+ },
16
+ "keywords": [
17
+ "pixel-art",
18
+ "dithering",
19
+ "palette",
20
+ "quantization",
21
+ "floyd-steinberg",
22
+ "bayer",
23
+ "webgl",
24
+ "glsl",
25
+ "shader",
26
+ "gif",
27
+ "sprite-sheet",
28
+ "lospec"
29
+ ],
30
+ "main": "./dist/image-to-pixel.cjs",
31
+ "module": "./dist/image-to-pixel.esm.js",
32
+ "types": "./types/index.d.ts",
33
+ "unpkg": "./dist/image-to-pixel.min.js",
34
+ "jsdelivr": "./dist/image-to-pixel.min.js",
35
+ "exports": {
36
+ ".": {
37
+ "types": "./types/index.d.ts",
38
+ "import": "./dist/image-to-pixel.esm.js",
39
+ "require": "./dist/image-to-pixel.cjs",
40
+ "default": "./dist/image-to-pixel.esm.js"
41
+ },
42
+ "./video": {
43
+ "types": "./types/video.d.ts",
44
+ "import": "./dist/video.esm.js",
45
+ "default": "./dist/video.esm.js"
46
+ },
47
+ "./editor": {
48
+ "types": "./types/editor.d.ts",
49
+ "import": "./dist/editor.esm.js",
50
+ "default": "./dist/editor.esm.js"
51
+ },
52
+ "./editor.css": "./dist/editor.css",
53
+ "./dist/*": "./dist/*",
54
+ "./package.json": "./package.json"
55
+ },
56
+ "sideEffects": [
57
+ "./dist/editor.css",
58
+ "./dist/image-to-pixel.js",
59
+ "./dist/image-to-pixel.min.js"
60
+ ],
61
+ "files": [
62
+ "dist",
63
+ "lib",
64
+ "types",
65
+ "LICENSE",
66
+ "README.md"
67
+ ],
68
+ "engines": {
69
+ "node": ">=18"
70
+ },
71
+ "scripts": {
72
+ "build": "node esbuild.config.mjs",
73
+ "build:site": "npm run build && node scripts/build-site.mjs",
74
+ "watch": "node esbuild.config.mjs --watch",
75
+ "dev": "node scripts/serve.mjs --dev",
76
+ "preview": "node scripts/serve.mjs",
77
+ "bluenoise": "node scripts/gen-bluenoise.mjs",
78
+ "screenshots": "node scripts/screenshots.mjs",
79
+ "clean": "node scripts/clean.mjs",
80
+ "test": "npm run build:site && node --test",
81
+ "prepublishOnly": "npm test",
82
+ "check:runtime": "node scripts/nodepod-check.mjs"
83
+ },
84
+ "dependencies": {
85
+ "shadermate": "^2.0.0"
86
+ },
87
+ "devDependencies": {
88
+ "esbuild": "^0.25.12",
89
+ "highlight.js": "^11.12.0",
90
+ "marked": "^15.0.12"
91
+ }
92
+ }