pixel-data-js 0.18.0 → 0.19.1

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 (77) hide show
  1. package/README.md +6 -1
  2. package/dist/index.dev.cjs +2723 -1487
  3. package/dist/index.dev.cjs.map +1 -1
  4. package/dist/index.dev.js +2690 -1481
  5. package/dist/index.dev.js.map +1 -1
  6. package/dist/index.prod.cjs +2723 -1487
  7. package/dist/index.prod.cjs.map +1 -1
  8. package/dist/index.prod.d.ts +400 -246
  9. package/dist/index.prod.js +2690 -1481
  10. package/dist/index.prod.js.map +1 -1
  11. package/package.json +22 -7
  12. package/src/Algorithm/forEachLinePoint.ts +36 -0
  13. package/src/BlendModes/BlendModeRegistry.ts +2 -0
  14. package/src/BlendModes/blend-modes-fast.ts +2 -2
  15. package/src/BlendModes/blend-modes-perfect.ts +5 -4
  16. package/src/BlendModes/toBlendModeIndexAndName.ts +41 -0
  17. package/src/History/PixelAccumulator.ts +2 -2
  18. package/src/History/PixelMutator/mutatorApplyAlphaMask.ts +30 -0
  19. package/src/History/PixelMutator/mutatorApplyBinaryMask.ts +30 -0
  20. package/src/History/PixelMutator/mutatorApplyCircleBrush.ts +23 -9
  21. package/src/History/PixelMutator/mutatorApplyCircleBrushStroke.ts +138 -0
  22. package/src/History/PixelMutator/mutatorApplyCirclePencil.ts +59 -0
  23. package/src/History/PixelMutator/mutatorApplyCirclePencilStroke.ts +131 -0
  24. package/src/History/PixelMutator/mutatorApplyRectBrush.ts +20 -7
  25. package/src/History/PixelMutator/mutatorApplyRectBrushStroke.ts +169 -0
  26. package/src/History/PixelMutator/mutatorApplyRectPencil.ts +62 -0
  27. package/src/History/PixelMutator/mutatorApplyRectPencilStroke.ts +149 -0
  28. package/src/History/PixelMutator/mutatorBlendColor.ts +9 -4
  29. package/src/History/PixelMutator/mutatorBlendPixelData.ts +10 -5
  30. package/src/History/PixelMutator/mutatorClear.ts +27 -0
  31. package/src/History/PixelMutator/{mutatorFillPixelData.ts → mutatorFill.ts} +9 -3
  32. package/src/History/PixelMutator/mutatorInvert.ts +10 -3
  33. package/src/History/PixelMutator.ts +23 -3
  34. package/src/History/PixelPatchTiles.ts +2 -2
  35. package/src/History/PixelWriter.ts +3 -3
  36. package/src/ImageData/ImageDataLike.ts +13 -0
  37. package/src/ImageData/extractImageDataBuffer.ts +22 -15
  38. package/src/ImageData/serialization.ts +4 -4
  39. package/src/ImageData/uInt32ArrayToImageData.ts +29 -0
  40. package/src/ImageData/writeImageData.ts +26 -18
  41. package/src/ImageData/writeImageDataBuffer.ts +30 -18
  42. package/src/IndexedImage/indexedImageToAverageColor.ts +1 -1
  43. package/src/Internal/resolveClipping.ts +140 -0
  44. package/src/Mask/applyBinaryMaskToAlphaMask.ts +89 -0
  45. package/src/Mask/copyMask.ts +1 -3
  46. package/src/Mask/mergeAlphaMasks.ts +81 -0
  47. package/src/Mask/mergeBinaryMasks.ts +89 -0
  48. package/src/PixelData/PixelBuffer32.ts +28 -0
  49. package/src/PixelData/PixelData.ts +38 -33
  50. package/src/PixelData/applyAlphaMaskToPixelData.ts +119 -0
  51. package/src/PixelData/applyBinaryMaskToPixelData.ts +111 -0
  52. package/src/PixelData/applyCircleBrushToPixelData.ts +31 -56
  53. package/src/PixelData/applyRectBrushToPixelData.ts +39 -71
  54. package/src/PixelData/blendColorPixelData.ts +18 -111
  55. package/src/PixelData/blendColorPixelDataAlphaMask.ts +111 -0
  56. package/src/PixelData/blendColorPixelDataBinaryMask.ts +89 -0
  57. package/src/PixelData/blendPixelData.ts +19 -107
  58. package/src/PixelData/blendPixelDataAlphaMask.ts +149 -0
  59. package/src/PixelData/blendPixelDataBinaryMask.ts +133 -0
  60. package/src/PixelData/clearPixelData.ts +2 -3
  61. package/src/PixelData/extractPixelData.ts +4 -4
  62. package/src/PixelData/extractPixelDataBuffer.ts +38 -26
  63. package/src/PixelData/fillPixelData.ts +18 -20
  64. package/src/PixelData/invertPixelData.ts +13 -21
  65. package/src/PixelData/pixelDataToAlphaMask.ts +2 -3
  66. package/src/PixelData/reflectPixelData.ts +3 -3
  67. package/src/PixelData/resamplePixelData.ts +2 -6
  68. package/src/PixelData/writePixelDataBuffer.ts +34 -20
  69. package/src/Rect/getCircleBrushOrPencilBounds.ts +43 -0
  70. package/src/Rect/getCircleBrushOrPencilStrokeBounds.ts +24 -0
  71. package/src/Rect/getRectBrushOrPencilBounds.ts +38 -0
  72. package/src/Rect/getRectBrushOrPencilStrokeBounds.ts +26 -0
  73. package/src/_types.ts +49 -33
  74. package/src/index.ts +47 -11
  75. package/src/History/PixelMutator/mutatorApplyMask.ts +0 -20
  76. package/src/Mask/mergeMasks.ts +0 -100
  77. package/src/PixelData/applyMaskToPixelData.ts +0 -129
