pixel-data-js 0.31.0 → 0.33.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.31.0",
4
+ "version": "0.33.0",
5
5
  "packageManager": "pnpm@10.33.0",
6
6
  "description": "JS Pixel and ImageData operations",
7
7
  "author": {
@@ -1,7 +1,9 @@
1
1
  import type { ImageDataLike } from './_ImageData-types'
2
2
 
3
- export function copyImageData({ data, width, height }: ImageDataLike): ImageData {
4
- return new ImageData(data.slice(), width, height)
3
+ export function copyImageData(source: ImageDataLike): ImageData {
4
+ const dataCopy = new Uint8ClampedArray(source.data)
5
+
6
+ return new ImageData(dataCopy, source.width, source.height)
5
7
  }
6
8
 
7
9
  export function copyImageDataLike({ data, width, height }: ImageDataLike): ImageDataLike {
@@ -0,0 +1,54 @@
1
+ import type { Rect } from '../Rect/_rect-types'
2
+ import type { ImageDataLike } from './_ImageData-types'
3
+ import { extractImageDataBuffer } from './extractImageDataBuffer'
4
+
5
+ /**
6
+ * Extracts a specific rectangular region of pixels from a larger {@link ImageDataLike}
7
+ * source into a new {@link Uint8ClampedArray}.
8
+ *
9
+ * This is a "read-only" operation that returns a copy of the pixel data.
10
+ *
11
+ * @param imageData - The source image data to read from.
12
+ * @param rect - A rect defining the region to extract.
13
+ * @returns A buffer containing the RGBA pixel data of the region.
14
+ */
15
+ export function extractImageData(
16
+ imageData: ImageDataLike,
17
+ rect: Rect,
18
+ ): ImageData | null
19
+ /**
20
+ * @param imageData - The source image data to read from.
21
+ * @param x - The starting horizontal coordinate.
22
+ * @param y - The starting vertical coordinate.
23
+ * @param w - The width of the region to extract.
24
+ * @param h - The height of the region to extract.
25
+ * @returns A buffer containing the RGBA pixel data of the region.
26
+ */
27
+ export function extractImageData(
28
+ imageData: ImageDataLike,
29
+ x: number,
30
+ y: number,
31
+ w: number,
32
+ h: number,
33
+ ): ImageData | null
34
+ export function extractImageData(
35
+ imageData: ImageDataLike,
36
+ _x: Rect | number,
37
+ _y?: number,
38
+ _w?: number,
39
+ _h?: number,
40
+ ): ImageData | null {
41
+ const { x, y, w, h } = typeof _x === 'object'
42
+ ? _x
43
+ : { x: _x, y: _y!, w: _w!, h: _h! }
44
+
45
+ if (w <= 0 || h <= 0) return null
46
+
47
+ const result = new ImageData(w, h)
48
+
49
+ const buffer = extractImageDataBuffer(imageData, x, y, w, h)
50
+ result.data.set(buffer)
51
+
52
+ return result
53
+
54
+ }
@@ -1,9 +1,6 @@
1
1
  import type { Rect } from '../Rect/_rect-types'
2
- import { makeClippedBlit, resolveBlitClipping } from '../Rect/resolveClipping'
3
2
  import type { ImageDataLike } from './_ImageData-types'
4
3
 
5
- const SCRATCH_BLIT = makeClippedBlit()
6
-
7
4
  /**
8
5
  * Extracts a specific rectangular region of pixels from a larger {@link ImageDataLike}
9
6
  * source into a new {@link Uint8ClampedArray}.
@@ -43,37 +40,67 @@ export function extractImageDataBuffer(
43
40
  const { x, y, w, h } = typeof _x === 'object'
44
41
  ? _x
45
42
  : { x: _x, y: _y!, w: _w!, h: _h! }
46
-
47
- const { width: srcW, height: srcH, data: src } = imageData
48
- // Safety check for invalid dimensions
49
43
  if (w <= 0 || h <= 0) return new Uint8ClampedArray(0)
50
- const out = new Uint8ClampedArray(w * h * 4)
51
44
 
52
- const clip = resolveBlitClipping(
53
- 0,
54
- 0,
55
- x,
56
- y,
57
- w,
58
- h,
59
- w,
60
- h,
61
- srcW,
62
- srcH,
63
- SCRATCH_BLIT,
64
- )
45
+ const srcW = imageData.width
46
+ const srcH = imageData.height
47
+ const src = imageData.data
48
+
49
+ const outLen = w * h * 4
50
+ const out = new Uint8ClampedArray(outLen)
51
+
52
+ let srcX = x
53
+ let srcY = y
54
+ let dstX = 0
55
+ let dstY = 0
56
+ let copyW = w
57
+ let copyH = h
58
+
59
+ if (srcX < 0) {
60
+ dstX = -srcX
61
+ copyW += srcX
62
+ srcX = 0
63
+ }
64
+
65
+ if (srcY < 0) {
66
+ dstY = -srcY
67
+ copyH += srcY
68
+ srcY = 0
69
+ }
70
+
71
+ copyW = Math.min(copyW, srcW - srcX)
72
+ copyH = Math.min(copyH, srcH - srcY)
73
+
74
+ if (copyW <= 0 || copyH <= 0) return out
75
+
76
+ // 2. Perform high-speed block copy
77
+ // Attempt to use a 32-bit view if the buffer is memory-aligned.
78
+ // This reduces loop iterations and arithmetic by 4x.
79
+ const isAligned = src.byteOffset % 4 === 0
80
+
81
+ if (isAligned) {
82
+ const srcLen32 = src.byteLength / 4
83
+ const src32 = new Uint32Array(src.buffer, src.byteOffset, srcLen32)
84
+ const out32 = new Uint32Array(out.buffer)
65
85
 
66
- if (!clip.inBounds) return out
86
+ for (let row = 0; row < copyH; row++) {
87
+ const srcStart = (srcY + row) * srcW + srcX
88
+ const dstStart = (dstY + row) * w + dstX
89
+ const chunk = src32.subarray(srcStart, srcStart + copyW)
67
90
 
68
- const { x: dstX, y: dstY, sx: srcX, sy: srcY, w: copyW, h: copyH } = clip
69
- const rowLen = copyW * 4
91
+ out32.set(chunk, dstStart)
92
+ }
93
+ } else {
94
+ // Fallback for unaligned data
95
+ const rowLen = copyW * 4
70
96
 
71
- for (let row = 0; row < copyH; row++) {
72
- const srcStart = ((srcY + row) * srcW + srcX) * 4
73
- const dstStart = ((dstY + row) * w + dstX) * 4
97
+ for (let row = 0; row < copyH; row++) {
98
+ const srcStart = ((srcY + row) * srcW + srcX) * 4
99
+ const dstStart = ((dstY + row) * w + dstX) * 4
100
+ const chunk = src.subarray(srcStart, srcStart + rowLen)
74
101
 
75
- // Perform the high-speed bulk copy
76
- out.set(src.subarray(srcStart, srcStart + rowLen), dstStart)
102
+ out.set(chunk, dstStart)
103
+ }
77
104
  }
78
105
 
79
106
  return out
@@ -1,97 +1,75 @@
1
- import { MaskType } from '../Mask/_mask-types'
2
- import { makeClippedBlit, resolveBlitClipping } from '../Rect/resolveClipping'
3
-
4
- const SCRATCH_BLIT = makeClippedBlit()
5
-
6
1
  /**
7
- * Writes image data from a source to a target with support for clipping and alpha masking.
2
+ * Writes image data from a source to a target.
8
3
  *
9
4
  * @param target - The destination ImageData to write to.
10
5
  * @param source - The source ImageData to read from.
11
6
  * @param x - The x-coordinate in the target where drawing starts.
12
7
  * @param y - The y-coordinate in the target where drawing starts.
13
- * @param sx - The x-coordinate in the source to start copying from.
14
- * @param sy - The y-coordinate in the source to start copying from.
15
- * @param sw - The width of the rectangle to copy.
16
- * @param sh - The height of the rectangle to copy.
17
- * @param mask - An optional Uint8Array mask (0-255). 0 is transparent, 255 is opaque.
18
- * @param maskType - type of mask
19
8
  */
20
9
  export function writeImageData(
21
10
  target: ImageData,
22
11
  source: ImageData,
23
- x: number,
24
- y: number,
25
- sx: number = 0,
26
- sy: number = 0,
27
- sw: number = source.width,
28
- sh: number = source.height,
29
- mask: Uint8Array | null = null,
30
- maskType: MaskType = MaskType.BINARY,
12
+ x = 0,
13
+ y = 0,
31
14
  ): void {
32
15
  const dstW = target.width
33
16
  const dstH = target.height
34
- const dstData = target.data
17
+ const dst = target.data
18
+
35
19
  const srcW = source.width
36
- const srcData = source.data
20
+ const srcH = source.height
21
+ const src = source.data
37
22
 
38
- const clip = resolveBlitClipping(
39
- x, y, sx, sy, sw, sh,
40
- dstW, dstH, srcW, source.height,
41
- SCRATCH_BLIT,
42
- )
23
+ let dstX = x
24
+ let dstY = y
25
+ let srcX = 0
26
+ let srcY = 0
27
+ let copyW = srcW
28
+ let copyH = srcH
43
29
 
44
- if (!clip.inBounds) return
30
+ if (dstX < 0) {
31
+ srcX = -dstX
32
+ copyW += dstX
33
+ dstX = 0
34
+ }
45
35
 
46
- const {
47
- x: dstX,
48
- y: dstY,
49
- sx: srcX,
50
- sy: srcY,
51
- w: copyW,
52
- h: copyH,
53
- } = clip
36
+ if (dstY < 0) {
37
+ srcY = -dstY
38
+ copyH += dstY
39
+ dstY = 0
40
+ }
41
+
42
+ copyW = Math.min(copyW, dstW - dstX)
43
+ copyH = Math.min(copyH, dstH - dstY)
54
44
 
55
- const useMask = !!mask
45
+ if (copyW <= 0 || copyH <= 0) return
56
46
 
57
- for (let row = 0; row < copyH; row++) {
58
- const currentDstY = dstY + row
59
- const currentSrcY = srcY + row
47
+ const isDstAligned = dst.byteOffset % 4 === 0
48
+ const isSrcAligned = src.byteOffset % 4 === 0
60
49
 
61
- const dstStart = (currentDstY * dstW + dstX) * 4
62
- const srcStart = (currentSrcY * srcW + srcX) * 4
50
+ if (isDstAligned && isSrcAligned) {
51
+ const dstLen32 = dst.byteLength / 4
52
+ const dst32 = new Uint32Array(dst.buffer, dst.byteOffset, dstLen32)
63
53
 
64
- if (useMask && mask) {
65
- for (let ix = 0; ix < copyW; ix++) {
66
- const mi = currentSrcY * srcW + (srcX + ix)
67
- const alpha = mask[mi]
54
+ const srcLen32 = src.byteLength / 4
55
+ const src32 = new Uint32Array(src.buffer, src.byteOffset, srcLen32)
68
56
 
69
- if (alpha === 0) {
70
- continue
71
- }
57
+ for (let row = 0; row < copyH; row++) {
58
+ const dstStart = (dstY + row) * dstW + dstX
59
+ const srcStart = (srcY + row) * srcW + srcX
60
+ const chunk = src32.subarray(srcStart, srcStart + copyW)
72
61
 
73
- const di = dstStart + (ix * 4)
74
- const si = srcStart + (ix * 4)
62
+ dst32.set(chunk, dstStart)
63
+ }
64
+ } else {
65
+ const rowLen = copyW * 4
75
66
 
76
- if (maskType === MaskType.BINARY || alpha === 255) {
77
- dstData[di] = srcData[si]
78
- dstData[di + 1] = srcData[si + 1]
79
- dstData[di + 2] = srcData[si + 2]
80
- dstData[di + 3] = srcData[si + 3]
81
- } else {
82
- const a = alpha / 255
83
- const invA = 1 - a
67
+ for (let row = 0; row < copyH; row++) {
68
+ const dstStart = ((dstY + row) * dstW + dstX) * 4
69
+ const srcStart = ((srcY + row) * srcW + srcX) * 4
70
+ const chunk = src.subarray(srcStart, srcStart + rowLen)
84
71
 
85
- dstData[di] = srcData[si] * a + dstData[di] * invA
86
- dstData[di + 1] = srcData[si + 1] * a + dstData[di + 1] * invA
87
- dstData[di + 2] = srcData[si + 2] * a + dstData[di + 2] * invA
88
- dstData[di + 3] = srcData[si + 3] * a + dstData[di + 3] * invA
89
- }
90
- }
91
- } else {
92
- const byteLen = copyW * 4
93
- const sub = srcData.subarray(srcStart, srcStart + byteLen)
94
- dstData.set(sub, dstStart)
72
+ dst.set(chunk, dstStart)
95
73
  }
96
74
  }
97
75
  }
@@ -1,7 +1,4 @@
1
1
  import type { Rect } from '../Rect/_rect-types'
2
- import { makeClippedBlit, resolveBlitClipping } from '../Rect/resolveClipping'
3
-
4
- const SCRATCH_BLIT = makeClippedBlit()
5
2
 
6
3
  /**
7
4
  * Copies a pixel buffer into a specific region of an {@link ImageData} object.
@@ -43,43 +40,82 @@ export function writeImageDataBuffer(
43
40
  _w?: number,
44
41
  _h?: number,
45
42
  ): void {
46
- const { x, y, w, h } = typeof _x === 'object'
47
- ? _x
48
- : { x: _x, y: _y!, w: _w!, h: _h! }
49
-
50
- const { width: dstW, height: dstH, data: dst } = target
51
-
52
- const clip = resolveBlitClipping(
53
- x,
54
- y,
55
- 0,
56
- 0,
57
- w,
58
- h,
59
- dstW,
60
- dstH,
61
- w,
62
- h,
63
- SCRATCH_BLIT,
64
- )
65
-
66
- if (!clip.inBounds) return
67
-
68
- const {
69
- x: dstX,
70
- y: dstY,
71
- sx: srcX,
72
- sy: srcY,
73
- w: copyW,
74
- h: copyH,
75
- } = clip
76
-
77
- const rowLen = copyW * 4
78
-
79
- for (let row = 0; row < copyH; row++) {
80
- const dstStart = ((dstY + row) * dstW + dstX) * 4
81
- const srcStart = ((srcY + row) * w + srcX) * 4
82
-
83
- dst.set(data.subarray(srcStart, srcStart + rowLen), dstStart)
43
+ let x: number
44
+ let y: number
45
+ let w: number
46
+ let h: number
47
+
48
+ if (typeof _x === 'object') {
49
+ x = _x.x
50
+ y = _x.y
51
+ w = _x.w
52
+ h = _x.h
53
+ } else {
54
+ x = _x
55
+ y = _y!
56
+ w = _w!
57
+ h = _h!
58
+ }
59
+
60
+ if (w <= 0 || h <= 0) return
61
+
62
+ const dstW = target.width
63
+ const dstH = target.height
64
+ const dst = target.data
65
+
66
+ // Inline clipping logic for destination boundaries
67
+ let dstX = x
68
+ let dstY = y
69
+ let srcX = 0
70
+ let srcY = 0
71
+ let copyW = w
72
+ let copyH = h
73
+
74
+ if (dstX < 0) {
75
+ srcX = -dstX
76
+ copyW += dstX
77
+ dstX = 0
78
+ }
79
+
80
+ if (dstY < 0) {
81
+ srcY = -dstY
82
+ copyH += dstY
83
+ dstY = 0
84
+ }
85
+
86
+ copyW = Math.min(copyW, dstW - dstX)
87
+ copyH = Math.min(copyH, dstH - dstY)
88
+
89
+ if (copyW <= 0 || copyH <= 0) return
90
+
91
+ // Fast-path: Both arrays must be 4-byte aligned to use Uint32Array safely
92
+ const isDstAligned = dst.byteOffset % 4 === 0
93
+ const isSrcAligned = data.byteOffset % 4 === 0
94
+
95
+ if (isDstAligned && isSrcAligned) {
96
+ const dstLen32 = dst.byteLength / 4
97
+ const dst32 = new Uint32Array(dst.buffer, dst.byteOffset, dstLen32)
98
+
99
+ const srcLen32 = data.byteLength / 4
100
+ const src32 = new Uint32Array(data.buffer, data.byteOffset, srcLen32)
101
+
102
+ for (let row = 0; row < copyH; row++) {
103
+ const dstStart = (dstY + row) * dstW + dstX
104
+ const srcStart = (srcY + row) * w + srcX
105
+ const chunk = src32.subarray(srcStart, srcStart + copyW)
106
+
107
+ dst32.set(chunk, dstStart)
108
+ }
109
+ } else {
110
+ // Fallback for unaligned data arrays
111
+ const rowLen = copyW * 4
112
+
113
+ for (let row = 0; row < copyH; row++) {
114
+ const dstStart = ((dstY + row) * dstW + dstX) * 4
115
+ const srcStart = ((srcY + row) * w + srcX) * 4
116
+ const chunk = data.subarray(srcStart, srcStart + rowLen)
117
+
118
+ dst.set(chunk, dstStart)
119
+ }
84
120
  }
85
121
  }
@@ -1,15 +1,21 @@
1
1
  import type { Rect } from '../Rect/_rect-types'
2
- import { makeClippedBlit, resolveBlitClipping } from '../Rect/resolveClipping'
3
2
  import type { PixelData32 } from './_pixelData-types'
4
3
 
5
- const SCRATCH_BLIT = makeClippedBlit()
6
-
7
4
  /**
8
5
  * Extracts a rectangular region of pixels from PixelData.
9
6
  * Returns a new Uint32Array containing the extracted pixels.
10
7
  */
11
- export function extractPixelDataBuffer(source: PixelData32, rect: Rect): Uint32Array
12
- export function extractPixelDataBuffer(source: PixelData32, x: number, y: number, w: number, h: number): Uint32Array
8
+ export function extractPixelDataBuffer(
9
+ source: PixelData32,
10
+ rect: Rect,
11
+ ): Uint32Array
12
+ export function extractPixelDataBuffer(
13
+ source: PixelData32,
14
+ x: number,
15
+ y: number,
16
+ w: number,
17
+ h: number,
18
+ ): Uint32Array
13
19
  export function extractPixelDataBuffer(
14
20
  source: PixelData32,
15
21
  _x: Rect | number,
@@ -17,56 +23,61 @@ export function extractPixelDataBuffer(
17
23
  _w?: number,
18
24
  _h?: number,
19
25
  ): Uint32Array {
20
- const { x, y, w, h } = typeof _x === 'object'
21
- ? _x
22
- : { x: _x, y: _y!, w: _w!, h: _h! }
26
+ let x: number
27
+ let y: number
28
+ let w: number
29
+ let h: number
30
+
31
+ if (typeof _x === 'object') {
32
+ x = _x.x
33
+ y = _x.y
34
+ w = _x.w
35
+ h = _x.h
36
+ } else {
37
+ x = _x
38
+ y = _y!
39
+ w = _w!
40
+ h = _h!
41
+ }
23
42
 
24
43
  const srcW = source.w
25
44
  const srcH = source.h
26
45
  const srcData = source.data
27
46
 
28
- // Safety check for empty or invalid dimensions
29
- if (w <= 0 || h <= 0) {
30
- return new Uint32Array(0)
31
- }
47
+ if (w <= 0 || h <= 0) return new Uint32Array(0)
32
48
 
33
49
  const dstData = new Uint32Array(w * h)
34
50
 
35
- // We map from Source (srcW, srcH) at (x,y)
36
- // To Dest (w, h) at (0,0)
37
- // Note: resolveBlitClipping usually takes (dstX, dstY, srcX, srcY...)
38
- // Here we are "blitting" FROM x,y TO 0,0.
39
- const clip = resolveBlitClipping(
40
- 0,
41
- 0,
42
- x,
43
- y,
44
- w,
45
- h,
46
- w,
47
- h,
48
- srcW,
49
- srcH,
50
- SCRATCH_BLIT,
51
- )
51
+ // Inline clipping logic to avoid object allocations
52
+ let srcX = x
53
+ let srcY = y
54
+ let dstX = 0
55
+ let dstY = 0
56
+ let copyW = w
57
+ let copyH = h
58
+
59
+ if (srcX < 0) {
60
+ dstX = -srcX
61
+ copyW += srcX
62
+ srcX = 0
63
+ }
52
64
 
53
- if (!clip.inBounds) return dstData
65
+ if (srcY < 0) {
66
+ dstY = -srcY
67
+ copyH += srcY
68
+ srcY = 0
69
+ }
54
70
 
55
- const {
56
- x: dstX,
57
- y: dstY,
58
- sx: srcX,
59
- sy: srcY,
60
- w: copyW,
61
- h: copyH,
62
- } = clip
71
+ copyW = Math.min(copyW, srcW - srcX)
72
+ copyH = Math.min(copyH, srcH - srcY)
73
+
74
+ if (copyW <= 0 || copyH <= 0) return dstData
63
75
 
64
76
  for (let row = 0; row < copyH; row++) {
65
77
  const srcStart = (srcY + row) * srcW + srcX
66
78
  const dstStart = (dstY + row) * w + dstX
67
-
68
- // Perform the high-speed 32-bit bulk copy
69
79
  const chunk = srcData.subarray(srcStart, srcStart + copyW)
80
+
70
81
  dstData.set(chunk, dstStart)
71
82
  }
72
83
 
@@ -1,10 +1,7 @@
1
1
  import type { Color32 } from '../_types'
2
2
  import type { Rect } from '../Rect/_rect-types'
3
- import { makeClippedRect, resolveRectClipping } from '../Rect/resolveClipping'
4
3
  import type { PixelData32 } from './_pixelData-types'
5
4
 
6
- const SCRATCH_RECT = makeClippedRect()
7
-
8
5
  /**
9
6
  * Fills a region or the {@link PixelData32} buffer with a solid color.
10
7
  *
@@ -42,6 +39,9 @@ export function fillPixelData(
42
39
  _w?: number,
43
40
  _h?: number,
44
41
  ): boolean {
42
+ const dstW = dst.w
43
+ const dstH = dst.h
44
+
45
45
  let x: number
46
46
  let y: number
47
47
  let w: number
@@ -50,8 +50,8 @@ export function fillPixelData(
50
50
  if (typeof _x === 'object') {
51
51
  x = _x.x ?? 0
52
52
  y = _x.y ?? 0
53
- w = _x.w ?? dst.w
54
- h = _x.h ?? dst.h
53
+ w = _x.w ?? dstW
54
+ h = _x.h ?? dstH
55
55
  } else if (typeof _x === 'number') {
56
56
  x = _x
57
57
  y = _y!
@@ -60,37 +60,54 @@ export function fillPixelData(
60
60
  } else {
61
61
  x = 0
62
62
  y = 0
63
- w = dst.w
64
- h = dst.h
63
+ w = dstW
64
+ h = dstH
65
+ }
66
+
67
+ // Inline bounds clipping
68
+ let dstX = x
69
+ let dstY = y
70
+ let fillW = w
71
+ let fillH = h
72
+
73
+ if (dstX < 0) {
74
+ fillW += dstX
75
+ dstX = 0
65
76
  }
66
77
 
67
- const clip = resolveRectClipping(
68
- x,
69
- y,
70
- w,
71
- h,
72
- dst.w,
73
- dst.h,
74
- SCRATCH_RECT,
75
- )
78
+ if (dstY < 0) {
79
+ fillH += dstY
80
+ dstY = 0
81
+ }
76
82
 
77
- if (!clip.inBounds) return false
83
+ fillW = Math.min(fillW, dstW - dstX)
84
+ fillH = Math.min(fillH, dstH - dstY)
78
85
 
79
- const {
80
- x: finalX,
81
- y: finalY,
82
- w: actualW,
83
- h: actualH,
84
- } = clip
86
+ if (fillW <= 0 || fillH <= 0) return false
85
87
 
86
88
  const dst32 = dst.data
87
- const dw = dst.w
88
89
  let hasChanged = false
89
90
 
90
- for (let iy = 0; iy < actualH; iy++) {
91
- const rowOffset = (finalY + iy) * dw
92
- const start = rowOffset + finalX
93
- const end = start + actualW
91
+ // Fast-path: If the area spans the full width, we can treat it as a contiguous 1D array
92
+ if (dstX === 0 && fillW === dstW) {
93
+ const start = dstY * dstW
94
+ const end = start + fillW * fillH
95
+
96
+ for (let i = start; i < end; i++) {
97
+ if (dst32[i] !== color) {
98
+ dst32[i] = color
99
+ hasChanged = true
100
+ }
101
+ }
102
+
103
+ return hasChanged
104
+ }
105
+
106
+ // Standard path: row-by-row
107
+ for (let iy = 0; iy < fillH; iy++) {
108
+ const rowOffset = (dstY + iy) * dstW
109
+ const start = rowOffset + dstX
110
+ const end = start + fillW
94
111
 
95
112
  for (let i = start; i < end; i++) {
96
113
  if (dst32[i] !== color) {