image-to-pixel 3.0.0 → 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.
Files changed (71) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +2 -2
  3. package/dist/editor.css +1 -1
  4. package/dist/editor.css.map +2 -2
  5. package/dist/editor.esm.js +60 -60
  6. package/dist/editor.esm.js.map +4 -4
  7. package/dist/fonts/README.md +6 -6
  8. package/dist/fonts/geist-LICENSE.txt +93 -93
  9. package/dist/image-to-pixel.bundle.esm.js +16 -16
  10. package/dist/image-to-pixel.bundle.esm.js.map +3 -3
  11. package/dist/image-to-pixel.cjs +2 -2
  12. package/dist/image-to-pixel.cjs.map +2 -2
  13. package/dist/image-to-pixel.esm.js +2 -2
  14. package/dist/image-to-pixel.esm.js.map +2 -2
  15. package/dist/image-to-pixel.js +2 -2
  16. package/dist/image-to-pixel.js.map +2 -2
  17. package/dist/image-to-pixel.min.js +11 -11
  18. package/dist/image-to-pixel.min.js.map +3 -3
  19. package/dist/video.esm.js +2 -2
  20. package/dist/video.esm.js.map +4 -4
  21. package/lib/core/color.js +69 -69
  22. package/lib/core/cpu/pipeline.js +21 -21
  23. package/lib/core/cpu/stages.js +227 -227
  24. package/lib/core/dither/bluenoise-data.js +3 -3
  25. package/lib/core/dither/kernels.js +27 -27
  26. package/lib/core/dither/matrices.js +95 -95
  27. package/lib/core/engine.js +158 -158
  28. package/lib/core/export/canvas.js +22 -22
  29. package/lib/core/export/crc32.js +15 -15
  30. package/lib/core/export/gif.js +153 -153
  31. package/lib/core/export/png.js +94 -94
  32. package/lib/core/export/scale.js +103 -103
  33. package/lib/core/export/sheet.js +95 -95
  34. package/lib/core/export/zip.js +55 -55
  35. package/lib/core/gpu/backend.js +159 -159
  36. package/lib/core/gpu/shaders.js +258 -258
  37. package/lib/core/palette/builtin.js +36 -36
  38. package/lib/core/palette/extract.js +69 -69
  39. package/lib/core/palette/lospec.js +24 -24
  40. package/lib/core/palette/parse.js +112 -112
  41. package/lib/core/plan.js +58 -58
  42. package/lib/core/presets.js +42 -42
  43. package/lib/core/settings.js +274 -274
  44. package/lib/core/source.js +89 -89
  45. package/lib/core/video/audio.js +159 -0
  46. package/lib/core/video/frames.js +38 -0
  47. package/lib/core/video/mux-mp4.js +213 -24
  48. package/lib/core/video/mux-webm.js +63 -22
  49. package/lib/editor/controls.js +139 -139
  50. package/lib/editor/dom.js +65 -65
  51. package/lib/editor/exporter.js +43 -3
  52. package/lib/editor/icons.js +4 -0
  53. package/lib/editor/index.js +218 -9
  54. package/lib/editor/inspector.js +19 -4
  55. package/lib/editor/library.js +8 -2
  56. package/lib/editor/persist.js +18 -18
  57. package/lib/editor/samples.js +90 -90
  58. package/lib/editor/stage.js +239 -239
  59. package/lib/editor/store.js +80 -80
  60. package/lib/editor/styles/editor.css +34 -1
  61. package/lib/editor/styles/fonts/README.md +6 -6
  62. package/lib/editor/styles/fonts/geist-LICENSE.txt +93 -93
  63. package/lib/editor/styles/tokens.css +89 -89
  64. package/lib/editor/ui.js +97 -97
  65. package/lib/editor/workspace.js +62 -0
  66. package/lib/index.js +31 -29
  67. package/lib/video.js +37 -7
  68. package/package.json +1 -1
  69. package/types/editor.d.ts +34 -34
  70. package/types/index.d.ts +196 -196
  71. package/types/video.d.ts +61 -5
