pixel-data-js 0.16.0 → 0.17.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.
Files changed (33) hide show
  1. package/dist/index.dev.cjs +1648 -908
  2. package/dist/index.dev.cjs.map +1 -1
  3. package/dist/index.dev.js +1629 -906
  4. package/dist/index.dev.js.map +1 -1
  5. package/dist/index.prod.cjs +1648 -908
  6. package/dist/index.prod.cjs.map +1 -1
  7. package/dist/index.prod.d.ts +287 -67
  8. package/dist/index.prod.js +1629 -906
  9. package/dist/index.prod.js.map +1 -1
  10. package/package.json +1 -1
  11. package/src/BlendModes/BlendModeRegistry.ts +34 -50
  12. package/src/BlendModes/blend-modes-perfect.ts +313 -136
  13. package/src/BlendModes/blend-modes.ts +6 -0
  14. package/src/History/HistoryManager.ts +83 -0
  15. package/src/History/PixelAccumulator.ts +191 -0
  16. package/src/History/PixelEngineConfig.ts +18 -0
  17. package/src/History/PixelMutator/mutatorApplyMask.ts +20 -0
  18. package/src/History/PixelMutator/mutatorBlendColor.ts +22 -0
  19. package/src/History/PixelMutator/mutatorBlendPixel.ts +37 -0
  20. package/src/History/PixelMutator/mutatorBlendPixelData.ts +24 -0
  21. package/src/History/PixelMutator/mutatorFillPixelData.ts +21 -0
  22. package/src/History/PixelMutator/mutatorInvert.ts +18 -0
  23. package/src/History/PixelMutator.ts +18 -0
  24. package/src/History/PixelPatchTiles.ts +52 -0
  25. package/src/History/PixelWriter.ts +79 -0
  26. package/src/ImageData/{writeImageDataPixels.ts → writeImageDataBuffer.ts} +3 -3
  27. package/src/PixelData/applyCircleBrushToPixelData.ts +69 -0
  28. package/src/PixelData/applyMaskToPixelData.ts +1 -1
  29. package/src/PixelData/applyRectBrushToPixelData.ts +102 -0
  30. package/src/PixelData/invertPixelData.ts +74 -7
  31. package/src/PixelData/writePixelDataBuffer.ts +65 -0
  32. package/src/_types.ts +31 -11
  33. package/src/index.ts +18 -1
