pixel-data-js 0.20.0 → 0.22.2

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 (65) hide show
  1. package/dist/index.dev.cjs +722 -357
  2. package/dist/index.dev.cjs.map +1 -1
  3. package/dist/index.dev.js +709 -356
  4. package/dist/index.dev.js.map +1 -1
  5. package/dist/index.prod.cjs +722 -357
  6. package/dist/index.prod.cjs.map +1 -1
  7. package/dist/index.prod.d.ts +283 -128
  8. package/dist/index.prod.js +709 -356
  9. package/dist/index.prod.js.map +1 -1
  10. package/package.json +1 -1
  11. package/src/Algorithm/floodFillSelection.ts +12 -14
  12. package/src/BlendModes/toBlendModeIndexAndName.ts +0 -7
  13. package/src/Clipboard/writeImgBlobToClipboard.ts +1 -1
  14. package/src/History/PixelMutator/mutatorApplyAlphaMask.ts +3 -0
  15. package/src/History/PixelMutator/mutatorApplyBinaryMask.ts +3 -0
  16. package/src/History/PixelMutator/mutatorApplyCircleBrush.ts +25 -6
  17. package/src/History/PixelMutator/mutatorApplyCircleBrushStroke.ts +89 -46
  18. package/src/History/PixelMutator/mutatorApplyCirclePencil.ts +7 -7
  19. package/src/History/PixelMutator/mutatorApplyCirclePencilStroke.ts +81 -41
  20. package/src/History/PixelMutator/mutatorApplyRectBrush.ts +3 -0
  21. package/src/History/PixelMutator/mutatorApplyRectBrushStroke.ts +18 -5
  22. package/src/History/PixelMutator/mutatorApplyRectPencil.ts +3 -0
  23. package/src/History/PixelMutator/mutatorApplyRectPencilStroke.ts +19 -4
  24. package/src/History/PixelMutator/mutatorBlendColor.ts +4 -0
  25. package/src/History/PixelMutator/mutatorBlendPixelData.ts +4 -0
  26. package/src/History/PixelMutator/mutatorClear.ts +11 -8
  27. package/src/History/PixelMutator/mutatorFill.ts +4 -0
  28. package/src/History/PixelMutator/mutatorFillBinaryMask.ts +28 -0
  29. package/src/History/PixelMutator/mutatorInvert.ts +3 -0
  30. package/src/ImageData/extractImageDataBuffer.ts +3 -3
  31. package/src/ImageData/{imageDataToAlphaMask.ts → imageDataToAlphaMaskBuffer.ts} +3 -4
  32. package/src/ImageData/resizeImageData.ts +3 -5
  33. package/src/ImageData/writeImageDataBuffer.ts +7 -7
  34. package/src/Mask/AlphaMask.ts +16 -0
  35. package/src/Mask/BinaryMask.ts +16 -0
  36. package/src/Mask/CircleBrushAlphaMask.ts +32 -0
  37. package/src/Mask/CircleBrushBinaryMask.ts +30 -0
  38. package/src/Mask/applyBinaryMaskToAlphaMask.ts +12 -9
  39. package/src/Mask/copyMask.ts +9 -3
  40. package/src/Mask/extractMask.ts +33 -31
  41. package/src/Mask/extractMaskBuffer.ts +87 -0
  42. package/src/Mask/invertMask.ts +6 -4
  43. package/src/Mask/mergeAlphaMasks.ts +11 -10
  44. package/src/Mask/mergeBinaryMasks.ts +10 -9
  45. package/src/Mask/setMaskData.ts +7 -0
  46. package/src/MaskRect/merge2BinaryMaskRects.ts +81 -0
  47. package/src/MaskRect/mergeBinaryMaskRects.ts +39 -0
  48. package/src/MaskRect/subtractBinaryMaskRects.ts +80 -0
  49. package/src/PixelData/applyAlphaMaskToPixelData.ts +8 -5
  50. package/src/PixelData/applyBinaryMaskToPixelData.ts +8 -9
  51. package/src/PixelData/applyCircleBrushToPixelData.ts +54 -62
  52. package/src/PixelData/blendColorPixelDataAlphaMask.ts +35 -25
  53. package/src/PixelData/blendColorPixelDataBinaryMask.ts +26 -19
  54. package/src/PixelData/blendPixelDataAlphaMask.ts +3 -3
  55. package/src/PixelData/blendPixelDataBinaryMask.ts +3 -3
  56. package/src/PixelData/clearPixelData.ts +2 -2
  57. package/src/PixelData/fillPixelData.ts +15 -42
  58. package/src/PixelData/fillPixelDataBinaryMask.ts +79 -0
  59. package/src/PixelData/invertPixelData.ts +3 -3
  60. package/src/PixelData/pixelDataToAlphaMask.ts +4 -2
  61. package/src/PixelData/writePixelDataBuffer.ts +2 -3
  62. package/src/Rect/getRectsBounds.ts +22 -0
  63. package/src/Rect/trimRectBounds.ts +20 -17
  64. package/src/_types.ts +55 -29
  65. package/src/index.ts +16 -1
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pixel-data-js",
3
3
  "type": "module",
