pixel-data-js 0.24.0 → 0.25.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/dist/index.dev.cjs +1476 -1834
  2. package/dist/index.dev.cjs.map +1 -1
  3. package/dist/index.dev.js +1465 -1816
  4. package/dist/index.dev.js.map +1 -1
  5. package/dist/index.prod.cjs +1475 -1833
  6. package/dist/index.prod.cjs.map +1 -1
  7. package/dist/index.prod.d.ts +233 -310
  8. package/dist/index.prod.js +1465 -1816
  9. package/dist/index.prod.js.map +1 -1
  10. package/package.json +1 -1
  11. package/src/Algorithm/floodFillSelection.ts +2 -2
  12. package/src/Canvas/canvas-blend-modes.ts +28 -0
  13. package/src/History/PixelAccumulator.ts +52 -29
  14. package/src/History/PixelEngineConfig.ts +7 -9
  15. package/src/History/PixelMutator/mutatorBlendPaintMask.ts +60 -0
  16. package/src/History/PixelMutator/mutatorBlendPixelData.ts +2 -2
  17. package/src/History/PixelMutator/mutatorBlendPixelDataAlphaMask.ts +2 -2
  18. package/src/History/PixelMutator/mutatorBlendPixelDataBinaryMask.ts +2 -2
  19. package/src/History/PixelMutator.ts +0 -20
  20. package/src/History/PixelPatchTiles.ts +2 -2
  21. package/src/History/PixelWriter.ts +132 -9
  22. package/src/Internal/helpers.ts +2 -0
  23. package/src/Paint/PaintBuffer.ts +269 -0
  24. package/src/{PixelTile/PaintBufferRenderer.ts → Paint/PaintBufferCanvasRenderer.ts} +13 -5
  25. package/src/Paint/makeCirclePaintAlphaMask.ts +41 -0
  26. package/src/{Mask/CircleBinaryMask.ts → Paint/makeCirclePaintBinaryMask.ts} +5 -6
  27. package/src/Paint/makePaintMask.ts +28 -0
  28. package/src/Paint/makeRectFalloffPaintAlphaMask.ts +47 -0
  29. package/src/PixelData/PixelBuffer32.ts +2 -2
  30. package/src/PixelData/PixelData.ts +1 -1
  31. package/src/PixelData/applyAlphaMaskToPixelData.ts +2 -2
  32. package/src/PixelData/applyBinaryMaskToPixelData.ts +2 -2
  33. package/src/PixelData/blendColorPixelData.ts +2 -2
  34. package/src/PixelData/blendColorPixelDataAlphaMask.ts +3 -3
  35. package/src/PixelData/blendColorPixelDataBinaryMask.ts +3 -3
  36. package/src/PixelData/blendPixel.ts +2 -2
  37. package/src/PixelData/blendPixelData.ts +3 -3
  38. package/src/PixelData/blendPixelDataAlphaMask.ts +3 -3
  39. package/src/PixelData/blendPixelDataBinaryMask.ts +3 -3
  40. package/src/PixelData/blendPixelDataPaintBuffer.ts +3 -3
  41. package/src/PixelData/clearPixelData.ts +2 -2
  42. package/src/PixelData/extractPixelData.ts +4 -4
  43. package/src/PixelData/extractPixelDataBuffer.ts +4 -4
  44. package/src/PixelData/fillPixelData.ts +5 -5
  45. package/src/PixelData/fillPixelDataBinaryMask.ts +3 -3
  46. package/src/PixelData/fillPixelDataFast.ts +5 -5
  47. package/src/PixelData/invertPixelData.ts +2 -2
  48. package/src/PixelData/pixelDataToAlphaMask.ts +2 -2
  49. package/src/PixelData/reflectPixelData.ts +3 -3
  50. package/src/PixelData/resamplePixelData.ts +2 -2
  51. package/src/PixelData/writePaintBufferToPixelData.ts +26 -0
  52. package/src/PixelData/writePixelDataBuffer.ts +5 -5
  53. package/src/Rect/trimMaskRectBounds.ts +121 -0
  54. package/src/Rect/trimRectBounds.ts +25 -116
  55. package/src/_types.ts +16 -15
  56. package/src/index.ts +11 -24
  57. package/src/History/PixelMutator/mutatorApplyCircleBrushStroke.ts +0 -182
  58. package/src/History/PixelMutator/mutatorApplyCirclePencil.ts +0 -59
  59. package/src/History/PixelMutator/mutatorApplyCirclePencilStroke.ts +0 -172
  60. package/src/History/PixelMutator/mutatorApplyRectBrush.ts +0 -64
  61. package/src/History/PixelMutator/mutatorApplyRectBrushStroke.ts +0 -184
  62. package/src/History/PixelMutator/mutatorApplyRectPencil.ts +0 -65
  63. package/src/History/PixelMutator/mutatorApplyRectPencilStroke.ts +0 -166
  64. package/src/History/PixelMutator/mutatorBlendColorCircleMask.ts +0 -71
  65. package/src/Mask/CircleAlphaMask.ts +0 -32
  66. package/src/PixelData/applyRectBrushToPixelData.ts +0 -98
  67. package/src/PixelData/blendColorPixelDataCircleMask.ts +0 -92
  68. package/src/PixelTile/PaintBuffer.ts +0 -122
  69. package/src/Rect/getCircleBrushOrPencilBounds.ts +0 -43
  70. package/src/Rect/getCircleBrushOrPencilStrokeBounds.ts +0 -24
  71. package/src/Rect/getRectBrushOrPencilBounds.ts +0 -38
  72. package/src/Rect/getRectBrushOrPencilStrokeBounds.ts +0 -26
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pixel-data-js",
3
3
  "type": "module",
