pixel-data-js 0.20.0 → 0.22.2
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/dist/index.dev.cjs +722 -357
- package/dist/index.dev.cjs.map +1 -1
- package/dist/index.dev.js +709 -356
- package/dist/index.dev.js.map +1 -1
- package/dist/index.prod.cjs +722 -357
- package/dist/index.prod.cjs.map +1 -1
- package/dist/index.prod.d.ts +283 -128
- package/dist/index.prod.js +709 -356
- package/dist/index.prod.js.map +1 -1
- package/package.json +1 -1
- package/src/Algorithm/floodFillSelection.ts +12 -14
- package/src/BlendModes/toBlendModeIndexAndName.ts +0 -7
- package/src/Clipboard/writeImgBlobToClipboard.ts +1 -1
- package/src/History/PixelMutator/mutatorApplyAlphaMask.ts +3 -0
- package/src/History/PixelMutator/mutatorApplyBinaryMask.ts +3 -0
- package/src/History/PixelMutator/mutatorApplyCircleBrush.ts +25 -6
- package/src/History/PixelMutator/mutatorApplyCircleBrushStroke.ts +89 -46
- package/src/History/PixelMutator/mutatorApplyCirclePencil.ts +7 -7
- package/src/History/PixelMutator/mutatorApplyCirclePencilStroke.ts +81 -41
- package/src/History/PixelMutator/mutatorApplyRectBrush.ts +3 -0
- package/src/History/PixelMutator/mutatorApplyRectBrushStroke.ts +18 -5
- package/src/History/PixelMutator/mutatorApplyRectPencil.ts +3 -0
- package/src/History/PixelMutator/mutatorApplyRectPencilStroke.ts +19 -4
- package/src/History/PixelMutator/mutatorBlendColor.ts +4 -0
- package/src/History/PixelMutator/mutatorBlendPixelData.ts +4 -0
- package/src/History/PixelMutator/mutatorClear.ts +11 -8
- package/src/History/PixelMutator/mutatorFill.ts +4 -0
- package/src/History/PixelMutator/mutatorFillBinaryMask.ts +28 -0
- package/src/History/PixelMutator/mutatorInvert.ts +3 -0
- package/src/ImageData/extractImageDataBuffer.ts +3 -3
- package/src/ImageData/{imageDataToAlphaMask.ts → imageDataToAlphaMaskBuffer.ts} +3 -4
- package/src/ImageData/resizeImageData.ts +3 -5
- package/src/ImageData/writeImageDataBuffer.ts +7 -7
- package/src/Mask/AlphaMask.ts +16 -0
- package/src/Mask/BinaryMask.ts +16 -0
- package/src/Mask/CircleBrushAlphaMask.ts +32 -0
- package/src/Mask/CircleBrushBinaryMask.ts +30 -0
- package/src/Mask/applyBinaryMaskToAlphaMask.ts +12 -9
- package/src/Mask/copyMask.ts +9 -3
- package/src/Mask/extractMask.ts +33 -31
- package/src/Mask/extractMaskBuffer.ts +87 -0
- package/src/Mask/invertMask.ts +6 -4
- package/src/Mask/mergeAlphaMasks.ts +11 -10
- package/src/Mask/mergeBinaryMasks.ts +10 -9
- package/src/Mask/setMaskData.ts +7 -0
- package/src/MaskRect/merge2BinaryMaskRects.ts +81 -0
- package/src/MaskRect/mergeBinaryMaskRects.ts +39 -0
- package/src/MaskRect/subtractBinaryMaskRects.ts +80 -0
- package/src/PixelData/applyAlphaMaskToPixelData.ts +8 -5
- package/src/PixelData/applyBinaryMaskToPixelData.ts +8 -9
- package/src/PixelData/applyCircleBrushToPixelData.ts +54 -62
- package/src/PixelData/blendColorPixelDataAlphaMask.ts +35 -25
- package/src/PixelData/blendColorPixelDataBinaryMask.ts +26 -19
- package/src/PixelData/blendPixelDataAlphaMask.ts +3 -3
- package/src/PixelData/blendPixelDataBinaryMask.ts +3 -3
- package/src/PixelData/clearPixelData.ts +2 -2
- package/src/PixelData/fillPixelData.ts +15 -42
- package/src/PixelData/fillPixelDataBinaryMask.ts +79 -0
- package/src/PixelData/invertPixelData.ts +3 -3
- package/src/PixelData/pixelDataToAlphaMask.ts +4 -2
- package/src/PixelData/writePixelDataBuffer.ts +2 -3
- package/src/Rect/getRectsBounds.ts +22 -0
- package/src/Rect/trimRectBounds.ts +20 -17
- package/src/_types.ts +55 -29
- package/src/index.ts +16 -1
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { Rect } from '../_types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Extracts a rectangular region from a 1D {@link Uint8Array} mask.
|
|
5
|
+
* This utility calculates the necessary offsets based on the `maskWidth` to
|
|
6
|
+
* slice out a specific area.
|
|
7
|
+
*
|
|
8
|
+
* @param maskBuffer - The source 1D array representing the full 2D mask.
|
|
9
|
+
* @param maskWidth - The width of the original source mask (stride).
|
|
10
|
+
* @param rect - A {@link Rect} object defining the region to extract.
|
|
11
|
+
* @returns A new {@link Uint8Array} containing the extracted region.
|
|
12
|
+
*/
|
|
13
|
+
export function extractMaskBuffer(
|
|
14
|
+
maskBuffer: Uint8Array,
|
|
15
|
+
maskWidth: number,
|
|
16
|
+
rect: Rect,
|
|
17
|
+
): Uint8Array
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param maskBuffer - The source 1D array representing the full 2D mask.
|
|
21
|
+
* @param maskWidth - The width of the original source mask (stride).
|
|
22
|
+
* @param x - The starting horizontal coordinate.
|
|
23
|
+
* @param y - The starting vertical coordinate.
|
|
24
|
+
* @param w - The width of the region to extract.
|
|
25
|
+
* @param h - The height of the region to extract.
|
|
26
|
+
* @returns A new {@link Uint8Array} containing the extracted region.
|
|
27
|
+
*/
|
|
28
|
+
export function extractMaskBuffer(
|
|
29
|
+
maskBuffer: Uint8Array,
|
|
30
|
+
maskWidth: number,
|
|
31
|
+
x: number,
|
|
32
|
+
y: number,
|
|
33
|
+
w: number,
|
|
34
|
+
h: number,
|
|
35
|
+
): Uint8Array
|
|
36
|
+
export function extractMaskBuffer(
|
|
37
|
+
maskBuffer: Uint8Array,
|
|
38
|
+
maskWidth: number,
|
|
39
|
+
xOrRect: number | Rect,
|
|
40
|
+
y?: number,
|
|
41
|
+
w?: number,
|
|
42
|
+
h?: number,
|
|
43
|
+
): Uint8Array {
|
|
44
|
+
let finalX: number
|
|
45
|
+
let finalY: number
|
|
46
|
+
let finalW: number
|
|
47
|
+
let finalH: number
|
|
48
|
+
|
|
49
|
+
if (typeof xOrRect === 'object') {
|
|
50
|
+
finalX = xOrRect.x
|
|
51
|
+
finalY = xOrRect.y
|
|
52
|
+
finalW = xOrRect.w
|
|
53
|
+
finalH = xOrRect.h
|
|
54
|
+
} else {
|
|
55
|
+
finalX = xOrRect
|
|
56
|
+
finalY = y!
|
|
57
|
+
finalW = w!
|
|
58
|
+
finalH = h!
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const out = new Uint8Array(finalW * finalH)
|
|
62
|
+
const srcH = maskBuffer.length / maskWidth
|
|
63
|
+
|
|
64
|
+
for (let row = 0; row < finalH; row++) {
|
|
65
|
+
const currentSrcY = finalY + row
|
|
66
|
+
|
|
67
|
+
if (currentSrcY < 0 || currentSrcY >= srcH) {
|
|
68
|
+
continue
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const start = Math.max(0, finalX)
|
|
72
|
+
const end = Math.min(maskWidth, finalX + finalW)
|
|
73
|
+
|
|
74
|
+
if (start < end) {
|
|
75
|
+
const srcOffset = currentSrcY * maskWidth + start
|
|
76
|
+
const dstOffset = (row * finalW) + (start - finalX)
|
|
77
|
+
const count = end - start
|
|
78
|
+
|
|
79
|
+
out.set(
|
|
80
|
+
maskBuffer.subarray(srcOffset, srcOffset + count),
|
|
81
|
+
dstOffset,
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return out
|
|
87
|
+
}
|
package/src/Mask/invertMask.ts
CHANGED
|
@@ -4,10 +4,11 @@ import type { AlphaMask, BinaryMask } from '../index'
|
|
|
4
4
|
* Inverts a BinaryMask in-place.
|
|
5
5
|
*/
|
|
6
6
|
export function invertBinaryMask(dst: BinaryMask): void {
|
|
7
|
-
const
|
|
7
|
+
const data = dst.data
|
|
8
|
+
const len = data.length
|
|
8
9
|
|
|
9
10
|
for (let i = 0; i < len; i++) {
|
|
10
|
-
|
|
11
|
+
data[i] = data[i] === 0
|
|
11
12
|
? 1
|
|
12
13
|
: 0
|
|
13
14
|
}
|
|
@@ -17,9 +18,10 @@ export function invertBinaryMask(dst: BinaryMask): void {
|
|
|
17
18
|
* Inverts an AlphaMask in-place.
|
|
18
19
|
*/
|
|
19
20
|
export function invertAlphaMask(dst: AlphaMask): void {
|
|
20
|
-
const
|
|
21
|
+
const data = dst.data
|
|
22
|
+
const len = data.length
|
|
21
23
|
|
|
22
24
|
for (let i = 0; i < len; i++) {
|
|
23
|
-
|
|
25
|
+
data[i] = 255 - data[i]
|
|
24
26
|
}
|
|
25
27
|
}
|
|
@@ -5,9 +5,7 @@ import { type AlphaMask, type MergeAlphaMasksOptions } from '../_types'
|
|
|
5
5
|
*/
|
|
6
6
|
export function mergeAlphaMasks(
|
|
7
7
|
dst: AlphaMask,
|
|
8
|
-
dstWidth: number,
|
|
9
8
|
src: AlphaMask,
|
|
10
|
-
srcWidth: number,
|
|
11
9
|
opts: MergeAlphaMasksOptions,
|
|
12
10
|
): void {
|
|
13
11
|
const {
|
|
@@ -20,18 +18,21 @@ export function mergeAlphaMasks(
|
|
|
20
18
|
my = 0,
|
|
21
19
|
invertMask = false,
|
|
22
20
|
} = opts
|
|
23
|
-
const dstHeight = (dst.length / dstWidth) | 0
|
|
24
|
-
const srcHeight = (src.length / srcWidth) | 0
|
|
25
21
|
|
|
26
22
|
if (width <= 0) return
|
|
27
23
|
if (height <= 0) return
|
|
28
24
|
if (globalAlpha === 0) return
|
|
29
25
|
|
|
26
|
+
const dstData = dst.data
|
|
27
|
+
const srcData = src.data
|
|
28
|
+
const srcWidth = src.w
|
|
29
|
+
const dstWidth = dst.w
|
|
30
|
+
|
|
30
31
|
const startX = Math.max(0, -targetX, -mx)
|
|
31
32
|
const startY = Math.max(0, -targetY, -my)
|
|
32
33
|
|
|
33
34
|
const endX = Math.min(width, dstWidth - targetX, srcWidth - mx)
|
|
34
|
-
const endY = Math.min(height,
|
|
35
|
+
const endY = Math.min(height, dst.h - targetY, src.h - my)
|
|
35
36
|
|
|
36
37
|
if (startX >= endX) return
|
|
37
38
|
if (startY >= endY) return
|
|
@@ -44,7 +45,7 @@ export function mergeAlphaMasks(
|
|
|
44
45
|
let sIdx = sy * srcWidth + mx + startX
|
|
45
46
|
|
|
46
47
|
for (let ix = startX; ix < endX; ix++) {
|
|
47
|
-
const rawM =
|
|
48
|
+
const rawM = srcData[sIdx]
|
|
48
49
|
// Unified logic branch inside the hot path
|
|
49
50
|
const effectiveM = invertMask ? 255 - rawM : rawM
|
|
50
51
|
|
|
@@ -62,14 +63,14 @@ export function mergeAlphaMasks(
|
|
|
62
63
|
|
|
63
64
|
if (weight !== 255) {
|
|
64
65
|
if (weight === 0) {
|
|
65
|
-
|
|
66
|
+
dstData[dIdx] = 0
|
|
66
67
|
} else {
|
|
67
|
-
const da =
|
|
68
|
+
const da = dstData[dIdx]
|
|
68
69
|
|
|
69
70
|
if (da === 255) {
|
|
70
|
-
|
|
71
|
+
dstData[dIdx] = weight
|
|
71
72
|
} else if (da !== 0) {
|
|
72
|
-
|
|
73
|
+
dstData[dIdx] = (da * weight + 128) >> 8
|
|
73
74
|
}
|
|
74
75
|
}
|
|
75
76
|
}
|
|
@@ -2,9 +2,7 @@ import type { BinaryMask, MergeAlphaMasksOptions } from '../_types'
|
|
|
2
2
|
|
|
3
3
|
export function mergeBinaryMasks(
|
|
4
4
|
dst: BinaryMask,
|
|
5
|
-
dstWidth: number,
|
|
6
5
|
src: BinaryMask,
|
|
7
|
-
srcWidth: number,
|
|
8
6
|
opts: MergeAlphaMasksOptions,
|
|
9
7
|
): void {
|
|
10
8
|
const {
|
|
@@ -16,12 +14,15 @@ export function mergeBinaryMasks(
|
|
|
16
14
|
my = 0,
|
|
17
15
|
invertMask = false,
|
|
18
16
|
} = opts
|
|
17
|
+
|
|
18
|
+
const dstData = dst.data
|
|
19
|
+
const srcData = src.data
|
|
20
|
+
const srcWidth = src.w
|
|
21
|
+
const dstWidth = dst.w
|
|
22
|
+
|
|
19
23
|
if (dstWidth <= 0) return
|
|
20
24
|
if (srcWidth <= 0) return
|
|
21
25
|
|
|
22
|
-
const dstHeight = (dst.length / dstWidth) | 0
|
|
23
|
-
const srcHeight = (src.length / srcWidth) | 0
|
|
24
|
-
|
|
25
26
|
// 1. Destination Clipping
|
|
26
27
|
let x = targetX
|
|
27
28
|
let y = targetY
|
|
@@ -39,7 +40,7 @@ export function mergeBinaryMasks(
|
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
w = Math.min(w, dstWidth - x)
|
|
42
|
-
h = Math.min(h,
|
|
43
|
+
h = Math.min(h, dst.h - y)
|
|
43
44
|
|
|
44
45
|
if (w <= 0) return
|
|
45
46
|
if (h <= 0) return
|
|
@@ -51,7 +52,7 @@ export function mergeBinaryMasks(
|
|
|
51
52
|
const sX0 = Math.max(0, startX)
|
|
52
53
|
const sY0 = Math.max(0, startY)
|
|
53
54
|
const sX1 = Math.min(srcWidth, startX + w)
|
|
54
|
-
const sY1 = Math.min(
|
|
55
|
+
const sY1 = Math.min(src.h, startY + h)
|
|
55
56
|
|
|
56
57
|
const finalW = sX1 - sX0
|
|
57
58
|
const finalH = sY1 - sY0
|
|
@@ -71,12 +72,12 @@ export function mergeBinaryMasks(
|
|
|
71
72
|
|
|
72
73
|
for (let iy = 0; iy < finalH; iy++) {
|
|
73
74
|
for (let ix = 0; ix < finalW; ix++) {
|
|
74
|
-
const mVal =
|
|
75
|
+
const mVal = srcData[sIdx]
|
|
75
76
|
// Determine if the source pixel effectively "clears" the destination
|
|
76
77
|
const isMaskedOut = invertMask ? mVal !== 0 : mVal === 0
|
|
77
78
|
|
|
78
79
|
if (isMaskedOut) {
|
|
79
|
-
|
|
80
|
+
dstData[dIdx] = 0
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
dIdx++
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { MaskType, type NullableBinaryMaskRect } from '../_types'
|
|
2
|
+
import { getRectsBounds } from '../Rect/getRectsBounds'
|
|
3
|
+
|
|
4
|
+
export function merge2BinaryMaskRects(
|
|
5
|
+
a: NullableBinaryMaskRect,
|
|
6
|
+
b: NullableBinaryMaskRect,
|
|
7
|
+
): NullableBinaryMaskRect {
|
|
8
|
+
const bounds = getRectsBounds([a, b])
|
|
9
|
+
|
|
10
|
+
// If both are fully selected, check if they form a perfect, gapless rectangle
|
|
11
|
+
if (
|
|
12
|
+
(a.data === null || a.data === undefined)
|
|
13
|
+
&& (b.data === null || b.data === undefined)
|
|
14
|
+
) {
|
|
15
|
+
const ix = Math.max(a.x, b.x)
|
|
16
|
+
const iy = Math.max(a.y, b.y)
|
|
17
|
+
const ir = Math.min(a.x + a.w, b.x + b.w)
|
|
18
|
+
const ib = Math.min(a.y + a.h, b.y + b.h)
|
|
19
|
+
|
|
20
|
+
const iw = Math.max(0, ir - ix)
|
|
21
|
+
const ih = Math.max(0, ib - iy)
|
|
22
|
+
|
|
23
|
+
const intersectionArea = iw * ih
|
|
24
|
+
const areaA = a.w * a.h
|
|
25
|
+
const areaB = b.w * b.h
|
|
26
|
+
const boundsArea = bounds.w * bounds.h
|
|
27
|
+
|
|
28
|
+
if (boundsArea === areaA + areaB - intersectionArea) {
|
|
29
|
+
return {
|
|
30
|
+
...bounds,
|
|
31
|
+
data: null,
|
|
32
|
+
type: null,
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const maskData = new Uint8Array(bounds.w * bounds.h)
|
|
38
|
+
|
|
39
|
+
// --- Write A's contribution ---
|
|
40
|
+
const aOffY = a.y - bounds.y
|
|
41
|
+
const aOffX = a.x - bounds.x
|
|
42
|
+
|
|
43
|
+
if (a.data === undefined || a.data === null) {
|
|
44
|
+
for (let ay = 0; ay < a.h; ay++) {
|
|
45
|
+
const destRow = (aOffY + ay) * bounds.w + aOffX
|
|
46
|
+
maskData.fill(1, destRow, destRow + a.w)
|
|
47
|
+
}
|
|
48
|
+
} else {
|
|
49
|
+
for (let ay = 0; ay < a.h; ay++) {
|
|
50
|
+
const srcRow = ay * a.w
|
|
51
|
+
const destRow = (aOffY + ay) * bounds.w + aOffX
|
|
52
|
+
maskData.set(a.data.subarray(srcRow, srcRow + a.w), destRow)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// --- OR B's contribution ---
|
|
57
|
+
const bOffY = b.y - bounds.y
|
|
58
|
+
const bOffX = b.x - bounds.x
|
|
59
|
+
|
|
60
|
+
if (b.data === undefined || b.data === null) {
|
|
61
|
+
for (let by = 0; by < b.h; by++) {
|
|
62
|
+
const destRow = (bOffY + by) * bounds.w + bOffX
|
|
63
|
+
maskData.fill(1, destRow, destRow + b.w)
|
|
64
|
+
}
|
|
65
|
+
} else {
|
|
66
|
+
for (let by = 0; by < b.h; by++) {
|
|
67
|
+
const srcRow = by * b.w
|
|
68
|
+
const destRow = (bOffY + by) * bounds.w + bOffX
|
|
69
|
+
|
|
70
|
+
for (let bx = 0; bx < b.w; bx++) {
|
|
71
|
+
maskData[destRow + bx] |= b.data[srcRow + bx]
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
...bounds,
|
|
78
|
+
data: maskData,
|
|
79
|
+
type: MaskType.BINARY,
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { NullableBinaryMaskRect } from '../_types'
|
|
2
|
+
import { merge2BinaryMaskRects } from './merge2BinaryMaskRects'
|
|
3
|
+
|
|
4
|
+
export function mergeBinaryMaskRects(current: NullableBinaryMaskRect[], adding: NullableBinaryMaskRect[]): NullableBinaryMaskRect[] {
|
|
5
|
+
const rects = [...current, ...adding]
|
|
6
|
+
|
|
7
|
+
let changed = true
|
|
8
|
+
while (changed) {
|
|
9
|
+
changed = false
|
|
10
|
+
const next: NullableBinaryMaskRect[] = []
|
|
11
|
+
|
|
12
|
+
for (const r of rects) {
|
|
13
|
+
let merged = false
|
|
14
|
+
|
|
15
|
+
for (let i = 0; i < next.length; i++) {
|
|
16
|
+
const n = next[i]
|
|
17
|
+
|
|
18
|
+
const overlap =
|
|
19
|
+
r.x <= n.x + n.w &&
|
|
20
|
+
r.x + r.w >= n.x &&
|
|
21
|
+
r.y <= n.y + n.h &&
|
|
22
|
+
r.y + r.h >= n.y
|
|
23
|
+
|
|
24
|
+
if (overlap) {
|
|
25
|
+
next[i] = merge2BinaryMaskRects(n, r)
|
|
26
|
+
merged = true
|
|
27
|
+
changed = true
|
|
28
|
+
break
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!merged) next.push(r)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
rects.splice(0, rects.length, ...next)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return rects
|
|
39
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { MaskType, type NullableBinaryMaskRect } from '../_types'
|
|
2
|
+
|
|
3
|
+
export function subtractBinaryMaskRects(
|
|
4
|
+
current: NullableBinaryMaskRect[],
|
|
5
|
+
subtracting: NullableBinaryMaskRect[],
|
|
6
|
+
): NullableBinaryMaskRect[] {
|
|
7
|
+
let result = [...current]
|
|
8
|
+
|
|
9
|
+
for (const sub of subtracting) {
|
|
10
|
+
const next: NullableBinaryMaskRect[] = []
|
|
11
|
+
|
|
12
|
+
for (const r of result) {
|
|
13
|
+
const ix = Math.max(r.x, sub.x)
|
|
14
|
+
const iy = Math.max(r.y, sub.y)
|
|
15
|
+
const ix2 = Math.min(r.x + r.w, sub.x + sub.w)
|
|
16
|
+
const iy2 = Math.min(r.y + r.h, sub.y + sub.h)
|
|
17
|
+
|
|
18
|
+
if (ix >= ix2 || iy >= iy2) {
|
|
19
|
+
next.push(r)
|
|
20
|
+
continue
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Split r into up to 4 pieces around the intersection.
|
|
24
|
+
// Extract directly from r.mask — no intermediate copy, no mutation.
|
|
25
|
+
//
|
|
26
|
+
// ┌──────────────┐
|
|
27
|
+
// │ TOP │ r.y .. iy (full width)
|
|
28
|
+
// ├────┬─────┬───┤
|
|
29
|
+
// │LEFT│ sub │RGT│ iy .. iy2 (side strips)
|
|
30
|
+
// ├────┴─────┴───┤
|
|
31
|
+
// │ BOTTOM │ iy2 .. r.y+r.h (full width)
|
|
32
|
+
// └──────────────┘
|
|
33
|
+
|
|
34
|
+
if (r.y < iy) pushPiece(next, r, r.x, r.y, r.w, iy - r.y)
|
|
35
|
+
if (iy2 < r.y + r.h) pushPiece(next, r, r.x, iy2, r.w, r.y + r.h - iy2)
|
|
36
|
+
if (r.x < ix) pushPiece(next, r, r.x, iy, ix - r.x, iy2 - iy)
|
|
37
|
+
if (ix2 < r.x + r.w) pushPiece(next, r, ix2, iy, r.x + r.w - ix2, iy2 - iy)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
result = next
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return result
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Extract sub-region (x, y, w, h) in global coords from r's mask and push
|
|
48
|
+
* onto dest. If r.mask is null (fully selected) the piece is also null —
|
|
49
|
+
* zero allocations on the happy path.
|
|
50
|
+
*/
|
|
51
|
+
function pushPiece(
|
|
52
|
+
dest: NullableBinaryMaskRect[],
|
|
53
|
+
r: NullableBinaryMaskRect,
|
|
54
|
+
x: number,
|
|
55
|
+
y: number,
|
|
56
|
+
w: number,
|
|
57
|
+
h: number,
|
|
58
|
+
): void {
|
|
59
|
+
if (r.data === null || r.data === undefined) {
|
|
60
|
+
dest.push({ x, y, w, h, data: null, type: null })
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Coords local to r.mask
|
|
65
|
+
const lx = x - r.x
|
|
66
|
+
const ly = y - r.y
|
|
67
|
+
|
|
68
|
+
const data = new Uint8Array(w * h)
|
|
69
|
+
for (let row = 0; row < h; row++) {
|
|
70
|
+
data.set(
|
|
71
|
+
r.data.subarray(
|
|
72
|
+
(ly + row) * r.w + lx,
|
|
73
|
+
(ly + row) * r.w + lx + w,
|
|
74
|
+
),
|
|
75
|
+
row * w,
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
dest.push({ x, y, w, h, data, type: MaskType.BINARY })
|
|
80
|
+
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { type AlphaMask, type ApplyMaskToPixelDataOptions, type IPixelData } from '../_types'
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Directly applies a mask to a region of PixelData,
|
|
5
|
+
* modifying the destination's alpha channel in-place.
|
|
6
|
+
*/
|
|
3
7
|
export function applyAlphaMaskToPixelData(
|
|
4
8
|
dst: IPixelData,
|
|
5
9
|
mask: AlphaMask,
|
|
@@ -11,7 +15,6 @@ export function applyAlphaMaskToPixelData(
|
|
|
11
15
|
w: width = dst.width,
|
|
12
16
|
h: height = dst.height,
|
|
13
17
|
alpha: globalAlpha = 255,
|
|
14
|
-
mw,
|
|
15
18
|
mx = 0,
|
|
16
19
|
my = 0,
|
|
17
20
|
invertMask = false,
|
|
@@ -42,9 +45,8 @@ export function applyAlphaMaskToPixelData(
|
|
|
42
45
|
if (h <= 0) return
|
|
43
46
|
|
|
44
47
|
// 2. Determine Source Dimensions
|
|
45
|
-
const mPitch =
|
|
48
|
+
const mPitch = mask.w
|
|
46
49
|
if (mPitch <= 0) return
|
|
47
|
-
const maskHeight = (mask.length / mPitch) | 0
|
|
48
50
|
|
|
49
51
|
// 3. Source Bounds Clipping
|
|
50
52
|
// Calculate where we would start reading in the mask
|
|
@@ -55,7 +57,7 @@ export function applyAlphaMaskToPixelData(
|
|
|
55
57
|
const sX0 = Math.max(0, startX)
|
|
56
58
|
const sY0 = Math.max(0, startY)
|
|
57
59
|
const sX1 = Math.min(mPitch, startX + w)
|
|
58
|
-
const sY1 = Math.min(
|
|
60
|
+
const sY1 = Math.min(mask.h, startY + h)
|
|
59
61
|
|
|
60
62
|
const finalW = sX1 - sX0
|
|
61
63
|
const finalH = sY1 - sY0
|
|
@@ -73,13 +75,14 @@ export function applyAlphaMaskToPixelData(
|
|
|
73
75
|
const dw = dst.width
|
|
74
76
|
const dStride = dw - finalW
|
|
75
77
|
const mStride = mPitch - finalW
|
|
78
|
+
const maskData = mask.data
|
|
76
79
|
|
|
77
80
|
let dIdx = (y + yShift) * dw + (x + xShift)
|
|
78
81
|
let mIdx = sY0 * mPitch + sX0
|
|
79
82
|
|
|
80
83
|
for (let iy = 0; iy < h; iy++) {
|
|
81
84
|
for (let ix = 0; ix < w; ix++) {
|
|
82
|
-
const mVal =
|
|
85
|
+
const mVal = maskData[mIdx]
|
|
83
86
|
// Unified logic branch inside the hot path
|
|
84
87
|
const effectiveM = invertMask ? 255 - mVal : mVal
|
|
85
88
|
|
|
@@ -14,14 +14,13 @@ export function applyBinaryMaskToPixelData(
|
|
|
14
14
|
y: targetY = 0,
|
|
15
15
|
w: width = dst.width,
|
|
16
16
|
h: height = dst.height,
|
|
17
|
-
alpha = 255,
|
|
18
|
-
mw,
|
|
17
|
+
alpha: globalAlpha = 255,
|
|
19
18
|
mx = 0,
|
|
20
19
|
my = 0,
|
|
21
20
|
invertMask = false,
|
|
22
21
|
} = opts
|
|
23
22
|
|
|
24
|
-
if (
|
|
23
|
+
if (globalAlpha === 0) return
|
|
25
24
|
|
|
26
25
|
// 1. Initial Destination Clipping
|
|
27
26
|
let x = targetX
|
|
@@ -46,9 +45,8 @@ export function applyBinaryMaskToPixelData(
|
|
|
46
45
|
if (h <= 0) return
|
|
47
46
|
|
|
48
47
|
// 2. Determine Source Dimensions
|
|
49
|
-
const mPitch =
|
|
48
|
+
const mPitch = mask.w
|
|
50
49
|
if (mPitch <= 0) return
|
|
51
|
-
const maskHeight = (mask.length / mPitch) | 0
|
|
52
50
|
|
|
53
51
|
// 3. Source Bounds Clipping
|
|
54
52
|
// Calculate where we would start reading in the mask
|
|
@@ -59,7 +57,7 @@ export function applyBinaryMaskToPixelData(
|
|
|
59
57
|
const sX0 = Math.max(0, startX)
|
|
60
58
|
const sY0 = Math.max(0, startY)
|
|
61
59
|
const sX1 = Math.min(mPitch, startX + w)
|
|
62
|
-
const sY1 = Math.min(
|
|
60
|
+
const sY1 = Math.min(mask.h, startY + h)
|
|
63
61
|
|
|
64
62
|
const finalW = sX1 - sX0
|
|
65
63
|
const finalH = sY1 - sY0
|
|
@@ -77,26 +75,27 @@ export function applyBinaryMaskToPixelData(
|
|
|
77
75
|
const dw = dst.width
|
|
78
76
|
const dStride = dw - finalW
|
|
79
77
|
const mStride = mPitch - finalW
|
|
78
|
+
const maskData = mask.data
|
|
80
79
|
|
|
81
80
|
let dIdx = (y + yShift) * dw + (x + xShift)
|
|
82
81
|
let mIdx = sY0 * mPitch + sX0
|
|
83
82
|
|
|
84
83
|
for (let iy = 0; iy < h; iy++) {
|
|
85
84
|
for (let ix = 0; ix < w; ix++) {
|
|
86
|
-
const mVal =
|
|
85
|
+
const mVal = maskData[mIdx]
|
|
87
86
|
// Consistently determines if this pixel should be "masked out" (cleared)
|
|
88
87
|
const isMaskedOut = invertMask ? mVal !== 0 : mVal === 0
|
|
89
88
|
|
|
90
89
|
if (isMaskedOut) {
|
|
91
90
|
// Clear alpha channel only (keep RGB)
|
|
92
91
|
dst32[dIdx] = (dst32[dIdx] & 0x00ffffff) >>> 0
|
|
93
|
-
} else if (
|
|
92
|
+
} else if (globalAlpha !== 255) {
|
|
94
93
|
const d = dst32[dIdx]
|
|
95
94
|
const da = d >>> 24
|
|
96
95
|
|
|
97
96
|
// If pixel isn't already fully transparent, apply global alpha
|
|
98
97
|
if (da !== 0) {
|
|
99
|
-
const finalAlpha = da === 255 ?
|
|
98
|
+
const finalAlpha = da === 255 ? globalAlpha : (da * globalAlpha + 128) >> 8
|
|
100
99
|
dst32[dIdx] = ((d & 0x00ffffff) | (finalAlpha << 24)) >>> 0
|
|
101
100
|
}
|
|
102
101
|
}
|