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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pixel-data-js",
3
3
  "type": "module",
4
- "version": "0.1.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": "^4.0.18",
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": "4.0.18"
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
+ }