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.
- package/dist/index.dev.cjs +871 -257
- package/dist/index.dev.cjs.map +1 -1
- package/dist/index.dev.js +814 -231
- package/dist/index.dev.js.map +1 -1
- package/dist/index.prod.cjs +871 -257
- package/dist/index.prod.cjs.map +1 -1
- package/dist/index.prod.d.ts +171 -100
- package/dist/index.prod.js +814 -231
- package/dist/index.prod.js.map +1 -1
- package/package.json +1 -1
- package/src/Algorithm/floodFillSelection.ts +1 -1
- package/src/{blend-modes.ts → BlendModes/blend-modes-fast.ts} +215 -217
- package/src/BlendModes/blend-modes-perfect.ts +651 -0
- package/src/BlendModes/blend-modes.ts +31 -0
- package/src/IndexedImage/IndexedImage.ts +32 -49
- package/src/IndexedImage/indexedImageToAverageColor.ts +65 -0
- package/src/PixelData/PixelData.ts +31 -0
- package/src/PixelData/applyMaskToPixelData.ts +1 -1
- package/src/PixelData/blendColorPixelData.ts +4 -3
- package/src/PixelData/blendPixelData.ts +4 -3
- package/src/PixelData/clearPixelData.ts +1 -1
- package/src/PixelData/fillPixelData.ts +1 -1
- package/src/PixelData/invertPixelData.ts +1 -1
- package/src/PixelData/pixelDataToAlphaMask.ts +1 -1
- package/src/PixelData/reflectPixelData.ts +42 -0
- package/src/PixelData/rotatePixelData.ts +56 -0
- package/src/_types.ts +3 -3
- package/src/index.ts +7 -2
- package/src/PixelData.ts +0 -20
|
@@ -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
|
-
*
|
|
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
|
|
18
|
-
* Every 4 bytes represent one color: `[r, g, b, a]`.
|
|
17
|
+
* A palette of packed 32-bit colors (ABGR).
|
|
19
18
|
*/
|
|
20
|
-
palette:
|
|
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
|
-
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
const
|
|
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
|
|
44
|
-
const transparentPalletIndex = 0
|
|
38
|
+
const transparentColor = 0; // 0x00000000
|
|
39
|
+
const transparentPalletIndex = 0;
|
|
45
40
|
|
|
46
|
-
// Initialize palette with normalized transparent color
|
|
47
|
-
colorMap.set(
|
|
48
|
-
tempPalette.push(
|
|
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 <
|
|
54
|
-
const
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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(
|
|
52
|
+
let id = colorMap.get(colorKey);
|
|
67
53
|
|
|
68
54
|
if (id === undefined) {
|
|
69
|
-
id = colorMap.size
|
|
70
|
-
tempPalette.push(
|
|
71
|
-
|
|
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
|
|
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,6 +1,7 @@
|
|
|
1
1
|
import { type Color32, type ColorBlendOptions, MaskType } from '../_types'
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
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 =
|
|
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 {
|
|
3
|
-
import
|
|
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 =
|
|
33
|
+
blendFn = FAST_BLEND_MODES[BlendMode.sourceOver],
|
|
33
34
|
mask,
|
|
34
35
|
maskType = MaskType.ALPHA,
|
|
35
36
|
mw,
|
|
@@ -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 {
|
|
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
|
|
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
|
|
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
|
-
}
|