pixel-data-js 0.35.0 → 0.36.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.
Files changed (31) hide show
  1. package/dist/index.prod.cjs +157 -31
  2. package/dist/index.prod.cjs.map +1 -1
  3. package/dist/index.prod.d.ts +24 -12
  4. package/dist/index.prod.js +153 -30
  5. package/dist/index.prod.js.map +1 -1
  6. package/package.json +1 -1
  7. package/src/History/PixelAccumulator.ts +27 -6
  8. package/src/History/PixelMutator/mutatorApplyAlphaMask.ts +2 -0
  9. package/src/History/PixelMutator/mutatorApplyBinaryMask.ts +5 -1
  10. package/src/History/PixelMutator/mutatorApplyMask.ts +1 -0
  11. package/src/History/PixelMutator/mutatorBlendAlphaMask.ts +1 -0
  12. package/src/History/PixelMutator/mutatorBlendBinaryMask.ts +1 -0
  13. package/src/History/PixelMutator/mutatorBlendColor.ts +2 -0
  14. package/src/History/PixelMutator/mutatorBlendColorPaintAlphaMask.ts +1 -0
  15. package/src/History/PixelMutator/mutatorBlendColorPaintBinaryMask.ts +1 -0
  16. package/src/History/PixelMutator/mutatorBlendColorPaintMask.ts +1 -0
  17. package/src/History/PixelMutator/mutatorBlendColorPaintRect.ts +2 -0
  18. package/src/History/PixelMutator/mutatorBlendMask.ts +1 -0
  19. package/src/History/PixelMutator/mutatorBlendPixel.ts +1 -0
  20. package/src/History/PixelMutator/mutatorBlendPixelData.ts +1 -0
  21. package/src/History/PixelMutator/mutatorClear.ts +2 -1
  22. package/src/History/PixelMutator/mutatorFill.ts +53 -37
  23. package/src/History/PixelMutator/mutatorFillBinaryMask.ts +2 -1
  24. package/src/History/PixelMutator/mutatorInvert.ts +3 -2
  25. package/src/History/PixelMutator.ts +1 -2
  26. package/src/Paint/Render/PaintCursorRenderer.ts +9 -0
  27. package/src/PixelData/_pixelData-types.ts +7 -0
  28. package/src/PixelData/cropPixelData.ts +36 -0
  29. package/src/PixelData/fillPixelData.ts +6 -6
  30. package/src/PixelData/trimPixelData.ts +49 -0
  31. package/src/index.ts +2 -0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pixel-data-js",
3
3
  "type": "module",
4
- "version": "0.35.0",
4
+ "version": "0.36.0",
5
5
  "packageManager": "pnpm@10.33.0",
6
6
  "description": "JS Pixel and ImageData operations",