@@ -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,29 +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
- export const VERSION = '3.0.0';
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,32 +1,51 @@
1
- // image-to-pixel/video: encode processed frames to MP4 (H.264) or WebM (VP9/VP8) with WebCodecs.
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).
2
3
  // The muxers are this project's own (lib/core/video/mux-*.js): MIT, no dependencies.
3
4
 
4
5
  import { createMp4Muxer } from './core/video/mux-mp4.js';
5
6
  import { createWebmMuxer } from './core/video/mux-webm.js';
7
+ import { decodeAudio, encodeAudio, audioCodecSupported, findAudioConfig } from './core/video/audio.js';
6
8
  import { upscale } from './core/export/scale.js';
7
9
 
8
10
  export { forEachFrame, estimateFps, seekTo, frameTimes, videoReady } from './core/video/frames.js';
9
- export { createMp4Muxer, createWebmMuxer };
11
+ export { createMp4Muxer, createWebmMuxer, decodeAudio, encodeAudio, audioCodecSupported, findAudioConfig };
10
12
 
11
13
  const AVC = ['avc1.640033', 'avc1.4d0033', 'avc1.42003e', 'avc1.640028', 'avc1.42001f'];
12
14
  const VP = [['vp09.00.10.08', 'vp9'], ['vp8', 'vp8']];
13
15
 
14
- /** Which formats this browser can encode. */
16
+ /** Which formats this browser can encode. `aac` / `opus`: audio for MP4 (either) and WebM (Opus). */
15
17
  export async function videoSupport(width = 640, height = 480) {
16
- const out = { mp4: false, webm: false, webcodecs: typeof VideoEncoder !== 'undefined' };
18
+ const out = { mp4: false, webm: false, webcodecs: typeof VideoEncoder !== 'undefined', aac: false, opus: false };
17
19
  if (out.webcodecs) {
18
20
  for (const codec of AVC) if ((await VideoEncoder.isConfigSupported({ codec, width, height }).catch(() => ({}))).supported) { out.mp4 = true; break; }
19
21
  for (const [codec] of VP) if ((await VideoEncoder.isConfigSupported({ codec, width, height }).catch(() => ({}))).supported) { out.webm = true; break; }
20
22
  }
23
+ out.aac = await audioCodecSupported('aac');
24
+ out.opus = await audioCodecSupported('opus');
21
25
  return out;
22
26
  }
23
27
 
24
28
  const even = n => n + (n & 1);
25
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
+
26
40
  /**
27
41
  * 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
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
29
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`).
30
49
  */
