pixel-data-js 0.0.3 → 0.2.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 +462 -2
- package/dist/index.dev.cjs.map +1 -1
- package/dist/index.dev.js +431 -1
- package/dist/index.dev.js.map +1 -1
- package/dist/index.prod.cjs +462 -2
- package/dist/index.prod.cjs.map +1 -1
- package/dist/index.prod.d.ts +249 -3
- package/dist/index.prod.js +431 -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 +177 -0
- package/src/ImageData/mask.ts +150 -0
- package/src/ImageData/read-write-pixels.ts +66 -1
- package/src/ImageData/serialization.ts +1 -1
- package/src/_types.ts +29 -7
- package/src/color.ts +34 -1
- package/src/index.ts +6 -0
|
@@ -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,177 @@
|
|
|
1
|
+
import { type AnyMask, type BlendColor32, type Color32, type ImageDataLike, MaskType } from '../_types'
|
|
2
|
+
import { sourceOverColor32 } from './blend-modes'
|
|
3
|
+
|
|
4
|
+
export type BlendImageDataOptions = {
|
|
5
|
+
/**
|
|
6
|
+
* The x-coordinate in the destination image where the blend begins.
|
|
7
|
+
* @default 0
|
|
8
|
+
*/
|
|
9
|
+
dx?: number
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The y-coordinate in the destination image where the blend begins.
|
|
13
|
+
* @default 0
|
|
14
|
+
*/
|
|
15
|
+
dy?: number
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The x-coordinate of the top-left corner of the sub-rectangle
|
|
19
|
+
* of the source image to extract.
|
|
20
|
+
* @default 0
|
|
21
|
+
*/
|
|
22
|
+
sx?: number
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The y-coordinate of the top-left corner of the sub-rectangle
|
|
26
|
+
* of the source image to extract.
|
|
27
|
+
* @default 0
|
|
28
|
+
*/
|
|
29
|
+
sy?: number
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The width of the sub-rectangle of the source image to extract.
|
|
33
|
+
* Defaults to the full remaining width of the source.
|
|
34
|
+
*/
|
|
35
|
+
sw?: number
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The height of the sub-rectangle of the source image to extract.
|
|
39
|
+
* Defaults to the full remaining height of the source.
|
|
40
|
+
*/
|
|
41
|
+
sh?: number
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Overall layer opacity, typically ranging from 0.0 (transparent) to 1.0 (opaque).
|
|
45
|
+
* @default 1.0
|
|
46
|
+
*/
|
|
47
|
+
opacity?: number
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Same as opacity but is 0-255 and faster when processing. If Present opacity is ignored.
|
|
51
|
+
* @default undefined
|
|
52
|
+
*/
|
|
53
|
+
alpha?: number
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* An optional alpha mask buffer.
|
|
57
|
+
* The values in this array (0-255) determine the intensity of the blend
|
|
58
|
+
* at each corresponding pixel.
|
|
59
|
+
*/
|
|
60
|
+
mask?: AnyMask | null
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The specific blending function/algorithm to use for pixel math
|
|
64
|
+
* (e.g., Multiply, Screen, Overlay).
|
|
65
|
+
*/
|
|
66
|
+
blendFn?: BlendColor32
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Blits source ImageData into a destination ImageData using 32-bit integer bitwise blending.
|
|
71
|
+
* This function bypasses standard Canvas API limitations by operating directly on
|
|
72
|
+
* Uint32Array views. It supports various blend modes, binary/alpha masking, and
|
|
73
|
+
* automatic clipping of both source and destination bounds.
|
|
74
|
+
* @example
|
|
75
|
+
* blendImageData32(ctx.getImageData(0,0,100,100), sprite, {
|
|
76
|
+
* blendFn: COLOR_32_BLEND_MODES.multiply,
|
|
77
|
+
* mask: brushMask,
|
|
78
|
+
* maskMode: MaskMode.ALPHA
|
|
79
|
+
* });
|
|
80
|
+
*/
|
|
81
|
+
export function blendImageData(
|
|
82
|
+
dst: ImageDataLike,
|
|
83
|
+
src: ImageDataLike,
|
|
84
|
+
opts: BlendImageDataOptions,
|
|
85
|
+
) {
|
|
86
|
+
let {
|
|
87
|
+
dx = 0,
|
|
88
|
+
dy = 0,
|
|
89
|
+
sx = 0,
|
|
90
|
+
sy = 0,
|
|
91
|
+
sw = src.width,
|
|
92
|
+
sh = src.height,
|
|
93
|
+
opacity = 1,
|
|
94
|
+
alpha,
|
|
95
|
+
blendFn = sourceOverColor32,
|
|
96
|
+
mask,
|
|
97
|
+
} = opts
|
|
98
|
+
|
|
99
|
+
// 1. Clip Source Area
|
|
100
|
+
if (sx < 0) {
|
|
101
|
+
dx -= sx
|
|
102
|
+
sw += sx
|
|
103
|
+
sx = 0
|
|
104
|
+
}
|
|
105
|
+
if (sy < 0) {
|
|
106
|
+
dy -= sy
|
|
107
|
+
sh += sy
|
|
108
|
+
sy = 0
|
|
109
|
+
}
|
|
110
|
+
sw = Math.min(sw, src.width - sx)
|
|
111
|
+
sh = Math.min(sh, src.height - sy)
|
|
112
|
+
|
|
113
|
+
// 2. Clip Destination Area
|
|
114
|
+
if (dx < 0) {
|
|
115
|
+
sx -= dx
|
|
116
|
+
sw += dx
|
|
117
|
+
dx = 0
|
|
118
|
+
}
|
|
119
|
+
if (dy < 0) {
|
|
120
|
+
sy -= dy
|
|
121
|
+
sh += dy
|
|
122
|
+
dy = 0
|
|
123
|
+
}
|
|
124
|
+
const actualW = Math.min(sw, dst.width - dx)
|
|
125
|
+
const actualH = Math.min(sh, dst.height - dy)
|
|
126
|
+
|
|
127
|
+
if (actualW <= 0 || actualH <= 0) return
|
|
128
|
+
|
|
129
|
+
// 32-bit views of the same memory
|
|
130
|
+
const dst32 = new Uint32Array(dst.data.buffer)
|
|
131
|
+
const src32 = new Uint32Array(src.data.buffer)
|
|
132
|
+
|
|
133
|
+
const dw = dst.width
|
|
134
|
+
const sw_orig = src.width
|
|
135
|
+
|
|
136
|
+
const gAlpha = alpha !== undefined
|
|
137
|
+
? (alpha | 0)
|
|
138
|
+
: Math.round(opacity * 255)
|
|
139
|
+
|
|
140
|
+
const maskIsAlpha = mask?.type === MaskType.ALPHA
|
|
141
|
+
|
|
142
|
+
for (let iy = 0; iy < actualH; iy++) {
|
|
143
|
+
const dRow = (iy + dy) * dw
|
|
144
|
+
const sRow = (iy + sy) * sw_orig
|
|
145
|
+
|
|
146
|
+
for (let ix = 0; ix < actualW; ix++) {
|
|
147
|
+
const di = dRow + (ix + dx)
|
|
148
|
+
const si = sRow + (ix + sx)
|
|
149
|
+
|
|
150
|
+
let s = src32[si] as Color32
|
|
151
|
+
let sa = (s >>> 24) & 0xFF
|
|
152
|
+
|
|
153
|
+
// skip fully transparent pixel
|
|
154
|
+
if (sa === 0) continue
|
|
155
|
+
|
|
156
|
+
let activeWeight = gAlpha
|
|
157
|
+
|
|
158
|
+
if (mask) {
|
|
159
|
+
const m = mask.data[si]
|
|
160
|
+
if (m === 0) continue
|
|
161
|
+
activeWeight = maskIsAlpha ? (m * activeWeight + 128) >> 8 : activeWeight
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (activeWeight < 255) {
|
|
165
|
+
sa = (sa * activeWeight + 128) >> 8
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// If combined alpha is 0 after masking/opacity, skip the blend math
|
|
169
|
+
if (sa === 0) continue
|
|
170
|
+
|
|
171
|
+
// Re-pack source with final calculated alpha
|
|
172
|
+
s = ((s & 0x00FFFFFF) | (sa << 24)) >>> 0 as Color32
|
|
173
|
+
|
|
174
|
+
dst32[di] = blendFn(s, dst32[di] as Color32)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type { AlphaMask, BinaryMask, ImageDataLike } from '../_types'
|
|
2
|
+
|
|
3
|
+
export type ApplyMaskOptions = {
|
|
4
|
+
/**
|
|
5
|
+
* The x-coordinate in the destination image where the mask begins.
|
|
6
|
+
* @default 0
|
|
7
|
+
*/
|
|
8
|
+
dx?: number
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The y-coordinate in the destination image where the mask begins.
|
|
12
|
+
* @default 0
|
|
13
|
+
*/
|
|
14
|
+
dy?: number
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The x-coordinate of the top-left corner of the sub-rectangle
|
|
18
|
+
* of the source image to extract.
|
|
19
|
+
* @default 0
|
|
20
|
+
*/
|
|
21
|
+
sx?: number
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The y-coordinate of the top-left corner of the sub-rectangle
|
|
25
|
+
* of the source image to extract.
|
|
26
|
+
* @default 0
|
|
27
|
+
*/
|
|
28
|
+
sy?: number
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The width of the sub-rectangle of the source image to extract.
|
|
32
|
+
* Defaults to the full remaining width of the source.
|
|
33
|
+
*/
|
|
34
|
+
sw?: number
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The height of the sub-rectangle of the source image to extract.
|
|
38
|
+
* Defaults to the full remaining height of the source.
|
|
39
|
+
*/
|
|
40
|
+
sh?: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Applies a binary (on/off) mask to an RGBA buffer.
|
|
45
|
+
* If mask value is 0, pixel becomes transparent.
|
|
46
|
+
*/
|
|
47
|
+
export function applyBinaryMask(
|
|
48
|
+
dst: ImageDataLike,
|
|
49
|
+
mask: BinaryMask,
|
|
50
|
+
opts: ApplyMaskOptions = {},
|
|
51
|
+
) {
|
|
52
|
+
const { width: maskWidth, height: maskHeight } = mask
|
|
53
|
+
|
|
54
|
+
const { dx = 0, dy = 0, sx = 0, sy = 0, sw = maskWidth, sh = maskHeight } = opts
|
|
55
|
+
|
|
56
|
+
// 1. Calculate intersection boundaries
|
|
57
|
+
const x0 = Math.max(0, dx, dx + (0 - sx))
|
|
58
|
+
const y0 = Math.max(0, dy, dy + (0 - sy))
|
|
59
|
+
const x1 = Math.min(dst.width, dx + sw, dx + (maskWidth - sx))
|
|
60
|
+
const y1 = Math.min(dst.height, dy + sh, dy + (maskHeight - sy))
|
|
61
|
+
|
|
62
|
+
if (x1 <= x0 || y1 <= y0) return
|
|
63
|
+
|
|
64
|
+
const { data: dstData, width: dstW } = dst
|
|
65
|
+
|
|
66
|
+
for (let y = y0; y < y1; y++) {
|
|
67
|
+
const maskY = y - dy + sy
|
|
68
|
+
const dstRowOffset = y * dstW * 4
|
|
69
|
+
const maskRowOffset = maskY * maskWidth
|
|
70
|
+
|
|
71
|
+
for (let x = x0; x < x1; x++) {
|
|
72
|
+
const maskX = x - dx + sx
|
|
73
|
+
const mIdx = maskRowOffset + maskX
|
|
74
|
+
|
|
75
|
+
// Binary check: If mask is 0, kill the alpha
|
|
76
|
+
if (mask.data[mIdx] === 0) {
|
|
77
|
+
const aIdx = dstRowOffset + (x * 4) + 3
|
|
78
|
+
dstData[aIdx] = 0
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return dst
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Applies a smooth alpha mask to an RGBA buffer.
|
|
88
|
+
* Multiplies existing Alpha by (maskValue / 255).
|
|
89
|
+
*/
|
|
90
|
+
export function applyAlphaMask(
|
|
91
|
+
dst: ImageData,
|
|
92
|
+
mask: AlphaMask,
|
|
93
|
+
opts: ApplyMaskOptions = {},
|
|
94
|
+
): void {
|
|
95
|
+
let { dx = 0, dy = 0, sx = 0, sy = 0, sw = mask.width, sh = mask.height } = opts
|
|
96
|
+
|
|
97
|
+
// 1. Clipping Logic
|
|
98
|
+
if (dx < 0) {
|
|
99
|
+
sx -= dx
|
|
100
|
+
sw += dx
|
|
101
|
+
dx = 0
|
|
102
|
+
}
|
|
103
|
+
if (dy < 0) {
|
|
104
|
+
sy -= dy
|
|
105
|
+
sh += dy
|
|
106
|
+
dy = 0
|
|
107
|
+
}
|
|
108
|
+
if (sx < 0) {
|
|
109
|
+
dx -= sx
|
|
110
|
+
sw += sx
|
|
111
|
+
sx = 0
|
|
112
|
+
}
|
|
113
|
+
if (sy < 0) {
|
|
114
|
+
dy -= sy
|
|
115
|
+
sh += sy
|
|
116
|
+
sy = 0
|
|
117
|
+
}
|
|
118
|
+
const actualW = Math.min(sw, dst.width - dx, mask.width - sx)
|
|
119
|
+
const actualH = Math.min(sh, dst.height - dy, mask.height - sy)
|
|
120
|
+
|
|
121
|
+
if (actualW <= 0 || actualH <= 0) return
|
|
122
|
+
|
|
123
|
+
const dData = dst.data
|
|
124
|
+
const mData = mask.data
|
|
125
|
+
const dW = dst.width
|
|
126
|
+
const mW = mask.width
|
|
127
|
+
|
|
128
|
+
for (let y = 0; y < actualH; y++) {
|
|
129
|
+
const dOffset = ((dy + y) * dW + dx) << 2
|
|
130
|
+
const mOffset = (sy + y) * mW + sx
|
|
131
|
+
|
|
132
|
+
for (let x = 0; x < actualW; x++) {
|
|
133
|
+
const mVal = mData[mOffset + x]
|
|
134
|
+
|
|
135
|
+
if (mVal === 255) continue
|
|
136
|
+
|
|
137
|
+
const aIdx = dOffset + (x << 2) + 3
|
|
138
|
+
|
|
139
|
+
// --- BRANCH: Zero Alpha ---
|
|
140
|
+
if (mVal === 0) {
|
|
141
|
+
dData[aIdx] = 0
|
|
142
|
+
continue
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// To get 101 from 200 * 128, we use the bias (a * m + 257) >> 8
|
|
146
|
+
// 25600 + 257 = 25857. 25857 >> 8 = 101.
|
|
147
|
+
dData[aIdx] = (dData[aIdx] * mVal + 257) >> 8
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Color32, ImageDataLike } from '../_types'
|
|
1
|
+
import type { Color32, ImageDataLike, Rect } from '../_types'
|
|
2
2
|
|
|
3
3
|
export function makeImageDataColor32Adapter(imageData: ImageDataLike) {
|
|
4
4
|
const data32 = new Uint32Array(imageData.data.buffer)
|
|
@@ -34,3 +34,68 @@ export function makeImageDataColor32Adapter(imageData: ImageDataLike) {
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
export function extractPixelData(
|
|
38
|
+
imageData: ImageDataLike,
|
|
39
|
+
rect: Rect,
|
|
40
|
+
): Uint8ClampedArray
|
|
41
|
+
|
|
42
|
+
export function extractPixelData(
|
|
43
|
+
imageData: ImageDataLike,
|
|
44
|
+
x: number,
|
|
45
|
+
y: number,
|
|
46
|
+
w: number,
|
|
47
|
+
h: number,
|
|
48
|
+
): Uint8ClampedArray
|
|
49
|
+
export function extractPixelData(
|
|
50
|
+
imageData: ImageDataLike,
|
|
51
|
+
_x: Rect | number,
|
|
52
|
+
_y?: number,
|
|
53
|
+
_w?: number,
|
|
54
|
+
_h?: number,
|
|
55
|
+
): Uint8ClampedArray {
|
|
56
|
+
const { x, y, w, h } = typeof _x === 'object'
|
|
57
|
+
? _x
|
|
58
|
+
: { x: _x, y: _y!, w: _w!, h: _h! }
|
|
59
|
+
|
|
60
|
+
const { width: srcW, height: srcH, data: src } = imageData
|
|
61
|
+
// Safety check for invalid dimensions
|
|
62
|
+
if (w <= 0 || h <= 0) return new Uint8ClampedArray(0)
|
|
63
|
+
const out = new Uint8ClampedArray(w * h * 4)
|
|
64
|
+
|
|
65
|
+
const x0 = Math.max(0, x)
|
|
66
|
+
const y0 = Math.max(0, y)
|
|
67
|
+
const x1 = Math.min(srcW, x + w)
|
|
68
|
+
const y1 = Math.min(srcH, y + h)
|
|
69
|
+
|
|
70
|
+
// If no intersection, return the empty
|
|
71
|
+
if (x1 <= x0 || y1 <= y0) return out
|
|
72
|
+
|
|
73
|
+
for (let row = 0; row < (y1 - y0); row++) {
|
|
74
|
+
// Where to read from the source canvas
|
|
75
|
+
const srcRow = y0 + row
|
|
76
|
+
const srcStart = (srcRow * srcW + x0) * 4
|
|
77
|
+
const rowLen = (x1 - x0) * 4
|
|
78
|
+
|
|
79
|
+
// Where to write into the 'out' patch
|
|
80
|
+
const dstRow = (y0 - y) + row
|
|
81
|
+
const dstCol = (x0 - x)
|
|
82
|
+
const dstStart = (dstRow * w + dstCol) * 4
|
|
83
|
+
|
|
84
|
+
// Perform the high-speed bulk copy
|
|
85
|
+
out.set(src.subarray(srcStart, srcStart + rowLen), dstStart)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return out
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function copyImageData({ data, width, height }: ImageDataLike): ImageData {
|
|
92
|
+
return new ImageData(data.slice(), width, height)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function copyImageDataLike({ data, width, height }: ImageDataLike): ImageDataLike {
|
|
96
|
+
return {
|
|
97
|
+
data: data.slice(),
|
|
98
|
+
width,
|
|
99
|
+
height,
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -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++) {
|
package/src/_types.ts
CHANGED
|
@@ -2,21 +2,43 @@
|
|
|
2
2
|
export type RGBA = { r: number, g: number, b: number, a: number }
|
|
3
3
|
|
|
4
4
|
// A 32-bit integer containing r,g,b,a data
|
|
5
|
-
export type Color32 = number & { readonly __brandColor32: unique symbol }
|
|
5
|
+
export type Color32 = number & { readonly __brandColor32: unique symbol }
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
export type RGBAFloat = RGBA & { readonly __brandRGBAFloat: unique symbol }
|
|
7
|
+
export type BlendColor32 = (src: Color32, dst: Color32) => Color32
|
|
9
8
|
|
|
10
9
|
export type ImageDataLike = {
|
|
11
10
|
width: number
|
|
12
11
|
height: number
|
|
13
|
-
data: Uint8ClampedArray
|
|
12
|
+
data: Uint8ClampedArray<ArrayBuffer>
|
|
14
13
|
}
|
|
15
14
|
|
|
16
15
|
export type SerializedImageData = {
|
|
17
|
-
width: number
|
|
18
|
-
height: number
|
|
19
|
-
data: string
|
|
16
|
+
width: number
|
|
17
|
+
height: number
|
|
18
|
+
data: string
|
|
20
19
|
}
|
|
21
20
|
|
|
22
21
|
export type Base64EncodedUInt8Array = string & { readonly __brandBase64UInt8Array: unique symbol }
|
|
22
|
+
|
|
23
|
+
export type Rect = { x: number; y: number; w: number; h: number }
|
|
24
|
+
|
|
25
|
+
export enum MaskType {
|
|
26
|
+
ALPHA,
|
|
27
|
+
BINARY
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface BaseMaskData {
|
|
31
|
+
readonly width: number
|
|
32
|
+
readonly height: number
|
|
33
|
+
readonly data: Uint8Array
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface AlphaMask extends BaseMaskData {
|
|
37
|
+
readonly type: MaskType.ALPHA
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface BinaryMask extends BaseMaskData {
|
|
41
|
+
readonly type: MaskType.BINARY
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type AnyMask = AlphaMask | BinaryMask
|