fragment-tools 0.2.11 → 0.2.13

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 (67) hide show
  1. package/package.json +12 -11
  2. package/src/cli/build.js +1 -0
  3. package/src/cli/create.js +22 -4
  4. package/src/cli/createConfig.js +2 -2
  5. package/src/cli/getEntries.js +10 -1
  6. package/src/cli/plugins/hot-shader-replacement.js +54 -16
  7. package/src/cli/plugins/save.js +97 -38
  8. package/src/cli/prompts.js +89 -36
  9. package/src/cli/run.js +1 -1
  10. package/src/cli/templates/blank/index.ts +1 -1
  11. package/src/cli/templates/default/index.js +10 -2
  12. package/src/cli/templates/default/index.ts +5 -2
  13. package/src/cli/templates/fragment-gl/index.ts +1 -1
  14. package/src/cli/templates/p5/index.ts +1 -1
  15. package/src/cli/templates/p5-webgl/index.ts +1 -1
  16. package/src/cli/templates/three-fragment/index.js +5 -3
  17. package/src/cli/templates/three-fragment/index.ts +5 -4
  18. package/src/cli/templates/three-orthographic/index.js +6 -1
  19. package/src/cli/templates/three-orthographic/index.ts +6 -2
  20. package/src/cli/templates/three-perspective/index.js +6 -1
  21. package/src/cli/templates/three-perspective/index.ts +6 -2
  22. package/src/client/app/actions/resize.js +8 -1
  23. package/src/client/app/attachments/draggable.js +93 -0
  24. package/src/client/app/client.js +90 -18
  25. package/src/client/app/components/IconFlip.svelte +46 -0
  26. package/src/client/app/hooks.js +25 -1
  27. package/src/client/app/lib/canvas-recorder/CanvasRecorder.js +95 -3
  28. package/src/client/app/lib/canvas-recorder/FrameRecorder.js +45 -3
  29. package/src/client/app/lib/canvas-recorder/GIFRecorder.js +72 -13
  30. package/src/client/app/lib/canvas-recorder/MediaBunnyRecorder.js +43 -9
  31. package/src/client/app/lib/canvas-recorder/utils.js +18 -9
  32. package/src/client/app/modules/Params.svelte +1 -0
  33. package/src/client/app/renderers/2DRenderer.js +20 -16
  34. package/src/client/app/renderers/P5GLRenderer.js +13 -5
  35. package/src/client/app/renderers/P5Renderer.js +9 -1
  36. package/src/client/app/renderers/THREERenderer.js +63 -48
  37. package/src/client/app/state/Sketch.svelte.js +150 -10
  38. package/src/client/app/state/errors.svelte.js +19 -0
  39. package/src/client/app/state/exports.svelte.js +14 -1
  40. package/src/client/app/state/rendering.svelte.js +90 -13
  41. package/src/client/app/state/sketches.svelte.js +43 -7
  42. package/src/client/app/state/utils.svelte.js +49 -0
  43. package/src/client/app/ui/Field.svelte +63 -16
  44. package/src/client/app/ui/FieldSection.svelte +4 -4
  45. package/src/client/app/ui/ParamsOutput.svelte +7 -5
  46. package/src/client/app/ui/SketchRenderer.svelte +21 -0
  47. package/src/client/app/ui/fields/ButtonInput.svelte +2 -0
  48. package/src/client/app/ui/fields/CheckboxInput.svelte +13 -11
  49. package/src/client/app/ui/fields/ColorInput.svelte +16 -11
  50. package/src/client/app/ui/fields/GradientInput.svelte +607 -0
  51. package/src/client/app/ui/fields/Input.svelte +10 -6
  52. package/src/client/app/ui/fields/IntervalInput.svelte +27 -35
  53. package/src/client/app/ui/fields/NumberInput.svelte +51 -13
  54. package/src/client/app/ui/fields/PaletteInput.svelte +181 -0
  55. package/src/client/app/ui/fields/ProgressInput.svelte +44 -16
  56. package/src/client/app/ui/fields/TextareaInput.svelte +93 -0
  57. package/src/client/app/utils/canvas.utils.js +105 -28
  58. package/src/client/app/utils/color.utils.js +74 -17
  59. package/src/client/app/utils/fields.utils.js +70 -17
  60. package/src/client/app/utils/file.utils.js +86 -31
  61. package/src/client/app/utils/glsl.utils.js +11 -2
  62. package/src/client/app/utils/glslErrors.js +31 -21
  63. package/src/client/app/utils/index.js +28 -12
  64. package/src/client/main.js +7 -1
  65. package/src/types/global.d.ts +143 -0
  66. package/src/types/props.d.ts +41 -15
  67. package/tsconfig.json +1 -1
