pixel-data-js 0.23.1 → 0.24.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 +1024 -596
- package/dist/index.dev.cjs.map +1 -1
- package/dist/index.dev.js +1010 -592
- package/dist/index.dev.js.map +1 -1
- package/dist/index.prod.cjs +1024 -596
- package/dist/index.prod.cjs.map +1 -1
- package/dist/index.prod.d.ts +280 -165
- package/dist/index.prod.js +1010 -592
- package/dist/index.prod.js.map +1 -1
- package/package.json +3 -2
- package/src/Canvas/CanvasFrameRenderer.ts +57 -0
- package/src/Canvas/ReusableCanvas.ts +60 -11
- package/src/History/HistoryAction.ts +38 -0
- package/src/History/HistoryManager.ts +4 -8
- package/src/History/PixelAccumulator.ts +95 -80
- package/src/History/PixelEngineConfig.ts +18 -6
- package/src/History/PixelMutator/mutatorApplyAlphaMask.ts +6 -6
- package/src/History/PixelMutator/mutatorApplyBinaryMask.ts +6 -6
- package/src/History/PixelMutator/mutatorApplyCircleBrushStroke.ts +6 -5
- package/src/History/PixelMutator/mutatorApplyCirclePencil.ts +22 -22
- package/src/History/PixelMutator/mutatorApplyCirclePencilStroke.ts +6 -5
- package/src/History/PixelMutator/mutatorApplyRectBrush.ts +19 -19
- package/src/History/PixelMutator/mutatorApplyRectBrushStroke.ts +6 -4
- package/src/History/PixelMutator/mutatorApplyRectPencil.ts +20 -20
- package/src/History/PixelMutator/mutatorApplyRectPencilStroke.ts +6 -4
- package/src/History/PixelMutator/mutatorBlendColor.ts +8 -5
- package/src/History/PixelMutator/mutatorBlendColorCircleMask.ts +71 -0
- package/src/History/PixelMutator/mutatorBlendPixel.ts +22 -26
- package/src/History/PixelMutator/mutatorBlendPixelData.ts +5 -3
- package/src/History/PixelMutator/mutatorBlendPixelDataAlphaMask.ts +5 -3
- package/src/History/PixelMutator/mutatorBlendPixelDataBinaryMask.ts +5 -3
- 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 +4 -3
- package/src/History/PixelPatchTiles.ts +3 -15
- package/src/History/PixelWriter.ts +29 -33
- package/src/ImageData/ReusableImageData.ts +3 -5
- package/src/Mask/{CircleBrushAlphaMask.ts → CircleAlphaMask.ts} +2 -2
- package/src/Mask/{CircleBrushBinaryMask.ts → CircleBinaryMask.ts} +2 -2
- package/src/PixelData/PixelData.ts +1 -27
- package/src/PixelData/applyAlphaMaskToPixelData.ts +19 -9
- package/src/PixelData/applyBinaryMaskToPixelData.ts +24 -17
- package/src/PixelData/applyRectBrushToPixelData.ts +18 -5
- package/src/PixelData/blendColorPixelData.ts +31 -7
- package/src/PixelData/blendColorPixelDataAlphaMask.ts +16 -6
- package/src/PixelData/blendColorPixelDataBinaryMask.ts +16 -7
- package/src/PixelData/{applyCircleBrushToPixelData.ts → blendColorPixelDataCircleMask.ts} +11 -10
- package/src/PixelData/blendPixel.ts +47 -0
- package/src/PixelData/blendPixelData.ts +14 -4
- package/src/PixelData/blendPixelDataAlphaMask.ts +12 -4
- package/src/PixelData/blendPixelDataBinaryMask.ts +13 -4
- package/src/PixelData/blendPixelDataPaintBuffer.ts +37 -0
- package/src/PixelData/clearPixelData.ts +2 -2
- package/src/PixelData/fillPixelData.ts +26 -16
- package/src/PixelData/fillPixelDataBinaryMask.ts +12 -4
- package/src/PixelData/fillPixelDataFast.ts +94 -0
- package/src/PixelData/invertPixelData.ts +4 -2
- package/src/PixelTile/PaintBuffer.ts +122 -0
- package/src/PixelTile/PaintBufferRenderer.ts +40 -0
- package/src/PixelTile/PixelTile.ts +21 -0
- package/src/PixelTile/PixelTilePool.ts +63 -0
- package/src/_types.ts +9 -9
- package/src/index.ts +16 -6
- package/src/History/PixelMutator/mutatorApplyCircleBrush.ts +0 -78
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.24.0",
|
|
5
5
|
"packageManager": "pnpm@10.30.0",
|
|
6
6
|
"description": "JS Pixel and ImageData operations",
|
|
7
7
|
"author": {
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
"build": "tsup",
|
|
51
51
|
"test": "vitest --coverage --project unit",
|
|
52
52
|
"test:build": "pnpm build && vitest run --project dist",
|
|
53
|
+
"check-circ": "tsx _scripts/check-circular.ts",
|
|
53
54
|
"test:mutation": "stryker run",
|
|
54
55
|
"docs": "npx typedoc",
|
|
55
56
|
"typecheck": "tsc --noEmit",
|
|
@@ -88,7 +89,7 @@
|
|
|
88
89
|
"typedoc-plugin-mdn-links": "^5.1.1",
|
|
89
90
|
"typedoc-rhineai-theme": "^1.2.0",
|
|
90
91
|
"typescript": "^5.9.3",
|
|
91
|
-
"unplugin-inline": "^1.
|
|
92
|
+
"unplugin-inline": "^1.9.0",
|
|
92
93
|
"vite-tsconfig-paths": "^6.1.1",
|
|
93
94
|
"vitest": "3.2.4"
|
|
94
95
|
},
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { PixelCanvas } from './PixelCanvas'
|
|
2
|
+
import { makeReusableCanvas } from './ReusableCanvas'
|
|
3
|
+
|
|
4
|
+
export type DrawPixelLayer = (ctx: CanvasRenderingContext2D) => void
|
|
5
|
+
export type DrawScreenLayer = (ctx: CanvasRenderingContext2D, scale: number) => void
|
|
6
|
+
export type CanvasFrameRenderer = ReturnType<typeof makeCanvasFrameRenderer>
|
|
7
|
+
|
|
8
|
+
const defaults = {
|
|
9
|
+
makeReusableCanvas,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type Deps = Partial<typeof defaults>
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param deps - @hidden
|
|
16
|
+
*/
|
|
17
|
+
export function makeCanvasFrameRenderer(deps: Deps = defaults) {
|
|
18
|
+
const {
|
|
19
|
+
makeReusableCanvas = defaults.makeReusableCanvas,
|
|
20
|
+
} = deps
|
|
21
|
+
|
|
22
|
+
const bufferCanvas = makeReusableCanvas()
|
|
23
|
+
|
|
24
|
+
return function renderCanvasFrame(
|
|
25
|
+
pixelCanvas: PixelCanvas,
|
|
26
|
+
scale: number,
|
|
27
|
+
getImageData: () => ImageData | undefined | null,
|
|
28
|
+
drawPixelLayer?: DrawPixelLayer,
|
|
29
|
+
drawScreenLayer?: DrawScreenLayer,
|
|
30
|
+
) {
|
|
31
|
+
const { canvas, ctx } = pixelCanvas
|
|
32
|
+
|
|
33
|
+
// 1. Clear pixel buffer (unscaled)
|
|
34
|
+
const { ctx: pxCtx, canvas: pxCanvas } = bufferCanvas(canvas.width, canvas.height)
|
|
35
|
+
|
|
36
|
+
// 2. Draw pixel data into pixel buffer
|
|
37
|
+
const img = getImageData()
|
|
38
|
+
if (img) {
|
|
39
|
+
pxCtx.putImageData(img, 0, 0)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// draw transient pixel data
|
|
43
|
+
drawPixelLayer?.(pxCtx)
|
|
44
|
+
|
|
45
|
+
// clear target canvas
|
|
46
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0)
|
|
47
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
|
48
|
+
|
|
49
|
+
// Draw pixel buffer scaled onto screen
|
|
50
|
+
ctx.setTransform(scale, 0, 0, scale, 0, 0)
|
|
51
|
+
ctx.drawImage(pxCanvas, 0, 0)
|
|
52
|
+
|
|
53
|
+
// Draw overlays in screen space
|
|
54
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0)
|
|
55
|
+
drawScreenLayer?.(ctx, scale)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -1,25 +1,69 @@
|
|
|
1
1
|
import { CANVAS_CTX_FAILED } from './_constants'
|
|
2
2
|
|
|
3
|
-
export type
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
export type CanvasContext<T> = T extends HTMLCanvasElement
|
|
4
|
+
? CanvasRenderingContext2D
|
|
5
|
+
: OffscreenCanvasRenderingContext2D
|
|
6
|
+
|
|
7
|
+
export type ReusableCanvas<T extends HTMLCanvasElement | OffscreenCanvas> = {
|
|
8
|
+
readonly canvas: T
|
|
9
|
+
readonly ctx: CanvasContext<T>
|
|
6
10
|
}
|
|
7
11
|
|
|
8
12
|
/**
|
|
9
|
-
* Creates a reusable
|
|
13
|
+
* Creates a reusable HTMLCanvasElement and context that are not part of the DOM.
|
|
10
14
|
* Ensures it is always set to `context.imageSmoothingEnabled = false`
|
|
11
15
|
* @see makePixelCanvas
|
|
12
16
|
* @throws {Error} If the {@link HTMLCanvasElement} context cannot be initialized.
|
|
13
17
|
*/
|
|
14
18
|
export function makeReusableCanvas() {
|
|
15
|
-
|
|
16
|
-
|
|
19
|
+
return makeReusableCanvasMeta<HTMLCanvasElement>((w, h) => {
|
|
20
|
+
const canvas = document.createElement('canvas')
|
|
21
|
+
|
|
22
|
+
canvas.width = w
|
|
23
|
+
canvas.height = h
|
|
24
|
+
|
|
25
|
+
return canvas
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Creates a reusable OffscreenCanvas and context.
|
|
31
|
+
* Ensures it is always set to `context.imageSmoothingEnabled = false`
|
|
32
|
+
* @see makePixelCanvas
|
|
33
|
+
* @throws {Error} If the {@link OffscreenCanvasRenderingContext2D} context cannot be initialized.
|
|
34
|
+
*/
|
|
35
|
+
export function makeReusableOffscreenCanvas() {
|
|
36
|
+
return makeReusableCanvasMeta<OffscreenCanvas>((w, h) => new OffscreenCanvas(w, h))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function makeReusableCanvasMeta<T extends HTMLCanvasElement | OffscreenCanvas>(
|
|
40
|
+
factory: (w: number, h: number) => T,
|
|
41
|
+
) {
|
|
42
|
+
let canvas: T | null = null
|
|
43
|
+
let ctx: CanvasContext<T> | null = null
|
|
17
44
|
|
|
18
|
-
|
|
45
|
+
const result: ReusableCanvas<T> = {
|
|
46
|
+
canvas: null as any,
|
|
47
|
+
ctx: null as any,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function get(width: number, height: number): ReusableCanvas<T> {
|
|
19
51
|
if (canvas === null) {
|
|
20
|
-
canvas =
|
|
21
|
-
ctx = canvas.getContext('2d')
|
|
22
|
-
|
|
52
|
+
canvas = factory(width, height)
|
|
53
|
+
ctx = canvas.getContext('2d') as CanvasContext<T> | null
|
|
54
|
+
|
|
55
|
+
if (!ctx) {
|
|
56
|
+
throw new Error(CANVAS_CTX_FAILED)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Initialize the fresh context state
|
|
60
|
+
ctx.imageSmoothingEnabled = false
|
|
61
|
+
|
|
62
|
+
;(result as any).canvas = canvas
|
|
63
|
+
;(result as any).ctx = ctx
|
|
64
|
+
|
|
65
|
+
// Early return to skip resize/clear checks for brand new canvases
|
|
66
|
+
return result
|
|
23
67
|
}
|
|
24
68
|
|
|
25
69
|
// Resize if needed (resizing auto-clears)
|
|
@@ -28,16 +72,21 @@ export function makeReusableCanvas() {
|
|
|
28
72
|
canvas.height = height
|
|
29
73
|
ctx!.imageSmoothingEnabled = false
|
|
30
74
|
} else {
|
|
75
|
+
// Always reset transform before clearing to ensure the whole buffer is wiped
|
|
76
|
+
ctx!.setTransform(1, 0, 0, 1, 0, 0)
|
|
31
77
|
// Same size → manually clear
|
|
32
78
|
ctx!.clearRect(0, 0, width, height)
|
|
33
79
|
}
|
|
34
80
|
|
|
35
|
-
return
|
|
81
|
+
return result
|
|
36
82
|
}
|
|
37
83
|
|
|
38
84
|
get.reset = () => {
|
|
39
85
|
canvas = null
|
|
40
86
|
ctx = null
|
|
87
|
+
|
|
88
|
+
;(result as any).canvas = null
|
|
89
|
+
;(result as any).ctx = null
|
|
41
90
|
}
|
|
42
91
|
|
|
43
92
|
return get
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { applyPatchTiles, type PixelPatchTiles } from './PixelPatchTiles'
|
|
2
|
+
import type { PixelWriter } from './PixelWriter'
|
|
3
|
+
|
|
4
|
+
export interface HistoryAction {
|
|
5
|
+
undo: () => void
|
|
6
|
+
redo: () => void
|
|
7
|
+
dispose?: () => void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type HistoryActionFactory = typeof makeHistoryAction
|
|
11
|
+
|
|
12
|
+
export function makeHistoryAction(
|
|
13
|
+
writer: PixelWriter<any>,
|
|
14
|
+
patch: PixelPatchTiles,
|
|
15
|
+
after?: () => void,
|
|
16
|
+
afterUndo?: () => void,
|
|
17
|
+
afterRedo?: () => void,
|
|
18
|
+
applyPatchTilesFn = applyPatchTiles,
|
|
19
|
+
): HistoryAction {
|
|
20
|
+
|
|
21
|
+
const target = writer.config.target
|
|
22
|
+
const tileSize = writer.config.tileSize
|
|
23
|
+
const accumulator = writer.accumulator
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
undo: () => {
|
|
27
|
+
applyPatchTilesFn(target, patch.beforeTiles, tileSize)
|
|
28
|
+
afterUndo?.()
|
|
29
|
+
after?.()
|
|
30
|
+
},
|
|
31
|
+
redo: () => {
|
|
32
|
+
applyPatchTilesFn(target, patch.afterTiles, tileSize)
|
|
33
|
+
afterRedo?.()
|
|
34
|
+
after?.()
|
|
35
|
+
},
|
|
36
|
+
dispose: () => accumulator.recyclePatch(patch),
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -1,13 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
undo: () => void
|
|
3
|
-
redo: () => void
|
|
4
|
-
dispose?: () => void
|
|
5
|
-
}
|
|
1
|
+
import type { HistoryAction } from './HistoryAction'
|
|
6
2
|
|
|
7
3
|
export class HistoryManager {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
4
|
+
readonly undoStack: HistoryAction[]
|
|
5
|
+
readonly redoStack: HistoryAction[]
|
|
6
|
+
readonly listeners: Set<() => void>
|
|
11
7
|
|
|
12
8
|
constructor(
|
|
13
9
|
public maxSteps = 50,
|
|
@@ -1,95 +1,61 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { PixelTile } from '../PixelTile/PixelTile'
|
|
2
|
+
import type { PixelTilePool } from '../PixelTile/PixelTilePool'
|
|
2
3
|
import type { PixelEngineConfig } from './PixelEngineConfig'
|
|
3
|
-
import { type PixelPatchTiles
|
|
4
|
+
import { applyPatchTiles, type PixelPatchTiles } from './PixelPatchTiles'
|
|
5
|
+
|
|
6
|
+
export type DidChangeFn = (didChange: boolean) => boolean
|
|
4
7
|
|
|
5
8
|
export class PixelAccumulator {
|
|
6
9
|
public lookup: (PixelTile | undefined)[]
|
|
7
10
|
public beforeTiles: PixelTile[]
|
|
8
|
-
public pool: PixelTile[]
|
|
9
11
|
|
|
10
12
|
constructor(
|
|
11
|
-
public target: IPixelData,
|
|
12
13
|
readonly config: PixelEngineConfig,
|
|
14
|
+
readonly tilePool: PixelTilePool,
|
|
13
15
|
) {
|
|
14
16
|
this.lookup = []
|
|
15
17
|
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
18
|
}
|
|
41
19
|
|
|
42
20
|
recyclePatch(patch: PixelPatchTiles) {
|
|
43
|
-
|
|
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
|
-
}
|
|
21
|
+
this.tilePool.releaseTiles(patch.beforeTiles)
|
|
22
|
+
this.tilePool.releaseTiles(patch.afterTiles)
|
|
62
23
|
}
|
|
63
24
|
|
|
64
25
|
/**
|
|
65
26
|
* @param x pixel x coordinate
|
|
66
27
|
* @param y pixel y coordinate
|
|
67
28
|
*/
|
|
68
|
-
|
|
69
|
-
let target = this.target
|
|
29
|
+
storePixelBeforeState(x: number, y: number): DidChangeFn {
|
|
70
30
|
let shift = this.config.tileShift
|
|
71
|
-
let columns =
|
|
31
|
+
let columns = this.config.targetColumns
|
|
72
32
|
let tx = x >> shift
|
|
73
33
|
let ty = y >> shift
|
|
74
34
|
let id = ty * columns + tx
|
|
75
35
|
|
|
76
36
|
let tile = this.lookup[id]
|
|
37
|
+
let added = false
|
|
77
38
|
|
|
78
39
|
if (!tile) {
|
|
79
|
-
tile = this.getTile(
|
|
80
|
-
id,
|
|
81
|
-
tx,
|
|
82
|
-
ty,
|
|
83
|
-
)
|
|
40
|
+
tile = this.tilePool.getTile(id, tx, ty)
|
|
84
41
|
|
|
85
42
|
this.extractState(tile)
|
|
86
43
|
this.lookup[id] = tile
|
|
87
44
|
this.beforeTiles.push(tile)
|
|
45
|
+
added = true
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return (didChange: boolean) => {
|
|
49
|
+
if (!didChange && added) {
|
|
50
|
+
this.beforeTiles.pop()
|
|
51
|
+
this.lookup[id] = undefined
|
|
52
|
+
this.tilePool.releaseTile(tile!)
|
|
53
|
+
}
|
|
54
|
+
return didChange
|
|
88
55
|
}
|
|
89
56
|
}
|
|
90
57
|
|
|
91
58
|
/**
|
|
92
|
-
*
|
|
93
59
|
* @param x pixel x coordinate
|
|
94
60
|
* @param y pixel y coordinate
|
|
95
61
|
* @param w pixel width
|
|
@@ -100,27 +66,24 @@ export class PixelAccumulator {
|
|
|
100
66
|
y: number,
|
|
101
67
|
w: number,
|
|
102
68
|
h: number,
|
|
103
|
-
) {
|
|
104
|
-
let target = this.target
|
|
69
|
+
): DidChangeFn {
|
|
105
70
|
let shift = this.config.tileShift
|
|
106
|
-
let columns =
|
|
71
|
+
let columns = this.config.targetColumns
|
|
107
72
|
|
|
108
73
|
let startX = x >> shift
|
|
109
74
|
let startY = y >> shift
|
|
110
75
|
let endX = (x + w - 1) >> shift
|
|
111
76
|
let endY = (y + h - 1) >> shift
|
|
112
77
|
|
|
78
|
+
let startIndex = this.beforeTiles.length
|
|
79
|
+
|
|
113
80
|
for (let ty = startY; ty <= endY; ty++) {
|
|
114
81
|
for (let tx = startX; tx <= endX; tx++) {
|
|
115
82
|
let id = ty * columns + tx
|
|
116
83
|
let tile = this.lookup[id]
|
|
117
84
|
|
|
118
85
|
if (!tile) {
|
|
119
|
-
tile = this.getTile(
|
|
120
|
-
id,
|
|
121
|
-
tx,
|
|
122
|
-
ty,
|
|
123
|
-
)
|
|
86
|
+
tile = this.tilePool.getTile(id, tx, ty)
|
|
124
87
|
|
|
125
88
|
this.extractState(tile)
|
|
126
89
|
this.lookup[id] = tile
|
|
@@ -128,10 +91,28 @@ export class PixelAccumulator {
|
|
|
128
91
|
}
|
|
129
92
|
}
|
|
130
93
|
}
|
|
94
|
+
|
|
95
|
+
return (didChange: boolean) => {
|
|
96
|
+
if (!didChange) {
|
|
97
|
+
let length = this.beforeTiles.length
|
|
98
|
+
|
|
99
|
+
for (let i = startIndex; i < length; i++) {
|
|
100
|
+
let t = this.beforeTiles[i]
|
|
101
|
+
|
|
102
|
+
if (t) {
|
|
103
|
+
this.lookup[t.id] = undefined
|
|
104
|
+
this.tilePool.releaseTile(t)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
this.beforeTiles.length = startIndex
|
|
109
|
+
}
|
|
110
|
+
return didChange
|
|
111
|
+
}
|
|
131
112
|
}
|
|
132
113
|
|
|
133
114
|
extractState(tile: PixelTile) {
|
|
134
|
-
let target = this.target
|
|
115
|
+
let target = this.config.target
|
|
135
116
|
let TILE_SIZE = this.config.tileSize
|
|
136
117
|
let dst = tile.data32
|
|
137
118
|
let src = target.data32
|
|
@@ -140,29 +121,45 @@ export class PixelAccumulator {
|
|
|
140
121
|
let targetWidth = target.width
|
|
141
122
|
let targetHeight = target.height
|
|
142
123
|
|
|
143
|
-
|
|
124
|
+
// If the tile is completely outside the canvas, zero it out.
|
|
125
|
+
if (startX >= targetWidth || startX + TILE_SIZE <= 0 || startY >= targetHeight || startY + TILE_SIZE <= 0) {
|
|
126
|
+
dst.fill(0)
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Calculate offset if tile starts off the left side of the screen
|
|
131
|
+
let srcOffsetX = Math.max(0, -startX)
|
|
132
|
+
let copyWidth = Math.max(0, Math.min(TILE_SIZE - srcOffsetX, targetWidth - Math.max(0, startX)))
|
|
144
133
|
|
|
145
134
|
for (let ly = 0; ly < TILE_SIZE; ly++) {
|
|
146
135
|
let globalY = startY + ly
|
|
147
136
|
let dstIndex = ly * TILE_SIZE
|
|
148
137
|
|
|
138
|
+
// Check negative bounds accurately
|
|
149
139
|
if (globalY < 0 || globalY >= targetHeight || copyWidth === 0) {
|
|
150
140
|
dst.fill(0, dstIndex, dstIndex + TILE_SIZE)
|
|
151
141
|
continue
|
|
152
142
|
}
|
|
153
143
|
|
|
154
|
-
let srcIndex = globalY * targetWidth + startX
|
|
144
|
+
let srcIndex = globalY * targetWidth + Math.max(0, startX)
|
|
155
145
|
let rowData = src.subarray(srcIndex, srcIndex + copyWidth)
|
|
156
146
|
|
|
157
|
-
|
|
147
|
+
// Shift the paste over by the offset
|
|
148
|
+
dst.set(rowData, dstIndex + srcOffsetX)
|
|
149
|
+
|
|
150
|
+
// Pad the left edge with 0s if we hung off the left side
|
|
151
|
+
if (srcOffsetX > 0) {
|
|
152
|
+
dst.fill(0, dstIndex, dstIndex + srcOffsetX)
|
|
153
|
+
}
|
|
158
154
|
|
|
159
|
-
if
|
|
160
|
-
|
|
155
|
+
// Pad the right edge with 0s if we hung off the right side
|
|
156
|
+
if (srcOffsetX + copyWidth < TILE_SIZE) {
|
|
157
|
+
dst.fill(0, dstIndex + srcOffsetX + copyWidth, dstIndex + TILE_SIZE)
|
|
161
158
|
}
|
|
162
159
|
}
|
|
163
160
|
}
|
|
164
161
|
|
|
165
|
-
|
|
162
|
+
extractPatch(): PixelPatchTiles {
|
|
166
163
|
let afterTiles: PixelTile[] = []
|
|
167
164
|
let length = this.beforeTiles.length
|
|
168
165
|
|
|
@@ -170,22 +167,40 @@ export class PixelAccumulator {
|
|
|
170
167
|
let beforeTile = this.beforeTiles[i]
|
|
171
168
|
|
|
172
169
|
if (beforeTile) {
|
|
173
|
-
let afterTile = this.getTile(
|
|
174
|
-
beforeTile.id,
|
|
175
|
-
beforeTile.tx,
|
|
176
|
-
beforeTile.ty,
|
|
177
|
-
)
|
|
170
|
+
let afterTile = this.tilePool.getTile(beforeTile.id, beforeTile.tx, beforeTile.ty)
|
|
178
171
|
|
|
179
172
|
this.extractState(afterTile)
|
|
180
173
|
afterTiles.push(afterTile)
|
|
181
174
|
}
|
|
182
175
|
}
|
|
183
176
|
|
|
184
|
-
|
|
177
|
+
let beforeTiles = this.beforeTiles
|
|
178
|
+
this.beforeTiles = []
|
|
179
|
+
this.lookup.length = 0
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
beforeTiles,
|
|
183
|
+
afterTiles,
|
|
184
|
+
}
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
187
|
+
rollback() {
|
|
188
|
+
let target = this.config.target
|
|
189
|
+
let tileSize = this.config.tileSize
|
|
190
|
+
let length = this.beforeTiles.length
|
|
191
|
+
|
|
192
|
+
applyPatchTiles(target, this.beforeTiles, tileSize)
|
|
193
|
+
|
|
194
|
+
for (let i = 0; i < length; i++) {
|
|
195
|
+
let tile = this.beforeTiles[i]
|
|
196
|
+
|
|
197
|
+
if (tile) {
|
|
198
|
+
this.lookup[tile.id] = undefined
|
|
199
|
+
this.tilePool.releaseTile(tile)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
this.beforeTiles.length = 0
|
|
204
|
+
this.lookup.length = 0
|
|
190
205
|
}
|
|
191
206
|
}
|
|
@@ -1,18 +1,30 @@
|
|
|
1
|
+
import type { IPixelData } from '../_types'
|
|
2
|
+
|
|
1
3
|
export class PixelEngineConfig {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
readonly tileSize: number
|
|
5
|
+
// pixelX = tileX << tileShift
|
|
6
|
+
// pixelY = tileY << tileShift
|
|
7
|
+
readonly tileShift: number
|
|
8
|
+
readonly tileMask: number
|
|
9
|
+
readonly tileArea: number
|
|
10
|
+
readonly target!: IPixelData
|
|
11
|
+
readonly targetColumns: number = 0
|
|
6
12
|
|
|
7
|
-
constructor(tileSize: number
|
|
13
|
+
constructor(tileSize: number, target: IPixelData) {
|
|
8
14
|
// Ensure it's a power of 2 to guarantee bitwise math works
|
|
9
15
|
if ((tileSize & (tileSize - 1)) !== 0) {
|
|
10
16
|
throw new Error('tileSize must be a power of 2')
|
|
11
17
|
}
|
|
12
18
|
|
|
13
19
|
this.tileSize = tileSize
|
|
14
|
-
this.tileShift = Math.
|
|
20
|
+
this.tileShift = 31 - Math.clz32(tileSize)
|
|
15
21
|
this.tileMask = tileSize - 1
|
|
16
22
|
this.tileArea = tileSize * tileSize
|
|
23
|
+
this.setTarget(target)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
setTarget(target: IPixelData) {
|
|
27
|
+
;(this as any).target = target
|
|
28
|
+
;(this as any).targetColumns = (target.width + this.tileMask) >> this.tileShift
|
|
17
29
|
}
|
|
18
30
|
}
|
|
@@ -17,17 +17,17 @@ export const mutatorApplyAlphaMask = ((writer: PixelWriter<any>, deps: Deps = de
|
|
|
17
17
|
} = deps
|
|
18
18
|
|
|
19
19
|
return {
|
|
20
|
-
applyAlphaMask
|
|
21
|
-
let target = writer.target
|
|
20
|
+
applyAlphaMask(mask: AlphaMask, opts: ApplyMaskToPixelDataOptions = {}): boolean {
|
|
21
|
+
let target = writer.config.target
|
|
22
22
|
const {
|
|
23
23
|
x = 0,
|
|
24
24
|
y = 0,
|
|
25
|
-
w =
|
|
26
|
-
h =
|
|
25
|
+
w = target.width,
|
|
26
|
+
h = target.height,
|
|
27
27
|
} = opts
|
|
28
28
|
|
|
29
|
-
writer.accumulator.storeRegionBeforeState(x, y, w, h)
|
|
30
|
-
applyAlphaMaskToPixelData(target, mask, opts)
|
|
29
|
+
const didChange = writer.accumulator.storeRegionBeforeState(x, y, w, h)
|
|
30
|
+
return didChange(applyAlphaMaskToPixelData(target, mask, opts))
|
|
31
31
|
},
|
|
32
32
|
}
|
|
33
33
|
}) satisfies HistoryMutator<any, Deps>
|
|
@@ -17,17 +17,17 @@ export const mutatorApplyBinaryMask = ((writer: PixelWriter<any>, deps: Deps = d
|
|
|
17
17
|
} = deps
|
|
18
18
|
|
|
19
19
|
return {
|
|
20
|
-
applyBinaryMask
|
|
21
|
-
let target = writer.target
|
|
20
|
+
applyBinaryMask(mask: BinaryMask, opts: ApplyMaskToPixelDataOptions = {}): boolean {
|
|
21
|
+
let target = writer.config.target
|
|
22
22
|
const {
|
|
23
23
|
x = 0,
|
|
24
24
|
y = 0,
|
|
25
|
-
w =
|
|
26
|
-
h =
|
|
25
|
+
w = target.width,
|
|
26
|
+
h = target.height,
|
|
27
27
|
} = opts
|
|
28
28
|
|
|
29
|
-
writer.accumulator.storeRegionBeforeState(x, y, w, h)
|
|
30
|
-
applyBinaryMaskToPixelData(target, mask, opts)
|
|
29
|
+
const didChange = writer.accumulator.storeRegionBeforeState(x, y, w, h)
|
|
30
|
+
return didChange(applyBinaryMaskToPixelData(target, mask, opts))
|
|
31
31
|
},
|
|
32
32
|
}
|
|
33
33
|
}) satisfies HistoryMutator<any, Deps>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type AlphaMask,
|
|
3
3
|
type BlendColor32,
|
|
4
|
-
type
|
|
4
|
+
type CircleAlphaMask,
|
|
5
5
|
type Color32,
|
|
6
6
|
type ColorBlendMaskOptions,
|
|
7
7
|
type HistoryMutator,
|
|
@@ -72,7 +72,7 @@ export const mutatorApplyCircleBrushStroke = ((writer: PixelWriter<any>, deps: D
|
|
|
72
72
|
y0: number,
|
|
73
73
|
x1: number,
|
|
74
74
|
y1: number,
|
|
75
|
-
brush:
|
|
75
|
+
brush: CircleAlphaMask,
|
|
76
76
|
alpha = 255,
|
|
77
77
|
blendFn: BlendColor32 = sourceOverPerfect,
|
|
78
78
|
) {
|
|
@@ -102,8 +102,9 @@ export const mutatorApplyCircleBrushStroke = ((writer: PixelWriter<any>, deps: D
|
|
|
102
102
|
const brushData = brush.data
|
|
103
103
|
const minOffset = brush.minOffset
|
|
104
104
|
|
|
105
|
-
const
|
|
106
|
-
const
|
|
105
|
+
const target = writer.config.target
|
|
106
|
+
const targetWidth = target.width
|
|
107
|
+
const targetHeight = target.height
|
|
107
108
|
|
|
108
109
|
forEachLinePoint(
|
|
109
110
|
x0,
|
|
@@ -171,7 +172,7 @@ export const mutatorApplyCircleBrushStroke = ((writer: PixelWriter<any>, deps: D
|
|
|
171
172
|
blendColorPixelOptions.h = bh
|
|
172
173
|
|
|
173
174
|
blendColorPixelDataAlphaMask(
|
|
174
|
-
|
|
175
|
+
target,
|
|
175
176
|
color,
|
|
176
177
|
mask as AlphaMask,
|
|
177
178
|
blendColorPixelOptions,
|