pixel-data-js 0.0.2 → 0.1.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,228 @@
1
+ import type { BlendColor32, Color32 } from '../_types'
2
+
3
+ export const sourceOverColor32: BlendColor32 = (src, dst) => {
4
+ const a = (src >>> 24) & 0xFF
5
+ if (a === 255) return src
6
+ if (a === 0) return dst
7
+
8
+ // Pattern: (src * a + dst * (255 - a)) >> 8
9
+ // We process RB and G separately so they don't overflow into each other
10
+ const rbMask = 0xFF00FF
11
+ const gMask = 0x00FF00
12
+
13
+ const sRB = src & rbMask
14
+ const sG = src & gMask
15
+ const dRB = dst & rbMask
16
+ const dG = dst & gMask
17
+
18
+ const invA = 255 - a
19
+
20
+ const outRB = ((sRB * a + dRB * invA) >> 8) & rbMask
21
+ const outG = ((sG * a + dG * invA) >> 8) & gMask
22
+
23
+ // Re-pack with opaque alpha (or calculate combined alpha if needed)
24
+ const outA = (a + (((dst >>> 24) & 0xFF) * invA >> 8))
25
+ return ((outA << 24) | outRB | outG) >>> 0 as Color32
26
+ }
27
+
28
+ /**
29
+ * Screen: Lightens the destination (inverse of Multiply).
30
+ * Result = 1 - ((1 - Src) * (1 - Dst))
31
+ */
32
+ export const screenColor32: BlendColor32 = (src, dst) => {
33
+ const sa = (src >>> 24) & 0xFF
34
+ if (sa === 0) return dst
35
+
36
+ const dr = dst & 0xFF, dg = (dst >> 8) & 0xFF, db = (dst >> 16) & 0xFF
37
+
38
+ // 1. Core Math
39
+ const br = 255 - (((255 - (src & 0xFF)) * (255 - dr)) >> 8)
40
+ const bg = 255 - (((255 - ((src >> 8) & 0xFF)) * (255 - dg)) >> 8)
41
+ const bb = 255 - (((255 - ((src >> 16) & 0xFF)) * (255 - db)) >> 8)
42
+
43
+ if (sa === 255) return (0xFF000000 | (bb << 16) | (bg << 8) | br) >>> 0 as Color32
44
+
45
+ // 2. Alpha Lerp inlined
46
+ const invA = 255 - sa
47
+ const r = (br * sa + dr * invA) >> 8
48
+ const g = (bg * sa + dg * invA) >> 8
49
+ const b = (bb * sa + db * invA) >> 8
50
+ const a = (255 * sa + ((dst >>> 24) & 0xFF) * invA) >> 8
51
+
52
+ return ((a << 24) | (b << 16) | (g << 8) | r) >>> 0 as Color32
53
+ }
54
+
55
+ /**
56
+ * Linear Dodge (Additive): Simply adds the source to the destination.
57
+ * Clamps at 255.
58
+ */
59
+ export const linearDodgeColor32: BlendColor32 = (src, dst) => {
60
+ const sa = (src >>> 24) & 0xFF
61
+ if (sa === 0) return dst
62
+
63
+ const dr = dst & 0xFF, dg = (dst >> 8) & 0xFF, db = (dst >> 16) & 0xFF
64
+
65
+ // 1. Core Math (Additive with clamping)
66
+ const br = Math.min(255, (src & 0xFF) + dr)
67
+ const bg = Math.min(255, ((src >> 8) & 0xFF) + dg)
68
+ const bb = Math.min(255, ((src >> 16) & 0xFF) + db)
69
+
70
+ if (sa === 255) return (0xFF000000 | (bb << 16) | (bg << 8) | br) >>> 0 as Color32
71
+
72
+ // 2. Alpha Lerp inlined
73
+ const invA = 255 - sa
74
+ const r = (br * sa + dr * invA) >> 8
75
+ const g = (bg * sa + dg * invA) >> 8
76
+ const b = (bb * sa + db * invA) >> 8
77
+ const a = (255 * sa + ((dst >>> 24) & 0xFF) * invA) >> 8
78
+
79
+ return ((a << 24) | (b << 16) | (g << 8) | r) >>> 0 as Color32
80
+ }
81
+
82
+ /**
83
+ * Multiply: Darkens the destination based on the source color.
84
+ * Result = (Src * Dst) / 255
85
+ */
86
+ export const multiplyColor32: BlendColor32 = (src, dst) => {
87
+ const sa = (src >>> 24) & 0xFF
88
+ if (sa === 0) return dst
89
+
90
+ const dr = dst & 0xFF, dg = (dst >> 8) & 0xFF, db = (dst >> 16) & 0xFF
91
+
92
+ // 1. Core Math
93
+ const br = ((src & 0xFF) * dr) >> 8
94
+ const bg = (((src >> 8) & 0xFF) * dg) >> 8
95
+ const bb = (((src >> 16) & 0xFF) * db) >> 8
96
+
97
+ if (sa === 255) return (0xFF000000 | (bb << 16) | (bg << 8) | br) >>> 0 as Color32
98
+
99
+ // 2. Alpha Lerp inlined
100
+ const invA = 255 - sa
101
+ const r = (br * sa + dr * invA) >> 8
102
+ const g = (bg * sa + dg * invA) >> 8
103
+ const b = (bb * sa + db * invA) >> 8
104
+ const a = (255 * sa + ((dst >>> 24) & 0xFF) * invA) >> 8
105
+
106
+ return ((a << 24) | (b << 16) | (g << 8) | r) >>> 0 as Color32
107
+ }
108
+
109
+ /**
110
+ * Difference: Subtracts the darker color from the lighter color.
111
+ * Result = |Src - Dst|
112
+ */
113
+ export const differenceColor32: BlendColor32 = (src, dst) => {
114
+ const sa = (src >>> 24) & 0xFF
115
+ if (sa === 0) return dst
116
+
117
+ const dr = dst & 0xFF, dg = (dst >> 8) & 0xFF, db = (dst >> 16) & 0xFF
118
+
119
+ // 1. Core Math
120
+ const br = Math.abs((src & 0xFF) - dr)
121
+ const bg = Math.abs(((src >> 8) & 0xFF) - dg)
122
+ const bb = Math.abs(((src >> 16) & 0xFF) - db)
123
+
124
+ if (sa === 255) return (0xFF000000 | (bb << 16) | (bg << 8) | br) >>> 0 as Color32
125
+
126
+ // 2. Alpha Lerp inlined
127
+ const invA = 255 - sa
128
+ const r = (br * sa + dr * invA) >> 8
129
+ const g = (bg * sa + dg * invA) >> 8
130
+ const b = (bb * sa + db * invA) >> 8
131
+ const a = (255 * sa + ((dst >>> 24) & 0xFF) * invA) >> 8
132
+
133
+ return ((a << 24) | (b << 16) | (g << 8) | r) >>> 0 as Color32
134
+ }
135
+
136
+ /**
137
+ * Hard Light: Decides Multiply vs Screen based on SOURCE brightness.
138
+ * Acts like a harsh spotlight.
139
+ */
140
+ export const hardLightColor32: BlendColor32 = (src, dst) => {
141
+ const sa = (src >>> 24) & 0xFF
142
+ if (sa === 0) return dst
143
+
144
+ const sr = src & 0xFF, sg = (src >> 8) & 0xFF, sb = (src >> 16) & 0xFF
145
+ const dr = dst & 0xFF, dg = (dst >> 8) & 0xFF, db = (dst >> 16) & 0xFF
146
+
147
+ // 1. Core Math
148
+ const br = sr < 128 ? (2 * sr * dr) >> 8 : 255 - (2 * (255 - sr) * (255 - dr) >> 8)
149
+ const bg = sg < 128 ? (2 * sg * dg) >> 8 : 255 - (2 * (255 - sg) * (255 - dg) >> 8)
150
+ const bb = sb < 128 ? (2 * sb * db) >> 8 : 255 - (2 * (255 - sb) * (255 - db) >> 8)
151
+
152
+ if (sa === 255) return (0xFF000000 | (bb << 16) | (bg << 8) | br) >>> 0 as Color32
153
+
154
+ // 2. Alpha Lerp inlined
155
+ const invA = 255 - sa
156
+ const r = (br * sa + dr * invA) >> 8
157
+ const g = (bg * sa + dg * invA) >> 8
158
+ const b = (bb * sa + db * invA) >> 8
159
+ const a = (255 * sa + ((dst >>> 24) & 0xFF) * invA) >> 8
160
+
161
+ return ((a << 24) | (b << 16) | (g << 8) | r) >>> 0 as Color32
162
+ }
163
+
164
+ /**
165
+ * Color Burn: Darkens the destination to reflect the source color.
166
+ * Intense saturation in the darks.
167
+ */
168
+ export const colorBurnColor32: BlendColor32 = (src, dst) => {
169
+ const sa = (src >>> 24) & 0xFF
170
+ if (sa === 0) return dst
171
+
172
+ const sr = src & 0xFF, sg = (src >> 8) & 0xFF, sb = (src >> 16) & 0xFF
173
+ const dr = dst & 0xFF, dg = (dst >> 8) & 0xFF, db = (dst >> 16) & 0xFF
174
+
175
+ // 1. Core Math (Avoid division by zero)
176
+ const br = dr === 255 ? 255 : Math.max(0, 255 - ((255 - dr) << 8) / (sr || 1))
177
+ const bg = dg === 255 ? 255 : Math.max(0, 255 - ((255 - dg) << 8) / (sg || 1))
178
+ const bb = db === 255 ? 255 : Math.max(0, 255 - ((255 - db) << 8) / (sb || 1))
179
+
180
+ if (sa === 255) return (0xFF000000 | (bb << 16) | (bg << 8) | br) >>> 0 as Color32
181
+
182
+ // 2. Alpha Lerp inlined
183
+ const invA = 255 - sa
184
+ const r = (br * sa + dr * invA) >> 8
185
+ const g = (bg * sa + dg * invA) >> 8
186
+ const b = (bb * sa + db * invA) >> 8
187
+ const a = (255 * sa + ((dst >>> 24) & 0xFF) * invA) >> 8
188
+
189
+ return ((a << 24) | (b << 16) | (g << 8) | r) >>> 0 as Color32
190
+ }
191
+ /**
192
+ * Overlay: The classic "Contrast" mode.
193
+ * Decides Multiply vs Screen based on DESTINATION brightness.
194
+ */
195
+ export const overlayColor32: BlendColor32 = (src, dst) => {
196
+ const sa = (src >>> 24) & 0xFF
197
+ if (sa === 0) return dst
198
+
199
+ const sr = src & 0xFF, sg = (src >> 8) & 0xFF, sb = (src >> 16) & 0xFF
200
+ const dr = dst & 0xFF, dg = (dst >> 8) & 0xFF, db = (dst >> 16) & 0xFF
201
+
202
+ // 1. Core Math
203
+ const br = dr < 128 ? (2 * sr * dr) >> 8 : 255 - (2 * (255 - sr) * (255 - dr) >> 8)
204
+ const bg = dg < 128 ? (2 * sg * dg) >> 8 : 255 - (2 * (255 - sg) * (255 - dg) >> 8)
205
+ const bb = db < 128 ? (2 * sb * db) >> 8 : 255 - (2 * (255 - sb) * (255 - db) >> 8)
206
+
207
+ if (sa === 255) return (0xFF000000 | (bb << 16) | (bg << 8) | br) >>> 0 as Color32
208
+
209
+ // 2. Alpha Lerp inlined
210
+ const invA = 255 - sa
211
+ const r = (br * sa + dr * invA) >> 8
212
+ const g = (bg * sa + dg * invA) >> 8
213
+ const b = (bb * sa + db * invA) >> 8
214
+ const a = (255 * sa + ((dst >>> 24) & 0xFF) * invA) >> 8
215
+
216
+ return ((a << 24) | (b << 16) | (g << 8) | r) >>> 0 as Color32
217
+ }
218
+
219
+ export const COLOR_32_BLEND_MODES = {
220
+ sourceOver: sourceOverColor32,
221
+ screen: screenColor32,
222
+ linearDodge: linearDodgeColor32,
223
+ multiply: multiplyColor32,
224
+ difference: differenceColor32,
225
+ overlay: overlayColor32,
226
+ hardLight: hardLightColor32,
227
+ colorBurn: colorBurnColor32,
228
+ }
@@ -0,0 +1,145 @@
1
+ import type { BlendColor32, Color32 } from '../_types'
2
+ import { sourceOverColor32 } from './blend-modes'
3
+
4
+ export type BlendImageDataOptions = {
5
+ dx?: number
6
+ dy?: number
7
+ sx?: number
8
+ sy?: number
9
+ sw?: number
10
+ sh?: number
11
+ opacity?: number
12
+ alpha?: number
13
+ mask?: Uint8Array | null
14
+ maskMode?: 'binary' | 'alpha'
15
+ blendFn?: BlendColor32
16
+ }
17
+
18
+ /**
19
+ * Blits source ImageData into a destination ImageData using 32-bit integer bitwise blending.
20
+ * * This function bypasses standard Canvas API limitations by operating directly on
21
+ * Uint32Array views. It supports various blend modes, binary/alpha masking, and
22
+ * automatic clipping of both source and destination bounds.
23
+ * * @param dst - The destination ImageData to write into.
24
+ * @param src - The source ImageData to read from.
25
+ * @param dst - The destination ImageData to write to.
26
+ * @param opts - Configuration for the blit operation.
27
+ * @param opts.dx - Destination X offset. Defaults to 0.
28
+ * @param opts.dy - Destination Y offset. Defaults to 0.
29
+ * @param opts.sx - Source X offset. Defaults to 0.
30
+ * @param opts.sy - Source Y offset. Defaults to 0.
31
+ * @param opts.sw - Width of the source area to blit. Defaults to src.width.
32
+ * @param opts.sh - Height of the source area to blit. Defaults to src.height.
33
+ * @param opts.opacity - Global strength of the blit (0.0 to 1.0). Defaults to 1.0.
34
+ * @param opts.alpha - Global strength of the blit (0 to 255). Overrides 'opacity' if provided.
35
+ * @param opts.mask - An optional Uint8Array acting as a stencil or alpha mask.
36
+ * Must match source dimensions.
37
+ * @param opts.maskMode - 'binary' ignores pixels where mask is 0.
38
+ * 'alpha' scales source alpha by mask value (0-255).
39
+ * @param opts.blendFn - The math logic used to combine pixels.
40
+ * Defaults to `sourceOverColor32`.
41
+ * * @example
42
+ * blendImageData32(ctx.getImageData(0,0,100,100), sprite, {
43
+ * blendFn: COLOR_32_BLEND_MODES.multiply,
44
+ * mask: brushMask,
45
+ * maskMode: 'alpha'
46
+ * });
47
+ */
48
+ export function blendImageData(
49
+ dst: ImageData,
50
+ src: ImageData,
51
+ opts: BlendImageDataOptions,
52
+ ) {
53
+ let {
54
+ dx = 0,
55
+ dy = 0,
56
+ sx = 0,
57
+ sy = 0,
58
+ sw = src.width,
59
+ sh = src.height,
60
+ maskMode = 'alpha',
61
+ opacity = 1,
62
+ alpha,
63
+ blendFn = sourceOverColor32,
64
+ mask,
65
+ } = opts
66
+
67
+ // 1. Clip Source Area
68
+ if (sx < 0) {
69
+ dx -= sx
70
+ sw += sx
71
+ sx = 0
72
+ }
73
+ if (sy < 0) {
74
+ dy -= sy
75
+ sh += sy
76
+ sy = 0
77
+ }
78
+ sw = Math.min(sw, src.width - sx)
79
+ sh = Math.min(sh, src.height - sy)
80
+
81
+ // 2. Clip Destination Area
82
+ if (dx < 0) {
83
+ sx -= dx
84
+ sw += dx
85
+ dx = 0
86
+ }
87
+ if (dy < 0) {
88
+ sy -= dy
89
+ sh += dy
90
+ dy = 0
91
+ }
92
+ const actualW = Math.min(sw, dst.width - dx)
93
+ const actualH = Math.min(sh, dst.height - dy)
94
+
95
+ if (actualW <= 0 || actualH <= 0) return
96
+
97
+ // 32-bit views of the same memory
98
+ const dst32 = new Uint32Array(dst.data.buffer)
99
+ const src32 = new Uint32Array(src.data.buffer)
100
+
101
+ const dw = dst.width
102
+ const sw_orig = src.width
103
+
104
+ const gAlpha = alpha !== undefined
105
+ ? (alpha | 0)
106
+ : Math.round(opacity * 255)
107
+
108
+ const maskIsAlpha = maskMode === 'alpha'
109
+
110
+ for (let iy = 0; iy < actualH; iy++) {
111
+ const dRow = (iy + dy) * dw
112
+ const sRow = (iy + sy) * sw_orig
113
+
114
+ for (let ix = 0; ix < actualW; ix++) {
115
+ const di = dRow + (ix + dx)
116
+ const si = sRow + (ix + sx)
117
+
118
+ let s = src32[si] as Color32
119
+ let sa = (s >>> 24) & 0xFF
120
+
121
+ // skip fully transparent pixel
122
+ if (sa === 0) continue
123
+
124
+ let activeWeight = gAlpha
125
+
126
+ if (mask) {
127
+ const m = mask[si]
128
+ if (m === 0) continue
129
+ activeWeight = maskIsAlpha ? (m * activeWeight + 128) >> 8 : activeWeight
130
+ }
131
+
132
+ if (activeWeight < 255) {
133
+ sa = (sa * activeWeight + 128) >> 8
134
+ }
135
+
136
+ // If combined alpha is 0 after masking/opacity, skip the blend math
137
+ if (sa === 0) continue
138
+
139
+ // Re-pack source with final calculated alpha
140
+ s = ((s & 0x00FFFFFF) | (sa << 24)) >>> 0 as Color32
141
+
142
+ dst32[di] = blendFn(s, dst32[di] as Color32)
143
+ }
144
+ }
145
+ }
@@ -0,0 +1,36 @@
1
+ import type { Color32, ImageDataLike } from '../_types'
2
+
3
+ export function makeImageDataColor32Adapter(imageData: ImageDataLike) {
4
+ const data32 = new Uint32Array(imageData.data.buffer)
5
+
6
+ function inBounds(x: number, y: number) {
7
+ return x < 0 || x >= imageData.width || y < 0 || y >= imageData.height
8
+ }
9
+
10
+ function setPixel(
11
+ x: number,
12
+ y: number,
13
+ color: Color32,
14
+ ): void {
15
+ if (x < 0 || x >= imageData.width || y < 0 || y >= imageData.height) return
16
+ data32[y * imageData.width + x] = color
17
+ }
18
+
19
+ function getPixel(
20
+ x: number,
21
+ y: number,
22
+ ): Color32 | undefined {
23
+ if (x < 0 || x >= imageData.width || y < 0 || y >= imageData.height) return
24
+
25
+ return data32[y * imageData.width + x] as Color32
26
+ }
27
+
28
+ return {
29
+ inBounds,
30
+ imageData,
31
+ data32,
32
+ setPixel,
33
+ getPixel,
34
+ }
35
+ }
36
+
@@ -1,37 +1,48 @@
1
- export type SerializedImageData = {
2
- width: number,
3
- height: number,
4
- data: string,
1
+ import type { Base64EncodedUInt8Array, ImageDataLike, SerializedImageData } from '../_types'
2
+
3
+ export function base64EncodeArrayBuffer(buffer: ArrayBufferLike): Base64EncodedUInt8Array {
4
+ const binary = String.fromCharCode(...new Uint8Array(buffer))
5
+ return btoa(binary) as Base64EncodedUInt8Array
6
+ }
7
+
8
+ export function base64DecodeArrayBuffer(encoded: Base64EncodedUInt8Array): Uint8ClampedArray {
9
+ const binary = atob(encoded)
10
+ const bytes = new Uint8ClampedArray(binary.length)
11
+ for (let i = 0; i < binary.length; i++) {
12
+ bytes[i] = binary.charCodeAt(i)
13
+ }
14
+ return bytes
5
15
  }
6
16
 
7
17
  /**
8
- * Serialize for use in JSON. Stored as base64 encoded string.
18
+ * Serialize for use in JSON. Pixel data is stored as base64 encoded string.
9
19
  */
10
- export function serializeImageData<T extends ImageData>(imageData: T): SerializedImageData {
11
- const binary = String.fromCharCode(...new Uint8Array(imageData.data.buffer))
12
- const base64 = btoa(binary)
13
-
20
+ export function serializeImageData<T extends ImageDataLike>(imageData: T): SerializedImageData {
14
21
  return {
15
22
  width: imageData.width,
16
23
  height: imageData.height,
17
- data: base64,
24
+ data: base64EncodeArrayBuffer(imageData.data.buffer),
18
25
  }
19
26
  }
20
27
 
21
- export function serializeNullableImageData<T extends ImageData | null>(imageData: T): T extends null ? null : SerializedImageData {
28
+ export function serializeNullableImageData<T extends ImageDataLike | null>(imageData: T): T extends null ? null : SerializedImageData {
22
29
  if (!imageData) return null as any
23
30
 
24
31
  return serializeImageData(imageData) as any
25
32
  }
26
33
 
27
- export function deserializeImageData<T extends SerializedImageData>(serialized: T): ImageData {
28
- const binary = atob(serialized.data as string)
29
- const bytes = new Uint8ClampedArray(binary.length)
30
- for (let i = 0; i < binary.length; i++) {
31
- bytes[i] = binary.charCodeAt(i)
34
+ export function deserializeRawImageData<T extends SerializedImageData>(serialized: T): ImageDataLike {
35
+ return {
36
+ width: serialized.width,
37
+ height: serialized.height,
38
+ data: base64DecodeArrayBuffer(serialized.data as Base64EncodedUInt8Array),
32
39
  }
40
+ }
41
+
42
+ export function deserializeImageData<T extends SerializedImageData>(serialized: T): ImageData {
43
+ const data = base64DecodeArrayBuffer(serialized.data as Base64EncodedUInt8Array)
33
44
 
34
- return new ImageData(bytes, serialized.width, serialized.height) as any
45
+ return new ImageData(data as ImageDataArray, serialized.width, serialized.height) as any
35
46
  }
36
47
 
37
48
  export function deserializeNullableImageData<T extends SerializedImageData | null>(serialized: T): T extends null ? null : ImageData {
package/src/_types.ts ADDED
@@ -0,0 +1,24 @@
1
+ // ALL values are 0-255 (including alpha which in CSS is 0-1)
2
+ export type RGBA = { r: number, g: number, b: number, a: number }
3
+
4
+ // A 32-bit integer containing r,g,b,a data
5
+ export type Color32 = number & { readonly __brandColor32: unique symbol };
6
+
7
+ // ALL values are floats from 0-1
8
+ export type RGBAFloat = RGBA & { readonly __brandRGBAFloat: unique symbol }
9
+
10
+ export type BlendColor32 = (src: Color32, dst: Color32) => Color32;
11
+
12
+ export type ImageDataLike = {
13
+ width: number
14
+ height: number
15
+ data: Uint8ClampedArray
16
+ }
17
+
18
+ export type SerializedImageData = {
19
+ width: number,
20
+ height: number,
21
+ data: string,
22
+ }
23
+
24
+ export type Base64EncodedUInt8Array = string & { readonly __brandBase64UInt8Array: unique symbol }
package/src/color.ts ADDED
@@ -0,0 +1,112 @@
1
+ import type { Color32, RGBA } from './_types'
2
+
3
+ /**
4
+ * Packs RGBA into a 32-bit integer compatible with
5
+ * Little-Endian Uint32Array views on ImageData.
6
+ */
7
+ export function packColor(r: number, g: number, b: number, a: number): Color32 {
8
+ return ((a << 24) | (b << 16) | (g << 8) | r) >>> 0 as Color32
9
+ }
10
+
11
+ export function packRGBA({ r, g, b, a }: RGBA): Color32 {
12
+ return ((a << 24) | (b << 16) | (g << 8) | r) >>> 0 as Color32
13
+ }
14
+
15
+ export const unpackRed = (packed: Color32): number => (packed >>> 0) & 0xFF
16
+ export const unpackGreen = (packed: Color32): number => (packed >>> 8) & 0xFF
17
+ export const unpackBlue = (packed: Color32): number => (packed >>> 16) & 0xFF
18
+ export const unpackAlpha = (packed: Color32): number => (packed >>> 24) & 0xFF
19
+
20
+ export function unpackColor(packed: Color32): RGBA {
21
+ return {
22
+ r: (packed >>> 0) & 0xFF,
23
+ g: (packed >>> 8) & 0xFF,
24
+ b: (packed >>> 16) & 0xFF,
25
+ a: (packed >>> 24) & 0xFF,
26
+ }
27
+ }
28
+
29
+ const SCRATCH_RGBA: RGBA = { r: 0, g: 0, b: 0, a: 0 }
30
+
31
+ // uses a scratch arg for memory perf. Be careful about re-use.
32
+ export function unpackColorTo(packed: Color32, scratch = SCRATCH_RGBA): RGBA {
33
+ scratch.r = (packed >>> 0) & 0xFF
34
+ scratch.g = (packed >>> 8) & 0xFF
35
+ scratch.b = (packed >>> 16) & 0xFF
36
+ scratch.a = (packed >>> 24) & 0xFF
37
+ return scratch
38
+ }
39
+
40
+ export function colorDistance(a: Color32, b: Color32): number {
41
+ const dr = (a & 0xFF) - (b & 0xFF)
42
+ const dg = ((a >>> 8) & 0xFF) - ((b >>> 8) & 0xFF)
43
+ const db = ((a >>> 16) & 0xFF) - ((b >>> 16) & 0xFF)
44
+ const da = ((a >>> 24) & 0xFF) - ((b >>> 24) & 0xFF)
45
+ return dr * dr + dg * dg + db * db + da * da
46
+ }
47
+
48
+ /**
49
+ * Linearly interpolates between two 32-bit colors using a floating-point weight.
50
+ * * This is the preferred method for UI animations or scenarios where high
51
+ * precision is required. It uses the standard `a + t * (b - a)` formula
52
+ * for each channel.
53
+ * @param a - The starting color as a 32-bit integer (AABBGGRR).
54
+ * @param b - The target color as a 32-bit integer (AABBGGRR).
55
+ * @param t - The interpolation factor between 0.0 and 1.0.
56
+ * @returns The interpolated 32-bit color.
57
+ */
58
+ export function lerpColor32(a: Color32, b: Color32, t: number): Color32 {
59
+ const r = (a & 0xFF) + t * ((b & 0xFF) - (a & 0xFF))
60
+ const g = ((a >>> 8) & 0xFF) + t * (((b >>> 8) & 0xFF) - ((a >>> 8) & 0xFF))
61
+ const b_ = ((a >>> 16) & 0xFF) + t * (((b >>> 16) & 0xFF) - ((a >>> 16) & 0xFF))
62
+ const a_ = ((a >>> 24) & 0xFF) + t * (((b >>> 24) & 0xFF) - ((a >>> 24) & 0xFF))
63
+
64
+ return ((a_ << 24) | (b_ << 16) | (g << 8) | r) >>> 0 as Color32
65
+ }
66
+
67
+ /**
68
+ * Linearly interpolates between two 32-bit colors using integer fixed-point math.
69
+ * Highly optimized for image processing and real-time blitting. It processes
70
+ * channels in parallel using bitmasks (RB and GA pairs).
71
+ * @note Subject to a 1-bit drift (rounding down) due to fast bit-shift division.
72
+ * @param src - The source (foreground) color as a 32-bit integer.
73
+ * @param dst - The destination (background) color as a 32-bit integer.
74
+ * @param w - The blend weight as a byte value from 0 to 255. Where 0 is 100% dst and 255 is 100% src
75
+ * @returns The blended 32-bit color.
76
+ */export function lerpColor32Fast(src: Color32, dst: Color32, w: number): Color32 {
77
+ const invA = 255 - w;
78
+
79
+ // Masking Red and Blue: 0x00FF00FF
80
+ // We process R and B in one go, then shift back down
81
+ const rb = (((src & 0x00FF00FF) * w + (dst & 0x00FF00FF) * invA) >>> 8) & 0x00FF00FF;
82
+
83
+ // Masking Green and Alpha: 0xFF00FF00
84
+ // We shift down first to avoid overflow, then shift back up
85
+ const ga = ((((src >>> 8) & 0x00FF00FF) * w + ((dst >>> 8) & 0x00FF00FF) * invA) >>> 8) & 0x00FF00FF;
86
+
87
+ return (rb | (ga << 8)) >>> 0 as Color32;
88
+ }
89
+
90
+ // Convert 0xAABBGGRR to #RRGGBBAA
91
+ export function color32ToHex(color: Color32): string {
92
+ const r = (color & 0xFF).toString(16).padStart(2, '0')
93
+ const g = ((color >>> 8) & 0xFF).toString(16).padStart(2, '0')
94
+ const b = ((color >>> 16) & 0xFF).toString(16).padStart(2, '0')
95
+ const a = ((color >>> 24) & 0xFF).toString(16).padStart(2, '0')
96
+ return `#${r}${g}${b}${a}`
97
+ }
98
+
99
+ /**
100
+ * Converts a 32-bit integer (0xAABBGGRR) to a CSS rgba() string.
101
+ * Example: 0xFF0000FF -> "rgba(255,0,0,1)"
102
+ */
103
+ export function color32ToCssRGBA(color: Color32): string {
104
+ const r = color & 0xFF
105
+ const g = (color >>> 8) & 0xFF
106
+ const b = (color >>> 16) & 0xFF
107
+ const a = (color >>> 24) & 0xFF
108
+
109
+ const alpha = Number((a / 255).toFixed(3))
110
+
111
+ return `rgba(${r},${g},${b},${alpha})`
112
+ }
package/src/index.ts CHANGED
@@ -1 +1,5 @@
1
+ export * from './color'
1
2
  export * from './ImageData/serialization'
3
+ export * from './ImageData/blit'
4
+ export * from './ImageData/blend-modes'
5
+ export * from './ImageData/read-write-pixels'