pixel-data-js 0.23.0 → 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 +7 -6
  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
@@ -12,13 +12,14 @@ import { sourceOverPerfect } from '../BlendModes/blend-modes-perfect'
12
12
  * @param color - The solid color to apply.
13
13
  * @param mask - The mask defining the per-pixel opacity of the target area.
14
14
  * @param opts - Configuration options including placement coordinates, bounds, global alpha, and mask offsets.
15
+ * @returns true if any pixels were actually modified.
15
16
  */
16
17
  export function blendColorPixelDataBinaryMask(
17
18
  dst: IPixelData,
18
19
  color: Color32,
19
20
  mask: BinaryMask,
20
21
  opts: ColorBlendMaskOptions = {},
21
- ) {
22
+ ): boolean {
22
23
  const targetX = opts.x ?? 0
23
24
  const targetY = opts.y ?? 0
24
25
  let w = opts.w ?? mask.w
@@ -29,12 +30,12 @@ export function blendColorPixelDataBinaryMask(
29
30
  const my = opts.my ?? 0
30
31
  const invertMask = opts.invertMask ?? false
31
32
 
32
- if (globalAlpha === 0) return
33
+ if (globalAlpha === 0) return false
33
34
 
34
35
  const baseSrcAlpha = (color >>> 24)
35
36
  const isOverwrite = (blendFn as any).isOverwrite || false
36
37
 
37
- if (baseSrcAlpha === 0 && !isOverwrite) return
38
+ if (baseSrcAlpha === 0 && !isOverwrite) return false
38
39
 
39
40
  let x = targetX
40
41
  let y = targetY
@@ -52,19 +53,18 @@ export function blendColorPixelDataBinaryMask(
52
53
  const actualW = Math.min(w, dst.width - x)
53
54
  const actualH = Math.min(h, dst.height - y)
54
55
 
55
- if (actualW <= 0 || actualH <= 0) return
56
+ if (actualW <= 0 || actualH <= 0) return false
56
57
 
57
58
  let baseColorWithGlobalAlpha = color
58
59
 
59
60
  if (globalAlpha < 255) {
60
61
  const a = (baseSrcAlpha * globalAlpha + 128) >> 8
61
- if (a === 0 && !isOverwrite) return
62
+ if (a === 0 && !isOverwrite) return false
62
63
  baseColorWithGlobalAlpha = ((color & 0x00ffffff) | (a << 24)) >>> 0 as Color32
63
64
  }
64
65
 
65
66
  const dx = (x - targetX) | 0
66
67
  const dy = (y - targetY) | 0
67
-
68
68
  const dst32 = dst.data32
69
69
  const dw = dst.width
70
70
  const mPitch = mask.w
@@ -75,6 +75,7 @@ export function blendColorPixelDataBinaryMask(
75
75
  const dStride = (dw - actualW) | 0
76
76
  const mStride = (mPitch - actualW) | 0
77
77
  const skipVal = invertMask ? 1 : 0
78
+ let didChange = false
78
79
 
79
80
  for (let iy = 0; iy < actualH; iy++) {
80
81
  for (let ix = 0; ix < actualW; ix++) {
@@ -84,7 +85,13 @@ export function blendColorPixelDataBinaryMask(
84
85
  continue
85
86
  }
86
87
 
87
- dst32[dIdx] = blendFn(baseColorWithGlobalAlpha, dst32[dIdx] as Color32)
88
+ const current = dst32[dIdx] as Color32
89
+ const next = blendFn(baseColorWithGlobalAlpha, current)
90
+
91
+ if (current !== next) {
92
+ dst32[dIdx] = next
93
+ didChange = true
94
+ }
88
95
 
89
96
  dIdx++
90
97
  mIdx++
@@ -93,4 +100,6 @@ export function blendColorPixelDataBinaryMask(
93
100
  dIdx += dStride
94
101
  mIdx += mStride
95
102
  }
103
+
104
+ return didChange
96
105
  }
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  type BlendColor32,
3
- type CircleBrushMask,
3
+ type CircleMask,
4
4
  type Color32,
5
5
  type ColorBlendMaskOptions,
6
6
  type IPixelData,
@@ -13,7 +13,7 @@ import { blendColorPixelDataAlphaMask } from './blendColorPixelDataAlphaMask'
13
13
  import { blendColorPixelDataBinaryMask } from './blendColorPixelDataBinaryMask'
14
14
 
15
15
  /**
16
- * Applies a circular brush to pixel data using a pre-calculated alpha mask.
16
+ * Applies a circular mask to pixel data using a pre-calculated alpha mask.
17
17
  *
18
18
  * @param target The PixelData to modify.
19
19
  * @param color The brush color.
@@ -25,17 +25,17 @@ import { blendColorPixelDataBinaryMask } from './blendColorPixelDataBinaryMask'
25
25
  * @param scratchOptions
26
26
  * @param bounds precalculated result from {@link getCircleBrushOrPencilBounds}
27
27
  */
28
- export function applyCircleBrushToPixelData(
28
+ export function blendColorPixelDataCircleMask(
29
29
  target: IPixelData,
30
30
  color: Color32,
31
31
  centerX: number,
32
32
  centerY: number,
33
- brush: CircleBrushMask,
33
+ brush: CircleMask,
34
34
  alpha = 255,
35
35
  blendFn: BlendColor32 = sourceOverPerfect,
36
36
  scratchOptions: ColorBlendMaskOptions = {},
37
37
  bounds?: Rect,
38
- ): void {
38
+ ): boolean {
39
39
  const b = bounds ?? getCircleBrushOrPencilBounds(
40
40
  centerX,
41
41
  centerY,
@@ -44,7 +44,7 @@ export function applyCircleBrushToPixelData(
44
44
  target.height,
45
45
  )
46
46
 
47
- if (b.w <= 0 || b.h <= 0) return
47
+ if (b.w <= 0 || b.h <= 0) return false
48
48
 
49
49
  const unclippedStartX = Math.floor(centerX + brush.minOffset)
50
50
  const unclippedStartY = Math.floor(centerY + brush.minOffset)
@@ -59,9 +59,8 @@ export function applyCircleBrushToPixelData(
59
59
  const ih = ib - iy
60
60
 
61
61
  // If the mask falls entirely outside the bounds, exit
62
- if (iw <= 0 || ih <= 0) return
62
+ if (iw <= 0 || ih <= 0) return false
63
63
 
64
- // Apply the intersected coordinates and internal mask offsets
65
64
  scratchOptions.x = ix
66
65
  scratchOptions.y = iy
67
66
  scratchOptions.w = iw
@@ -72,7 +71,7 @@ export function applyCircleBrushToPixelData(
72
71
  scratchOptions.blendFn = blendFn
73
72
 
74
73
  if (brush.type === MaskType.ALPHA) {
75
- blendColorPixelDataAlphaMask(
74
+ return blendColorPixelDataAlphaMask(
76
75
  target,
77
76
  color,
78
77
  brush,
@@ -81,11 +80,13 @@ export function applyCircleBrushToPixelData(
81
80
  }
82
81
 
83
82
  if (brush.type === MaskType.BINARY) {
84
- blendColorPixelDataBinaryMask(
83
+ return blendColorPixelDataBinaryMask(
85
84
  target,
86
85
  color,
87
86
  brush,
88
87
  scratchOptions,
89
88
  )
90
89
  }
90
+
91
+ return false
91
92
  }
@@ -0,0 +1,47 @@
1
+ import type { BlendColor32, Color32, IPixelData } from '../_types'
2
+ import { sourceOverPerfect } from '../BlendModes/blend-modes-perfect'
3
+
4
+ export function blendPixel(
5
+ target: IPixelData,
6
+ x: number,
7
+ y: number,
8
+ color: Color32,
9
+ alpha: number = 255,
10
+ blendFn: BlendColor32 = sourceOverPerfect,
11
+ ): boolean {
12
+ if (alpha === 0) return false
13
+
14
+ let width = target.width
15
+ let height = target.height
16
+
17
+ if (x < 0 || x >= width || y < 0 || y >= height) return false
18
+
19
+ let srcAlpha = color >>> 24
20
+ let isOverwrite = blendFn.isOverwrite
21
+
22
+ // Early exit for transparent source unless we are in an overwrite mode
23
+ if (srcAlpha === 0 && !isOverwrite) return false
24
+
25
+ let dst32 = target.data32
26
+ let index = y * width + x
27
+ let finalColor = color
28
+
29
+ if (alpha !== 255) {
30
+ let finalAlpha = (srcAlpha * alpha + 128) >> 8
31
+
32
+ if (finalAlpha === 0 && !isOverwrite) return false
33
+
34
+ finalColor = (((color & 0x00ffffff) | (finalAlpha << 24)) >>> 0) as Color32
35
+ }
36
+
37
+ let current = dst32[index] as Color32
38
+ let next = blendFn(finalColor, current)
39
+
40
+ if (current !== next) {
41
+ dst32[index] = next
42
+
43
+ return true
44
+ }
45
+
46
+ return false
47
+ }
@@ -19,7 +19,7 @@ export function blendPixelData(
19
19
  dst: IPixelData,
20
20
  src: IPixelData,
21
21
  opts: PixelBlendOptions = {},
22
- ) {
22
+ ): boolean {
23
23
  const {
24
24
  x: targetX = 0,
25
25
  y: targetY = 0,
@@ -31,7 +31,7 @@ export function blendPixelData(
31
31
  blendFn = sourceOverPerfect,
32
32
  } = opts
33
33
 
34
- if (globalAlpha === 0) return
34
+ if (globalAlpha === 0) return false
35
35
 
36
36
  let x = targetX
37
37
  let y = targetY
@@ -65,7 +65,7 @@ export function blendPixelData(
65
65
 
66
66
  const actualW = Math.min(w, dst.width - x)
67
67
  const actualH = Math.min(h, dst.height - y)
68
- if (actualW <= 0 || actualH <= 0) return
68
+ if (actualW <= 0 || actualH <= 0) return false
69
69
 
70
70
  const dst32 = dst.data32
71
71
  const src32 = src.data32
@@ -79,6 +79,7 @@ export function blendPixelData(
79
79
  const sStride = (sw - actualW) | 0
80
80
  const isOpaque = globalAlpha === 255
81
81
  const isOverwrite = blendFn.isOverwrite
82
+ let didChange = false
82
83
 
83
84
  for (let iy = 0; iy < actualH; iy++) {
84
85
  for (let ix = 0; ix < actualW; ix++) {
@@ -102,11 +103,20 @@ export function blendPixelData(
102
103
  finalCol = ((srcCol & 0x00ffffff) | (a << 24)) >>> 0 as Color32
103
104
  }
104
105
 
105
- dst32[dIdx] = blendFn(finalCol, dst32[dIdx] as Color32)
106
+ const current = dst32[dIdx] as Color32
107
+ const next = blendFn(finalCol, dst32[dIdx] as Color32)
108
+
109
+ if (current !== next) {
110
+ dst32[dIdx] = next
111
+ didChange = true
112
+ }
113
+
106
114
  dIdx++
107
115
  sIdx++
108
116
  }
109
117
  dIdx += dStride
110
118
  sIdx += sStride
111
119
  }
120
+
121
+ return didChange
112
122
  }
@@ -6,7 +6,7 @@ export function blendPixelDataAlphaMask(
6
6
  src: IPixelData,
7
7
  alphaMask: AlphaMask,
8
8
  opts: PixelBlendMaskOptions = {},
9
- ) {
9
+ ): boolean {
10
10
  const {
11
11
  x: targetX = 0,
12
12
  y: targetY = 0,
@@ -21,7 +21,7 @@ export function blendPixelDataAlphaMask(
21
21
  invertMask = false,
22
22
  } = opts
23
23
 
24
- if (globalAlpha === 0) return
24
+ if (globalAlpha === 0) return false
25
25
 
26
26
  let x = targetX
27
27
  let y = targetY
@@ -56,7 +56,7 @@ export function blendPixelDataAlphaMask(
56
56
 
57
57
  const actualW = Math.min(w, dst.width - x)
58
58
  const actualH = Math.min(h, dst.height - y)
59
- if (actualW <= 0 || actualH <= 0) return
59
+ if (actualW <= 0 || actualH <= 0) return false
60
60
 
61
61
  // 2. Index Setup
62
62
  const dw = dst.width
@@ -82,6 +82,7 @@ export function blendPixelDataAlphaMask(
82
82
 
83
83
  const isOpaque = globalAlpha === 255
84
84
  const isOverwrite = blendFn.isOverwrite || false
85
+ let didChange = false
85
86
 
86
87
  for (let iy = 0; iy < actualH; iy++) {
87
88
  for (let ix = 0; ix < actualW; ix++) {
@@ -135,8 +136,13 @@ export function blendPixelDataAlphaMask(
135
136
  }
136
137
  finalCol = ((srcCol & 0x00ffffff) | (a << 24)) >>> 0 as Color32
137
138
  }
139
+ const current = dst32[dIdx] as Color32
140
+ const next = blendFn(finalCol, dst32[dIdx] as Color32)
138
141
 
139
- dst32[dIdx] = blendFn(finalCol, dst32[dIdx] as Color32)
142
+ if (current !== next) {
143
+ dst32[dIdx] = next
144
+ didChange = true
145
+ }
140
146
 
141
147
  dIdx++
142
148
  sIdx++
@@ -146,4 +152,6 @@ export function blendPixelDataAlphaMask(
146
152
  sIdx += sStride
147
153
  mIdx += mStride
148
154
  }
155
+
156
+ return didChange
149
157
  }
@@ -6,7 +6,7 @@ export function blendPixelDataBinaryMask(
6
6
  src: IPixelData,
7
7
  binaryMask: BinaryMask,
8
8
  opts: PixelBlendMaskOptions = {},
9
- ) {
9
+ ): boolean {
10
10
  const {
11
11
  x: targetX = 0,
12
12
  y: targetY = 0,
@@ -21,7 +21,7 @@ export function blendPixelDataBinaryMask(
21
21
  invertMask = false,
22
22
  } = opts
23
23
 
24
- if (globalAlpha === 0) return
24
+ if (globalAlpha === 0) return false
25
25
 
26
26
  let x = targetX
27
27
  let y = targetY
@@ -59,7 +59,7 @@ export function blendPixelDataBinaryMask(
59
59
  const actualW = Math.min(w, dst.width - x)
60
60
  const actualH = Math.min(h, dst.height - y)
61
61
 
62
- if (actualW <= 0 || actualH <= 0) return
62
+ if (actualW <= 0 || actualH <= 0) return false
63
63
 
64
64
  // 3. Coordinate Displacement for Mask Sync
65
65
  // dx/dy represents how far the clipped start is from the requested start.
@@ -85,6 +85,7 @@ export function blendPixelDataBinaryMask(
85
85
  const skipVal = invertMask ? 1 : 0
86
86
  const isOpaque = globalAlpha === 255
87
87
  const isOverwrite = blendFn.isOverwrite || false
88
+ let didChange = false
88
89
 
89
90
  for (let iy = 0; iy < actualH; iy++) {
90
91
  for (let ix = 0; ix < actualW; ix++) {
@@ -120,7 +121,13 @@ export function blendPixelDataBinaryMask(
120
121
  finalCol = ((srcCol & 0x00ffffff) | (a << 24)) >>> 0 as Color32
121
122
  }
122
123
 
123
- dst32[dIdx] = blendFn(finalCol, dst32[dIdx] as Color32)
124
+ const current = dst32[dIdx] as Color32
125
+ const next = blendFn(finalCol, dst32[dIdx] as Color32)
126
+
127
+ if (current !== next) {
128
+ dst32[dIdx] = next
129
+ didChange = true
130
+ }
124
131
 
125
132
  dIdx++
126
133
  sIdx++
@@ -130,4 +137,6 @@ export function blendPixelDataBinaryMask(
130
137
  sIdx += sStride
131
138
  mIdx += mStride
132
139
  }
140
+
141
+ return didChange
133
142
  }
@@ -0,0 +1,37 @@
1
+ import type { BlendColor32, IPixelData } from '../_types'
2
+ import { blendPixelData } from './blendPixelData'
3
+ import type { PaintBuffer } from '../PixelTile/PaintBuffer'
4
+
5
+ const SCRATCH_OPTS = {
6
+ x: 0,
7
+ y: 0,
8
+ alpha: 255,
9
+ blendFn: undefined as BlendColor32 | undefined,
10
+ }
11
+
12
+ export function blendPixelDataPaintBuffer(
13
+ paintBuffer: PaintBuffer,
14
+ target: IPixelData,
15
+ alpha = 255,
16
+ blendFn?: BlendColor32,
17
+ blendPixelDataFn = blendPixelData,
18
+ ): void {
19
+ const tileShift = paintBuffer.config.tileShift
20
+ const lookup = paintBuffer.lookup
21
+ for (let i = 0; i < lookup.length; i++) {
22
+ const tile = lookup[i]
23
+
24
+ if (tile) {
25
+ const x = tile.tx << tileShift
26
+ const y = tile.ty << tileShift
27
+
28
+ SCRATCH_OPTS.x = x
29
+ SCRATCH_OPTS.y = y
30
+ SCRATCH_OPTS.alpha = alpha
31
+ SCRATCH_OPTS.blendFn = blendFn
32
+
33
+ blendPixelDataFn(target, tile, SCRATCH_OPTS)
34
+ }
35
+ }
36
+ }
37
+
@@ -1,5 +1,5 @@
1
1
  import type { BinaryMaskRect, Color32, IPixelData } from '../_types'
2
- import { fillPixelData } from './fillPixelData'
2
+ import { fillPixelDataFast } from './fillPixelDataFast'
3
3
 
4
4
  /**
5
5
  * Clears a region of the PixelData to transparent (0x00000000).
@@ -9,5 +9,5 @@ export function clearPixelData(
9
9
  dst: IPixelData,
10
10
  rect?: Partial<BinaryMaskRect>,
11
11
  ): void {
12
- fillPixelData(dst, 0 as Color32, rect)
12
+ fillPixelDataFast(dst, 0 as Color32, rect)
13
13
  }
@@ -9,13 +9,13 @@ const SCRATCH_RECT = makeClippedRect()
9
9
  * @param dst - The target to modify.
10
10
  * @param color - The color to apply.
11
11
  * @param rect - Defines the area to fill. If omitted, the entire
12
- * buffer is filled.
12
+ * @returns true if any pixels were actually modified.
13
13
  */
14
14
  export function fillPixelData(
15
15
  dst: IPixelData,
16
16
  color: Color32,
17
17
  rect?: Partial<Rect>,
18
- ): void
18
+ ): boolean
19
19
  /**
20
20
  * @param dst - The target to modify.
21
21
  * @param color - The color to apply.
@@ -31,7 +31,7 @@ export function fillPixelData(
31
31
  y: number,
32
32
  w: number,
33
33
  h: number,
34
- ): void
34
+ ): boolean
35
35
  export function fillPixelData(
36
36
  dst: IPixelData,
37
37
  color: Color32,
@@ -39,7 +39,7 @@ export function fillPixelData(
39
39
  _y?: number,
40
40
  _w?: number,
41
41
  _h?: number,
42
- ): void {
42
+ ): boolean {
43
43
  let x: number
44
44
  let y: number
45
45
  let w: number
@@ -62,11 +62,18 @@ export function fillPixelData(
62
62
  h = dst.height
63
63
  }
64
64
 
65
- const clip = resolveRectClipping(x, y, w, h, dst.width, dst.height, SCRATCH_RECT)
65
+ const clip = resolveRectClipping(
66
+ x,
67
+ y,
68
+ w,
69
+ h,
70
+ dst.width,
71
+ dst.height,
72
+ SCRATCH_RECT,
73
+ )
66
74
 
67
- if (!clip.inBounds) return
75
+ if (!clip.inBounds) return false
68
76
 
69
- // Use the clipped values
70
77
  const {
71
78
  x: finalX,
72
79
  y: finalY,
@@ -76,17 +83,20 @@ export function fillPixelData(
76
83
 
77
84
  const dst32 = dst.data32
78
85
  const dw = dst.width
86
+ let hasChanged = false
79
87
 
80
- // Optimization: If filling the entire buffer, use the native .fill()
81
- if (actualW === dw && actualH === dst.height && finalX === 0 && finalY === 0) {
82
- dst32.fill(color)
83
- return
84
- }
85
-
86
- // Row-by-row fill for partial rectangles
87
88
  for (let iy = 0; iy < actualH; iy++) {
88
- const start = (finalY + iy) * dw + finalX
89
+ const rowOffset = (finalY + iy) * dw
90
+ const start = rowOffset + finalX
89
91
  const end = start + actualW
90
- dst32.fill(color, start, end)
92
+
93
+ for (let i = start; i < end; i++) {
94
+ if (dst32[i] !== color) {
95
+ dst32[i] = color
96
+ hasChanged = true
97
+ }
98
+ }
91
99
  }
100
+
101
+ return hasChanged
92
102
  }
@@ -19,8 +19,8 @@ export function fillPixelDataBinaryMask(
19
19
  alpha = 255,
20
20
  x = 0,
21
21
  y = 0,
22
- ): void {
23
- if (alpha === 0) return
22
+ ): boolean {
23
+ if (alpha === 0) return false
24
24
 
25
25
  const maskW = mask.w
26
26
  const maskH = mask.h
@@ -35,7 +35,7 @@ export function fillPixelDataBinaryMask(
35
35
  SCRATCH_RECT,
36
36
  )
37
37
 
38
- if (!clip.inBounds) return
38
+ if (!clip.inBounds) return false
39
39
 
40
40
  const {
41
41
  x: finalX,
@@ -59,6 +59,8 @@ export function fillPixelDataBinaryMask(
59
59
  finalCol = ((colorRGB | (a << 24)) >>> 0) as Color32
60
60
  }
61
61
 
62
+ let hasChanged = false
63
+
62
64
  for (let iy = 0; iy < actualH; iy++) {
63
65
  const currentY = finalY + iy
64
66
  const maskY = currentY - y
@@ -72,8 +74,14 @@ export function fillPixelDataBinaryMask(
72
74
  const maskIndex = maskOffset + maskX
73
75
 
74
76
  if (maskData[maskIndex]) {
75
- dst32[dstRowOffset + currentX] = finalCol
77
+ const current = dst32[dstRowOffset + currentX]
78
+ if (current !== finalCol) {
79
+ dst32[dstRowOffset + currentX] = finalCol
80
+ hasChanged = true
81
+ }
76
82
  }
77
83
  }
78
84
  }
85
+
86
+ return hasChanged
79
87
  }
@@ -0,0 +1,94 @@
1
+ import type { Color32, IPixelData, Rect } from '../_types'
2
+ import { makeClippedRect, resolveRectClipping } from '../Internal/resolveClipping'
3
+
4
+ const SCRATCH_RECT = makeClippedRect()
5
+
6
+ /**
7
+ * Fills a region or the {@link IPixelData} buffer with a solid color.
8
+ * This function is faster than {@link fillPixelData} but does not
9
+ * return a boolean value indicating changes were made.
10
+ *
11
+ * @param dst - The target to modify.
12
+ * @param color - The color to apply.
13
+ * @param rect - Defines the area to fill. If omitted, the entire
14
+ * buffer is filled.
15
+ */
16
+ export function fillPixelDataFast(
17
+ dst: IPixelData,
18
+ color: Color32,
19
+ rect?: Partial<Rect>,
20
+ ): void
21
+ /**
22
+ * @param dst - The target to modify.
23
+ * @param color - The color to apply.
24
+ * @param x - Starting horizontal coordinate.
25
+ * @param y - Starting vertical coordinate.
26
+ * @param w - Width of the fill area.
27
+ * @param h - Height of the fill area.
28
+ */
29
+ export function fillPixelDataFast(
30
+ dst: IPixelData,
31
+ color: Color32,
32
+ x: number,
33
+ y: number,
34
+ w: number,
35
+ h: number,
36
+ ): void
37
+ export function fillPixelDataFast(
38
+ dst: IPixelData,
39
+ color: Color32,
40
+ _x?: Partial<Rect> | number,
41
+ _y?: number,
42
+ _w?: number,
43
+ _h?: number,
44
+ ): void {
45
+ let x: number
46
+ let y: number
47
+ let w: number
48
+ let h: number
49
+
50
+ if (typeof _x === 'object') {
51
+ x = _x.x ?? 0
52
+ y = _x.y ?? 0
53
+ w = _x.w ?? dst.width
54
+ h = _x.h ?? dst.height
55
+ } else if (typeof _x === 'number') {
56
+ x = _x
57
+ y = _y!
58
+ w = _w!
59
+ h = _h!
60
+ } else {
61
+ x = 0
62
+ y = 0
63
+ w = dst.width
64
+ h = dst.height
65
+ }
66
+
67
+ const clip = resolveRectClipping(x, y, w, h, dst.width, dst.height, SCRATCH_RECT)
68
+
69
+ if (!clip.inBounds) return
70
+
71
+ // Use the clipped values
72
+ const {
73
+ x: finalX,
74
+ y: finalY,
75
+ w: actualW,
76
+ h: actualH,
77
+ } = clip
78
+
79
+ const dst32 = dst.data32
80
+ const dw = dst.width
81
+
82
+ // Optimization: If filling the entire buffer, use the native .fill()
83
+ if (actualW === dw && actualH === dst.height && finalX === 0 && finalY === 0) {
84
+ dst32.fill(color)
85
+ return
86
+ }
87
+
88
+ // Row-by-row fill for partial rectangles
89
+ for (let iy = 0; iy < actualH; iy++) {
90
+ const start = (finalY + iy) * dw + finalX
91
+ const end = start + actualW
92
+ dst32.fill(color, start, end)
93
+ }
94
+ }
@@ -6,7 +6,7 @@ const SCRATCH_RECT = makeClippedRect()
6
6
  export function invertPixelData(
7
7
  pixelData: IPixelData,
8
8
  opts: PixelMutateOptions = {},
9
- ): void {
9
+ ): boolean {
10
10
  const dst = pixelData
11
11
  const {
12
12
  x: targetX = 0,
@@ -21,7 +21,7 @@ export function invertPixelData(
21
21
 
22
22
  const clip = resolveRectClipping(targetX, targetY, width, height, dst.width, dst.height, SCRATCH_RECT)
23
23
 
24
- if (!clip.inBounds) return
24
+ if (!clip.inBounds) return false
25
25
 
26
26
  const {
27
27
  x,
@@ -72,4 +72,6 @@ export function invertPixelData(
72
72
  dIdx += dStride
73
73
  }
74
74
  }
75
+
76
+ return true
75
77
  }