pixel-data-js 0.23.1 → 0.24.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 (66) hide show
  1. package/dist/index.dev.cjs +1024 -596
  2. package/dist/index.dev.cjs.map +1 -1
  3. package/dist/index.dev.js +1010 -592
  4. package/dist/index.dev.js.map +1 -1
  5. package/dist/index.prod.cjs +1024 -596
  6. package/dist/index.prod.cjs.map +1 -1
  7. package/dist/index.prod.d.ts +280 -165
  8. package/dist/index.prod.js +1010 -592
  9. package/dist/index.prod.js.map +1 -1
  10. package/package.json +3 -2
  11. package/src/Canvas/CanvasFrameRenderer.ts +57 -0
  12. package/src/Canvas/ReusableCanvas.ts +60 -11
  13. package/src/History/HistoryAction.ts +38 -0
  14. package/src/History/HistoryManager.ts +4 -8
  15. package/src/History/PixelAccumulator.ts +95 -80
  16. package/src/History/PixelEngineConfig.ts +18 -6
  17. package/src/History/PixelMutator/mutatorApplyAlphaMask.ts +6 -6
  18. package/src/History/PixelMutator/mutatorApplyBinaryMask.ts +6 -6
  19. package/src/History/PixelMutator/mutatorApplyCircleBrushStroke.ts +6 -5
  20. package/src/History/PixelMutator/mutatorApplyCirclePencil.ts +22 -22
  21. package/src/History/PixelMutator/mutatorApplyCirclePencilStroke.ts +6 -5
  22. package/src/History/PixelMutator/mutatorApplyRectBrush.ts +19 -19
  23. package/src/History/PixelMutator/mutatorApplyRectBrushStroke.ts +6 -4
  24. package/src/History/PixelMutator/mutatorApplyRectPencil.ts +20 -20
  25. package/src/History/PixelMutator/mutatorApplyRectPencilStroke.ts +6 -4
  26. package/src/History/PixelMutator/mutatorBlendColor.ts +8 -5
  27. package/src/History/PixelMutator/mutatorBlendColorCircleMask.ts +71 -0
  28. package/src/History/PixelMutator/mutatorBlendPixel.ts +22 -26
  29. package/src/History/PixelMutator/mutatorBlendPixelData.ts +5 -3
  30. package/src/History/PixelMutator/mutatorBlendPixelDataAlphaMask.ts +5 -3
  31. package/src/History/PixelMutator/mutatorBlendPixelDataBinaryMask.ts +5 -3
  32. package/src/History/PixelMutator/mutatorClear.ts +6 -5
  33. package/src/History/PixelMutator/mutatorFill.ts +34 -9
  34. package/src/History/PixelMutator/mutatorFillBinaryMask.ts +4 -2
  35. package/src/History/PixelMutator/mutatorInvert.ts +8 -4
  36. package/src/History/PixelMutator.ts +4 -3
  37. package/src/History/PixelPatchTiles.ts +3 -15
  38. package/src/History/PixelWriter.ts +29 -33
  39. package/src/ImageData/ReusableImageData.ts +3 -5
  40. package/src/Mask/{CircleBrushAlphaMask.ts → CircleAlphaMask.ts} +2 -2
  41. package/src/Mask/{CircleBrushBinaryMask.ts → CircleBinaryMask.ts} +2 -2
  42. package/src/PixelData/PixelData.ts +1 -27
  43. package/src/PixelData/applyAlphaMaskToPixelData.ts +19 -9
  44. package/src/PixelData/applyBinaryMaskToPixelData.ts +24 -17
  45. package/src/PixelData/applyRectBrushToPixelData.ts +18 -5
  46. package/src/PixelData/blendColorPixelData.ts +31 -7
  47. package/src/PixelData/blendColorPixelDataAlphaMask.ts +16 -6
  48. package/src/PixelData/blendColorPixelDataBinaryMask.ts +16 -7
  49. package/src/PixelData/{applyCircleBrushToPixelData.ts → blendColorPixelDataCircleMask.ts} +11 -10
  50. package/src/PixelData/blendPixel.ts +47 -0
  51. package/src/PixelData/blendPixelData.ts +14 -4
  52. package/src/PixelData/blendPixelDataAlphaMask.ts +12 -4
  53. package/src/PixelData/blendPixelDataBinaryMask.ts +13 -4
  54. package/src/PixelData/blendPixelDataPaintBuffer.ts +37 -0
  55. package/src/PixelData/clearPixelData.ts +2 -2
  56. package/src/PixelData/fillPixelData.ts +26 -16
  57. package/src/PixelData/fillPixelDataBinaryMask.ts +12 -4
  58. package/src/PixelData/fillPixelDataFast.ts +94 -0
  59. package/src/PixelData/invertPixelData.ts +4 -2
  60. package/src/PixelTile/PaintBuffer.ts +122 -0
  61. package/src/PixelTile/PaintBufferRenderer.ts +40 -0
  62. package/src/PixelTile/PixelTile.ts +21 -0
  63. package/src/PixelTile/PixelTilePool.ts +63 -0
  64. package/src/_types.ts +9 -9
  65. package/src/index.ts +16 -6
  66. package/src/History/PixelMutator/mutatorApplyCircleBrush.ts +0 -78
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pixel-data-js",
3
3
  "type": "module",
