pixel-data-js 0.23.1 → 0.25.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 (85) hide show
  1. package/dist/index.dev.cjs +1816 -1802
  2. package/dist/index.dev.cjs.map +1 -1
  3. package/dist/index.dev.js +1799 -1786
  4. package/dist/index.dev.js.map +1 -1
  5. package/dist/index.prod.cjs +1816 -1802
  6. package/dist/index.prod.cjs.map +1 -1
  7. package/dist/index.prod.d.ts +349 -342
  8. package/dist/index.prod.js +1799 -1786
  9. package/dist/index.prod.js.map +1 -1
  10. package/package.json +3 -2
  11. package/src/Algorithm/floodFillSelection.ts +2 -2
  12. package/src/Canvas/CanvasFrameRenderer.ts +57 -0
  13. package/src/Canvas/ReusableCanvas.ts +60 -11
  14. package/src/Canvas/canvas-blend-modes.ts +28 -0
  15. package/src/History/HistoryAction.ts +38 -0
  16. package/src/History/HistoryManager.ts +4 -8
  17. package/src/History/PixelAccumulator.ts +137 -99
  18. package/src/History/PixelEngineConfig.ts +16 -6
  19. package/src/History/PixelMutator/mutatorApplyAlphaMask.ts +6 -6
  20. package/src/History/PixelMutator/mutatorApplyBinaryMask.ts +6 -6
  21. package/src/History/PixelMutator/mutatorBlendColor.ts +8 -5
  22. package/src/History/PixelMutator/mutatorBlendPixel.ts +22 -26
  23. package/src/History/PixelMutator/mutatorBlendPixelData.ts +7 -5
  24. package/src/History/PixelMutator/mutatorBlendPixelDataAlphaMask.ts +7 -5
  25. package/src/History/PixelMutator/mutatorBlendPixelDataBinaryMask.ts +7 -5
  26. package/src/History/PixelMutator/mutatorClear.ts +6 -5
  27. package/src/History/PixelMutator/mutatorFill.ts +34 -9
  28. package/src/History/PixelMutator/mutatorFillBinaryMask.ts +4 -2
  29. package/src/History/PixelMutator/mutatorInvert.ts +8 -4
  30. package/src/History/PixelMutator.ts +2 -21
  31. package/src/History/PixelPatchTiles.ts +4 -16
  32. package/src/History/PixelWriter.ts +150 -31
  33. package/src/ImageData/ReusableImageData.ts +3 -5
  34. package/src/Internal/helpers.ts +2 -0
  35. package/src/Paint/PaintBuffer.ts +269 -0
  36. package/src/Paint/PaintBufferCanvasRenderer.ts +48 -0
  37. package/src/Paint/makeCirclePaintAlphaMask.ts +41 -0
  38. package/src/{Mask/CircleBrushBinaryMask.ts → Paint/makeCirclePaintBinaryMask.ts} +5 -6
  39. package/src/Paint/makePaintMask.ts +28 -0
  40. package/src/Paint/makeRectFalloffPaintAlphaMask.ts +47 -0
  41. package/src/PixelData/PixelBuffer32.ts +2 -2
  42. package/src/PixelData/PixelData.ts +2 -28
  43. package/src/PixelData/applyAlphaMaskToPixelData.ts +20 -10
  44. package/src/PixelData/applyBinaryMaskToPixelData.ts +26 -19
  45. package/src/PixelData/blendColorPixelData.ts +33 -9
  46. package/src/PixelData/blendColorPixelDataAlphaMask.ts +19 -9
  47. package/src/PixelData/blendColorPixelDataBinaryMask.ts +19 -10
  48. package/src/PixelData/blendPixel.ts +47 -0
  49. package/src/PixelData/blendPixelData.ts +17 -7
  50. package/src/PixelData/blendPixelDataAlphaMask.ts +15 -7
  51. package/src/PixelData/blendPixelDataBinaryMask.ts +16 -7
  52. package/src/PixelData/blendPixelDataPaintBuffer.ts +37 -0
  53. package/src/PixelData/clearPixelData.ts +4 -4
  54. package/src/PixelData/extractPixelData.ts +4 -4
  55. package/src/PixelData/extractPixelDataBuffer.ts +4 -4
  56. package/src/PixelData/fillPixelData.ts +31 -21
  57. package/src/PixelData/fillPixelDataBinaryMask.ts +15 -7
  58. package/src/PixelData/fillPixelDataFast.ts +94 -0
  59. package/src/PixelData/invertPixelData.ts +6 -4
  60. package/src/PixelData/pixelDataToAlphaMask.ts +2 -2
  61. package/src/PixelData/reflectPixelData.ts +3 -3
  62. package/src/PixelData/resamplePixelData.ts +2 -2
  63. package/src/PixelData/writePaintBufferToPixelData.ts +26 -0
  64. package/src/PixelData/writePixelDataBuffer.ts +5 -5
  65. package/src/PixelTile/PixelTile.ts +21 -0
  66. package/src/PixelTile/PixelTilePool.ts +63 -0
  67. package/src/Rect/trimMaskRectBounds.ts +121 -0
  68. package/src/Rect/trimRectBounds.ts +25 -116
  69. package/src/_types.ts +17 -16
  70. package/src/index.ts +19 -24
  71. package/src/History/PixelMutator/mutatorApplyCircleBrush.ts +0 -78
  72. package/src/History/PixelMutator/mutatorApplyCircleBrushStroke.ts +0 -181
  73. package/src/History/PixelMutator/mutatorApplyCirclePencil.ts +0 -59
  74. package/src/History/PixelMutator/mutatorApplyCirclePencilStroke.ts +0 -171
  75. package/src/History/PixelMutator/mutatorApplyRectBrush.ts +0 -64
  76. package/src/History/PixelMutator/mutatorApplyRectBrushStroke.ts +0 -182
  77. package/src/History/PixelMutator/mutatorApplyRectPencil.ts +0 -65
  78. package/src/History/PixelMutator/mutatorApplyRectPencilStroke.ts +0 -164
  79. package/src/Mask/CircleBrushAlphaMask.ts +0 -32
  80. package/src/PixelData/applyCircleBrushToPixelData.ts +0 -91
  81. package/src/PixelData/applyRectBrushToPixelData.ts +0 -85
  82. package/src/Rect/getCircleBrushOrPencilBounds.ts +0 -43
  83. package/src/Rect/getCircleBrushOrPencilStrokeBounds.ts +0 -24
  84. package/src/Rect/getRectBrushOrPencilBounds.ts +0 -38
  85. package/src/Rect/getRectBrushOrPencilStrokeBounds.ts +0 -26