@@ -0,0 +1,83 @@
1
+ export interface HistoryAction {
2
+ undo: () => void
3
+ redo: () => void
4
+ dispose?: () => void
5
+ }
6
+
7
+ export class HistoryManager {
8
+ public undoStack: HistoryAction[]
9
+ public redoStack: HistoryAction[]
10
+ public listeners: Set<() => void>
11
+
12
+ constructor(
13
+ public maxSteps = 50,
14
+ ) {
15
+ this.undoStack = []
16
+ this.redoStack = []
17
+ this.listeners = new Set()
18
+ }
19
+
20
+ get canUndo() {
21
+ return this.undoStack.length > 0
22
+ }
23
+
24
+ get canRedo() {
25
+ return this.redoStack.length > 0
26
+ }
27
+
28
+ subscribe(fn: () => void) {
29
+ this.listeners.add(fn)
30
+ return () => this.listeners.delete(fn)
31
+ }
32
+
33
+ notify() {
34
+ this.listeners.forEach((fn) => fn())
35
+ }
36
+
37
+ commit(action: HistoryAction) {
38
+ this.undoStack.push(action)
39
+ this.clearRedoStack()
40
+
41
+ if (this.undoStack.length > this.maxSteps) {
42
+ this.undoStack.shift()?.dispose?.()
43
+ }
44
+
45
+ this.notify()
46
+ }
47
+
48
+ undo() {
49
+ let action = this.undoStack.pop()
50
+
51
+ if (!action) return
52
+
53
+ this.redoStack.push(action)
54
+ action.undo()
55
+
56
+ this.notify()
57
+ }
58
+
59
+ redo() {
60
+ let action = this.redoStack.pop()
61
+
62
+ if (!action) return
63
+
64
+ this.undoStack.push(action)
65
+ action.redo()
66
+
67
+ this.notify()
68
+ }
69
+
70
+ clearRedoStack() {
71
+ let length = this.redoStack.length
72
+
73
+ for (let i = 0; i < length; i++) {
74
+ let action = this.redoStack[i]
75
+
76
+ if (action) {
77
+ action.dispose?.()
78
+ }
79
+ }
80
+
81
+ this.redoStack.length = 0
82
+ }
83
+ }
@@ -0,0 +1,191 @@
1
+ import type { PixelData } from '../PixelData/PixelData'
2
+ import type { PixelEngineConfig } from './PixelEngineConfig'
3
+ import { type PixelPatchTiles, PixelTile } from './PixelPatchTiles'
4
+
5
+ export class PixelAccumulator {
6
+ public lookup: (PixelTile | undefined)[]
7
+ public beforeTiles: PixelTile[]
8
+ public pool: PixelTile[]
9
+
10
+ constructor(
11
+ public target: PixelData,
12
+ readonly config: PixelEngineConfig,
13
+ ) {
14
+ this.lookup = []
15
+ this.beforeTiles = []
16
+ this.pool = []
17
+ }
18
+
19
+ getTile(
20
+ id: number,
21
+ tx: number,
22
+ ty: number,
23
+ ): PixelTile {
24
+ let tile = this.pool.pop()
25
+
26
+ if (tile) {
27
+ tile.id = id
28
+ tile.tx = tx
29
+ tile.ty = ty
30
+
31
+ return tile
32
+ }
33
+
34
+ return new PixelTile(
35
+ id,
36
+ tx,
37
+ ty,
38
+ this.config.tileArea,
39
+ )
40
+ }
41
+
42
+ recyclePatch(patch: PixelPatchTiles) {
43
+ const before = patch.beforeTiles
44
+
45
+ for (let i = 0; i < before.length; i++) {
46
+ let tile = before[i]
47
+
48
+ if (tile) {
49
+ this.pool.push(tile)
50
+ }
51
+ }
52
+
53
+ const after = patch.afterTiles
54
+
55
+ for (let i = 0; i < after.length; i++) {
56
+ let tile = after[i]
57
+
58
+ if (tile) {
59
+ this.pool.push(tile)
60
+ }
61
+ }
62
+ }
63
+
64
+ /**
65
+ * @param x pixel x coordinate
66
+ * @param y pixel y coordinate
67
+ */
68
+ storeTileBeforeState(x: number, y: number): void {
69
+ let target = this.target
70
+ let shift = this.config.tileShift
71
+ let columns = (target.width + this.config.tileMask) >> shift
72
+ let tx = x >> shift
73
+ let ty = y >> shift
74
+ let id = ty * columns + tx
75
+
76
+ let tile = this.lookup[id]
77
+
78
+ if (!tile) {
79
+ tile = this.getTile(
80
+ id,
81
+ tx,
82
+ ty,
83
+ )
84
+
85
+ this.extractState(tile)
86
+ this.lookup[id] = tile
87
+ this.beforeTiles.push(tile)
88
+ }
89
+ }
90
+
91
+ /**
92
+ *
93
+ * @param x pixel x coordinate
94
+ * @param y pixel y coordinate
95
+ * @param w pixel width
96
+ * @param h pixel height
97
+ */
98
+ storeRegionBeforeState(
99
+ x: number,
100
+ y: number,
101
+ w: number,
102
+ h: number,
103
+ ) {
104
+ let target = this.target
105
+ let shift = this.config.tileShift
106
+ let columns = (target.width + this.config.tileMask) >> shift
107
+
108
+ let startX = x >> shift
109
+ let startY = y >> shift
110
+ let endX = (x + w - 1) >> shift
111
+ let endY = (y + h - 1) >> shift
112
+
113
+ for (let ty = startY; ty <= endY; ty++) {
114
+ for (let tx = startX; tx <= endX; tx++) {
115
+ let id = ty * columns + tx
116
+ let tile = this.lookup[id]
117
+
118
+ if (!tile) {
119
+ tile = this.getTile(
120
+ id,
121
+ tx,
122
+ ty,
123
+ )
124
+
125
+ this.extractState(tile)
126
+ this.lookup[id] = tile
127
+ this.beforeTiles.push(tile)
128
+ }
129
+ }
130
+ }
131
+ }
132
+
133
+ extractState(tile: PixelTile) {
134
+ let target = this.target
135
+ let TILE_SIZE = this.config.tileSize
136
+ let dst = tile.data32
137
+ let src = target.data32
138
+ let startX = tile.tx * TILE_SIZE
139
+ let startY = tile.ty * TILE_SIZE
140
+ let targetWidth = target.width
141
+ let targetHeight = target.height
142
+
143
+ let copyWidth = Math.max(0, Math.min(TILE_SIZE, targetWidth - startX))
144
+
145
+ for (let ly = 0; ly < TILE_SIZE; ly++) {
146
+ let globalY = startY + ly
147
+ let dstIndex = ly * TILE_SIZE
148
+
149
+ if (globalY < 0 || globalY >= targetHeight || copyWidth === 0) {
150
+ dst.fill(0, dstIndex, dstIndex + TILE_SIZE)
151
+ continue
152
+ }
153
+
154
+ let srcIndex = globalY * targetWidth + startX
155
+ let rowData = src.subarray(srcIndex, srcIndex + copyWidth)
156
+
157
+ dst.set(rowData, dstIndex)
158
+
159
+ if (copyWidth < TILE_SIZE) {
160
+ dst.fill(0, dstIndex + copyWidth, dstIndex + TILE_SIZE)
161
+ }
162
+ }
163
+ }
164
+
165
+ extractAfterTiles(): PixelTile[] {
166
+ let afterTiles: PixelTile[] = []
167
+ let length = this.beforeTiles.length
168
+
169
+ for (let i = 0; i < length; i++) {
170
+ let beforeTile = this.beforeTiles[i]
171
+
172
+ if (beforeTile) {
173
+ let afterTile = this.getTile(
174
+ beforeTile.id,
175
+ beforeTile.tx,
176
+ beforeTile.ty,
177
+ )
178
+
179
+ this.extractState(afterTile)
180
+ afterTiles.push(afterTile)
181
+ }
182
+ }
183
+
184
+ return afterTiles
185
+ }
186
+
187
+ reset() {
188
+ this.lookup = []
189
+ this.beforeTiles = []
190
+ }
191
+ }
@@ -0,0 +1,18 @@
1
+ export class PixelEngineConfig {
2
+ public readonly tileSize: number
3
+ public readonly tileShift: number
4
+ public readonly tileMask: number
5
+ public readonly tileArea: number
6
+
7
+ constructor(tileSize: number = 256) {
8
+ // Ensure it's a power of 2 to guarantee bitwise math works
9
+ if ((tileSize & (tileSize - 1)) !== 0) {
10
+ throw new Error('tileSize must be a power of 2')
11
+ }
12
+
13
+ this.tileSize = tileSize
14
+ this.tileShift = Math.log2(tileSize)
15
+ this.tileMask = tileSize - 1
16
+ this.tileArea = tileSize * tileSize
17
+ }
18
+ }
@@ -0,0 +1,20 @@
1
+ import { type AnyMask, type ApplyMaskOptions } from '../../_types'
2
+ import { applyMaskToPixelData } from '../../PixelData/applyMaskToPixelData'
3
+ import { PixelWriter } from '../PixelWriter'
4
+
5
+ export function mutatorApplyMask(writer: PixelWriter<any>) {
6
+ return {
7
+ applyMask: (mask: AnyMask, opts: ApplyMaskOptions = {}) => {
8
+ let target = writer.target
9
+ const {
10
+ x = 0,
11
+ y = 0,
12
+ w = writer.target.width,
13
+ h = writer.target.height,
14
+ } = opts
15
+
16
+ writer.accumulator.storeRegionBeforeState(x, y, w, h)
17
+ applyMaskToPixelData(target, mask, opts)
18
+ },
19
+ }
20
+ }
@@ -0,0 +1,22 @@
1
+ import type { Color32, ColorBlendOptions } from '../../_types'
2
+ import { blendColorPixelData } from '../../PixelData/blendColorPixelData'
3
+ import { PixelWriter } from '../PixelWriter'
4
+
5
+ export function mutatorBlendColor(writer: PixelWriter<any>) {
6
+ return {
7
+ blendColor(
8
+ color: Color32,
9
+ opts: ColorBlendOptions = {},
10
+ ) {
11
+
12
+ const {
13
+ x = 0,
14
+ y = 0,
15
+ w = writer.target.width,
16
+ h = writer.target.height,
17
+ } = opts
18
+ writer.accumulator.storeRegionBeforeState(x, y, w, h)
19
+ blendColorPixelData(writer.target, color, opts)
20
+ },
21
+ }
22
+ }
@@ -0,0 +1,37 @@
1
+ import type { BlendColor32, Color32 } from '../../_types'
2
+ import { overwriteFast } from '../../BlendModes/blend-modes-fast'
3
+ import { PixelWriter } from '../PixelWriter'
4
+
5
+ export function mutatorBlendPixel(writer: PixelWriter<any>) {
6
+ return {
7
+ blendPixel(
8
+ x: number,
9
+ y: number,
10
+ color: Color32,
11
+ alpha: number = 255,
12
+ blendFn: BlendColor32 = overwriteFast,
13
+ ) {
14
+ let target = writer.target
15
+ let width = target.width
16
+ let height = target.height
17
+
18
+ if (x < 0 || x >= width || y < 0 || y >= height) return
19
+
20
+ writer.accumulator.storeTileBeforeState(x, y)
21
+
22
+ let index = y * width + x
23
+ let bg = target.data32[index] as Color32
24
+
25
+ let finalColor = color
26
+
27
+ if (alpha < 255) {
28
+ let baseSrcAlpha = color >>> 24
29
+ let finalAlpha = (baseSrcAlpha * alpha + 128) >> 8
30
+
31
+ finalColor = (((color & 0x00ffffff) | (finalAlpha << 24)) >>> 0) as Color32
32
+ }
33
+
34
+ target.data32[index] = blendFn(finalColor, bg)
35
+ },
36
+ }
37
+ }
@@ -0,0 +1,24 @@
1
+ import type { PixelBlendOptions } from '../../_types'
2
+ import { blendPixelData } from '../../PixelData/blendPixelData'
3
+ import type { PixelData } from '../../PixelData/PixelData'
4
+ import { PixelWriter } from '../PixelWriter'
5
+
6
+ export function mutatorBlendPixelData(writer: PixelWriter<any>) {
7
+ return {
8
+ blendPixelData(
9
+ src: PixelData,
10
+ opts: PixelBlendOptions,
11
+ ) {
12
+ const {
13
+ x = 0,
14
+ y = 0,
15
+ w = src.width,
16
+ h = src.height,
17
+ } = opts
18
+ writer.accumulator.storeRegionBeforeState(x, y, w, h)
19
+
20
+ blendPixelData(writer.target, src, opts)
21
+ },
22
+ }
23
+ }
24
+
@@ -0,0 +1,21 @@
1
+ import type { Color32, Rect } from '../../_types'
2
+ import { fillPixelData } from '../../PixelData/fillPixelData'
3
+ import { PixelWriter } from '../PixelWriter'
4
+
5
+ export function mutatorFill(writer: PixelWriter<any>) {
6
+ return {
7
+ fill(
8
+ color: Color32,
9
+ rect: Partial<Rect> = {},
10
+ ) {
11
+ const {
12
+ x = 0,
13
+ y = 0,
14
+ w = writer.target.width,
15
+ h = writer.target.height,
16
+ } = rect
17
+ writer.accumulator.storeRegionBeforeState(x, y, w, h)
18
+ fillPixelData(writer.target, color, x, y, w, h)
19
+ },
20
+ }
21
+ }
@@ -0,0 +1,18 @@
1
+ import type { PixelMutateOptions } from '../../_types'
2
+ import { invertPixelData } from '../../PixelData/invertPixelData'
3
+ import { PixelWriter } from '../PixelWriter'
4
+
5
+ export function mutatorInvert(writer: PixelWriter<any>) {
6
+ return {
7
+ invert(opts: PixelMutateOptions = {}) {
8
+ const {
9
+ x = 0,
10
+ y = 0,
11
+ w = writer.target.width,
12
+ h = writer.target.height,
13
+ } = opts
14
+ writer.accumulator.storeRegionBeforeState(x, y, w, h)
15
+ invertPixelData(writer.target, opts)
16
+ },
17
+ }
18
+ }
@@ -0,0 +1,18 @@
1
+ import { mutatorApplyMask } from './PixelMutator/mutatorApplyMask'
2
+ import { mutatorBlendColor } from './PixelMutator/mutatorBlendColor'
3
+ import { mutatorBlendPixel } from './PixelMutator/mutatorBlendPixel'
4
+ import { mutatorBlendPixelData } from './PixelMutator/mutatorBlendPixelData'
5
+ import { mutatorFill } from './PixelMutator/mutatorFillPixelData'
6
+ import { mutatorInvert } from './PixelMutator/mutatorInvert'
7
+ import type { PixelWriter } from './PixelWriter'
8
+
9
+ export function makeFullPixelMutator(writer: PixelWriter<any>) {
10
+ return {
11
+ ...mutatorApplyMask(writer),
12
+ ...mutatorBlendPixelData(writer),
13
+ ...mutatorBlendColor(writer),
14
+ ...mutatorBlendPixel(writer),
15
+ ...mutatorFill(writer),
16
+ ...mutatorInvert(writer),
17
+ }
18
+ }
@@ -0,0 +1,52 @@
1
+ import type { PixelData } from '../PixelData/PixelData'
2
+
3
+ export type PixelPatchTiles = {
4
+ beforeTiles: PixelTile[]
5
+ afterTiles: PixelTile[]
6
+ }
7
+
8
+ export class PixelTile {
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: PixelData, tiles: PixelTile[], tileSize: number = 256) {
22
+ for (let i = 0; i < tiles.length; i++) {
23
+ const tile = tiles[i]
24
+
25
+ if (!tile) continue
26
+
27
+ const dst = target.data32
28
+ const src = tile.data32
29
+ const dstWidth = target.width
30
+ const dstHeight = target.height
31
+ const startX = tile.tx * tileSize
32
+ const startY = tile.ty * tileSize
33
+
34
+ // Calculate clamping to prevent wrapping artifacts on image edges
35
+ const copyWidth = Math.max(0, Math.min(tileSize, dstWidth - startX))
36
+
37
+ if (copyWidth <= 0) return
38
+
39
+ for (let ly = 0; ly < tileSize; ly++) {
40
+ const globalY = startY + ly
41
+
42
+ // Stop if we go below the image
43
+ if (globalY >= dstHeight) break
44
+
45
+ const dstIndex = globalY * dstWidth + startX
46
+ const srcIndex = ly * tileSize
47
+ const rowData = src.subarray(srcIndex, srcIndex + copyWidth)
48
+
49
+ dst.set(rowData, dstIndex)
50
+ }
51
+ }
52
+ }
@@ -0,0 +1,79 @@
1
+ import type { PixelData } from '../PixelData/PixelData'
2
+ import { type HistoryAction, HistoryManager } from './HistoryManager'
3
+ import { PixelAccumulator } from './PixelAccumulator'
4
+ import { PixelEngineConfig } from './PixelEngineConfig'
5
+ import { applyPatchTiles, type PixelPatchTiles } from './PixelPatchTiles'
6
+
7
+ export interface PixelWriterOptions {
8
+ maxHistorySteps?: number
9
+ tileSize?: number
10
+ historyManager?: HistoryManager
11
+ }
12
+
13
+ /**
14
+ * @example
15
+ * const targ = new PixelData(new ImageData(10, 10))
16
+ * const writer = new PixelWriter(targ, (writer) => {
17
+ * return {
18
+ * ...mutatorApplyMask(writer),
19
+ * ...mutatorBlendPixelData(writer),
20
+ * ...mutatorBlendColor(writer),
21
+ * ...mutatorBlendPixel(writer),
22
+ * ...mutatorFill(writer),
23
+ * }
24
+ * })
25
+ *
26
+ * // to import all mutator functions
27
+ * const writer = new PixelWriter(targ, makeFullPixelMutator)
28
+ *
29
+ * writer.withHistory((mutator) => {
30
+ * mutator.applyMask()
31
+ * mutator.blendPixelData()
32
+ * })
33
+ */
34
+ export class PixelWriter<M> {
35
+ public target: PixelData
36
+ public historyManager: HistoryManager
37
+ public accumulator: PixelAccumulator
38
+ protected config: PixelEngineConfig
39
+ private mutator: M
40
+
41
+ constructor(target: PixelData, mutatorFactory: (writer: PixelWriter<any>) => M, {
42
+ tileSize = 256,
43
+ maxHistorySteps = 50,
44
+ historyManager = new HistoryManager(maxHistorySteps),
45
+ }: PixelWriterOptions = {}) {
46
+ this.target = target
47
+ this.config = new PixelEngineConfig(tileSize)
48
+ this.historyManager = historyManager
49
+ this.accumulator = new PixelAccumulator(target, this.config)
50
+ this.mutator = mutatorFactory(this)
51
+ }
52
+
53
+ withHistory(cb: (mutator: M) => void) {
54
+ cb(this.mutator)
55
+
56
+ const beforeTiles = this.accumulator.beforeTiles
57
+ if (beforeTiles.length === 0) return
58
+
59
+ const afterTiles = this.accumulator.extractAfterTiles()
60
+
61
+ const patch: PixelPatchTiles = {
62
+ beforeTiles: beforeTiles,
63
+ afterTiles: afterTiles,
64
+ }
65
+
66
+ const target = this.target
67
+ const tileSize = this.config.tileSize
68
+ const accumulator = this.accumulator
69
+
70
+ const action: HistoryAction = {
71
+ undo: () => applyPatchTiles(target, patch.beforeTiles, tileSize),
72
+ redo: () => applyPatchTiles(target, patch.afterTiles, tileSize),
73
+ dispose: () => accumulator.recyclePatch(patch),
74
+ }
75
+
76
+ this.historyManager.commit(action)
77
+ this.accumulator.reset()
78
+ }
79
+ }
@@ -11,7 +11,7 @@ import type { Rect } from '../_types'
11
11
  * @param data - The source pixel data (RGBA).
