react-img-cutout 1.0.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/README.md ADDED
@@ -0,0 +1,263 @@
1
+ # react-img-cutout
2
+
3
+ `react-img-cutout` provides a simple, composable component for creating interactive image regions.
4
+ It enables pixel-perfect interaction using transparent PNG cutouts, while also supporting standard bounding boxes and polygons for geometric shapes.
5
+
6
+
7
+
8
+ https://github.com/user-attachments/assets/4e4413b6-362a-4023-91f1-44452ea0c605
9
+
10
+
11
+
12
+ ## Quick Start
13
+
14
+ ### 1) Install
15
+
16
+ ```bash
17
+ npm install react-img-cutout
18
+ ```
19
+
20
+ Peer dependencies:
21
+ - `react >= 18`
22
+ - `react-dom >= 18`
23
+
24
+ ### 2) Render a viewer
25
+
26
+ ```tsx
27
+ import { CutoutViewer } from "react-img-cutout"
28
+
29
+ export function ProductHero() {
30
+ return (
31
+ <CutoutViewer
32
+ mainImage="/images/main.png"
33
+ mainImageAlt="Product scene"
34
+ effect="elevate"
35
+ onSelect={(id) => console.log("selected:", id)}
36
+ >
37
+ <CutoutViewer.Cutout
38
+ id="shoe"
39
+ src="/images/cutouts/shoe.png"
40
+ label="Shoe"
41
+ >
42
+ <CutoutViewer.Overlay placement="top-center">
43
+ <button>View details</button>
44
+ </CutoutViewer.Overlay>
45
+ </CutoutViewer.Cutout>
46
+
47
+ <CutoutViewer.Cutout
48
+ id="bag"
49
+ src="/images/cutouts/bag.png"
50
+ label="Bag"
51
+ />
52
+
53
+ {/* No image needed — define regions with coordinates */}
54
+ <CutoutViewer.BBoxCutout
55
+ id="logo"
56
+ bounds={{ x: 0.05, y: 0.05, w: 0.15, h: 0.1 }}
57
+ label="Logo"
58
+ />
59
+
60
+ <CutoutViewer.PolygonCutout
61
+ id="accent"
62
+ points={[[0.7, 0.2], [0.9, 0.2], [0.85, 0.5], [0.65, 0.45]]}
63
+ label="Accent"
64
+ />
65
+ </CutoutViewer>
66
+ )
67
+ }
68
+ ```
69
+
70
+ ## Cutout Types
71
+
72
+ The library supports three different cutout types, each suited for different use cases.
73
+
74
+ ### Image Cutout (`CutoutViewer.Cutout`)
75
+
76
+ The original cutout type — uses a transparent PNG aligned to the same coordinate
77
+ space as the main image. Hit-testing is performed per-pixel using the alpha channel.
78
+
79
+ ```tsx
80
+ <CutoutViewer.Cutout
81
+ id="shoe"
82
+ src="/images/cutouts/shoe.png"
83
+ label="Shoe"
84
+ >
85
+ <CutoutViewer.Overlay placement="top-center">
86
+ <button>View details</button>
87
+ </CutoutViewer.Overlay>
88
+ </CutoutViewer.Cutout>
89
+ ```
90
+
91
+ | Prop | Type | Description |
92
+ |------|------|-------------|
93
+ | `id` | `string` | Unique identifier for the cutout |
94
+ | `src` | `string` | URL of the cutout image (transparent PNG, same resolution as `mainImage`) |
95
+ | `label` | `string?` | Human-readable label (used as `alt` text) |
96
+ | `effect` | `HoverEffectPreset \| HoverEffect?` | Override the viewer-level hover effect for this cutout |
97
+ | `renderLayer` | `(props: RenderLayerProps) => ReactNode?` | Custom renderer replacing the default `<img>` |
98
+ | `children` | `ReactNode?` | Overlay content |
99
+
100
+ ### Bounding Box Cutout (`CutoutViewer.BBoxCutout`)
101
+
102
+ Defines a rectangular region using normalized 0–1 coordinates. No image
103
+ required — the component renders a styled rectangle. Ideal for highlighting
104
+ areas of the image programmatically (e.g. from object-detection output).
105
+
106
+ ```tsx
107
+ <CutoutViewer.BBoxCutout
108
+ id="face"
109
+ bounds={{ x: 0.3, y: 0.1, w: 0.2, h: 0.25 }}
110
+ label="Face"
111
+ >
112
+ <CutoutViewer.Overlay placement="top-center">
113
+ <span>Detected face</span>
114
+ </CutoutViewer.Overlay>
115
+ </CutoutViewer.BBoxCutout>
116
+ ```
117
+
118
+ | Prop | Type | Description |
119
+ |------|------|-------------|
120
+ | `id` | `string` | Unique identifier for the cutout |
121
+ | `bounds` | `{ x, y, w, h }` | Normalized 0–1 bounding box (`x` and `y` are the top-left corner) |
122
+ | `label` | `string?` | Human-readable label |
123
+ | `effect` | `HoverEffectPreset \| HoverEffect?` | Override the viewer-level hover effect for this cutout |
124
+ | `renderLayer` | `(props: RenderLayerProps) => ReactNode?` | Custom renderer replacing the default rectangle |
125
+ | `children` | `ReactNode?` | Overlay content |
126
+
127
+ ### Polygon Cutout (`CutoutViewer.PolygonCutout`)
128
+
129
+ Defines an arbitrary closed shape using an array of `[x, y]` normalized 0–1
130
+ points. Rendered as an SVG `<polygon>`. Great for non-rectangular regions such
131
+ as segmentation masks or hand-drawn annotations.
132
+
133
+ ```tsx
134
+ <CutoutViewer.PolygonCutout
135
+ id="lake"
136
+ points={[
137
+ [0.2, 0.6],
138
+ [0.5, 0.55],
139
+ [0.6, 0.7],
140
+ [0.35, 0.8],
141
+ ]}
142
+ label="Lake"
143
+ >
144
+ <CutoutViewer.Overlay placement="center">
145
+ <span>Lake area</span>
146
+ </CutoutViewer.Overlay>
147
+ </CutoutViewer.PolygonCutout>
148
+ ```
149
+
150
+ | Prop | Type | Description |
151
+ |------|------|-------------|
152
+ | `id` | `string` | Unique identifier for the cutout |
153
+ | `points` | `[number, number][]` | Array of normalized 0–1 `[x, y]` points forming a closed path |
154
+ | `label` | `string?` | Human-readable label |
155
+ | `effect` | `HoverEffectPreset \| HoverEffect?` | Override the viewer-level hover effect for this cutout |
156
+ | `renderLayer` | `(props: RenderLayerProps) => ReactNode?` | Custom renderer replacing the default SVG polygon |
157
+ | `children` | `ReactNode?` | Overlay content |
158
+
159
+ ## Public API
160
+
161
+ - `CutoutViewer`
162
+ - Props: `mainImage`, `mainImageAlt`, `effect`, `enabled`, `showAll`,
163
+ `alphaThreshold`, `hoverLeaveDelay`, `onHover`, `onActiveChange`, `onSelect`
164
+ - `CutoutViewer.Cutout` — image-based cutout (alpha hit-testing)
165
+ - `CutoutViewer.BBoxCutout` — bounding-box cutout (rectangular region)
166
+ - `CutoutViewer.PolygonCutout` — polygon cutout (arbitrary closed shape)
167
+ - `CutoutViewer.Overlay`
168
+ - Props: `placement`, `className`, `style`
169
+ - `useCutout()`
170
+ - Read nearest cutout state (`id`, `bounds`, `isActive`, `isHovered`, `isSelected`, `effect`)
171
+ - `hoverEffects`
172
+ - Built-in presets: `elevate`, `glow`, `lift`, `subtle`, `trace`, `shimmer`
173
+ - `defineKeyframes(name, css)` — helper for declaring CSS `@keyframes` in custom effects
174
+
175
+ ## How It Works
176
+
177
+ - **Image cutouts** are loaded into an offscreen canvas; opaque bounds and alpha
178
+ data are computed once for pixel-level hit-testing.
179
+ - **BBox cutouts** use simple point-in-rect checks against normalized coordinates.
180
+ - **Polygon cutouts** use a ray-casting algorithm for point-in-polygon testing.
181
+ - Pointer positions are normalized to the container and hit-tested from front to back.
182
+ - Click locks selection; clicking empty space clears selection.
183
+ - Overlays are positioned from normalized cutout bounds using one of 9 placements.
184
+
185
+ ## Important Implementation Notes
186
+
187
+ - Use transparent PNG/WebP cutouts aligned to the same coordinate space as `mainImage`.
188
+ - Best results come from matching cutout resolution to the base image resolution.
189
+ - BBox and Polygon cutouts do not require any image — they are defined purely by coordinates.
190
+ - Geometry cutouts (BBox/Polygon) are invisible when idle and appear on hover/selection,
191
+ unlike image cutouts which blend naturally with the background.
192
+ - If a cutout image cannot be read from canvas (for example, CORS restrictions),
193
+ hit testing for that cutout gracefully falls back and does not crash.
194
+ - The component does not require Tailwind to render correctly.
195
+
196
+ ## Effects
197
+
198
+ Pass a preset name or a fully custom `HoverEffect` object to the `effect` prop.
199
+
200
+ ### Built-in presets
201
+
202
+ | Preset | Description |
203
+ |--------|-------------|
204
+ | `elevate` | Lifts the hovered cutout with a blue glow and deep shadow |
205
+ | `glow` | Warm glow around the hovered cutout, no lift |
206
+ | `lift` | Strong lift with deep shadow, no color glow |
207
+ | `subtle` | Minimal — dims non-hovered cutouts with no animation |
208
+ | `trace` | A white dash continuously traces the cutout border |
209
+ | `shimmer` | style brightness flash that sweeps over the hovered subject |
210
+
211
+ ### Custom static effects
212
+
213
+ ```tsx
214
+ import { CutoutViewer, type HoverEffect } from "react-img-cutout"
215
+
216
+ const customEffect: HoverEffect = {
217
+ name: "neon",
218
+ transition: "all 0.4s ease",
219
+ mainImageHovered: { filter: "brightness(0.2) grayscale(1)" },
220
+ vignetteStyle: { background: "rgba(0,0,0,0.45)" },
221
+ cutoutActive: { transform: "scale(1.03)", opacity: 1 },
222
+ cutoutInactive: { transform: "scale(1)", opacity: 0.35 },
223
+ cutoutIdle: { transform: "scale(1)", opacity: 1 },
224
+ }
225
+ ```
226
+
227
+ ### Custom animated effects
228
+
229
+ Use `defineKeyframes` to declare CSS `@keyframes` that the viewer injects
230
+ automatically. Reference the keyframe `name` in any `animation` property.
231
+
232
+ ```tsx
233
+ import { defineKeyframes, type HoverEffect } from "react-img-cutout"
234
+
235
+ const pulse = defineKeyframes("my-pulse", `
236
+ 0%, 100% { transform: scale(1); filter: brightness(1); }
237
+ 50% { transform: scale(1.06); filter: brightness(1.15); }
238
+ `)
239
+
240
+ const pulseEffect: HoverEffect = {
241
+ name: "pulse",
242
+ transition: "all 0.4s ease",
243
+ keyframes: [pulse],
244
+ mainImageHovered: { filter: "brightness(0.3)" },
245
+ vignetteStyle: { background: "rgba(0,0,0,0.4)" },
246
+ cutoutActive: {
247
+ animation: `${pulse.name} 1.2s ease-in-out infinite`,
248
+ opacity: 1,
249
+ },
250
+ cutoutInactive: { opacity: 0.3 },
251
+ cutoutIdle: { opacity: 1 },
252
+ }
253
+ ```
254
+
255
+ Geometry cutouts (BBox / Polygon) also support `strokeDasharray` and
256
+ `animation` on their `geometryActive` style for SVG-level animations
257
+ like the built-in trace effect.
258
+
259
+ ## Local Development
260
+
261
+ - `npm run storybook` for interactive component testing
262
+ - `npm run lint` for lint checks
263
+ - `npm run build:lib` to generate `dist/` for npm
@@ -0,0 +1,45 @@
1
+ import { type ReactNode, type ReactElement, type CSSProperties } from "react";
2
+ import { type HoverEffectPreset, type HoverEffect } from "./hover-effects";
3
+ import { Cutout } from "./cutouts/image/cutout";
4
+ import { BBoxCutout } from "./cutouts/bbox/bbox-cutout";
5
+ import { PolygonCutout } from "./cutouts/polygon/polygon-cutout";
6
+ import { CutoutOverlay } from "./cutouts/cutout-overlay";
7
+ export interface CutoutViewerProps {
8
+ /** URL of the main background image */
9
+ mainImage: string;
10
+ /** Accessible alt text for the main image */
11
+ mainImageAlt?: string;
12
+ /** Hover effect preset name or a custom HoverEffect object */
13
+ effect?: HoverEffectPreset | HoverEffect;
14
+ /** Whether the hover interaction is enabled (default: true) */
15
+ enabled?: boolean;
16
+ /** When true, all cutouts show their active/hovered state simultaneously */
17
+ showAll?: boolean;
18
+ /** Minimum alpha value 0-255 for pixel hit-testing (default: 30) */
19
+ alphaThreshold?: number;
20
+ /** Delay in ms before the hover state clears after leaving a cutout (default: 150) */
21
+ hoverLeaveDelay?: number;
22
+ /**
23
+ * Composable children — use `<CutoutViewer.Cutout>` to declare cutout layers.
24
+ * Other elements are rendered on top of the viewer.
25
+ */
26
+ children?: ReactNode;
27
+ /** Additional className on the root container */
28
+ className?: string;
29
+ /** Additional inline style on the root container */
30
+ style?: CSSProperties;
31
+ /** Callback when a cutout is hovered (not selected) */
32
+ onHover?: (cutoutId: string | null) => void;
33
+ /** Callback when a cutout becomes active (hovered or selected) */
34
+ onActiveChange?: (cutoutId: string | null) => void;
35
+ /** Callback when a cutout is clicked / selected */
36
+ onSelect?: (cutoutId: string | null) => void;
37
+ }
38
+ type CutoutViewerComponent = ((props: CutoutViewerProps) => ReactElement) & {
39
+ Cutout: typeof Cutout;
40
+ BBoxCutout: typeof BBoxCutout;
41
+ PolygonCutout: typeof PolygonCutout;
42
+ Overlay: typeof CutoutOverlay;
43
+ };
44
+ export declare const CutoutViewer: CutoutViewerComponent;
45
+ export {};
@@ -0,0 +1,23 @@
1
+ import { type ReactNode } from "react";
2
+ import { type HoverEffect, type HoverEffectPreset } from "../../hover-effects";
3
+ import type { RenderLayerProps } from "../image/cutout";
4
+ export interface BBoxCutoutProps {
5
+ /** Unique identifier for this cutout */
6
+ id: string;
7
+ /** Normalized 0-1 bounding box coordinates */
8
+ bounds: {
9
+ x: number;
10
+ y: number;
11
+ w: number;
12
+ h: number;
13
+ };
14
+ /** Human-readable label */
15
+ label?: string;
16
+ /** Override the viewer-level hover effect for this specific cutout */
17
+ effect?: HoverEffectPreset | HoverEffect;
18
+ /** Children rendered inside this cutout's context (e.g. `<Overlay>`) */
19
+ children?: ReactNode;
20
+ /** Custom renderer for the cutout layer. When provided, replaces the default rendering. */
21
+ renderLayer?: (props: RenderLayerProps) => ReactNode;
22
+ }
23
+ export declare function BBoxCutout({ id, bounds: defBounds, label, effect: effectOverride, children, renderLayer, }: BBoxCutoutProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,7 @@
1
+ import type { CutoutBounds, HitTestStrategy, BoundingBoxCutoutDefinition } from "../../hit-test-strategy";
2
+ export declare class RectHitTestStrategy implements HitTestStrategy {
3
+ id: string;
4
+ bounds: CutoutBounds;
5
+ constructor(def: BoundingBoxCutoutDefinition);
6
+ hitTest(nx: number, ny: number): boolean;
7
+ }
@@ -0,0 +1,17 @@
1
+ import type { CutoutBounds } from "../hit-test-strategy";
2
+ import type { HoverEffect } from "../hover-effects";
3
+ export interface CutoutContextValue {
4
+ id: string;
5
+ label?: string;
6
+ bounds: CutoutBounds;
7
+ isActive: boolean;
8
+ isHovered: boolean;
9
+ isSelected: boolean;
10
+ effect: HoverEffect;
11
+ }
12
+ export declare const CutoutContext: import("react").Context<CutoutContextValue | null>;
13
+ /**
14
+ * Access the state of the nearest parent `<CutoutViewer.Cutout>`.
15
+ * Must be used inside a `<CutoutViewer.Cutout>`.
16
+ */
17
+ export declare function useCutout(): CutoutContextValue;
@@ -0,0 +1,28 @@
1
+ import { type ReactNode, type CSSProperties } from "react";
2
+ export type Placement = "top-left" | "top-center" | "top-right" | "center-left" | "center" | "center-right" | "bottom-left" | "bottom-center" | "bottom-right";
3
+ export interface CutoutOverlayProps {
4
+ /**
5
+ * Where to position the overlay relative to the cutout's bounding box.
6
+ * @default "top-center"
7
+ */
8
+ placement?: Placement;
9
+ /** Content to render inside the overlay */
10
+ children: ReactNode;
11
+ /** Additional className */
12
+ className?: string;
13
+ /** Additional inline styles (merged after placement styles) */
14
+ style?: CSSProperties;
15
+ }
16
+ /**
17
+ * Renders custom UI positioned relative to the parent `<CutoutViewer.Cutout>`'s
18
+ * opaque bounding box. The overlay is visible when its cutout is active (hovered
19
+ * or selected), or always visible in `showAll` mode.
20
+ *
21
+ * @example
22
+ * <CutoutViewer.Cutout id="face" src="/face.png" label="Face">
23
+ * <CutoutViewer.Overlay placement="top-center">
24
+ * <button>View Profile</button>
25
+ * </CutoutViewer.Overlay>
26
+ * </CutoutViewer.Cutout>
27
+ */
28
+ export declare function CutoutOverlay({ placement, children, className, style, }: CutoutOverlayProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,25 @@
1
+ import { type ReactNode } from "react";
2
+ import type { CutoutBounds } from "../../hit-test-strategy";
3
+ import { type HoverEffect, type HoverEffectPreset } from "../../hover-effects";
4
+ export interface RenderLayerProps {
5
+ isActive: boolean;
6
+ isHovered: boolean;
7
+ isSelected: boolean;
8
+ bounds: CutoutBounds;
9
+ effect: HoverEffect;
10
+ }
11
+ export interface CutoutProps {
12
+ /** Unique identifier for this cutout */
13
+ id: string;
14
+ /** URL of the cutout image (transparent PNG, same resolution as mainImage) */
15
+ src: string;
16
+ /** Human-readable label */
17
+ label?: string;
18
+ /** Override the viewer-level hover effect for this specific cutout */
19
+ effect?: HoverEffectPreset | HoverEffect;
20
+ /** Children rendered inside this cutout's context (e.g. `<Overlay>`) */
21
+ children?: ReactNode;
22
+ /** Custom renderer for the cutout layer. When provided, replaces the default `<img>` rendering. */
23
+ renderLayer?: (props: RenderLayerProps) => ReactNode;
24
+ }
25
+ export declare function Cutout({ id, src, label, effect: effectOverride, children, renderLayer }: CutoutProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,61 @@
1
+ import type { CutoutBounds, HitTestStrategy, ImageCutoutDefinition } from "../../hit-test-strategy";
2
+ /**
3
+ * Hit-test strategy for image-based (alpha mask) cutouts.
4
+ *
5
+ * Instead of using geometric shapes, this strategy detects whether the cursor
6
+ * is over a **visible (non-transparent) pixel** in a cutout image. This is
7
+ * useful for irregularly shaped cutouts where polygons would be too complex.
8
+ *
9
+ * Overall flow:
10
+ * 1. **prepare()** — loads the cutout image, draws it onto an offscreen canvas,
11
+ * reads the pixel data, and extracts just the alpha channel into a compact
12
+ * buffer. It also computes a tight bounding box around the visible pixels.
13
+ * 2. **hitTest(nx, ny)** — given a normalized mouse position (0-1), first does
14
+ * a cheap bounding-box check, then looks up the exact pixel in the alpha
15
+ * buffer to decide if the point is over a visible part of the image.
16
+ */
17
+ export declare class ImageHitTestStrategy implements HitTestStrategy {
18
+ id: string;
19
+ bounds: CutoutBounds;
20
+ /** URL of the cutout mask image */
21
+ private src;
22
+ /** Alpha value (0-255) a pixel must exceed to be considered "visible" */
23
+ private threshold;
24
+ /** Pre-extracted alpha channel — one byte per pixel, for fast lookups */
25
+ private alpha;
26
+ /** Source image dimensions (pixels) — needed to map normalized coords to pixel indices */
27
+ private width;
28
+ private height;
29
+ constructor(def: ImageCutoutDefinition, threshold: number);
30
+ /**
31
+ * Loads the cutout image and pre-computes the alpha buffer + bounding box.
32
+ *
33
+ * Steps:
34
+ * 1. Create an <img> element and wait for it to load.
35
+ * 2. Draw the image onto a temporary offscreen <canvas>.
36
+ * 3. Read the raw RGBA pixel data from the canvas.
37
+ * 4. Extract only the alpha channel into a compact buffer (see `extractAlpha`).
38
+ * 5. Compute the tight bounding box of visible pixels (see `computeBoundsFromAlpha`).
39
+ *
40
+ * If the canvas is CORS-tainted (image from a different origin without proper
41
+ * headers), reading pixel data will throw. In that case we fall back to an
42
+ * empty alpha buffer, which means hitTest will always return false.
43
+ */
44
+ prepare(): Promise<void>;
45
+ /**
46
+ * Tests whether the normalized point (nx, ny) is over a visible pixel.
47
+ *
48
+ * Three-phase approach:
49
+ * 1. **Alpha buffer check** — if the buffer is empty (image failed to load or
50
+ * was CORS-tainted), return false immediately.
51
+ * 2. **Bounding-box check** — reject points outside the pre-computed AABB of
52
+ * visible pixels (very cheap).
53
+ * 3. **Per-pixel alpha lookup** — convert the normalized coordinates to pixel
54
+ * indices, look up the alpha value in the pre-extracted buffer, and compare
55
+ * it against the threshold.
56
+ *
57
+ * @param nx - normalized x-coordinate (0-1, relative to the image width)
58
+ * @param ny - normalized y-coordinate (0-1, relative to the image height)
59
+ */
60
+ hitTest(nx: number, ny: number): boolean;
61
+ }
@@ -0,0 +1,18 @@
1
+ import { type ReactNode } from "react";
2
+ import { type HoverEffect, type HoverEffectPreset } from "../../hover-effects";
3
+ import type { RenderLayerProps } from "../image/cutout";
4
+ export interface PolygonCutoutProps {
5
+ /** Unique identifier for this cutout */
6
+ id: string;
7
+ /** Array of [x, y] normalized 0-1 points forming a closed path */
8
+ points: [number, number][];
9
+ /** Human-readable label */
10
+ label?: string;
11
+ /** Override the viewer-level hover effect for this specific cutout */
12
+ effect?: HoverEffectPreset | HoverEffect;
13
+ /** Children rendered inside this cutout's context (e.g. `<Overlay>`) */
14
+ children?: ReactNode;
15
+ /** Custom renderer for the cutout layer. When provided, replaces the default SVG rendering. */
16
+ renderLayer?: (props: RenderLayerProps) => ReactNode;
17
+ }
18
+ export declare function PolygonCutout({ id, points: defPoints, label, effect: effectOverride, children, renderLayer, }: PolygonCutoutProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,28 @@
1
+ import type { CutoutBounds, HitTestStrategy, PolygonCutoutDefinition } from "../../hit-test-strategy";
2
+ /**
3
+ * Hit-test strategy for polygon-shaped cutouts.
4
+ *
5
+ * On construction it pre-computes an axis-aligned bounding box (AABB) from the
6
+ * polygon vertices. During hit testing it first checks the cheap AABB test to
7
+ * quickly reject points that are clearly outside, then falls back to the more
8
+ * expensive ray-casting test only when needed.
9
+ */
10
+ export declare class PolygonHitTestStrategy implements HitTestStrategy {
11
+ id: string;
12
+ bounds: CutoutBounds;
13
+ private points;
14
+ constructor(def: PolygonCutoutDefinition);
15
+ /**
16
+ * Tests whether the normalized point (nx, ny) is inside this polygon.
17
+ *
18
+ * Two-phase approach for performance:
19
+ * 1. **Bounding-box check** — if the point is outside the AABB, return false
20
+ * immediately (very cheap).
21
+ * 2. **Ray-cast check** — run the full point-in-polygon algorithm for precise
22
+ * hit detection only if the point passed the bounding-box check.
23
+ *
24
+ * @param nx - normalized x-coordinate (0-1, relative to the image width)
25
+ * @param ny - normalized y-coordinate (0-1, relative to the image height)
26
+ */
27
+ hitTest(nx: number, ny: number): boolean;
28
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Normalized bounding box (0-1 range) of the opaque pixels in a cutout.
3
+ * Values represent fractions of the image dimensions.
4
+ */
5
+ export interface CutoutBounds {
6
+ /** Left edge as a fraction of image width (0-1) */
7
+ x: number;
8
+ /** Top edge as a fraction of image height (0-1) */
9
+ y: number;
10
+ /** Width as a fraction of image width (0-1) */
11
+ w: number;
12
+ /** Height as a fraction of image height (0-1) */
13
+ h: number;
14
+ }
15
+ interface BaseCutoutDefinition {
16
+ id: string;
17
+ label?: string;
18
+ }
19
+ export interface ImageCutoutDefinition extends BaseCutoutDefinition {
20
+ type: "image";
21
+ /** Transparent PNG, same resolution as the main image */
22
+ src: string;
23
+ }
24
+ export interface BoundingBoxCutoutDefinition extends BaseCutoutDefinition {
25
+ type: "bbox";
26
+ /** Normalized 0-1 coordinates */
27
+ bounds: {
28
+ x: number;
29
+ y: number;
30
+ w: number;
31
+ h: number;
32
+ };
33
+ }
34
+ export interface PolygonCutoutDefinition extends BaseCutoutDefinition {
35
+ type: "polygon";
36
+ /** Array of [x, y] normalized points forming a closed path */
37
+ points: [number, number][];
38
+ }
39
+ export type CutoutDefinition = ImageCutoutDefinition | BoundingBoxCutoutDefinition | PolygonCutoutDefinition;
40
+ export interface HitTestStrategy {
41
+ /** Cutout identifier */
42
+ id: string;
43
+ /** Returns true if the normalized point (nx, ny) in [0,1] is inside this cutout */
44
+ hitTest(nx: number, ny: number): boolean;
45
+ /** Pre-computed bounding box (normalized 0-1) */
46
+ bounds: CutoutBounds;
47
+ /** Optional async setup (e.g., image loading) */
48
+ prepare?(): Promise<void>;
49
+ /** Cleanup */
50
+ dispose?(): void;
51
+ }
52
+ export { ImageHitTestStrategy } from "./cutouts/image/image-hit-test-strategy";
53
+ export { RectHitTestStrategy } from "./cutouts/bbox/bbox-hit-test-strategy";
54
+ export { PolygonHitTestStrategy } from "./cutouts/polygon/polygon-hit-test-strategy";
55
+ export declare function createHitTestStrategy(def: CutoutDefinition, alphaThreshold: number): HitTestStrategy;