pixel-data-js 0.29.0 → 0.31.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pixel-data-js",
3
3
  "type": "module",
4
- "version": "0.29.0",
4
+ "version": "0.31.0",
5
5
  "packageManager": "pnpm@10.33.0",
6
6
  "description": "JS Pixel and ImageData operations",
7
7
  "author": {
@@ -0,0 +1,76 @@
1
+ export type BatchedQueueFn = (fn: () => void) => void
2
+
3
+ export type BatchedQueue = ReturnType<typeof makeBatchedQueue>
4
+
5
+ /**
6
+ * Creates a high-performance, zero-allocation batching queue.
7
+ * This utility collects items marked as "dirty" and flushes them in a single batch.
8
+ * * **⚠️ CRITICAL: Synchronous Processing Required**
9
+ * Because the internal sets are reused, the `Set` passed to the `processor` is instantly
10
+ * cleared the moment the processor function returns. If you need to process the items
11
+ * asynchronously, you **must** manually clone the set inside your processor.
12
+ * @template T - The type of items being batched.
13
+ * @param processor - The callback executed when the batch flushes. Receives a `Set` of all batched items.
14
+ * @param queue - The scheduling function used to defer the flush. Defaults to `queueMicrotask`.
15
+ * @returns An object containing methods to mark items as dirty.
16
+ * @example
17
+ * * @example
18
+ * ```ts
19
+ * import { nextTick } from 'vue'
20
+ * let bq = makeBatchedQueue<string>(
21
+ * (items) => drawSomething(items),
22
+ * nextTick,
23
+ * )
24
+ * ```
25
+ */
26
+ export function makeBatchedQueue<T>(
27
+ processor: (items: Set<T>) => void,
28
+ queue: BatchedQueueFn,
29
+ ) {
30
+ let activeSet = new Set<T>()
31
+ let processingSet = new Set<T>()
32
+ let scheduled = false
33
+
34
+ const flush = () => {
35
+ // swap sets
36
+ const current = activeSet
37
+ activeSet = processingSet
38
+ processingSet = current
39
+
40
+ scheduled = false
41
+
42
+ try {
43
+ processor(processingSet)
44
+ } finally {
45
+ processingSet.clear()
46
+ }
47
+ }
48
+
49
+ function markDirty(item: T) {
50
+ activeSet.add(item)
51
+
52
+ if (!scheduled) {
53
+ scheduled = true
54
+ queue(flush)
55
+ }
56
+ }
57
+
58
+ function markMultipleDirty(items: T[]) {
59
+ let len = items.length
60
+ if (len === 0) return
61
+
62
+ for (let i = 0; i < len; i++) {
63
+ activeSet.add(items[i])
64
+ }
65
+
66
+ if (!scheduled) {
67
+ scheduled = true
68
+ queue(flush)
69
+ }
70
+ }
71
+
72
+ return {
73
+ markDirty,
74
+ markMultipleDirty,
75
+ }
76
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Creates a debounced render queue using `requestAnimationFrame`.
3
+ * This utility ensures that a callback is executed exactly once right before
4
+ * the next visual frame, regardless of how many times the trigger is called
5
+ * synchronously. It safely prevents layout thrashing and redundant computations.
6
+ * @param cb - The function to execute on the next animation frame.
7
+ * @returns A trigger function that schedules the callback. It includes a `.cancel()` method to abort the pending frame.
8
+ * * @example
9
+ * ```ts
10
+ * let renderQueue = makeRenderQueue(() => {
11
+ * console.log('DOM updated!')
12
+ * })
13
+ * * // Calling this multiple times synchronously...
14
+ * renderQueue()
15
+ * renderQueue()
16
+ * renderQueue()
17
+ * * // ...will only result in one 'DOM updated!' log on the next frame.
18
+ * ```
19
+ * * @example
20
+ * ```ts
21
+ * // Canceling a scheduled render (e.g., when a component unmounts)
22
+ * let trigger = makeRenderQueue(updateLayout)
23
+ * trigger()
24
+ * trigger.cancel() // The callback will not execute
25
+ * ```
26
+ */
27
+ export function makeRenderQueue(cb: () => void) {
28
+ let needsRender = false
29
+ let frameId = 0
30
+
31
+ const trigger = () => {
32
+ if (needsRender) return
33
+ needsRender = true
34
+
35
+ frameId = requestAnimationFrame(() => {
36
+ needsRender = false
37
+ cb()
38
+ })
39
+ }
40
+
41
+ trigger.cancel = () => {
42
+ if (needsRender) {
43
+ cancelAnimationFrame(frameId)
44
+ needsRender = false
45
+ }
46
+ }
47
+
48
+ return trigger
49
+ }
@@ -14,9 +14,8 @@ export function makeHistoryAction(
14
14
  config: PixelEngineConfig,
15
15
  accumulator: PixelAccumulator,
16
16
  patch: PixelPatchTiles,
17
- after?: () => void,
18
- afterUndo?: () => void,
19
- afterRedo?: () => void,
17
+ afterUndo?: (patch: PixelPatchTiles) => void,
18
+ afterRedo?: (patch: PixelPatchTiles) => void,
20
19
  applyPatchTilesFn = applyPatchTiles,
21
20
  ): HistoryAction {
22
21
 
@@ -26,13 +25,11 @@ export function makeHistoryAction(
26
25
  return {
27
26
  undo: () => {
28
27
  applyPatchTilesFn(target, patch.beforeTiles, tileSize)
29
- afterUndo?.()
30
- after?.()
28
+ afterUndo?.(patch)
31
29
  },
32
30
  redo: () => {
33
31
  applyPatchTilesFn(target, patch.afterTiles, tileSize)
34
- afterRedo?.()
35
- after?.()
32
+ afterRedo?.(patch)
36
33
  },
37
34
  dispose: () => accumulator.recyclePatch(patch),
38
35
  }
@@ -8,6 +8,7 @@ import { type HistoryActionFactory, makeHistoryAction } from './HistoryAction'
8
8
  import { HistoryManager } from './HistoryManager'
9
9
  import { PixelAccumulator } from './PixelAccumulator'
10
10
  import { PixelEngineConfig } from './PixelEngineConfig'
11
+ import type { PixelPatchTiles } from './PixelPatchTiles'
11
12
 
12
13
  export interface PixelWriterOptions {
13
14
  maxHistorySteps?: number
@@ -72,15 +73,13 @@ export class PixelWriter<M> {
72
73
  * throw immediately to prevent silent data loss from a nested extractPatch.
73
74
  *
74
75
  * @param transaction Callback to be executed inside the transaction.
75
- * @param after Called after both undo and redo — use for generic change notifications.
76
- * @param afterUndo Called after undo only — use for dimension or state changes specific to undo.
76
+ * @param afterUndo Called after undo only.
77
77
  * @param afterRedo Called after redo only.
78
78
  */
79
79
  withHistory(
80
80
  transaction: (mutator: M) => void,
81
- after?: () => void,
82
- afterUndo?: () => void,
83
- afterRedo?: () => void,
81
+ afterUndo?: (patch: PixelPatchTiles) => void,
82
+ afterRedo?: (patch: PixelPatchTiles) => void,
84
83
  ): void {
85
84
  if (this._inProgress) {
86
85
  throw new Error('withHistory is not re-entrant — commit or rollback the current operation first')
@@ -100,7 +99,7 @@ export class PixelWriter<M> {
100
99
  if (this.accumulator.beforeTiles.length === 0) return
101
100
 
102
101
  const patch = this.accumulator.extractPatch()
103
- const action = this.historyActionFactory(this.config, this.accumulator, patch, after, afterUndo, afterRedo)
102
+ const action = this.historyActionFactory(this.config, this.accumulator, patch, afterUndo, afterRedo)
104
103
 
105
104
  this.historyManager.commit(action)
106
105
  }
@@ -110,7 +109,6 @@ export class PixelWriter<M> {
110
109
  newHeight: number,
111
110
  offsetX = 0,
112
111
  offsetY = 0,
113
- after?: (target: ImageData) => void,
114
112
  afterUndo?: (target: ImageData) => void,
115
113
  afterRedo?: (target: ImageData) => void,
116
114
  resizeImageDataFn = resizeImageData,
@@ -134,12 +132,10 @@ export class PixelWriter<M> {
134
132
  undo: () => {
135
133
  setPixelData(target, beforeImageData)
136
134
  afterUndo?.(beforeImageData)
137
- after?.(beforeImageData)
138
135
  },
139
136
  redo: () => {
140
137
  setPixelData(target, afterImageData)
141
138
  afterRedo?.(afterImageData)
142
- after?.(afterImageData)
143
139
  },
144
140
  })
145
141
  }
package/src/index.ts CHANGED
@@ -26,6 +26,9 @@ export * from './Clipboard/writeImgBlobToClipboard'
26
26
 
27
27
  export * from './color'
28
28
 
29
+ export * from './Control/BatchedQueue'
30
+ export * from './Control/RenderQueue'
31
+
29
32
  export * from './History/HistoryAction'
30
33
  export * from './History/HistoryManager'
31
34
  export * from './History/PixelAccumulator'