pixel-data-js 0.18.0 → 0.19.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/README.md +6 -1
- package/dist/index.dev.cjs +2723 -1487
- package/dist/index.dev.cjs.map +1 -1
- package/dist/index.dev.js +2690 -1481
- package/dist/index.dev.js.map +1 -1
- package/dist/index.prod.cjs +2723 -1487
- package/dist/index.prod.cjs.map +1 -1
- package/dist/index.prod.d.ts +400 -246
- package/dist/index.prod.js +2690 -1481
- package/dist/index.prod.js.map +1 -1
- package/package.json +21 -6
- package/src/Algorithm/forEachLinePoint.ts +36 -0
- package/src/BlendModes/BlendModeRegistry.ts +2 -0
- package/src/BlendModes/blend-modes-fast.ts +2 -2
- package/src/BlendModes/blend-modes-perfect.ts +5 -4
- package/src/BlendModes/toBlendModeIndexAndName.ts +41 -0
- package/src/History/PixelAccumulator.ts +2 -2
- package/src/History/PixelMutator/mutatorApplyAlphaMask.ts +30 -0
- package/src/History/PixelMutator/mutatorApplyBinaryMask.ts +30 -0
- package/src/History/PixelMutator/mutatorApplyCircleBrush.ts +23 -9
- package/src/History/PixelMutator/mutatorApplyCircleBrushStroke.ts +138 -0
- package/src/History/PixelMutator/mutatorApplyCirclePencil.ts +59 -0
- package/src/History/PixelMutator/mutatorApplyCirclePencilStroke.ts +131 -0
- package/src/History/PixelMutator/mutatorApplyRectBrush.ts +20 -7
- package/src/History/PixelMutator/mutatorApplyRectBrushStroke.ts +169 -0
- package/src/History/PixelMutator/mutatorApplyRectPencil.ts +62 -0
- package/src/History/PixelMutator/mutatorApplyRectPencilStroke.ts +149 -0
- package/src/History/PixelMutator/mutatorBlendColor.ts +9 -4
- package/src/History/PixelMutator/mutatorBlendPixelData.ts +10 -5
- package/src/History/PixelMutator/mutatorClear.ts +27 -0
- package/src/History/PixelMutator/{mutatorFillPixelData.ts → mutatorFill.ts} +9 -3
- package/src/History/PixelMutator/mutatorInvert.ts +10 -3
- package/src/History/PixelMutator.ts +23 -3
- package/src/History/PixelPatchTiles.ts +2 -2
- package/src/History/PixelWriter.ts +3 -3
- package/src/ImageData/ImageDataLike.ts +13 -0
- package/src/ImageData/extractImageDataBuffer.ts +22 -15
- package/src/ImageData/serialization.ts +4 -4
- package/src/ImageData/uInt32ArrayToImageData.ts +29 -0
- package/src/ImageData/writeImageData.ts +26 -18
- package/src/ImageData/writeImageDataBuffer.ts +30 -18
- package/src/IndexedImage/indexedImageToAverageColor.ts +1 -1
- package/src/Internal/resolveClipping.ts +140 -0
- package/src/Mask/applyBinaryMaskToAlphaMask.ts +89 -0
- package/src/Mask/copyMask.ts +1 -3
- package/src/Mask/mergeAlphaMasks.ts +81 -0
- package/src/Mask/mergeBinaryMasks.ts +89 -0
- package/src/PixelData/PixelBuffer32.ts +28 -0
- package/src/PixelData/PixelData.ts +38 -33
- package/src/PixelData/applyAlphaMaskToPixelData.ts +119 -0
- package/src/PixelData/applyBinaryMaskToPixelData.ts +111 -0
- package/src/PixelData/applyCircleBrushToPixelData.ts +31 -56
- package/src/PixelData/applyRectBrushToPixelData.ts +39 -71
- package/src/PixelData/blendColorPixelData.ts +18 -111
- package/src/PixelData/blendColorPixelDataAlphaMask.ts +111 -0
- package/src/PixelData/blendColorPixelDataBinaryMask.ts +89 -0
- package/src/PixelData/blendPixelData.ts +19 -107
- package/src/PixelData/blendPixelDataAlphaMask.ts +149 -0
- package/src/PixelData/blendPixelDataBinaryMask.ts +133 -0
- package/src/PixelData/clearPixelData.ts +2 -3
- package/src/PixelData/extractPixelData.ts +4 -4
- package/src/PixelData/extractPixelDataBuffer.ts +38 -26
- package/src/PixelData/fillPixelData.ts +18 -20
- package/src/PixelData/invertPixelData.ts +13 -21
- package/src/PixelData/pixelDataToAlphaMask.ts +2 -3
- package/src/PixelData/reflectPixelData.ts +3 -3
- package/src/PixelData/resamplePixelData.ts +2 -6
- package/src/PixelData/writePixelDataBuffer.ts +34 -20
- package/src/Rect/getCircleBrushOrPencilBounds.ts +43 -0
- package/src/Rect/getCircleBrushOrPencilStrokeBounds.ts +24 -0
- package/src/Rect/getRectBrushOrPencilBounds.ts +38 -0
- package/src/Rect/getRectBrushOrPencilStrokeBounds.ts +26 -0
- package/src/_types.ts +49 -33
- package/src/index.ts +47 -11
- package/src/History/PixelMutator/mutatorApplyMask.ts +0 -20
- package/src/Mask/mergeMasks.ts +0 -100
- package/src/PixelData/applyMaskToPixelData.ts +0 -129
|
@@ -1,44 +1,49 @@
|
|
|
1
|
+
import type { ImageDataLike, ImageDataLikeConstructor, IPixelData } from '../_types'
|
|
1
2
|
import { imageDataToUInt32Array } from '../ImageData/imageDataToUInt32Array'
|
|
2
3
|
|
|
3
|
-
export class PixelData {
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
export class PixelData<T extends ImageDataLike = ImageData> implements IPixelData {
|
|
5
|
+
readonly data32: Uint32Array
|
|
6
|
+
readonly imageData: T
|
|
7
|
+
readonly width: number
|
|
8
|
+
readonly height: number
|
|
6
9
|
|
|
7
|
-
|
|
8
|
-
return this.imageData.width
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
get height(): number {
|
|
12
|
-
return this.imageData.height
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
constructor(imageData: ImageData) {
|
|
10
|
+
constructor(imageData: T) {
|
|
16
11
|
this.data32 = imageDataToUInt32Array(imageData)
|
|
17
12
|
this.imageData = imageData
|
|
13
|
+
this.width = imageData.width
|
|
14
|
+
this.height = imageData.height
|
|
18
15
|
}
|
|
19
16
|
|
|
20
|
-
set(imageData:
|
|
21
|
-
this.imageData = imageData
|
|
22
|
-
this.data32 = imageDataToUInt32Array(imageData)
|
|
17
|
+
set(imageData: T): void {
|
|
18
|
+
;(this as any).imageData = imageData
|
|
19
|
+
;(this as any).data32 = imageDataToUInt32Array(imageData)
|
|
20
|
+
;(this as any).width = imageData.width
|
|
21
|
+
;(this as any).height = imageData.height
|
|
23
22
|
}
|
|
24
23
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
24
|
+
// should only be used for debug and testing
|
|
25
|
+
copy(): PixelData<T> {
|
|
26
|
+
const data = this.imageData.data
|
|
27
|
+
const buffer = new Uint8ClampedArray(data)
|
|
28
|
+
const Ctor = this.imageData.constructor
|
|
29
|
+
const isCtorValid = typeof Ctor === 'function'
|
|
30
|
+
|
|
31
|
+
let newImageData: T
|
|
32
|
+
if (isCtorValid && Ctor !== Object) {
|
|
33
|
+
const ImageConstructor = Ctor as ImageDataLikeConstructor<T>
|
|
34
|
+
newImageData = new ImageConstructor(
|
|
35
|
+
buffer,
|
|
36
|
+
this.width,
|
|
37
|
+
this.height,
|
|
38
|
+
)
|
|
39
|
+
} else {
|
|
40
|
+
newImageData = {
|
|
41
|
+
width: this.width,
|
|
42
|
+
height: this.height,
|
|
43
|
+
data: buffer,
|
|
44
|
+
} as unknown as T
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return new PixelData<T>(newImageData)
|
|
43
48
|
}
|
|
44
49
|
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { type AlphaMask, type ApplyMaskToPixelDataOptions, type IPixelData } from '../_types'
|
|
2
|
+
|
|
3
|
+
export function applyAlphaMaskToPixelData(
|
|
4
|
+
dst: IPixelData,
|
|
5
|
+
mask: AlphaMask,
|
|
6
|
+
opts: ApplyMaskToPixelDataOptions = {},
|
|
7
|
+
): void {
|
|
8
|
+
const {
|
|
9
|
+
x: targetX = 0,
|
|
10
|
+
y: targetY = 0,
|
|
11
|
+
w: width = dst.width,
|
|
12
|
+
h: height = dst.height,
|
|
13
|
+
alpha: globalAlpha = 255,
|
|
14
|
+
mw,
|
|
15
|
+
mx = 0,
|
|
16
|
+
my = 0,
|
|
17
|
+
invertMask = false,
|
|
18
|
+
} = opts
|
|
19
|
+
|
|
20
|
+
if (globalAlpha === 0) return
|
|
21
|
+
|
|
22
|
+
// 1. Initial Destination Clipping
|
|
23
|
+
let x = targetX
|
|
24
|
+
let y = targetY
|
|
25
|
+
let w = width
|
|
26
|
+
let h = height
|
|
27
|
+
|
|
28
|
+
if (x < 0) {
|
|
29
|
+
w += x
|
|
30
|
+
x = 0
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (y < 0) {
|
|
34
|
+
h += y
|
|
35
|
+
y = 0
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
w = Math.min(w, dst.width - x)
|
|
39
|
+
h = Math.min(h, dst.height - y)
|
|
40
|
+
|
|
41
|
+
if (w <= 0) return
|
|
42
|
+
if (h <= 0) return
|
|
43
|
+
|
|
44
|
+
// 2. Determine Source Dimensions
|
|
45
|
+
const mPitch = mw ?? width
|
|
46
|
+
if (mPitch <= 0) return
|
|
47
|
+
const maskHeight = (mask.length / mPitch) | 0
|
|
48
|
+
|
|
49
|
+
// 3. Source Bounds Clipping
|
|
50
|
+
// Calculate where we would start reading in the mask
|
|
51
|
+
const startX = mx + (x - targetX)
|
|
52
|
+
const startY = my + (y - targetY)
|
|
53
|
+
|
|
54
|
+
// Find the safe overlap between the requested region and the mask bounds
|
|
55
|
+
const sX0 = Math.max(0, startX)
|
|
56
|
+
const sY0 = Math.max(0, startY)
|
|
57
|
+
const sX1 = Math.min(mPitch, startX + w)
|
|
58
|
+
const sY1 = Math.min(maskHeight, startY + h)
|
|
59
|
+
|
|
60
|
+
const finalW = sX1 - sX0
|
|
61
|
+
const finalH = sY1 - sY0
|
|
62
|
+
|
|
63
|
+
// This is where your failing tests are now caught
|
|
64
|
+
if (finalW <= 0) return
|
|
65
|
+
if (finalH <= 0) return
|
|
66
|
+
|
|
67
|
+
// 4. Align Destination with Source Clipping
|
|
68
|
+
// If the source was clipped on the top/left, we must shift the destination start
|
|
69
|
+
const xShift = sX0 - startX
|
|
70
|
+
const yShift = sY0 - startY
|
|
71
|
+
|
|
72
|
+
const dst32 = dst.data32
|
|
73
|
+
const dw = dst.width
|
|
74
|
+
const dStride = dw - finalW
|
|
75
|
+
const mStride = mPitch - finalW
|
|
76
|
+
|
|
77
|
+
let dIdx = (y + yShift) * dw + (x + xShift)
|
|
78
|
+
let mIdx = sY0 * mPitch + sX0
|
|
79
|
+
|
|
80
|
+
for (let iy = 0; iy < h; iy++) {
|
|
81
|
+
for (let ix = 0; ix < w; ix++) {
|
|
82
|
+
const mVal = mask[mIdx]
|
|
83
|
+
// Unified logic branch inside the hot path
|
|
84
|
+
const effectiveM = invertMask ? 255 - mVal : mVal
|
|
85
|
+
|
|
86
|
+
let weight = 0
|
|
87
|
+
|
|
88
|
+
if (effectiveM === 0) {
|
|
89
|
+
weight = 0
|
|
90
|
+
} else if (effectiveM === 255) {
|
|
91
|
+
weight = globalAlpha
|
|
92
|
+
} else if (globalAlpha === 255) {
|
|
93
|
+
weight = effectiveM
|
|
94
|
+
} else {
|
|
95
|
+
weight = (effectiveM * globalAlpha + 128) >> 8
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (weight === 0) {
|
|
99
|
+
// Clear alpha channel
|
|
100
|
+
dst32[dIdx] = (dst32[dIdx] & 0x00ffffff) >>> 0
|
|
101
|
+
} else if (weight !== 255) {
|
|
102
|
+
// Merge alpha channel
|
|
103
|
+
const d = dst32[dIdx]
|
|
104
|
+
const da = d >>> 24
|
|
105
|
+
|
|
106
|
+
if (da !== 0) {
|
|
107
|
+
const finalAlpha = da === 255 ? weight : (da * weight + 128) >> 8
|
|
108
|
+
dst32[dIdx] = ((d & 0x00ffffff) | (finalAlpha << 24)) >>> 0
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
dIdx++
|
|
113
|
+
mIdx++
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
dIdx += dStride
|
|
117
|
+
mIdx += mStride
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { type ApplyMaskToPixelDataOptions, type BinaryMask, type IPixelData } from '../_types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Directly applies a mask to a region of PixelData,
|
|
5
|
+
* modifying the destination's alpha channel in-place.
|
|
6
|
+
*/
|
|
7
|
+
export function applyBinaryMaskToPixelData(
|
|
8
|
+
dst: IPixelData,
|
|
9
|
+
mask: BinaryMask,
|
|
10
|
+
opts: ApplyMaskToPixelDataOptions = {},
|
|
11
|
+
): void {
|
|
12
|
+
const {
|
|
13
|
+
x: targetX = 0,
|
|
14
|
+
y: targetY = 0,
|
|
15
|
+
w: width = dst.width,
|
|
16
|
+
h: height = dst.height,
|
|
17
|
+
alpha = 255,
|
|
18
|
+
mw,
|
|
19
|
+
mx = 0,
|
|
20
|
+
my = 0,
|
|
21
|
+
invertMask = false,
|
|
22
|
+
} = opts
|
|
23
|
+
|
|
24
|
+
if (alpha === 0) return
|
|
25
|
+
|
|
26
|
+
// 1. Initial Destination Clipping
|
|
27
|
+
let x = targetX
|
|
28
|
+
let y = targetY
|
|
29
|
+
let w = width
|
|
30
|
+
let h = height
|
|
31
|
+
|
|
32
|
+
if (x < 0) {
|
|
33
|
+
w += x
|
|
34
|
+
x = 0
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (y < 0) {
|
|
38
|
+
h += y
|
|
39
|
+
y = 0
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
w = Math.min(w, dst.width - x)
|
|
43
|
+
h = Math.min(h, dst.height - y)
|
|
44
|
+
|
|
45
|
+
if (w <= 0) return
|
|
46
|
+
if (h <= 0) return
|
|
47
|
+
|
|
48
|
+
// 2. Determine Source Dimensions
|
|
49
|
+
const mPitch = mw ?? width
|
|
50
|
+
if (mPitch <= 0) return
|
|
51
|
+
const maskHeight = (mask.length / mPitch) | 0
|
|
52
|
+
|
|
53
|
+
// 3. Source Bounds Clipping
|
|
54
|
+
// Calculate where we would start reading in the mask
|
|
55
|
+
const startX = mx + (x - targetX)
|
|
56
|
+
const startY = my + (y - targetY)
|
|
57
|
+
|
|
58
|
+
// Find the safe overlap between the requested region and the mask bounds
|
|
59
|
+
const sX0 = Math.max(0, startX)
|
|
60
|
+
const sY0 = Math.max(0, startY)
|
|
61
|
+
const sX1 = Math.min(mPitch, startX + w)
|
|
62
|
+
const sY1 = Math.min(maskHeight, startY + h)
|
|
63
|
+
|
|
64
|
+
const finalW = sX1 - sX0
|
|
65
|
+
const finalH = sY1 - sY0
|
|
66
|
+
|
|
67
|
+
// This is where your failing tests are now caught
|
|
68
|
+
if (finalW <= 0) return
|
|
69
|
+
if (finalH <= 0) return
|
|
70
|
+
|
|
71
|
+
// 4. Align Destination with Source Clipping
|
|
72
|
+
// If the source was clipped on the top/left, we must shift the destination start
|
|
73
|
+
const xShift = sX0 - startX
|
|
74
|
+
const yShift = sY0 - startY
|
|
75
|
+
|
|
76
|
+
const dst32 = dst.data32
|
|
77
|
+
const dw = dst.width
|
|
78
|
+
const dStride = dw - finalW
|
|
79
|
+
const mStride = mPitch - finalW
|
|
80
|
+
|
|
81
|
+
let dIdx = (y + yShift) * dw + (x + xShift)
|
|
82
|
+
let mIdx = sY0 * mPitch + sX0
|
|
83
|
+
|
|
84
|
+
for (let iy = 0; iy < h; iy++) {
|
|
85
|
+
for (let ix = 0; ix < w; ix++) {
|
|
86
|
+
const mVal = mask[mIdx]
|
|
87
|
+
// Consistently determines if this pixel should be "masked out" (cleared)
|
|
88
|
+
const isMaskedOut = invertMask ? mVal !== 0 : mVal === 0
|
|
89
|
+
|
|
90
|
+
if (isMaskedOut) {
|
|
91
|
+
// Clear alpha channel only (keep RGB)
|
|
92
|
+
dst32[dIdx] = (dst32[dIdx] & 0x00ffffff) >>> 0
|
|
93
|
+
} else if (alpha !== 255) {
|
|
94
|
+
const d = dst32[dIdx]
|
|
95
|
+
const da = d >>> 24
|
|
96
|
+
|
|
97
|
+
// If pixel isn't already fully transparent, apply global alpha
|
|
98
|
+
if (da !== 0) {
|
|
99
|
+
const finalAlpha = da === 255 ? alpha : (da * alpha + 128) >> 8
|
|
100
|
+
dst32[dIdx] = ((d & 0x00ffffff) | (finalAlpha << 24)) >>> 0
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
dIdx++
|
|
105
|
+
mIdx++
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
dIdx += dStride
|
|
109
|
+
mIdx += mStride
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { BlendColor32, Color32, Rect } from '../_types'
|
|
1
|
+
import type { BlendColor32, Color32, IPixelData, Rect } from '../_types'
|
|
2
2
|
import { sourceOverPerfect } from '../BlendModes/blend-modes-perfect'
|
|
3
|
-
import
|
|
3
|
+
import { getCircleBrushOrPencilBounds } from '../Rect/getCircleBrushOrPencilBounds'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Applies a circular brush to pixel data, blending a color with optional falloff.
|
|
@@ -14,17 +14,17 @@ import type { PixelData } from './PixelData'
|
|
|
14
14
|
* @default 255
|
|
15
15
|
* @param fallOff A function that returns an alpha multiplier (0-1) based on the normalized distance (0-1) from the circle's center.
|
|
16
16
|
* @param blendFn
|
|
17
|
-
* @param bounds precalculated result from {@link
|
|
17
|
+
* @param bounds precalculated result from {@link getCircleBrushOrPencilBounds}
|
|
18
18
|
* @default sourceOverPerfect
|
|
19
19
|
*/
|
|
20
20
|
export function applyCircleBrushToPixelData(
|
|
21
|
-
target:
|
|
21
|
+
target: IPixelData,
|
|
22
22
|
color: Color32,
|
|
23
23
|
centerX: number,
|
|
24
24
|
centerY: number,
|
|
25
25
|
brushSize: number,
|
|
26
26
|
alpha = 255,
|
|
27
|
-
fallOff
|
|
27
|
+
fallOff: (dist: number) => number,
|
|
28
28
|
blendFn: BlendColor32 = sourceOverPerfect,
|
|
29
29
|
bounds?: Rect,
|
|
30
30
|
): void {
|
|
@@ -32,12 +32,12 @@ export function applyCircleBrushToPixelData(
|
|
|
32
32
|
const targetHeight = target.height
|
|
33
33
|
|
|
34
34
|
// Use provided bounds OR calculate them once
|
|
35
|
-
const b = bounds ??
|
|
35
|
+
const b = bounds ?? getCircleBrushOrPencilBounds(
|
|
36
36
|
centerX,
|
|
37
37
|
centerY,
|
|
38
38
|
brushSize,
|
|
39
39
|
targetWidth,
|
|
40
|
-
targetHeight
|
|
40
|
+
targetHeight,
|
|
41
41
|
)
|
|
42
42
|
|
|
43
43
|
if (b.w <= 0 || b.h <= 0) return
|
|
@@ -48,8 +48,6 @@ export function applyCircleBrushToPixelData(
|
|
|
48
48
|
const invR = 1 / r
|
|
49
49
|
|
|
50
50
|
const centerOffset = (brushSize % 2 === 0) ? 0.5 : 0
|
|
51
|
-
const baseColor = color & 0x00ffffff
|
|
52
|
-
const constantSrc = ((alpha << 24) | baseColor) >>> 0 as Color32
|
|
53
51
|
|
|
54
52
|
const endX = b.x + b.w
|
|
55
53
|
const endY = b.y + b.h
|
|
@@ -57,6 +55,10 @@ export function applyCircleBrushToPixelData(
|
|
|
57
55
|
// Anchor the math to the floor of the center for exact pixel art parity
|
|
58
56
|
const fCenterX = Math.floor(centerX)
|
|
59
57
|
const fCenterY = Math.floor(centerY)
|
|
58
|
+
const baseSrcAlpha = (color >>> 24)
|
|
59
|
+
const colorRGB = color & 0x00ffffff
|
|
60
|
+
const isOpaque = alpha === 255
|
|
61
|
+
const isOverwrite = (blendFn as any).isOverwrite
|
|
60
62
|
|
|
61
63
|
for (let cy = b.y; cy < endY; cy++) {
|
|
62
64
|
const relY = (cy - fCenterY) + centerOffset
|
|
@@ -69,56 +71,29 @@ export function applyCircleBrushToPixelData(
|
|
|
69
71
|
|
|
70
72
|
if (dSqr <= rSqr) {
|
|
71
73
|
const idx = rowOffset + cx
|
|
74
|
+
let weight = alpha
|
|
72
75
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const src = ((fAlpha << 24) | baseColor) >>> 0 as Color32
|
|
77
|
-
data32[idx] = blendFn(src, data32[idx] as Color32)
|
|
78
|
-
} else {
|
|
79
|
-
data32[idx] = blendFn(constantSrc, data32[idx] as Color32)
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
76
|
+
const strength = fallOff(1 - (Math.sqrt(dSqr) * invR))
|
|
77
|
+
const maskVal = (strength * 255) | 0
|
|
78
|
+
if (maskVal === 0) continue
|
|
85
79
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
out?: Rect,
|
|
93
|
-
): Rect {
|
|
94
|
-
const r = brushSize / 2
|
|
95
|
-
|
|
96
|
-
// These offsets match your getPerfectCircleCoords exactly
|
|
97
|
-
const minOffset = -Math.ceil(r - 0.5)
|
|
98
|
-
const maxOffset = Math.floor(r - 0.5)
|
|
80
|
+
// Match Blitter's weight calculation exactly
|
|
81
|
+
if (isOpaque) {
|
|
82
|
+
weight = maskVal
|
|
83
|
+
} else if (maskVal !== 255) {
|
|
84
|
+
weight = (maskVal * alpha + 128) >> 8
|
|
85
|
+
}
|
|
99
86
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
87
|
+
// Match Blitter's final color calculation exactly
|
|
88
|
+
let finalCol = color
|
|
89
|
+
if (weight < 255) {
|
|
90
|
+
const a = (baseSrcAlpha * weight + 128) >> 8
|
|
91
|
+
if (a === 0 && !isOverwrite) continue
|
|
92
|
+
finalCol = (colorRGB | (a << 24)) >>> 0 as Color32
|
|
93
|
+
}
|
|
105
94
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
w: 0,
|
|
110
|
-
h: 0,
|
|
95
|
+
data32[idx] = blendFn(finalCol, data32[idx] as Color32)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
111
98
|
}
|
|
112
|
-
|
|
113
|
-
const cStartX = targetWidth !== undefined ? Math.max(0, startX) : startX
|
|
114
|
-
const cStartY = targetHeight !== undefined ? Math.max(0, startY) : startY
|
|
115
|
-
const cEndX = targetWidth !== undefined ? Math.min(targetWidth, endX) : endX
|
|
116
|
-
const cEndY = targetHeight !== undefined ? Math.min(targetHeight, endY) : endY
|
|
117
|
-
|
|
118
|
-
res.x = cStartX
|
|
119
|
-
res.y = cStartY
|
|
120
|
-
res.w = Math.max(0, cEndX - cStartX)
|
|
121
|
-
res.h = Math.max(0, cEndY - cStartY)
|
|
122
|
-
|
|
123
|
-
return res
|
|
124
99
|
}
|
|
@@ -1,117 +1,85 @@
|
|
|
1
|
-
import type { BlendColor32, Color32, Rect } from '../_types'
|
|
1
|
+
import type { BlendColor32, Color32, IPixelData, Rect } from '../_types'
|
|
2
2
|
import { sourceOverPerfect } from '../BlendModes/blend-modes-perfect'
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Applies a rectangular brush to pixel data, blending a color with optional falloff.
|
|
7
|
-
*
|
|
8
|
-
* @param target The PixelData to modify.
|
|
9
|
-
* @param color The brush color.
|
|
10
|
-
* @param centerX The center x-coordinate of the brush.
|
|
11
|
-
* @param centerY The center y-coordinate of the brush.
|
|
12
|
-
* @param brushWidth
|
|
13
|
-
* @param brushHeight
|
|
14
|
-
* @param alpha The overall opacity of the brush (0-255).
|
|
15
|
-
* @default 255
|
|
16
|
-
* @param fallOff A function that returns an alpha multiplier (0-1) based on the normalized distance (0-1) from the circle's center.
|
|
17
|
-
* @param blendFn
|
|
18
|
-
* @param bounds precalculated result from {@link getRectBrushBounds}
|
|
19
|
-
* @default sourceOverPerfect
|
|
20
|
-
*/
|
|
3
|
+
import { getRectBrushOrPencilBounds } from '../Rect/getRectBrushOrPencilBounds'
|
|
4
|
+
|
|
21
5
|
export function applyRectBrushToPixelData(
|
|
22
|
-
target:
|
|
6
|
+
target: IPixelData,
|
|
23
7
|
color: Color32,
|
|
24
8
|
centerX: number,
|
|
25
9
|
centerY: number,
|
|
26
10
|
brushWidth: number,
|
|
27
11
|
brushHeight: number,
|
|
28
12
|
alpha = 255,
|
|
29
|
-
fallOff
|
|
13
|
+
fallOff: (dist: number) => number,
|
|
30
14
|
blendFn: BlendColor32 = sourceOverPerfect,
|
|
31
15
|
bounds?: Rect,
|
|
32
16
|
): void {
|
|
33
17
|
const targetWidth = target.width
|
|
34
18
|
const targetHeight = target.height
|
|
35
19
|
|
|
36
|
-
|
|
37
|
-
const b = bounds ?? getRectBrushBounds(
|
|
20
|
+
const b = bounds ?? getRectBrushOrPencilBounds(
|
|
38
21
|
centerX,
|
|
39
22
|
centerY,
|
|
40
23
|
brushWidth,
|
|
41
24
|
brushHeight,
|
|
42
25
|
targetWidth,
|
|
43
|
-
targetHeight
|
|
26
|
+
targetHeight,
|
|
44
27
|
)
|
|
45
28
|
|
|
46
29
|
if (b.w <= 0 || b.h <= 0) return
|
|
47
30
|
|
|
48
31
|
const data32 = target.data32
|
|
49
32
|
const baseColor = color & 0x00ffffff
|
|
50
|
-
const
|
|
33
|
+
const baseSrcAlpha = color >>> 24
|
|
34
|
+
const isOpaque = alpha === 255
|
|
51
35
|
|
|
52
36
|
const invHalfW = 1 / (brushWidth / 2)
|
|
53
37
|
const invHalfH = 1 / (brushHeight / 2)
|
|
38
|
+
|
|
39
|
+
// Restore the pixel-art centering logic
|
|
40
|
+
const centerOffsetX = (brushWidth % 2 === 0) ? 0.5 : 0
|
|
41
|
+
const centerOffsetY = (brushHeight % 2 === 0) ? 0.5 : 0
|
|
42
|
+
const fCenterX = Math.floor(centerX)
|
|
43
|
+
const fCenterY = Math.floor(centerY)
|
|
44
|
+
|
|
54
45
|
const endX = b.x + b.w
|
|
55
46
|
const endY = b.y + b.h
|
|
47
|
+
const isOverwrite = (blendFn as any).isOverwrite
|
|
56
48
|
|
|
57
49
|
for (let py = b.y; py < endY; py++) {
|
|
58
50
|
const rowOffset = py * targetWidth
|
|
59
|
-
|
|
60
|
-
// Y-distance check for falloff (center of pixel to center of brush)
|
|
61
|
-
const dy = fallOff ? Math.abs(py + 0.5 - centerY) * invHalfH : 0
|
|
51
|
+
const dy = Math.abs((py - fCenterY) + centerOffsetY) * invHalfH
|
|
62
52
|
|
|
63
53
|
for (let px = b.x; px < endX; px++) {
|
|
64
54
|
const idx = rowOffset + px
|
|
65
55
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
56
|
+
const dx = Math.abs((px - fCenterX) + centerOffsetX) * invHalfW
|
|
57
|
+
const dist = dx > dy ? dx : dy
|
|
58
|
+
|
|
59
|
+
const strength = fallOff(dist)
|
|
60
|
+
const maskVal = (strength * 255) | 0
|
|
69
61
|
|
|
70
|
-
|
|
71
|
-
const fAlpha = (alpha * strength) | 0
|
|
72
|
-
const src = ((fAlpha << 24) | baseColor) >>> 0 as Color32
|
|
62
|
+
if (maskVal <= 0) continue
|
|
73
63
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
64
|
+
let weight = alpha
|
|
65
|
+
|
|
66
|
+
if (isOpaque) {
|
|
67
|
+
weight = maskVal
|
|
68
|
+
} else if (maskVal !== 255) {
|
|
69
|
+
weight = (maskVal * alpha + 128) >> 8
|
|
77
70
|
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
71
|
|
|
82
|
-
|
|
83
|
-
centerX: number,
|
|
84
|
-
centerY: number,
|
|
85
|
-
brushWidth: number,
|
|
86
|
-
brushHeight: number,
|
|
87
|
-
targetWidth?: number,
|
|
88
|
-
targetHeight?: number,
|
|
89
|
-
out?: Rect,
|
|
90
|
-
): Rect {
|
|
91
|
-
const startX = Math.floor(centerX - brushWidth / 2)
|
|
92
|
-
const startY = Math.floor(centerY - brushHeight / 2)
|
|
93
|
-
const endX = startX + brushWidth
|
|
94
|
-
const endY = startY + brushHeight
|
|
95
|
-
|
|
96
|
-
const res = out ?? {
|
|
97
|
-
x: 0,
|
|
98
|
-
y: 0,
|
|
99
|
-
w: 0,
|
|
100
|
-
h: 0,
|
|
101
|
-
}
|
|
72
|
+
let finalCol = color
|
|
102
73
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const cEndX = targetWidth !== undefined ? Math.min(targetWidth, endX) : endX
|
|
106
|
-
const cEndY = targetHeight !== undefined ? Math.min(targetHeight, endY) : endY
|
|
74
|
+
if (weight < 255) {
|
|
75
|
+
const a = (baseSrcAlpha * weight + 128) >> 8
|
|
107
76
|
|
|
108
|
-
|
|
109
|
-
const h = cEndY - cStartY
|
|
77
|
+
if (a === 0 && !isOverwrite) continue
|
|
110
78
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
res.w = w < 0 ? 0 : w
|
|
114
|
-
res.h = h < 0 ? 0 : h
|
|
79
|
+
finalCol = ((a << 24) | baseColor) >>> 0 as Color32
|
|
80
|
+
}
|
|
115
81
|
|
|
116
|
-
|
|
82
|
+
data32[idx] = blendFn(finalCol, data32[idx] as Color32)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
117
85
|
}
|