12
12
  * @param rect - A {@link Rect} object defining the destination region.
13
13
  */
14
- export function writeImageDataPixels(
14
+ export function writeImageDataBuffer(
15
15
  imageData: ImageData,
16
16
  data: Uint8ClampedArray,
17
17
  rect: Rect,
@@ -24,7 +24,7 @@ export function writeImageDataPixels(
24
24
  * @param w - The width of the region to write.
25
25
  * @param h - The height of the region to write.
26
26
  */
27
- export function writeImageDataPixels(
27
+ export function writeImageDataBuffer(
28
28
  imageData: ImageData,
29
29
  data: Uint8ClampedArray,
30
30
  x: number,
@@ -32,7 +32,7 @@ export function writeImageDataPixels(
32
32
  w: number,
33
33
  h: number,
34
34
  ): void
35
- export function writeImageDataPixels(
35
+ export function writeImageDataBuffer(
36
36
  imageData: ImageData,
37
37
  data: Uint8ClampedArray,
38
38
  _x: Rect | number,
@@ -0,0 +1,69 @@
1
+ import type { BlendColor32, Color32 } from '../_types'
2
+ import { sourceOverPerfect } from '../BlendModes/blend-modes-perfect'
3
+ import type { PixelData } from './PixelData'
4
+
5
+ /**
6
+ * Applies a circular 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 brushSize The diameter of the brush.
13
+ * @param alpha The overall opacity of the brush (0-255).
14
+ * @default 255
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
+ * @param blendFn
17
+ * @default sourceOverPerfect
18
+ */
19
+ export function applyCircleBrushToPixelData(
20
+ target: PixelData,
21
+ color: Color32,
22
+ centerX: number,
23
+ centerY: number,
24
+ brushSize: number,
25
+ alpha = 255,
26
+ fallOff?: (dist: number) => number,
27
+ blendFn: BlendColor32 = sourceOverPerfect,
28
+ ): void {
29
+ const r = brushSize / 2
30
+ const rSqr = r * r
31
+ const centerOffset = (brushSize % 2 === 0) ? 0.5 : 0
32
+
33
+ const xStart = Math.max(0, Math.ceil(centerX - r))
34
+ const xEnd = Math.min(target.width - 1, Math.floor(centerX + r))
35
+ const yStart = Math.max(0, Math.ceil(centerY - r))
36
+ const yEnd = Math.min(target.height - 1, Math.floor(centerY + r))
37
+
38
+ const data32 = target.data32
39
+ const targetWidth = target.width
40
+ const baseColor = color & 0x00ffffff
41
+ const invR = 1 / r
42
+
43
+ // Pre-calculate the constant source for cases where fallOff is null
44
+ const constantSrc = ((alpha << 24) | baseColor) >>> 0 as Color32
45
+
46
+ for (let cy = yStart; cy <= yEnd; cy++) {
47
+ const dy = cy - centerY + centerOffset
48
+ const dySqr = dy * dy
49
+ const rowOffset = cy * targetWidth
50
+
51
+ for (let cx = xStart; cx <= xEnd; cx++) {
52
+ const dx = cx - centerX + centerOffset
53
+ const dSqr = dx * dx + dySqr
54
+
55
+ if (dSqr <= rSqr) {
56
+ const idx = rowOffset + cx
57
+
58
+ if (fallOff) {
59
+ const strength = fallOff(Math.sqrt(dSqr) * invR)
60
+ const fAlpha = (alpha * strength) & 0xFF
61
+ const src = ((fAlpha << 24) | baseColor) >>> 0 as Color32
62
+ data32[idx] = blendFn(src, data32[idx] as Color32)
63
+ } else {
64
+ data32[idx] = blendFn(constantSrc, data32[idx] as Color32)
65
+ }
66
+ }
67
+ }
68
+ }
69
+ }
@@ -8,7 +8,7 @@ import type { PixelData } from './PixelData'
8
8
  export function applyMaskToPixelData(
9
9
  dst: PixelData,
10
10
  mask: AnyMask,
11
- opts: ApplyMaskOptions,
11
+ opts: ApplyMaskOptions = {},
12
12
  ): void {
13
13
  const {
14
14
  x: targetX = 0,