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.
- package/README.md +6 -1
- package/dist/index.dev.cjs +2723 -1487
- package/dist/index.dev.cjs.map +1 -1
- package/dist/index.dev.js +2690 -1481
- package/dist/index.dev.js.map +1 -1
- package/dist/index.prod.cjs +2723 -1487
- package/dist/index.prod.cjs.map +1 -1
- package/dist/index.prod.d.ts +400 -246
- package/dist/index.prod.js +2690 -1481
- package/dist/index.prod.js.map +1 -1
- package/package.json +22 -7
- package/src/Algorithm/forEachLinePoint.ts +36 -0
- package/src/BlendModes/BlendModeRegistry.ts +2 -0
- package/src/BlendModes/blend-modes-fast.ts +2 -2
- package/src/BlendModes/blend-modes-perfect.ts +5 -4
- package/src/BlendModes/toBlendModeIndexAndName.ts +41 -0
- package/src/History/PixelAccumulator.ts +2 -2
- package/src/History/PixelMutator/mutatorApplyAlphaMask.ts +30 -0
- package/src/History/PixelMutator/mutatorApplyBinaryMask.ts +30 -0
- package/src/History/PixelMutator/mutatorApplyCircleBrush.ts +23 -9
- package/src/History/PixelMutator/mutatorApplyCircleBrushStroke.ts +138 -0
- package/src/History/PixelMutator/mutatorApplyCirclePencil.ts +59 -0
- package/src/History/PixelMutator/mutatorApplyCirclePencilStroke.ts +131 -0
- package/src/History/PixelMutator/mutatorApplyRectBrush.ts +20 -7
- package/src/History/PixelMutator/mutatorApplyRectBrushStroke.ts +169 -0
- package/src/History/PixelMutator/mutatorApplyRectPencil.ts +62 -0
- package/src/History/PixelMutator/mutatorApplyRectPencilStroke.ts +149 -0
- package/src/History/PixelMutator/mutatorBlendColor.ts +9 -4
- package/src/History/PixelMutator/mutatorBlendPixelData.ts +10 -5
- package/src/History/PixelMutator/mutatorClear.ts +27 -0
- package/src/History/PixelMutator/{mutatorFillPixelData.ts → mutatorFill.ts} +9 -3
- package/src/History/PixelMutator/mutatorInvert.ts +10 -3
- package/src/History/PixelMutator.ts +23 -3
- package/src/History/PixelPatchTiles.ts +2 -2
- package/src/History/PixelWriter.ts +3 -3
- package/src/ImageData/ImageDataLike.ts +13 -0
- package/src/ImageData/extractImageDataBuffer.ts +22 -15
- package/src/ImageData/serialization.ts +4 -4
- package/src/ImageData/uInt32ArrayToImageData.ts +29 -0
- package/src/ImageData/writeImageData.ts +26 -18
- package/src/ImageData/writeImageDataBuffer.ts +30 -18
- package/src/IndexedImage/indexedImageToAverageColor.ts +1 -1
- package/src/Internal/resolveClipping.ts +140 -0
- package/src/Mask/applyBinaryMaskToAlphaMask.ts +89 -0
- package/src/Mask/copyMask.ts +1 -3
- package/src/Mask/mergeAlphaMasks.ts +81 -0
- package/src/Mask/mergeBinaryMasks.ts +89 -0
- package/src/PixelData/PixelBuffer32.ts +28 -0
- package/src/PixelData/PixelData.ts +38 -33
- package/src/PixelData/applyAlphaMaskToPixelData.ts +119 -0
- package/src/PixelData/applyBinaryMaskToPixelData.ts +111 -0
- package/src/PixelData/applyCircleBrushToPixelData.ts +31 -56
- package/src/PixelData/applyRectBrushToPixelData.ts +39 -71
- package/src/PixelData/blendColorPixelData.ts +18 -111
- package/src/PixelData/blendColorPixelDataAlphaMask.ts +111 -0
- package/src/PixelData/blendColorPixelDataBinaryMask.ts +89 -0
- package/src/PixelData/blendPixelData.ts +19 -107
- package/src/PixelData/blendPixelDataAlphaMask.ts +149 -0
- package/src/PixelData/blendPixelDataBinaryMask.ts +133 -0
- package/src/PixelData/clearPixelData.ts +2 -3
- package/src/PixelData/extractPixelData.ts +4 -4
- package/src/PixelData/extractPixelDataBuffer.ts +38 -26
- package/src/PixelData/fillPixelData.ts +18 -20
- package/src/PixelData/invertPixelData.ts +13 -21
- package/src/PixelData/pixelDataToAlphaMask.ts +2 -3
- package/src/PixelData/reflectPixelData.ts +3 -3
- package/src/PixelData/resamplePixelData.ts +2 -6
- package/src/PixelData/writePixelDataBuffer.ts +34 -20
- package/src/Rect/getCircleBrushOrPencilBounds.ts +43 -0
- package/src/Rect/getCircleBrushOrPencilStrokeBounds.ts +24 -0
- package/src/Rect/getRectBrushOrPencilBounds.ts +38 -0
- package/src/Rect/getRectBrushOrPencilStrokeBounds.ts +26 -0
- package/src/_types.ts +49 -33
- package/src/index.ts +47 -11
- package/src/History/PixelMutator/mutatorApplyMask.ts +0 -20
- package/src/Mask/mergeMasks.ts +0 -100
- package/src/PixelData/applyMaskToPixelData.ts +0 -129
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pixel-data-js",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "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
|
|
54
|
+
"docs": "npx typedoc",
|
|
54
55
|
"typecheck": "tsc --noEmit",
|
|
55
|
-
"check-exports": "tsx
|
|
56
|
-
"
|
|
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
|
-
"
|
|
70
|
-
"
|
|
71
|
-
"
|
|
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
|
|
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 {
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
29
|
+
fallOff: (dist: number) => number,
|
|
16
30
|
blendFn?: BlendColor32,
|
|
17
31
|
) {
|
|
18
32
|
|
|
19
|
-
const
|
|
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 } =
|
|
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
|
-
|
|
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>
|