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 +263 -0
- package/dist/components/cutout-viewer/cutout-viewer.d.ts +45 -0
- package/dist/components/cutout-viewer/cutouts/bbox/bbox-cutout.d.ts +23 -0
- package/dist/components/cutout-viewer/cutouts/bbox/bbox-hit-test-strategy.d.ts +7 -0
- package/dist/components/cutout-viewer/cutouts/cutout-context.d.ts +17 -0
- package/dist/components/cutout-viewer/cutouts/cutout-overlay.d.ts +28 -0
- package/dist/components/cutout-viewer/cutouts/image/cutout.d.ts +25 -0
- package/dist/components/cutout-viewer/cutouts/image/image-hit-test-strategy.d.ts +61 -0
- package/dist/components/cutout-viewer/cutouts/polygon/polygon-cutout.d.ts +18 -0
- package/dist/components/cutout-viewer/cutouts/polygon/polygon-hit-test-strategy.d.ts +28 -0
- package/dist/components/cutout-viewer/hit-test-strategy.d.ts +55 -0
- package/dist/components/cutout-viewer/hover-effects.d.ts +192 -0
- package/dist/components/cutout-viewer/index.d.ts +14 -0
- package/dist/components/cutout-viewer/use-cutout-hit-test.d.ts +29 -0
- package/dist/components/cutout-viewer/viewer-context.d.ts +19 -0
- package/dist/demo-images/main.png +0 -0
- package/dist/demo-images/man.png +0 -0
- package/dist/demo-images/woman.png +0 -0
- package/dist/index.cjs +27 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1245 -0
- package/package.json +81 -0
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;
|