pixel-data-js 0.23.0 → 0.24.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 +1024 -596
- package/dist/index.dev.cjs.map +1 -1
- package/dist/index.dev.js +1010 -592
- package/dist/index.dev.js.map +1 -1
- package/dist/index.prod.cjs +1024 -596
- package/dist/index.prod.cjs.map +1 -1
- package/dist/index.prod.d.ts +280 -165
- package/dist/index.prod.js +1010 -592
- package/dist/index.prod.js.map +1 -1
- package/package.json +3 -2
- package/src/Canvas/CanvasFrameRenderer.ts +57 -0
- package/src/Canvas/ReusableCanvas.ts +60 -11
- package/src/History/HistoryAction.ts +38 -0
- package/src/History/HistoryManager.ts +4 -8
- package/src/History/PixelAccumulator.ts +95 -80
- package/src/History/PixelEngineConfig.ts +18 -6
- package/src/History/PixelMutator/mutatorApplyAlphaMask.ts +6 -6
- package/src/History/PixelMutator/mutatorApplyBinaryMask.ts +6 -6
- package/src/History/PixelMutator/mutatorApplyCircleBrushStroke.ts +6 -5
- package/src/History/PixelMutator/mutatorApplyCirclePencil.ts +22 -22
- package/src/History/PixelMutator/mutatorApplyCirclePencilStroke.ts +6 -5
- package/src/History/PixelMutator/mutatorApplyRectBrush.ts +19 -19
- package/src/History/PixelMutator/mutatorApplyRectBrushStroke.ts +6 -4
- package/src/History/PixelMutator/mutatorApplyRectPencil.ts +20 -20
- package/src/History/PixelMutator/mutatorApplyRectPencilStroke.ts +6 -4
- package/src/History/PixelMutator/mutatorBlendColor.ts +8 -5
- package/src/History/PixelMutator/mutatorBlendColorCircleMask.ts +71 -0
- package/src/History/PixelMutator/mutatorBlendPixel.ts +22 -26
- package/src/History/PixelMutator/mutatorBlendPixelData.ts +5 -3
- package/src/History/PixelMutator/mutatorBlendPixelDataAlphaMask.ts +5 -3
- package/src/History/PixelMutator/mutatorBlendPixelDataBinaryMask.ts +5 -3
- package/src/History/PixelMutator/mutatorClear.ts +7 -6
- package/src/History/PixelMutator/mutatorFill.ts +34 -9
- package/src/History/PixelMutator/mutatorFillBinaryMask.ts +4 -2
- package/src/History/PixelMutator/mutatorInvert.ts +8 -4
- package/src/History/PixelMutator.ts +4 -3
- package/src/History/PixelPatchTiles.ts +3 -15
- package/src/History/PixelWriter.ts +29 -33
- package/src/ImageData/ReusableImageData.ts +3 -5
- package/src/Mask/{CircleBrushAlphaMask.ts → CircleAlphaMask.ts} +2 -2
- package/src/Mask/{CircleBrushBinaryMask.ts → CircleBinaryMask.ts} +2 -2
- package/src/PixelData/PixelData.ts +1 -27
- package/src/PixelData/applyAlphaMaskToPixelData.ts +19 -9
- package/src/PixelData/applyBinaryMaskToPixelData.ts +24 -17
- package/src/PixelData/applyRectBrushToPixelData.ts +18 -5
- package/src/PixelData/blendColorPixelData.ts +31 -7
- package/src/PixelData/blendColorPixelDataAlphaMask.ts +16 -6
- package/src/PixelData/blendColorPixelDataBinaryMask.ts +16 -7
- package/src/PixelData/{applyCircleBrushToPixelData.ts → blendColorPixelDataCircleMask.ts} +11 -10
- package/src/PixelData/blendPixel.ts +47 -0
- package/src/PixelData/blendPixelData.ts +14 -4
- package/src/PixelData/blendPixelDataAlphaMask.ts +12 -4
- package/src/PixelData/blendPixelDataBinaryMask.ts +13 -4
- package/src/PixelData/blendPixelDataPaintBuffer.ts +37 -0
- package/src/PixelData/clearPixelData.ts +2 -2
- package/src/PixelData/fillPixelData.ts +26 -16
- package/src/PixelData/fillPixelDataBinaryMask.ts +12 -4
- package/src/PixelData/fillPixelDataFast.ts +94 -0
- package/src/PixelData/invertPixelData.ts +4 -2
- package/src/PixelTile/PaintBuffer.ts +122 -0
- package/src/PixelTile/PaintBufferRenderer.ts +40 -0
- package/src/PixelTile/PixelTile.ts +21 -0
- package/src/PixelTile/PixelTilePool.ts +63 -0
- package/src/_types.ts +9 -9
- package/src/index.ts +16 -6
- package/src/History/PixelMutator/mutatorApplyCircleBrush.ts +0 -78
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { mutatorApplyAlphaMask } from './PixelMutator/mutatorApplyAlphaMask'
|
|
2
2
|
import { mutatorApplyBinaryMask } from './PixelMutator/mutatorApplyBinaryMask'
|
|
3
|
-
import { mutatorApplyCircleBrush } from './PixelMutator/mutatorApplyCircleBrush'
|
|
4
3
|
import { mutatorApplyCircleBrushStroke } from './PixelMutator/mutatorApplyCircleBrushStroke'
|
|
4
|
+
import { mutatorBlendColorCircleMask } from './PixelMutator/mutatorBlendColorCircleMask'
|
|
5
5
|
import { mutatorApplyCirclePencil } from './PixelMutator/mutatorApplyCirclePencil'
|
|
6
6
|
import { mutatorApplyCirclePencilStroke } from './PixelMutator/mutatorApplyCirclePencilStroke'
|
|
7
7
|
import { mutatorApplyRectBrush } from './PixelMutator/mutatorApplyRectBrush'
|
|
@@ -14,7 +14,7 @@ import { mutatorBlendPixelData } from './PixelMutator/mutatorBlendPixelData'
|
|
|
14
14
|
import { mutatorBlendPixelDataAlphaMask } from './PixelMutator/mutatorBlendPixelDataAlphaMask'
|
|
15
15
|
import { mutatorBlendPixelDataBinaryMask } from './PixelMutator/mutatorBlendPixelDataBinaryMask'
|
|
16
16
|
import { mutatorClear } from './PixelMutator/mutatorClear'
|
|
17
|
-
import { mutatorFill } from './PixelMutator/mutatorFill'
|
|
17
|
+
import { mutatorFill, mutatorFillRect } from './PixelMutator/mutatorFill'
|
|
18
18
|
import { mutatorFillBinaryMask } from './PixelMutator/mutatorFillBinaryMask'
|
|
19
19
|
import { mutatorInvert } from './PixelMutator/mutatorInvert'
|
|
20
20
|
import type { PixelWriter } from './PixelWriter'
|
|
@@ -24,7 +24,6 @@ export function makeFullPixelMutator(writer: PixelWriter<any>) {
|
|
|
24
24
|
// @sort
|
|
25
25
|
...mutatorApplyAlphaMask(writer),
|
|
26
26
|
...mutatorApplyBinaryMask(writer),
|
|
27
|
-
...mutatorApplyCircleBrush(writer),
|
|
28
27
|
...mutatorApplyCircleBrushStroke(writer),
|
|
29
28
|
...mutatorApplyCirclePencil(writer),
|
|
30
29
|
...mutatorApplyCirclePencilStroke(writer),
|
|
@@ -33,6 +32,7 @@ export function makeFullPixelMutator(writer: PixelWriter<any>) {
|
|
|
33
32
|
...mutatorApplyRectPencil(writer),
|
|
34
33
|
...mutatorApplyRectPencilStroke(writer),
|
|
35
34
|
...mutatorBlendColor(writer),
|
|
35
|
+
...mutatorBlendColorCircleMask(writer),
|
|
36
36
|
...mutatorBlendPixel(writer),
|
|
37
37
|
...mutatorBlendPixelData(writer),
|
|
38
38
|
...mutatorBlendPixelDataAlphaMask(writer),
|
|
@@ -40,6 +40,7 @@ export function makeFullPixelMutator(writer: PixelWriter<any>) {
|
|
|
40
40
|
...mutatorClear(writer),
|
|
41
41
|
...mutatorFill(writer),
|
|
42
42
|
...mutatorFillBinaryMask(writer),
|
|
43
|
+
...mutatorFillRect(writer),
|
|
43
44
|
...mutatorInvert(writer),
|
|
44
45
|
}
|
|
45
46
|
}
|
|
@@ -1,24 +1,12 @@
|
|
|
1
1
|
import type { IPixelData } from '../_types'
|
|
2
|
+
import { PixelTile } from '../PixelTile/PixelTile'
|
|
2
3
|
|
|
3
4
|
export type PixelPatchTiles = {
|
|
4
5
|
beforeTiles: PixelTile[]
|
|
5
6
|
afterTiles: PixelTile[]
|
|
6
7
|
}
|
|
7
8
|
|
|
8
|
-
export
|
|
9
|
-
public data32: Uint32Array
|
|
10
|
-
|
|
11
|
-
constructor(
|
|
12
|
-
public id: number,
|
|
13
|
-
public tx: number,
|
|
14
|
-
public ty: number,
|
|
15
|
-
tileArea: number,
|
|
16
|
-
) {
|
|
17
|
-
this.data32 = new Uint32Array(tileArea)
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function applyPatchTiles(target: IPixelData, tiles: PixelTile[], tileSize: number = 256) {
|
|
9
|
+
export function applyPatchTiles(target: IPixelData, tiles: PixelTile[], tileSize: number) {
|
|
22
10
|
for (let i = 0; i < tiles.length; i++) {
|
|
23
11
|
const tile = tiles[i]
|
|
24
12
|
|
|
@@ -34,7 +22,7 @@ export function applyPatchTiles(target: IPixelData, tiles: PixelTile[], tileSize
|
|
|
34
22
|
// Calculate clamping to prevent wrapping artifacts on image edges
|
|
35
23
|
const copyWidth = Math.max(0, Math.min(tileSize, dstWidth - startX))
|
|
36
24
|
|
|
37
|
-
if (copyWidth <= 0)
|
|
25
|
+
if (copyWidth <= 0) continue
|
|
38
26
|
|
|
39
27
|
for (let ly = 0; ly < tileSize; ly++) {
|
|
40
28
|
const globalY = startY + ly
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import type { IPixelData } from '../_types'
|
|
2
|
-
import { type
|
|
2
|
+
import { type HistoryActionFactory, makeHistoryAction } from './HistoryAction'
|
|
3
|
+
import { HistoryManager } from './HistoryManager'
|
|
3
4
|
import { PixelAccumulator } from './PixelAccumulator'
|
|
4
5
|
import { PixelEngineConfig } from './PixelEngineConfig'
|
|
5
|
-
import {
|
|
6
|
+
import { PixelTilePool } from '../PixelTile/PixelTilePool'
|
|
6
7
|
|
|
7
8
|
export interface PixelWriterOptions {
|
|
8
9
|
maxHistorySteps?: number
|
|
9
10
|
tileSize?: number
|
|
10
11
|
historyManager?: HistoryManager
|
|
12
|
+
historyActionFactory?: HistoryActionFactory
|
|
13
|
+
pixelTilePool?: PixelTilePool,
|
|
11
14
|
}
|
|
12
15
|
|
|
13
16
|
/**
|
|
@@ -32,52 +35,45 @@ export interface PixelWriterOptions {
|
|
|
32
35
|
* })
|
|
33
36
|
*/
|
|
34
37
|
export class PixelWriter<M> {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
readonly historyManager: HistoryManager
|
|
39
|
+
readonly accumulator: PixelAccumulator
|
|
40
|
+
readonly historyActionFactory: HistoryActionFactory
|
|
41
|
+
readonly config: PixelEngineConfig
|
|
39
42
|
readonly mutator: M
|
|
40
43
|
|
|
41
44
|
constructor(target: IPixelData, mutatorFactory: (writer: PixelWriter<any>) => M, {
|
|
42
45
|
tileSize = 256,
|
|
43
46
|
maxHistorySteps = 50,
|
|
44
47
|
historyManager = new HistoryManager(maxHistorySteps),
|
|
48
|
+
historyActionFactory = makeHistoryAction,
|
|
49
|
+
pixelTilePool,
|
|
45
50
|
}: PixelWriterOptions = {}) {
|
|
46
|
-
this.
|
|
47
|
-
this.config = new PixelEngineConfig(tileSize)
|
|
51
|
+
this.config = new PixelEngineConfig(tileSize, target)
|
|
48
52
|
this.historyManager = historyManager
|
|
49
|
-
|
|
53
|
+
pixelTilePool ??= new PixelTilePool(this.config)
|
|
54
|
+
this.accumulator = new PixelAccumulator(this.config, pixelTilePool)
|
|
55
|
+
this.historyActionFactory = historyActionFactory
|
|
50
56
|
this.mutator = mutatorFactory(this)
|
|
51
57
|
}
|
|
52
58
|
|
|
53
|
-
withHistory(
|
|
54
|
-
cb(
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const patch: PixelPatchTiles = {
|
|
66
|
-
beforeTiles: beforeTiles,
|
|
67
|
-
afterTiles: afterTiles,
|
|
59
|
+
withHistory(
|
|
60
|
+
cb: (mutator: M) => void,
|
|
61
|
+
after?: () => void,
|
|
62
|
+
afterUndo?: () => void,
|
|
63
|
+
afterRedo?: () => void,
|
|
64
|
+
) {
|
|
65
|
+
try {
|
|
66
|
+
cb(this.mutator)
|
|
67
|
+
} catch (e) {
|
|
68
|
+
this.accumulator.rollback()
|
|
69
|
+
throw e
|
|
68
70
|
}
|
|
69
71
|
|
|
70
|
-
|
|
71
|
-
const tileSize = this.config.tileSize
|
|
72
|
-
const accumulator = this.accumulator
|
|
72
|
+
if (this.accumulator.beforeTiles.length === 0) return
|
|
73
73
|
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
redo: () => applyPatchTiles(target, patch.afterTiles, tileSize),
|
|
77
|
-
dispose: () => accumulator.recyclePatch(patch),
|
|
78
|
-
}
|
|
74
|
+
const patch = this.accumulator.extractPatch()
|
|
75
|
+
const action = this.historyActionFactory(this, patch, after, afterUndo, afterRedo)
|
|
79
76
|
|
|
80
77
|
this.historyManager.commit(action)
|
|
81
|
-
this.accumulator.reset()
|
|
82
78
|
}
|
|
83
79
|
}
|
|
@@ -17,12 +17,10 @@ export function makeReusableImageData() {
|
|
|
17
17
|
* @returns The cached or newly allocated ImageData object.
|
|
18
18
|
*/
|
|
19
19
|
return function getReusableImageData(width: number, height: number) {
|
|
20
|
-
|
|
21
|
-
const widthMatches = hasInstance && imageData!.width === width
|
|
22
|
-
const heightMatches = hasInstance && imageData!.height === height
|
|
23
|
-
|
|
24
|
-
if (!widthMatches || !heightMatches) {
|
|
20
|
+
if (imageData === null || imageData.width !== width || imageData.height !== height) {
|
|
25
21
|
imageData = new ImageData(width, height)
|
|
22
|
+
} else {
|
|
23
|
+
imageData.data.fill(0)
|
|
26
24
|
}
|
|
27
25
|
|
|
28
26
|
return imageData!
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type CircleAlphaMask, MaskType } from '../_types'
|
|
2
2
|
|
|
3
|
-
export function
|
|
3
|
+
export function makeCircleAlphaMask(size: number, fallOff: (d: number) => number = () => 1): CircleAlphaMask {
|
|
4
4
|
const area = size * size
|
|
5
5
|
const data = new Uint8Array(area)
|
|
6
6
|
const radius = size / 2
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type CircleBinaryMask, MaskType } from '../_types'
|
|
2
2
|
|
|
3
|
-
export function
|
|
3
|
+
export function makeCircleBinaryMask(size: number): CircleBinaryMask {
|
|
4
4
|
const area = size * size
|
|
5
5
|
const data = new Uint8Array(area)
|
|
6
6
|
const radius = size / 2
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ImageDataLike,
|
|
1
|
+
import type { ImageDataLike, IPixelData } from '../_types'
|
|
2
2
|
import { imageDataToUInt32Array } from '../ImageData/imageDataToUInt32Array'
|
|
3
3
|
|
|
4
4
|
export class PixelData<T extends ImageDataLike = ImageData> implements IPixelData {
|
|
@@ -20,30 +20,4 @@ export class PixelData<T extends ImageDataLike = ImageData> implements IPixelDat
|
|
|
20
20
|
;(this as any).width = imageData.width
|
|
21
21
|
;(this as any).height = imageData.height
|
|
22
22
|
}
|
|
23
|
-
|
|
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)
|
|
48
|
-
}
|
|
49
23
|
}
|
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import { type AlphaMask, type ApplyMaskToPixelDataOptions, type IPixelData } from '../_types'
|
|
1
|
+
import { type AlphaMask, type ApplyMaskToPixelDataOptions, type Color32, type IPixelData } from '../_types'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Directly applies a mask to a region of PixelData,
|
|
5
5
|
* modifying the destination's alpha channel in-place.
|
|
6
|
+
* @returns true if any pixels were actually modified.
|
|
6
7
|
*/
|
|
7
8
|
export function applyAlphaMaskToPixelData(
|
|
8
9
|
dst: IPixelData,
|
|
9
10
|
mask: AlphaMask,
|
|
10
11
|
opts: ApplyMaskToPixelDataOptions = {},
|
|
11
|
-
):
|
|
12
|
+
): boolean {
|
|
12
13
|
const {
|
|
13
14
|
x: targetX = 0,
|
|
14
15
|
y: targetY = 0,
|
|
@@ -20,7 +21,7 @@ export function applyAlphaMaskToPixelData(
|
|
|
20
21
|
invertMask = false,
|
|
21
22
|
} = opts
|
|
22
23
|
|
|
23
|
-
if (globalAlpha === 0) return
|
|
24
|
+
if (globalAlpha === 0) return false
|
|
24
25
|
|
|
25
26
|
// 1. Initial Destination Clipping
|
|
26
27
|
let x = targetX
|
|
@@ -41,12 +42,12 @@ export function applyAlphaMaskToPixelData(
|
|
|
41
42
|
w = Math.min(w, dst.width - x)
|
|
42
43
|
h = Math.min(h, dst.height - y)
|
|
43
44
|
|
|
44
|
-
if (w <= 0) return
|
|
45
|
-
if (h <= 0) return
|
|
45
|
+
if (w <= 0) return false
|
|
46
|
+
if (h <= 0) return false
|
|
46
47
|
|
|
47
48
|
// 2. Determine Source Dimensions
|
|
48
49
|
const mPitch = mask.w
|
|
49
|
-
if (mPitch <= 0) return
|
|
50
|
+
if (mPitch <= 0) return false
|
|
50
51
|
|
|
51
52
|
// 3. Source Bounds Clipping
|
|
52
53
|
// Calculate where we would start reading in the mask
|
|
@@ -63,8 +64,8 @@ export function applyAlphaMaskToPixelData(
|
|
|
63
64
|
const finalH = sY1 - sY0
|
|
64
65
|
|
|
65
66
|
// This is where your failing tests are now caught
|
|
66
|
-
if (finalW <= 0) return
|
|
67
|
-
if (finalH <= 0) return
|
|
67
|
+
if (finalW <= 0) return false
|
|
68
|
+
if (finalH <= 0) return false
|
|
68
69
|
|
|
69
70
|
// 4. Align Destination with Source Clipping
|
|
70
71
|
// If the source was clipped on the top/left, we must shift the destination start
|
|
@@ -80,6 +81,7 @@ export function applyAlphaMaskToPixelData(
|
|
|
80
81
|
let dIdx = (y + yShift) * dw + (x + xShift)
|
|
81
82
|
let mIdx = sY0 * mPitch + sX0
|
|
82
83
|
|
|
84
|
+
let didChange = false
|
|
83
85
|
for (let iy = 0; iy < h; iy++) {
|
|
84
86
|
for (let ix = 0; ix < w; ix++) {
|
|
85
87
|
const mVal = maskData[mIdx]
|
|
@@ -101,6 +103,7 @@ export function applyAlphaMaskToPixelData(
|
|
|
101
103
|
if (weight === 0) {
|
|
102
104
|
// Clear alpha channel
|
|
103
105
|
dst32[dIdx] = (dst32[dIdx] & 0x00ffffff) >>> 0
|
|
106
|
+
didChange = true
|
|
104
107
|
} else if (weight !== 255) {
|
|
105
108
|
// Merge alpha channel
|
|
106
109
|
const d = dst32[dIdx]
|
|
@@ -108,7 +111,13 @@ export function applyAlphaMaskToPixelData(
|
|
|
108
111
|
|
|
109
112
|
if (da !== 0) {
|
|
110
113
|
const finalAlpha = da === 255 ? weight : (da * weight + 128) >> 8
|
|
111
|
-
|
|
114
|
+
|
|
115
|
+
const current = dst32[dIdx] as Color32
|
|
116
|
+
const next = ((d & 0x00ffffff) | (finalAlpha << 24)) >>> 0
|
|
117
|
+
if (current !== next) {
|
|
118
|
+
dst32[dIdx] = next
|
|
119
|
+
didChange = true
|
|
120
|
+
}
|
|
112
121
|
}
|
|
113
122
|
}
|
|
114
123
|
|
|
@@ -119,4 +128,5 @@ export function applyAlphaMaskToPixelData(
|
|
|
119
128
|
dIdx += dStride
|
|
120
129
|
mIdx += mStride
|
|
121
130
|
}
|
|
131
|
+
return didChange
|
|
122
132
|
}
|
|
@@ -3,12 +3,13 @@ import { type ApplyMaskToPixelDataOptions, type BinaryMask, type IPixelData } fr
|
|
|
3
3
|
/**
|
|
4
4
|
* Directly applies a mask to a region of PixelData,
|
|
5
5
|
* modifying the destination's alpha channel in-place.
|
|
6
|
+
* @returns true if any pixels were actually modified.
|
|
6
7
|
*/
|
|
7
8
|
export function applyBinaryMaskToPixelData(
|
|
8
9
|
dst: IPixelData,
|
|
9
10
|
mask: BinaryMask,
|
|
10
11
|
opts: ApplyMaskToPixelDataOptions = {},
|
|
11
|
-
):
|
|
12
|
+
): boolean {
|
|
12
13
|
const {
|
|
13
14
|
x: targetX = 0,
|
|
14
15
|
y: targetY = 0,
|
|
@@ -20,9 +21,8 @@ export function applyBinaryMaskToPixelData(
|
|
|
20
21
|
invertMask = false,
|
|
21
22
|
} = opts
|
|
22
23
|
|
|
23
|
-
if (globalAlpha === 0) return
|
|
24
|
+
if (globalAlpha === 0) return false
|
|
24
25
|
|
|
25
|
-
// 1. Initial Destination Clipping
|
|
26
26
|
let x = targetX
|
|
27
27
|
let y = targetY
|
|
28
28
|
let w = width
|
|
@@ -41,12 +41,10 @@ export function applyBinaryMaskToPixelData(
|
|
|
41
41
|
w = Math.min(w, dst.width - x)
|
|
42
42
|
h = Math.min(h, dst.height - y)
|
|
43
43
|
|
|
44
|
-
if (w <= 0) return
|
|
45
|
-
if (h <= 0) return
|
|
44
|
+
if (w <= 0 || h <= 0) return false
|
|
46
45
|
|
|
47
|
-
// 2. Determine Source Dimensions
|
|
48
46
|
const mPitch = mask.w
|
|
49
|
-
if (mPitch <= 0) return
|
|
47
|
+
if (mPitch <= 0) return false
|
|
50
48
|
|
|
51
49
|
// 3. Source Bounds Clipping
|
|
52
50
|
// Calculate where we would start reading in the mask
|
|
@@ -62,9 +60,9 @@ export function applyBinaryMaskToPixelData(
|
|
|
62
60
|
const finalW = sX1 - sX0
|
|
63
61
|
const finalH = sY1 - sY0
|
|
64
62
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
63
|
+
if (finalW <= 0 || finalH <= 0) {
|
|
64
|
+
return false
|
|
65
|
+
}
|
|
68
66
|
|
|
69
67
|
// 4. Align Destination with Source Clipping
|
|
70
68
|
// If the source was clipped on the top/left, we must shift the destination start
|
|
@@ -79,24 +77,31 @@ export function applyBinaryMaskToPixelData(
|
|
|
79
77
|
|
|
80
78
|
let dIdx = (y + yShift) * dw + (x + xShift)
|
|
81
79
|
let mIdx = sY0 * mPitch + sX0
|
|
80
|
+
let didChange = false
|
|
82
81
|
|
|
83
|
-
for (let iy = 0; iy <
|
|
84
|
-
for (let ix = 0; ix <
|
|
82
|
+
for (let iy = 0; iy < finalH; iy++) {
|
|
83
|
+
for (let ix = 0; ix < finalW; ix++) {
|
|
85
84
|
const mVal = maskData[mIdx]
|
|
86
|
-
// Consistently determines if this pixel should be "masked out" (cleared)
|
|
87
85
|
const isMaskedOut = invertMask ? mVal !== 0 : mVal === 0
|
|
88
86
|
|
|
89
87
|
if (isMaskedOut) {
|
|
90
|
-
|
|
91
|
-
|
|
88
|
+
const current = dst32[dIdx]
|
|
89
|
+
const next = (current & 0x00ffffff) >>> 0
|
|
90
|
+
if (current !== next) {
|
|
91
|
+
dst32[dIdx] = next
|
|
92
|
+
didChange = true
|
|
93
|
+
}
|
|
92
94
|
} else if (globalAlpha !== 255) {
|
|
93
95
|
const d = dst32[dIdx]
|
|
94
96
|
const da = d >>> 24
|
|
95
97
|
|
|
96
|
-
// If pixel isn't already fully transparent, apply global alpha
|
|
97
98
|
if (da !== 0) {
|
|
98
99
|
const finalAlpha = da === 255 ? globalAlpha : (da * globalAlpha + 128) >> 8
|
|
99
|
-
|
|
100
|
+
const next = ((d & 0x00ffffff) | (finalAlpha << 24)) >>> 0
|
|
101
|
+
if (d !== next) {
|
|
102
|
+
dst32[dIdx] = next
|
|
103
|
+
didChange = true
|
|
104
|
+
}
|
|
100
105
|
}
|
|
101
106
|
}
|
|
102
107
|
|
|
@@ -107,4 +112,6 @@ export function applyBinaryMaskToPixelData(
|
|
|
107
112
|
dIdx += dStride
|
|
108
113
|
mIdx += mStride
|
|
109
114
|
}
|
|
115
|
+
|
|
116
|
+
return didChange
|
|
110
117
|
}
|
|
@@ -2,6 +2,10 @@ import type { BlendColor32, Color32, IPixelData, Rect } from '../_types'
|
|
|
2
2
|
import { sourceOverPerfect } from '../BlendModes/blend-modes-perfect'
|
|
3
3
|
import { getRectBrushOrPencilBounds } from '../Rect/getRectBrushOrPencilBounds'
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Applies a rectangular brush to the pixel data.
|
|
7
|
+
* @returns true if any pixels were actually modified.
|
|
8
|
+
*/
|
|
5
9
|
export function applyRectBrushToPixelData(
|
|
6
10
|
target: IPixelData,
|
|
7
11
|
color: Color32,
|
|
@@ -13,7 +17,7 @@ export function applyRectBrushToPixelData(
|
|
|
13
17
|
fallOff: (dist: number) => number,
|
|
14
18
|
blendFn: BlendColor32 = sourceOverPerfect,
|
|
15
19
|
bounds?: Rect,
|
|
16
|
-
):
|
|
20
|
+
): boolean {
|
|
17
21
|
const targetWidth = target.width
|
|
18
22
|
const targetHeight = target.height
|
|
19
23
|
|
|
@@ -26,7 +30,9 @@ export function applyRectBrushToPixelData(
|
|
|
26
30
|
targetHeight,
|
|
27
31
|
)
|
|
28
32
|
|
|
29
|
-
if (b.w <= 0 || b.h <= 0)
|
|
33
|
+
if (b.w <= 0 || b.h <= 0) {
|
|
34
|
+
return false
|
|
35
|
+
}
|
|
30
36
|
|
|
31
37
|
const data32 = target.data32
|
|
32
38
|
const baseColor = color & 0x00ffffff
|
|
@@ -36,7 +42,6 @@ export function applyRectBrushToPixelData(
|
|
|
36
42
|
const invHalfW = 1 / (brushWidth / 2)
|
|
37
43
|
const invHalfH = 1 / (brushHeight / 2)
|
|
38
44
|
|
|
39
|
-
// Restore the pixel-art centering logic
|
|
40
45
|
const centerOffsetX = (brushWidth % 2 === 0) ? 0.5 : 0
|
|
41
46
|
const centerOffsetY = (brushHeight % 2 === 0) ? 0.5 : 0
|
|
42
47
|
const fCenterX = Math.floor(centerX)
|
|
@@ -45,6 +50,7 @@ export function applyRectBrushToPixelData(
|
|
|
45
50
|
const endX = b.x + b.w
|
|
46
51
|
const endY = b.y + b.h
|
|
47
52
|
const isOverwrite = (blendFn as any).isOverwrite
|
|
53
|
+
let didChange = false
|
|
48
54
|
|
|
49
55
|
for (let py = b.y; py < endY; py++) {
|
|
50
56
|
const rowOffset = py * targetWidth
|
|
@@ -52,7 +58,6 @@ export function applyRectBrushToPixelData(
|
|
|
52
58
|
|
|
53
59
|
for (let px = b.x; px < endX; px++) {
|
|
54
60
|
const idx = rowOffset + px
|
|
55
|
-
|
|
56
61
|
const dx = Math.abs((px - fCenterX) + centerOffsetX) * invHalfW
|
|
57
62
|
const dist = dx > dy ? dx : dy
|
|
58
63
|
|
|
@@ -79,7 +84,15 @@ export function applyRectBrushToPixelData(
|
|
|
79
84
|
finalCol = ((a << 24) | baseColor) >>> 0 as Color32
|
|
80
85
|
}
|
|
81
86
|
|
|
82
|
-
|
|
87
|
+
const current = data32[idx] as Color32
|
|
88
|
+
const next = blendFn(finalCol, current)
|
|
89
|
+
|
|
90
|
+
if (current !== next) {
|
|
91
|
+
data32[idx] = next
|
|
92
|
+
didChange = true
|
|
93
|
+
}
|
|
83
94
|
}
|
|
84
95
|
}
|
|
96
|
+
|
|
97
|
+
return didChange
|
|
85
98
|
}
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { type Color32, type ColorBlendOptions, type IPixelData } from '../_types'
|
|
2
2
|
import { sourceOverPerfect } from '../BlendModes/blend-modes-perfect'
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Blends a solid color into a target pixel buffer.
|
|
6
|
+
* @returns true if any pixels were actually modified.
|
|
7
|
+
*/
|
|
4
8
|
export function blendColorPixelData(
|
|
5
9
|
dst: IPixelData,
|
|
6
10
|
color: Color32,
|
|
7
11
|
opts: ColorBlendOptions = {},
|
|
8
|
-
) {
|
|
12
|
+
): boolean {
|
|
9
13
|
const {
|
|
10
14
|
x: targetX = 0,
|
|
11
15
|
y: targetY = 0,
|
|
@@ -15,30 +19,40 @@ export function blendColorPixelData(
|
|
|
15
19
|
blendFn = sourceOverPerfect,
|
|
16
20
|
} = opts
|
|
17
21
|
|
|
18
|
-
if (globalAlpha === 0) return
|
|
22
|
+
if (globalAlpha === 0) return false
|
|
23
|
+
|
|
19
24
|
const baseSrcAlpha = (color >>> 24)
|
|
20
25
|
const isOverwrite = (blendFn as any).isOverwrite || false
|
|
21
|
-
|
|
26
|
+
|
|
27
|
+
if (baseSrcAlpha === 0 && !isOverwrite) return false
|
|
22
28
|
|
|
23
29
|
// Clipping
|
|
24
|
-
let x = targetX
|
|
30
|
+
let x = targetX
|
|
31
|
+
let y = targetY
|
|
32
|
+
let w = width
|
|
33
|
+
let h = height
|
|
34
|
+
|
|
25
35
|
if (x < 0) {
|
|
26
36
|
w += x
|
|
27
37
|
x = 0
|
|
28
38
|
}
|
|
39
|
+
|
|
29
40
|
if (y < 0) {
|
|
30
41
|
h += y
|
|
31
42
|
y = 0
|
|
32
43
|
}
|
|
44
|
+
|
|
33
45
|
const actualW = Math.min(w, dst.width - x)
|
|
34
46
|
const actualH = Math.min(h, dst.height - y)
|
|
35
|
-
|
|
47
|
+
|
|
48
|
+
if (actualW <= 0 || actualH <= 0) return false
|
|
36
49
|
|
|
37
50
|
// Single-color fills can pre-calculate the source color once
|
|
38
51
|
let finalSrcColor = color
|
|
52
|
+
|
|
39
53
|
if (globalAlpha < 255) {
|
|
40
54
|
const a = (baseSrcAlpha * globalAlpha + 128) >> 8
|
|
41
|
-
if (a === 0 && !isOverwrite) return
|
|
55
|
+
if (a === 0 && !isOverwrite) return false
|
|
42
56
|
finalSrcColor = ((color & 0x00ffffff) | (a << 24)) >>> 0 as Color32
|
|
43
57
|
}
|
|
44
58
|
|
|
@@ -46,12 +60,22 @@ export function blendColorPixelData(
|
|
|
46
60
|
const dw = dst.width
|
|
47
61
|
let dIdx = (y * dw + x) | 0
|
|
48
62
|
const dStride = (dw - actualW) | 0
|
|
63
|
+
let didChange = false
|
|
49
64
|
|
|
50
65
|
for (let iy = 0; iy < actualH; iy++) {
|
|
51
66
|
for (let ix = 0; ix < actualW; ix++) {
|
|
52
|
-
|
|
67
|
+
const current = dst32[dIdx] as Color32
|
|
68
|
+
const next = blendFn(finalSrcColor, current)
|
|
69
|
+
|
|
70
|
+
if (current !== next) {
|
|
71
|
+
dst32[dIdx] = next
|
|
72
|
+
didChange = true
|
|
73
|
+
}
|
|
74
|
+
|
|
53
75
|
dIdx++
|
|
54
76
|
}
|
|
55
77
|
dIdx += dStride
|
|
56
78
|
}
|
|
79
|
+
|
|
80
|
+
return didChange
|
|
57
81
|
}
|
|
@@ -12,13 +12,14 @@ import { sourceOverPerfect } from '../BlendModes/blend-modes-perfect'
|
|
|
12
12
|
* @param color - The solid color to apply.
|
|
13
13
|
* @param mask - The mask defining the per-pixel opacity of the target area.
|
|
14
14
|
* @param opts - Configuration options including placement coordinates, bounds, global alpha, and mask offsets.
|
|
15
|
+
* @returns true if any pixels were actually modified.
|
|
15
16
|
*/
|
|
16
17
|
export function blendColorPixelDataAlphaMask(
|
|
17
18
|
dst: IPixelData,
|
|
18
19
|
color: Color32,
|
|
19
20
|
mask: AlphaMask,
|
|
20
21
|
opts: ColorBlendMaskOptions = {},
|
|
21
|
-
):
|
|
22
|
+
): boolean {
|
|
22
23
|
const targetX = opts.x ?? 0
|
|
23
24
|
const targetY = opts.y ?? 0
|
|
24
25
|
const w = opts.w ?? mask.w
|
|
@@ -29,12 +30,12 @@ export function blendColorPixelDataAlphaMask(
|
|
|
29
30
|
const my = opts.my ?? 0
|
|
30
31
|
const invertMask = opts.invertMask ?? false
|
|
31
32
|
|
|
32
|
-
if (globalAlpha === 0) return
|
|
33
|
+
if (globalAlpha === 0) return false
|
|
33
34
|
|
|
34
35
|
const baseSrcAlpha = (color >>> 24)
|
|
35
36
|
const isOverwrite = (blendFn as any).isOverwrite || false
|
|
36
37
|
|
|
37
|
-
if (baseSrcAlpha === 0 && !isOverwrite) return
|
|
38
|
+
if (baseSrcAlpha === 0 && !isOverwrite) return false
|
|
38
39
|
|
|
39
40
|
let x = targetX
|
|
40
41
|
let y = targetY
|
|
@@ -54,7 +55,7 @@ export function blendColorPixelDataAlphaMask(
|
|
|
54
55
|
actualW = Math.min(actualW, dst.width - x)
|
|
55
56
|
actualH = Math.min(actualH, dst.height - y)
|
|
56
57
|
|
|
57
|
-
if (actualW <= 0 || actualH <= 0) return
|
|
58
|
+
if (actualW <= 0 || actualH <= 0) return false
|
|
58
59
|
|
|
59
60
|
const dx = (x - targetX) | 0
|
|
60
61
|
const dy = (y - targetY) | 0
|
|
@@ -68,9 +69,10 @@ export function blendColorPixelDataAlphaMask(
|
|
|
68
69
|
let mIdx = ((my + dy) * mPitch + (mx + dx)) | 0
|
|
69
70
|
|
|
70
71
|
const dStride = (dw - actualW) | 0
|
|
71
|
-
|
|
72
|
+
const mStride = (mPitch - actualW) | 0
|
|
72
73
|
const isOpaque = globalAlpha === 255
|
|
73
74
|
const colorRGB = color & 0x00ffffff
|
|
75
|
+
let didChange = false
|
|
74
76
|
|
|
75
77
|
for (let iy = 0; iy < actualH; iy++) {
|
|
76
78
|
for (let ix = 0; ix < actualW; ix++) {
|
|
@@ -109,7 +111,13 @@ export function blendColorPixelDataAlphaMask(
|
|
|
109
111
|
finalCol = ((colorRGB | (a << 24)) >>> 0) as Color32
|
|
110
112
|
}
|
|
111
113
|
|
|
112
|
-
|
|
114
|
+
const current = dst32[dIdx] as Color32
|
|
115
|
+
const next = blendFn(finalCol, current)
|
|
116
|
+
|
|
117
|
+
if (current !== next) {
|
|
118
|
+
dst32[dIdx] = next
|
|
119
|
+
didChange = true
|
|
120
|
+
}
|
|
113
121
|
|
|
114
122
|
dIdx++
|
|
115
123
|
mIdx++
|
|
@@ -118,4 +126,6 @@ export function blendColorPixelDataAlphaMask(
|
|
|
118
126
|
dIdx += dStride
|
|
119
127
|
mIdx += mStride
|
|
120
128
|
}
|
|
129
|
+
|
|
130
|
+
return didChange
|
|
121
131
|
}
|