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.
- package/dist/index.dev.cjs +1648 -908
- package/dist/index.dev.cjs.map +1 -1
- package/dist/index.dev.js +1629 -906
- package/dist/index.dev.js.map +1 -1
- package/dist/index.prod.cjs +1648 -908
- package/dist/index.prod.cjs.map +1 -1
- package/dist/index.prod.d.ts +287 -67
- package/dist/index.prod.js +1629 -906
- package/dist/index.prod.js.map +1 -1
- package/package.json +1 -1
- package/src/BlendModes/BlendModeRegistry.ts +34 -50
- package/src/BlendModes/blend-modes-perfect.ts +313 -136
- package/src/BlendModes/blend-modes.ts +6 -0
- package/src/History/HistoryManager.ts +83 -0
- package/src/History/PixelAccumulator.ts +191 -0
- package/src/History/PixelEngineConfig.ts +18 -0
- package/src/History/PixelMutator/mutatorApplyMask.ts +20 -0
- package/src/History/PixelMutator/mutatorBlendColor.ts +22 -0
- package/src/History/PixelMutator/mutatorBlendPixel.ts +37 -0
- package/src/History/PixelMutator/mutatorBlendPixelData.ts +24 -0
- package/src/History/PixelMutator/mutatorFillPixelData.ts +21 -0
- package/src/History/PixelMutator/mutatorInvert.ts +18 -0
- package/src/History/PixelMutator.ts +18 -0
- package/src/History/PixelPatchTiles.ts +52 -0
- package/src/History/PixelWriter.ts +79 -0
- package/src/ImageData/{writeImageDataPixels.ts → writeImageDataBuffer.ts} +3 -3
- package/src/PixelData/applyCircleBrushToPixelData.ts +69 -0
- package/src/PixelData/applyMaskToPixelData.ts +1 -1
- package/src/PixelData/applyRectBrushToPixelData.ts +102 -0
- package/src/PixelData/invertPixelData.ts +74 -7
- package/src/PixelData/writePixelDataBuffer.ts +65 -0
- package/src/_types.ts +31 -11
- 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
|
|
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
|
|
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
|
|
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
|
+
}
|