pixel-data-js 0.2.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.
@@ -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
+ }
package/src/_types.ts CHANGED
@@ -1,9 +1,15 @@
1
- // ALL values are 0-255 (including alpha which in CSS is 0-1)
1
+ /** ALL values are 0-255 (including alpha which in CSS is 0-1) */
2
2
  export type RGBA = { r: number, g: number, b: number, a: number }
3
3
 
4
- // A 32-bit integer containing r,g,b,a data
4
+ /** Represents a 32-bit color in 0xAABBGGRR (Little endian) */
5
5
  export type Color32 = number & { readonly __brandColor32: unique symbol }
6
6
 
7
+ /**
8
+ * A function that defines how to combine a source color with a destination color.
9
+ * @param src - The incoming color (source).
10
+ * @param dst - The existing color in the buffer (destination).
11
+ * @returns The resulting 32-bit color to be written to the buffer.
12
+ */
7
13
  export type BlendColor32 = (src: Color32, dst: Color32) => Color32
8
14
 
9
15
  export type ImageDataLike = {
@@ -20,25 +26,127 @@ export type SerializedImageData = {
20
26
 
21
27
  export type Base64EncodedUInt8Array = string & { readonly __brandBase64UInt8Array: unique symbol }
22
28
 
23
- export type Rect = { x: number; y: number; w: number; h: number }
29
+ /** Rectangle definition */
30
+ export type Rect = {
31
+ x: number
32
+ y: number
33
+ w: number
34
+ h: number
35
+ }
24
36
 
37
+ /**
38
+ * Defines how mask values should be interpreted during a draw operation.
39
+ */
25
40
  export enum MaskType {
41
+ /**
42
+ * Values are treated as alpha weights.
43
+ * 0 is skipped, values > 0 are processed.
44
+ */
26
45
  ALPHA,
46
+ /**
47
+ * Values are treated as on/off.
48
+ * 0 is fully transparent (skipped), any other value is fully opaque.
49
+ */
27
50
  BINARY
28
51
  }
29
52
 
30
- interface BaseMaskData {
31
- readonly width: number
32
- readonly height: number
33
- readonly data: Uint8Array
53
+ /** Strictly 0 or 1 */
54
+ export type BinaryMask = Uint8Array & { readonly __brand: 'Binary' }
55
+ /** Strictly 0-255 */
56
+ export type AlphaMask = Uint8Array & { readonly __brand: 'Alpha' }
57
+
58
+ export type AnyMask = BinaryMask | AlphaMask
59
+
60
+ /**
61
+ * Configuration for pixel manipulation operations.
62
+ * Designed to be used by spreading a Rect object ({x, y, w, h}) directly.
63
+ */
64
+ export interface PixelOptions {
65
+ /**
66
+ * The starting X coordinate in the destination buffer.
67
+ * @Defaults 0.
68
+ * */
69
+ x?: number
70
+ /**
71
+ * The starting Y coordinate in the destination buffer.
72
+ * @Default 0.
73
+ * */
74
+ y?: number
75
+ /**
76
+ * The width of the region to process.
77
+ * @Default Source width.
78
+ * */
79
+ w?: number
80
+ /**
81
+ * The height of the region to process.
82
+ * @Default Source height.
83
+ * */
84
+ h?: number
85
+
86
+ /**
87
+ * Overall layer opacity 0-255.
88
+ * @default 255
89
+ */
90
+ alpha?: number
91
+
92
+ /**
93
+ * Mask width.
94
+ * @default w
95
+ * */
96
+ mw?: number
97
+
98
+ /**
99
+ * X offset into the mask buffer.
100
+ * @default 0
101
+ * */
102
+ mx?: number
103
+
104
+ /**
105
+ * Y offset into the mask buffer.
106
+ * @default 0
107
+ * */
108
+ my?: number
109
+
110
+ /** An optional mask to restrict where pixels are written. */
111
+ mask?: AnyMask | null
112
+
113
+ /** The interpretation logic for the provided mask. Defaults to MaskType.Binary. */
114
+ maskType?: MaskType
115
+
116
+ /** If true the inverse of the mask will be applied */
117
+ invertMask?: boolean
34
118
  }
35
119
 
36
- export interface AlphaMask extends BaseMaskData {
37
- readonly type: MaskType.ALPHA
120
+ /**
121
+ * Configuration for blitting (copying/blending) one image into another.
122
+ */
123
+ export interface PixelBlendOptions extends PixelOptions {
124
+ /**
125
+ * The source rectangle x-coordinate
126
+ * @default 0
127
+ */
128
+ sx?: number
129
+
130
+ /**
131
+ * The source rectangle y-coordinate
132
+ * @default 0
133
+ */
134
+ sy?: number
135
+
136
+ /** The specific blending function/algorithm to use for pixel math. */
137
+ blendFn?: BlendColor32
38
138
  }
39
139
 
40
- export interface BinaryMask extends BaseMaskData {
41
- readonly type: MaskType.BINARY
140
+ /**
141
+ * Configuration for operations that require color blending.
142
+ */
143
+ export interface ColorBlendOptions extends PixelOptions {
144
+ /** The blending logic used to combine source and destination pixels. */
145
+ blendFn?: BlendColor32
42
146
  }
43
147
 
44
- export type AnyMask = AlphaMask | BinaryMask
148
+ export type ApplyMaskOptions = Omit<PixelOptions, 'mask'>
149
+
150
+ // export function invertBinaryMask(dst: BinaryMask): void
151
+ // export function invertAlphaMask(dst: AlphaMask): void
152
+
@@ -1,4 +1,4 @@
1
- import type { BlendColor32, Color32 } from '../_types'
1
+ import type { BlendColor32, Color32 } from './_types'
2
2
 
3
3
  export const sourceOverColor32: BlendColor32 = (src, dst) => {
4
4
  const a = (src >>> 24) & 0xFF
package/src/index.ts CHANGED
@@ -1,7 +1,7 @@
1
- export * from './ImageData/blend-modes'
2
- export * from './ImageData/blit'
3
- export * from './ImageData/mask'
4
- export * from './ImageData/read-write-pixels'
5
- export * from './ImageData/serialization'
6
1
  export * from './_types'
2
+ export * from './blend-modes'
7
3
  export * from './color'
4
+ export * from './ImageData/copyImageData'
5
+ export * from './ImageData/extractImageData'
6
+ export * from './ImageData/serialization'
7
+ export * from './PixelData/blendPixelData'