@@ -1,4 +1,7 @@
1
1
  import type { ImageDataLike, Rect } from '../_types'
2
+ import { makeClippedBlit, resolveBlitClipping } from '../Internal/resolveClipping'
3
+
4
+ const SCRATCH_BLIT = makeClippedBlit()
2
5
 
3
6
  /**
4
7
  * Extracts a specific rectangular region of pixels from a larger {@link ImageDataLike}
@@ -45,24 +48,28 @@ export function extractImageDataBuffer(
45
48
  if (w <= 0 || h <= 0) return new Uint8ClampedArray(0)
46
49
  const out = new Uint8ClampedArray(w * h * 4)
47
50
 
48
- const x0 = Math.max(0, x)
49
- const y0 = Math.max(0, y)
50
- const x1 = Math.min(srcW, x + w)
51
- const y1 = Math.min(srcH, y + h)
51
+ const clip = resolveBlitClipping(
52
+ 0,
53
+ 0,
54
+ x,
55
+ y,
56
+ w,
57
+ h,
58
+ w,
59
+ h,
60
+ srcW,
61
+ srcH,
62
+ SCRATCH_BLIT,
63
+ )
52
64
 
53
- // If no intersection, return the empty
54
- if (x1 <= x0 || y1 <= y0) return out
65
+ if (!clip.inBounds) return out
55
66
 
56
- for (let row = 0; row < (y1 - y0); row++) {
57
- // Where to read from the source canvas
58
- const srcRow = y0 + row
59
- const srcStart = (srcRow * srcW + x0) * 4
60
- const rowLen = (x1 - x0) * 4
67
+ const { x: dstX, y: dstY, sx: srcX, sy: srcY, w: copyW, h: copyH } = clip
68
+ const rowLen = copyW * 4
61
69
 
62
- // Where to write into the 'out' patch
63
- const dstRow = (y0 - y) + row
64
- const dstCol = (x0 - x)
65
- const dstStart = (dstRow * w + dstCol) * 4
70
+ for (let row = 0; row < copyH; row++) {
71
+ const srcStart = ((srcY + row) * srcW + srcX) * 4
72
+ const dstStart = ((dstY + row) * w + dstX) * 4
66
73
 
67
74
  // Perform the high-speed bulk copy
68
75
  out.set(src.subarray(srcStart, srcStart + rowLen), dstStart)
@@ -1,11 +1,11 @@
1
1
  import type { Base64EncodedUInt8Array, ImageDataLike, SerializedImageData } from '../_types'
2
2
 
3
3
  export function base64EncodeArrayBuffer(buffer: ArrayBufferLike): Base64EncodedUInt8Array {
4
- const uint8 = new Uint8Array(buffer);
5
- const decoder = new TextDecoder('latin1');
6
- const binary = decoder.decode(uint8);
4
+ const uint8 = new Uint8Array(buffer)
5
+ const decoder = new TextDecoder('latin1')
6
+ const binary = decoder.decode(uint8)
7
7
 
8
- return btoa(binary) as Base64EncodedUInt8Array;
8
+ return btoa(binary) as Base64EncodedUInt8Array
9
9
  }
10
10
 
11
11
  export function base64DecodeArrayBuffer(encoded: Base64EncodedUInt8Array): Uint8ClampedArray<ArrayBuffer> {
@@ -0,0 +1,29 @@
1
+ import type { ImageDataLike } from '../_types'
2
+
3
+ export function uInt32ArrayToImageData(
4
+ data: Uint32Array,
5
+ width: number,
6
+ height: number,
7
+ ): ImageData {
8
+ const buffer = data.buffer as ArrayBuffer
9
+ const byteOffset = data.byteOffset
10
+ const byteLength = data.byteLength
11
+ const clampedArray = new Uint8ClampedArray(buffer, byteOffset, byteLength)
12
+ return new ImageData(clampedArray, width, height)
13
+ }
14
+
15
+ export function uInt32ArrayToImageDataLike(
16
+ data: Uint32Array,
17
+ width: number,
18
+ height: number,
19
+ ): ImageDataLike {
20
+ const buffer = data.buffer
21
+ const byteOffset = data.byteOffset
22
+ const byteLength = data.byteLength
23
+ const clampedArray = new Uint8ClampedArray(buffer, byteOffset, byteLength)
24
+ return {
25
+ width,
26
+ height,
27
+ data: clampedArray,
28
+ }
29
+ }
@@ -1,4 +1,7 @@
1
1
  import { MaskType } from '../_types'
2
+ import { makeClippedBlit, resolveBlitClipping } from '../Internal/resolveClipping'
3
+
4
+ const SCRATCH_BLIT = makeClippedBlit()
2
5
 
3
6
  /**
4
7
  * Writes image data from a source to a target with support for clipping and alpha masking.
@@ -32,30 +35,35 @@ export function writeImageData(
32
35
  const srcW = source.width
33
36
  const srcData = source.data
34
37
 
35
- const x0 = Math.max(0, x)
36
- const y0 = Math.max(0, y)
37
- const x1 = Math.min(dstW, x + sw)
38
- const y1 = Math.min(dstH, y + sh)
38
+ const clip = resolveBlitClipping(
39
+ x, y, sx, sy, sw, sh,
40
+ dstW, dstH, srcW, source.height,
41
+ SCRATCH_BLIT,
42
+ )
39
43
 
40
- if (x1 <= x0 || y1 <= y0) {
41
- return
42
- }
44
+ if (!clip.inBounds) return
45
+
46
+ const {
47
+ x: dstX,
48
+ y: dstY,
49
+ sx: srcX,
50
+ sy: srcY,
51
+ w: copyW,
52
+ h: copyH,
53
+ } = clip
43
54
 
44
55
  const useMask = !!mask
45
- const rowCount = y1 - y0
46
- const rowLenPixels = x1 - x0
47
56
 
48
- for (let row = 0; row < rowCount; row++) {
49
- const dstY = y0 + row
50
- const srcY = sy + (dstY - y)
51
- const srcXBase = sx + (x0 - x)
57
+ for (let row = 0; row < copyH; row++) {
58
+ const currentDstY = dstY + row
59
+ const currentSrcY = srcY + row
52
60
 
53
- const dstStart = (dstY * dstW + x0) * 4
54
- const srcStart = (srcY * srcW + srcXBase) * 4
61
+ const dstStart = (currentDstY * dstW + dstX) * 4
62
+ const srcStart = (currentSrcY * srcW + srcX) * 4
55
63
 
56
64
  if (useMask && mask) {
57
- for (let ix = 0; ix < rowLenPixels; ix++) {
58
- const mi = srcY * srcW + (srcXBase + ix)
65
+ for (let ix = 0; ix < copyW; ix++) {
66
+ const mi = currentSrcY * srcW + (srcX + ix)
59
67
  const alpha = mask[mi]
60
68
 
61
69
  if (alpha === 0) {
@@ -81,7 +89,7 @@ export function writeImageData(
81
89
  }
82
90
  }
83
91
  } else {
84
- const byteLen = rowLenPixels * 4
92
+ const byteLen = copyW * 4
85
93
  const sub = srcData.subarray(srcStart, srcStart + byteLen)
86
94
  dstData.set(sub, dstStart)
87
95
  }
@@ -1,4 +1,7 @@
1
1
  import type { Rect } from '../_types'
2
+ import { makeClippedBlit, resolveBlitClipping } from '../Internal/resolveClipping'
3
+
4
+ const SCRATCH_BLIT = makeClippedBlit()
2
5
 
3
6
  /**
4
7
  * Copies a pixel buffer into a specific region of an {@link ImageData} object.
@@ -46,28 +49,37 @@ export function writeImageDataBuffer(
46
49
 
47
50
  const { width: dstW, height: dstH, data: dst } = imageData
48
51
 
49
- // 1. Calculate the intersection of the patch and the canvas
50
- const x0 = Math.max(0, x)
51
- const y0 = Math.max(0, y)
52
- const x1 = Math.min(dstW, x + w)
53
- const y1 = Math.min(dstH, y + h)
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
+ )
54
65
 
55
- // If the intersection is empty, do nothing
56
- if (x1 <= x0 || y1 <= y0) return
66
+ if (!clip.inBounds) return
57
67
 
58
- const rowLen = (x1 - x0) * 4
59
- const srcCol = x0 - x
60
- const srcYOffset = y0 - y
61
- const actualH = y1 - y0
68
+ const {
69
+ x: dstX,
70
+ y: dstY,
71
+ sx: srcX,
72
+ sy: srcY,
73
+ w: copyW,
74
+ h: copyH,
75
+ } = clip
62
76
 
63
- for (let row = 0; row < actualH; row++) {
64
- // Target index
65
- const dstStart = ((y0 + row) * dstW + x0) * 4
77
+ const rowLen = copyW * 4
66
78
 
67
- // Source data index (must account for the offset if the rect was partially OOB)
68
- const srcRow = srcYOffset + row
69
- const o = (srcRow * w + srcCol) * 4
79
+ for (let row = 0; row < copyH; row++) {
80
+ const dstStart = ((dstY + row) * dstW + dstX) * 4
81
+ const srcStart = ((srcY + row) * w + srcX) * 4
70
82
 
71
- dst.set(data.subarray(o, o + rowLen), dstStart)
83
+ dst.set(data.subarray(srcStart, srcStart + rowLen), dstStart)
72
84
  }
73
85
  }
@@ -39,7 +39,7 @@ export function indexedImageToAverageColor(
39
39
  continue
40
40
  }
41
41
 
42
- const color = palette[id]! >>> 0;
42
+ const color = palette[id]! >>> 0
43
43
 
44
44
  const r = color & 0xFF
45
45
  const g = (color >> 8) & 0xFF
@@ -0,0 +1,140 @@
1
+ export type ClippedRect = {
2
+ x: number
3
+ y: number
4
+ w: number
5
+ h: number
6
+ inBounds: boolean
7
+ }
8
+
9
+ export type ClippedBlit = {
10
+ x: number
11
+ y: number
12
+ sx: number
13
+ sy: number
14
+ w: number
15
+ h: number
16
+ inBounds: boolean
17
+ }
18
+
19
+ // use factory functions when creating reusable objects ensure property order for JIT perf
20
+ export const makeClippedRect = (): ClippedRect => ({
21
+ x: 0,
22
+ y: 0,
23
+ w: 0,
24
+ h: 0,
25
+ inBounds: false,
26
+ })
27
+
28
+ export const makeClippedBlit = (): ClippedBlit => ({
29
+ x: 0,
30
+ y: 0,
31
+ sx: 0,
32
+ sy: 0,
33
+ w: 0,
34
+ h: 0,
35
+ inBounds: false,
36
+ })
37
+
38
+ /**
39
+ * Calculates the intersection of a target rectangle and a bounding box (usually 0,0 -> width,height).
40
+ * Handles negative offsets by shrinking dimensions.
41
+ */
42
+ export function resolveRectClipping(
43
+ x: number,
44
+ y: number,
45
+ w: number,
46
+ h: number,
47
+ boundaryW: number,
48
+ boundaryH: number,
49
+ out: ClippedRect,
50
+ ): ClippedRect {
51
+ // Destination Clipping (Top/Left)
52
+ if (x < 0) {
53
+ w += x
54
+ x = 0
55
+ }
56
+ if (y < 0) {
57
+ h += y
58
+ y = 0
59
+ }
60
+
61
+ // Destination Clipping (Bottom/Right)
62
+ const actualW = Math.min(w, boundaryW - x)
63
+ const actualH = Math.min(h, boundaryH - y)
64
+
65
+ if (actualW <= 0 || actualH <= 0) {
66
+ out.inBounds = false
67
+ return out
68
+ }
69
+
70
+ out.x = x
71
+ out.y = y
72
+ out.w = actualW
73
+ out.h = actualH
74
+ out.inBounds = true
75
+
76
+ return out
77
+ }
78
+
79
+ /**
80
+ * Calculates the clipping for transferring data from a Source to a Destination.
81
+ * Handles cases where the source is out of bounds (shifting the destination target)
82
+ * AND cases where the destination is out of bounds (shifting the source target).
83
+ */
84
+ export function resolveBlitClipping(
85
+ x: number,
86
+ y: number,
87
+ sx: number,
88
+ sy: number,
89
+ w: number,
90
+ h: number,
91
+ dstW: number,
92
+ dstH: number,
93
+ srcW: number,
94
+ srcH: number,
95
+ out: ClippedBlit,
96
+ ): ClippedBlit {
97
+ // 1. Source Clipping: If reading from negative source, shift target right and shrink
98
+ if (sx < 0) {
99
+ x -= sx
100
+ w += sx
101
+ sx = 0
102
+ }
103
+ if (sy < 0) {
104
+ y -= sy
105
+ h += sy
106
+ sy = 0
107
+ }
108
+ w = Math.min(w, srcW - sx)
109
+ h = Math.min(h, srcH - sy)
110
+
111
+ // 2. Destination Clipping: If writing to negative dest, shift source right and shrink
112
+ if (x < 0) {
113
+ sx -= x
114
+ w += x
115
+ x = 0
116
+ }
117
+ if (y < 0) {
118
+ sy -= y
119
+ h += y
120
+ y = 0
121
+ }
122
+
123
+ const actualW = Math.min(w, dstW - x)
124
+ const actualH = Math.min(h, dstH - y)
125
+
126
+ if (actualW <= 0 || actualH <= 0) {
127
+ out.inBounds = false
128
+ return out
129
+ }
130
+
131
+ out.x = x
132
+ out.y = y
133
+ out.sx = sx
134
+ out.sy = sy
135
+ out.w = actualW
136
+ out.h = actualH
137
+ out.inBounds = true
138
+
139
+ return out
140
+ }
@@ -0,0 +1,89 @@
1
+ import type { AlphaMask, ApplyMaskToPixelDataOptions, BinaryMask } from '../_types'
2
+
3
+ export function applyBinaryMaskToAlphaMask(
4
+ alphaMaskDst: AlphaMask,
5
+ dstWidth: number,
6
+ binaryMaskSrc: BinaryMask,
7
+ srcWidth: number,
8
+ opts: ApplyMaskToPixelDataOptions = {},
9
+ ): void {
10
+ const {
11
+ x: targetX = 0,
12
+ y: targetY = 0,
13
+ w: reqWidth = 0,
14
+ h: reqHeight = 0,
15
+ mx = 0,
16
+ my = 0,
17
+ invertMask = false,
18
+ } = opts
19
+
20
+ if (dstWidth <= 0) return
21
+ if (binaryMaskSrc.length === 0) return
22
+ if (srcWidth <= 0) return
23
+
24
+ const dstHeight = (alphaMaskDst.length / dstWidth) | 0
25
+ const srcHeight = (binaryMaskSrc.length / srcWidth) | 0
26
+
27
+ if (dstHeight <= 0) return
28
+ if (srcHeight <= 0) return
29
+
30
+ const dstX0 = Math.max(0, targetX)
31
+ const dstY0 = Math.max(0, targetY)
32
+ const dstX1 = reqWidth > 0 ? Math.min(dstWidth, targetX + reqWidth) : dstWidth
33
+ const dstY1 = reqHeight > 0 ? Math.min(dstHeight, targetY + reqHeight) : dstHeight
34
+
35
+ if (dstX0 >= dstX1) return
36
+ if (dstY0 >= dstY1) return
37
+
38
+ const srcX0 = mx + (dstX0 - targetX)
39
+ const srcY0 = my + (dstY0 - targetY)
40
+
41
+ if (srcX0 >= srcWidth) return
42
+ if (srcY0 >= srcHeight) return
43
+ if (srcX0 + (dstX1 - dstX0) <= 0) return
44
+ if (srcY0 + (dstY1 - dstY0) <= 0) return
45
+
46
+ const iterW = Math.min(dstX1 - dstX0, srcWidth - srcX0)
47
+ const iterH = Math.min(dstY1 - dstY0, srcHeight - srcY0)
48
+
49
+ let dstIdx = dstY0 * dstWidth + dstX0
50
+ let srcIdx = srcY0 * srcWidth + srcX0
51
+
52
+ if (invertMask) {
53
+ for (let row = 0; row < iterH; row++) {
54
+ const dstEnd = dstIdx + iterW
55
+ let d = dstIdx
56
+ let s = srcIdx
57
+
58
+ while (d < dstEnd) {
59
+ // inverted
60
+ if (binaryMaskSrc[s] !== 0) {
61
+ alphaMaskDst[d] = 0
62
+ }
63
+ d++
64
+ s++
65
+ }
66
+
67
+ dstIdx += dstWidth
68
+ srcIdx += srcWidth
69
+ }
70
+ } else {
71
+ for (let row = 0; row < iterH; row++) {
72
+ const dstEnd = dstIdx + iterW
73
+ let d = dstIdx
74
+ let s = srcIdx
75
+
76
+ while (d < dstEnd) {
77
+ // If binary mask is empty, clear the alpha pixel.
78
+ if (binaryMaskSrc[s] === 0) {
79
+ alphaMaskDst[d] = 0
80
+ }
81
+ d++
82
+ s++
83
+ }
84
+
85
+ dstIdx += dstWidth
86
+ srcIdx += srcWidth
87
+ }
88
+ }
89
+ }
@@ -1,10 +1,8 @@
1
- import type { AnyMask } from '../index'
2
-
3
1
  /**
4
2
  * Creates a new copy of a mask.
5
3
  * Uses the underlying buffer's slice method for high-performance memory copying.
6
4
  */