4
- "version": "0.23.1",
4
+ "version": "0.24.0",
5
5
  "packageManager": "pnpm@10.30.0",
6
6
  "description": "JS Pixel and ImageData operations",
7
7
  "author": {
@@ -50,6 +50,7 @@
50
50
  "build": "tsup",
51
51
  "test": "vitest --coverage --project unit",
52
52
  "test:build": "pnpm build && vitest run --project dist",
53
+ "check-circ": "tsx _scripts/check-circular.ts",
53
54
  "test:mutation": "stryker run",
54
55
  "docs": "npx typedoc",
55
56
  "typecheck": "tsc --noEmit",
@@ -88,7 +89,7 @@
88
89
  "typedoc-plugin-mdn-links": "^5.1.1",
89
90
  "typedoc-rhineai-theme": "^1.2.0",
90
91
  "typescript": "^5.9.3",
91
- "unplugin-inline": "^1.8.0",
92
+ "unplugin-inline": "^1.9.0",
92
93
  "vite-tsconfig-paths": "^6.1.1",
93
94
  "vitest": "3.2.4"
94
95
  },
@@ -0,0 +1,57 @@
1
+ import type { PixelCanvas } from './PixelCanvas'
2
+ import { makeReusableCanvas } from './ReusableCanvas'
3
+
4
+ export type DrawPixelLayer = (ctx: CanvasRenderingContext2D) => void
5
+ export type DrawScreenLayer = (ctx: CanvasRenderingContext2D, scale: number) => void
6
+ export type CanvasFrameRenderer = ReturnType<typeof makeCanvasFrameRenderer>
7
+
8
+ const defaults = {
9
+ makeReusableCanvas,
10
+ }
11
+
12
+ type Deps = Partial<typeof defaults>
13
+
14
+ /**
15
+ * @param deps - @hidden
16
+ */
17
+ export function makeCanvasFrameRenderer(deps: Deps = defaults) {
18
+ const {
19
+ makeReusableCanvas = defaults.makeReusableCanvas,
20
+ } = deps
21
+
22
+ const bufferCanvas = makeReusableCanvas()
23
+
24
+ return function renderCanvasFrame(
25
+ pixelCanvas: PixelCanvas,
26
+ scale: number,
27
+ getImageData: () => ImageData | undefined | null,
28
+ drawPixelLayer?: DrawPixelLayer,
29
+ drawScreenLayer?: DrawScreenLayer,
30
+ ) {
31
+ const { canvas, ctx } = pixelCanvas
32
+
33
+ // 1. Clear pixel buffer (unscaled)
34
+ const { ctx: pxCtx, canvas: pxCanvas } = bufferCanvas(canvas.width, canvas.height)
35
+
36
+ // 2. Draw pixel data into pixel buffer
37
+ const img = getImageData()
38
+ if (img) {
39
+ pxCtx.putImageData(img, 0, 0)
40
+ }
41
+
42
+ // draw transient pixel data
43
+ drawPixelLayer?.(pxCtx)
44
+
45
+ // clear target canvas
46
+ ctx.setTransform(1, 0, 0, 1, 0, 0)
47
+ ctx.clearRect(0, 0, canvas.width, canvas.height)
48
+
49
+ // Draw pixel buffer scaled onto screen
50
+ ctx.setTransform(scale, 0, 0, scale, 0, 0)
51
+ ctx.drawImage(pxCanvas, 0, 0)
52
+
53
+ // Draw overlays in screen space
54
+ ctx.setTransform(1, 0, 0, 1, 0, 0)
55
+ drawScreenLayer?.(ctx, scale)
56
+ }
57
+ }
@@ -1,25 +1,69 @@
1
1
  import { CANVAS_CTX_FAILED } from './_constants'