31
50
  export async function createVideoEncoder(opts) {
32
51
  const scale = Math.max(1, Math.round(opts.scale || 1));
@@ -43,9 +62,14 @@ export async function createVideoEncoder(opts) {
43
62
  }
44
63
  if (!codec) throw new Error(`No ${opts.format.toUpperCase()} encoder available in this browser at ${W}×${H}.`);
45
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
+
46
70
  const muxer = opts.format === 'mp4'
47
- ? createMp4Muxer({ width: W, height: H })
48
- : createWebmMuxer({ width: W, height: H, codec: muxCodec, fps });
71
+ ? createMp4Muxer({ width: W, height: H, audio: audioTrack })
72
+ : createWebmMuxer({ width: W, height: H, codec: muxCodec, fps, audio: audioTrack });
49
73
 
50
74
  let error = null;
51
75
  const encoder = new VideoEncoder({
@@ -62,6 +86,8 @@ export async function createVideoEncoder(opts) {
62
86
 
63
87
  return {
64
88
  width: W, height: H, codec,
89
+ /** 'aac' or 'opus' when the soundtrack will be included, null otherwise. */
90
+ audioCodec,
65
91
  /** @param {{ width, height, data }} img */
66
92
  async add(img) {
67
93
  if (error) throw error;
@@ -79,6 +105,10 @@ export async function createVideoEncoder(opts) {
79
105
  await encoder.flush();
80
106
  if (error) throw error;
81
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
+ }
82
112
  return new Blob([muxer.finish()], { type: opts.format === 'mp4' ? 'video/mp4' : 'video/webm' });
83
113
  },
84
114
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "image-to-pixel",
3
- "version": "3.0.0",
3
+ "version": "3.1.0",
4
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
5
  "type": "module",
6
6
  "license": "MIT",
package/types/editor.d.ts CHANGED
@@ -1,34 +1,34 @@
1
- // image-to-pixel/editor
2
- import type { Settings, SettingsInput, PixelResult, Source } from './index';
3
-
4
- export interface EditorOptions {
5
- settings?: SettingsInput;
6
- /** Remember settings, theme and palettes in localStorage (default true). */
7
- persist?: boolean;
8
- /** Show sample images in the empty state (default true). */
9
- samples?: boolean;
10
- /** Turn the logo into a link. */
11
- homeHref?: string;
12
- }
13
- export interface EditorStore {
14
- readonly settings: Settings;
15
- update(patch: SettingsInput, opts?: { key?: string; history?: boolean }): void;
16
- replace(settings: SettingsInput): void;
17
- subscribe(fn: (settings: Settings, why: string) => void): () => void;
18
- undo(): boolean;
19
- redo(): boolean;
20
- readonly canUndo: boolean;
21
- readonly canRedo: boolean;
22
- }
23
- export interface Editor {
24
- store: EditorStore;
25
- addFiles(files: File[]): Promise<void>;
26
- addImage(source: Source, name?: string): Promise<unknown>;
27
- openSample(id: 'dusk' | 'still-life' | 'potion'): Promise<void>;
28
- applyPreset(id: string): void;
29
- setTheme(theme: 'dark' | 'light'): void;
30
- readonly result: PixelResult | null;
31
- idle(): Promise<void>;
32
- }
33
- export function mountEditor(root: HTMLElement, options?: EditorOptions): Promise<Editor>;
34
- export const SAMPLES: { id: string; name: string; svg: string }[];
1
+ // image-to-pixel/editor
2
+ import type { Settings, SettingsInput, PixelResult, Source } from './index';
3
+
4
+ export interface EditorOptions {
5
+ settings?: SettingsInput;
6
+ /** Remember settings, theme and palettes in localStorage (default true). */
7
+ persist?: boolean;
8
+ /** Show sample images in the empty state (default true). */
9
+ samples?: boolean;
10
+ /** Turn the logo into a link. */
11
+ homeHref?: string;
12
+ }
13
+ export interface EditorStore {
14
+ readonly settings: Settings;
15
+ update(patch: SettingsInput, opts?: { key?: string; history?: boolean }): void;
16
+ replace(settings: SettingsInput): void;
17
+ subscribe(fn: (settings: Settings, why: string) => void): () => void;
18
+ undo(): boolean;
19
+ redo(): boolean;
20
+ readonly canUndo: boolean;
21
+ readonly canRedo: boolean;
22
+ }
23
+ export interface Editor {
24
+ store: EditorStore;
25
+ addFiles(files: File[]): Promise<void>;
26
+ addImage(source: Source, name?: string): Promise<unknown>;
27
+ openSample(id: 'dusk' | 'still-life' | 'potion'): Promise<void>;
28
+ applyPreset(id: string): void;
29
+ setTheme(theme: 'dark' | 'light'): void;
30
+ readonly result: PixelResult | null;
31
+ idle(): Promise<void>;
32
+ }
33
+ export function mountEditor(root: HTMLElement, options?: EditorOptions): Promise<Editor>;
34
+ export const SAMPLES: { id: string; name: string; svg: string }[];