7
- export function copyMask<T extends AnyMask>(src: T): T {
5
+ export function copyMask<T extends Uint8Array>(src: T): T {
8
6
  // Uint8Array.slice() is highly optimized at the engine level
9
7
  return src.slice() as T
10
8
  }
@@ -0,0 +1,81 @@
1
+ import { type AlphaMask, type MergeAlphaMasksOptions } from '../_types'
2
+
3
+ /**
4
+ * Merges 2 alpha masks values are 0-255
5
+ */
6
+ export function mergeAlphaMasks(
7
+ dst: AlphaMask,
8
+ dstWidth: number,
9
+ src: AlphaMask,
10
+ srcWidth: number,
11
+ opts: MergeAlphaMasksOptions,
12
+ ): void {
13
+ const {
14
+ x: targetX = 0,
15
+ y: targetY = 0,
16
+ w: width = 0,
17
+ h: height = 0,
18
+ alpha: globalAlpha = 255,
19
+ mx = 0,
20
+ my = 0,
21
+ invertMask = false,
22
+ } = opts
23
+ const dstHeight = (dst.length / dstWidth) | 0
24
+ const srcHeight = (src.length / srcWidth) | 0
25
+
26
+ if (width <= 0) return
27
+ if (height <= 0) return
28
+ if (globalAlpha === 0) return
29
+
30
+ const startX = Math.max(0, -targetX, -mx)
31
+ const startY = Math.max(0, -targetY, -my)
32
+
33
+ const endX = Math.min(width, dstWidth - targetX, srcWidth - mx)
34
+ const endY = Math.min(height, dstHeight - targetY, srcHeight - my)
35
+
36
+ if (startX >= endX) return
37
+ if (startY >= endY) return
38
+
39
+ for (let iy = startY; iy < endY; iy++) {
40
+ const dy = targetY + iy
41
+ const sy = my + iy
42
+
43
+ let dIdx = dy * dstWidth + targetX + startX
44
+ let sIdx = sy * srcWidth + mx + startX
45
+
46
+ for (let ix = startX; ix < endX; ix++) {
47
+ const rawM = src[sIdx]
48
+ // Unified logic branch inside the hot path
49
+ const effectiveM = invertMask ? 255 - rawM : rawM
50
+
51
+ let weight = 0
52
+
53
+ if (effectiveM === 0) {
54
+ weight = 0
55
+ } else if (effectiveM === 255) {
56
+ weight = globalAlpha
57
+ } else if (globalAlpha === 255) {
58
+ weight = effectiveM
59
+ } else {
60
+ weight = (effectiveM * globalAlpha + 128) >> 8
61
+ }
62
+
63
+ if (weight !== 255) {
64
+ if (weight === 0) {
65
+ dst[dIdx] = 0
66
+ } else {
67
+ const da = dst[dIdx]
68
+
69
+ if (da === 255) {
70
+ dst[dIdx] = weight
71
+ } else if (da !== 0) {
72
+ dst[dIdx] = (da * weight + 128) >> 8
73
+ }
74
+ }
75
+ }
76
+
77
+ sIdx++
78
+ dIdx++
79
+ }
80
+ }
81
+ }
@@ -0,0 +1,89 @@
1
+ import type { BinaryMask, MergeAlphaMasksOptions } from '../_types'
2
+
3
+ export function mergeBinaryMasks(
4
+ dst: BinaryMask,
5
+ dstWidth: number,
6
+ src: BinaryMask,
7
+ srcWidth: number,
8
+ opts: MergeAlphaMasksOptions,
9
+ ): void {
10
+ const {
11
+ x: targetX = 0,
12
+ y: targetY = 0,
13
+ w: width = 0,
14
+ h: height = 0,
15
+ mx = 0,
16
+ my = 0,
17
+ invertMask = false,
18
+ } = opts
19
+ if (dstWidth <= 0) return
20
+ if (srcWidth <= 0) return
21
+
22
+ const dstHeight = (dst.length / dstWidth) | 0
23
+ const srcHeight = (src.length / srcWidth) | 0
24
+
25
+ // 1. Destination Clipping
26
+ let x = targetX
27
+ let y = targetY
28
+ let w = width
29
+ let h = height
30
+
31
+ if (x < 0) {
32
+ w += x
33
+ x = 0
34
+ }
35
+
36
+ if (y < 0) {
37
+ h += y
38
+ y = 0
39
+ }
40
+
41
+ w = Math.min(w, dstWidth - x)
42
+ h = Math.min(h, dstHeight - y)
43
+
44
+ if (w <= 0) return
45
+ if (h <= 0) return
46
+
47
+ // 2. Source Bounds Clipping (Double Clipping)
48
+ const startX = mx + (x - targetX)
49
+ const startY = my + (y - targetY)
50
+
51
+ const sX0 = Math.max(0, startX)
52
+ const sY0 = Math.max(0, startY)
53
+ const sX1 = Math.min(srcWidth, startX + w)
54
+ const sY1 = Math.min(srcHeight, startY + h)
55
+
56
+ const finalW = sX1 - sX0
57
+ const finalH = sY1 - sY0
58
+
59
+ if (finalW <= 0) return
60
+ if (finalH <= 0) return
61
+
62
+ // 3. Coordinate Alignment
63
+ const xShift = sX0 - startX
64
+ const yShift = sY0 - startY
65
+
66
+ const dStride = dstWidth - finalW
67
+ const sStride = srcWidth - finalW
68
+
69
+ let dIdx = (y + yShift) * dstWidth + (x + xShift)
70
+ let sIdx = sY0 * srcWidth + sX0
71
+
72
+ for (let iy = 0; iy < finalH; iy++) {
73
+ for (let ix = 0; ix < finalW; ix++) {
74
+ const mVal = src[sIdx]
75
+ // Determine if the source pixel effectively "clears" the destination
76
+ const isMaskedOut = invertMask ? mVal !== 0 : mVal === 0
77
+
78
+ if (isMaskedOut) {
79
+ dst[dIdx] = 0
80
+ }
81
+
82
+ dIdx++
83
+ sIdx++
84
+ }
85
+
86
+ dIdx += dStride
87
+ sIdx += sStride
88
+ }
89
+ }
@@ -0,0 +1,28 @@
1
+ import type { IPixelData } from '../_types'
2
+
3
+ export class PixelBuffer32 implements IPixelData {
4
+ readonly data32: Uint32Array
5
+
6
+ constructor(
7
+ readonly width: number,
8
+ readonly height: number,
9
+ data32?: Uint32Array,
10
+ ) {
11
+ this.data32 = data32 ?? new Uint32Array(width * height)
12
+ }
13
+
14
+ set(width: number, height: number, data32?: Uint32Array): void {
15
+ ;(this as any).data32 = data32 ?? new Uint32Array(width * height)
16
+ ;(this as any).width = width
17
+ ;(this as any).height = height
18
+ }
19
+
20
+ copy(): PixelBuffer32 {
21
+ const newData32 = new Uint32Array(this.data32)
22
+ return new PixelBuffer32(
23
+ this.width,
24
+ this.height,
25
+ newData32,
26
+ )
27
+ }
28
+ }