@@ -0,0 +1,269 @@
1
+ import type { Color32, PaintAlphaMask, PaintBinaryMask, Rect } from '../_types'
2
+ import { forEachLinePoint } from '../Algorithm/forEachLinePoint'
3
+ import type { PixelEngineConfig } from '../History/PixelEngineConfig'
4
+ import { macro_halfAndFloor } from '../Internal/helpers'
5
+ import type { PixelTile } from '../PixelTile/PixelTile'
6
+ import type { PixelTilePool } from '../PixelTile/PixelTilePool'
7
+ import { trimRectBounds } from '../Rect/trimRectBounds'
8
+
9
+ export class PaintBuffer {
10
+ readonly lookup: (PixelTile | undefined)[]
11
+ private readonly scratchBounds: Rect = { x: 0, y: 0, w: 0, h: 0 }
12
+
13
+ constructor(
14
+ readonly config: PixelEngineConfig,
15
+ readonly tilePool: PixelTilePool,
16
+ ) {
17
+ this.lookup = []
18
+ }
19
+
20
+ private eachTileInBounds(
21
+ bounds: Rect,
22
+ callback: (tile: PixelTile, bX: number, bY: number, bW: number, bH: number) => void,
23
+ ): void {
24
+ const { tileShift, targetColumns, targetRows, tileSize } = this.config
25
+
26
+ const x1 = Math.max(0, bounds.x >> tileShift)
27
+ const y1 = Math.max(0, bounds.y >> tileShift)
28
+ const x2 = Math.min(targetColumns - 1, (bounds.x + bounds.w - 1) >> tileShift)
29
+ const y2 = Math.min(targetRows - 1, (bounds.y + bounds.h - 1) >> tileShift)
30
+
31
+ if (x1 > x2 || y1 > y2) return
32
+
33
+ const lookup = this.lookup
34
+ const tilePool = this.tilePool
35
+
36
+ for (let ty = y1; ty <= y2; ty++) {
37
+ const rowOffset = ty * targetColumns
38
+ const tileTop = ty << tileShift
39
+
40
+ for (let tx = x1; tx <= x2; tx++) {
41
+ const id = rowOffset + tx
42
+ const tile = lookup[id] ?? (lookup[id] = tilePool.getTile(id, tx, ty))
43
+ const tileLeft = tx << tileShift
44
+
45
+ const startX = bounds.x > tileLeft ? bounds.x : tileLeft
46
+ const startY = bounds.y > tileTop ? bounds.y : tileTop
47
+
48
+ const maskEndX = bounds.x + bounds.w
49
+ const tileEndX = tileLeft + tileSize
50
+ const endX = maskEndX < tileEndX ? maskEndX : tileEndX
51
+
52
+ const maskEndY = bounds.y + bounds.h
53
+ const tileEndY = tileTop + tileSize
54
+ const endY = maskEndY < tileEndY ? maskEndY : tileEndY
55
+
56
+ callback(tile, startX, startY, endX - startX, endY - startY)
57
+ }
58
+ }
59
+ }
60
+
61
+ writePaintAlphaMaskStroke(
62
+ color: Color32,
63
+ brush: PaintAlphaMask,
64
+ x0: number,
65
+ y0: number,
66
+ x1: number,
67
+ y1: number,
68
+ ): boolean {
69
+ const cA = color >>> 24
70
+ if (cA === 0) return false
71
+
72
+ const { tileShift, tileMask, target } = this.config
73
+ const { w: bW, h: bH, data: bD, centerOffsetX, centerOffsetY } = brush
74
+ const cRGB = color & 0x00ffffff
75
+ const scratch = this.scratchBounds
76
+
77
+ let changed = false
78
+
79
+ forEachLinePoint(x0, y0, x1, y1, (px, py) => {
80
+
81
+ const topLeftX = Math.floor(px + centerOffsetX)
82
+ const topLeftY = Math.floor(py + centerOffsetY)
83
+ trimRectBounds(
84
+ topLeftX,
85
+ topLeftY,
86
+ bW,
87
+ bH,
88
+ target.width,
89
+ target.height,
90
+ scratch,
91
+ )
92
+
93
+ if (scratch.w <= 0 || scratch.h <= 0) return
94
+
95
+ this.eachTileInBounds(scratch, (tile, bX, bY, bW_t, bH_t) => {
96
+ const d32 = tile.data32
97
+ let tileChanged = false
98
+
99
+ for (let i = 0; i < bH_t; i++) {
100
+ const canvasY = bY + i
101
+ const bOff = (canvasY - topLeftY) * bW
102
+ const tOff = (canvasY & tileMask) << tileShift
103
+ const dS = tOff + (bX & tileMask)
104
+
105
+ for (let j = 0; j < bW_t; j++) {
106
+ const canvasX = bX + j
107
+ const brushA = bD[bOff + (canvasX - topLeftX)]
108
+ if (brushA === 0) continue
109
+
110
+ const t = cA * brushA + 128
111
+ const blendedA = (t + (t >> 8)) >> 8
112
+
113
+ const idx = dS + j
114
+ const cur = d32[idx]
115
+ if (brushA > (cur >>> 24)) {
116
+ const next = (cRGB | (blendedA << 24)) >>> 0
117
+ if (cur !== next) {
118
+ d32[idx] = next as Color32
119
+ tileChanged = true
120
+ }
121
+ }
122
+ }
123
+ }
124
+ if (tileChanged) changed = true
125
+ })
126
+ })
127
+ return changed
128
+ }
129
+
130
+ writePaintBinaryMaskStroke(
131
+ color: Color32,
132
+ brush: PaintBinaryMask,
133
+ x0: number,
134
+ y0: number,
135
+ x1: number,
136
+ y1: number,
137
+ ): boolean {
138
+ const alphaIsZero = (color >>> 24) === 0
139
+ if (alphaIsZero) return false
140
+
141
+ const { tileShift, tileMask, target } = this.config
142
+ const { w: bW, h: bH, data: bD, centerOffsetX, centerOffsetY } = brush
143
+ const scratch = this.scratchBounds
144
+ let changed = false
145
+
146
+ forEachLinePoint(x0, y0, x1, y1, (px, py) => {
147
+ const topLeftX = Math.floor(px + centerOffsetX)
148
+ const topLeftY = Math.floor(py + centerOffsetY)
149
+
150
+ trimRectBounds(
151
+ topLeftX,
152
+ topLeftY,
153
+ bW,
154
+ bH,
155
+ target.width,
156
+ target.height,
157
+ scratch,
158
+ )
159
+
160
+ if (scratch.w <= 0 || scratch.h <= 0) return
161
+
162
+ this.eachTileInBounds(scratch, (tile, bX, bY, bW_t, bH_t) => {
163
+ const d32 = tile.data32
164
+ let tileChanged = false
165
+
166
+ for (let i = 0; i < bH_t; i++) {
167
+ const canvasY = bY + i
168
+ const bOff = (canvasY - topLeftY) * bW
169
+ const tOff = (canvasY & tileMask) << tileShift
170
+ const dS = tOff + (bX & tileMask)
171
+
172
+ for (let j = 0; j < bW_t; j++) {
173
+ const canvasX = bX + j
174
+ if (bD[bOff + (canvasX - topLeftX)]) {
175
+ const idx = dS + j
176
+ if (d32[idx] !== color) {
177
+ d32[idx] = color
178
+ tileChanged = true
179
+ }
180
+ }
181
+ }
182
+ }
183
+ if (tileChanged) changed = true
184
+ })
185
+ })
186
+
187
+ return changed
188
+ }
189
+
190
+ writeRectStroke(
191
+ color: Color32,
192
+ brushWidth: number,
193
+ brushHeight: number,
194
+ x0: number,
195
+ y0: number,
196
+ x1: number,
197
+ y1: number,
198
+ ): boolean {
199
+ const alphaIsZero = (color >>> 24) === 0
200
+ if (alphaIsZero) return false
201
+
202
+ const config = this.config
203
+ const tileShift = config.tileShift
204
+ const tileMask = config.tileMask
205
+ const target = config.target
206
+ const scratch = this.scratchBounds
207
+
208
+ const centerOffsetX = macro_halfAndFloor(brushWidth - 1)
209
+ const centerOffsetY = macro_halfAndFloor(brushHeight - 1)
210
+
211
+ let changed = false
212
+
213
+ forEachLinePoint(
214
+ x0,
215
+ y0,
216
+ x1,
217
+ y1,
218
+ (px, py) => {
219
+ const topLeftX = Math.floor(px + centerOffsetX)
220
+ const topLeftY = Math.floor(py + centerOffsetY)
221
+
222
+ trimRectBounds(
223
+ topLeftX,
224
+ topLeftY,
225
+ brushWidth,
226
+ brushHeight,
227
+ target.width,
228
+ target.height,
229
+ scratch,
230
+ )
231
+
232
+ if (scratch.w <= 0 || scratch.h <= 0) return
233
+
234
+ this.eachTileInBounds(
235
+ scratch,
236
+ (tile, bX, bY, bW_t, bH_t) => {
237
+ const d32 = tile.data32
238
+ let tileChanged = false
239
+
240
+ for (let i = 0; i < bH_t; i++) {
241
+ const canvasY = bY + i
242
+ const tOff = (canvasY & tileMask) << tileShift
243
+ const dS = tOff + (bX & tileMask)
244
+
245
+ for (let j = 0; j < bW_t; j++) {
246
+ const idx = dS + j
247
+
248
+ if (d32[idx] !== color) {
249
+ d32[idx] = color
250
+ tileChanged = true
251
+ }
252
+ }
253
+ }
254
+
255
+ if (tileChanged) {
256
+ changed = true
257
+ }
258
+ },
259
+ )
260
+ },
261
+ )
262
+
263
+ return changed
264
+ }
265
+
266
+ clear(): void {
267
+ this.tilePool.releaseTiles(this.lookup)
268
+ }
269
+ }
@@ -0,0 +1,48 @@
1
+ import { CANVAS_CTX_FAILED } from '../Canvas/_constants'
2
+ import type { PaintBuffer } from './PaintBuffer'
3
+
4
+ export type PaintBufferCanvasRenderer = ReturnType<typeof makePaintBufferCanvasRenderer>
5
+
6
+ /**
7
+ *
8
+ * @param offscreenCanvasClass - @internal
9
+ */
10
+ export function makePaintBufferCanvasRenderer(
11
+ paintBuffer: PaintBuffer,
12
+ offscreenCanvasClass = OffscreenCanvas,
13
+ ) {
14
+ const config = paintBuffer.config
15
+ const tileSize = config.tileSize
16
+ const tileShift = config.tileShift
17
+ const lookup = paintBuffer.lookup
18
+ const canvas = new offscreenCanvasClass(tileSize, tileSize)
19
+ const ctx = canvas.getContext('2d')
20
+ if (!ctx) throw new Error(CANVAS_CTX_FAILED)
21
+ ctx.imageSmoothingEnabled = false
22
+
23
+ return function drawPaintBuffer(
24
+ targetCtx: CanvasRenderingContext2D,
25
+ alpha = 255,
26
+ compOperation: GlobalCompositeOperation = 'source-over',
27
+ ): void {
28
+
29
+ targetCtx.globalAlpha = alpha / 255
30
+ targetCtx.globalCompositeOperation = compOperation
31
+
32
+ for (let i = 0; i < lookup.length; i++) {
33
+ const tile = lookup[i]
34
+
35
+ if (tile) {
36
+ const dx = tile.tx << tileShift
37
+ const dy = tile.ty << tileShift
38
+
39
+ ctx.putImageData(tile.imageData, 0, 0)
40
+
41
+ targetCtx.drawImage(canvas, dx, dy)
42
+ }
43
+ }
44
+
45
+ targetCtx.globalAlpha = 1
46
+ targetCtx.globalCompositeOperation = 'source-over'
47
+ }
48
+ }
@@ -0,0 +1,41 @@
1
+ import { MaskType, type PaintAlphaMask } from '../_types'
2
+
3
+ export function makeCirclePaintAlphaMask(size: number, fallOff: (d: number) => number = (d) => d): PaintAlphaMask {
4
+ const area = size * size
5
+ const data = new Uint8Array(area)
6
+ const radius = size / 2
7
+ const invR = 1 / radius
8
+
9
+ const centerOffset = -Math.ceil(radius - 0.5)
10
+
11
+ for (let y = 0; y < size; y++) {
12
+ const rowOffset = y * size
13
+ const dy = y - radius + 0.5
14
+ const dy2 = dy * dy
15
+
16
+ for (let x = 0; x < size; x++) {
17
+ const dx = x - radius + 0.5
18
+ const distSqr = dx * dx + dy2
19
+
20
+ if (distSqr <= (radius * radius)) {
21
+ const dist = Math.sqrt(distSqr) * invR
22
+
23
+ // Pass 1.0 at center, 0.0 at edge
24
+ const strength = fallOff(1 - dist)
25
+ if (strength > 0) {
26
+ const intensity = (strength * 255) | 0
27
+ data[rowOffset + x] = Math.max(0, Math.min(255, intensity))
28
+ }
29
+ }
30
+ }
31
+ }
32
+
33
+ return {
34
+ type: MaskType.ALPHA,
35
+ data,
36
+ w: size,
37
+ h: size,
38
+ centerOffsetX: centerOffset,
39
+ centerOffsetY: centerOffset
40
+ }
41
+ }
@@ -1,11 +1,11 @@
1
- import { type CircleBrushBinaryMask, MaskType } from '../_types'
1
+ import { MaskType, type PaintBinaryMask } from '../_types'
2
2
 
