pixel-data-js 0.18.0 → 0.19.1

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 (77) hide show
  1. package/README.md +6 -1
  2. package/dist/index.dev.cjs +2723 -1487
  3. package/dist/index.dev.cjs.map +1 -1
  4. package/dist/index.dev.js +2690 -1481
  5. package/dist/index.dev.js.map +1 -1
  6. package/dist/index.prod.cjs +2723 -1487
  7. package/dist/index.prod.cjs.map +1 -1
  8. package/dist/index.prod.d.ts +400 -246
  9. package/dist/index.prod.js +2690 -1481
  10. package/dist/index.prod.js.map +1 -1
  11. package/package.json +22 -7
  12. package/src/Algorithm/forEachLinePoint.ts +36 -0
  13. package/src/BlendModes/BlendModeRegistry.ts +2 -0
  14. package/src/BlendModes/blend-modes-fast.ts +2 -2
  15. package/src/BlendModes/blend-modes-perfect.ts +5 -4
  16. package/src/BlendModes/toBlendModeIndexAndName.ts +41 -0
  17. package/src/History/PixelAccumulator.ts +2 -2
  18. package/src/History/PixelMutator/mutatorApplyAlphaMask.ts +30 -0
  19. package/src/History/PixelMutator/mutatorApplyBinaryMask.ts +30 -0
  20. package/src/History/PixelMutator/mutatorApplyCircleBrush.ts +23 -9
  21. package/src/History/PixelMutator/mutatorApplyCircleBrushStroke.ts +138 -0
  22. package/src/History/PixelMutator/mutatorApplyCirclePencil.ts +59 -0
  23. package/src/History/PixelMutator/mutatorApplyCirclePencilStroke.ts +131 -0
  24. package/src/History/PixelMutator/mutatorApplyRectBrush.ts +20 -7
  25. package/src/History/PixelMutator/mutatorApplyRectBrushStroke.ts +169 -0
  26. package/src/History/PixelMutator/mutatorApplyRectPencil.ts +62 -0
  27. package/src/History/PixelMutator/mutatorApplyRectPencilStroke.ts +149 -0
  28. package/src/History/PixelMutator/mutatorBlendColor.ts +9 -4
  29. package/src/History/PixelMutator/mutatorBlendPixelData.ts +10 -5
  30. package/src/History/PixelMutator/mutatorClear.ts +27 -0
  31. package/src/History/PixelMutator/{mutatorFillPixelData.ts → mutatorFill.ts} +9 -3
  32. package/src/History/PixelMutator/mutatorInvert.ts +10 -3
  33. package/src/History/PixelMutator.ts +23 -3
  34. package/src/History/PixelPatchTiles.ts +2 -2
  35. package/src/History/PixelWriter.ts +3 -3
  36. package/src/ImageData/ImageDataLike.ts +13 -0
  37. package/src/ImageData/extractImageDataBuffer.ts +22 -15
  38. package/src/ImageData/serialization.ts +4 -4
  39. package/src/ImageData/uInt32ArrayToImageData.ts +29 -0
  40. package/src/ImageData/writeImageData.ts +26 -18
  41. package/src/ImageData/writeImageDataBuffer.ts +30 -18
  42. package/src/IndexedImage/indexedImageToAverageColor.ts +1 -1
  43. package/src/Internal/resolveClipping.ts +140 -0
  44. package/src/Mask/applyBinaryMaskToAlphaMask.ts +89 -0
  45. package/src/Mask/copyMask.ts +1 -3
  46. package/src/Mask/mergeAlphaMasks.ts +81 -0
  47. package/src/Mask/mergeBinaryMasks.ts +89 -0
  48. package/src/PixelData/PixelBuffer32.ts +28 -0
  49. package/src/PixelData/PixelData.ts +38 -33
  50. package/src/PixelData/applyAlphaMaskToPixelData.ts +119 -0
  51. package/src/PixelData/applyBinaryMaskToPixelData.ts +111 -0
  52. package/src/PixelData/applyCircleBrushToPixelData.ts +31 -56
  53. package/src/PixelData/applyRectBrushToPixelData.ts +39 -71
  54. package/src/PixelData/blendColorPixelData.ts +18 -111
  55. package/src/PixelData/blendColorPixelDataAlphaMask.ts +111 -0
  56. package/src/PixelData/blendColorPixelDataBinaryMask.ts +89 -0
  57. package/src/PixelData/blendPixelData.ts +19 -107
  58. package/src/PixelData/blendPixelDataAlphaMask.ts +149 -0
  59. package/src/PixelData/blendPixelDataBinaryMask.ts +133 -0
  60. package/src/PixelData/clearPixelData.ts +2 -3
  61. package/src/PixelData/extractPixelData.ts +4 -4
  62. package/src/PixelData/extractPixelDataBuffer.ts +38 -26
  63. package/src/PixelData/fillPixelData.ts +18 -20
  64. package/src/PixelData/invertPixelData.ts +13 -21
  65. package/src/PixelData/pixelDataToAlphaMask.ts +2 -3
  66. package/src/PixelData/reflectPixelData.ts +3 -3
  67. package/src/PixelData/resamplePixelData.ts +2 -6
  68. package/src/PixelData/writePixelDataBuffer.ts +34 -20
  69. package/src/Rect/getCircleBrushOrPencilBounds.ts +43 -0
  70. package/src/Rect/getCircleBrushOrPencilStrokeBounds.ts +24 -0
  71. package/src/Rect/getRectBrushOrPencilBounds.ts +38 -0
  72. package/src/Rect/getRectBrushOrPencilStrokeBounds.ts +26 -0
  73. package/src/_types.ts +49 -33
  74. package/src/index.ts +47 -11
  75. package/src/History/PixelMutator/mutatorApplyMask.ts +0 -20
  76. package/src/Mask/mergeMasks.ts +0 -100
  77. package/src/PixelData/applyMaskToPixelData.ts +0 -129
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pixel-data-js",
3
3
  "type": "module",