2
2
 
3
- export type ReusableCanvas = {
4
- readonly canvas: HTMLCanvasElement
5
- readonly ctx: CanvasRenderingContext2D
3
+ export type CanvasContext<T> = T extends HTMLCanvasElement
4
+ ? CanvasRenderingContext2D
5
+ : OffscreenCanvasRenderingContext2D
6
+
7
+ export type ReusableCanvas<T extends HTMLCanvasElement | OffscreenCanvas> = {
8
+ readonly canvas: T
9
+ readonly ctx: CanvasContext<T>
6
10
  }
7
11
 
8
12
  /**
9
- * Creates a reusable canvas and context that are not part of the DOM.
13
+ * Creates a reusable HTMLCanvasElement and context that are not part of the DOM.
10
14
  * Ensures it is always set to `context.imageSmoothingEnabled = false`
11
15
  * @see makePixelCanvas
12
16
  * @throws {Error} If the {@link HTMLCanvasElement} context cannot be initialized.
13
17
  */
14
18
  export function makeReusableCanvas() {
15
- let canvas: HTMLCanvasElement | null = null
16
- let ctx: CanvasRenderingContext2D | null = null
19
+ return makeReusableCanvasMeta<HTMLCanvasElement>((w, h) => {
20
+ const canvas = document.createElement('canvas')
21
+
22
+ canvas.width = w
23
+ canvas.height = h
24
+
25
+ return canvas
26
+ })
27
+ }
28
+
29
+ /**
30
+ * Creates a reusable OffscreenCanvas and context.
31
+ * Ensures it is always set to `context.imageSmoothingEnabled = false`
32
+ * @see makePixelCanvas
33
+ * @throws {Error} If the {@link OffscreenCanvasRenderingContext2D} context cannot be initialized.
34
+ */
35
+ export function makeReusableOffscreenCanvas() {
36
+ return makeReusableCanvasMeta<OffscreenCanvas>((w, h) => new OffscreenCanvas(w, h))
37
+ }
38
+
39
+ function makeReusableCanvasMeta<T extends HTMLCanvasElement | OffscreenCanvas>(
40
+ factory: (w: number, h: number) => T,
41
+ ) {
42
+ let canvas: T | null = null
43
+ let ctx: CanvasContext<T> | null = null
17
44
 
18
- function get(width: number, height: number): ReusableCanvas {
45
+ const result: ReusableCanvas<T> = {
46
+ canvas: null as any,
47
+ ctx: null as any,
48
+ }
49
+
50
+ function get(width: number, height: number): ReusableCanvas<T> {
19
51
  if (canvas === null) {
20
- canvas = document.createElement('canvas')!
21
- ctx = canvas.getContext('2d')!
22
- if (!ctx) throw new Error(CANVAS_CTX_FAILED)
52
+ canvas = factory(width, height)
53
+ ctx = canvas.getContext('2d') as CanvasContext<T> | null
54
+
55
+ if (!ctx) {
56
+ throw new Error(CANVAS_CTX_FAILED)
57
+ }
58
+
59
+ // Initialize the fresh context state
60
+ ctx.imageSmoothingEnabled = false
61
+
62
+ ;(result as any).canvas = canvas
63
+ ;(result as any).ctx = ctx
64
+
65
+ // Early return to skip resize/clear checks for brand new canvases
66
+ return result
23
67
  }
24
68
 
25
69
  // Resize if needed (resizing auto-clears)
@@ -28,16 +72,21 @@ export function makeReusableCanvas() {
28
72
  canvas.height = height
29
73
  ctx!.imageSmoothingEnabled = false
30
74
  } else {
75
+ // Always reset transform before clearing to ensure the whole buffer is wiped
76
+ ctx!.setTransform(1, 0, 0, 1, 0, 0)
31
77
  // Same size → manually clear
32
78
  ctx!.clearRect(0, 0, width, height)
33
79
  }
34
80
 
35
- return { canvas, ctx: ctx! }
81
+ return result
36
82
  }
37
83
 
38
84
  get.reset = () => {
39
85
  canvas = null
40
86
  ctx = null
87
+
88
+ ;(result as any).canvas = null
89
+ ;(result as any).ctx = null
41
90
  }
42
91
 
43
92
  return get
@@ -0,0 +1,38 @@
1
+ import { applyPatchTiles, type PixelPatchTiles } from './PixelPatchTiles'
2
+ import type { PixelWriter } from './PixelWriter'
3
+
4
+ export interface HistoryAction {
5
+ undo: () => void
6
+ redo: () => void
7
+ dispose?: () => void
8
+ }
9
+
10
+ export type HistoryActionFactory = typeof makeHistoryAction
11
+
12
+ export function makeHistoryAction(
13
+ writer: PixelWriter<any>,
14
+ patch: PixelPatchTiles,
15
+ after?: () => void,
16
+ afterUndo?: () => void,
17
+ afterRedo?: () => void,
18
+ applyPatchTilesFn = applyPatchTiles,
19
+ ): HistoryAction {
20
+
21
+ const target = writer.config.target
22
+ const tileSize = writer.config.tileSize
23
+ const accumulator = writer.accumulator
24
+
25
+ return {
26
+ undo: () => {
27
+ applyPatchTilesFn(target, patch.beforeTiles, tileSize)
28
+ afterUndo?.()
29
+ after?.()
30
+ },
31
+ redo: () => {
32
+ applyPatchTilesFn(target, patch.afterTiles, tileSize)
33
+ afterRedo?.()
34
+ after?.()
35
+ },
36
+ dispose: () => accumulator.recyclePatch(patch),
37
+ }
38
+ }
@@ -1,13 +1,9 @@
1
- export interface HistoryAction {
2
- undo: () => void
3
- redo: () => void
4
- dispose?: () => void
5
- }
1
+ import type { HistoryAction } from './HistoryAction'
6
2
 
7
3
  export class HistoryManager {
8
- public undoStack: HistoryAction[]
9
- public redoStack: HistoryAction[]
10
- public listeners: Set<() => void>
4
+ readonly undoStack: HistoryAction[]
5
+ readonly redoStack: HistoryAction[]
6
+ readonly listeners: Set<() => void>
11
7
 
12
8
  constructor(
13
9
  public maxSteps = 50,
@@ -1,95 +1,61 @@
1
- import type { IPixelData } from '../_types'
1
+ import { PixelTile } from '../PixelTile/PixelTile'
2
+ import type { PixelTilePool } from '../PixelTile/PixelTilePool'
2
3
  import type { PixelEngineConfig } from './PixelEngineConfig'
3
- import { type PixelPatchTiles, PixelTile } from './PixelPatchTiles'
4
+ import { applyPatchTiles, type PixelPatchTiles } from './PixelPatchTiles'
5
+
6
+ export type DidChangeFn = (didChange: boolean) => boolean
4
7
 
5
8
  export class PixelAccumulator {
6
9
  public lookup: (PixelTile | undefined)[]
7
10
  public beforeTiles: PixelTile[]
8
- public pool: PixelTile[]
9
11
 
10
12
  constructor(
11
- public target: IPixelData,
12
13
  readonly config: PixelEngineConfig,
14
+ readonly tilePool: PixelTilePool,
13
15
  ) {
14
16
  this.lookup = []
15
17
  this.beforeTiles = []
16
- this.pool = []
17
- }
18
-
19
- getTile(
20
- id: number,
21
- tx: number,
22
- ty: number,
23
- ): PixelTile {
24
- let tile = this.pool.pop()
25
-
26
- if (tile) {
27
- tile.id = id
28
- tile.tx = tx
29
- tile.ty = ty
30
-
31
- return tile
32
- }
33
-
34
- return new PixelTile(
35
- id,
36
- tx,
37
- ty,
38
- this.config.tileArea,
39
- )
40
18
  }
41
19
 
42
20
  recyclePatch(patch: PixelPatchTiles) {
43
- const before = patch.beforeTiles
44
-
45
- for (let i = 0; i < before.length; i++) {
46
- let tile = before[i]
47
-
48
- if (tile) {
49
- this.pool.push(tile)
50
- }
51
- }
52
-
53
- const after = patch.afterTiles
54
-
55
- for (let i = 0; i < after.length; i++) {
56
- let tile = after[i]
57
-
58
- if (tile) {
59
- this.pool.push(tile)
60
- }
61
- }
21
+ this.tilePool.releaseTiles(patch.beforeTiles)
22
+ this.tilePool.releaseTiles(patch.afterTiles)
62
23
  }
63
24
 
64
25
  /**
65
26
  * @param x pixel x coordinate
66
27
  * @param y pixel y coordinate
67
28
  */
68
- storeTileBeforeState(x: number, y: number): void {
69
- let target = this.target
29
+ storePixelBeforeState(x: number, y: number): DidChangeFn {
70
30
  let shift = this.config.tileShift
71
- let columns = (target.width + this.config.tileMask) >> shift
31
+ let columns = this.config.targetColumns
72
32
  let tx = x >> shift
73
33
  let ty = y >> shift
74
34
  let id = ty * columns + tx
75
35
 
76
36
  let tile = this.lookup[id]
37
+ let added = false
77
38
 
78
39
  if (!tile) {
79
- tile = this.getTile(
80
- id,
81
- tx,
82
- ty,
83
- )
40
+ tile = this.tilePool.getTile(id, tx, ty)
84
41
 
85
42
  this.extractState(tile)
86
43
  this.lookup[id] = tile
87
44
  this.beforeTiles.push(tile)
45
+ added = true
46
+ }
47
+
48
+ return (didChange: boolean) => {
49
+ if (!didChange && added) {
50
+ this.beforeTiles.pop()
51
+ this.lookup[id] = undefined
52
+ this.tilePool.releaseTile(tile!)
53
+ }
54
+ return didChange
88
55
  }
89
56
  }
90
57
 
91
58
  /**
92
- *
93
59
  * @param x pixel x coordinate
94
60
  * @param y pixel y coordinate
95
61
  * @param w pixel width
@@ -100,27 +66,24 @@ export class PixelAccumulator {
100
66
  y: number,
101
67
  w: number,
102
68
  h: number,
103
- ) {
104
- let target = this.target
69
+ ): DidChangeFn {
105
70
  let shift = this.config.tileShift
106
- let columns = (target.width + this.config.tileMask) >> shift
71
+ let columns = this.config.targetColumns
107
72
 
108
73
  let startX = x >> shift
109
74
  let startY = y >> shift
110
75
  let endX = (x + w - 1) >> shift
111
76
  let endY = (y + h - 1) >> shift
112
77
 
78
+ let startIndex = this.beforeTiles.length
79
+
113
80
  for (let ty = startY; ty <= endY; ty++) {
114
81
  for (let tx = startX; tx <= endX; tx++) {
115
82
  let id = ty * columns + tx
116
83
  let tile = this.lookup[id]
117
84
 
118
85
  if (!tile) {
119
- tile = this.getTile(
120
- id,
121
- tx,
122
- ty,
123
- )
86
+ tile = this.tilePool.getTile(id, tx, ty)
124
87
 
125
88
  this.extractState(tile)
126
89
  this.lookup[id] = tile
@@ -128,10 +91,28 @@ export class PixelAccumulator {
128
91
  }
129
92
  }
130
93
  }
94
+
95
+ return (didChange: boolean) => {
96
+ if (!didChange) {
97
+ let length = this.beforeTiles.length
98
+
99
+ for (let i = startIndex; i < length; i++) {
100
+ let t = this.beforeTiles[i]
101
+
102
+ if (t) {
103
+ this.lookup[t.id] = undefined
104
+ this.tilePool.releaseTile(t)
105
+ }
106
+ }
107
+
108
+ this.beforeTiles.length = startIndex
109
+ }
110
+ return didChange
111
+ }
131
112
  }
132
113
 
133
114
  extractState(tile: PixelTile) {
134
- let target = this.target
115
+ let target = this.config.target
135
116
  let TILE_SIZE = this.config.tileSize
136
117
  let dst = tile.data32
137
118
  let src = target.data32
@@ -140,29 +121,45 @@ export class PixelAccumulator {
140
121
  let targetWidth = target.width
141
122
  let targetHeight = target.height
142
123
 
143
- let copyWidth = Math.max(0, Math.min(TILE_SIZE, targetWidth - startX))
124
+ // If the tile is completely outside the canvas, zero it out.
125
+ if (startX >= targetWidth || startX + TILE_SIZE <= 0 || startY >= targetHeight || startY + TILE_SIZE <= 0) {
126
+ dst.fill(0)
127
+ return
128
+ }
129
+
130
+ // Calculate offset if tile starts off the left side of the screen
131
+ let srcOffsetX = Math.max(0, -startX)
132
+ let copyWidth = Math.max(0, Math.min(TILE_SIZE - srcOffsetX, targetWidth - Math.max(0, startX)))
144
133
 
145
134
  for (let ly = 0; ly < TILE_SIZE; ly++) {
146
135
  let globalY = startY + ly
147
136
  let dstIndex = ly * TILE_SIZE
148
137
 
138
+ // Check negative bounds accurately
149
139
  if (globalY < 0 || globalY >= targetHeight || copyWidth === 0) {
150
140
  dst.fill(0, dstIndex, dstIndex + TILE_SIZE)
151
141
  continue
152
142
  }
153
143
 
154
- let srcIndex = globalY * targetWidth + startX
144
+ let srcIndex = globalY * targetWidth + Math.max(0, startX)
155
145
  let rowData = src.subarray(srcIndex, srcIndex + copyWidth)
156
146
 
157
- dst.set(rowData, dstIndex)
147
+ // Shift the paste over by the offset
148
+ dst.set(rowData, dstIndex + srcOffsetX)
149
+
150
+ // Pad the left edge with 0s if we hung off the left side
151
+ if (srcOffsetX > 0) {
152
+ dst.fill(0, dstIndex, dstIndex + srcOffsetX)
153
+ }
158
154
 
159
- if (copyWidth < TILE_SIZE) {
160
- dst.fill(0, dstIndex + copyWidth, dstIndex + TILE_SIZE)
155
+ // Pad the right edge with 0s if we hung off the right side
156
+ if (srcOffsetX + copyWidth < TILE_SIZE) {
157
+ dst.fill(0, dstIndex + srcOffsetX + copyWidth, dstIndex + TILE_SIZE)
161
158
  }
162
159
  }
163
160
  }
164
161
 
165
- extractAfterTiles(): PixelTile[] {
162
+ extractPatch(): PixelPatchTiles {
166
163
  let afterTiles: PixelTile[] = []
167
164
  let length = this.beforeTiles.length
168
165
 
@@ -170,22 +167,40 @@ export class PixelAccumulator {
170
167
  let beforeTile = this.beforeTiles[i]
171
168
 
172
169
  if (beforeTile) {
173
- let afterTile = this.getTile(
174
- beforeTile.id,
175
- beforeTile.tx,
176
- beforeTile.ty,
177
- )
170
+ let afterTile = this.tilePool.getTile(beforeTile.id, beforeTile.tx, beforeTile.ty)
178
171
 
179
172
  this.extractState(afterTile)
180
173
  afterTiles.push(afterTile)
181
174
  }
182
175
  }
183
176
 
184
- return afterTiles
177
+ let beforeTiles = this.beforeTiles
178
+ this.beforeTiles = []
179
+ this.lookup.length = 0
180
+
181
+ return {
182
+ beforeTiles,
183
+ afterTiles,
184
+ }
185
185
  }
186
186
 
187
- reset() {
188
- this.lookup = []
189
- this.beforeTiles = []
187
+ rollback() {
188
+ let target = this.config.target
189
+ let tileSize = this.config.tileSize
190
+ let length = this.beforeTiles.length
191
+
192
+ applyPatchTiles(target, this.beforeTiles, tileSize)
193
+
194
+ for (let i = 0; i < length; i++) {
195
+ let tile = this.beforeTiles[i]
196
+
197
+ if (tile) {
198
+ this.lookup[tile.id] = undefined
199
+ this.tilePool.releaseTile(tile)
200
+ }
201
+ }
202
+
203
+ this.beforeTiles.length = 0
204
+ this.lookup.length = 0
190
205
  }
191
206
  }
@@ -1,18 +1,30 @@
1
+ import type { IPixelData } from '../_types'
2
+
1
3
  export class PixelEngineConfig {
2
- public readonly tileSize: number
3
- public readonly tileShift: number
4
- public readonly tileMask: number
5
- public readonly tileArea: number
4
+ readonly tileSize: number
5
+ // pixelX = tileX << tileShift
6
+ // pixelY = tileY << tileShift
7
+ readonly tileShift: number
8
+ readonly tileMask: number
9
+ readonly tileArea: number
10
+ readonly target!: IPixelData
11
+ readonly targetColumns: number = 0
6
12
 
7
- constructor(tileSize: number = 256) {
13
+ constructor(tileSize: number, target: IPixelData) {
8
14
  // Ensure it's a power of 2 to guarantee bitwise math works
9
15
  if ((tileSize & (tileSize - 1)) !== 0) {
10
16
  throw new Error('tileSize must be a power of 2')
11
17
  }
12
18
 
13
19
  this.tileSize = tileSize
14
- this.tileShift = Math.log2(tileSize)
20
+ this.tileShift = 31 - Math.clz32(tileSize)
15
21
  this.tileMask = tileSize - 1
16
22
  this.tileArea = tileSize * tileSize
23
+ this.setTarget(target)
24
+ }
25
+
26
+ setTarget(target: IPixelData) {
27
+ ;(this as any).target = target
28
+ ;(this as any).targetColumns = (target.width + this.tileMask) >> this.tileShift
17
29
  }
18
30
  }
@@ -17,17 +17,17 @@ export const mutatorApplyAlphaMask = ((writer: PixelWriter<any>, deps: Deps = de
17
17
  } = deps
18
18
 
19
19
  return {
20
- applyAlphaMask: (mask: AlphaMask, opts: ApplyMaskToPixelDataOptions = {}) => {
21
- let target = writer.target
20
+ applyAlphaMask(mask: AlphaMask, opts: ApplyMaskToPixelDataOptions = {}): boolean {
21
+ let target = writer.config.target
22
22
  const {
23
23
  x = 0,
24
24
  y = 0,
25
- w = writer.target.width,
26
- h = writer.target.height,
25
+ w = target.width,
26
+ h = target.height,
27
27
  } = opts
28
28
 
29
- writer.accumulator.storeRegionBeforeState(x, y, w, h)
30
- applyAlphaMaskToPixelData(target, mask, opts)
29
+ const didChange = writer.accumulator.storeRegionBeforeState(x, y, w, h)
30
+ return didChange(applyAlphaMaskToPixelData(target, mask, opts))
31
31
  },
32
32
  }
33
33
  }) satisfies HistoryMutator<any, Deps>
@@ -17,17 +17,17 @@ export const mutatorApplyBinaryMask = ((writer: PixelWriter<any>, deps: Deps = d
17
17
  } = deps
18
18
 
19
19
  return {
20
- applyBinaryMask: (mask: BinaryMask, opts: ApplyMaskToPixelDataOptions = {}) => {
21
- let target = writer.target
20
+ applyBinaryMask(mask: BinaryMask, opts: ApplyMaskToPixelDataOptions = {}): boolean {
21
+ let target = writer.config.target
22
22
  const {
23
23
  x = 0,
24
24
  y = 0,
25
- w = writer.target.width,
26
- h = writer.target.height,
25
+ w = target.width,
26
+ h = target.height,
27
27
  } = opts
28
28
 
29
- writer.accumulator.storeRegionBeforeState(x, y, w, h)
30
- applyBinaryMaskToPixelData(target, mask, opts)
29
+ const didChange = writer.accumulator.storeRegionBeforeState(x, y, w, h)
30
+ return didChange(applyBinaryMaskToPixelData(target, mask, opts))
31
31
  },
32
32
  }
33
33
  }) satisfies HistoryMutator<any, Deps>
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  type AlphaMask,
3
3
  type BlendColor32,
4
- type CircleBrushAlphaMask,
4
+ type CircleAlphaMask,
5
5
  type Color32,
6
6
  type ColorBlendMaskOptions,
7
7
  type HistoryMutator,
@@ -72,7 +72,7 @@ export const mutatorApplyCircleBrushStroke = ((writer: PixelWriter<any>, deps: D
72
72
  y0: number,
73
73
  x1: number,
74
74
  y1: number,
75
- brush: CircleBrushAlphaMask,
75
+ brush: CircleAlphaMask,
76
76
  alpha = 255,
77
77
  blendFn: BlendColor32 = sourceOverPerfect,
78
78
  ) {
@@ -102,8 +102,9 @@ export const mutatorApplyCircleBrushStroke = ((writer: PixelWriter<any>, deps: D
102
102
  const brushData = brush.data
103
103
  const minOffset = brush.minOffset
104
104
 
105
- const targetWidth = writer.target.width
106
- const targetHeight = writer.target.height
105
+ const target = writer.config.target
106
+ const targetWidth = target.width
107
+ const targetHeight = target.height
107
108
 
108
109
  forEachLinePoint(
109
110
  x0,
@@ -171,7 +172,7 @@ export const mutatorApplyCircleBrushStroke = ((writer: PixelWriter<any>, deps: D
171
172
  blendColorPixelOptions.h = bh
172
173
 
173
174
  blendColorPixelDataAlphaMask(
174
- writer.target,
175
+ target,
175
176
  color,
176
177
  mask as AlphaMask,
177
178
  blendColorPixelOptions,