pixel-data-js 0.2.0 → 0.4.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.
@@ -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
+ }
@@ -0,0 +1,196 @@
1
+ import { type Color32, MaskType, type PixelBlendOptions } from '../_types'
2
+ import { sourceOverColor32 } from '../blend-modes'
3
+ import type { PixelData } from '../PixelData'
4
+
5
+ /**
6
+ * Blits source PixelData into a destination PixelData using 32-bit integer bitwise blending.
7
+ * This function bypasses standard ImageData limitations by operating directly on
8
+ * Uint32Array views. It supports various blend modes, binary/alpha masking, and
9
+ * automatic clipping of both source and destination bounds.
10
+ * @example
11
+ *
12
+ * const dst = new PixelData(ctx.getImageData(0,0,100,100))
13
+ * blendImageData32(dst, sprite, {
14
+ * blendFn: COLOR_32_BLEND_MODES.multiply,
15
+ * mask: brushMask,
16
+ * maskType: MaskType.ALPHA
17
+ * });
18
+ */
19
+ export function blendPixelData(
20
+ dst: PixelData,
21
+ src: PixelData,
22
+ opts: PixelBlendOptions,
23
+ ) {
24
+ const {
25
+ x: targetX = 0,
26
+ y: targetY = 0,
27
+ sx: sourceX = 0,
28
+ sy: sourceY = 0,
29
+ w: width = src.width,
30
+ h: height = src.height,
31
+ alpha: globalAlpha = 255,
32
+ blendFn = sourceOverColor32,
33
+ mask,
34
+ maskType = MaskType.ALPHA,
35
+ mw,
36
+ mx = 0,
37
+ my = 0,
38
+ invertMask = false,
39
+ } = opts
40
+
41
+ if (globalAlpha === 0) return
42
+
43
+ let x = targetX
44
+ let y = targetY
45
+ let sx = sourceX
46
+ let sy = sourceY
47
+ let w = width
48
+ let h = height
49
+
50
+ // 1. Source Clipping
51
+ if (sx < 0) {
52
+ x -= sx
53
+ w += sx
54
+ sx = 0
55
+ }
56
+ if (sy < 0) {
57
+ y -= sy
58
+ h += sy
59
+ sy = 0
60
+ }
61
+ w = Math.min(w, src.width - sx)
62
+ h = Math.min(h, src.height - sy)
63
+
64
+ // 2. Destination Clipping
65
+ if (x < 0) {
66
+ sx -= x
67
+ w += x
68
+ x = 0
69
+ }
70
+ if (y < 0) {
71
+ sy -= y
72
+ h += y
73
+ y = 0
74
+ }
75
+
76
+ const actualW = Math.min(w, dst.width - x)
77
+ const actualH = Math.min(h, dst.height - y)
78
+
79
+ if (actualW <= 0 || actualH <= 0) return
80
+
81
+ const dst32 = dst.data32
82
+ const src32 = src.data32
83
+ const dw = dst.width
84
+ const sw = src.width
85
+ const mPitch = mw ?? width
86
+ const isAlphaMask = maskType === MaskType.ALPHA
87
+
88
+ const dx = x - targetX
89
+ const dy = y - targetY
90
+
91
+ let dIdx = y * dw + x
92
+ let sIdx = sy * sw + sx
93
+ let mIdx = (my + dy) * mPitch + (mx + dx)
94
+
95
+ const dStride = dw - actualW
96
+ const sStride = sw - actualW
97
+ const mStride = mPitch - actualW
98
+
99
+ for (let iy = 0; iy < actualH; iy++) {
100
+ for (let ix = 0; ix < actualW; ix++) {
101
+ const baseSrcColor = src32[sIdx] as Color32
102
+ const baseSrcAlpha = (baseSrcColor >>> 24)
103
+
104
+ // Early exit if source pixel is already transparent
105
+ if (baseSrcAlpha === 0) {
106
+ dIdx++
107
+ sIdx++
108
+ mIdx++
109
+ continue
110
+ }
111
+
112
+ let weight = globalAlpha
113
+
114
+ if (mask) {
115
+ const mVal = mask[mIdx]
116
+
117
+ if (isAlphaMask) {
118
+ const effectiveM = invertMask
119
+ ? 255 - mVal
120
+ : mVal
121
+
122
+ // If mask is transparent, skip
123
+ if (effectiveM === 0) {
124
+ dIdx++
125
+ sIdx++
126
+ mIdx++
127
+ continue
128
+ }
129
+
130
+ // globalAlpha is not a factor
131
+ if (globalAlpha === 255) {
132
+ weight = effectiveM
133
+ // mask is not a factor
134
+ } else if (effectiveM === 255) {
135
+ weight = globalAlpha
136
+ } else {
137
+ // use rounding-corrected multiplication
138
+ weight = (effectiveM * globalAlpha + 128) >> 8
139
+ }
140
+ } else {
141
+ const isHit = invertMask
142
+ ? mVal === 0
143
+ : mVal === 1
144
+
145
+ if (!isHit) {
146
+ dIdx++
147
+ sIdx++
148
+ mIdx++
149
+ continue
150
+ }
151
+
152
+ weight = globalAlpha
153
+ }
154
+
155
+ // Final safety check for weight (can be 0 if globalAlpha or alphaMask rounds down)
156
+ if (weight === 0) {
157
+ dIdx++
158
+ sIdx++
159
+ mIdx++
160
+ continue
161
+ }
162
+ }
163
+
164
+ // Apply Weight to Source Alpha
165
+ let currentSrcAlpha = baseSrcAlpha
166
+ let currentSrcColor = baseSrcColor
167
+
168
+ if (weight < 255) {
169
+ if (baseSrcAlpha === 255) {
170
+ currentSrcAlpha = weight
171
+ } else {
172
+ currentSrcAlpha = (baseSrcAlpha * weight + 128) >> 8
173
+ }
174
+
175
+ if (currentSrcAlpha === 0) {
176
+ dIdx++
177
+ sIdx++
178
+ mIdx++
179
+ continue
180
+ }
181
+
182
+ currentSrcColor = ((baseSrcColor & 0x00ffffff) | (currentSrcAlpha << 24)) >>> 0 as Color32
183
+ }
184
+
185
+ dst32[dIdx] = blendFn(currentSrcColor, dst32[dIdx] as Color32)
186
+
187
+ dIdx++
188
+ sIdx++
189
+ mIdx++
190
+ }
191
+
192
+ dIdx += dStride
193
+ sIdx += sStride
194
+ mIdx += mStride
195
+ }
196
+ }
@@ -0,0 +1,14 @@
1
+ import type { Color32, Rect } from '../_types'
2
+ import type { PixelData } from '../PixelData'
3
+ import { fillPixelData } from './fillPixelData'
4
+
5
+ /**
6
+ * Clears a region of the PixelData to transparent (0x00000000).
7
+ * Internally uses the optimized fillPixelData.
8
+ */
9
+ export function clearPixelData(
10
+ dst: PixelData,
11
+ rect?: Partial<Rect>,
12
+ ): void {
13
+ fillPixelData(dst, 0 as Color32, rect)
14
+ }
@@ -0,0 +1,56 @@
1
+ import type { Color32, Rect } from '../_types'
2
+ import type { PixelData } from '../PixelData'
3
+
4
+ /**
5
+ * A high-performance solid fill for PixelData.
6
+ */
7
+ export function fillPixelData(
8
+ dst: PixelData,
9
+ color: Color32,
10
+ rect?: Partial<Rect>,
11
+ ): void {
12
+ const {
13
+ x: targetX = 0,
14
+ y: targetY = 0,
15
+ w: width = dst.width,
16
+ h: height = dst.height,
17
+ } = rect || {}
18
+
19
+ let x = targetX
20
+ let y = targetY
21
+ let w = width
22
+ let h = height
23
+
24
+ // Destination Clipping
25
+ if (x < 0) {
26
+ w += x
27
+ x = 0
28
+ }
29
+ if (y < 0) {
30
+ h += y
31
+ y = 0
32
+ }
33
+
34
+ const actualW = Math.min(w, dst.width - x)
35
+ const actualH = Math.min(h, dst.height - y)
36
+
37
+ if (actualW <= 0 || actualH <= 0) {
38
+ return
39
+ }
40
+
41
+ const dst32 = dst.data32
42
+ const dw = dst.width
43
+
44
+ // Optimization: If filling the entire buffer, use the native .fill()
45
+ if (actualW === dw && actualH === dst.height && x === 0 && y === 0) {
46
+ dst32.fill(color)
47
+ return
48
+ }
49
+
50
+ // Row-by-row fill for partial rectangles
51
+ for (let iy = 0; iy < actualH; iy++) {
52
+ const start = (y + iy) * dw + x
53
+ const end = start + actualW
54
+ dst32.fill(color, start, end)
55
+ }
56
+ }
@@ -0,0 +1,20 @@
1
+ import type { ImageDataLike } from './_types'
2
+
3
+ export class PixelData {
4
+ public readonly data32: Uint32Array
5
+ public readonly width: number
6
+ public readonly height: number
7
+
8
+ constructor(public readonly imageData: ImageDataLike) {
9
+ this.width = imageData.width
10
+ this.height = imageData.height
11
+
12
+ // Create the view once.
13
+ // Shift right by 2 is a fast bitwise division by 4.
14
+ this.data32 = new Uint32Array(
15
+ imageData.data.buffer,
16
+ imageData.data.byteOffset,
17
+ imageData.data.byteLength >> 2,
18
+ )
19
+ }
20
+ }