pixel-data-js 0.30.0 → 0.32.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.
@@ -1,15 +1,21 @@
1
1
  import type { Rect } from '../Rect/_rect-types'
2
- import { makeClippedBlit, resolveBlitClipping } from '../Rect/resolveClipping'
3
2
  import type { PixelData32 } from './_pixelData-types'
4
3
 
5
- const SCRATCH_BLIT = makeClippedBlit()
6
-
7
4
  /**
8
5
  * Extracts a rectangular region of pixels from PixelData.
9
6
  * Returns a new Uint32Array containing the extracted pixels.
10
7
  */
11
- export function extractPixelDataBuffer(source: PixelData32, rect: Rect): Uint32Array
12
- export function extractPixelDataBuffer(source: PixelData32, x: number, y: number, w: number, h: number): Uint32Array
8
+ export function extractPixelDataBuffer(
9
+ source: PixelData32,
10
+ rect: Rect,
11
+ ): Uint32Array
12
+ export function extractPixelDataBuffer(
13
+ source: PixelData32,
14
+ x: number,
15
+ y: number,
16
+ w: number,
17
+ h: number,
18
+ ): Uint32Array
13
19
  export function extractPixelDataBuffer(
14
20
  source: PixelData32,
15
21
  _x: Rect | number,
@@ -17,56 +23,63 @@ export function extractPixelDataBuffer(
17
23
  _w?: number,
18
24
  _h?: number,
19
25
  ): Uint32Array {
20
- const { x, y, w, h } = typeof _x === 'object'
21
- ? _x
22
- : { x: _x, y: _y!, w: _w!, h: _h! }
26
+ let x: number
27
+ let y: number
28
+ let w: number
29
+ let h: number
30
+
31
+ if (typeof _x === 'object') {
32
+ x = _x.x
33
+ y = _x.y
34
+ w = _x.w
35
+ h = _x.h
36
+ } else {
37
+ x = _x
38
+ y = _y!
39
+ w = _w!
40
+ h = _h!
41
+ }
23
42
 
24
43
  const srcW = source.w
25
44
  const srcH = source.h
26
45
  const srcData = source.data
27
46
 
28
- // Safety check for empty or invalid dimensions
29
- if (w <= 0 || h <= 0) {
30
- return new Uint32Array(0)
31
- }
47
+ if (w <= 0) return new Uint32Array(0)
48
+ if (h <= 0) return new Uint32Array(0)
32
49
 
33
50
  const dstData = new Uint32Array(w * h)
34
51
 
35
- // We map from Source (srcW, srcH) at (x,y)
36
- // To Dest (w, h) at (0,0)
37
- // Note: resolveBlitClipping usually takes (dstX, dstY, srcX, srcY...)
38
- // Here we are "blitting" FROM x,y TO 0,0.
39
- const clip = resolveBlitClipping(
40
- 0,
41
- 0,
42
- x,
43
- y,
44
- w,
45
- h,
46
- w,
47
- h,
48
- srcW,
49
- srcH,
50
- SCRATCH_BLIT,
51
- )
52
+ // Inline clipping logic to avoid object allocations
53
+ let srcX = x
54
+ let srcY = y
55
+ let dstX = 0
56
+ let dstY = 0
57
+ let copyW = w
58
+ let copyH = h
59
+
60
+ if (srcX < 0) {
61
+ dstX = -srcX
62
+ copyW += srcX
63
+ srcX = 0
64
+ }
52
65
 
53
- if (!clip.inBounds) return dstData
66
+ if (srcY < 0) {
67
+ dstY = -srcY
68
+ copyH += srcY
69
+ srcY = 0
70
+ }
54
71
 
55
- const {
56
- x: dstX,
57
- y: dstY,
58
- sx: srcX,
59
- sy: srcY,
60
- w: copyW,
61
- h: copyH,
62
- } = clip
72
+ copyW = Math.min(copyW, srcW - srcX)
73
+ copyH = Math.min(copyH, srcH - srcY)
74
+
75
+ if (copyW <= 0) return dstData
76
+ if (copyH <= 0) return dstData
63
77
 
64
78
  for (let row = 0; row < copyH; row++) {
65
79
  const srcStart = (srcY + row) * srcW + srcX
66
80
  const dstStart = (dstY + row) * w + dstX
67
-
68
- // Perform the high-speed 32-bit bulk copy
69
81
  const chunk = srcData.subarray(srcStart, srcStart + copyW)
82
+
70
83
  dstData.set(chunk, dstStart)
71
84
  }
72
85
 
@@ -1,10 +1,7 @@
1
1
  import type { Color32 } from '../_types'
2
2
  import type { Rect } from '../Rect/_rect-types'
3
- import { makeClippedRect, resolveRectClipping } from '../Rect/resolveClipping'
4
3
  import type { PixelData32 } from './_pixelData-types'
5
4
 
6
- const SCRATCH_RECT = makeClippedRect()
7
-
8
5
  /**
9
6
  * Fills a region or the {@link PixelData32} buffer with a solid color.
10
7
  *
@@ -42,6 +39,9 @@ export function fillPixelData(
42
39
  _w?: number,
43
40
  _h?: number,
44
41
  ): boolean {
42
+ const dstW = dst.w
43
+ const dstH = dst.h
44
+
45
45
  let x: number
46
46
  let y: number
47
47
  let w: number
@@ -50,8 +50,8 @@ export function fillPixelData(
50
50
  if (typeof _x === 'object') {
51
51
  x = _x.x ?? 0
52
52
  y = _x.y ?? 0
53
- w = _x.w ?? dst.w
54
- h = _x.h ?? dst.h
53
+ w = _x.w ?? dstW
54
+ h = _x.h ?? dstH
55
55
  } else if (typeof _x === 'number') {
56
56
  x = _x
57
57
  y = _y!
@@ -60,37 +60,55 @@ export function fillPixelData(
60
60
  } else {
61
61
  x = 0
62
62
  y = 0
63
- w = dst.w
64
- h = dst.h
63
+ w = dstW
64
+ h = dstH
65
+ }
66
+
67
+ // Inline bounds clipping
68
+ let dstX = x
69
+ let dstY = y
70
+ let fillW = w
71
+ let fillH = h
72
+
73
+ if (dstX < 0) {
74
+ fillW += dstX
75
+ dstX = 0
65
76
  }
66
77
 
67
- const clip = resolveRectClipping(
68
- x,
69
- y,
70
- w,
71
- h,
72
- dst.w,
73
- dst.h,
74
- SCRATCH_RECT,
75
- )
78
+ if (dstY < 0) {
79
+ fillH += dstY
80
+ dstY = 0
81
+ }
76
82
 
77
- if (!clip.inBounds) return false
83
+ fillW = Math.min(fillW, dstW - dstX)
84
+ fillH = Math.min(fillH, dstH - dstY)
78
85
 
79
- const {
80
- x: finalX,
81
- y: finalY,
82
- w: actualW,
83
- h: actualH,
84
- } = clip
86
+ if (fillW <= 0) return false
87
+ if (fillH <= 0) return false
85
88
 
86
89
  const dst32 = dst.data
87
- const dw = dst.w
88
90
  let hasChanged = false
89
91
 
90
- for (let iy = 0; iy < actualH; iy++) {
91
- const rowOffset = (finalY + iy) * dw
92
- const start = rowOffset + finalX
93
- const end = start + actualW
92
+ // Fast-path: If the area spans the full width, we can treat it as a contiguous 1D array
93
+ if (dstX === 0 && fillW === dstW) {
94
+ const start = dstY * dstW
95
+ const end = start + fillW * fillH
96
+
97
+ for (let i = start; i < end; i++) {
98
+ if (dst32[i] !== color) {
99
+ dst32[i] = color
100
+ hasChanged = true
101
+ }
102
+ }
103
+
104
+ return hasChanged
105
+ }
106
+
107
+ // Standard path: row-by-row
108
+ for (let iy = 0; iy < fillH; iy++) {
109
+ const rowOffset = (dstY + iy) * dstW
110
+ const start = rowOffset + dstX
111
+ const end = start + fillW
94
112
 
95
113
  for (let i = start; i < end; i++) {
96
114
  if (dst32[i] !== color) {
@@ -1,17 +1,16 @@
1
1
  import type { Color32 } from '../_types'
2
2
  import type { BinaryMask } from '../Mask/_mask-types'
3
- import { makeClippedRect, resolveRectClipping } from '../Rect/resolveClipping'
4
3
  import type { PixelData32 } from './_pixelData-types'
5
4
 
6
- const SCRATCH_RECT = makeClippedRect()
7
-
8
5
  /**
9
- * Fills a region of the {@link PixelData32} buffer with a solid color using a mask.
6
+ * Fills the target PixelData with a color based on a binary mask.
7
+ *
10
8
  * @param target - The target to modify.
11
9
  * @param color - The color to apply.
12
- * @param mask - The mask defining the area to fill.
13
- * @param x - Starting horizontal coordinate for the mask placement.
14
- * @param y - Starting vertical coordinate for the mask placement.
10
+ * @param mask - The binary mask determining where to fill.
11
+ * @param x - Horizontal offset to place the mask.
12
+ * @param y - Vertical offset to place the mask.
13
+ * @returns true if any pixels were actually modified.
15
14
  */
16
15
  export function fillPixelDataBinaryMask(
17
16
  target: PixelData32,
@@ -20,55 +19,61 @@ export function fillPixelDataBinaryMask(
20
19
  x = 0,
21
20
  y = 0,
22
21
  ): boolean {
23
-
22
+ const targetW = target.w
23
+ const targetH = target.h
24
24
  const maskW = mask.w
25
25
  const maskH = mask.h
26
26
 
27
- const clip = resolveRectClipping(
28
- x,
29
- y,
30
- maskW,
31
- maskH,
32
- target.w,
33
- target.h,
34
- SCRATCH_RECT,
35
- )
27
+ // Inline clipping logic
28
+ let dstX = x
29
+ let dstY = y
30
+ let actualW = maskW
31
+ let actualH = maskH
32
+
33
+ if (dstX < 0) {
34
+ actualW += dstX
35
+ dstX = 0
36
+ }
37
+
38
+ if (dstY < 0) {
39
+ actualH += dstY
40
+ dstY = 0
41
+ }
36
42
 
37
- if (!clip.inBounds) return false
43
+ actualW = Math.min(actualW, targetW - dstX)
44
+ actualH = Math.min(actualH, targetH - dstY)
38
45
 
39
- const {
40
- x: finalX,
41
- y: finalY,
42
- w: actualW,
43
- h: actualH,
44
- } = clip
46
+ if (actualW <= 0 || actualH <= 0) return false
45
47
 
46
48
  const maskData = mask.data
47
49
  const dst32 = target.data
48
- const dw = target.w
50
+
51
+ // Calculate offsets for the mask based on clipping
52
+ const mx = dstX - x
53
+ const my = dstY - y
49
54
 
50
55
  let hasChanged = false
51
56
 
52
- for (let iy = 0; iy < actualH; iy++) {
53
- const currentY = finalY + iy
54
- const maskY = currentY - y
55
- const maskOffset = maskY * maskW
57
+ // Stride-based loop for performance
58
+ let dIdx = dstY * targetW + dstX
59
+ let mIdx = my * maskW + mx
56
60
 
57
- const dstRowOffset = currentY * dw
61
+ const dStride = targetW - actualW
62
+ const mStride = maskW - actualW
58
63
 
64
+ for (let iy = 0; iy < actualH; iy++) {
59
65
  for (let ix = 0; ix < actualW; ix++) {
60
- const currentX = finalX + ix
61
- const maskX = currentX - x
62
- const maskIndex = maskOffset + maskX
63
-
64
- if (maskData[maskIndex]) {
65
- const current = dst32[dstRowOffset + currentX]
66
- if (current !== color) {
67
- dst32[dstRowOffset + currentX] = color
66
+ if (maskData[mIdx]) {
67
+ if (dst32[dIdx] !== color) {
68
+ dst32[dIdx] = color
68
69
  hasChanged = true
69
70
  }
70
71
  }
72
+ dIdx++
73
+ mIdx++
71
74
  }
75
+ dIdx += dStride
76
+ mIdx += mStride
72
77
  }
73
78
 
74
79
  return hasChanged
@@ -1,10 +1,7 @@
1
1
  import type { Color32 } from '../_types'
2
2
  import type { Rect } from '../Rect/_rect-types'
3
- import { makeClippedRect, resolveRectClipping } from '../Rect/resolveClipping'
4
3
  import type { PixelData32 } from './_pixelData-types'
5
4
 
6
- const SCRATCH_RECT = makeClippedRect()
7
-
8
5
  /**
9
6
  * Fills a region or the {@link PixelData32} buffer with a solid color.
10
7
  * This function is faster than {@link fillPixelData} but does not
@@ -44,6 +41,9 @@ export function fillPixelDataFast(
44
41
  _w?: number,
45
42
  _h?: number,
46
43
  ): void {
44
+ const dstW = dst.w
45
+ const dstH = dst.h
46
+
47
47
  let x: number
48
48
  let y: number
49
49
  let w: number
@@ -66,31 +66,41 @@ export function fillPixelDataFast(
66
66
  h = dst.h
67
67
  }
68
68
 
69
- const clip = resolveRectClipping(x, y, w, h, dst.w, dst.h, SCRATCH_RECT)
69
+ // Inline bounds clipping
70
+ let dstX = x
71
+ let dstY = y
72
+ let fillW = w
73
+ let fillH = h
74
+
75
+ if (dstX < 0) {
76
+ fillW += dstX
77
+ dstX = 0
78
+ }
79
+
80
+ if (dstY < 0) {
81
+ fillH += dstY
82
+ dstY = 0
83
+ }
70
84
 
71
- if (!clip.inBounds) return
85
+ fillW = Math.min(fillW, dstW - dstX)
86
+ fillH = Math.min(fillH, dstH - dstY)
72
87
 
73
- // Use the clipped values
74
- const {
75
- x: finalX,
76
- y: finalY,
77
- w: actualW,
78
- h: actualH,
79
- } = clip
88
+ if (fillW <= 0) return
89
+ if (fillH <= 0) return
80
90
 
81
91
  const dst32 = dst.data
82
92
  const dw = dst.w
83
93
 
84
94
  // Optimization: If filling the entire buffer, use the native .fill()
85
- if (actualW === dw && actualH === dst.h && finalX === 0 && finalY === 0) {
95
+ if (fillW === dw && fillH === dst.h && dstX === 0 && dstY === 0) {
86
96
  dst32.fill(color)
87
97
  return
88
98
  }
89
99
 
90
100
  // Row-by-row fill for partial rectangles
91
- for (let iy = 0; iy < actualH; iy++) {
92
- const start = (finalY + iy) * dw + finalX
93
- const end = start + actualW
101
+ for (let iy = 0; iy < fillH; iy++) {
102
+ const start = (dstY + iy) * dw + dstX
103
+ const end = start + fillW
94
104
  dst32.fill(color, start, end)
95
105
  }
96
106
  }
@@ -1,49 +1,66 @@
1
1
  import { type PixelMutateOptions } from '../_types'
2
- import { makeClippedRect, resolveRectClipping } from '../Rect/resolveClipping'
3
2
  import type { PixelData32 } from './_pixelData-types'
4
3
 
5
- const SCRATCH_RECT = makeClippedRect()
6
-
4
+ /**
5
+ * Inverts the RGB color data of the target PixelData, optionally controlled by a mask.
6
+ * @param target - The target to modify.
7
+ * @param opts - Options defining the area, mask, and offsets.
8
+ * @returns true if the operation was performed within bounds.
9
+ */
7
10
  export function invertPixelData(
8
11
  target: PixelData32,
9
12
  opts?: PixelMutateOptions,
10
13
  ): boolean {
14
+ const targetW = target.w
15
+ const targetH = target.h
16
+
11
17
  const mask = opts?.mask
18
+ const invertMask = opts?.invertMask ?? false
19
+
12
20
  const targetX = opts?.x ?? 0
13
21
  const targetY = opts?.y ?? 0
14
22
  const mx = opts?.mx ?? 0
15
23
  const my = opts?.my ?? 0
16
- const width = opts?.w ?? target.w
17
- const height = opts?.h ?? target.h
18
- const invertMask = opts?.invertMask ?? false
24
+ const w = opts?.w ?? targetW
25
+ const h = opts?.h ?? targetH
19
26
 
20
- const clip = resolveRectClipping(targetX, targetY, width, height, target.w, target.h, SCRATCH_RECT)
27
+ // Inline clipping logic
28
+ let x = targetX
29
+ let y = targetY
30
+ let actualW = w
31
+ let actualH = h
21
32
 
22
- if (!clip.inBounds) return false
33
+ if (x < 0) {
34
+ actualW += x
35
+ x = 0
36
+ }
23
37
 
24
- const {
25
- x,
26
- y,
27
- w: actualW,
28
- h: actualH,
29
- } = clip
38
+ if (y < 0) {
39
+ actualH += y
40
+ y = 0
41
+ }
42
+
43
+ actualW = Math.min(actualW, targetW - x)
44
+ actualH = Math.min(actualH, targetH - y)
45
+
46
+ if (actualW <= 0 || actualH <= 0) return false
30
47
 
31
48
  const dst32 = target.data
32
- const dw = target.w
33
- const mPitch = mask?.w ?? width
49
+ const dw = targetW
34
50
 
51
+ // Calculate relative movement for the mask coordinate
35
52
  const dx = x - targetX
36
53
  const dy = y - targetY
37
54
 
38
55
  let dIdx = y * dw + x
39
- let mIdx = (my + dy) * mPitch + (mx + dx)
40
-
41
56
  const dStride = dw - actualW
42
- const mStride = mPitch - actualW
43
57
 
44
- // Optimization: Split loops to avoid checking `if (mask)` for every pixel.
45
58
  if (mask) {
46
59
  const maskData = mask.data
60
+ const mPitch = mask.w
61
+ let mIdx = (my + dy) * mPitch + (mx + dx)
62
+ const mStride = mPitch - actualW
63
+
47
64
  for (let iy = 0; iy < actualH; iy++) {
48
65
  for (let ix = 0; ix < actualW; ix++) {
49
66
  const mVal = maskData[mIdx]
@@ -52,7 +69,7 @@ export function invertPixelData(
52
69
  : mVal === 1
53
70
 
54
71
  if (isHit) {
55
- // XOR with 0x00FFFFFF flips RGB bits and ignores Alpha (the top 8 bits)
72
+ // XOR with 0x00FFFFFF flips RGB bits and ignores Alpha
56
73
  dst32[dIdx] = dst32[dIdx] ^ 0x00FFFFFF
57
74
  }
58
75
  dIdx++
@@ -0,0 +1,75 @@
1
+ import type { MutablePixelData32, PixelData32 } from './_pixelData-types'
2
+
3
+ /**
4
+ * Non-destructively resizes the {@link PixelData32} buffer to new dimensions, optionally
5
+ * offsetting the original content.
6
+ * This operation creates a new buffer. It does not scale or stretch pixels;
7
+ * instead, it crops or pads the image based on the new dimensions.
8
+ *
9
+ * @param target - The source pixel data to resize.
10
+ * @param newWidth - The target width in pixels.
11
+ * @param newHeight - The target height in pixels.
12
+ * @param offsetX - The horizontal offset for placing the original image.
13
+ * @param offsetY - The vertical offset for placing the original image.
14
+ * @param out - output object
15
+ * @returns A new {@link PixelData32} object with the specified dimensions.
16
+ */
17
+ export function resizePixelData(
18
+ target: PixelData32,
19
+ newWidth: number,
20
+ newHeight: number,
21
+ offsetX = 0,
22
+ offsetY = 0,
23
+ out?: MutablePixelData32,
24
+ ): PixelData32 {
25
+ const newData = new Uint32Array(newWidth * newHeight)
26
+ const {
27
+ w: oldW,
28
+ h: oldH,
29
+ data: oldData,
30
+ } = target
31
+
32
+ const result = out ?? {} as MutablePixelData32
33
+ result.w = newWidth
34
+ result.h = newHeight
35
+ result.data = newData
36
+
37
+ // Determine intersection of the old image (at offset) and new canvas bounds
38
+ const x0 = Math.max(0, offsetX)
39
+ const y0 = Math.max(0, offsetY)
40
+ const x1 = Math.min(newWidth, offsetX + oldW)
41
+ const y1 = Math.min(newHeight, offsetY + oldH)
42
+
43
+ if (x1 <= x0 || y1 <= y0) {
44
+ return result
45
+ }
46
+
47
+ const copyW = x1 - x0
48
+ const copyH = y1 - y0
49
+
50
+ // Optimization: If we are copying the full width of both buffers,
51
+ // we can perform a single bulk 1D copy.
52
+ if (copyW === oldW && copyW === newWidth && offsetX === 0) {
53
+ const srcStart = (y0 - offsetY) * oldW
54
+ const dstStart = y0 * newWidth
55
+ const len = copyW * copyH
56
+
57
+ newData.set(oldData.subarray(srcStart, srcStart + len), dstStart)
58
+ return result
59
+ }
60
+
61
+ // Standard row-by-row copy
62
+ for (let row = 0; row < copyH; row++) {
63
+ const dstY = y0 + row
64
+ const srcY = dstY - offsetY
65
+ const srcX = x0 - offsetX
66
+
67
+ const dstStart = dstY * newWidth + x0
68
+ const srcStart = srcY * oldW + srcX
69
+ const chunk = oldData.subarray(srcStart, srcStart + copyW)
70
+
71
+ newData.set(chunk, dstStart)
72
+ }
73
+
74
+ return result
75
+ }