pixel-data-js 0.0.3 → 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.
- package/dist/index.dev.cjs +343 -2
- package/dist/index.dev.cjs.map +1 -1
- package/dist/index.dev.js +318 -1
- package/dist/index.dev.js.map +1 -1
- package/dist/index.prod.cjs +343 -2
- package/dist/index.prod.cjs.map +1 -1
- package/dist/index.prod.d.ts +152 -1
- package/dist/index.prod.js +318 -1
- package/dist/index.prod.js.map +1 -1
- package/package.json +1 -1
- package/src/ImageData/blend-modes.ts +228 -0
- package/src/ImageData/blit.ts +145 -0
- package/src/_types.ts +2 -0
- package/src/color.ts +34 -1
- package/src/index.ts +4 -0
|
@@ -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
|
+
}
|
package/src/_types.ts
CHANGED
|
@@ -7,6 +7,8 @@ export type Color32 = number & { readonly __brandColor32: unique symbol };
|
|
|
7
7
|
// ALL values are floats from 0-1
|
|
8
8
|
export type RGBAFloat = RGBA & { readonly __brandRGBAFloat: unique symbol }
|
|
9
9
|
|
|
10
|
+
export type BlendColor32 = (src: Color32, dst: Color32) => Color32;
|
|
11
|
+
|
|
10
12
|
export type ImageDataLike = {
|
|
11
13
|
width: number
|
|
12
14
|
height: number
|
package/src/color.ts
CHANGED
|
@@ -45,13 +45,46 @@ export function colorDistance(a: Color32, b: Color32): number {
|
|
|
45
45
|
return dr * dr + dg * dg + db * db + da * da
|
|
46
46
|
}
|
|
47
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
|
+
*/
|
|
48
58
|
export function lerpColor32(a: Color32, b: Color32, t: number): Color32 {
|
|
49
59
|
const r = (a & 0xFF) + t * ((b & 0xFF) - (a & 0xFF))
|
|
50
60
|
const g = ((a >>> 8) & 0xFF) + t * (((b >>> 8) & 0xFF) - ((a >>> 8) & 0xFF))
|
|
51
61
|
const b_ = ((a >>> 16) & 0xFF) + t * (((b >>> 16) & 0xFF) - ((a >>> 16) & 0xFF))
|
|
52
62
|
const a_ = ((a >>> 24) & 0xFF) + t * (((b >>> 24) & 0xFF) - ((a >>> 24) & 0xFF))
|
|
53
63
|
|
|
54
|
-
return
|
|
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;
|
|
55
88
|
}
|
|
56
89
|
|
|
57
90
|
// Convert 0xAABBGGRR to #RRGGBBAA
|