pixel-data-js 0.5.2 → 0.8.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.
@@ -5,79 +5,62 @@
5
5
  */
6
6
  export type IndexedImage = {
7
7
  /** The width of the image in pixels. */
8
- width: number,
8
+ width: number;
9
9
  /** The height of the image in pixels. */
10
- height: number,
10
+ height: number;
11
11
  /**
12
- * A flat array of indices where each value points to a color in the palette.
12
+ * A flat array of indices where each value points to a color in the palette.
13
13
  * Accessible via the formula: `index = x + (y * width)`.
14
14
  */
15
- data: Int32Array,
15
+ data: Int32Array;
16
16
  /**
17
- * A flattened Uint8Array of RGBA values.
18
- * Every 4 bytes represent one color: `[r, g, b, a]`.
17
+ * A palette of packed 32-bit colors (ABGR).
19
18
  */
20
- palette: Uint8Array,
19
+ palette: Int32Array;
21
20
  /**
22
21
  * The specific index in the palette that represents a fully transparent pixel.
23
- * All pixels with an alpha value of 0 are normalized to this index.
24
22
  */
25
- transparentPalletIndex: number,
26
- }
23
+ transparentPalletIndex: number;
24
+ };
27
25
 
28
26
  /**
29
27
  * Converts standard ImageData into an IndexedImage format.
30
- * This process normalizes all transparent pixels into a single palette entry
31
- * and maps all unique RGBA colors to sequential integer IDs.
32
- * @param imageData - The raw ImageData from a canvas or image source.
33
- * @returns An IndexedImage object containing the index grid and color palette.
34
28
  */