@@ -1,9 +1,53 @@
1
+ /**
2
+ * @typedef {Object} MessagePayload
3
+ * @property {string} event - The event name
4
+ * @property {any} [data] - Optional event data
5
+ */
6
+
7
+ /**
8
+ * @typedef {Object} ShaderWarning
9
+ * @property {string} type - Warning type
10
+ * @property {string} importer - File that imported the shader
11
+ * @property {string} message - Warning message
12
+ * @property {Object} location - Location of the warning
13
+ * @property {string} location.lineText - The line of code with the warning
14
+ */
15
+
16
+ /**
17
+ * @typedef {Object} ShaderUpdate
18
+ * @property {ShaderWarning[]} [warnings] - Array of shader warnings
19
+ */
20
+
21
+ /**
22
+ * @typedef {Object} SketchUpdate
23
+ * @property {string} filepath
24
+ */
25
+
26
+ /**
27
+ * @callback EventCallback
28
+ * @param {any} data - Event data
29
+ * @returns {void}
30
+ */
31
+
32
+ /**
33
+ * @callback UnsubscribeFunction
34
+ * @returns {void}
35
+ */
36
+
1
37
  const socketProtocol = location.protocol === 'https:' ? 'wss' : 'ws';
2
38
  const socketHost = `${location.hostname}:${__FRAGMENT_PORT__}`;
3
39
 
4
- let socket,
5
- listeners = {};
40
+ /** @type {WebSocket | undefined} */
41
+ let socket;
42
+ /** @type {Record<string, EventCallback[]>} */
43
+ let listeners = {};
44
+ let opened = false;
6
45
 
46
+ /**
47
+ * Handle incoming WebSocket message
48
+ * @param {MessagePayload} payload - The message payload
49
+ * @returns {void}
50
+ */
7
51
  function handleMessage(payload) {
8
52
  const { event, data = {} } = payload;
9
53
  const callbacks = listeners[event];
@@ -13,6 +57,12 @@ function handleMessage(payload) {
13
57
  }
14
58
  }
15
59
 
