pixel-data-js 0.3.0 → 0.5.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 +1405 -70
- package/dist/index.dev.cjs.map +1 -1
- package/dist/index.dev.js +1355 -68
- package/dist/index.dev.js.map +1 -1
- package/dist/index.prod.cjs +1405 -70
- package/dist/index.prod.cjs.map +1 -1
- package/dist/index.prod.d.ts +581 -64
- package/dist/index.prod.js +1355 -68
- package/dist/index.prod.js.map +1 -1
- package/package.json +14 -3
- package/src/Algorithm/floodFillSelection.ts +229 -0
- package/src/Canvas/PixelCanvas.ts +31 -0
- package/src/Canvas/ReusableCanvas.ts +44 -0
- package/src/Canvas/_constants.ts +2 -0
- package/src/Clipboard/getImageDataFromClipboard.ts +42 -0
- package/src/Clipboard/writeImageDataToClipboard.ts +25 -0
- package/src/Clipboard/writeImgBlobToClipboard.ts +13 -0
- package/src/ImageData/{extractImageData.ts → extractImageDataPixels.ts} +21 -3
- package/src/ImageData/imageDataToAlphaMask.ts +35 -0
- package/src/ImageData/imageDataToDataUrl.ts +27 -0
- package/src/ImageData/imageDataToImgBlob.ts +31 -0
- package/src/ImageData/imgBlobToImageData.ts +52 -0
- package/src/ImageData/invertImageData.ts +10 -0
- package/src/ImageData/resizeImageData.ts +75 -0
- package/src/ImageData/{writeImageData.ts → writeImageDataPixels.ts} +22 -3
- package/src/Input/fileInputChangeToImageData.ts +37 -0
- package/src/Input/fileToImageData.ts +75 -0
- package/src/Input/getSupportedRasterFormats.ts +74 -0
- package/src/Mask/extractMask.ts +86 -0
- package/src/Mask/mergeMasks.ts +1 -6
- package/src/PixelData/blendColorPixelData.ts +9 -9
- package/src/PixelData/fillPixelData.ts +51 -12
- package/src/PixelData/invertPixelData.ts +16 -0
- package/src/PixelData/pixelDataToAlphaMask.ts +28 -0
- package/src/Rect/trimRectBounds.ts +118 -0
- package/src/_types.ts +37 -20
- package/src/blend-modes.ts +506 -66
- package/src/color.ts +6 -6
- package/src/globals.d.ts +2 -0
- package/src/index.ts +37 -1
|
@@ -1,11 +1,30 @@
|
|
|
1
1
|
import type { Rect } from '../_types'
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Copies a pixel buffer into a specific region of an {@link ImageData} object.
|
|
5
|
+
*
|
|
6
|
+
* This function performs a direct memory copy from a {@link Uint8ClampedArray}
|
|
7
|
+
* into the target {@link ImageData} buffer. It supports both {@link Rect}
|
|
8
|
+
* objects and discrete coordinates.
|
|
9
|
+
*
|
|
10
|
+
* @param imageData - The target {@link ImageData} to write into. Must match the rect width/height.
|
|
11
|
+
* @param data - The source pixel data (RGBA).
|
|
12
|
+
* @param rect - A {@link Rect} object defining the destination region.
|
|
13
|
+
*/
|
|
14
|
+
export function writeImageDataPixels(
|
|
4
15
|
imageData: ImageData,
|
|
5
16
|
data: Uint8ClampedArray,
|
|
6
17
|
rect: Rect,
|
|
7
18
|
): void
|
|
8
|
-
|
|
19
|
+
/**
|
|
20
|
+
* @param imageData - The target {@link ImageData} to write into.
|
|
21
|
+
* @param data - The source pixel data (RGBA). Must match the width/height.
|
|
22
|
+
* @param x - The starting horizontal coordinate in the target.
|
|
23
|
+
* @param y - The starting vertical coordinate in the target.
|
|
24
|
+
* @param w - The width of the region to write.
|
|
25
|
+
* @param h - The height of the region to write.
|
|
26
|
+
*/
|
|
27
|
+
export function writeImageDataPixels(
|
|
9
28
|
imageData: ImageData,
|
|
10
29
|
data: Uint8ClampedArray,
|
|
11
30
|
x: number,
|
|
@@ -13,7 +32,7 @@ export function writeImageData(
|
|
|
13
32
|
w: number,
|
|
14
33
|
h: number,
|
|
15
34
|
): void
|
|
16
|
-
export function
|
|
35
|
+
export function writeImageDataPixels(
|
|
17
36
|
imageData: ImageData,
|
|
18
37
|
data: Uint8ClampedArray,
|
|
19
38
|
_x: Rect | number,
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { fileToImageData } from '../../src'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A convenience wrapper that extracts the first {@link File} from an
|
|
5
|
+
* {@link HTMLInputElement} change event and converts it into {@link ImageData}.
|
|
6
|
+
*
|
|
7
|
+
* This function handles the boilerplate of accessing the file list and checking
|
|
8
|
+
* for existence. It is ideal for use directly in an `onchange` event listener.
|
|
9
|
+
*
|
|
10
|
+
* @param event - The change {@link Event} from an `<input type="file">` element.
|
|
11
|
+
*
|
|
12
|
+
* @returns A promise that resolves to {@link ImageData} if a file was successfully
|
|
13
|
+
* processed, or `null` if no file was selected or the input was cleared.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* const input = document.querySelector('input[type="file"]');
|
|
18
|
+
*
|
|
19
|
+
* input.addEventListener('change', async (event) => {
|
|
20
|
+
* const imageData = await fileInputChangeToImageData(event);
|
|
21
|
+
*
|
|
22
|
+
* if (imageData) {
|
|
23
|
+
* console.log('Image loaded:', imageData.width, imageData.height);
|
|
24
|
+
* }
|
|
25
|
+
* });
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export async function fileInputChangeToImageData(
|
|
29
|
+
event: Event,
|
|
30
|
+
): Promise<ImageData | null> {
|
|
31
|
+
const target = event.target as HTMLInputElement
|
|
32
|
+
|
|
33
|
+
const file = target.files?.[0]
|
|
34
|
+
if (!file) return null
|
|
35
|
+
|
|
36
|
+
return await fileToImageData(file)
|
|
37
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { OFFSCREEN_CANVAS_CTX_FAILED } from '../Canvas/_constants'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Thrown when the user provides a file that isn't an image.
|
|
5
|
+
*/
|
|
6
|
+
export class UnsupportedFormatError extends Error {
|
|
7
|
+
constructor(mimeType: string) {
|
|
8
|
+
super(`File type ${mimeType} is not a supported image format.`)
|
|
9
|
+
this.name = 'UnsupportedFormatError'
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Converts a browser {@link File} object into {@link ImageData}.
|
|
15
|
+
* This utility handles the full pipeline of image decoding using hardware-accelerated
|
|
16
|
+
* APIs {@link createImageBitmap} and {@link OffscreenCanvas}. It ensures that underlying
|
|
17
|
+
* resources like `ImageBitmap` are properly closed even if the conversion fails.
|
|
18
|
+
*
|
|
19
|
+
* @param file - The image file to convert. Can be null or undefined.
|
|
20
|
+
* @returns A `Promise` resolving to the pixel data as {@link ImageData},
|
|
21
|
+
* or `null` if no file was provided.
|
|
22
|
+
* @throws {@link UnsupportedFormatError}
|
|
23
|
+
* Thrown if the provided file's MIME type does not start with `image/`.
|
|
24
|
+
* @example
|
|
25
|
+
* ```typescript
|
|
26
|
+
* try {
|
|
27
|
+
* const imageData = await fileToImageData(file);
|
|
28
|
+
* if (imageData) {
|
|
29
|
+
* console.log('Pixels:', imageData.data);
|
|
30
|
+
* }
|
|
31
|
+
* } catch (err) {
|
|
32
|
+
* if (err instanceof UnsupportedFormatError) {
|
|
33
|
+
* // Handle bad file type
|
|
34
|
+
* }
|
|
35
|
+
* }
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export async function fileToImageData(
|
|
39
|
+
file: File | null | undefined,
|
|
40
|
+
): Promise<ImageData | null> {
|
|
41
|
+
if (!file) return null
|
|
42
|
+
|
|
43
|
+
if (!file.type.startsWith('image/')) {
|
|
44
|
+
throw new UnsupportedFormatError(file.type)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let bitmap: ImageBitmap | null = null
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
bitmap = await createImageBitmap(file)
|
|
51
|
+
|
|
52
|
+
const canvas = new OffscreenCanvas(
|
|
53
|
+
bitmap.width,
|
|
54
|
+
bitmap.height,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
const ctx = canvas.getContext('2d')
|
|
58
|
+
if (!ctx) throw new Error(OFFSCREEN_CANVAS_CTX_FAILED)
|
|
59
|
+
|
|
60
|
+
ctx.drawImage(
|
|
61
|
+
bitmap,
|
|
62
|
+
0,
|
|
63
|
+
0,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
return ctx.getImageData(
|
|
67
|
+
0,
|
|
68
|
+
0,
|
|
69
|
+
bitmap.width,
|
|
70
|
+
bitmap.height,
|
|
71
|
+
)
|
|
72
|
+
} finally {
|
|
73
|
+
bitmap?.close()
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Cache the Promise to prevent race conditions during initialization
|
|
2
|
+
let formatsPromise: Promise<string[]> | null = null
|
|
3
|
+
|
|
4
|
+
const defaultRasterMimes = [
|
|
5
|
+
'image/png',
|
|
6
|
+
'image/jpeg',
|
|
7
|
+
'image/webp',
|
|
8
|
+
'image/avif',
|
|
9
|
+
'image/gif',
|
|
10
|
+
'image/bmp',
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Probes the browser environment to determine which image MIME types are
|
|
15
|
+
* supported for pixel-level operations.
|
|
16
|
+
* This function performs a one-time check by attempting to convert a
|
|
17
|
+
* {@link OffscreenCanvas} to MIME types. The result is
|
|
18
|
+
* cached to prevent redundant hardware-accelerated operations on
|
|
19
|
+
* subsequent calls.
|
|
20
|
+
* @param rasterMimes List of MIME types to check
|
|
21
|
+
* @default ['image/png',
|
|
22
|
+
* 'image/jpeg',
|
|
23
|
+
* 'image/webp',
|
|
24
|
+
* 'image/avif',
|
|
25
|
+
* 'image/gif',
|
|
26
|
+
* 'image/bmp']
|
|
27
|
+
* @returns A `Promise` resolving to an array of supported MIME
|
|
28
|
+
* types from the `rasterMimes` list.
|
|
29
|
+
* @throws {Error} If the {@link OffscreenCanvas} context cannot be initialized.
|
|
30
|
+
* @example
|
|
31
|
+
* ```typescript
|
|
32
|
+
* const supported = await getSupportedPixelFormats();
|
|
33
|
+
* if (supported.includes('image/avif')) {
|
|
34
|
+
* console.log('High-efficiency formats available');
|
|
35
|
+
* }
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export async function getSupportedPixelFormats(rasterMimes = defaultRasterMimes): Promise<string[]> {
|
|
39
|
+
if (formatsPromise) {
|
|
40
|
+
return formatsPromise
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const probeCanvas = async () => {
|
|
44
|
+
const canvas = new OffscreenCanvas(1, 1)
|
|
45
|
+
|
|
46
|
+
const results = await Promise.all(
|
|
47
|
+
rasterMimes.map(async (mime) => {
|
|
48
|
+
try {
|
|
49
|
+
const blob = await canvas.convertToBlob({
|
|
50
|
+
type: mime,
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
return blob.type === mime ? mime : null
|
|
54
|
+
} catch {
|
|
55
|
+
return null
|
|
56
|
+
}
|
|
57
|
+
}),
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
return results.filter((type): type is string => {
|
|
61
|
+
return type !== null
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// By chaining .catch here, the microtask guarantees formatsPromise
|
|
66
|
+
// is assigned the promise BEFORE the catch block runs to reset it.
|
|
67
|
+
formatsPromise = probeCanvas().catch((error) => {
|
|
68
|
+
formatsPromise = null
|
|
69
|
+
|
|
70
|
+
throw error
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
return formatsPromise
|
|
74
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { Rect } from '../_types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Extracts a rectangular region from a 1D {@link Uint8Array} mask.
|
|
5
|
+
* This utility calculates the necessary offsets based on the `maskWidth` to
|
|
6
|
+
* slice out a specific area.
|
|
7
|
+
*
|
|
8
|
+
* @param mask - The source 1D array representing the full 2D mask.
|
|
9
|
+
* @param maskWidth - The width of the original source mask (stride).
|
|
10
|
+
* @param rect - A {@link Rect} object defining the region to extract.
|
|
11
|
+
* @returns A new {@link Uint8Array} containing the extracted region.
|
|
12
|
+
*/
|
|
13
|
+
export function extractMask(
|
|
14
|
+
mask: Uint8Array,
|
|
15
|
+
maskWidth: number,
|
|
16
|
+
rect: Rect,
|
|
17
|
+
): Uint8Array
|
|
18
|
+
/**
|
|
19
|
+
* @param mask - The source 1D array representing the full 2D mask.
|
|
20
|
+
* @param maskWidth - The width of the original source mask (stride).
|
|
21
|
+
* @param x - The starting horizontal coordinate.
|
|
22
|
+
* @param y - The starting vertical coordinate.
|
|
23
|
+
* @param w - The width of the region to extract.
|
|
24
|
+
* @param h - The height of the region to extract.
|
|
25
|
+
* @returns A new {@link Uint8Array} containing the extracted region.
|
|
26
|
+
*/
|
|
27
|
+
export function extractMask(
|
|
28
|
+
mask: Uint8Array,
|
|
29
|
+
maskWidth: number,
|
|
30
|
+
x: number,
|
|
31
|
+
y: number,
|
|
32
|
+
w: number,
|
|
33
|
+
h: number,
|
|
34
|
+
): Uint8Array
|
|
35
|
+
export function extractMask(
|
|
36
|
+
mask: Uint8Array,
|
|
37
|
+
maskWidth: number,
|
|
38
|
+
xOrRect: number | Rect,
|
|
39
|
+
y?: number,
|
|
40
|
+
w?: number,
|
|
41
|
+
h?: number,
|
|
42
|
+
): Uint8Array {
|
|
43
|
+
let finalX: number
|
|
44
|
+
let finalY: number
|
|
45
|
+
let finalW: number
|
|
46
|
+
let finalH: number
|
|
47
|
+
|
|
48
|
+
if (typeof xOrRect === 'object') {
|
|
49
|
+
finalX = xOrRect.x
|
|
50
|
+
finalY = xOrRect.y
|
|
51
|
+
finalW = xOrRect.w
|
|
52
|
+
finalH = xOrRect.h
|
|
53
|
+
} else {
|
|
54
|
+
finalX = xOrRect
|
|
55
|
+
finalY = y!
|
|
56
|
+
finalW = w!
|
|
57
|
+
finalH = h!
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const out = new Uint8Array(finalW * finalH)
|
|
61
|
+
const srcH = mask.length / maskWidth
|
|
62
|
+
|
|
63
|
+
for (let row = 0; row < finalH; row++) {
|
|
64
|
+
const currentSrcY = finalY + row
|
|
65
|
+
|
|
66
|
+
if (currentSrcY < 0 || currentSrcY >= srcH) {
|
|
67
|
+
continue
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const start = Math.max(0, finalX)
|
|
71
|
+
const end = Math.min(maskWidth, finalX + finalW)
|
|
72
|
+
|
|
73
|
+
if (start < end) {
|
|
74
|
+
const srcOffset = currentSrcY * maskWidth + start
|
|
75
|
+
const dstOffset = (row * finalW) + (start - finalX)
|
|
76
|
+
const count = end - start
|
|
77
|
+
|
|
78
|
+
out.set(
|
|
79
|
+
mask.subarray(srcOffset, srcOffset + count),
|
|
80
|
+
dstOffset,
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return out
|
|
86
|
+
}
|
package/src/Mask/mergeMasks.ts
CHANGED
|
@@ -82,16 +82,16 @@ export function blendColorPixelData(
|
|
|
82
82
|
const mVal = mask[mIdx]
|
|
83
83
|
|
|
84
84
|
if (isAlphaMask) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
85
|
+
const effectiveM = invertMask
|
|
86
|
+
? 255 - mVal
|
|
87
|
+
: mVal
|
|
88
88
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
89
|
+
// If mask is transparent, skip
|
|
90
|
+
if (effectiveM === 0) {
|
|
91
|
+
dIdx++
|
|
92
|
+
mIdx++
|
|
93
|
+
continue
|
|
94
|
+
}
|
|
95
95
|
|
|
96
96
|
// globalAlpha is not a factor
|
|
97
97
|
if (globalAlpha === 255) {
|
|
@@ -2,24 +2,63 @@ import type { Color32, Rect } from '../_types'
|
|
|
2
2
|
import type { PixelData } from '../PixelData'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* Fills a region or the {@link PixelData} buffer with a solid color.
|
|
6
|
+
*
|
|
7
|
+
* @param dst - The target {@link PixelData} to modify.
|
|
8
|
+
* @param color - The {@link Color32} value to apply.
|
|
9
|
+
* @param rect - A {@link Rect} defining the area to fill. If omitted, the entire
|
|
10
|
+
* buffer is filled.
|
|
6
11
|
*/
|
|
7
12
|
export function fillPixelData(
|
|
8
13
|
dst: PixelData,
|
|
9
14
|
color: Color32,
|
|
10
15
|
rect?: Partial<Rect>,
|
|
16
|
+
): void
|
|
17
|
+
/**
|
|
18
|
+
* @param dst - The target {@link PixelData} to modify.
|
|
19
|
+
* @param color - The {@link Color32} value to apply.
|
|
20
|
+
* @param x - Starting horizontal coordinate.
|
|
21
|
+
* @param y - Starting vertical coordinate.
|
|
22
|
+
* @param w - Width of the fill area.
|
|
23
|
+
* @param h - Height of the fill area.
|
|
24
|
+
*/
|
|
25
|
+
export function fillPixelData(
|
|
26
|
+
dst: PixelData,
|
|
27
|
+
color: Color32,
|
|
28
|
+
x: number,
|
|
29
|
+
y: number,
|
|
30
|
+
w: number,
|
|
31
|
+
h: number,
|
|
32
|
+
): void
|
|
33
|
+
export function fillPixelData(
|
|
34
|
+
dst: PixelData,
|
|
35
|
+
color: Color32,
|
|
36
|
+
_x?: Partial<Rect> | number,
|
|
37
|
+
_y?: number,
|
|
38
|
+
_w?: number,
|
|
39
|
+
_h?: number,
|
|
11
40
|
): void {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
41
|
+
let x: number
|
|
42
|
+
let y: number
|
|
43
|
+
let w: number
|
|
44
|
+
let h: number
|
|
45
|
+
|
|
46
|
+
if (typeof _x === 'object') {
|
|
47
|
+
x = _x.x ?? 0
|
|
48
|
+
y = _x.y ?? 0
|
|
49
|
+
w = _x.w ?? dst.width
|
|
50
|
+
h = _x.h ?? dst.height
|
|
51
|
+
} else if (typeof _x === 'number') {
|
|
52
|
+
x = _x
|
|
53
|
+
y = _y!
|
|
54
|
+
w = _w!
|
|
55
|
+
h = _h!
|
|
56
|
+
} else {
|
|
57
|
+
x = 0
|
|
58
|
+
y = 0
|
|
59
|
+
w = dst.width
|
|
60
|
+
h = dst.height
|
|
61
|
+
}
|
|
23
62
|
|
|
24
63
|
// Destination Clipping
|
|
25
64
|
if (x < 0) {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { PixelData } from '../PixelData'
|
|
2
|
+
|
|
3
|
+
export function invertPixelData(
|
|
4
|
+
pixelData: PixelData,
|
|
5
|
+
): PixelData {
|
|
6
|
+
|
|
7
|
+
const data32 = pixelData.data32
|
|
8
|
+
const len = data32.length
|
|
9
|
+
|
|
10
|
+
for (let i = 0; i < len; i++) {
|
|
11
|
+
// XOR with 0x00FFFFFF flips RGB bits and ignores Alpha (the top 8 bits)
|
|
12
|
+
data32[i] = data32[i] ^ 0x00FFFFFF
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return pixelData
|
|
16
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { AlphaMask } from '../_types'
|
|
2
|
+
import type { PixelData } from '../PixelData'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Extracts the alpha channel from PixelData into a single-channel mask.
|
|
6
|
+
* Returns a Uint8Array branded as AlphaMask.
|
|
7
|
+
*/
|
|
8
|
+
export function pixelDataToAlphaMask(
|
|
9
|
+
pixelData: PixelData,
|
|
10
|
+
): AlphaMask {
|
|
11
|
+
const {
|
|
12
|
+
data32,
|
|
13
|
+
width,
|
|
14
|
+
height,
|
|
15
|
+
} = pixelData
|
|
16
|
+
const len = data32.length
|
|
17
|
+
const mask = new Uint8Array(width * height) as AlphaMask
|
|
18
|
+
|
|
19
|
+
for (let i = 0; i < len; i++) {
|
|
20
|
+
const val = data32[i]
|
|
21
|
+
|
|
22
|
+
// Extract the Alpha byte (top 8 bits in ABGR / Little-Endian)
|
|
23
|
+
// Shift right by 24 moves the 4th byte to the 1st position
|
|
24
|
+
mask[i] = (val >>> 24) & 0xff
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return mask
|
|
28
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { Rect, SelectionRect } from '../_types'
|
|
2
|
+
import { extractMask } from '../Mask/extractMask'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Intersects a target rectangle with a boundary, trimming dimensions and masks in-place.
|
|
6
|
+
* This utility calculates the axis-aligned intersection between the `target` and `bounds`.
|
|
7
|
+
* If the `target` includes a `mask` (as in a {@link SelectionRect}), the mask is physically
|
|
8
|
+
* cropped and re-aligned using `extractMask` to match the new dimensions.
|
|
9
|
+
* @param target - The rectangle or selection object to be trimmed. **Note:** This object is mutated in-place.
|
|
10
|
+
* @param bounds - The boundary rectangle defining the maximum allowable area (e.g., canvas dimensions).
|
|
11
|
+
* @example
|
|
12
|
+
* const selection = { x: -10, y: -10, w: 50, h: 50, mask: new Uint8Array(2500) };
|
|
13
|
+
* const canvas = { x: 0, y: 0, w: 100, h: 100 };
|
|
14
|
+
* // Selection will be moved to (0,0) and resized to 40x40.
|
|
15
|
+
* // The mask is cropped by 10 px on the top and left.
|
|
16
|
+
* trimRectBounds(selection, canvas);
|
|
17
|
+
*/
|
|
18
|
+
export function trimRectBounds<T extends Rect | SelectionRect>(
|
|
19
|
+
target: T,
|
|
20
|
+
bounds: Rect,
|
|
21
|
+
): void {
|
|
22
|
+
const originalX = target.x
|
|
23
|
+
const originalY = target.y
|
|
24
|
+
const originalW = target.w
|
|
25
|
+
|
|
26
|
+
const intersectedX = Math.max(target.x, bounds.x)
|
|
27
|
+
const intersectedY = Math.max(target.y, bounds.y)
|
|
28
|
+
|
|
29
|
+
const intersectedMaxX = Math.min(
|
|
30
|
+
target.x + target.w,
|
|
31
|
+
bounds.x + bounds.w,
|
|
32
|
+
)
|
|
33
|
+
const intersectedMaxY = Math.min(
|
|
34
|
+
target.y + target.h,
|
|
35
|
+
bounds.y + bounds.h,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
// Intersection check
|
|
39
|
+
if (intersectedMaxX <= intersectedX || intersectedMaxY <= intersectedY) {
|
|
40
|
+
target.w = 0
|
|
41
|
+
target.h = 0
|
|
42
|
+
|
|
43
|
+
if ('mask' in target && target.mask) {
|
|
44
|
+
// This line is now hit by the 'empty intersection' test below
|
|
45
|
+
target.mask = new Uint8Array(0)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const intersectedW = intersectedMaxX - intersectedX
|
|
52
|
+
const intersectedH = intersectedMaxY - intersectedY
|
|
53
|
+
const offsetX = intersectedX - originalX
|
|
54
|
+
const offsetY = intersectedY - originalY
|
|
55
|
+
|
|
56
|
+
target.x = intersectedX
|
|
57
|
+
target.y = intersectedY
|
|
58
|
+
target.w = intersectedW
|
|
59
|
+
target.h = intersectedH
|
|
60
|
+
|
|
61
|
+
if ('mask' in target && target.mask) {
|
|
62
|
+
const currentMask = extractMask(
|
|
63
|
+
target.mask,
|
|
64
|
+
originalW,
|
|
65
|
+
offsetX,
|
|
66
|
+
offsetY,
|
|
67
|
+
intersectedW,
|
|
68
|
+
intersectedH,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
let minX = intersectedW
|
|
72
|
+
let maxX = -1
|
|
73
|
+
let minY = intersectedH
|
|
74
|
+
let maxY = -1
|
|
75
|
+
|
|
76
|
+
// Scan for content
|
|
77
|
+
for (let y = 0; y < intersectedH; y++) {
|
|
78
|
+
for (let x = 0; x < intersectedW; x++) {
|
|
79
|
+
if (currentMask[y * intersectedW + x] !== 0) {
|
|
80
|
+
if (x < minX) minX = x
|
|
81
|
+
if (x > maxX) maxX = x
|
|
82
|
+
if (y < minY) minY = y
|
|
83
|
+
if (y > maxY) maxY = y
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// If no content is found (all zeros)
|
|
89
|
+
if (maxX === -1) {
|
|
90
|
+
target.w = 0
|
|
91
|
+
target.h = 0
|
|
92
|
+
// This covers the specific line you mentioned
|
|
93
|
+
target.mask = new Uint8Array(0)
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const finalW = maxX - minX + 1
|
|
98
|
+
const finalH = maxY - minY + 1
|
|
99
|
+
|
|
100
|
+
// Only shift and crop if the content is smaller than the intersection
|
|
101
|
+
if (finalW !== intersectedW || finalH !== intersectedH) {
|
|
102
|
+
target.mask = extractMask(
|
|
103
|
+
currentMask,
|
|
104
|
+
intersectedW,
|
|
105
|
+
minX,
|
|
106
|
+
minY,
|
|
107
|
+
finalW,
|
|
108
|
+
finalH,
|
|
109
|
+
)
|
|
110
|
+
target.x += minX
|
|
111
|
+
target.y += minY
|
|
112
|
+
target.w = finalW
|
|
113
|
+
target.h = finalH
|
|
114
|
+
} else {
|
|
115
|
+
target.mask = currentMask
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|