Bin
2025-12-16 9e0b2ba2c317b1a86212f24cbae3195ad1f3dbfa
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
export const BitmaskDrawing = {
  /**
   * Draws initial point on the canvas
   */
  begin({
    ctx,
    x,
    y,
    brushSize = 10,
    eraserMode = false,
  }: { ctx: CanvasRenderingContext2D; x: number; y: number; brushSize: number; color: string; eraserMode: boolean }): {
    x: number;
    y: number;
  } {
    ctx.fillStyle = eraserMode ? "white" : "black";
    ctx.globalCompositeOperation = eraserMode ? "destination-out" : "source-over";
 
    if (brushSize === 1) {
      ctx.fillRect(x, y, 1, 1);
    } else {
      ctx.beginPath();
      ctx.arc(x + 0.5, y + 0.5, brushSize, 0, Math.PI * 2);
      ctx.fill();
    }
    return { x, y };
  },
 
  /**
   * Draws a line between last and current position
   */
  draw({
    ctx,
    x,
    y,
    brushSize = 10,
    eraserMode = false,
    lastPos,
  }: {
    ctx: CanvasRenderingContext2D;
    x: number;
    y: number;
    brushSize: number;
    color: string;
    lastPos: { x: number; y: number };
    eraserMode: boolean;
  }): { x: number; y: number } {
    ctx.fillStyle = eraserMode ? "white" : "black";
    ctx.globalCompositeOperation = eraserMode ? "destination-out" : "source-over";
 
    this.drawLine(ctx, lastPos.x, lastPos.y, x, y, brushSize);
    return { x, y };
  },
 
  /**
   * Interpolation algorithm to connect separate
   * dots on the canvas
   */
  drawLine(ctx: CanvasRenderingContext2D, x0: number, y0: number, x1: number, y1: number, size: number) {
    const dx = Math.abs(x1 - x0);
    const dy = Math.abs(y1 - y0);
    const sx = x0 < x1 ? 1 : -1;
    const sy = y0 < y1 ? 1 : -1;
    let err = dx - dy;
 
    while (true) {
      if (size === 1) {
        ctx.fillRect(x0, y0, 1, 1);
      } else {
        ctx.beginPath();
        ctx.arc(x0 + 0.5, y0 + 0.5, size, 0, Math.PI * 2);
        ctx.fill();
      }
 
      if (x0 === x1 && y0 === y1) break;
      const e2 = 2 * err;
      if (e2 > -dy) {
        err -= dy;
        x0 += sx;
      }
      if (e2 < dx) {
        err += dx;
        y0 += sy;
      }
    }
  },
};
 
/**
 * Checks if the mouse pointer is hovering over a non-transparent pixel in a canvas-based image.
 * This function is used to determine if the user is interacting with a visible part of a bitmask region.
 *
 * @param item - An object containing references to the canvas layer, offscreen canvas, and image
 * @param item.layerRef - Reference to the Konva layer containing the canvas
 * @param item.offscreenCanvasRef - The offscreen canvas element containing the bitmask data
 * @param item.imageRef - Reference to the Konva image element
 * @param item.scale - Scale factor of the image
 * @returns {boolean} True if hovering over a non-transparent pixel, false otherwise
 */
export function isHoveringNonTransparentPixel(item: any): boolean {
  if (!item?.layerRef || !item?.offscreenCanvasRef || !item?.imageRef) {
    return false;
  }
 
  const stage = item.layerRef.getStage();
  const pointer = stage?.getPointerPosition();
  const ctx = item.offscreenCanvasRef.getContext("2d");
 
  if (!pointer || !ctx) return false;
 
  try {
    // Convert global pointer to image-local coordinates
    const transform = item.imageRef.getAbsoluteTransform().copy().invert();
    const localPos = transform.point(pointer);
 
    const { width, height } = item.offscreenCanvasRef;
 
    // Convert to pixel coords in the canvas backing the image
    const x = Math.floor(localPos.x / item.parent.stageZoom);
    const y = Math.floor(localPos.y / item.parent.stageZoom);
 
    if (x < 0 || y < 0 || x >= width || y >= height) return false;
 
    const alpha = ctx.getImageData(x, y, 1, 1).data[3];
    return alpha > 0;
  } catch (error) {
    console.warn("Error checking pixel transparency:", error);
    return false;
  }
}
 
/**
 * Calculates the bounding box of non-transparent pixels in a canvas.
 * This function scans the canvas pixel by pixel to find the minimum rectangle
 * that contains all visible (non-transparent) pixels.
 *
 * @param canvas - The HTML canvas element to analyze
 * @param scale - Scale factor to apply to the returned coordinates
 * @returns {Object|null} An object containing the bounds of non-transparent pixels:
 *   - left: Leftmost x-coordinate of visible pixels
 *   - top: Topmost y-coordinate of visible pixels
 *   - right: Rightmost x-coordinate of visible pixels (exclusive)
 *   - bottom: Bottommost y-coordinate of visible pixels (exclusive)
 *   Returns null if no visible pixels are found
 */
export function getCanvasPixelBounds(
  canvas: HTMLCanvasElement,
  scale: number,
): { left: number; top: number; right: number; bottom: number } | null {
  const ctx = canvas.getContext("2d");
  if (!ctx) return null;
 
  const { width, height } = canvas;
  const imageData = ctx.getImageData(0, 0, width, height);
  const data = imageData.data;
 
  let minX = width;
  let minY = height;
  let maxX = -1;
  let maxY = -1;
 
  for (let y = 0; y < height; y++) {
    for (let x = 0; x < width; x++) {
      const index = (y * width + x) * 4;
      const alpha = data[index + 3]; // alpha channel
      if (alpha > 0) {
        if (x < minX) minX = x;
        if (x > maxX) maxX = x;
        if (y < minY) minY = y;
        if (y > maxY) maxY = y;
      }
    }
  }
 
  const hasVisiblePixels = maxX >= minX && maxY >= minY;
  if (!hasVisiblePixels) return null;
 
  // Scale is applied to the points to compensate for
  // the image being different size than the stage
  return {
    left: minX * scale,
    top: minY * scale,
    right: (maxX + 1) * scale,
    bottom: (maxY + 1) * scale,
  };
}