7
7
  "author": {
@@ -26,9 +26,17 @@ export class PixelAccumulator {
26
26
  * @param x pixel x coordinate
27
27
  * @param y pixel y coordinate
28
28
  */
29
- storePixelBeforeState(x: number, y: number): DidChangeFn {
29
+ storePixelBeforeState(x: number, y: number): DidChangeFn | null {
30
30
  const shift = this.config.tileShift
31
31
  const columns = this.config.targetColumns
32
+ const targetWidth = this.config.target.w
33
+ const targetHeight = this.config.target.h
34
+
35
+ // Return a no-op if the pixel is outside the target boundaries
36
+ if (x < 0 || x >= targetWidth || y < 0 || y >= targetHeight) {
37
+ return null
38
+ }
39
+
32
40
  const tx = x >> shift
33
41
  const ty = y >> shift
34
42
  const id = ty * columns + tx
@@ -66,14 +74,27 @@ export class PixelAccumulator {
66
74
  y: number,
67
75
  w: number,
68
76
  h: number,
69
- ): DidChangeFn {
77
+ ): DidChangeFn | null {
70
78
  const shift = this.config.tileShift
71
79
  const columns = this.config.targetColumns
80
+ const targetWidth = this.config.target.w
81
+ const targetHeight = this.config.target.h
82
+
83
+ // Clamp the bounding box to the actual canvas dimensions
84
+ const clipX1 = Math.max(0, x)
85
+ const clipY1 = Math.max(0, y)
86
+ const clipX2 = Math.min(targetWidth - 1, x + w - 1)
87
+ const clipY2 = Math.min(targetHeight - 1, y + h - 1)
88
+
89
+ // If the region is entirely off-canvas, return a no-op
90
+ if (clipX2 < clipX1 || clipY2 < clipY1) {
91
+ return null
92
+ }
72
93
 
73
- const startX = x >> shift
74
- const startY = y >> shift
75
- const endX = (x + w - 1) >> shift
76
- const endY = (y + h - 1) >> shift
94
+ const startX = clipX1 >> shift
95
+ const startY = clipY1 >> shift
96
+ const endX = clipX2 >> shift
97
+ const endY = clipY2 >> shift
77
98
 
78
99
  const startIndex = this.beforeTiles.length
79
100
 
@@ -26,6 +26,8 @@ export const mutatorApplyAlphaMask = ((writer: PixelWriter<any>, deps: Deps = de
26
26
  const h = opts?.h ?? target.h
27
27
 
28
28
  const didChange = writer.accumulator.storeRegionBeforeState(x, y, w, h)
29
+ if (!didChange) return false
30
+
29
31
  return didChange(applyAlphaMaskToPixelData(target, mask, opts))
30
32
  },
31
33
  }
@@ -26,7 +26,11 @@ export const mutatorApplyBinaryMask = ((writer: PixelWriter<any>, deps: Deps = d
26
26
  const h = opts?.h ?? target.h
27
27
 
28
28
  const didChange = writer.accumulator.storeRegionBeforeState(x, y, w, h)
29
- return didChange(applyBinaryMaskToPixelData(target, mask, opts))
29
+ if (!didChange) return false
30
+
31
+ const b = applyBinaryMaskToPixelData(target, mask, opts)
32
+ console.log({ b })
33
+ return didChange(b)
30
34
  },
31
35
  }
32
36
  }) satisfies HistoryMutator<any, Deps>
@@ -29,6 +29,7 @@ export const mutatorApplyMask = ((writer: PixelWriter<any>, deps: Deps = default
29
29
  const h = opts?.h ?? target.h
30
30
 
31
31
  const didChange = writer.accumulator.storeRegionBeforeState(x, y, w, h)
32
+ if (!didChange) return false
32
33
 
33
34
  if (mask.type === MaskType.BINARY) {
34
35
  return didChange(applyBinaryMaskToPixelData(target, mask, opts))
@@ -27,6 +27,7 @@ export const mutatorBlendAlphaMask = ((writer: PixelWriter<any>, deps: Partial<D
27
27
  const h = opts?.h ?? src.h
28
28
 
29
29
  const didChange = writer.accumulator.storeRegionBeforeState(x, y, w, h)
30
+ if (!didChange) return false
30
31
 
31
32
  return didChange(
32
33
  blendPixelDataAlphaMask(writer.config.target, src, mask, opts),
@@ -27,6 +27,7 @@ export const mutatorBlendBinaryMask = ((writer: PixelWriter<any>, deps: Partial<
27
27
  const h = opts?.h ?? src.h
28
28
 
29
29
  const didChange = writer.accumulator.storeRegionBeforeState(x, y, w, h)
30
+ if (!didChange) return false
30
31
 
31
32
  return didChange(
32
33
  blendPixelDataBinaryMask(writer.config.target, src, mask, opts),
@@ -25,6 +25,8 @@ export const mutatorBlendColor = ((writer: PixelWriter<any>, deps: Deps = defaul
25
25
  const h = opts?.h ?? target.h
26
26
 
27
27
  const didChange = writer.accumulator.storeRegionBeforeState(x, y, w, h)
28
+ if (!didChange) return false
29
+
28
30
  return didChange(
29
31
  blendColorPixelData(target, color, opts),
30
32
  )
@@ -37,6 +37,7 @@ export const mutatorBlendColorPaintAlphaMask = ((writer: PixelWriter<any>, deps:
37
37
  const ty = y + mask.centerOffsetY
38
38
 
39
39
  const didChange = writer.accumulator.storeRegionBeforeState(tx, ty, mask.w, mask.h)
40
+ if (!didChange) return false
40
41
 
41
42
  OPTS.x = tx
42
43
  OPTS.y = ty
@@ -37,6 +37,7 @@ export const mutatorBlendColorPaintBinaryMask = ((writer: PixelWriter<any>, deps
37
37
  const ty = y + mask.centerOffsetY
38
38
 
39
39
  const didChange = writer.accumulator.storeRegionBeforeState(tx, ty, mask.w, mask.h)
40
+ if (!didChange) return false
40
41
 
41
42
  OPTS.x = tx
42
43
  OPTS.y = ty
@@ -41,6 +41,7 @@ export const mutatorBlendColorPaintMask = ((writer: PixelWriter<any>, deps: Part
41
41
  const ty = y + mask.centerOffsetY
42
42
 
43
43
  const didChange = writer.accumulator.storeRegionBeforeState(tx, ty, mask.w, mask.h)
44
+ if (!didChange) return false
44
45
 
45
46
  OPTS.x = tx
46
47
  OPTS.y = ty
@@ -43,6 +43,8 @@ export const mutatorBlendColorPaintRect = ((writer: PixelWriter<any>, deps: Deps
43
43
  OPTS.alpha = alpha
44
44
 
45
45
  const didChange = writer.accumulator.storeRegionBeforeState(topLeftX, topLeftY, brushWidth, brushHeight)
46
+ if (!didChange) return false
47
+
46
48
  return didChange(
47
49
  blendColorPixelData(
48
50
  target,
@@ -29,6 +29,7 @@ export const mutatorBlendMask = ((writer: PixelWriter<any>, deps: Partial<Deps>
29
29
  const h = opts?.h ?? src.h
30
30
 
31
31
  const didChange = writer.accumulator.storeRegionBeforeState(x, y, w, h)
32
+ if (!didChange) return false
32
33
 
33
34
  if (mask.type === MaskType.BINARY) {
34
35
  return didChange(
@@ -23,6 +23,7 @@ export const mutatorBlendPixel = ((writer: PixelWriter<any>, deps: Partial<Deps>
23
23
  ): boolean {
24
24
 
25
25
  const didChange = writer.accumulator.storePixelBeforeState(x, y)
26
+ if (!didChange) return false
26
27
 
27
28
  return didChange(
28
29
  blendPixel(writer.config.target, x, y, color, alpha, blendFn),
@@ -25,6 +25,7 @@ export const mutatorBlendPixelData = ((writer: PixelWriter<any>, deps: Partial<D
25
25
  const h = opts?.h ?? src.h
26
26
 
27
27
  const didChange = writer.accumulator.storeRegionBeforeState(x, y, w, h)
28
+ if (!didChange) return false
28
29
 
29
30
  return didChange(
30
31
  blendPixelData(writer.config.target, src, opts),
@@ -18,7 +18,7 @@ export const mutatorClear = ((writer: PixelWriter<any>, deps: Deps = defaults) =
18
18
  return {
19
19
  clear(
20
20
  rect?: Partial<Rect>,
21
- ) {
21
+ ): boolean {
22
22
  const target = writer.config.target
23
23
  const x = rect?.x ?? 0
24
24
  const y = rect?.y ?? 0
@@ -26,6 +26,7 @@ export const mutatorClear = ((writer: PixelWriter<any>, deps: Deps = defaults) =
26
26
  const h = rect?.h ?? target.h
27
27
 
28
28
  const didChange = writer.accumulator.storeRegionBeforeState(x, y, w, h)
29
+ if (!didChange) return false
29
30
  return didChange(
30
31
  fillPixelData(target, 0 as Color32, x, y, w, h),
31
32
  )
@@ -14,44 +14,60 @@ export const mutatorFill = ((writer: PixelWriter<any>, deps: Deps = defaults) =>
14
14
  fillPixelData = defaults.fillPixelData,
15
15
  } = deps
16
16
 
17
- return {
18
- fill(
19
- color: Color32,
20
- x = 0,
21
- y = 0,
22
- w = writer.config.target.w,
23
- h = writer.config.target.h,
24
- ) {
25
- const target = writer.config.target
26
-
27
- const didChange = writer.accumulator.storeRegionBeforeState(x, y, w, h)
28
- return didChange(
29
- fillPixelData(target, color, x, y, w, h),
30
- )
31
- },
32
- }
33
- }) satisfies HistoryMutator<any, Deps>
17
+ const config = writer.config
34
18
 
35
- /**
36
- * @param deps - @hidden
37
- */
38
- export const mutatorFillRect = ((writer: PixelWriter<any>, deps: Deps = defaults) => {
39
- const {
40
- fillPixelData = defaults.fillPixelData,
41
- } = deps
19
+ function fill(
20
+ color: Color32,
21
+ rect?: Partial<Rect>,
22
+ ): boolean
23
+
24
+ function fill(
25
+ color: Color32,
26
+ x: number,
27
+ y: number,
28
+ w: number,
29
+ h: number,
30
+ ): boolean
31
+ function fill(
32
+ color: Color32,
33
+ _x?: Partial<Rect> | number,
34
+ _y?: number,
35
+ _w?: number,
36
+ _h?: number,
37
+ ): boolean {
38
+ const target = config.target
39
+
40
+ const dstW = target.w
41
+ const dstH = target.h
42
+
43
+ let x: number
44
+ let y: number
45
+ let w: number
46
+ let h: number
42
47
 
43
- return {
44
- fillRect(
45
- color: Color32,
46
- rect: Rect,
47
- ) {
48
- const target = writer.config.target
49
-
50
- const didChange = writer.accumulator.storeRegionBeforeState(rect.x, rect.y, rect.w, rect.h)
51
- return didChange(
52
- fillPixelData(target, color, rect.x, rect.y, rect.w, rect.h),
53
- )
54
- },
48
+ if (typeof _x === 'number') {
49
+ x = _x
50
+ y = _y!
51
+ w = _w!
52
+ h = _h!
53
+ } else if (typeof _x === 'object') {
54
+ x = _x.x ?? 0
55
+ y = _x.y ?? 0
56
+ w = _x.w ?? dstW
57
+ h = _x.h ?? dstH
58
+ } else {
59
+ x = 0
60
+ y = 0
61
+ w = dstW
62
+ h = dstH
63
+ }
64
+
65
+ const didChange = writer.accumulator.storeRegionBeforeState(x, y, w, h)
66
+ if (!didChange) return false
67
+ return didChange(
68
+ fillPixelData(target, color, x, y, w, h),
69
+ )
55
70
  }
56
- }) satisfies HistoryMutator<any, Deps>
57
71
 
72
+ return { fill }
73
+ }) satisfies HistoryMutator<any, Deps>
@@ -20,8 +20,9 @@ export const mutatorFillBinaryMask = ((writer: PixelWriter<any>, deps: Deps = de
20
20
  mask: BinaryMask,
21
21
  x = 0,
22
22
  y = 0,
23
- ) {
23
+ ): boolean {
24
24
  const didChange = writer.accumulator.storeRegionBeforeState(x, y, mask.w, mask.h)
25
+ if (!didChange) return false
25
26
  return didChange(
26
27
  fillPixelDataBinaryMask(writer.config.target, color, mask, x, y),
27
28
  )
@@ -14,7 +14,7 @@ export const mutatorInvert = ((writer: PixelWriter<any>, deps: Deps = defaults)
14
14
  } = deps
15
15
 
16
16
  return {
17
- invert(opts?: PixelMutateOptions) {
17
+ invert(opts?: PixelMutateOptions): boolean {
18
18
  const target = writer.config.target
19
19
  const x = opts?.x ?? 0
20
20
  const y = opts?.y ?? 0
@@ -22,8 +22,9 @@ export const mutatorInvert = ((writer: PixelWriter<any>, deps: Deps = defaults)
22
22
  const h = opts?.h ?? target.h
23
23
 
24
24
  const didChange = writer.accumulator.storeRegionBeforeState(x, y, w, h)
25
+ if (!didChange) return false
25
26
 
26
- return didChange(
27
+ return didChange?.(
27
28
  invertPixelData(target, opts),
28
29
  )
29
30
  },
@@ -12,7 +12,7 @@ import { mutatorBlendMask } from './PixelMutator/mutatorBlendMask'
12
12
  import { mutatorBlendPixel } from './PixelMutator/mutatorBlendPixel'
13
13
  import { mutatorBlendPixelData } from './PixelMutator/mutatorBlendPixelData'
14
14
  import { mutatorClear } from './PixelMutator/mutatorClear'
15
- import { mutatorFill, mutatorFillRect } from './PixelMutator/mutatorFill'
15
+ import { mutatorFill } from './PixelMutator/mutatorFill'
16
16
  import { mutatorFillBinaryMask } from './PixelMutator/mutatorFillBinaryMask'
17
17
  import { mutatorInvert } from './PixelMutator/mutatorInvert'
18
18
  import type { PixelWriter } from './PixelWriter'
@@ -36,7 +36,6 @@ export function makeFullPixelMutator(writer: PixelWriter<any>) {
36
36
  ...mutatorClear(writer),
37
37
  ...mutatorFill(writer),
38
38
  ...mutatorFillBinaryMask(writer),
39
- ...mutatorFillRect(writer),
40
39
  ...mutatorInvert(writer),
41
40
  }
42
41
  }
@@ -103,6 +103,14 @@ export function makePaintCursorRenderer<T extends HTMLCanvasElement | OffscreenC
103
103
  drawCtx.drawImage(canvas, Math.floor(dx), Math.floor(dy))
104
104
  }
105
105
 
106
+ function drawRaw(
107
+ drawCtx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D,
108
+ x: number,
109
+ y: number,
110
+ ) {
111
+ drawCtx.drawImage(canvas, Math.floor(x * _scale), Math.floor(y * _scale))
112
+ }
113
+
106
114
  function getSettings() {
107
115
  return {
108
116
  color: _color,
@@ -116,6 +124,7 @@ export function makePaintCursorRenderer<T extends HTMLCanvasElement | OffscreenC
116
124
  getBounds,
117
125
  getBoundsScaled: getOutlineBoundsScaled,
118
126
  draw,
127
+ drawRaw,
119
128
  getSettings,
120
129
  }
121
130
  }
@@ -15,3 +15,10 @@ export interface MutablePixelData32 {
15
15
  export interface PixelData<T extends ImageDataLike = ImageData> extends PixelData32 {
16
16
  readonly imageData: T
17
17
  }
18
+
19
+ export interface MutablePixelData<T extends ImageDataLike = ImageData> extends PixelData32 {
20
+ imageData: T
21
+ data: Uint32Array
22
+ w: number
23
+ h: number
24
+ }
@@ -0,0 +1,36 @@
1
+ import type { MutablePixelData, PixelData, PixelData32 } from './_pixelData-types'
2
+ import { setPixelData } from './PixelData'
3
+
4
+ export function cropPixelData(src: PixelData32, x: number, y: number, w: number, h: number, out?: MutablePixelData): PixelData {
5
+ const cx = Math.max(x, 0)
6
+ const cy = Math.max(y, 0)
7
+ const cw = Math.min(x + w, src.w) - cx
8
+ const ch = Math.min(y + h, src.h) - cy
9
+
10
+ if (cw <= 0 || ch <= 0) {
11
+ throw new Error(`Crop [${x},${y} ${w}x${h}] does not overlap PixelData [${src.w}x${src.h}]`)
12
+ }
13
+
14
+ const cropped = new ImageData(cw, ch)
15
+
16
+ let dst32: Uint32Array
17
+ if (out) {
18
+ setPixelData(out, cropped)
19
+ dst32 = out.data
20
+ } else {
21
+ dst32 = new Uint32Array(cropped.data.buffer)
22
+ }
23
+
24
+ for (let row = 0; row < ch; row++) {
25
+ const srcOffset = ((cy + row) * src.w) + cx
26
+ const dstOffset = row * cw
27
+ dst32.set(src.data.subarray(srcOffset, srcOffset + cw), dstOffset)
28
+ }
29
+
30
+ return out ?? {
31
+ data: dst32,
32
+ imageData: cropped,
33
+ w: cw,
34
+ h: ch,
35
+ }
36
+ }
@@ -47,16 +47,16 @@ export function fillPixelData(
47
47
  let w: number
48
48
  let h: number
49
49
 
50
- if (typeof _x === 'object') {
51
- x = _x.x ?? 0
52
- y = _x.y ?? 0
53
- w = _x.w ?? dstW
54
- h = _x.h ?? dstH
55
- } else if (typeof _x === 'number') {
50
+ if (typeof _x === 'number') {
56
51
  x = _x
57
52
  y = _y!
58
53
  w = _w!
59
54
  h = _h!
55
+ } else if (typeof _x === 'object') {
56
+ x = _x.x ?? 0
57
+ y = _x.y ?? 0
58
+ w = _x.w ?? dstW
59
+ h = _x.h ?? dstH
60
60
  } else {
61
61
  x = 0
62
62
  y = 0
@@ -0,0 +1,49 @@
1
+ import type { Rect } from '../Rect/_rect-types'
2
+ import type { MutablePixelData, PixelData, PixelData32 } from './_pixelData-types'
3
+ import { cropPixelData } from './cropPixelData'
4
+
5
+ export function getPixelDataTransparentTrimmedBounds(target: PixelData32): Rect | null {
6
+ let minX = target.w
7
+ let minY = target.h
8
+ let maxX = -1
9
+ let maxY = -1
10
+
11
+ for (let y = 0; y < target.h; y++) {
12
+ for (let x = 0; x < target.w; x++) {
13
+ const alpha = target.data[y * target.w + x] >>> 24
14
+ if (alpha !== 0) {
15
+ if (x < minX) minX = x
16
+ if (x > maxX) maxX = x
17
+ if (y < minY) minY = y
18
+ if (y > maxY) maxY = y
19
+ }
20
+ }
21
+ }
22
+
23
+ if (maxX === -1) return null
24
+
25
+ return {
26
+ x: minX,
27
+ y: minY,
28
+ w: maxX - minX + 1,
29
+ h: maxY - minY + 1,
30
+ }
31
+ }
32
+
33
+ export function trimTransparentPixelData(target: PixelData32): PixelData {
34
+ const r = getPixelDataTransparentTrimmedBounds(target)
35
+ if (!r) {
36
+ throw new Error('PixelData is fully transparent — no crop bounds found')
37
+ }
38
+
39
+ return cropPixelData(target, r.x, r.y, r.w, r.h)
40
+ }
41
+
42
+ export function trimTransparentPixelDataInPlace(target: MutablePixelData) {
43
+ const r = getPixelDataTransparentTrimmedBounds(target)
44
+ if (!r) {
45
+ throw new Error('PixelData is fully transparent — no crop bounds found')
46
+ }
47
+
48
+ cropPixelData(target, r.x, r.y, r.w, r.h, target)
49
+ }
package/src/index.ts CHANGED
@@ -152,6 +152,7 @@ export * from './PixelData/blendPixelDataMask'
152
152
  export * from './PixelData/blendPixelDataPaintBuffer'
153
153
  export * from './PixelData/clearPixelDataFast'
154
154
  export * from './PixelData/copyPixelData'
155
+ export * from './PixelData/cropPixelData'
155
156
  export * from './PixelData/extractPixelData'
156
157
  export * from './PixelData/extractPixelDataBuffer'
157
158
  export * from './PixelData/fillPixelData'
@@ -165,6 +166,7 @@ export * from './PixelData/resamplePixelData'
165
166
  export * from './PixelData/resizePixelData'
166
167
  export * from './PixelData/ReusablePixelData'
167
168
  export * from './PixelData/rotatePixelData'
169
+ export * from './PixelData/trimPixelData'
168
170
  export * from './PixelData/uInt32ArrayToPixelData'
169
171
  export * from './PixelData/writePaintBufferToPixelData'
170
172
  export * from './PixelData/writePixelData'