4
- "version": "0.20.0",
4
+ "version": "0.22.2",
5
5
  "packageManager": "pnpm@10.30.0",
6
6
  "description": "JS Pixel and ImageData operations",
7
7
  "author": {
@@ -1,4 +1,4 @@
1
- import { type Color32, type ImageDataLike, MaskType, type Rect, type SelectionRect } from '../_types'
1
+ import { type BinaryMaskRect, type Color32, type ImageDataLike, MaskType, type Rect } from '../_types'
2
2
  import { colorDistance } from '../color'
3
3
  import { extractImageDataBuffer } from '../ImageData/extractImageDataBuffer'
4
4
  import type { PixelData } from '../PixelData/PixelData'
@@ -13,7 +13,7 @@ export type FloodFillImageDataOptions = {
13
13
  export type FloodFillResult = {
14
14
  startX: number
15
15
  startY: number
16
- selectionRect: SelectionRect
16
+ selectionRect: BinaryMaskRect
17
17
  pixels: Uint8ClampedArray
18
18
  }
19
19
 
@@ -27,9 +27,9 @@ export type FloodFillResult = {
27
27
  * @param startX - The starting horizontal coordinate.
28
28
  * @param startY - The starting vertical coordinate.
29
29
  * @param options - Configuration for the fill operation.
30
- * @param options.contiguous - @default true. If true, only connected pixels are
30
+ * @param options.contiguous - If true, only connected pixels are
31
31
  * selected. If false, all pixels within tolerance are selected regardless of position.
32
- * @param options.tolerance - @default 0. The maximum allowed difference in color
32
+ * @param options.tolerance - The maximum allowed difference in color
33
33
  * distance (0-255) for a pixel to be included.
34
34
  * @param options.bounds - Optional bounding box to restrict the search area.
35
35
  *
@@ -181,20 +181,20 @@ export function floodFillSelection(
181
181
  if (matchCount === 0) {
182
182
  return null
183
183
  }
184
- const selectionRect: SelectionRect = {
184
+ const w = maxX - minX + 1
185
+ const h = maxY - minY + 1
186
+ const selectionRect: BinaryMaskRect = {
185
187
  x: minX,
186
188
  y: minY,
187
- w: maxX - minX + 1,
188
- h: maxY - minY + 1,
189
- mask: new Uint8Array((maxX - minX + 1) * (maxY - minY + 1)),
190
- maskType: MaskType.BINARY,
189
+ w,
190
+ h,
191
+ data: new Uint8Array(w * h),
192
+ type: MaskType.BINARY,
191
193
  }
192
194
 
193
- // REMOVED trimRectBounds from here
194
-
195
195
  const sw = selectionRect.w
196
196
  const sh = selectionRect.h
197
- const finalMask = selectionRect.mask!
197
+ const finalMask = selectionRect.data
198
198
 
199
199
  for (let i = 0; i < matchCount; i++) {
200
200
  const mx = matchX[i] - selectionRect.x
@@ -205,13 +205,11 @@ export function floodFillSelection(
205
205
  }
206
206
  }
207
207
 
208
- // trimRectBounds can see them and work correctly.
209
208
  trimRectBounds(
210
209
  selectionRect,
211
210
  { x: 0, y: 0, w: width, h: height },
212
211
  )
213
212
 
214
- // Use the UPDATED values from the selectionRect after trimming
215
213
  const extracted = extractImageDataBuffer(
216
214
  imageData,
217
215
  selectionRect.x,
@@ -12,14 +12,7 @@ export function toBlendModeIndexAndName(input: string | number) {
12
12
  const isNumeric = trimmed !== '' && !Number.isNaN(num)
13
13
 
14
14
  if (isNumeric && Number.isInteger(num)) {
15
- console.log({
16
- trimmed,
17
- num,
18
- isNumeric,
19
- isInt: Number.isInteger(num),
20
- })
21
15
  const name = getKeyByValue(BaseBlendMode, num)
22
- console.log({name})
23
16
  if (name === undefined) throw new Error(`Invalid index: ${num}`)
24
17
  return { blendIndex: num, blendName: name }
25
18
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Writes a {@link Blob} image to the system clipboard.
3
3
  *
4
- * @param blob - The image {@link Blob} (typically `image/png`) to copy.
4
+ * @param blob - The image blob (typically `image/png`) to copy.
5
5
  * @returns A promise that resolves when the clipboard has been updated.
6
6
  */
7
7
  export async function writeImgBlobToClipboard(blob: Blob): Promise<void> {
@@ -8,6 +8,9 @@ const defaults = {
8
8
 
9
9
  type Deps = Partial<typeof defaults>
10
10
 
11
+ /**
12
+ * @param deps - @hidden
13
+ */
11
14
  export const mutatorApplyAlphaMask = ((writer: PixelWriter<any>, deps: Deps = defaults) => {
12
15
  const {
13
16
  applyAlphaMaskToPixelData = defaults.applyAlphaMaskToPixelData,
@@ -8,6 +8,9 @@ const defaults = {
8
8
 
9
9
  type Deps = Partial<typeof defaults>
10
10
 
11
+ /**
12
+ * @param deps - @hidden
13
+ */
11
14
  export const mutatorApplyBinaryMask = ((writer: PixelWriter<any>, deps: Deps = defaults) => {
12
15
  const {
13
16
  applyBinaryMaskToPixelData = defaults.applyBinaryMaskToPixelData,
@@ -1,4 +1,12 @@
1
- import type { BlendColor32, Color32, HistoryMutator, Rect } from '../../_types'
1
+ import type {
2
+ BlendColor32,
3
+ CircleBrushAlphaMask,
4
+ Color32,
5
+ ColorBlendMaskOptions,
6
+ HistoryMutator,
7
+ Rect,
8
+ } from '../../_types'
9
+ import { sourceOverPerfect } from '../../BlendModes/blend-modes-perfect'
2
10
  import { applyCircleBrushToPixelData } from '../../PixelData/applyCircleBrushToPixelData'
3
11
  import { getCircleBrushOrPencilBounds } from '../../Rect/getCircleBrushOrPencilBounds'
4
12
  import { PixelWriter } from '../PixelWriter'
@@ -10,6 +18,9 @@ const defaults = {
10
18
 
11
19
  type Deps = Partial<typeof defaults>
12
20
 
21
+ /**
22
+ * @param deps - @hidden
23
+ */
13
24
  export const mutatorApplyCircleBrush = ((writer: PixelWriter<any>, deps: Deps = defaults) => {
14
25
  const {
15
26
  applyCircleBrushToPixelData = defaults.applyCircleBrushToPixelData,
@@ -19,21 +30,29 @@ export const mutatorApplyCircleBrush = ((writer: PixelWriter<any>, deps: Deps =
19
30
 
20
31
  const boundsOut: Rect = { x: 0, y: 0, w: 0, h: 0 }
21
32
 
33
+ const blendColorPixelOptions: ColorBlendMaskOptions = {
34
+ alpha: 255,
35
+ blendFn: sourceOverPerfect,
36
+ x: 0,
37
+ y: 0,
38
+ w: 0,
39
+ h: 0,
40
+ }
41
+
22
42
  return {
23
43
  applyCircleBrush(
24
44
  color: Color32,
25
45
  centerX: number,
26
46
  centerY: number,
27
- brushSize: number,
47
+ brush: CircleBrushAlphaMask,
28
48
  alpha = 255,
29
- fallOff: (dist: number) => number,
30
49
  blendFn?: BlendColor32,
31
50
  ) {
32
51
 
33
52
  const bounds = getCircleBrushOrPencilBounds(
34
53
  centerX,
35
54
  centerY,
36
- brushSize,
55
+ brush.size,
37
56
  writer.target.width,
38
57
  writer.target.height,
39
58
  boundsOut,
@@ -48,10 +67,10 @@ export const mutatorApplyCircleBrush = ((writer: PixelWriter<any>, deps: Deps =
48
67
  color,
49
68
  centerX,
50
69
  centerY,
51
- brushSize,
70
+ brush,
52
71
  alpha,
53
- fallOff,
54
72
  blendFn,
73
+ blendColorPixelOptions,
55
74
  bounds,
56
75
  )
57
76
  },
@@ -1,9 +1,11 @@
1
1
  import {
2
2
  type AlphaMask,
3
3
  type BlendColor32,
4
+ type CircleBrushAlphaMask,
4
5
  type Color32,
5
6
  type ColorBlendMaskOptions,
6
7
  type HistoryMutator,
8
+ MaskType,
7
9
  type Rect,
8
10
  } from '../../_types'
9
11
  import { forEachLinePoint } from '../../Algorithm/forEachLinePoint'
@@ -22,6 +24,9 @@ const defaults = {
22
24
 
23
25
  type Deps = Partial<typeof defaults>
24
26
 
27
+ /**
28
+ * @param deps - @hidden
29
+ */
25
30
  export const mutatorApplyCircleBrushStroke = ((writer: PixelWriter<any>, deps: Deps = defaults) => {
26
31
  const {
27
32
  forEachLinePoint = defaults.forEachLinePoint,
@@ -53,6 +58,13 @@ export const mutatorApplyCircleBrushStroke = ((writer: PixelWriter<any>, deps: D
53
58
  h: 0,
54
59
  }
55
60
 
61
+ const mask = {
62
+ type: MaskType.ALPHA,
63
+ data: null as unknown as Uint8Array,
64
+ w: 0,
65
+ h: 0,
66
+ }
67
+
56
68
  return {
57
69
  applyCircleBrushStroke(
58
70
  color: Color32,
@@ -60,70 +72,96 @@ export const mutatorApplyCircleBrushStroke = ((writer: PixelWriter<any>, deps: D
60
72
  y0: number,
61
73
  x1: number,
62
74
  y1: number,
63
- brushSize: number,
75
+ brush: CircleBrushAlphaMask,
64
76
  alpha = 255,
65
- fallOff: (dist: number) => number,
66
77
  blendFn: BlendColor32 = sourceOverPerfect,
67
78
  ) {
79
+ const brushSize = brush.size
80
+
68
81
  const {
69
82
  x: bx,
70
83
  y: by,
71
84
  w: bw,
72
85
  h: bh,
73
- } = getCircleBrushOrPencilStrokeBounds(x0, y0, x1, y1, brushSize, strokeBoundsOut)
86
+ } = getCircleBrushOrPencilStrokeBounds(
87
+ x0,
88
+ y0,
89
+ x1,
90
+ y1,
91
+ brushSize,
92
+ strokeBoundsOut,
93
+ )
74
94
 
75
95
  if (bw <= 0 || bh <= 0) return
76
96
 
77
- const mask = new Uint8Array(bw * bh) as AlphaMask
97
+ mask.data = new Uint8Array(bw * bh)
98
+ mask.w = bw
99
+ mask.h = bh
78
100
 
79
- const r = brushSize / 2
80
- const rSqr = r * r
81
- const invR = 1 / r
82
- const centerOffset = (brushSize % 2 === 0) ? 0.5 : 0
101
+ const maskData = mask.data
102
+ const brushData = brush.data
103
+ const minOffset = brush.minOffset
83
104
 
84
105
  const targetWidth = writer.target.width
85
106
  const targetHeight = writer.target.height
86
107
 
87
- forEachLinePoint(x0, y0, x1, y1, (px, py) => {
88
- // 2. Calculate bounds for this specific stamp
89
- const {
90
- x: cbx,
91
- y: cby,
92
- w: cbw,
93
- h: cbh,
94
- } = getCircleBrushOrPencilBounds(px, py, brushSize, targetWidth, targetHeight, circleBrushBounds)
95
-
96
- writer.accumulator.storeRegionBeforeState(cbx, cby, cbw, cbh)
97
-
98
- const startX = Math.max(bx, cbx)
99
- const startY = Math.max(by, cby)
100
- const endX = Math.min(bx + bw, cbx + cbw)
101
- const endY = Math.min(by + bh, cby + cbh)
102
-
103
- const fPx = Math.floor(px)
104
- const fPy = Math.floor(py)
105
-
106
- for (let my = startY; my < endY; my++) {
107
- const dy = (my - fPy) + centerOffset
108
- const dySqr = dy * dy
109
- const maskRowOffset = (my - by) * bw
110
-
111
- for (let mx = startX; mx < endX; mx++) {
112
- const dx = (mx - fPx) + centerOffset
113
- const dSqr = dx * dx + dySqr
114
-
115
- if (dSqr <= rSqr) {
116
- const maskIdx = maskRowOffset + (mx - bx)
117
-
118
- const dist = Math.sqrt(dSqr) * invR
119
- const intensity = (fallOff(1 - dist) * 255) | 0
120
- if (intensity > mask[maskIdx]) {
121
- mask[maskIdx] = intensity
108
+ forEachLinePoint(
109
+ x0,
110
+ y0,
111
+ x1,
112
+ y1,
113
+ (px, py) => {
114
+ const {
115
+ x: cbx,
116
+ y: cby,
117
+ w: cbw,
118
+ h: cbh,
119
+ } = getCircleBrushOrPencilBounds(
120
+ px,
121
+ py,
122
+ brushSize,
123
+ targetWidth,
124
+ targetHeight,
125
+ circleBrushBounds,
126
+ )
127
+
128
+ writer.accumulator.storeRegionBeforeState(
129
+ cbx,
130
+ cby,
131
+ cbw,
132
+ cbh,
133
+ )
134
+
135
+ const startX = Math.max(bx, cbx)
136
+ const startY = Math.max(by, cby)
137
+ const endX = Math.min(bx + bw, cbx + cbw)
138
+ const endY = Math.min(by + bh, cby + cbh)
139
+
140
+ const unclippedStartX = Math.floor(px + minOffset)
141
+ const unclippedStartY = Math.floor(py + minOffset)
142
+
143
+ for (let my = startY; my < endY; my++) {
144
+ const strokeMaskY = my - by
145
+ const strokeMaskRowOffset = strokeMaskY * bw
146
+
147
+ const brushY = my - unclippedStartY
148
+ const brushRowOffset = brushY * brushSize
149
+
150
+ for (let mx = startX; mx < endX; mx++) {
151
+ const brushX = mx - unclippedStartX
152
+ const brushVal = brushData[brushRowOffset + brushX]
153
+
154
+ if (brushVal > 0) {
155
+ const strokeMaskIdx = strokeMaskRowOffset + (mx - bx)
156
+
157
+ if (brushVal > maskData[strokeMaskIdx]) {
158
+ maskData[strokeMaskIdx] = brushVal
159
+ }
122
160
  }
123
161
  }
124
162
  }
125
- }
126
- })
163
+ },
164
+ )
127
165
 
128
166
  blendColorPixelOptions.blendFn = blendFn
129
167
  blendColorPixelOptions.alpha = alpha
@@ -132,7 +170,12 @@ export const mutatorApplyCircleBrushStroke = ((writer: PixelWriter<any>, deps: D
132
170
  blendColorPixelOptions.w = bw
133
171
  blendColorPixelOptions.h = bh
134
172
 
135
- blendColorPixelDataAlphaMask(writer.target, color, mask, blendColorPixelOptions)
173
+ blendColorPixelDataAlphaMask(
174
+ writer.target,
175
+ color,
176
+ mask as AlphaMask,
177
+ blendColorPixelOptions,
178
+ )
136
179
  },
137
180
  }
138
181
  }) satisfies HistoryMutator<any, Deps>
@@ -1,4 +1,4 @@
1
- import type { BlendColor32, Color32, HistoryMutator, Rect } from '../../_types'
1
+ import type { BlendColor32, CircleBrushMask, Color32, HistoryMutator, Rect } from '../../_types'
2
2
  import { applyCircleBrushToPixelData } from '../../PixelData/applyCircleBrushToPixelData'
3
3
  import { getCircleBrushOrPencilBounds } from '../../Rect/getCircleBrushOrPencilBounds'
4
4
  import { PixelWriter } from '../PixelWriter'
@@ -6,16 +6,17 @@ import { PixelWriter } from '../PixelWriter'
6
6
  const defaults = {
7
7
  applyCircleBrushToPixelData,
8
8
  getCircleBrushOrPencilBounds,
9
- fallOff: () => 1,
10
9
  }
11
10
 
12
11
  type Deps = Partial<typeof defaults>
13
12
 
13
+ /**
14
+ * @param deps - @hidden
15
+ **/
14
16
  export const mutatorApplyCirclePencil = ((writer: PixelWriter<any>, deps: Deps = defaults) => {
15
17
  const {
16
18
  applyCircleBrushToPixelData = defaults.applyCircleBrushToPixelData,
17
19
  getCircleBrushOrPencilBounds = defaults.getCircleBrushOrPencilBounds,
18
- fallOff = defaults.fallOff,
19
20
  } = deps
20
21
 
21
22
  const boundsOut: Rect = { x: 0, y: 0, w: 0, h: 0 }
@@ -25,7 +26,7 @@ export const mutatorApplyCirclePencil = ((writer: PixelWriter<any>, deps: Deps =
25
26
  color: Color32,
26
27
  centerX: number,
27
28
  centerY: number,
28
- brushSize: number,
29
+ brush: CircleBrushMask,
29
30
  alpha = 255,
30
31
  blendFn?: BlendColor32,
31
32
  ) {
@@ -33,7 +34,7 @@ export const mutatorApplyCirclePencil = ((writer: PixelWriter<any>, deps: Deps =
33
34
  const bounds = getCircleBrushOrPencilBounds(
34
35
  centerX,
35
36
  centerY,
36
- brushSize,
37
+ brush.size,
37
38
  writer.target.width,
38
39
  writer.target.height,
39
40
  boundsOut,
@@ -48,9 +49,8 @@ export const mutatorApplyCirclePencil = ((writer: PixelWriter<any>, deps: Deps =
48
49
  color,
49
50
  centerX,
50
51
  centerY,
51
- brushSize,
52
+ brush,
52
53
  alpha,
53
- fallOff,
54
54
  blendFn,
55
55
  bounds,
56
56
  )
@@ -1,9 +1,11 @@
1
1
  import {
2
2
  type BinaryMask,
3
3
  type BlendColor32,
4
+ type CircleBrushBinaryMask,
4
5
  type Color32,
5
6
  type ColorBlendMaskOptions,
6
7
  type HistoryMutator,
8
+ MaskType,
7
9
  type Rect,
8
10
  } from '../../_types'
9
11
  import { forEachLinePoint } from '../../Algorithm/forEachLinePoint'
@@ -22,6 +24,9 @@ const defaults = {
22
24
 
23
25
  type Deps = Partial<typeof defaults>
24
26
 
27
+ /**
28
+ * @param deps - @hidden
29
+ */
25
30
  export const mutatorApplyCirclePencilStroke = ((writer: PixelWriter<any>, deps: Deps = defaults) => {
26
31
  const {
27
32
  forEachLinePoint = defaults.forEachLinePoint,
@@ -53,6 +58,13 @@ export const mutatorApplyCirclePencilStroke = ((writer: PixelWriter<any>, deps:
53
58
  h: 0,
54
59
  }
55
60
 
61
+ const mask = {
62
+ type: MaskType.BINARY,
63
+ data: null as unknown as Uint8Array,
64
+ w: 0,
65
+ h: 0,
66
+ }
67
+
56
68
  return {
57
69
  applyCirclePencilStroke(
58
70
  color: Color32,
@@ -60,7 +72,7 @@ export const mutatorApplyCirclePencilStroke = ((writer: PixelWriter<any>, deps:
60
72
  y0: number,
61
73
  x1: number,
62
74
  y1: number,
63
- brushSize: number,
75
+ brush: CircleBrushBinaryMask,
64
76
  alpha = 255,
65
77
  blendFn: BlendColor32 = sourceOverPerfect,
66
78
  ) {
@@ -69,54 +81,77 @@ export const mutatorApplyCirclePencilStroke = ((writer: PixelWriter<any>, deps:
69
81
  y: by,
70
82
  w: bw,
71
83
  h: bh,
72
- } = getCircleBrushOrPencilStrokeBounds(x0, y0, x1, y1, brushSize, strokeBoundsOut)
84
+ } = getCircleBrushOrPencilStrokeBounds(
85
+ x0,
86
+ y0,
87
+ x1,
88
+ y1,
89
+ brush.size,
90
+ strokeBoundsOut,
91
+ )
73
92
 
74
93
  if (bw <= 0 || bh <= 0) return
75
94
 
76
- const mask = new Uint8Array(bw * bh) as BinaryMask
77
-
78
- const r = brushSize / 2
79
- const rSqr = r * r
80
- const centerOffset = (brushSize % 2 === 0) ? 0.5 : 0
95
+ mask.data = new Uint8Array(bw * bh)
96
+ mask.w = bw
97
+ mask.h = bh
81
98
 
99
+ const maskData = mask.data
82
100
  const targetWidth = writer.target.width
83
101
  const targetHeight = writer.target.height
84
102
 
85
- forEachLinePoint(x0, y0, x1, y1, (px, py) => {
86
- // 2. Calculate bounds for this specific stamp
87
- const {
88
- x: cbx,
89
- y: cby,
90
- w: cbw,
91
- h: cbh,
92
- } = getCircleBrushOrPencilBounds(px, py, brushSize, targetWidth, targetHeight, circlePencilBounds)
93
-
94
- writer.accumulator.storeRegionBeforeState(cbx, cby, cbw, cbh)
95
-
96
- const startX = Math.max(bx, cbx)
97
- const startY = Math.max(by, cby)
98
- const endX = Math.min(bx + bw, cbx + cbw)
99
- const endY = Math.min(by + bh, cby + cbh)
100
-
101
- const fPx = Math.floor(px)
102
- const fPy = Math.floor(py)
103
-
104
- for (let my = startY; my < endY; my++) {
105
- const dy = (my - fPy) + centerOffset
106
- const dySqr = dy * dy
107
- const maskRowOffset = (my - by) * bw
108
-
109
- for (let mx = startX; mx < endX; mx++) {
110
- const dx = (mx - fPx) + centerOffset
111
- const dSqr = dx * dx + dySqr
112
-
113
- if (dSqr <= rSqr) {
114
- const maskIdx = maskRowOffset + (mx - bx)
115
- mask[maskIdx] = 1
103
+ forEachLinePoint(
104
+ x0,
105
+ y0,
106
+ x1,
107
+ y1,
108
+ (px, py) => {
109
+ const {
110
+ x: cbx,
111
+ y: cby,
112
+ w: cbw,
113
+ h: cbh,
114
+ } = getCircleBrushOrPencilBounds(
115
+ px,
116
+ py,
117
+ brush.size,
118
+ targetWidth,
119
+ targetHeight,
120
+ circlePencilBounds,
121
+ )
122
+
123
+ writer.accumulator.storeRegionBeforeState(
124
+ cbx,
125
+ cby,
126
+ cbw,
127
+ cbh,
128
+ )
129
+
130
+ const unclippedStartX = Math.floor(px + brush.minOffset)
131
+ const unclippedStartY = Math.floor(py + brush.minOffset)
132
+
133
+ const startX = Math.max(bx, unclippedStartX)
134
+ const startY = Math.max(by, unclippedStartY)
135
+ const endX = Math.min(bx + bw, unclippedStartX + brush.w)
136
+ const endY = Math.min(by + bh, unclippedStartY + brush.h)
137
+
138
+ for (let my = startY; my < endY; my++) {
139
+ const brushY = my - unclippedStartY
140
+ const maskRowOffset = (my - by) * bw
141
+ const brushRowOffset = brushY * brush.w
142
+
143
+ for (let mx = startX; mx < endX; mx++) {
144
+ const brushX = mx - unclippedStartX
145
+ const brushAlpha = brush.data[brushRowOffset + brushX]
146
+
147
+ if (brushAlpha > 0) {
148
+ const maskIdx = maskRowOffset + (mx - bx)
149
+ maskData[maskIdx] = brushAlpha
150
+ }
116
151
  }
117
152
  }
118
- }
119
- })
153
+ },
154
+ )
120
155
 
121
156
  blendColorPixelOptions.blendFn = blendFn
122
157
  blendColorPixelOptions.alpha = alpha
@@ -125,7 +160,12 @@ export const mutatorApplyCirclePencilStroke = ((writer: PixelWriter<any>, deps:
125
160
  blendColorPixelOptions.w = bw
126
161
  blendColorPixelOptions.h = bh
127
162
 
128
- blendColorPixelDataBinaryMask(writer.target, color, mask, blendColorPixelOptions)
163
+ blendColorPixelDataBinaryMask(
164
+ writer.target,
165
+ color,
166
+ mask as BinaryMask,
167
+ blendColorPixelOptions,
168
+ )
129
169
  },
130
170
  }
131
171
  }) satisfies HistoryMutator<any, Deps>
@@ -10,6 +10,9 @@ const defaults = {
10
10
 
11
11
  type Deps = Partial<typeof defaults>
12
12
 
13
+ /**
14
+ * @param deps - @hidden
15
+ */
13
16
  export const mutatorApplyRectBrush = ((writer: PixelWriter<any>, deps: Deps = defaults) => {
14
17
  const {
15
18
  applyRectBrushToPixelData = defaults.applyRectBrushToPixelData,