4
- "version": "0.18.0",
4
+ "version": "0.19.1",
5
5
  "packageManager": "pnpm@10.30.0",
6
6
  "description": "JS Pixel and ImageData operations",
7
7
  "author": {
@@ -49,32 +49,47 @@
49
49
  "scripts": {
50
50
  "build": "tsup",
51
51
  "test": "vitest --coverage --project unit",
52
+ "test:build": "pnpm build && vitest run --project dist",
52
53
  "test:mutation": "stryker run",
53
- "docs": "npx typedoc --entryPointStrategy Expand src",
54
+ "docs": "npx typedoc",
54
55
  "typecheck": "tsc --noEmit",
55
- "check-exports": "tsx bin/check-exports.ts -- .test.ts .d.ts",
56
- "bench": "npx vitest --project browser"
56
+ "check-exports": "tsx _scripts/check-exports.ts",
57
+ "sort": "tsx _scripts/apply-sorting.ts",
58
+ "bench": "tsx ./benchmark/run.ts",
59
+ "bench:all": "tsx ./benchmark/run-all.ts",
60
+ "bench:compare": "tsx ./benchmark/compare.ts"
57
61
  },
58
62
  "devDependencies": {
63
+ "@clack/prompts": "^1.1.0",
64
+ "@mitata/counters": "^0.0.8",
59
65
  "@napi-rs/canvas": "^0.1.93",
60
66
  "@stryker-mutator/core": "^9.5.1",
61
67
  "@stryker-mutator/typescript-checker": "^9.5.1",
62
68
  "@stryker-mutator/vitest-runner": "^9.5.1",
69
+ "@types/cli-progress": "^3.11.6",
63
70
  "@types/node": "^25.2.3",
64
71
  "@vitest/browser": "3.2.4",
65
72
  "@vitest/coverage-v8": "3.2.4",
73
+ "cli-progress": "^3.12.0",
74
+ "cmd-ts": "^0.15.0",
66
75
  "console-table-printer": "^2.15.0",
67
76
  "esbuild": "^0.27.3",
77
+ "execa": "^9.6.1",
78
+ "fast-glob": "^3.3.3",
68
79
  "jsdom": "^28.1.0",
69
- "playwright": "^1.58.2",
70
- "pretty-ms": "^9.3.0",
71
- "tinybench": "^6.0.0",
80
+ "mitata-ts": "^1.0.4",
81
+ "picocolors": "^1.1.1",
82
+ "sanitize-filename": "^1.6.3",
83
+ "sharp": "^0.34.5",
84
+ "simple-git": "^3.33.0",
72
85
  "tsup": "^8.5.1",
73
86
  "tsx": "^4.21.0",
74
87
  "typedoc": "^0.28.17",
75
88
  "typedoc-plugin-mdn-links": "^5.1.1",
76
89
  "typedoc-rhineai-theme": "^1.2.0",
77
90
  "typescript": "^5.9.3",
91
+ "unplugin-inline": "^1.8.0",
92
+ "vite-tsconfig-paths": "^6.1.1",
78
93
  "vitest": "3.2.4"
79
94
  },
80
95
  "repository": {
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Iterates through a line with sub-pixel precision.
3
+ * Guarantees that the first and last points are exactly (x0, y0) and (x1, y1).
4
+ */
5
+ export function forEachLinePoint(
6
+ x0: number,
7
+ y0: number,
8
+ x1: number,
9
+ y1: number,
10
+ callback: (x: number, y: number) => void,
11
+ ): void {
12
+ const dx = x1 - x0
13
+ const dy = y1 - y0
14
+
15
+ // Determine the number of steps based on the longest axis
16
+ const steps = Math.max(Math.abs(dx), Math.abs(dy))
17
+
18
+ // Handle the zero-length line (Single Stamp Case)
19
+ if (steps === 0) {
20
+ callback(x0, y0)
21
+ return
22
+ }
23
+
24
+ const xInc = dx / steps
25
+ const yInc = dy / steps
26
+
27
+ let curX = x0
28
+ let curY = y0
29
+
30
+ // We add +1 to the loop to ensure we reach the final (x1, y1)
31
+ for (let i = 0; i <= steps; i++) {
32
+ callback(curX, curY)
33
+ curX += xInc
34
+ curY += yInc
35
+ }
36
+ }
@@ -15,6 +15,7 @@ export function makeBlendModeRegistry<
15
15
  >(
16
16
  blendModes: BlendModes,
17
17
  initialEntries: Record<Index, BlendColor32>,
18
+ registryName = 'anonymous',
18
19
  ) {
19
20
 
20
21
  const blendToName = new Map<BlendColor32, Name>()
@@ -47,6 +48,7 @@ export function makeBlendModeRegistry<
47
48
  }
48
49
 
49
50
  return {
51
+ registryName,
50
52
  nameToBlend,
51
53
  nameToIndex,
52
54
 
@@ -604,6 +604,6 @@ export const BASE_FAST_BLEND_MODE_FUNCTIONS: Record<number, BlendColor32> = {
604
604
  [BaseBlendMode.divide]: divideFast,
605
605
  }
606
606
 
607
- export function makeFastBlendModeRegistry() {
608
- return makeBlendModeRegistry(BaseBlendMode, BASE_FAST_BLEND_MODE_FUNCTIONS)
607
+ export function makeFastBlendModeRegistry(name = 'fast') {
608
+ return makeBlendModeRegistry(BaseBlendMode, BASE_FAST_BLEND_MODE_FUNCTIONS, name)
609
609
  }
@@ -9,12 +9,13 @@ export const sourceOverPerfect: BlendColor32 = (src, dst) => {
9
9
  if (sa === 255) return src
10
10
  if (sa === 0) return dst
11
11
 
12
- const invA = 255 - sa
12
+ const da = (dst >>> 24) & 0xFF
13
+ if (da === 0) return src
13
14
 
14
15
  const sr = src & 0xFF, sg = (src >>> 8) & 0xFF, sb = (src >>> 16) & 0xFF
15
16
  const dr = dst & 0xFF, dg = (dst >>> 8) & 0xFF, db = (dst >>> 16) & 0xFF
16
- const da = (dst >>> 24) & 0xFF
17
17
 
18
+ const invA = 255 - sa
18
19
  // Exact division by 255 using bit-shifts
19
20
  // Formula: (v + 1 + (v >> 8)) >> 8
20
21
  const tR = (sr * sa + dr * invA)
@@ -774,6 +775,6 @@ export const BASE_PERFECT_BLEND_MODE_FUNCTIONS: Record<number, BlendColor32> = {
774
775
  [BaseBlendMode.divide]: dividePerfect,
775
776
  }
776
777
 
777
- export function makePerfectBlendModeRegistry() {
778
- return makeBlendModeRegistry(BaseBlendMode, BASE_PERFECT_BLEND_MODE_FUNCTIONS)
778
+ export function makePerfectBlendModeRegistry(name = 'perfect') {
779
+ return makeBlendModeRegistry(BaseBlendMode, BASE_PERFECT_BLEND_MODE_FUNCTIONS, name)
779
780
  }
@@ -0,0 +1,41 @@
1
+ import { BaseBlendMode } from './blend-modes'
2
+
3
+ export function toBlendModeIndexAndName(input: string | number) {
4
+ if (typeof input === 'number') {
5
+ const name = getKeyByValue(BaseBlendMode, input)
6
+ if (name === undefined) throw new Error(`Invalid index: ${input}`)
7
+ return { blendIndex: input, blendName: name }
8
+ }
9
+
10
+ const trimmed = input.trim()
11
+ const num = Number(trimmed)
12
+ const isNumeric = trimmed !== '' && !Number.isNaN(num)
13
+
14
+ if (isNumeric && Number.isInteger(num)) {
15
+ console.log({
16
+ trimmed,
17
+ num,
18
+ isNumeric,
19
+ isInt: Number.isInteger(num),
20
+ })
21
+ const name = getKeyByValue(BaseBlendMode, num)
22
+ console.log({name})
23
+ if (name === undefined) throw new Error(`Invalid index: ${num}`)
24
+ return { blendIndex: num, blendName: name }
25
+ }
26
+
27
+ if (trimmed in BaseBlendMode) {
28
+ return {
29
+ blendIndex: BaseBlendMode[trimmed as keyof typeof BaseBlendMode],
30
+ blendName: trimmed as keyof typeof BaseBlendMode,
31
+ }
32
+ }
33
+
34
+ throw new Error(`Invalid blend mode: ${JSON.stringify(input)}`)
35
+ }
36
+
37
+ const getKeyByValue = (obj: any, value: any) => {
38
+ for (const key in obj) {
39
+ if (obj[key] === value) return key
40
+ }
41
+ }
@@ -1,4 +1,4 @@
1
- import type { PixelData } from '../PixelData/PixelData'
1
+ import type { IPixelData } from '../_types'
2
2
  import type { PixelEngineConfig } from './PixelEngineConfig'
3
3
  import { type PixelPatchTiles, PixelTile } from './PixelPatchTiles'
4
4
 
@@ -8,7 +8,7 @@ export class PixelAccumulator {
8
8
  public pool: PixelTile[]
9
9
 
10
10
  constructor(
11
- public target: PixelData,
11
+ public target: IPixelData,
12
12
  readonly config: PixelEngineConfig,
13
13
  ) {
14
14
  this.lookup = []
@@ -0,0 +1,30 @@
1
+ import { type AlphaMask, type ApplyMaskToPixelDataOptions, type HistoryMutator } from '../../_types'
2
+ import { applyAlphaMaskToPixelData } from '../../PixelData/applyAlphaMaskToPixelData'
3
+ import { PixelWriter } from '../PixelWriter'
4
+
5
+ const defaults = {
6
+ applyAlphaMaskToPixelData,
7
+ }
8
+
9
+ type Deps = Partial<typeof defaults>
10
+
11
+ export const mutatorApplyAlphaMask = ((writer: PixelWriter<any>, deps: Deps = defaults) => {
12
+ const {
13
+ applyAlphaMaskToPixelData = defaults.applyAlphaMaskToPixelData,
14
+ } = deps
15
+
16
+ return {
17
+ applyAlphaMask: (mask: AlphaMask, opts: ApplyMaskToPixelDataOptions = {}) => {
18
+ let target = writer.target
19
+ const {
20
+ x = 0,
21
+ y = 0,
22
+ w = writer.target.width,
23
+ h = writer.target.height,
24
+ } = opts
25
+
26
+ writer.accumulator.storeRegionBeforeState(x, y, w, h)
27
+ applyAlphaMaskToPixelData(target, mask, opts)
28
+ },
29
+ }
30
+ }) satisfies HistoryMutator<any, Deps>
@@ -0,0 +1,30 @@
1
+ import { type ApplyMaskToPixelDataOptions, type BinaryMask, type HistoryMutator } from '../../_types'
2
+ import { applyBinaryMaskToPixelData } from '../../PixelData/applyBinaryMaskToPixelData'
3
+ import { PixelWriter } from '../PixelWriter'
4
+
5
+ const defaults = {
6
+ applyBinaryMaskToPixelData,
7
+ }
8
+
9
+ type Deps = Partial<typeof defaults>
10
+
11
+ export const mutatorApplyBinaryMask = ((writer: PixelWriter<any>, deps: Deps = defaults) => {
12
+ const {
13
+ applyBinaryMaskToPixelData = defaults.applyBinaryMaskToPixelData,
14
+ } = deps
15
+
16
+ return {
17
+ applyBinaryMask: (mask: BinaryMask, opts: ApplyMaskToPixelDataOptions = {}) => {
18
+ let target = writer.target
19
+ const {
20
+ x = 0,
21
+ y = 0,
22
+ w = writer.target.width,
23
+ h = writer.target.height,
24
+ } = opts
25
+
26
+ writer.accumulator.storeRegionBeforeState(x, y, w, h)
27
+ applyBinaryMaskToPixelData(target, mask, opts)
28
+ },
29
+ }
30
+ }) satisfies HistoryMutator<any, Deps>
@@ -1,10 +1,24 @@
1
- import type { BlendColor32, Color32, Rect } from '../../_types'
2
- import { applyCircleBrushToPixelData, getCircleBrushBounds } from '../../PixelData/applyCircleBrushToPixelData'
1
+ import type { BlendColor32, Color32, HistoryMutator, Rect } from '../../_types'
2
+ import { applyCircleBrushToPixelData } from '../../PixelData/applyCircleBrushToPixelData'
3
+ import { getCircleBrushOrPencilBounds } from '../../Rect/getCircleBrushOrPencilBounds'
3
4
  import { PixelWriter } from '../PixelWriter'
4
5
 
5
- const boundsOut: Rect = { x: 0, y: 0, w: 0, h: 0 }
6
+ const defaults = {
7
+ applyCircleBrushToPixelData,
8
+ getCircleBrushOrPencilBounds,
9
+ }
10
+
11
+ type Deps = Partial<typeof defaults>
12
+
13
+ export const mutatorApplyCircleBrush = ((writer: PixelWriter<any>, deps: Deps = defaults) => {
14
+ const {
15
+ applyCircleBrushToPixelData = defaults.applyCircleBrushToPixelData,
16
+ getCircleBrushOrPencilBounds = defaults.getCircleBrushOrPencilBounds,
17
+
18
+ } = deps
19
+
20
+ const boundsOut: Rect = { x: 0, y: 0, w: 0, h: 0 }
6
21
 
7
- export function mutatorApplyCircleBrush(writer: PixelWriter<any>) {
8
22
  return {
9
23
  applyCircleBrush(
10
24
  color: Color32,
@@ -12,11 +26,11 @@ export function mutatorApplyCircleBrush(writer: PixelWriter<any>) {
12
26
  centerY: number,
13
27
  brushSize: number,
14
28
  alpha = 255,
15
- fallOff?: (dist: number) => number,
29
+ fallOff: (dist: number) => number,
16
30
  blendFn?: BlendColor32,
17
31
  ) {
18
32
 
19
- const circleBounds = getCircleBrushBounds(
33
+ const bounds = getCircleBrushOrPencilBounds(
20
34
  centerX,
21
35
  centerY,
22
36
  brushSize,
@@ -25,7 +39,7 @@ export function mutatorApplyCircleBrush(writer: PixelWriter<any>) {
25
39
  boundsOut,
26
40
  )
27
41
 
28
- const { x, y, w, h } = circleBounds
42
+ const { x, y, w, h } = bounds
29
43
 
30
44
  writer.accumulator.storeRegionBeforeState(x, y, w, h)
31
45
 
@@ -38,8 +52,8 @@ export function mutatorApplyCircleBrush(writer: PixelWriter<any>) {
38
52
  alpha,
39
53
  fallOff,
40
54
  blendFn,
41
- circleBounds,
55
+ bounds,
42
56
  )
43
57
  },
44
58
  }
45
- }
59
+ }) satisfies HistoryMutator<any, Deps>
@@ -0,0 +1,138 @@
1
+ import {
2
+ type AlphaMask,
3
+ type BlendColor32,
4
+ type Color32,
5
+ type ColorBlendMaskOptions,
6
+ type HistoryMutator,
7
+ type Rect,
8
+ } from '../../_types'
9
+ import { forEachLinePoint } from '../../Algorithm/forEachLinePoint'
10
+ import { sourceOverPerfect } from '../../BlendModes/blend-modes-perfect'
11
+ import { blendColorPixelDataAlphaMask } from '../../PixelData/blendColorPixelDataAlphaMask'
12
+ import { getCircleBrushOrPencilBounds } from '../../Rect/getCircleBrushOrPencilBounds'
13
+ import { getCircleBrushOrPencilStrokeBounds } from '../../Rect/getCircleBrushOrPencilStrokeBounds'
14
+ import { PixelWriter } from '../PixelWriter'
15
+
16
+ const defaults = {
17
+ forEachLinePoint,
18
+ blendColorPixelDataAlphaMask,
19
+ getCircleBrushOrPencilBounds,
20
+ getCircleBrushOrPencilStrokeBounds,
21
+ }
22
+
23
+ type Deps = Partial<typeof defaults>
24
+
25
+ export const mutatorApplyCircleBrushStroke = ((writer: PixelWriter<any>, deps: Deps = defaults) => {
26
+ const {
27
+ forEachLinePoint = defaults.forEachLinePoint,
28
+ blendColorPixelDataAlphaMask = defaults.blendColorPixelDataAlphaMask,
29
+ getCircleBrushOrPencilBounds = defaults.getCircleBrushOrPencilBounds,
30
+ getCircleBrushOrPencilStrokeBounds = defaults.getCircleBrushOrPencilStrokeBounds,
31
+ } = deps
32
+
33
+ const strokeBoundsOut: Rect = {
34
+ x: 0,
35
+ y: 0,
36
+ w: 0,
37
+ h: 0,
38
+ }
39
+
40
+ const circleBrushBounds: Rect = {
41
+ x: 0,
42
+ y: 0,
43
+ w: 0,
44
+ h: 0,
45
+ }
46
+
47
+ const blendColorPixelOptions: ColorBlendMaskOptions = {
48
+ alpha: 255,
49
+ blendFn: sourceOverPerfect,
50
+ x: 0,
51
+ y: 0,
52
+ w: 0,
53
+ h: 0,
54
+ }
55
+
56
+ return {
57
+ applyCircleBrushStroke(
58
+ color: Color32,
59
+ x0: number,
60
+ y0: number,
61
+ x1: number,
62
+ y1: number,
63
+ brushSize: number,
64
+ alpha = 255,
65
+ fallOff: (dist: number) => number,
66
+ blendFn: BlendColor32 = sourceOverPerfect,
67
+ ) {
68
+ const {
69
+ x: bx,
70
+ y: by,
71
+ w: bw,
72
+ h: bh,
73
+ } = getCircleBrushOrPencilStrokeBounds(x0, y0, x1, y1, brushSize, strokeBoundsOut)
74
+
75
+ if (bw <= 0 || bh <= 0) return
76
+
77
+ const mask = new Uint8Array(bw * bh) as AlphaMask
78
+
79
+ const r = brushSize / 2
80
+ const rSqr = r * r
81
+ const invR = 1 / r
82
+ const centerOffset = (brushSize % 2 === 0) ? 0.5 : 0
83
+
84
+ const targetWidth = writer.target.width
85
+ const targetHeight = writer.target.height
86
+
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
122
+ }
123
+ }
124
+ }
125
+ }
126
+ })
127
+
128
+ blendColorPixelOptions.blendFn = blendFn
129
+ blendColorPixelOptions.alpha = alpha
130
+ blendColorPixelOptions.x = bx
131
+ blendColorPixelOptions.y = by
132
+ blendColorPixelOptions.w = bw
133
+ blendColorPixelOptions.h = bh
134
+
135
+ blendColorPixelDataAlphaMask(writer.target, color, mask, blendColorPixelOptions)
136
+ },
137
+ }
138
+ }) satisfies HistoryMutator<any, Deps>
@@ -0,0 +1,59 @@
1
+ import type { BlendColor32, Color32, HistoryMutator, Rect } from '../../_types'
2
+ import { applyCircleBrushToPixelData } from '../../PixelData/applyCircleBrushToPixelData'
3
+ import { getCircleBrushOrPencilBounds } from '../../Rect/getCircleBrushOrPencilBounds'
4
+ import { PixelWriter } from '../PixelWriter'
5
+
6
+ const defaults = {
7
+ applyCircleBrushToPixelData,
8
+ getCircleBrushOrPencilBounds,
9
+ fallOff: () => 1,
10
+ }
11
+
12
+ type Deps = Partial<typeof defaults>
13
+
14
+ export const mutatorApplyCirclePencil = ((writer: PixelWriter<any>, deps: Deps = defaults) => {
15
+ const {
16
+ applyCircleBrushToPixelData = defaults.applyCircleBrushToPixelData,
17
+ getCircleBrushOrPencilBounds = defaults.getCircleBrushOrPencilBounds,
18
+ fallOff = defaults.fallOff,
19
+ } = deps
20
+
21
+ const boundsOut: Rect = { x: 0, y: 0, w: 0, h: 0 }
22
+
23
+ return {
24
+ applyCirclePencil(
25
+ color: Color32,
26
+ centerX: number,
27
+ centerY: number,
28
+ brushSize: number,
29
+ alpha = 255,
30
+ blendFn?: BlendColor32,
31
+ ) {
32
+
33
+ const bounds = getCircleBrushOrPencilBounds(
34
+ centerX,
35
+ centerY,
36
+ brushSize,
37
+ writer.target.width,
38
+ writer.target.height,
39
+ boundsOut,
40
+ )
41
+
42
+ const { x, y, w, h } = bounds
43
+
44
+ writer.accumulator.storeRegionBeforeState(x, y, w, h)
45
+
46
+ applyCircleBrushToPixelData(
47
+ writer.target,
48
+ color,
49
+ centerX,
50
+ centerY,
51
+ brushSize,
52
+ alpha,
53
+ fallOff,
54
+ blendFn,
55
+ bounds,
56
+ )
57
+ },
58
+ }
59
+ }) satisfies HistoryMutator<any, Deps>
@@ -0,0 +1,131 @@
1
+ import {
2
+ type BinaryMask,
3
+ type BlendColor32,
4
+ type Color32,
5
+ type ColorBlendMaskOptions,
6
+ type HistoryMutator,
7
+ type Rect,
8
+ } from '../../_types'
9
+ import { forEachLinePoint } from '../../Algorithm/forEachLinePoint'
10
+ import { sourceOverPerfect } from '../../BlendModes/blend-modes-perfect'
11
+ import { blendColorPixelDataBinaryMask } from '../../PixelData/blendColorPixelDataBinaryMask'
12
+ import { getCircleBrushOrPencilBounds } from '../../Rect/getCircleBrushOrPencilBounds'
13
+ import { getCircleBrushOrPencilStrokeBounds } from '../../Rect/getCircleBrushOrPencilStrokeBounds'
14
+ import { PixelWriter } from '../PixelWriter'
15
+
16
+ const defaults = {
17
+ forEachLinePoint,
18
+ blendColorPixelDataBinaryMask,
19
+ getCircleBrushOrPencilBounds,
20
+ getCircleBrushOrPencilStrokeBounds,
21
+ }
22
+
23
+ type Deps = Partial<typeof defaults>
24
+
25
+ export const mutatorApplyCirclePencilStroke = ((writer: PixelWriter<any>, deps: Deps = defaults) => {
26
+ const {
27
+ forEachLinePoint = defaults.forEachLinePoint,
28
+ blendColorPixelDataBinaryMask = defaults.blendColorPixelDataBinaryMask,
29
+ getCircleBrushOrPencilStrokeBounds = defaults.getCircleBrushOrPencilStrokeBounds,
30
+ getCircleBrushOrPencilBounds = defaults.getCircleBrushOrPencilBounds,
31
+ } = deps
32
+
33
+ const strokeBoundsOut: Rect = {
34
+ x: 0,
35
+ y: 0,
36
+ w: 0,
37
+ h: 0,
38
+ }
39
+
40
+ const circlePencilBounds: Rect = {
41
+ x: 0,
42
+ y: 0,
43
+ w: 0,
44
+ h: 0,
45
+ }
46
+
47
+ const blendColorPixelOptions: ColorBlendMaskOptions = {
48
+ alpha: 255,
49
+ blendFn: sourceOverPerfect,
50
+ x: 0,
51
+ y: 0,
52
+ w: 0,
53
+ h: 0,
54
+ }
55
+
56
+ return {
57
+ applyCirclePencilStroke(
58
+ color: Color32,
59
+ x0: number,
60
+ y0: number,
61
+ x1: number,
62
+ y1: number,
63
+ brushSize: number,
64
+ alpha = 255,
65
+ blendFn: BlendColor32 = sourceOverPerfect,
66
+ ) {
67
+ const {
68
+ x: bx,
69
+ y: by,
70
+ w: bw,
71
+ h: bh,
72
+ } = getCircleBrushOrPencilStrokeBounds(x0, y0, x1, y1, brushSize, strokeBoundsOut)
73
+
74
+ if (bw <= 0 || bh <= 0) return
75
+
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
81
+
82
+ const targetWidth = writer.target.width
83
+ const targetHeight = writer.target.height
84
+
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
116
+ }
117
+ }
118
+ }
119
+ })
120
+
121
+ blendColorPixelOptions.blendFn = blendFn
122
+ blendColorPixelOptions.alpha = alpha
123
+ blendColorPixelOptions.x = bx
124
+ blendColorPixelOptions.y = by
125
+ blendColorPixelOptions.w = bw
126
+ blendColorPixelOptions.h = bh
127
+
128
+ blendColorPixelDataBinaryMask(writer.target, color, mask, blendColorPixelOptions)
129
+ },
130
+ }
131
+ }) satisfies HistoryMutator<any, Deps>