60
+ /**
61
+ * Subscribe to an event
62
+ * @param {string} event - The event name to listen for
63
+ * @param {EventCallback} cb - The callback function
64
+ * @returns {UnsubscribeFunction} Function to unsubscribe
65
+ */
16
66
  function on(event, cb) {
17
67
  if (!listeners[event]) {
18
68
  listeners[event] = [];
@@ -25,6 +75,12 @@ function on(event, cb) {
25
75
  };
26
76
  }
27
77
 
78
+ /**
79
+ * Unsubscribe from an event
80
+ * @param {string} event - The event name
81
+ * @param {EventCallback} cb - The callback function to remove
82
+ * @returns {void}
83
+ */
28
84
  function off(event, cb) {
29
85
  const callbacks = listeners[event];
30
86
 
@@ -35,9 +91,14 @@ function off(event, cb) {
35
91
  }
36
92
  }
37
93
 
38
- let opened = false;
94
+ /**
95
+ * Emit an event to the server
96
+ * @param {string} event - The event name
97
+ * @param {any} data - The data to send
98
+ * @returns {void}
99
+ */
39
100
  function emit(event, data) {
40
- if (opened) {
101
+ if (socket && opened) {
41
102
  socket.send(
42
103
  JSON.stringify({
43
104
  event,
@@ -52,7 +113,7 @@ if (import.meta.hot) {
52
113
 
53
114
  socket = new WebSocket(`${socketProtocol}://${socketHost}`);
54
115
 
55
- socket.addEventListener('message', async (message) => {
116
+ socket.addEventListener('message', (message) => {
56
117
  const { data } = message;
57
118
 
58
119
  handleMessage(JSON.parse(data));
@@ -63,22 +124,33 @@ if (import.meta.hot) {
63
124
  opened = true;
64
125
  });
65
126
 
66
- import.meta.hot.on('sketch-update', (data) => {
67
- console.log(`[fragment] hmr update /${data.filepath}`);
68
- });
127
+ import.meta.hot.on(
128
+ 'sketch-update',
129
+ /** @param {SketchUpdate} sketchUpdate */
130
+ (sketchUpdate) => {
131
+ console.log(`[fragment] hmr update /${sketchUpdate.filepath}`);
132
+ },
133
+ );
69
134
  }
70
135
 
136
+ /**
137
+ * Client API for WebSocket communication
138
+ * @type {{ on: typeof on, off: typeof off, emit: typeof emit }}
139
+ */
71
140
  export const client = { on, off, emit };
72
141
 
73
142
  client.on('shader-update', (shaderUpdates) => {
74
- shaderUpdates.forEach(({ warnings = [] } = {}) => {
75
- if (warnings.length > 0) {
76
- warnings.forEach((warning) => {
77
- const { location } = warning;
78
- console.warn(
79
- `[fragment-plugin-hsr] ${warning.type} ${warning.importer}\n\n ${location.lineText}\n\n${warning.message}`,
80
- );
81
- });
82
- }
83
- });
143
+ shaderUpdates.forEach(
144
+ /** @param {ShaderUpdate} shaderUpdate */
145
+ ({ warnings = [] } = {}) => {
146
+ if (warnings.length > 0) {
147
+ warnings.forEach((warning) => {
148
+ const { location } = warning;
149
+ console.warn(
150
+ `[fragment-plugin-hsr] ${warning.type} ${warning.importer}\n\n ${location.lineText}\n\n${warning.message}`,
151
+ );
152
+ });
153
+ }
154
+ },
155
+ );
84
156
  });
@@ -0,0 +1,46 @@
1
+ <script>
2
+ let { angle = 0 } = $props();
3
+ </script>
4
+
5
+ <svg
6
+ width="16"
7
+ height="16"
8
+ fill="none"
9
+ viewBox="0 0 24 24"
10
+ style="--angle: {angle}"
11
+ >
12
+ <path
13
+ stroke="currentColor"
14
+ stroke-linecap="round"
15
+ stroke-linejoin="round"
16
+ stroke-width="1.5"
17
+ d="M18 8H6"
18
+ ></path>
19
+ <path
20
+ stroke="currentColor"
21
+ stroke-linecap="round"
22
+ stroke-linejoin="round"
23
+ stroke-width="1.5"
24
+ d="M10 5L6 8L10 11"
25
+ ></path>
26
+ <path
27
+ stroke="currentColor"
28
+ stroke-linecap="round"
29
+ stroke-linejoin="round"
30
+ stroke-width="1.5"
31
+ d="M6 16H18"
32
+ ></path>
33
+ <path
34
+ stroke="currentColor"
35
+ stroke-linecap="round"
36
+ stroke-linejoin="round"
37
+ stroke-width="1.5"
38
+ d="M14 13L18 16L14 19"
39
+ ></path>
40
+ </svg>
41
+
42
+ <style>
43
+ svg {
44
+ transform: rotate(var(--angle));
45
+ }
46
+ </style>
@@ -2,18 +2,42 @@ import { rendering } from './state/rendering.svelte';
2
2
  import { sketchesManager } from './state/sketches.svelte';
3
3
  import { getContext } from './triggers/shared';
4
4
 
5
+ /**
6
+ * Register a callback to be called before capturing
7
+ * @param {Function} listener - The callback function to execute before capture
8
+ * @param {string} [context] - The sketch context (defaults to current context)
9
+ * @returns {void}
10
+ */
5
11
  export const onBeforeCapture = (listener, context = getContext()) => {
6
12
  sketchesManager.sketches[context]?.onBeforeCapture(listener);
7
13
  };
8
14
 
15
+ /**
16
+ * Register a callback to be called after capturing
17
+ * @param {Function} listener - The callback function to execute after capture
18
+ * @param {string} [context] - The sketch context (defaults to current context)
19
+ * @returns {void}
20
+ */
9
21
  export const onAfterCapture = (listener, context = getContext()) => {
10
22
  sketchesManager.sketches[context]?.onAfterCapture(listener);
11
23
  };
12
24
 
25
+ /**
26
+ * Register a callback to be called before recording
27
+ * @param {Function} listener - The callback function to execute before recording
28
+ * @param {string} [context] - The sketch context (defaults to current context)
29
+ * @returns {void}
30
+ */
13
31
  export const onBeforeRecord = (listener, context = getContext()) => {
14
32
  sketchesManager.sketches[context]?.onBeforeRecord(listener);
15
33
  };
16
34
 
35
+ /**
36
+ * Register a callback to be called after recording
37
+ * @param {Function} listener - The callback function to execute after recording
38
+ * @param {string} [context] - The sketch context (defaults to current context)
39
+ * @returns {void}
40
+ */
17
41
  export const onAfterRecord = (listener, context = getContext()) => {
18
42
  sketchesManager.sketches[context]?.onAfterRecord(listener);
19
43
  };
@@ -23,7 +47,7 @@ export const onAfterRecord = (listener, context = getContext()) => {
23
47
  * @param {object} options
24
48
  * @param {string} [options.filename]
25
49
  * @param {function} [options.pattern]
26
- * @param {exportDir} [options.pattern]
50
+ * @param {string} [options.exportDir]
27
51
  */
28
52
  export async function screenshot({ filename, pattern, exportDir } = {}) {
29
53
  const context = getContext();
@@ -1,6 +1,47 @@
1
+ /**
2
+ * @callback CanvasRecorderStartCallback
3
+ * @returns {void}
4
+ */
5
+
6
+ /**
7
+ * @callback CanvasRecorderTickCallback
8
+ * @param {TickData} data - Tick data
9
+ * @returns {void}
10
+ */
11
+
12
+ /**
13
+ * @callback CanvasRecorderCompleteCallback
14
+ * @param {Blob | Blob[] | null} result
15
+ * @returns {void}
16
+ */
17
+
18
+ /**
19
+ * @typedef {Object} TickData
20
+ * @property {number} time - Current time in milliseconds
21
+ * @property {number} deltaTime - Time since last frame in milliseconds
22
+ * @property {number} frameCount - Current frame count
23
+ */
24
+
25
+ /**
26
+ * @typedef {Object} CanvasRecorderOptions
27
+ * @property {number} [duration=Infinity] - Recording duration in seconds
28
+ * @property {number} [framerate=25] - Frames per second
29
+ * @property {number} [quality=100] - Recording quality (1-100)
30
+ * @property {string} format - Output format
31
+ * @property {CanvasRecorderStartCallback} [onStart] - Callback when recording starts
32
+ * @property {CanvasRecorderTickCallback} [onTick] - Callback on each frame
33
+ * @property {CanvasRecorderCompleteCallback} [onComplete] - Callback when recording completes
34
+ */
35
+
36
+ /** @type {CanvasRecorderStartCallback} */
1
37
  let noop = () => {};
2
38
 
3
39
  class CanvasRecorder {
40
+ /**
41
+ * Create a canvas recorder
42
+ * @param {HTMLCanvasElement} canvas - The canvas to record
43
+ * @param {CanvasRecorderOptions} options - Recording options
44
+ */
4
45
  constructor(
5
46
  canvas,
6
47
  {
@@ -13,30 +54,62 @@ class CanvasRecorder {
13
54
  onComplete = noop,
14
55
  },
15
56
  ) {
57
+ /** @type {HTMLCanvasElement} */
16
58
  this.canvas = canvas;
59
+ /** @type {number} */
17
60
  this.framerate = framerate;
61
+ /** @type {number} */
18
62
  this.duration = duration;
63
+ /** @type {number} */
19
64
  this.quality = quality;
65
+ /** @type {string} */
20
66
  this.format = format;
67
+ /** @type {CanvasRecorderStartCallback} */
21
68
  this.onStart = onStart;
69
+ /** @type {CanvasRecorderTickCallback} */
22
70
  this.onTick = onTick;
71
+ /** @type {Function} */
23
72
  this.onComplete = onComplete;
24
-
73
+ /** @type {number} */
25
74
  this.time = 0;
75
+
76
+ /** @type {number} */
26
77
  this.deltaTime = 1000 / this.framerate;
27
78
 
79
+ /** @type {number} */
28
80
  this.frameDuration = 1000 / this.framerate;
81
+
82
+ /** @type {number} */
29
83
  this.frameTotal = isFinite(this.duration)
30
84
  ? this.duration * this.framerate
31
85
  : Infinity;
86
+
87
+ /** @type {boolean} */
32
88
  this.started = false;
89
+
90
+ /** @type {boolean} */
33
91
  this.stopped = false;
34
92
 
93
+ /** @type {number} */
35
94
  this.startTime = 0;
95
+
96
+ /** @type {number} */
97
+ this.frameCount = 0;
98
+
99
+ /** @type {Blob | Blob[] | null} */
100
+ this.result = null;
36
101
  }
37
102
 
103
+ /**
104
+ * Load resources before recording (override in subclass)
105
+ * @returns {Promise<void>}
106
+ */
38
107
  async load() {}
39
108
 
109
+ /**
110
+ * Start the recording
111
+ * @returns {Promise<void>}
112
+ */
40
113
  async start() {
41
114
  this.startTime = performance.now();
42
115
  this.onStart();
@@ -65,11 +138,17 @@ class CanvasRecorder {
65
138
  this._tick();
66
139
  }
67
140
 
141
+ /**
142
+ * Internal tick handler
143
+ * @private
144
+ * @returns {Promise<void>}
145
+ */
68
146
  async _tick() {
69
147
  console.log(`CanvasRecorder - render frame ${this.frameCount + 1}`);
70
148
  this.onTick({
71
149
  time: this.time,
72
150
  deltaTime: this.deltaTime,
151
+ frameCount: this.frameCount,
73
152
  });
74
153
 
75
154
  await this.tick({
@@ -98,8 +177,17 @@ class CanvasRecorder {
98
177
  }
99
178
  }
100
179
 
101
- tick() {}
102
-
180
+ /**
181
+ * Process a single frame (override in subclass)
182
+ * @param {TickData} _data - Frame data
183
+ * @returns {Promise<void>}
184
+ */
185
+ async tick(_data) {}
186
+
187
+ /**
188
+ * End the recording and compile result
189
+ * @returns {void}
190
+ */
103
191
  end() {
104
192
  console.log(
105
193
  `CanvasRecorder - compiled ${this.frameCount + 1} frames in ${(performance.now() - this.startTime) / 1000}s`,
@@ -107,6 +195,10 @@ class CanvasRecorder {
107
195
  this.onComplete(this.result);
108
196
  }
109
197
 
198
+ /**
199
+ * Stop the recording
200
+ * @returns {void}
201
+ */
110
202
  stop() {
111
203
  this.stopped = true;
112
204
  }
@@ -3,24 +3,62 @@ import { map } from '../../utils/math.utils';
3
3
  import CanvasRecorder from './CanvasRecorder';
4
4
  import { exportCanvas } from './utils';
5
5
 
6
+ /**
7
+ * @typedef {Object} FrameRecorderOptions
8
+ * @property {string} [imageEncoding='png'] - Image encoding format (png, jpeg, webp)
9
+ * @property {number} [duration] - Recording duration in seconds
10
+ * @property {number} [framerate] - Frames per second
11
+ * @property {number} [quality] - Recording quality (1-100)
12
+ * @property {string} format - Output format
13
+ * @property {import('./CanvasRecorder').CanvasRecorderStartCallback} [onStart] - Callback when recording starts
14
+ * @property {import('./CanvasRecorder').CanvasRecorderTickCallback} [onTick] - Callback on each frame
15
+ * @property {import('./CanvasRecorder').CanvasRecorderCompleteCallback} [onComplete] - Callback when recording completes
16
+ */
17
+
18
+ /**
19
+ * Recorder that captures individual frames as images
20
+ * @extends CanvasRecorder
21
+ */
6
22
  class FrameRecorder extends CanvasRecorder {
23
+ /**
24
+ * Create a frame recorder
25
+ * @param {HTMLCanvasElement} canvas - The canvas to record
26
+ * @param {FrameRecorderOptions} options - Recording options
27
+ */
7
28
  constructor(canvas, options) {
8
29
  super(canvas, options);
9
30
 
10
31
  const { imageEncoding = 'png' } = options;
11
32
 
33
+ /** @type {string} */
12
34
  this.imageEncoding = imageEncoding;
13
35
 
36
+ /** @type {number} */
14
37
  this.imageQuality = map(this.quality, 1, 100, 0, 1);
38
+
39
+ /** @type {string[]} */
40
+ this.frames = [];
41
+
42
+ /** @type {Blob[]} */
43
+ this.result = [];
15
44
  }
16
45
 
17
- start() {
46
+ /**
47
+ * Start recording frames
48
+ * @returns {Promise<void>}
49
+ */
50
+ async start() {
18
51
  this.frames = [];
19
52
 
20
- super.start();
53
+ await super.start();
21
54
  }
22
55
 
23
- tick() {
56
+ /**
57
+ * Capture a single frame
58
+ * @param {import('./CanvasRecorder').TickData} _data - Frame data (unused)
59
+ * @returns {Promise<void>}
60
+ */
61
+ async tick(_data) {
24
62
  let { dataURL } = exportCanvas(this.canvas, {
25
63
  encoding: `image/${this.imageEncoding}`,
26
64
  encodingQuality: this.imageQuality,
@@ -29,6 +67,10 @@ class FrameRecorder extends CanvasRecorder {
29
67
  this.frames[this.frameCount] = dataURL;
30
68
  }
31
69
 
70
+ /**
71
+ * End recording and convert frames to blobs
72
+ * @returns {Promise<void>}
73
+ */
32
74
  async end() {
33
75
  this.result = await Promise.all(
34
76
  this.frames.map((dataURL) => createBlobFromDataURL(dataURL)),
@@ -2,8 +2,41 @@ import { map } from '../../utils/math.utils';
2
2
  import { GIFEncoder, quantize, applyPalette } from 'gifenc';
3
3
  import CanvasRecorder from './CanvasRecorder';
4
4
 
5
+ /**
6
+ * @typedef {import('./CanvasRecorder').CanvasRecorderOptions} GIFRecorderOptions
7
+ */
8
+
9
+ /**
10
+ * Recorder that captures frames and encodes them as an animated GIF
11
+ * @extends CanvasRecorder
12
+ */
5
13
  class GIFRecorder extends CanvasRecorder {
6
- start() {
14
+ /**
15
+ * Create a GIF recorder
16
+ * @param {HTMLCanvasElement} canvas - The canvas to record
17
+ * @param {GIFRecorderOptions} options - Recording options
18
+ */
19
+ constructor(canvas, options) {
20
+ super(canvas, options);
21
+
22
+ /** @type {ReturnType<typeof GIFEncoder> | null} */
23
+ this.encoder = null;
24
+
25
+ /** @type {HTMLCanvasElement} */
26
+ this.tmpCanvas = document.createElement('canvas');
27
+
28
+ /** @type {CanvasRenderingContext2D | null} */
29
+ this.tmpContext = this.tmpCanvas.getContext('2d');
30
+
31
+ /** @type {number} */
32
+ this.maxColors = 256;
33
+ }
34
+
35
+ /**
36
+ * Start GIF recording
37
+ * @returns {Promise<void>}
38
+ */
39
+ async start() {
7
40
  this.encoder = GIFEncoder();
8
41
 
9
42
  this.tmpCanvas = document.createElement('canvas');
@@ -21,34 +54,60 @@ class GIFRecorder extends CanvasRecorder {
21
54
  : Infinity;
22
55
  }
23
56
 
24
- super.start();
57
+ await super.start();
25
58
  }
26
59
 
60
+ /**
61
+ * Get RGBA pixel data from a bitmap
62
+ * @param {HTMLCanvasElement | ImageBitmap} bitmap - The bitmap to extract pixels from
63
+ * @param {number} [width=bitmap.width] - Target width
64
+ * @param {number} [height=bitmap.height] - Target height
65
+ * @returns {Uint8ClampedArray} RGBA pixel data
66
+ */
27
67
  getBitmapRGBA(bitmap, width = bitmap.width, height = bitmap.height) {
28
68
  this.tmpCanvas.width = width;
29
69
  this.tmpCanvas.height = height;
30
- this.tmpContext.clearRect(0, 0, width, height);
31
- this.tmpContext.drawImage(bitmap, 0, 0, width, height);
32
- return this.tmpContext.getImageData(0, 0, width, height).data;
70
+
71
+ if (this.tmpContext) {
72
+ this.tmpContext.clearRect(0, 0, width, height);
73
+ this.tmpContext.drawImage(bitmap, 0, 0, width, height);
74
+ return this.tmpContext.getImageData(0, 0, width, height).data;
75
+ }
76
+
77
+ return new Uint8ClampedArray();
33
78
  }
34
79
 
35
- tick() {
80
+ /**
81
+ * Capture and encode a single frame
82
+ * @param {import('./CanvasRecorder').TickData} _data - Frame data (unused)
83
+ * @returns {Promise<void>}
84
+ */
85
+ async tick(_data) {
36
86
  const { width, height } = this.canvas;
37
87
 
38
88
  const pixels = this.getBitmapRGBA(this.canvas, width, height);
39
89
  const palette = quantize(pixels, this.maxColors);
40
90
  const index = applyPalette(pixels, palette);
41
91
 
42
- this.encoder.writeFrame(index, width, height, {
43
- palette: palette,
44
- delay: this.frameDuration,
45
- });
92
+ if (this.encoder) {
93
+ this.encoder.writeFrame(index, width, height, {
94
+ palette: palette,
95
+ delay: this.frameDuration,
96
+ });
97
+ }
46
98
  }
47
99
 
100
+ /**
101
+ * End recording and create GIF blob
102
+ * @returns {void}
103
+ */
48
104
  end() {
49
- this.encoder.finish();
50
-
51
- this.result = new Blob([this.encoder.bytes()], { type: 'image/gif' });
105
+ if (this.encoder) {
106
+ this.encoder.finish();
107
+ this.result = new Blob([this.encoder.bytes()], {
108
+ type: 'image/gif',
109
+ });
110
+ }
52
111
 
53
112
  super.end();
54
113
  }
@@ -4,7 +4,6 @@ import {
4
4
  Mp4OutputFormat,
5
5
  MkvOutputFormat,
6
6
  MovOutputFormat,
7
- WebMInputFormat,
8
7
  BufferTarget,
9
8
  CanvasSource,
10
9
  Quality,
@@ -18,6 +17,26 @@ import {
18
17
  import { map } from '@fragment/utils/math.utils.js';
19
18
  import { VIDEO_FORMATS } from '@fragment/state/exports.svelte.js';
20
19
 
20
+ /**
21
+ * @typedef {'avc' | 'hevc' | 'vp9' | 'av1' | 'vp8'} VideoCodec
22
+ */
23
+
24
+ /**
25
+ * @typedef {Object} MediaBunnyRecorderOptions
26
+ * @property {VideoCodec} codec - Video codec to use
27
+ * @property {number} [duration] - Recording duration in seconds
28
+ * @property {number} [framerate] - Frames per second
29
+ * @property {number} [quality] - Recording quality (1-100)
30
+ * @property {string} format - Output format
31
+ * @property {import('./CanvasRecorder').CanvasRecorderStartCallback} [onStart] - Callback when recording starts
32
+ * @property {import('./CanvasRecorder').CanvasRecorderTickCallback} [onTick] - Callback on each frame
33
+ * @property {import('./CanvasRecorder').CanvasRecorderCompleteCallback} [onComplete] - Callback when recording completes
34
+ */
35
+
36
+ /**
37
+ * Recorder that uses MediaBunny to encode video
38
+ * @extends CanvasRecorder
39
+ */
21
40
  class MediaBunnyRecorder extends CanvasRecorder {
22
41
  /** @type Quality[] */
23
42
  static BITRATES = [
@@ -29,15 +48,14 @@ class MediaBunnyRecorder extends CanvasRecorder {
29
48
  ];
30
49
 
31
50
  /**
32
- *
33
- * @param {HTMLCanvasElement} canvas
34
- * @param {object} options
35
- * @param {codec} options.string
51
+ * Create a MediaBunny recorder
52
+ * @param {HTMLCanvasElement} canvas - The canvas to record
53
+ * @param {MediaBunnyRecorderOptions} options - Recording options
36
54
  */
37
55
  constructor(canvas, { codec, ...options }) {
38
56
  super(canvas, options);
39
57
 
40
- /** @type {string} */
58
+ /** @type {VideoCodec} */
41
59
  this.codec = codec;
42
60
 
43
61
  const outputFormats = new Map();
@@ -72,23 +90,39 @@ class MediaBunnyRecorder extends CanvasRecorder {
72
90
  });
73
91
  }
74
92
 
93
+ /**
94
+ * Load and start the output
95
+ * @returns {Promise<void>}
96
+ */
75
97
  async load() {
76
98
  await this.output.start();
77
99
  }
78
100
 
79
- async tick({ frameCount, time }) {
101
+ /**
102
+ * Process a single frame
103
+ * @param {import('./CanvasRecorder').TickData} tickData - Frame data
104
+ * @returns {Promise<void>}
105
+ */
106
+ async tick({ frameCount }) {
80
107
  const timestamp = frameCount / this.framerate;
81
108
 
82
109
  this.videoSource.add(timestamp, this.frameDuration / 1000);
83
110
  }
84
111
 
112
+ /**
113
+ * End recording and create video blob
114
+ * @returns {Promise<void>}
115
+ */
85
116
  async end() {
86
117
  await this.output.finalize();
87
118
 
88
119
  const { mimeType } = this.output.format;
89
- const { buffer } = this.output.target;
120
+ const target = /** @type {BufferTarget} */ (this.output.target);
121
+ const buffer = target.buffer;
90
122
 
91
- this.result = new Blob([buffer], { type: mimeType });
123
+ if (buffer) {
124
+ this.result = new Blob([buffer], { type: mimeType });
125
+ }
92
126
 
93
127
  super.end();
94
128
  }