pixel-data-js 0.23.1 → 0.25.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 +1816 -1802
- package/dist/index.dev.cjs.map +1 -1
- package/dist/index.dev.js +1799 -1786
- package/dist/index.dev.js.map +1 -1
- package/dist/index.prod.cjs +1816 -1802
- package/dist/index.prod.cjs.map +1 -1
- package/dist/index.prod.d.ts +349 -342
- package/dist/index.prod.js +1799 -1786
- package/dist/index.prod.js.map +1 -1
- package/package.json +3 -2
- package/src/Algorithm/floodFillSelection.ts +2 -2
- package/src/Canvas/CanvasFrameRenderer.ts +57 -0
- package/src/Canvas/ReusableCanvas.ts +60 -11
- package/src/Canvas/canvas-blend-modes.ts +28 -0
- package/src/History/HistoryAction.ts +38 -0
- package/src/History/HistoryManager.ts +4 -8
- package/src/History/PixelAccumulator.ts +137 -99
- package/src/History/PixelEngineConfig.ts +16 -6
- package/src/History/PixelMutator/mutatorApplyAlphaMask.ts +6 -6
- package/src/History/PixelMutator/mutatorApplyBinaryMask.ts +6 -6
- package/src/History/PixelMutator/mutatorBlendColor.ts +8 -5
- package/src/History/PixelMutator/mutatorBlendPixel.ts +22 -26
- package/src/History/PixelMutator/mutatorBlendPixelData.ts +7 -5
- package/src/History/PixelMutator/mutatorBlendPixelDataAlphaMask.ts +7 -5
- package/src/History/PixelMutator/mutatorBlendPixelDataBinaryMask.ts +7 -5
- package/src/History/PixelMutator/mutatorClear.ts +6 -5
- package/src/History/PixelMutator/mutatorFill.ts +34 -9
- package/src/History/PixelMutator/mutatorFillBinaryMask.ts +4 -2
- package/src/History/PixelMutator/mutatorInvert.ts +8 -4
- package/src/History/PixelMutator.ts +2 -21
- package/src/History/PixelPatchTiles.ts +4 -16
- package/src/History/PixelWriter.ts +150 -31
- package/src/ImageData/ReusableImageData.ts +3 -5
- package/src/Internal/helpers.ts +2 -0
- package/src/Paint/PaintBuffer.ts +269 -0
- package/src/Paint/PaintBufferCanvasRenderer.ts +48 -0
- package/src/Paint/makeCirclePaintAlphaMask.ts +41 -0
- package/src/{Mask/CircleBrushBinaryMask.ts → Paint/makeCirclePaintBinaryMask.ts} +5 -6
- package/src/Paint/makePaintMask.ts +28 -0
- package/src/Paint/makeRectFalloffPaintAlphaMask.ts +47 -0
- package/src/PixelData/PixelBuffer32.ts +2 -2
- package/src/PixelData/PixelData.ts +2 -28
- package/src/PixelData/applyAlphaMaskToPixelData.ts +20 -10
- package/src/PixelData/applyBinaryMaskToPixelData.ts +26 -19
- package/src/PixelData/blendColorPixelData.ts +33 -9
- package/src/PixelData/blendColorPixelDataAlphaMask.ts +19 -9
- package/src/PixelData/blendColorPixelDataBinaryMask.ts +19 -10
- package/src/PixelData/blendPixel.ts +47 -0
- package/src/PixelData/blendPixelData.ts +17 -7
- package/src/PixelData/blendPixelDataAlphaMask.ts +15 -7
- package/src/PixelData/blendPixelDataBinaryMask.ts +16 -7
- package/src/PixelData/blendPixelDataPaintBuffer.ts +37 -0
- package/src/PixelData/clearPixelData.ts +4 -4
- package/src/PixelData/extractPixelData.ts +4 -4
- package/src/PixelData/extractPixelDataBuffer.ts +4 -4
- package/src/PixelData/fillPixelData.ts +31 -21
- package/src/PixelData/fillPixelDataBinaryMask.ts +15 -7
- package/src/PixelData/fillPixelDataFast.ts +94 -0
- package/src/PixelData/invertPixelData.ts +6 -4
- package/src/PixelData/pixelDataToAlphaMask.ts +2 -2
- package/src/PixelData/reflectPixelData.ts +3 -3
- package/src/PixelData/resamplePixelData.ts +2 -2
- package/src/PixelData/writePaintBufferToPixelData.ts +26 -0
- package/src/PixelData/writePixelDataBuffer.ts +5 -5
- package/src/PixelTile/PixelTile.ts +21 -0
- package/src/PixelTile/PixelTilePool.ts +63 -0
- package/src/Rect/trimMaskRectBounds.ts +121 -0
- package/src/Rect/trimRectBounds.ts +25 -116
- package/src/_types.ts +17 -16
- package/src/index.ts +19 -24
- package/src/History/PixelMutator/mutatorApplyCircleBrush.ts +0 -78
- package/src/History/PixelMutator/mutatorApplyCircleBrushStroke.ts +0 -181
- package/src/History/PixelMutator/mutatorApplyCirclePencil.ts +0 -59
- package/src/History/PixelMutator/mutatorApplyCirclePencilStroke.ts +0 -171
- package/src/History/PixelMutator/mutatorApplyRectBrush.ts +0 -64
- package/src/History/PixelMutator/mutatorApplyRectBrushStroke.ts +0 -182
- package/src/History/PixelMutator/mutatorApplyRectPencil.ts +0 -65
- package/src/History/PixelMutator/mutatorApplyRectPencilStroke.ts +0 -164
- package/src/Mask/CircleBrushAlphaMask.ts +0 -32
- package/src/PixelData/applyCircleBrushToPixelData.ts +0 -91
- package/src/PixelData/applyRectBrushToPixelData.ts +0 -85
- package/src/Rect/getCircleBrushOrPencilBounds.ts +0 -43
- package/src/Rect/getCircleBrushOrPencilStrokeBounds.ts +0 -24
- package/src/Rect/getRectBrushOrPencilBounds.ts +0 -38
- package/src/Rect/getRectBrushOrPencilStrokeBounds.ts +0 -26
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import type { Color32, PaintAlphaMask, PaintBinaryMask, Rect } from '../_types'
|
|
2
|
+
import { forEachLinePoint } from '../Algorithm/forEachLinePoint'
|
|
3
|
+
import type { PixelEngineConfig } from '../History/PixelEngineConfig'
|
|
4
|
+
import { macro_halfAndFloor } from '../Internal/helpers'
|
|
5
|
+
import type { PixelTile } from '../PixelTile/PixelTile'
|
|
6
|
+
import type { PixelTilePool } from '../PixelTile/PixelTilePool'
|
|
7
|
+
import { trimRectBounds } from '../Rect/trimRectBounds'
|
|
8
|
+
|
|
9
|
+
export class PaintBuffer {
|
|
10
|
+
readonly lookup: (PixelTile | undefined)[]
|
|
11
|
+
private readonly scratchBounds: Rect = { x: 0, y: 0, w: 0, h: 0 }
|
|
12
|
+
|
|
13
|
+
constructor(
|
|
14
|
+
readonly config: PixelEngineConfig,
|
|
15
|
+
readonly tilePool: PixelTilePool,
|
|
16
|
+
) {
|
|
17
|
+
this.lookup = []
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
private eachTileInBounds(
|
|
21
|
+
bounds: Rect,
|
|
22
|
+
callback: (tile: PixelTile, bX: number, bY: number, bW: number, bH: number) => void,
|
|
23
|
+
): void {
|
|
24
|
+
const { tileShift, targetColumns, targetRows, tileSize } = this.config
|
|
25
|
+
|
|
26
|
+
const x1 = Math.max(0, bounds.x >> tileShift)
|
|
27
|
+
const y1 = Math.max(0, bounds.y >> tileShift)
|
|
28
|
+
const x2 = Math.min(targetColumns - 1, (bounds.x + bounds.w - 1) >> tileShift)
|
|
29
|
+
const y2 = Math.min(targetRows - 1, (bounds.y + bounds.h - 1) >> tileShift)
|
|
30
|
+
|
|
31
|
+
if (x1 > x2 || y1 > y2) return
|
|
32
|
+
|
|
33
|
+
const lookup = this.lookup
|
|
34
|
+
const tilePool = this.tilePool
|
|
35
|
+
|
|
36
|
+
for (let ty = y1; ty <= y2; ty++) {
|
|
37
|
+
const rowOffset = ty * targetColumns
|
|
38
|
+
const tileTop = ty << tileShift
|
|
39
|
+
|
|
40
|
+
for (let tx = x1; tx <= x2; tx++) {
|
|
41
|
+
const id = rowOffset + tx
|
|
42
|
+
const tile = lookup[id] ?? (lookup[id] = tilePool.getTile(id, tx, ty))
|
|
43
|
+
const tileLeft = tx << tileShift
|
|
44
|
+
|
|
45
|
+
const startX = bounds.x > tileLeft ? bounds.x : tileLeft
|
|
46
|
+
const startY = bounds.y > tileTop ? bounds.y : tileTop
|
|
47
|
+
|
|
48
|
+
const maskEndX = bounds.x + bounds.w
|
|
49
|
+
const tileEndX = tileLeft + tileSize
|
|
50
|
+
const endX = maskEndX < tileEndX ? maskEndX : tileEndX
|
|
51
|
+
|
|
52
|
+
const maskEndY = bounds.y + bounds.h
|
|
53
|
+
const tileEndY = tileTop + tileSize
|
|
54
|
+
const endY = maskEndY < tileEndY ? maskEndY : tileEndY
|
|
55
|
+
|
|
56
|
+
callback(tile, startX, startY, endX - startX, endY - startY)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
writePaintAlphaMaskStroke(
|
|
62
|
+
color: Color32,
|
|
63
|
+
brush: PaintAlphaMask,
|
|
64
|
+
x0: number,
|
|
65
|
+
y0: number,
|
|
66
|
+
x1: number,
|
|
67
|
+
y1: number,
|
|
68
|
+
): boolean {
|
|
69
|
+
const cA = color >>> 24
|
|
70
|
+
if (cA === 0) return false
|
|
71
|
+
|
|
72
|
+
const { tileShift, tileMask, target } = this.config
|
|
73
|
+
const { w: bW, h: bH, data: bD, centerOffsetX, centerOffsetY } = brush
|
|
74
|
+
const cRGB = color & 0x00ffffff
|
|
75
|
+
const scratch = this.scratchBounds
|
|
76
|
+
|
|
77
|
+
let changed = false
|
|
78
|
+
|
|
79
|
+
forEachLinePoint(x0, y0, x1, y1, (px, py) => {
|
|
80
|
+
|
|
81
|
+
const topLeftX = Math.floor(px + centerOffsetX)
|
|
82
|
+
const topLeftY = Math.floor(py + centerOffsetY)
|
|
83
|
+
trimRectBounds(
|
|
84
|
+
topLeftX,
|
|
85
|
+
topLeftY,
|
|
86
|
+
bW,
|
|
87
|
+
bH,
|
|
88
|
+
target.width,
|
|
89
|
+
target.height,
|
|
90
|
+
scratch,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
if (scratch.w <= 0 || scratch.h <= 0) return
|
|
94
|
+
|
|
95
|
+
this.eachTileInBounds(scratch, (tile, bX, bY, bW_t, bH_t) => {
|
|
96
|
+
const d32 = tile.data32
|
|
97
|
+
let tileChanged = false
|
|
98
|
+
|
|
99
|
+
for (let i = 0; i < bH_t; i++) {
|
|
100
|
+
const canvasY = bY + i
|
|
101
|
+
const bOff = (canvasY - topLeftY) * bW
|
|
102
|
+
const tOff = (canvasY & tileMask) << tileShift
|
|
103
|
+
const dS = tOff + (bX & tileMask)
|
|
104
|
+
|
|
105
|
+
for (let j = 0; j < bW_t; j++) {
|
|
106
|
+
const canvasX = bX + j
|
|
107
|
+
const brushA = bD[bOff + (canvasX - topLeftX)]
|
|
108
|
+
if (brushA === 0) continue
|
|
109
|
+
|
|
110
|
+
const t = cA * brushA + 128
|
|
111
|
+
const blendedA = (t + (t >> 8)) >> 8
|
|
112
|
+
|
|
113
|
+
const idx = dS + j
|
|
114
|
+
const cur = d32[idx]
|
|
115
|
+
if (brushA > (cur >>> 24)) {
|
|
116
|
+
const next = (cRGB | (blendedA << 24)) >>> 0
|
|
117
|
+
if (cur !== next) {
|
|
118
|
+
d32[idx] = next as Color32
|
|
119
|
+
tileChanged = true
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (tileChanged) changed = true
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
return changed
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
writePaintBinaryMaskStroke(
|
|
131
|
+
color: Color32,
|
|
132
|
+
brush: PaintBinaryMask,
|
|
133
|
+
x0: number,
|
|
134
|
+
y0: number,
|
|
135
|
+
x1: number,
|
|
136
|
+
y1: number,
|
|
137
|
+
): boolean {
|
|
138
|
+
const alphaIsZero = (color >>> 24) === 0
|
|
139
|
+
if (alphaIsZero) return false
|
|
140
|
+
|
|
141
|
+
const { tileShift, tileMask, target } = this.config
|
|
142
|
+
const { w: bW, h: bH, data: bD, centerOffsetX, centerOffsetY } = brush
|
|
143
|
+
const scratch = this.scratchBounds
|
|
144
|
+
let changed = false
|
|
145
|
+
|
|
146
|
+
forEachLinePoint(x0, y0, x1, y1, (px, py) => {
|
|
147
|
+
const topLeftX = Math.floor(px + centerOffsetX)
|
|
148
|
+
const topLeftY = Math.floor(py + centerOffsetY)
|
|
149
|
+
|
|
150
|
+
trimRectBounds(
|
|
151
|
+
topLeftX,
|
|
152
|
+
topLeftY,
|
|
153
|
+
bW,
|
|
154
|
+
bH,
|
|
155
|
+
target.width,
|
|
156
|
+
target.height,
|
|
157
|
+
scratch,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
if (scratch.w <= 0 || scratch.h <= 0) return
|
|
161
|
+
|
|
162
|
+
this.eachTileInBounds(scratch, (tile, bX, bY, bW_t, bH_t) => {
|
|
163
|
+
const d32 = tile.data32
|
|
164
|
+
let tileChanged = false
|
|
165
|
+
|
|
166
|
+
for (let i = 0; i < bH_t; i++) {
|
|
167
|
+
const canvasY = bY + i
|
|
168
|
+
const bOff = (canvasY - topLeftY) * bW
|
|
169
|
+
const tOff = (canvasY & tileMask) << tileShift
|
|
170
|
+
const dS = tOff + (bX & tileMask)
|
|
171
|
+
|
|
172
|
+
for (let j = 0; j < bW_t; j++) {
|
|
173
|
+
const canvasX = bX + j
|
|
174
|
+
if (bD[bOff + (canvasX - topLeftX)]) {
|
|
175
|
+
const idx = dS + j
|
|
176
|
+
if (d32[idx] !== color) {
|
|
177
|
+
d32[idx] = color
|
|
178
|
+
tileChanged = true
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (tileChanged) changed = true
|
|
184
|
+
})
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
return changed
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
writeRectStroke(
|
|
191
|
+
color: Color32,
|
|
192
|
+
brushWidth: number,
|
|
193
|
+
brushHeight: number,
|
|
194
|
+
x0: number,
|
|
195
|
+
y0: number,
|
|
196
|
+
x1: number,
|
|
197
|
+
y1: number,
|
|
198
|
+
): boolean {
|
|
199
|
+
const alphaIsZero = (color >>> 24) === 0
|
|
200
|
+
if (alphaIsZero) return false
|
|
201
|
+
|
|
202
|
+
const config = this.config
|
|
203
|
+
const tileShift = config.tileShift
|
|
204
|
+
const tileMask = config.tileMask
|
|
205
|
+
const target = config.target
|
|
206
|
+
const scratch = this.scratchBounds
|
|
207
|
+
|
|
208
|
+
const centerOffsetX = macro_halfAndFloor(brushWidth - 1)
|
|
209
|
+
const centerOffsetY = macro_halfAndFloor(brushHeight - 1)
|
|
210
|
+
|
|
211
|
+
let changed = false
|
|
212
|
+
|
|
213
|
+
forEachLinePoint(
|
|
214
|
+
x0,
|
|
215
|
+
y0,
|
|
216
|
+
x1,
|
|
217
|
+
y1,
|
|
218
|
+
(px, py) => {
|
|
219
|
+
const topLeftX = Math.floor(px + centerOffsetX)
|
|
220
|
+
const topLeftY = Math.floor(py + centerOffsetY)
|
|
221
|
+
|
|
222
|
+
trimRectBounds(
|
|
223
|
+
topLeftX,
|
|
224
|
+
topLeftY,
|
|
225
|
+
brushWidth,
|
|
226
|
+
brushHeight,
|
|
227
|
+
target.width,
|
|
228
|
+
target.height,
|
|
229
|
+
scratch,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
if (scratch.w <= 0 || scratch.h <= 0) return
|
|
233
|
+
|
|
234
|
+
this.eachTileInBounds(
|
|
235
|
+
scratch,
|
|
236
|
+
(tile, bX, bY, bW_t, bH_t) => {
|
|
237
|
+
const d32 = tile.data32
|
|
238
|
+
let tileChanged = false
|
|
239
|
+
|
|
240
|
+
for (let i = 0; i < bH_t; i++) {
|
|
241
|
+
const canvasY = bY + i
|
|
242
|
+
const tOff = (canvasY & tileMask) << tileShift
|
|
243
|
+
const dS = tOff + (bX & tileMask)
|
|
244
|
+
|
|
245
|
+
for (let j = 0; j < bW_t; j++) {
|
|
246
|
+
const idx = dS + j
|
|
247
|
+
|
|
248
|
+
if (d32[idx] !== color) {
|
|
249
|
+
d32[idx] = color
|
|
250
|
+
tileChanged = true
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (tileChanged) {
|
|
256
|
+
changed = true
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
)
|
|
260
|
+
},
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
return changed
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
clear(): void {
|
|
267
|
+
this.tilePool.releaseTiles(this.lookup)
|
|
268
|
+
}
|
|
269
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { CANVAS_CTX_FAILED } from '../Canvas/_constants'
|
|
2
|
+
import type { PaintBuffer } from './PaintBuffer'
|
|
3
|
+
|
|
4
|
+
export type PaintBufferCanvasRenderer = ReturnType<typeof makePaintBufferCanvasRenderer>
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
*
|
|
8
|
+
* @param offscreenCanvasClass - @internal
|
|
9
|
+
*/
|
|
10
|
+
export function makePaintBufferCanvasRenderer(
|
|
11
|
+
paintBuffer: PaintBuffer,
|
|
12
|
+
offscreenCanvasClass = OffscreenCanvas,
|
|
13
|
+
) {
|
|
14
|
+
const config = paintBuffer.config
|
|
15
|
+
const tileSize = config.tileSize
|
|
16
|
+
const tileShift = config.tileShift
|
|
17
|
+
const lookup = paintBuffer.lookup
|
|
18
|
+
const canvas = new offscreenCanvasClass(tileSize, tileSize)
|
|
19
|
+
const ctx = canvas.getContext('2d')
|
|
20
|
+
if (!ctx) throw new Error(CANVAS_CTX_FAILED)
|
|
21
|
+
ctx.imageSmoothingEnabled = false
|
|
22
|
+
|
|
23
|
+
return function drawPaintBuffer(
|
|
24
|
+
targetCtx: CanvasRenderingContext2D,
|
|
25
|
+
alpha = 255,
|
|
26
|
+
compOperation: GlobalCompositeOperation = 'source-over',
|
|
27
|
+
): void {
|
|
28
|
+
|
|
29
|
+
targetCtx.globalAlpha = alpha / 255
|
|
30
|
+
targetCtx.globalCompositeOperation = compOperation
|
|
31
|
+
|
|
32
|
+
for (let i = 0; i < lookup.length; i++) {
|
|
33
|
+
const tile = lookup[i]
|
|
34
|
+
|
|
35
|
+
if (tile) {
|
|
36
|
+
const dx = tile.tx << tileShift
|
|
37
|
+
const dy = tile.ty << tileShift
|
|
38
|
+
|
|
39
|
+
ctx.putImageData(tile.imageData, 0, 0)
|
|
40
|
+
|
|
41
|
+
targetCtx.drawImage(canvas, dx, dy)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
targetCtx.globalAlpha = 1
|
|
46
|
+
targetCtx.globalCompositeOperation = 'source-over'
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { MaskType, type PaintAlphaMask } from '../_types'
|
|
2
|
+
|
|
3
|
+
export function makeCirclePaintAlphaMask(size: number, fallOff: (d: number) => number = (d) => d): PaintAlphaMask {
|
|
4
|
+
const area = size * size
|
|
5
|
+
const data = new Uint8Array(area)
|
|
6
|
+
const radius = size / 2
|
|
7
|
+
const invR = 1 / radius
|
|
8
|
+
|
|
9
|
+
const centerOffset = -Math.ceil(radius - 0.5)
|
|
10
|
+
|
|
11
|
+
for (let y = 0; y < size; y++) {
|
|
12
|
+
const rowOffset = y * size
|
|
13
|
+
const dy = y - radius + 0.5
|
|
14
|
+
const dy2 = dy * dy
|
|
15
|
+
|
|
16
|
+
for (let x = 0; x < size; x++) {
|
|
17
|
+
const dx = x - radius + 0.5
|
|
18
|
+
const distSqr = dx * dx + dy2
|
|
19
|
+
|
|
20
|
+
if (distSqr <= (radius * radius)) {
|
|
21
|
+
const dist = Math.sqrt(distSqr) * invR
|
|
22
|
+
|
|
23
|
+
// Pass 1.0 at center, 0.0 at edge
|
|
24
|
+
const strength = fallOff(1 - dist)
|
|
25
|
+
if (strength > 0) {
|
|
26
|
+
const intensity = (strength * 255) | 0
|
|
27
|
+
data[rowOffset + x] = Math.max(0, Math.min(255, intensity))
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
type: MaskType.ALPHA,
|
|
35
|
+
data,
|
|
36
|
+
w: size,
|
|
37
|
+
h: size,
|
|
38
|
+
centerOffsetX: centerOffset,
|
|
39
|
+
centerOffsetY: centerOffset
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { MaskType, type PaintBinaryMask } from '../_types'
|
|
2
2
|
|
|
3
|
-
export function
|
|
3
|
+
export function makeCirclePaintBinaryMask(size: number): PaintBinaryMask {
|
|
4
4
|
const area = size * size
|
|
5
5
|
const data = new Uint8Array(area)
|
|
6
6
|
const radius = size / 2
|
|
7
7
|
|
|
8
|
-
const
|
|
8
|
+
const centerOffset = -Math.ceil(radius - 0.5)
|
|
9
9
|
|
|
10
10
|
for (let y = 0; y < size; y++) {
|
|
11
11
|
for (let x = 0; x < size; x++) {
|
|
@@ -23,8 +23,7 @@ export function makeCircleBrushBinaryMask(size: number): CircleBrushBinaryMask {
|
|
|
23
23
|
data,
|
|
24
24
|
w: size,
|
|
25
25
|
h: size,
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
minOffset,
|
|
26
|
+
centerOffsetX: centerOffset,
|
|
27
|
+
centerOffsetY: centerOffset,
|
|
29
28
|
}
|
|
30
29
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type AlphaMask, type BinaryMask, MaskType, type PaintAlphaMask, type PaintBinaryMask } from '../_types'
|
|
2
|
+
import { macro_halfAndFloor } from '../Internal/helpers'
|
|
3
|
+
|
|
4
|
+
export function makePaintBinaryMask(
|
|
5
|
+
mask: BinaryMask,
|
|
6
|
+
): PaintBinaryMask {
|
|
7
|
+
return {
|
|
8
|
+
type: MaskType.BINARY,
|
|
9
|
+
data: mask.data,
|
|
10
|
+
w: mask.w,
|
|
11
|
+
h: mask.h,
|
|
12
|
+
centerOffsetX: -macro_halfAndFloor(mask.w),
|
|
13
|
+
centerOffsetY: -macro_halfAndFloor(mask.h),
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function makePaintAlphaMask(
|
|
18
|
+
mask: AlphaMask,
|
|
19
|
+
): PaintAlphaMask {
|
|
20
|
+
return {
|
|
21
|
+
type: MaskType.ALPHA,
|
|
22
|
+
data: mask.data,
|
|
23
|
+
w: mask.w,
|
|
24
|
+
h: mask.h,
|
|
25
|
+
centerOffsetX: -macro_halfAndFloor(mask.w),
|
|
26
|
+
centerOffsetY: -macro_halfAndFloor(mask.h),
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { MaskType, type PaintAlphaMask } from '../_types'
|
|
2
|
+
import { macro_halfAndFloor } from '../Internal/helpers'
|
|
3
|
+
|
|
4
|
+
export function makeRectFalloffPaintAlphaMask(
|
|
5
|
+
width: number,
|
|
6
|
+
height: number,
|
|
7
|
+
fallOff: (d: number) => number = (d) => d,
|
|
8
|
+
): PaintAlphaMask {
|
|
9
|
+
const fPx = Math.floor(width / 2)
|
|
10
|
+
const fPy = Math.floor(height / 2)
|
|
11
|
+
|
|
12
|
+
const invHalfW = 2 / width
|
|
13
|
+
const invHalfH = 2 / height
|
|
14
|
+
|
|
15
|
+
const offX = (width % 2 === 0) ? 0.5 : 0
|
|
16
|
+
const offY = (height % 2 === 0) ? 0.5 : 0
|
|
17
|
+
|
|
18
|
+
const area = width * height
|
|
19
|
+
const data = new Uint8Array(area)
|
|
20
|
+
|
|
21
|
+
for (let y = 0; y < height; y++) {
|
|
22
|
+
const dy = Math.abs((y - fPy) + offY) * invHalfH
|
|
23
|
+
const rowOffset = y * width
|
|
24
|
+
|
|
25
|
+
for (let x = 0; x < width; x++) {
|
|
26
|
+
const dx = Math.abs((x - fPx) + offX) * invHalfW
|
|
27
|
+
|
|
28
|
+
// Chebyshev distance (square/rect shape)
|
|
29
|
+
const dist = dx > dy ? dx : dy
|
|
30
|
+
// Pass 1.0 at center, 0.0 at edge
|
|
31
|
+
const strength = fallOff(1 - dist)
|
|
32
|
+
if (strength > 0) {
|
|
33
|
+
const intensity = (strength * 255) | 0
|
|
34
|
+
data[rowOffset + x] = Math.max(0, Math.min(255, intensity))
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
type: MaskType.ALPHA,
|
|
41
|
+
data: data,
|
|
42
|
+
w: width,
|
|
43
|
+
h: height,
|
|
44
|
+
centerOffsetX: -macro_halfAndFloor(width),
|
|
45
|
+
centerOffsetY: -macro_halfAndFloor(height),
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { ImageDataLike,
|
|
1
|
+
import type { ImageDataLike, IPixelData } from '../_types'
|
|
2
2
|
import { imageDataToUInt32Array } from '../ImageData/imageDataToUInt32Array'
|
|
3
3
|
|
|
4
|
-
export class PixelData<T extends ImageDataLike = ImageData> implements IPixelData {
|
|
4
|
+
export class PixelData<T extends ImageDataLike = ImageData> implements IPixelData<T> {
|
|
5
5
|
readonly data32: Uint32Array
|
|
6
6
|
readonly imageData: T
|
|
7
7
|
readonly width: number
|
|
@@ -20,30 +20,4 @@ export class PixelData<T extends ImageDataLike = ImageData> implements IPixelDat
|
|
|
20
20
|
;(this as any).width = imageData.width
|
|
21
21
|
;(this as any).height = imageData.height
|
|
22
22
|
}
|
|
23
|
-
|
|
24
|
-
// should only be used for debug and testing
|
|
25
|
-
copy(): PixelData<T> {
|
|
26
|
-
const data = this.imageData.data
|
|
27
|
-
const buffer = new Uint8ClampedArray(data)
|
|
28
|
-
const Ctor = this.imageData.constructor
|
|
29
|
-
const isCtorValid = typeof Ctor === 'function'
|
|
30
|
-
|
|
31
|
-
let newImageData: T
|
|
32
|
-
if (isCtorValid && Ctor !== Object) {
|
|
33
|
-
const ImageConstructor = Ctor as ImageDataLikeConstructor<T>
|
|
34
|
-
newImageData = new ImageConstructor(
|
|
35
|
-
buffer,
|
|
36
|
-
this.width,
|
|
37
|
-
this.height,
|
|
38
|
-
)
|
|
39
|
-
} else {
|
|
40
|
-
newImageData = {
|
|
41
|
-
width: this.width,
|
|
42
|
-
height: this.height,
|
|
43
|
-
data: buffer,
|
|
44
|
-
} as unknown as T
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return new PixelData<T>(newImageData)
|
|
48
|
-
}
|
|
49
23
|
}
|
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import { type AlphaMask, type ApplyMaskToPixelDataOptions, type
|
|
1
|
+
import { type AlphaMask, type ApplyMaskToPixelDataOptions, type Color32, type IPixelData32 } from '../_types'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Directly applies a mask to a region of PixelData,
|
|
5
5
|
* modifying the destination's alpha channel in-place.
|
|
6
|
+
* @returns true if any pixels were actually modified.
|
|
6
7
|
*/
|
|
7
8
|
export function applyAlphaMaskToPixelData(
|
|
8
|
-
dst:
|
|
9
|
+
dst: IPixelData32,
|
|
9
10
|
mask: AlphaMask,
|
|
10
11
|
opts: ApplyMaskToPixelDataOptions = {},
|
|
11
|
-
):
|
|
12
|
+
): boolean {
|
|
12
13
|
const {
|
|
13
14
|
x: targetX = 0,
|
|
14
15
|
y: targetY = 0,
|
|
@@ -20,7 +21,7 @@ export function applyAlphaMaskToPixelData(
|
|
|
20
21
|
invertMask = false,
|
|
21
22
|
} = opts
|
|
22
23
|
|
|
23
|
-
if (globalAlpha === 0) return
|
|
24
|
+
if (globalAlpha === 0) return false
|
|
24
25
|
|
|
25
26
|
// 1. Initial Destination Clipping
|
|
26
27
|
let x = targetX
|
|
@@ -41,12 +42,12 @@ export function applyAlphaMaskToPixelData(
|
|
|
41
42
|
w = Math.min(w, dst.width - x)
|
|
42
43
|
h = Math.min(h, dst.height - y)
|
|
43
44
|
|
|
44
|
-
if (w <= 0) return
|
|
45
|
-
if (h <= 0) return
|
|
45
|
+
if (w <= 0) return false
|
|
46
|
+
if (h <= 0) return false
|
|
46
47
|
|
|
47
48
|
// 2. Determine Source Dimensions
|
|
48
49
|
const mPitch = mask.w
|
|
49
|
-
if (mPitch <= 0) return
|
|
50
|
+
if (mPitch <= 0) return false
|
|
50
51
|
|
|
51
52
|
// 3. Source Bounds Clipping
|
|
52
53
|
// Calculate where we would start reading in the mask
|
|
@@ -63,8 +64,8 @@ export function applyAlphaMaskToPixelData(
|
|
|
63
64
|
const finalH = sY1 - sY0
|
|
64
65
|
|
|
65
66
|
// This is where your failing tests are now caught
|
|
66
|
-
if (finalW <= 0) return
|
|
67
|
-
if (finalH <= 0) return
|
|
67
|
+
if (finalW <= 0) return false
|
|
68
|
+
if (finalH <= 0) return false
|
|
68
69
|
|
|
69
70
|
// 4. Align Destination with Source Clipping
|
|
70
71
|
// If the source was clipped on the top/left, we must shift the destination start
|
|
@@ -80,6 +81,7 @@ export function applyAlphaMaskToPixelData(
|
|
|
80
81
|
let dIdx = (y + yShift) * dw + (x + xShift)
|
|
81
82
|
let mIdx = sY0 * mPitch + sX0
|
|
82
83
|
|
|
84
|
+
let didChange = false
|
|
83
85
|
for (let iy = 0; iy < h; iy++) {
|
|
84
86
|
for (let ix = 0; ix < w; ix++) {
|
|
85
87
|
const mVal = maskData[mIdx]
|
|
@@ -101,6 +103,7 @@ export function applyAlphaMaskToPixelData(
|
|
|
101
103
|
if (weight === 0) {
|
|
102
104
|
// Clear alpha channel
|
|
103
105
|
dst32[dIdx] = (dst32[dIdx] & 0x00ffffff) >>> 0
|
|
106
|
+
didChange = true
|
|
104
107
|
} else if (weight !== 255) {
|
|
105
108
|
// Merge alpha channel
|
|
106
109
|
const d = dst32[dIdx]
|
|
@@ -108,7 +111,13 @@ export function applyAlphaMaskToPixelData(
|
|
|
108
111
|
|
|
109
112
|
if (da !== 0) {
|
|
110
113
|
const finalAlpha = da === 255 ? weight : (da * weight + 128) >> 8
|
|
111
|
-
|
|
114
|
+
|
|
115
|
+
const current = dst32[dIdx] as Color32
|
|
116
|
+
const next = ((d & 0x00ffffff) | (finalAlpha << 24)) >>> 0
|
|
117
|
+
if (current !== next) {
|
|
118
|
+
dst32[dIdx] = next
|
|
119
|
+
didChange = true
|
|
120
|
+
}
|
|
112
121
|
}
|
|
113
122
|
}
|
|
114
123
|
|
|
@@ -119,4 +128,5 @@ export function applyAlphaMaskToPixelData(
|
|
|
119
128
|
dIdx += dStride
|
|
120
129
|
mIdx += mStride
|
|
121
130
|
}
|
|
131
|
+
return didChange
|
|
122
132
|
}
|