4
- "version": "0.24.0",
4
+ "version": "0.25.2",
5
5
  "packageManager": "pnpm@10.30.0",
6
6
  "description": "JS Pixel and ImageData operations",
7
7
  "author": {
@@ -2,7 +2,7 @@ import { type BinaryMaskRect, type Color32, type ImageDataLike, MaskType, type R
2
2
  import { colorDistance } from '../color'
3
3
  import { extractImageDataBuffer } from '../ImageData/extractImageDataBuffer'
4
4
  import type { PixelData } from '../PixelData/PixelData'
5
- import { trimRectBounds } from '../Rect/trimRectBounds'
5
+ import { trimMaskRectBounds } from '../Rect/trimMaskRectBounds'
6
6
 
7
7
  export type FloodFillImageDataOptions = {
8
8
  contiguous?: boolean
@@ -205,7 +205,7 @@ export function floodFillSelection(
205
205
  }
206
206
  }
207
207
 
208
- trimRectBounds(
208
+ trimMaskRectBounds(
209
209
  selectionRect,
210
210
  { x: 0, y: 0, w: width, h: height },
211
211
  )
@@ -0,0 +1,28 @@
1
+ import { BaseBlendMode } from '../BlendModes/blend-modes'
2
+
3
+ export const CANVAS_COMPOSITE_MAP = {
4
+ [BaseBlendMode.overwrite]: 'copy',
5
+ [BaseBlendMode.sourceOver]: 'source-over',
6
+ [BaseBlendMode.darken]: 'darken',
7
+ [BaseBlendMode.multiply]: 'multiply',
8
+ [BaseBlendMode.colorBurn]: 'color-burn',
9
+ [BaseBlendMode.lighten]: 'lighten',
10
+ [BaseBlendMode.screen]: 'screen',
11
+ [BaseBlendMode.colorDodge]: 'color-dodge',
12
+ [BaseBlendMode.linearDodge]: 'lighter',
13
+ [BaseBlendMode.overlay]: 'overlay',
14
+ [BaseBlendMode.softLight]: 'soft-light',
15
+ [BaseBlendMode.hardLight]: 'hard-light',
16
+ [BaseBlendMode.difference]: 'difference',
17
+ [BaseBlendMode.exclusion]: 'exclusion',
18
+ } as const
19
+
20
+ export type CanvasBlendModeIndex = keyof typeof CANVAS_COMPOSITE_MAP
21
+ export type CanvasCompositeOperation = typeof CANVAS_COMPOSITE_MAP[CanvasBlendModeIndex]
22
+
23
+ /**
24
+ * example
25
+ * function getCanvasCompositeOperation(mode: CanvasBlendModeIndex): CanvasCompositeOperation {
26
+ * return CANVAS_COMPOSITE_MAP[mode]
27
+ * }
28
+ */
@@ -27,11 +27,11 @@ export class PixelAccumulator {
27
27
  * @param y pixel y coordinate
28
28
  */
29
29
  storePixelBeforeState(x: number, y: number): DidChangeFn {
30
- let shift = this.config.tileShift
31
- let columns = this.config.targetColumns
32
- let tx = x >> shift
33
- let ty = y >> shift
34
- let id = ty * columns + tx
30
+ const shift = this.config.tileShift
31
+ const columns = this.config.targetColumns
32
+ const tx = x >> shift
33
+ const ty = y >> shift
34
+ const id = ty * columns + tx
35
35
 
36
36
  let tile = this.lookup[id]
37
37
  let added = false
@@ -67,19 +67,19 @@ export class PixelAccumulator {
67
67
  w: number,
68
68
  h: number,
69
69
  ): DidChangeFn {
70
- let shift = this.config.tileShift
71
- let columns = this.config.targetColumns
70
+ const shift = this.config.tileShift
71
+ const columns = this.config.targetColumns
72
72
 
73
- let startX = x >> shift
74
- let startY = y >> shift
75
- let endX = (x + w - 1) >> shift
76
- let endY = (y + h - 1) >> shift
73
+ const startX = x >> shift
74
+ const startY = y >> shift
75
+ const endX = (x + w - 1) >> shift
76
+ const endY = (y + h - 1) >> shift
77
77
 
78
- let startIndex = this.beforeTiles.length
78
+ const startIndex = this.beforeTiles.length
79
79
 
80
80
  for (let ty = startY; ty <= endY; ty++) {
81
81
  for (let tx = startX; tx <= endX; tx++) {
82
- let id = ty * columns + tx
82
+ const id = ty * columns + tx
83
83
  let tile = this.lookup[id]
84
84
 
85
85
  if (!tile) {
@@ -94,7 +94,7 @@ export class PixelAccumulator {
94
94
 
95
95
  return (didChange: boolean) => {
96
96
  if (!didChange) {
97
- let length = this.beforeTiles.length
97
+ const length = this.beforeTiles.length
98
98
 
99
99
  for (let i = startIndex; i < length; i++) {
100
100
  let t = this.beforeTiles[i]
@@ -111,15 +111,38 @@ export class PixelAccumulator {
111
111
  }
112
112
  }
113
113
 
114
+ storeTileBeforeState(id: number, tx: number, ty: number): DidChangeFn {
115
+ let tile = this.lookup[id]
116
+ let added = false
117
+
118
+ if (!tile) {
119
+ tile = this.tilePool.getTile(id, tx, ty)
120
+
121
+ this.extractState(tile)
122
+ this.lookup[id] = tile
123
+ this.beforeTiles.push(tile)
124
+ added = true
125
+ }
126
+
127
+ return (didChange: boolean) => {
128
+ if (!didChange && added) {
129
+ this.beforeTiles.pop()
130
+ this.lookup[id] = undefined
131
+ this.tilePool.releaseTile(tile!)
132
+ }
133
+ return didChange
134
+ }
135
+ }
136
+
114
137
  extractState(tile: PixelTile) {
115
- let target = this.config.target
116
- let TILE_SIZE = this.config.tileSize
117
- let dst = tile.data32
118
- let src = target.data32
119
- let startX = tile.tx * TILE_SIZE
120
- let startY = tile.ty * TILE_SIZE
121
- let targetWidth = target.width
122
- let targetHeight = target.height
138
+ const target = this.config.target
139
+ const TILE_SIZE = this.config.tileSize
140
+ const dst = tile.data32
141
+ const src = target.data32
142
+ const startX = tile.tx * TILE_SIZE
143
+ const startY = tile.ty * TILE_SIZE
144
+ const targetWidth = target.width
145
+ const targetHeight = target.height
123
146
 
124
147
  // If the tile is completely outside the canvas, zero it out.
125
148
  if (startX >= targetWidth || startX + TILE_SIZE <= 0 || startY >= targetHeight || startY + TILE_SIZE <= 0) {
@@ -160,8 +183,8 @@ export class PixelAccumulator {
160
183
  }
161
184
 
162
185
  extractPatch(): PixelPatchTiles {
163
- let afterTiles: PixelTile[] = []
164
- let length = this.beforeTiles.length
186
+ const afterTiles: PixelTile[] = []
187
+ const length = this.beforeTiles.length
165
188
 
166
189
  for (let i = 0; i < length; i++) {
167
190
  let beforeTile = this.beforeTiles[i]
@@ -174,7 +197,7 @@ export class PixelAccumulator {
174
197
  }
175
198
  }
176
199
 
177
- let beforeTiles = this.beforeTiles
200
+ const beforeTiles = this.beforeTiles
178
201
  this.beforeTiles = []
179
202
  this.lookup.length = 0
180
203
 
@@ -184,10 +207,10 @@ export class PixelAccumulator {
184
207
  }
185
208
  }
186
209
 
187
- rollback() {
188
- let target = this.config.target
189
- let tileSize = this.config.tileSize
190
- let length = this.beforeTiles.length
210
+ rollbackAfterError() {
211
+ const target = this.config.target
212
+ const tileSize = this.config.tileSize
213
+ const length = this.beforeTiles.length
191
214
 
192
215
  applyPatchTiles(target, this.beforeTiles, tileSize)
193
216
 
@@ -1,4 +1,4 @@
1
- import type { IPixelData } from '../_types'
1
+ import type { PixelData } from '../PixelData/PixelData'
2
2
 
3
3
  export class PixelEngineConfig {
4
4
  readonly tileSize: number
@@ -7,10 +7,11 @@ export class PixelEngineConfig {
7
7
  readonly tileShift: number
8
8
  readonly tileMask: number
9
9
  readonly tileArea: number
10
- readonly target!: IPixelData
10
+ readonly target!: PixelData
11
11
  readonly targetColumns: number = 0
12
+ readonly targetRows: number = 0
12
13
 
13
- constructor(tileSize: number, target: IPixelData) {
14
+ constructor(tileSize: number, target: PixelData) {
14
15
  // Ensure it's a power of 2 to guarantee bitwise math works
15
16
  if ((tileSize & (tileSize - 1)) !== 0) {
16
17
  throw new Error('tileSize must be a power of 2')
@@ -20,11 +21,8 @@ export class PixelEngineConfig {
20
21
  this.tileShift = 31 - Math.clz32(tileSize)
21
22
  this.tileMask = tileSize - 1
22
23
  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
24
+ this.target = target
25
+ this.targetColumns = (target.width + this.tileMask) >> this.tileShift
26
+ this.targetRows = (target.height + this.tileMask) >> this.tileShift
29
27
  }
30
28
  }
@@ -0,0 +1,60 @@
1
+ import { type Color32, type HistoryMutator, MaskType, type PaintMask } from '../../_types'
2
+ import { sourceOverPerfect } from '../../BlendModes/blend-modes-perfect'
3
+ import { blendColorPixelDataAlphaMask } from '../../PixelData/blendColorPixelDataAlphaMask'
4
+ import { blendColorPixelDataBinaryMask } from '../../PixelData/blendColorPixelDataBinaryMask'
5
+ import { PixelWriter } from '../PixelWriter'
6
+
7
+ const defaults = {
8
+ blendColorPixelDataAlphaMask,
9
+ blendColorPixelDataBinaryMask,
10
+ }
11
+ type Deps = Partial<typeof defaults>
12
+
13
+ /**
14
+ * @param deps - @hidden
15
+ */
16
+ export const mutatorBlendPaintMask = ((writer: PixelWriter<any>, deps: Partial<Deps> = defaults) => {
17
+ const {
18
+ blendColorPixelDataBinaryMask = defaults.blendColorPixelDataBinaryMask,
19
+ blendColorPixelDataAlphaMask = defaults.blendColorPixelDataAlphaMask,
20
+ } = deps
21
+
22
+ const OPTS = {
23
+ x: 0,
24
+ y: 0,
25
+ blendFn: sourceOverPerfect,
26
+ alpha: 255,
27
+ }
28
+
29
+ return {
30
+ blendColorPaintMask(
31
+ color: Color32,
32
+ mask: PaintMask,
33
+ x: number,
34
+ y: number,
35
+ alpha = 255,
36
+ blendFn = sourceOverPerfect,
37
+ ): boolean {
38
+ const tx = x + mask.centerOffsetX
39
+ const ty = y + mask.centerOffsetY
40
+
41
+ const didChange = writer.accumulator.storeRegionBeforeState(tx, ty, mask.w, mask.h)
42
+
43
+ OPTS.x = tx
44
+ OPTS.y = ty
45
+ OPTS.alpha = alpha
46
+ OPTS.blendFn = blendFn
47
+
48
+ if (mask.type === MaskType.BINARY) {
49
+ return didChange(
50
+ blendColorPixelDataBinaryMask(writer.config.target, color, mask, OPTS),
51
+ )
52
+ } else {
53
+ return didChange(
54
+ blendColorPixelDataAlphaMask(writer.config.target, color, mask, OPTS),
55
+ )
56
+ }
57
+ },
58
+ }
59
+ }) satisfies HistoryMutator<any, Deps>
60
+
@@ -1,4 +1,4 @@
1
- import type { HistoryMutator, IPixelData, PixelBlendOptions } from '../../_types'
1
+ import type { HistoryMutator, IPixelData32, PixelBlendOptions } from '../../_types'
2
2
  import { blendPixelData } from '../../PixelData/blendPixelData'
3
3
  import { PixelWriter } from '../PixelWriter'
4
4
 
@@ -15,7 +15,7 @@ export const mutatorBlendPixelData = ((writer: PixelWriter<any>, deps: Partial<D
15
15
 
16
16
  return {
17
17
  blendPixelData(
18
- src: IPixelData,
18
+ src: IPixelData32,
19
19
  opts: PixelBlendOptions = {},
20
20
  ): boolean {
21
21
  const {
@@ -1,4 +1,4 @@
1
- import type { AlphaMask, HistoryMutator, IPixelData, PixelBlendMaskOptions } from '../../_types'
1
+ import type { AlphaMask, HistoryMutator, IPixelData32, PixelBlendMaskOptions } from '../../_types'
2
2
  import { blendPixelDataAlphaMask } from '../../PixelData/blendPixelDataAlphaMask'
3
3
  import { PixelWriter } from '../PixelWriter'
4
4
 
@@ -15,7 +15,7 @@ export const mutatorBlendPixelDataAlphaMask = ((writer: PixelWriter<any>, deps:
15
15
 
16
16
  return {
17
17
  blendPixelDataAlphaMask(
18
- src: IPixelData,
18
+ src: IPixelData32,
19
19
  mask: AlphaMask,
20
20
  opts: PixelBlendMaskOptions = {},
21
21
  ): boolean {
@@ -1,4 +1,4 @@
1
- import type { BinaryMask, HistoryMutator, IPixelData, PixelBlendMaskOptions } from '../../_types'
1
+ import type { BinaryMask, HistoryMutator, IPixelData32, PixelBlendMaskOptions } from '../../_types'
2
2
  import { blendPixelDataBinaryMask } from '../../PixelData/blendPixelDataBinaryMask'
3
3
  import { PixelWriter } from '../PixelWriter'
4
4
 
@@ -15,7 +15,7 @@ export const mutatorBlendPixelDataBinaryMask = ((writer: PixelWriter<any>, deps:
15
15
 
16
16
  return {
17
17
  blendPixelDataBinaryMask(
18
- src: IPixelData,
18
+ src: IPixelData32,
19
19
  mask: BinaryMask,
20
20
  opts: PixelBlendMaskOptions = {},
21
21
  ): boolean {
@@ -1,13 +1,3 @@
1
- import { mutatorApplyAlphaMask } from './PixelMutator/mutatorApplyAlphaMask'
2
- import { mutatorApplyBinaryMask } from './PixelMutator/mutatorApplyBinaryMask'
3
- import { mutatorApplyCircleBrushStroke } from './PixelMutator/mutatorApplyCircleBrushStroke'
4
- import { mutatorBlendColorCircleMask } from './PixelMutator/mutatorBlendColorCircleMask'
5
- import { mutatorApplyCirclePencil } from './PixelMutator/mutatorApplyCirclePencil'
6
- import { mutatorApplyCirclePencilStroke } from './PixelMutator/mutatorApplyCirclePencilStroke'
7
- import { mutatorApplyRectBrush } from './PixelMutator/mutatorApplyRectBrush'
8
- import { mutatorApplyRectBrushStroke } from './PixelMutator/mutatorApplyRectBrushStroke'
9
- import { mutatorApplyRectPencil } from './PixelMutator/mutatorApplyRectPencil'
10
- import { mutatorApplyRectPencilStroke } from './PixelMutator/mutatorApplyRectPencilStroke'
11
1
  import { mutatorBlendColor } from './PixelMutator/mutatorBlendColor'
12
2
  import { mutatorBlendPixel } from './PixelMutator/mutatorBlendPixel'
13
3
  import { mutatorBlendPixelData } from './PixelMutator/mutatorBlendPixelData'
@@ -22,17 +12,7 @@ import type { PixelWriter } from './PixelWriter'
22
12
  export function makeFullPixelMutator(writer: PixelWriter<any>) {
23
13
  return {
24
14
  // @sort
25
- ...mutatorApplyAlphaMask(writer),
26
- ...mutatorApplyBinaryMask(writer),
27
- ...mutatorApplyCircleBrushStroke(writer),
28
- ...mutatorApplyCirclePencil(writer),
29
- ...mutatorApplyCirclePencilStroke(writer),
30
- ...mutatorApplyRectBrush(writer),
31
- ...mutatorApplyRectBrushStroke(writer),
32
- ...mutatorApplyRectPencil(writer),
33
- ...mutatorApplyRectPencilStroke(writer),
34
15
  ...mutatorBlendColor(writer),
35
- ...mutatorBlendColorCircleMask(writer),
36
16
  ...mutatorBlendPixel(writer),
37
17
  ...mutatorBlendPixelData(writer),
38
18
  ...mutatorBlendPixelDataAlphaMask(writer),
@@ -1,4 +1,4 @@
1
- import type { IPixelData } from '../_types'
1
+ import type { IPixelData32 } from '../_types'
2
2
  import { PixelTile } from '../PixelTile/PixelTile'
3
3
 
4
4
  export type PixelPatchTiles = {
@@ -6,7 +6,7 @@ export type PixelPatchTiles = {
6
6
  afterTiles: PixelTile[]
7
7
  }
8
8
 
9
- export function applyPatchTiles(target: IPixelData, tiles: PixelTile[], tileSize: number) {
9
+ export function applyPatchTiles(target: IPixelData32, tiles: PixelTile[], tileSize: number) {
10
10
  for (let i = 0; i < tiles.length; i++) {
11
11
  const tile = tiles[i]
12
12
 
@@ -1,9 +1,13 @@
1
- import type { IPixelData } from '../_types'
1
+ import { sourceOverPerfect } from '../BlendModes/blend-modes-perfect'
2
+ import { resizeImageData } from '../ImageData/resizeImageData'
3
+ import { PaintBuffer } from '../Paint/PaintBuffer'
4
+ import { blendPixelData } from '../PixelData/blendPixelData'
5
+ import type { PixelData } from '../PixelData/PixelData'
6
+ import { PixelTilePool } from '../PixelTile/PixelTilePool'
2
7
  import { type HistoryActionFactory, makeHistoryAction } from './HistoryAction'
3
8
  import { HistoryManager } from './HistoryManager'
4
9
  import { PixelAccumulator } from './PixelAccumulator'
5
10
  import { PixelEngineConfig } from './PixelEngineConfig'
6
- import { PixelTilePool } from '../PixelTile/PixelTilePool'
7
11
 
8
12
  export interface PixelWriterOptions {
9
13
  maxHistorySteps?: number
@@ -11,6 +15,7 @@ export interface PixelWriterOptions {
11
15
  historyManager?: HistoryManager
12
16
  historyActionFactory?: HistoryActionFactory
13
17
  pixelTilePool?: PixelTilePool,
18
+ accumulator?: PixelAccumulator
14
19
  }
15
20
 
16
21
  /**
@@ -39,34 +44,72 @@ export class PixelWriter<M> {
39
44
  readonly accumulator: PixelAccumulator
40
45
  readonly historyActionFactory: HistoryActionFactory
41
46
  readonly config: PixelEngineConfig
47
+ readonly pixelTilePool: PixelTilePool
48
+ readonly paintBuffer: PaintBuffer
42
49
  readonly mutator: M
43
50
 
44
- constructor(target: IPixelData, mutatorFactory: (writer: PixelWriter<any>) => M, {
51
+ private blendPixelDataOpts = {
52
+ alpha: 255,
53
+ blendFn: sourceOverPerfect,
54
+ x: 0,
55
+ y: 0,
56
+ w: 0,
57
+ h: 0,
58
+ }
59
+
60
+ private _inProgress = false
61
+
62
+ constructor(target: PixelData, mutatorFactory: (writer: PixelWriter<any>) => M, {
45
63
  tileSize = 256,
46
64
  maxHistorySteps = 50,
47
65
  historyManager = new HistoryManager(maxHistorySteps),
48
66
  historyActionFactory = makeHistoryAction,
49
67
  pixelTilePool,
68
+ accumulator,
50
69
  }: PixelWriterOptions = {}) {
51
70
  this.config = new PixelEngineConfig(tileSize, target)
52
71
  this.historyManager = historyManager
53
- pixelTilePool ??= new PixelTilePool(this.config)
54
- this.accumulator = new PixelAccumulator(this.config, pixelTilePool)
72
+ this.pixelTilePool = pixelTilePool ?? new PixelTilePool(this.config)
73
+ this.accumulator = accumulator ?? new PixelAccumulator(this.config, this.pixelTilePool)
55
74
  this.historyActionFactory = historyActionFactory
56
75
  this.mutator = mutatorFactory(this)
76
+ this.paintBuffer = new PaintBuffer(this.config, this.pixelTilePool)
57
77
  }
58
78
 
79
+ /**
80
+ * Executes `transaction` and commits the resulting pixel changes as a single
81
+ * undoable history action.
82
+ *
83
+ * - If `transaction` throws, all accumulated changes are rolled back and the error
84
+ * is re-thrown. No action is committed.
85
+ * - If `transaction` completes without modifying any pixels, no action is committed.
86
+ * - `withHistory` is not re-entrant. Calling it again from inside `transaction` will
87
+ * throw immediately to prevent silent data loss from a nested extractPatch.
88
+ *
89
+ * @param transaction Callback to be executed inside the transaction.
90
+ * @param after Called after both undo and redo — use for generic change notifications.
91
+ * @param afterUndo Called after undo only — use for dimension or state changes specific to undo.
92
+ * @param afterRedo Called after redo only.
93
+ */
59
94
  withHistory(
60
- cb: (mutator: M) => void,
95
+ transaction: (mutator: M) => void,
61
96
  after?: () => void,
62
97
  afterUndo?: () => void,
63
98
  afterRedo?: () => void,
64
- ) {
99
+ ): void {
100
+ if (this._inProgress) {
101
+ throw new Error('withHistory is not re-entrant — commit or rollback the current operation first')
102
+ }
103
+
104
+ this._inProgress = true
105
+
65
106
  try {
66
- cb(this.mutator)
107
+ transaction(this.mutator)
67
108
  } catch (e) {
68
- this.accumulator.rollback()
109
+ this.accumulator.rollbackAfterError()
69
110
  throw e
111
+ } finally {
112
+ this._inProgress = false
70
113
  }
71
114
 
72
115
  if (this.accumulator.beforeTiles.length === 0) return
@@ -76,4 +119,84 @@ export class PixelWriter<M> {
76
119
 
77
120
  this.historyManager.commit(action)
78
121
  }
122
+
123
+ resize(
124
+ newWidth: number,
125
+ newHeight: number,
126
+ offsetX = 0,
127
+ offsetY = 0,
128
+ after?: (target: ImageData) => void,
129
+ afterUndo?: (target: ImageData) => void,
130
+ afterRedo?: (target: ImageData) => void,
131
+ resizeImageDataFn = resizeImageData,
132
+ ): void {
133
+ if (this._inProgress) {
134
+ throw new Error('Cannot resize inside a withHistory callback')
135
+ }
136
+
137
+ if (this.accumulator.beforeTiles.length > 0) {
138
+ throw new Error('Cannot resize with an open accumulator — commit or rollback first')
139
+ }
140
+
141
+ const config = this.config
142
+ const target = config.target
143
+ const beforeImageData = target.imageData
144
+ const afterImageData = resizeImageDataFn(beforeImageData, newWidth, newHeight, offsetX, offsetY)
145
+
146
+ target.set(afterImageData)
147
+
148
+ this.historyManager.commit({
149
+ undo: () => {
150
+ target.set(beforeImageData)
151
+ afterUndo?.(beforeImageData)
152
+ after?.(beforeImageData)
153
+ },
154
+ redo: () => {
155
+ target.set(afterImageData)
156
+ afterRedo?.(afterImageData)
157
+ after?.(afterImageData)
158
+ },
159
+ })
160
+ }
161
+
162
+ commitPaintBuffer(
163
+ alpha = 255,
164
+ blendFn = sourceOverPerfect,
165
+ blendPixelDataFn = blendPixelData,
166
+ ) {
167
+ const paintBuffer = this.paintBuffer
168
+ const tileShift = paintBuffer.config.tileShift
169
+ const lookup = paintBuffer.lookup
170
+
171
+ const opts = this.blendPixelDataOpts
172
+
173
+ opts.alpha = alpha
174
+ opts.blendFn = blendFn
175
+
176
+ for (let i = 0; i < lookup.length; i++) {
177
+ const tile = lookup[i]
178
+
179
+ if (tile) {
180
+ const didChange = this.accumulator.storeTileBeforeState(tile.id, tile.tx, tile.ty)
181
+
182
+ const dx = tile.tx << tileShift
183
+ const dy = tile.ty << tileShift
184
+
185
+ opts.x = dx
186
+ opts.y = dy
187
+ opts.w = tile.width
188
+ opts.h = tile.height
189
+
190
+ didChange(
191
+ blendPixelDataFn(
192
+ this.config.target,
193
+ tile,
194
+ opts,
195
+ ),
196
+ )
197
+ }
198
+ }
199
+
200
+ paintBuffer.clear()
201
+ }
79
202
  }
@@ -0,0 +1,2 @@
1
+ // @__INLINE_MACRO__
2
+ export const macro_halfAndFloor = (value: number) => value >> 1