35
29
  export function makeIndexedImage(imageData: ImageData): IndexedImage {
36
- const width = imageData.width
37
- const height = imageData.height
38
- const rawData = imageData.data
39
- const indexedData = new Int32Array(rawData.length / 4)
40
- const colorMap = new Map<string, number>()
41
- const tempPalette: number[] = []
30
+ const width = imageData.width;
31
+ const height = imageData.height;
32
+ // Use a 32-bit view to read pixels as packed integers
33
+ const rawData = new Uint32Array(imageData.data.buffer);
34
+ const indexedData = new Int32Array(rawData.length);
35
+ const colorMap = new Map<number, number>();
36
+ const tempPalette: number[] = [];
42
37
 
43
- const transparentKey = '0,0,0,0'
44
- const transparentPalletIndex = 0
38
+ const transparentColor = 0; // 0x00000000
39
+ const transparentPalletIndex = 0;
45
40
 
46
- // Initialize palette with normalized transparent color at index 0
47
- colorMap.set(transparentKey, transparentPalletIndex)
48
- tempPalette.push(0)
49
- tempPalette.push(0)
50
- tempPalette.push(0)
51
- tempPalette.push(0)
41
+ // Initialize palette with normalized transparent color
42
+ colorMap.set(transparentColor, transparentPalletIndex);
43
+ tempPalette.push(transparentColor);
52
44
 
53
- for (let i = 0; i < indexedData.length; i++) {
54
- const r = rawData[i * 4]!
55
- const g = rawData[i * 4 + 1]!
56
- const b = rawData[i * 4 + 2]!
57
- const a = rawData[i * 4 + 3]!
45
+ for (let i = 0; i < rawData.length; i++) {
46
+ const pixel = rawData[i]!;
58
47
 
59
- let key: string
60
- if (a === 0) {
61
- key = transparentKey
62
- } else {
63
- key = `${r},${g},${b},${a}`
64
- }
48
+ // Check if the pixel is fully transparent
49
+ const isTransparent = (pixel >>> 24) === 0;
50
+ const colorKey = isTransparent ? transparentColor : pixel;
65
51
 
66
- let id = colorMap.get(key)
52
+ let id = colorMap.get(colorKey);
67
53
 
68
54
  if (id === undefined) {
69
- id = colorMap.size
70
- tempPalette.push(r)
71
- tempPalette.push(g)
72
- tempPalette.push(b)
73
- tempPalette.push(a)
74
- colorMap.set(key, id)
55
+ id = colorMap.size;
56
+ tempPalette.push(colorKey);
57
+ colorMap.set(colorKey, id);
75
58
  }
76
59
 
77
- indexedData[i] = id
60
+ indexedData[i] = id;
78
61
  }
79
62
 
80
- const palette = new Uint8Array(tempPalette)
63
+ const palette = new Int32Array(tempPalette);
81
64
 
82
65
  return {
83
66
  width,
@@ -85,5 +68,5 @@ export function makeIndexedImage(imageData: ImageData): IndexedImage {
85
68
  data: indexedData,
86
69
  transparentPalletIndex,
87
70
  palette,
88
- }
71
+ };
89
72
  }
@@ -0,0 +1,65 @@
1
+ import type { Color32 } from '../_types'
2
+ import { packColor } from '../color'
3
+ import type { IndexedImage } from './IndexedImage'
4
+
5
+ /**
6
+ * Calculates the area-weighted average color of an IndexedImage.
7
+ * This accounts for how often each palette index appears in the pixel data.
8
+ * @param indexedImage - The IndexedImage containing pixel indices and the palette.
9
+ * @param includeTransparent - Whether to include the transparent pixels in the average.
10
+ * @returns The average RGBA color of the image.
11
+ */
12
+ export function indexedImageToAverageColor(
13
+ indexedImage: IndexedImage,
14
+ includeTransparent: boolean = false,
15
+ ): Color32 {
16
+ const { data, palette, transparentPalletIndex } = indexedImage
17
+ const counts = new Uint32Array(palette.length / 4)
18
+
19
+ // Tally occurrences of each index
20
+ for (let i = 0; i < data.length; i++) {
21
+ const id = data[i]!
22
+ counts[id]!++
23
+ }
24
+
25
+ let rSum = 0
26
+ let gSum = 0
27
+ let bSum = 0
28
+ let aSum = 0
29
+ let totalWeight = 0
30
+
31
+ for (let id = 0; id < counts.length; id++) {
32
+ const weight = counts[id]!
33
+
34
+ if (weight === 0) {
35
+ continue
36
+ }
37
+
38
+ if (!includeTransparent && id === transparentPalletIndex) {
39
+ continue
40
+ }
41
+
42
+ const pIdx = id * 4
43
+ const r = palette[pIdx]!
44
+ const g = palette[pIdx + 1]!
45
+ const b = palette[pIdx + 2]!
46
+ const a = palette[pIdx + 3]!
47
+
48
+ rSum += r * weight
49
+ gSum += g * weight
50
+ bSum += b * weight
51
+ aSum += a * weight
52
+ totalWeight += weight
53
+ }
54
+
55
+ if (totalWeight === 0) {
56
+ return packColor(0, 0, 0, 0)
57
+ }
58
+
59
+ const r = (rSum / totalWeight) | 0
60
+ const g = (gSum / totalWeight) | 0
61
+ const b = (bSum / totalWeight) | 0
62
+ const a = (aSum / totalWeight) | 0
63
+
64
+ return packColor(r, g, b, a)
65
+ }
@@ -0,0 +1,31 @@
1
+ import type { ImageDataLike } from '../_types'
2
+
3
+ export class PixelData {
4
+ public data32: Uint32Array
5
+ public width: number
6
+ public height: number
7
+
8
+ constructor(public readonly imageData: ImageDataLike) {
9
+ this.width = imageData.width
10
+ this.height = imageData.height
11
+
12
+ // Create the view once.
13
+ this.data32 = new Uint32Array(
14
+ imageData.data.buffer,
15
+ imageData.data.byteOffset,
16
+ // Shift right by 2 is a fast bitwise division by 4.
17
+ imageData.data.byteLength >> 2,
18
+ )
19
+ }
20
+
21
+ copy(): PixelData {
22
+ const buffer = new Uint8ClampedArray(this.data32.buffer.slice(0))
23
+ const imageData = {
24
+ data: buffer,
25
+ width: this.width,
26
+ height: this.height,
27
+ }
28
+
29
+ return new PixelData(imageData)
30
+ }
31
+ }
@@ -1,5 +1,5 @@
1
1
  import { type AnyMask, type ApplyMaskOptions, MaskType } from '../_types'
2
- import type { PixelData } from '../PixelData'
2
+ import type { PixelData } from './PixelData'
3
3
 
4
4
  /**
5
5
  * Directly applies a mask to a region of PixelData,
@@ -1,6 +1,7 @@
1
1
  import { type Color32, type ColorBlendOptions, MaskType } from '../_types'
2
- import { sourceOverColor32 } from '../blend-modes'
3
- import type { PixelData } from '../PixelData'
2
+ import { BlendMode } from '../BlendModes/blend-modes'
3
+ import { FAST_BLEND_MODES } from '../BlendModes/blend-modes-fast'
4
+ import type { PixelData } from './PixelData'
4
5
 
5
6
  /**
6
7
  * Fills a rectangle in the destination PixelData with a single color,
@@ -17,7 +18,7 @@ export function blendColorPixelData(
17
18
  w: width = dst.width,
18
19
  h: height = dst.height,
19
20
  alpha: globalAlpha = 255,
20
- blendFn = sourceOverColor32,
21
+ blendFn = FAST_BLEND_MODES[BlendMode.sourceOver],
21
22
  mask,
22
23
  maskType = MaskType.ALPHA,
23
24
  mw,
@@ -1,6 +1,7 @@
1
1
  import { type Color32, MaskType, type PixelBlendOptions } from '../_types'
2
- import { sourceOverColor32 } from '../blend-modes'
3
- import type { PixelData } from '../PixelData'
2
+ import { BlendMode } from '../BlendModes/blend-modes'
3
+ import { FAST_BLEND_MODES } from '../BlendModes/blend-modes-fast'
4
+ import type { PixelData } from './PixelData'
4
5
 
5
6
  /**
6
7
  * Blits source PixelData into a destination PixelData using 32-bit integer bitwise blending.
@@ -29,7 +30,7 @@ export function blendPixelData(
29
30
  w: width = src.width,
30
31
  h: height = src.height,
31
32
  alpha: globalAlpha = 255,
32
- blendFn = sourceOverColor32,
33
+ blendFn = FAST_BLEND_MODES[BlendMode.sourceOver],
33
34
  mask,
34
35
  maskType = MaskType.ALPHA,
35
36
  mw,
@@ -1,5 +1,5 @@
1
1
  import type { Color32, Rect } from '../_types'
2
- import type { PixelData } from '../PixelData'
2
+ import type { PixelData } from './PixelData'
3
3
  import { fillPixelData } from './fillPixelData'
4
4
 
5
5
  /**
@@ -1,5 +1,5 @@
1
1
  import type { Color32, Rect } from '../_types'
2
- import type { PixelData } from '../PixelData'
2
+ import type { PixelData } from './PixelData'
3
3
 
4
4
  /**
5
5
  * Fills a region or the {@link PixelData} buffer with a solid color.
@@ -1,4 +1,4 @@
1
- import type { PixelData } from '../PixelData'
1
+ import type { PixelData } from './PixelData'
2
2
 
3
3
  export function invertPixelData(
4
4
  pixelData: PixelData,
@@ -1,5 +1,5 @@
1
1
  import type { AlphaMask } from '../_types'
2
- import type { PixelData } from '../PixelData'
2
+ import type { PixelData } from './PixelData'
3
3
 
4
4
  /**
5
5
  * Extracts the alpha channel from PixelData into a single-channel mask.
@@ -0,0 +1,42 @@
1
+ import type { PixelData } from './PixelData'
2
+
3
+ export function reflectPixelDataHorizontal(pixelData: PixelData): void {
4
+ const width = pixelData.width
5
+ const height = pixelData.height
6
+ const data = pixelData.data32
7
+ const halfWidth = Math.floor(width / 2)
8
+
9
+ for (let y = 0; y < height; y++) {
10
+ const rowOffset = y * width
11
+
12
+ for (let x = 0; x < halfWidth; x++) {
13
+ const leftIdx = rowOffset + x
14
+ const rightIdx = rowOffset + (width - 1 - x)
15
+ const temp = data[leftIdx]
16
+
17
+ data[leftIdx] = data[rightIdx]
18
+ data[rightIdx] = temp
19
+ }
20
+ }
21
+ }
22
+
23
+ export function reflectPixelDataVertical(pixelData: PixelData): void {
24
+ const width = pixelData.width
25
+ const height = pixelData.height
26
+ const data = pixelData.data32
27
+ const halfHeight = Math.floor(height / 2)
28
+
29
+ for (let y = 0; y < halfHeight; y++) {
30
+ const topRowOffset = y * width
31
+ const bottomRowOffset = (height - 1 - y) * width
32
+
33
+ for (let x = 0; x < width; x++) {
34
+ const topIdx = topRowOffset + x
35
+ const bottomIdx = bottomRowOffset + x
36
+ const temp = data[topIdx]
37
+
38
+ data[topIdx] = data[bottomIdx]
39
+ data[bottomIdx] = temp
40
+ }
41
+ }
42
+ }
@@ -0,0 +1,56 @@
1
+ import { PixelData } from './PixelData'
2
+
3
+ /**
4
+ * Rotates pixel data 90 degrees clockwise.
5
+ * If the image is square, it performs the rotation in-place.
6
+ * If rectangular, it returns a new Uint32Array and updates dimensions.
7
+ */
8
+ export function rotatePixelData(pixelData: PixelData): void {
9
+ const width = pixelData.width
10
+ const height = pixelData.height
11
+ const data = pixelData.data32
12
+
13
+ if (width === height) {
14
+ rotateSquareInPlace(pixelData)
15
+ return
16
+ }
17
+
18
+ const newWidth = height
19
+ const newHeight = width
20
+ const newData = new Uint32Array(data.length)
21
+
22
+ for (let y = 0; y < height; y++) {
23
+ for (let x = 0; x < width; x++) {
24
+ const oldIdx = y * width + x
25
+ const newX = height - 1 - y
26
+ const newY = x
27
+ const newIdx = newY * newWidth + newX
28
+
29
+ newData[newIdx] = data[oldIdx]
30
+ }
31
+ }
32
+
33
+ pixelData.width = newWidth
34
+ pixelData.height = newHeight
35
+ pixelData.data32 = newData
36
+ }
37
+
38
+ function rotateSquareInPlace(pixelData: PixelData): void {
39
+ const n = pixelData.width
40
+ const data = pixelData.data32
41
+
42
+ for (let i = 0; i < n / 2; i++) {
43
+ for (let j = i; j < n - i - 1; j++) {
44
+ const temp = data[i * n + j]
45
+ const top = i * n + j
46
+ const right = j * n + (n - 1 - i)
47
+ const bottom = (n - 1 - i) * n + (n - 1 - j)
48
+ const left = (n - 1 - j) * n + i
49
+
50
+ data[top] = data[left]
51
+ data[left] = data[bottom]
52
+ data[bottom] = data[right]
53
+ data[right] = temp
54
+ }
55
+ }
56
+ }
package/src/_types.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { sourceOverColor32 } from './blend-modes'
1
+ import { sourceOverFast } from './BlendModes/blend-modes-fast'
2
2
 
3
3
  /** ALL values are 0-255 (including alpha which in CSS is 0-1) */
4
4
  export type RGBA = { r: number, g: number, b: number, a: number }
@@ -142,7 +142,7 @@ export interface PixelBlendOptions extends PixelOptions {
142
142
 
143
143
  /**
144
144
  * The blending algorithm to use for blending pixels.
145
- * @default {@link sourceOverColor32}
145
+ * @default {@link sourceOverFast}
146
146
  */
147
147
  blendFn?: BlendColor32
148
148
  }
@@ -153,7 +153,7 @@ export interface PixelBlendOptions extends PixelOptions {
153
153
  export interface ColorBlendOptions extends PixelOptions {
154
154
  /**
155
155
  * The blending algorithm to use for blending pixels.
156
- * @default {@link sourceOverColor32}
156
+ * @default {@link sourceOverFast}
157
157
  */
158
158
  blendFn?: BlendColor32
159
159
  }
package/src/index.ts CHANGED
@@ -1,9 +1,12 @@
1
1
  export * from './_types'
2
- export * from './blend-modes'
3
2
  export * from './color'
4
3
 
5
4
  export * from './Algorithm/floodFillSelection'
6
5
 
6
+ export * from './BlendModes/blend-modes'
7
+ export * from './BlendModes/blend-modes-fast'
8
+ export * from './BlendModes/blend-modes-perfect'
9
+
7
10
  export * from './Canvas/PixelCanvas'
8
11
  export * from './Canvas/ReusableCanvas'
9
12
 
@@ -23,6 +26,7 @@ export * from './ImageData/serialization'
23
26
  export * from './ImageData/writeImageDataPixels'
24
27
 
25
28
  export * from './IndexedImage/IndexedImage'
29
+ export * from './IndexedImage/indexedImageToAverageColor'
26
30
 
27
31
  export * from './Input/fileInputChangeToImageData'
28
32
  export * from './Input/fileToImageData'
@@ -33,7 +37,7 @@ export * from './Mask/extractMask'
33
37
  export * from './Mask/invertMask'
34
38
  export * from './Mask/mergeMasks'
35
39
 
36
- export * from './PixelData'
40
+ export * from './PixelData/PixelData'
37
41
  export * from './PixelData/applyMaskToPixelData'
38
42
  export * from './PixelData/blendColorPixelData'
39
43
  export * from './PixelData/blendPixelData'
@@ -43,3 +47,4 @@ export * from './PixelData/invertPixelData'
43
47
  export * from './PixelData/pixelDataToAlphaMask'
44
48
 
45
49
  export * from './Rect/trimRectBounds'
50
+ export { BlendMode } from './BlendModes/blend-modes'
package/src/PixelData.ts DELETED
@@ -1,20 +0,0 @@
1
- import type { ImageDataLike } from './_types'
2
-
3
- export class PixelData {
4
- public readonly data32: Uint32Array
5
- public readonly width: number
6
- public readonly height: number
7
-
8
- constructor(public readonly imageData: ImageDataLike) {
9
- this.width = imageData.width
10
- this.height = imageData.height
11
-
12
- // Create the view once.
13
- // Shift right by 2 is a fast bitwise division by 4.
14
- this.data32 = new Uint32Array(
15
- imageData.data.buffer,
16
- imageData.data.byteOffset,
17
- imageData.data.byteLength >> 2,
18
- )
19
- }
20
- }