3
- export function makeCircleBrushBinaryMask(size: number): CircleBrushBinaryMask {
3
+ export function makeCirclePaintBinaryMask(size: number): PaintBinaryMask {
4
4
  const area = size * size
5
5
  const data = new Uint8Array(area)
6
6
  const radius = size / 2
7
7
 
8
- const minOffset = -Math.ceil(radius - 0.5)
8
+ const centerOffset = -Math.ceil(radius - 0.5)
9
9
 
10
10
  for (let y = 0; y < size; y++) {
11
11
  for (let x = 0; x < size; x++) {
@@ -23,8 +23,7 @@ export function makeCircleBrushBinaryMask(size: number): CircleBrushBinaryMask {
23
23
  data,
24
24
  w: size,
25
25
  h: size,
26
- radius,
27
- size,
28
- minOffset,
26
+ centerOffsetX: centerOffset,
27
+ centerOffsetY: centerOffset,
29
28
  }
30
29
  }
@@ -0,0 +1,28 @@
1
+ import { type AlphaMask, type BinaryMask, MaskType, type PaintAlphaMask, type PaintBinaryMask } from '../_types'
2
+ import { macro_halfAndFloor } from '../Internal/helpers'
3
+
4
+ export function makePaintBinaryMask(
5
+ mask: BinaryMask,
6
+ ): PaintBinaryMask {
7
+ return {
8
+ type: MaskType.BINARY,
9
+ data: mask.data,
10
+ w: mask.w,
11
+ h: mask.h,
12
+ centerOffsetX: -macro_halfAndFloor(mask.w),
13
+ centerOffsetY: -macro_halfAndFloor(mask.h),
14
+ }
15
+ }
16
+
17
+ export function makePaintAlphaMask(
18
+ mask: AlphaMask,
19
+ ): PaintAlphaMask {
20
+ return {
21
+ type: MaskType.ALPHA,
22
+ data: mask.data,
23
+ w: mask.w,
24
+ h: mask.h,
25
+ centerOffsetX: -macro_halfAndFloor(mask.w),
26
+ centerOffsetY: -macro_halfAndFloor(mask.h),
27
+ }
28
+ }
@@ -0,0 +1,47 @@
1
+ import { MaskType, type PaintAlphaMask } from '../_types'
2
+ import { macro_halfAndFloor } from '../Internal/helpers'
3
+
4
+ export function makeRectFalloffPaintAlphaMask(
5
+ width: number,
6
+ height: number,
7
+ fallOff: (d: number) => number = (d) => d,
8
+ ): PaintAlphaMask {
9
+ const fPx = Math.floor(width / 2)
10
+ const fPy = Math.floor(height / 2)
11
+
12
+ const invHalfW = 2 / width
13
+ const invHalfH = 2 / height
14
+
15
+ const offX = (width % 2 === 0) ? 0.5 : 0
16
+ const offY = (height % 2 === 0) ? 0.5 : 0
17
+
18
+ const area = width * height
19
+ const data = new Uint8Array(area)
20
+
21
+ for (let y = 0; y < height; y++) {
22
+ const dy = Math.abs((y - fPy) + offY) * invHalfH
23
+ const rowOffset = y * width
24
+
25
+ for (let x = 0; x < width; x++) {
26
+ const dx = Math.abs((x - fPx) + offX) * invHalfW
27
+
28
+ // Chebyshev distance (square/rect shape)
29
+ const dist = dx > dy ? dx : dy
30
+ // Pass 1.0 at center, 0.0 at edge
31
+ const strength = fallOff(1 - dist)
32
+ if (strength > 0) {
33
+ const intensity = (strength * 255) | 0
34
+ data[rowOffset + x] = Math.max(0, Math.min(255, intensity))
35
+ }
36
+ }
37
+ }
38
+
39
+ return {
40
+ type: MaskType.ALPHA,
41
+ data: data,
42
+ w: width,
43
+ h: height,
44
+ centerOffsetX: -macro_halfAndFloor(width),
45
+ centerOffsetY: -macro_halfAndFloor(height),
46
+ }
47
+ }
@@ -1,6 +1,6 @@
1
- import type { IPixelData } from '../_types'
1
+ import type { IPixelData32 } from '../_types'
2
2
 
3
- export class PixelBuffer32 implements IPixelData {
3
+ export class PixelBuffer32 implements IPixelData32 {
4
4
  readonly data32: Uint32Array
5
5
 
6
6
  constructor(
@@ -1,7 +1,7 @@
1
- import type { ImageDataLike, ImageDataLikeConstructor, IPixelData } from '../_types'
1
+ import type { ImageDataLike, IPixelData } from '../_types'
2
2
  import { imageDataToUInt32Array } from '../ImageData/imageDataToUInt32Array'
3
3
 
4
- export class PixelData<T extends ImageDataLike = ImageData> implements IPixelData {
4
+ export class PixelData<T extends ImageDataLike = ImageData> implements IPixelData<T> {
5
5
  readonly data32: Uint32Array
6
6
  readonly imageData: T
7
7
  readonly width: number
@@ -20,30 +20,4 @@ export class PixelData<T extends ImageDataLike = ImageData> implements IPixelDat
20
20
  ;(this as any).width = imageData.width
21
21
  ;(this as any).height = imageData.height
22
22
  }
23
-
24
- // should only be used for debug and testing
25
- copy(): PixelData<T> {
26
- const data = this.imageData.data
27
- const buffer = new Uint8ClampedArray(data)
28
- const Ctor = this.imageData.constructor
29
- const isCtorValid = typeof Ctor === 'function'
30
-
31
- let newImageData: T
32
- if (isCtorValid && Ctor !== Object) {
33
- const ImageConstructor = Ctor as ImageDataLikeConstructor<T>
34
- newImageData = new ImageConstructor(
35
- buffer,
36
- this.width,
37
- this.height,
38
- )
39
- } else {
40
- newImageData = {
41
- width: this.width,
42
- height: this.height,
43
- data: buffer,
44
- } as unknown as T
45
- }
46
-
47
- return new PixelData<T>(newImageData)
48
- }
49
23
  }
@@ -1,14 +1,15 @@
1
- import { type AlphaMask, type ApplyMaskToPixelDataOptions, type IPixelData } from '../_types'
1
+ import { type AlphaMask, type ApplyMaskToPixelDataOptions, type Color32, type IPixelData32 } from '../_types'
2
2
 
3
3
  /**
4
4
  * Directly applies a mask to a region of PixelData,
5
5
  * modifying the destination's alpha channel in-place.
6
+ * @returns true if any pixels were actually modified.
6
7
  */
7
8
  export function applyAlphaMaskToPixelData(
8
- dst: IPixelData,
9
+ dst: IPixelData32,
9
10
  mask: AlphaMask,
10
11
  opts: ApplyMaskToPixelDataOptions = {},
11
- ): void {
12
+ ): boolean {
12
13
  const {
13
14
  x: targetX = 0,
14
15
  y: targetY = 0,
@@ -20,7 +21,7 @@ export function applyAlphaMaskToPixelData(
20
21
  invertMask = false,
21
22
  } = opts
22
23
 
23
- if (globalAlpha === 0) return
24
+ if (globalAlpha === 0) return false
24
25
 
25
26
  // 1. Initial Destination Clipping
26
27
  let x = targetX
@@ -41,12 +42,12 @@ export function applyAlphaMaskToPixelData(
41
42
  w = Math.min(w, dst.width - x)
42
43
  h = Math.min(h, dst.height - y)
43
44
 
44
- if (w <= 0) return
45
- if (h <= 0) return
45
+ if (w <= 0) return false
46
+ if (h <= 0) return false
46
47
 
47
48
  // 2. Determine Source Dimensions
48
49
  const mPitch = mask.w
49
- if (mPitch <= 0) return
50
+ if (mPitch <= 0) return false
50
51
 
51
52
  // 3. Source Bounds Clipping
52
53
  // Calculate where we would start reading in the mask
@@ -63,8 +64,8 @@ export function applyAlphaMaskToPixelData(
63
64
  const finalH = sY1 - sY0
64
65
 
65
66
  // This is where your failing tests are now caught
66
- if (finalW <= 0) return
67
- if (finalH <= 0) return
67
+ if (finalW <= 0) return false
68
+ if (finalH <= 0) return false
68
69
 
69
70
  // 4. Align Destination with Source Clipping
70
71
  // If the source was clipped on the top/left, we must shift the destination start
@@ -80,6 +81,7 @@ export function applyAlphaMaskToPixelData(
80
81
  let dIdx = (y + yShift) * dw + (x + xShift)
81
82
  let mIdx = sY0 * mPitch + sX0
82
83
 
84
+ let didChange = false
83
85
  for (let iy = 0; iy < h; iy++) {
84
86
  for (let ix = 0; ix < w; ix++) {
85
87
  const mVal = maskData[mIdx]
@@ -101,6 +103,7 @@ export function applyAlphaMaskToPixelData(
101
103
  if (weight === 0) {
102
104
  // Clear alpha channel
103
105
  dst32[dIdx] = (dst32[dIdx] & 0x00ffffff) >>> 0
106
+ didChange = true
104
107
  } else if (weight !== 255) {
105
108
  // Merge alpha channel
106
109
  const d = dst32[dIdx]
@@ -108,7 +111,13 @@ export function applyAlphaMaskToPixelData(
108
111
 
109
112
  if (da !== 0) {
110
113
  const finalAlpha = da === 255 ? weight : (da * weight + 128) >> 8
111
- dst32[dIdx] = ((d & 0x00ffffff) | (finalAlpha << 24)) >>> 0
114
+
115
+ const current = dst32[dIdx] as Color32
116
+ const next = ((d & 0x00ffffff) | (finalAlpha << 24)) >>> 0
117
+ if (current !== next) {
118
+ dst32[dIdx] = next
119
+ didChange = true
120
+ }
112
121
  }
113
122
  }
114
123
 
@@ -119,4 +128,5 @@ export function applyAlphaMaskToPixelData(
119
128
  dIdx += dStride
120
129
  mIdx += mStride
121
130
  }
131
+ return didChange
122
132
  }