pixel-data-js 0.1.0 → 0.3.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/dist/index.dev.cjs +270 -180
- package/dist/index.dev.cjs.map +1 -1
- package/dist/index.dev.js +265 -178
- package/dist/index.dev.js.map +1 -1
- package/dist/index.prod.cjs +270 -180
- package/dist/index.prod.cjs.map +1 -1
- package/dist/index.prod.d.ts +188 -95
- package/dist/index.prod.js +265 -178
- package/dist/index.prod.js.map +1 -1
- package/package.json +3 -4
- package/src/ImageData/copyImageData.ts +13 -0
- package/src/ImageData/extractImageData.ts +54 -0
- package/src/ImageData/serialization.ts +1 -1
- package/src/ImageData/writeImageData.ts +54 -0
- package/src/Mask/copyMask.ts +10 -0
- package/src/Mask/invertMask.ts +25 -0
- package/src/Mask/mergeMasks.ts +105 -0
- package/src/PixelData/applyMaskToPixelData.ts +129 -0
- package/src/PixelData/blendColorPixelData.ts +157 -0
- package/src/PixelData/blendPixelData.ts +196 -0
- package/src/PixelData/clearPixelData.ts +14 -0
- package/src/PixelData/fillPixelData.ts +56 -0
- package/src/PixelData.ts +20 -0
- package/src/_types.ts +139 -11
- package/src/{ImageData/blend-modes.ts → blend-modes.ts} +1 -1
- package/src/index.ts +5 -3
- package/src/ImageData/blit.ts +0 -145
- package/src/ImageData/read-write-pixels.ts +0 -36
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pixel-data-js",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.3.0",
|
|
5
5
|
"packageManager": "pnpm@10.30.0",
|
|
6
6
|
"description": "JS Pixel and ImageData operations",
|
|
7
7
|
"author": {
|
|
@@ -53,14 +53,13 @@
|
|
|
53
53
|
"@stryker-mutator/typescript-checker": "^9.5.1",
|
|
54
54
|
"@stryker-mutator/vitest-runner": "^9.5.1",
|
|
55
55
|
"@types/node": "^25.2.3",
|
|
56
|
-
"@vitest/coverage-v8": "
|
|
56
|
+
"@vitest/coverage-v8": "3.2.4",
|
|
57
57
|
"esbuild": "^0.27.3",
|
|
58
|
-
"happy-dom": "^20.6.1",
|
|
59
58
|
"tsup": "^8.5.1",
|
|
60
59
|
"tsx": "^4.21.0",
|
|
61
60
|
"typedoc": "^0.28.17",
|
|
62
61
|
"typescript": "^5.9.3",
|
|
63
|
-
"vitest": "
|
|
62
|
+
"vitest": "3.2.4"
|
|
64
63
|
},
|
|
65
64
|
"repository": {
|
|
66
65
|
"type": "git",
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ImageDataLike } from '../_types'
|
|
2
|
+
|
|
3
|
+
export function copyImageData({ data, width, height }: ImageDataLike): ImageData {
|
|
4
|
+
return new ImageData(data.slice(), width, height)
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function copyImageDataLike({ data, width, height }: ImageDataLike): ImageDataLike {
|
|
8
|
+
return {
|
|
9
|
+
data: data.slice(),
|
|
10
|
+
width,
|
|
11
|
+
height,
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { ImageDataLike, Rect } from '../_types'
|
|
2
|
+
|
|
3
|
+
export function extractImageData(
|
|
4
|
+
imageData: ImageDataLike,
|
|
5
|
+
rect: Rect,
|
|
6
|
+
): Uint8ClampedArray
|
|
7
|
+
export function extractImageData(
|
|
8
|
+
imageData: ImageDataLike,
|
|
9
|
+
x: number,
|
|
10
|
+
y: number,
|
|
11
|
+
w: number,
|
|
12
|
+
h: number,
|
|
13
|
+
): Uint8ClampedArray
|
|
14
|
+
export function extractImageData(
|
|
15
|
+
imageData: ImageDataLike,
|
|
16
|
+
_x: Rect | number,
|
|
17
|
+
_y?: number,
|
|
18
|
+
_w?: number,
|
|
19
|
+
_h?: number,
|
|
20
|
+
): Uint8ClampedArray {
|
|
21
|
+
const { x, y, w, h } = typeof _x === 'object'
|
|
22
|
+
? _x
|
|
23
|
+
: { x: _x, y: _y!, w: _w!, h: _h! }
|
|
24
|
+
|
|
25
|
+
const { width: srcW, height: srcH, data: src } = imageData
|
|
26
|
+
// Safety check for invalid dimensions
|
|
27
|
+
if (w <= 0 || h <= 0) return new Uint8ClampedArray(0)
|
|
28
|
+
const out = new Uint8ClampedArray(w * h * 4)
|
|
29
|
+
|
|
30
|
+
const x0 = Math.max(0, x)
|
|
31
|
+
const y0 = Math.max(0, y)
|
|
32
|
+
const x1 = Math.min(srcW, x + w)
|
|
33
|
+
const y1 = Math.min(srcH, y + h)
|
|
34
|
+
|
|
35
|
+
// If no intersection, return the empty
|
|
36
|
+
if (x1 <= x0 || y1 <= y0) return out
|
|
37
|
+
|
|
38
|
+
for (let row = 0; row < (y1 - y0); row++) {
|
|
39
|
+
// Where to read from the source canvas
|
|
40
|
+
const srcRow = y0 + row
|
|
41
|
+
const srcStart = (srcRow * srcW + x0) * 4
|
|
42
|
+
const rowLen = (x1 - x0) * 4
|
|
43
|
+
|
|
44
|
+
// Where to write into the 'out' patch
|
|
45
|
+
const dstRow = (y0 - y) + row
|
|
46
|
+
const dstCol = (x0 - x)
|
|
47
|
+
const dstStart = (dstRow * w + dstCol) * 4
|
|
48
|
+
|
|
49
|
+
// Perform the high-speed bulk copy
|
|
50
|
+
out.set(src.subarray(srcStart, srcStart + rowLen), dstStart)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return out
|
|
54
|
+
}
|
|
@@ -5,7 +5,7 @@ export function base64EncodeArrayBuffer(buffer: ArrayBufferLike): Base64EncodedU
|
|
|
5
5
|
return btoa(binary) as Base64EncodedUInt8Array
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
export function base64DecodeArrayBuffer(encoded: Base64EncodedUInt8Array): Uint8ClampedArray {
|
|
8
|
+
export function base64DecodeArrayBuffer(encoded: Base64EncodedUInt8Array): Uint8ClampedArray<ArrayBuffer> {
|
|
9
9
|
const binary = atob(encoded)
|
|
10
10
|
const bytes = new Uint8ClampedArray(binary.length)
|
|
11
11
|
for (let i = 0; i < binary.length; i++) {
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Rect } from '../_types'
|
|
2
|
+
|
|
3
|
+
export function writeImageData(
|
|
4
|
+
imageData: ImageData,
|
|
5
|
+
data: Uint8ClampedArray,
|
|
6
|
+
rect: Rect,
|
|
7
|
+
): void
|
|
8
|
+
export function writeImageData(
|
|
9
|
+
imageData: ImageData,
|
|
10
|
+
data: Uint8ClampedArray,
|
|
11
|
+
x: number,
|
|
12
|
+
y: number,
|
|
13
|
+
w: number,
|
|
14
|
+
h: number,
|
|
15
|
+
): void
|
|
16
|
+
export function writeImageData(
|
|
17
|
+
imageData: ImageData,
|
|
18
|
+
data: Uint8ClampedArray,
|
|
19
|
+
_x: Rect | number,
|
|
20
|
+
_y?: number,
|
|
21
|
+
_w?: number,
|
|
22
|
+
_h?: number,
|
|
23
|
+
): void {
|
|
24
|
+
const { x, y, w, h } = typeof _x === 'object'
|
|
25
|
+
? _x
|
|
26
|
+
: { x: _x, y: _y!, w: _w!, h: _h! }
|
|
27
|
+
|
|
28
|
+
const { width: dstW, height: dstH, data: dst } = imageData
|
|
29
|
+
|
|
30
|
+
// 1. Calculate the intersection of the patch and the canvas
|
|
31
|
+
const x0 = Math.max(0, x)
|
|
32
|
+
const y0 = Math.max(0, y)
|
|
33
|
+
const x1 = Math.min(dstW, x + w)
|
|
34
|
+
const y1 = Math.min(dstH, y + h)
|
|
35
|
+
|
|
36
|
+
// If the intersection is empty, do nothing
|
|
37
|
+
if (x1 <= x0 || y1 <= y0) return
|
|
38
|
+
|
|
39
|
+
const rowLen = (x1 - x0) * 4
|
|
40
|
+
const srcCol = x0 - x
|
|
41
|
+
const srcYOffset = y0 - y
|
|
42
|
+
const actualH = y1 - y0
|
|
43
|
+
|
|
44
|
+
for (let row = 0; row < actualH; row++) {
|
|
45
|
+
// Target index
|
|
46
|
+
const dstStart = ((y0 + row) * dstW + x0) * 4
|
|
47
|
+
|
|
48
|
+
// Source data index (must account for the offset if the rect was partially OOB)
|
|
49
|
+
const srcRow = srcYOffset + row
|
|
50
|
+
const o = (srcRow * w + srcCol) * 4
|
|
51
|
+
|
|
52
|
+
dst.set(data.subarray(o, o + rowLen), dstStart)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { AnyMask } from '../index'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Creates a new copy of a mask.
|
|
5
|
+
* Uses the underlying buffer's slice method for high-performance memory copying.
|
|
6
|
+
*/
|
|
7
|
+
export function copyMask<T extends AnyMask>(src: T): T {
|
|
8
|
+
// Uint8Array.slice() is highly optimized at the engine level
|
|
9
|
+
return src.slice() as T
|
|
10
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { AlphaMask, BinaryMask } from '../index'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Inverts a BinaryMask in-place.
|
|
5
|
+
*/
|
|
6
|
+
export function invertBinaryMask(dst: BinaryMask): void {
|
|
7
|
+
const len = dst.length
|
|
8
|
+
|
|
9
|
+
for (let i = 0; i < len; i++) {
|
|
10
|
+
dst[i] = dst[i] === 0
|
|
11
|
+
? 1
|
|
12
|
+
: 0
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Inverts an AlphaMask in-place.
|
|
18
|
+
*/
|
|
19
|
+
export function invertAlphaMask(dst: AlphaMask): void {
|
|
20
|
+
const len = dst.length
|
|
21
|
+
|
|
22
|
+
for (let i = 0; i < len; i++) {
|
|
23
|
+
dst[i] = 255 - dst[i]
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AnyMask,
|
|
3
|
+
type AlphaMask,
|
|
4
|
+
type ApplyMaskOptions,
|
|
5
|
+
MaskType,
|
|
6
|
+
} from '../_types'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Merges a source mask into a destination AlphaMask.
|
|
10
|
+
*/
|
|
11
|
+
export function mergeMasks(
|
|
12
|
+
dst: AlphaMask,
|
|
13
|
+
dstWidth: number,
|
|
14
|
+
src: AnyMask,
|
|
15
|
+
opts: ApplyMaskOptions,
|
|
16
|
+
): void {
|
|
17
|
+
const {
|
|
18
|
+
x: targetX = 0,
|
|
19
|
+
y: targetY = 0,
|
|
20
|
+
w: width = 0,
|
|
21
|
+
h: height = 0,
|
|
22
|
+
alpha: globalAlpha = 255,
|
|
23
|
+
maskType = MaskType.ALPHA,
|
|
24
|
+
mw,
|
|
25
|
+
mx = 0,
|
|
26
|
+
my = 0,
|
|
27
|
+
invertMask = false,
|
|
28
|
+
} = opts
|
|
29
|
+
|
|
30
|
+
if (width <= 0 || height <= 0 || globalAlpha === 0) {
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const sPitch = mw ?? width
|
|
35
|
+
const isAlpha = maskType === MaskType.ALPHA
|
|
36
|
+
|
|
37
|
+
for (let iy = 0; iy < height; iy++) {
|
|
38
|
+
const dy = targetY + iy
|
|
39
|
+
const sy = my + iy
|
|
40
|
+
|
|
41
|
+
if (dy < 0 || sy < 0) {
|
|
42
|
+
continue
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
for (let ix = 0; ix < width; ix++) {
|
|
46
|
+
const dx = targetX + ix
|
|
47
|
+
const sx = mx + ix
|
|
48
|
+
|
|
49
|
+
if (dx < 0 || dx >= dstWidth || sx < 0 || sx >= sPitch) {
|
|
50
|
+
continue
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const dIdx = dy * dstWidth + dx
|
|
54
|
+
const sIdx = sy * sPitch + sx
|
|
55
|
+
const mVal = src[sIdx]
|
|
56
|
+
let weight = globalAlpha
|
|
57
|
+
|
|
58
|
+
if (isAlpha) {
|
|
59
|
+
const effectiveM = invertMask
|
|
60
|
+
? 255 - mVal
|
|
61
|
+
: mVal
|
|
62
|
+
|
|
63
|
+
if (effectiveM === 0) {
|
|
64
|
+
dst[dIdx] = 0
|
|
65
|
+
continue
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
weight = globalAlpha === 255
|
|
69
|
+
? effectiveM
|
|
70
|
+
: (effectiveM * globalAlpha + 128) >> 8
|
|
71
|
+
} else {
|
|
72
|
+
// Strict Binary 1/0 Logic
|
|
73
|
+
const isHit = invertMask
|
|
74
|
+
? mVal === 0
|
|
75
|
+
: mVal === 1
|
|
76
|
+
|
|
77
|
+
if (!isHit) {
|
|
78
|
+
dst[dIdx] = 0
|
|
79
|
+
continue
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// If binary hit, weight is just the global alpha
|
|
83
|
+
weight = globalAlpha
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (weight === 0) {
|
|
87
|
+
dst[dIdx] = 0
|
|
88
|
+
continue
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const da = dst[dIdx]
|
|
92
|
+
|
|
93
|
+
if (da === 0) {
|
|
94
|
+
// Already transparent
|
|
95
|
+
} else if (weight === 255) {
|
|
96
|
+
// Identity: keep da
|
|
97
|
+
} else if (da === 255) {
|
|
98
|
+
// Identity: result is weight
|
|
99
|
+
dst[dIdx] = weight
|
|
100
|
+
} else {
|
|
101
|
+
dst[dIdx] = (da * weight + 128) >> 8
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { type AnyMask, type ApplyMaskOptions, MaskType } from '../_types'
|
|
2
|
+
import type { PixelData } from '../PixelData'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Directly applies a mask to a region of PixelData,
|
|
6
|
+
* modifying the destination's alpha channel in-place.
|
|
7
|
+
*/
|
|
8
|
+
export function applyMaskToPixelData(
|
|
9
|
+
dst: PixelData,
|
|
10
|
+
mask: AnyMask,
|
|
11
|
+
opts: ApplyMaskOptions,
|
|
12
|
+
): void {
|
|
13
|
+
const {
|
|
14
|
+
x: targetX = 0,
|
|
15
|
+
y: targetY = 0,
|
|
16
|
+
w: width = dst.width,
|
|
17
|
+
h: height = dst.height,
|
|
18
|
+
alpha: globalAlpha = 255,
|
|
19
|
+
maskType = MaskType.ALPHA,
|
|
20
|
+
mw,
|
|
21
|
+
mx = 0,
|
|
22
|
+
my = 0,
|
|
23
|
+
invertMask = false,
|
|
24
|
+
} = opts
|
|
25
|
+
|
|
26
|
+
let x = targetX
|
|
27
|
+
let y = targetY
|
|
28
|
+
let w = width
|
|
29
|
+
let h = height
|
|
30
|
+
|
|
31
|
+
// Clipping Logic
|
|
32
|
+
if (x < 0) {
|
|
33
|
+
w += x
|
|
34
|
+
x = 0
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (y < 0) {
|
|
38
|
+
h += y
|
|
39
|
+
y = 0
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const actualW = Math.min(w, dst.width - x)
|
|
43
|
+
const actualH = Math.min(h, dst.height - y)
|
|
44
|
+
|
|
45
|
+
if (actualW <= 0 || actualH <= 0 || globalAlpha === 0) {
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const dst32 = dst.data32
|
|
50
|
+
const dw = dst.width
|
|
51
|
+
const mPitch = mw ?? width
|
|
52
|
+
const isAlpha = maskType === MaskType.ALPHA
|
|
53
|
+
const dx = x - targetX
|
|
54
|
+
const dy = y - targetY
|
|
55
|
+
|
|
56
|
+
let dIdx = y * dw + x
|
|
57
|
+
let mIdx = (my + dy) * mPitch + (mx + dx)
|
|
58
|
+
|
|
59
|
+
const dStride = dw - actualW
|
|
60
|
+
const mStride = mPitch - actualW
|
|
61
|
+
|
|
62
|
+
for (let iy = 0; iy < actualH; iy++) {
|
|
63
|
+
for (let ix = 0; ix < actualW; ix++) {
|
|
64
|
+
const mVal = mask[mIdx]
|
|
65
|
+
let weight = globalAlpha
|
|
66
|
+
|
|
67
|
+
if (isAlpha) {
|
|
68
|
+
const effectiveM = invertMask
|
|
69
|
+
? 255 - mVal
|
|
70
|
+
: mVal
|
|
71
|
+
|
|
72
|
+
// Short-circuit: if source is 0, destination alpha becomes 0
|
|
73
|
+
if (effectiveM === 0) {
|
|
74
|
+
dst32[dIdx] = (dst32[dIdx] & 0x00ffffff) >>> 0
|
|
75
|
+
dIdx++
|
|
76
|
+
mIdx++
|
|
77
|
+
continue
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
weight = globalAlpha === 255
|
|
81
|
+
? effectiveM
|
|
82
|
+
: (effectiveM * globalAlpha + 128) >> 8
|
|
83
|
+
} else {
|
|
84
|
+
// Strict Binary 1/0 Logic
|
|
85
|
+
const isHit = invertMask
|
|
86
|
+
? mVal === 0
|
|
87
|
+
: mVal === 1
|
|
88
|
+
|
|
89
|
+
if (!isHit) {
|
|
90
|
+
dst32[dIdx] = (dst32[dIdx] & 0x00ffffff) >>> 0
|
|
91
|
+
dIdx++
|
|
92
|
+
mIdx++
|
|
93
|
+
continue
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
weight = globalAlpha
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// If calculated weight is 0, clear alpha
|
|
100
|
+
if (weight === 0) {
|
|
101
|
+
dst32[dIdx] = (dst32[dIdx] & 0x00ffffff) >>> 0
|
|
102
|
+
} else {
|
|
103
|
+
const d = dst32[dIdx]
|
|
104
|
+
const da = (d >>> 24)
|
|
105
|
+
|
|
106
|
+
let finalAlpha = da
|
|
107
|
+
|
|
108
|
+
if (da === 0) {
|
|
109
|
+
// Already transparent
|
|
110
|
+
} else if (weight === 255) {
|
|
111
|
+
// Identity: keep original da
|
|
112
|
+
} else if (da === 255) {
|
|
113
|
+
// Identity: result is just the weight
|
|
114
|
+
finalAlpha = weight
|
|
115
|
+
} else {
|
|
116
|
+
finalAlpha = (da * weight + 128) >> 8
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
dst32[dIdx] = ((d & 0x00ffffff) | (finalAlpha << 24)) >>> 0
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
dIdx++
|
|
123
|
+
mIdx++
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
dIdx += dStride
|
|
127
|
+
mIdx += mStride
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { type Color32, type ColorBlendOptions, MaskType } from '../_types'
|
|
2
|
+
import { sourceOverColor32 } from '../blend-modes'
|
|
3
|
+
import type { PixelData } from '../PixelData'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Fills a rectangle in the destination PixelData with a single color,
|
|
7
|
+
* supporting blend modes, global alpha, and masking.
|
|
8
|
+
*/
|
|
9
|
+
export function blendColorPixelData(
|
|
10
|
+
dst: PixelData,
|
|
11
|
+
color: Color32,
|
|
12
|
+
opts: ColorBlendOptions,
|
|
13
|
+
): void {
|
|
14
|
+
const {
|
|
15
|
+
x: targetX = 0,
|
|
16
|
+
y: targetY = 0,
|
|
17
|
+
w: width = dst.width,
|
|
18
|
+
h: height = dst.height,
|
|
19
|
+
alpha: globalAlpha = 255,
|
|
20
|
+
blendFn = sourceOverColor32,
|
|
21
|
+
mask,
|
|
22
|
+
maskType = MaskType.ALPHA,
|
|
23
|
+
mw,
|
|
24
|
+
mx = 0,
|
|
25
|
+
my = 0,
|
|
26
|
+
invertMask = false,
|
|
27
|
+
} = opts
|
|
28
|
+
|
|
29
|
+
if (globalAlpha === 0) return
|
|
30
|
+
|
|
31
|
+
let x = targetX
|
|
32
|
+
let y = targetY
|
|
33
|
+
let w = width
|
|
34
|
+
let h = height
|
|
35
|
+
|
|
36
|
+
// 1. Destination Clipping
|
|
37
|
+
if (x < 0) {
|
|
38
|
+
w += x
|
|
39
|
+
x = 0
|
|
40
|
+
}
|
|
41
|
+
if (y < 0) {
|
|
42
|
+
h += y
|
|
43
|
+
y = 0
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const actualW = Math.min(w, dst.width - x)
|
|
47
|
+
const actualH = Math.min(h, dst.height - y)
|
|
48
|
+
|
|
49
|
+
if (actualW <= 0 || actualH <= 0) return
|
|
50
|
+
|
|
51
|
+
const dst32 = dst.data32
|
|
52
|
+
const dw = dst.width
|
|
53
|
+
const mPitch = mw ?? width
|
|
54
|
+
const isAlphaMask = maskType === MaskType.ALPHA
|
|
55
|
+
|
|
56
|
+
const dx = x - targetX
|
|
57
|
+
const dy = y - targetY
|
|
58
|
+
|
|
59
|
+
let dIdx = y * dw + x
|
|
60
|
+
let mIdx = (my + dy) * mPitch + (mx + dx)
|
|
61
|
+
|
|
62
|
+
const dStride = dw - actualW
|
|
63
|
+
const mStride = mPitch - actualW
|
|
64
|
+
|
|
65
|
+
// Pre-calculate the source color with global alpha
|
|
66
|
+
const baseSrcColor = color
|
|
67
|
+
const baseSrcAlpha = (baseSrcColor >>> 24)
|
|
68
|
+
|
|
69
|
+
for (let iy = 0; iy < actualH; iy++) {
|
|
70
|
+
for (let ix = 0; ix < actualW; ix++) {
|
|
71
|
+
|
|
72
|
+
// Early exit if source pixel is already transparent
|
|
73
|
+
if (baseSrcAlpha === 0) {
|
|
74
|
+
dIdx++
|
|
75
|
+
mIdx++
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let weight = globalAlpha
|
|
80
|
+
|
|
81
|
+
if (mask) {
|
|
82
|
+
const mVal = mask[mIdx]
|
|
83
|
+
|
|
84
|
+
if (isAlphaMask) {
|
|
85
|
+
const effectiveM = invertMask
|
|
86
|
+
? 255 - mVal
|
|
87
|
+
: mVal
|
|
88
|
+
|
|
89
|
+
// If mask is transparent, skip
|
|
90
|
+
if (effectiveM === 0) {
|
|
91
|
+
dIdx++
|
|
92
|
+
mIdx++
|
|
93
|
+
continue
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// globalAlpha is not a factor
|
|
97
|
+
if (globalAlpha === 255) {
|
|
98
|
+
weight = effectiveM
|
|
99
|
+
// mask is not a factor
|
|
100
|
+
} else if (effectiveM === 255) {
|
|
101
|
+
weight = globalAlpha
|
|
102
|
+
} else {
|
|
103
|
+
// use rounding-corrected multiplication
|
|
104
|
+
weight = (effectiveM * globalAlpha + 128) >> 8
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
const isHit = invertMask
|
|
108
|
+
? mVal === 0
|
|
109
|
+
: mVal === 1
|
|
110
|
+
|
|
111
|
+
if (!isHit) {
|
|
112
|
+
dIdx++
|
|
113
|
+
mIdx++
|
|
114
|
+
continue
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
weight = globalAlpha
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Final safety check for weight (can be 0 if globalAlpha or alphaMask rounds down)
|
|
121
|
+
if (weight === 0) {
|
|
122
|
+
dIdx++
|
|
123
|
+
mIdx++
|
|
124
|
+
continue
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Apply Weight to Source Alpha
|
|
129
|
+
let currentSrcAlpha = baseSrcAlpha
|
|
130
|
+
let currentSrcColor = baseSrcColor
|
|
131
|
+
|
|
132
|
+
if (weight < 255) {
|
|
133
|
+
if (baseSrcAlpha === 255) {
|
|
134
|
+
currentSrcAlpha = weight
|
|
135
|
+
} else {
|
|
136
|
+
currentSrcAlpha = (baseSrcAlpha * weight + 128) >> 8
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (currentSrcAlpha === 0) {
|
|
140
|
+
dIdx++
|
|
141
|
+
mIdx++
|
|
142
|
+
continue
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
currentSrcColor = ((baseSrcColor & 0x00ffffff) | (currentSrcAlpha << 24)) >>> 0 as Color32
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
dst32[dIdx] = blendFn(currentSrcColor, dst32[dIdx] as Color32)
|
|
149
|
+
|
|
150
|
+
dIdx++
|
|
151
|
+
mIdx++
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
dIdx += dStride
|
|
155
|
+
mIdx += mStride
|
|
156
|
+
}
|
|
157
|
+
}
|