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.
@@ -0,0 +1,192 @@
1
+ import type { CSSProperties } from "react";
2
+ /**
3
+ * Describes a CSS `@keyframes` rule that an effect needs at runtime.
4
+ *
5
+ * Use the {@link defineKeyframes} helper to create instances.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * const pulse = defineKeyframes("my-pulse", `
10
+ * 0%, 100% { opacity: 1; }
11
+ * 50% { opacity: 0.5; }
12
+ * `)
13
+ * ```
14
+ */
15
+ export interface KeyframeAnimation {
16
+ /** Unique name for the `@keyframes` rule (used in CSS `animation-name`). */
17
+ name: string;
18
+ /** The raw CSS content placed inside `@keyframes <name> { … }`. */
19
+ css: string;
20
+ }
21
+ /**
22
+ * Helper to define a {@link KeyframeAnimation} for use in custom hover effects.
23
+ *
24
+ * @param name A unique animation name. Prefix with your library/app name to
25
+ * avoid collisions (e.g. `"myapp-pulse"`).
26
+ * @param css The CSS keyframe stops — the content that goes inside the
27
+ * `@keyframes` braces.
28
+ *
29
+ * @example
30
+ * ```ts
31
+ * const pulse = defineKeyframes("myapp-pulse", `
32
+ * 0%, 100% { transform: scale(1); }
33
+ * 50% { transform: scale(1.08); }
34
+ * `)
35
+ *
36
+ * const myEffect: HoverEffect = {
37
+ * name: "pulse",
38
+ * keyframes: [pulse],
39
+ * cutoutActive: {
40
+ * animation: `${pulse.name} 1.2s ease-in-out infinite`,
41
+ * // ...
42
+ * },
43
+ * // ...
44
+ * }
45
+ * ```
46
+ */
47
+ export declare function defineKeyframes(name: string, css: string): KeyframeAnimation;
48
+ /**
49
+ * Lazily inject the `@keyframes` rules declared by a {@link HoverEffect}
50
+ * into the document `<head>`. Each keyframe name is injected at most once
51
+ * regardless of how many times this function is called.
52
+ *
53
+ * Called automatically by `<CutoutViewer>` whenever the active effect
54
+ * changes, so consumers never need to call this manually.
55
+ * @internal
56
+ */
57
+ export declare function ensureEffectKeyframes(effect: HoverEffect): void;
58
+ /**
59
+ * Visual style for geometry-based cutouts (bbox, polygon).
60
+ * These are applied directly to the inner shape element rather than the
61
+ * wrapper div, because CSS `filter: drop-shadow()` on the wrapper doesn't
62
+ * produce good results for geometric shapes the way it does for images.
63
+ */
64
+ export interface GeometryStyle {
65
+ /** Fill color (CSS color string) */
66
+ fill: string;
67
+ /** Stroke / border color (CSS color string) */
68
+ stroke: string;
69
+ /** Stroke width in px for bbox borders, or normalized units for polygon SVG */
70
+ strokeWidth?: number;
71
+ /** Box-shadow string applied to bbox, or SVG filter glow for polygon */
72
+ glow?: string;
73
+ /**
74
+ * SVG `stroke-dasharray` value. Creates a dashed stroke pattern.
75
+ * Values are relative to `pathLength` (normalised to 1), so
76
+ * `"0.15 0.85"` = 15 % visible dash, 85 % gap.
77
+ */
78
+ strokeDasharray?: string;
79
+ /**
80
+ * CSS `animation` shorthand applied to the inner SVG shape element.
81
+ * Pair with a {@link KeyframeAnimation} declared in the parent
82
+ * effect's `keyframes` array for automatic injection.
83
+ */
84
+ animation?: string;
85
+ }
86
+ /**
87
+ * A hover effect preset defines how the main image, the hovered cutout,
88
+ * and the non-hovered cutouts should look during a hover interaction.
89
+ *
90
+ * The `cutout*` styles are applied to the wrapper div and work best with
91
+ * image-based cutouts (transparent PNGs). For geometry-based cutouts
92
+ * (bbox, polygon), the optional `geometry*` styles control the inner
93
+ * shape's fill, stroke, and glow independently.
94
+ *
95
+ * For effects that use CSS animations, declare the required `@keyframes`
96
+ * in the {@link keyframes} array and reference their names in the
97
+ * `animation` property of the relevant `cutout*` style. The viewer
98
+ * automatically injects them into `<head>` at runtime.
99
+ *
100
+ * @example
101
+ * ```ts
102
+ * import { defineKeyframes, type HoverEffect } from "react-img-cutout"
103
+ *
104
+ * const pulse = defineKeyframes("my-pulse", `
105
+ * 0%, 100% { transform: scale(1); filter: brightness(1); }
106
+ * 50% { transform: scale(1.05); filter: brightness(1.15); }
107
+ * `)
108
+ *
109
+ * export const pulseEffect: HoverEffect = {
110
+ * name: "pulse",
111
+ * keyframes: [pulse],
112
+ * transition: "all 0.4s ease",
113
+ * cutoutActive: {
114
+ * animation: \`\${pulse.name} 1.2s ease-in-out infinite\`,
115
+ * opacity: 1,
116
+ * },
117
+ * // … remaining style fields
118
+ * }
119
+ * ```
120
+ */
121
+ export interface HoverEffect {
122
+ /** Label for display / debugging */
123
+ name: string;
124
+ /** Transition CSS applied to all animated elements */
125
+ transition: string;
126
+ /**
127
+ * CSS `@keyframes` rules this effect requires at runtime.
128
+ * The viewer injects them into `<head>` automatically — consumers
129
+ * only need to declare them here and reference the animation `name`
130
+ * in the relevant `cutout*` / `geometry*` style properties.
131
+ */
132
+ keyframes?: KeyframeAnimation[];
133
+ /** Styles applied to the main background image when any cutout is hovered */
134
+ mainImageHovered: CSSProperties;
135
+ /** Styles for the vignette overlay when active */
136
+ vignetteStyle: CSSProperties;
137
+ /** Styles applied to the actively-hovered cutout layer */
138
+ cutoutActive: CSSProperties;
139
+ /** Styles applied to cutout layers that are NOT hovered while another is */
140
+ cutoutInactive: CSSProperties;
141
+ /** Default cutout style when nothing is hovered */
142
+ cutoutIdle: CSSProperties;
143
+ /** Styles for geometry-based cutout shapes when active (hovered/selected) */
144
+ geometryActive?: GeometryStyle;
145
+ /** Styles for geometry-based cutout shapes when another cutout is active */
146
+ geometryInactive?: GeometryStyle;
147
+ /** Styles for geometry-based cutout shapes in idle state (nothing hovered) */
148
+ geometryIdle?: GeometryStyle;
149
+ }
150
+ /**
151
+ * Elevate effect — the hovered item lifts, glows blue, background dims and desaturates.
152
+ * Inspired by Visual Look Up interactions.
153
+ */
154
+ export declare const elevateEffect: HoverEffect;
155
+ /**
156
+ * Subtle glow-only effect. No lift, just a warm glow around the hovered item.
157
+ */
158
+ export declare const glowEffect: HoverEffect;
159
+ /**
160
+ * Strong lift with deep shadow, no color glow.
161
+ */
162
+ export declare const liftEffect: HoverEffect;
163
+ /**
164
+ * No animation - just shows which cutout the cursor is over via subtle dimming.
165
+ */
166
+ export declare const subtleEffect: HoverEffect;
167
+ /**
168
+ * Trace effect — a short white dash endlessly travels around the cutout
169
+ * border, tracing its outline. For image-based cutouts a white drop-shadow
170
+ * highlight sweeps around the silhouette edge.
171
+ */
172
+ export declare const traceEffect: HoverEffect;
173
+ /**
174
+ * Shimmer effect — the hovered cutout lifts and periodically flashes
175
+ * brighter, as though light is sweeping across its surface. Inspired by
176
+ * the Apple "lift subject to create sticker" interaction.
177
+ *
178
+ * The brightness pulse occupies roughly the first third of each cycle,
179
+ * leaving the remaining time at the resting state so the glint feels
180
+ * natural rather than strobing.
181
+ */
182
+ export declare const shimmerEffect: HoverEffect;
183
+ /** Built-in preset map for convenience */
184
+ export declare const hoverEffects: {
185
+ readonly elevate: HoverEffect;
186
+ readonly glow: HoverEffect;
187
+ readonly lift: HoverEffect;
188
+ readonly subtle: HoverEffect;
189
+ readonly trace: HoverEffect;
190
+ readonly shimmer: HoverEffect;
191
+ };
192
+ export type HoverEffectPreset = keyof typeof hoverEffects;
@@ -0,0 +1,14 @@
1
+ export { CutoutViewer } from "./cutout-viewer";
2
+ export type { CutoutViewerProps } from "./cutout-viewer";
3
+ export { CutoutOverlay } from "./cutouts/cutout-overlay";
4
+ export type { CutoutOverlayProps, Placement } from "./cutouts/cutout-overlay";
5
+ export type { CutoutProps, RenderLayerProps } from "./cutouts/image/cutout";
6
+ export type { BBoxCutoutProps } from "./cutouts/bbox/bbox-cutout";
7
+ export type { PolygonCutoutProps } from "./cutouts/polygon/polygon-cutout";
8
+ export { useCutout } from "./cutouts/cutout-context";
9
+ export { useCutoutHitTest } from "./use-cutout-hit-test";
10
+ export type { CutoutImage, CutoutBounds } from "./use-cutout-hit-test";
11
+ export type { CutoutDefinition, ImageCutoutDefinition, BoundingBoxCutoutDefinition, PolygonCutoutDefinition, HitTestStrategy, } from "./hit-test-strategy";
12
+ export { ImageHitTestStrategy, RectHitTestStrategy, PolygonHitTestStrategy, createHitTestStrategy, } from "./hit-test-strategy";
13
+ export { hoverEffects, elevateEffect, glowEffect, liftEffect, subtleEffect, traceEffect, shimmerEffect, defineKeyframes, } from "./hover-effects";
14
+ export type { HoverEffect, HoverEffectPreset, GeometryStyle, KeyframeAnimation, } from "./hover-effects";
@@ -0,0 +1,29 @@
1
+ import { type CutoutDefinition, type CutoutBounds } from "./hit-test-strategy";
2
+ export type { CutoutBounds } from "./hit-test-strategy";
3
+ export type { CutoutDefinition, ImageCutoutDefinition, BoundingBoxCutoutDefinition, PolygonCutoutDefinition, HitTestStrategy, } from "./hit-test-strategy";
4
+ /** @deprecated Use `ImageCutoutDefinition` instead */
5
+ export interface CutoutImage {
6
+ id: string;
7
+ src: string;
8
+ label?: string;
9
+ }
10
+ /**
11
+ * Hook that performs hit-testing on a stack of cutout definitions using
12
+ * pluggable strategies. Returns the currently-hovered/selected cutout id,
13
+ * computed bounding boxes, and pointer handlers for the container.
14
+ *
15
+ * Supports click-to-lock: clicking a cutout "selects" it, holding the active
16
+ * state until the user clicks away from any cutout.
17
+ */
18
+ export declare function useCutoutHitTest(definitions: CutoutDefinition[], enabled?: boolean, alphaThreshold?: number, hoverLeaveDelay?: number): {
19
+ hoveredId: string | null;
20
+ selectedId: string | null;
21
+ activeId: string | null;
22
+ boundsMap: Record<string, CutoutBounds>;
23
+ containerRef: import("react").RefObject<HTMLDivElement | null>;
24
+ containerProps: {
25
+ onPointerMove: (e: React.PointerEvent<HTMLDivElement>) => void;
26
+ onPointerLeave: () => void;
27
+ onClick: (e: React.PointerEvent<HTMLDivElement>) => void;
28
+ };
29
+ };
@@ -0,0 +1,19 @@
1
+ import type { CutoutDefinition, CutoutBounds } from "./hit-test-strategy";
2
+ import type { HoverEffect } from "./hover-effects";
3
+ export interface CutoutRegistryContextValue {
4
+ registerCutout: (def: CutoutDefinition) => void;
5
+ unregisterCutout: (id: string) => void;
6
+ }
7
+ export declare const CutoutRegistryContext: import("react").Context<CutoutRegistryContextValue | null>;
8
+ export interface CutoutViewerContextValue {
9
+ activeId: string | null;
10
+ selectedId: string | null;
11
+ hoveredId: string | null;
12
+ effect: HoverEffect;
13
+ enabled: boolean;
14
+ showAll: boolean;
15
+ boundsMap: Record<string, CutoutBounds>;
16
+ isAnyActive: boolean;
17
+ }
18
+ export declare const CutoutViewerContext: import("react").Context<CutoutViewerContextValue | null>;
19
+ export declare function useCutoutViewerContext(): CutoutViewerContextValue;
Binary file
Binary file
Binary file
package/dist/index.cjs ADDED
@@ -0,0 +1,27 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const l=require("react");var J={exports:{}},z={};var nt;function _t(){if(nt)return z;nt=1;var t=Symbol.for("react.transitional.element"),r=Symbol.for("react.fragment");function o(n,a,u){var i=null;if(u!==void 0&&(i=""+u),a.key!==void 0&&(i=""+a.key),"key"in a){u={};for(var s in a)s!=="key"&&(u[s]=a[s])}else u=a;return a=u.ref,{$$typeof:t,type:n,key:i,ref:a!==void 0?a:null,props:u}}return z.Fragment=r,z.jsx=o,z.jsxs=o,z}var L={};var st;function St(){return st||(st=1,process.env.NODE_ENV!=="production"&&(function(){function t(e){if(e==null)return null;if(typeof e=="function")return e.$$typeof===v?null:e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case j:return"Fragment";case b:return"Profiler";case T:return"StrictMode";case O:return"Suspense";case Y:return"SuspenseList";case E:return"Activity"}if(typeof e=="object")switch(typeof e.tag=="number"&&console.error("Received an unexpected object in getComponentNameFromType(). This is likely a bug in React. Please file an issue."),e.$$typeof){case k:return"Portal";case M:return e.displayName||"Context";case $:return(e._context.displayName||"Context")+".Consumer";case y:var f=e.render;return e=e.displayName,e||(e=f.displayName||f.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case V:return f=e.displayName||null,f!==null?f:t(e.type)||"Memo";case C:f=e._payload,e=e._init;try{return t(e(f))}catch{}}return null}function r(e){return""+e}function o(e){try{r(e);var f=!1}catch{f=!0}if(f){f=console;var w=f.error,I=typeof Symbol=="function"&&Symbol.toStringTag&&e[Symbol.toStringTag]||e.constructor.name||"Object";return w.call(f,"The provided key is an unsupported type %s. This value must be coerced to a string before using it here.",I),r(e)}}function n(e){if(e===j)return"<>";if(typeof e=="object"&&e!==null&&e.$$typeof===C)return"<...>";try{var f=t(e);return f?"<"+f+">":"<...>"}catch{return"<...>"}}function a(){var e=x.A;return e===null?null:e.getOwner()}function u(){return Error("react-stack-top-frame")}function i(e){if(P.call(e,"key")){var f=Object.getOwnPropertyDescriptor(e,"key").get;if(f&&f.isReactWarning)return!1}return e.key!==void 0}function s(e,f){function w(){W||(W=!0,console.error("%s: `key` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://react.dev/link/special-props)",f))}w.isReactWarning=!0,Object.defineProperty(e,"key",{get:w,configurable:!0})}function p(){var e=t(this.type);return N[e]||(N[e]=!0,console.error("Accessing element.ref was removed in React 19. ref is now a regular prop. It will be removed from the JSX Element type in a future release.")),e=this.props.ref,e!==void 0?e:null}function c(e,f,w,I,q,tt){var R=w.ref;return e={$$typeof:m,type:e,key:f,props:w,_owner:I},(R!==void 0?R:null)!==null?Object.defineProperty(e,"ref",{enumerable:!1,get:p}):Object.defineProperty(e,"ref",{enumerable:!1,value:null}),e._store={},Object.defineProperty(e._store,"validated",{configurable:!1,enumerable:!1,writable:!0,value:0}),Object.defineProperty(e,"_debugInfo",{configurable:!1,enumerable:!1,writable:!0,value:null}),Object.defineProperty(e,"_debugStack",{configurable:!1,enumerable:!1,writable:!0,value:q}),Object.defineProperty(e,"_debugTask",{configurable:!1,enumerable:!1,writable:!0,value:tt}),Object.freeze&&(Object.freeze(e.props),Object.freeze(e)),e}function h(e,f,w,I,q,tt){var R=f.children;if(R!==void 0)if(I)if(B(R)){for(I=0;I<R.length;I++)A(R[I]);Object.freeze&&Object.freeze(R)}else console.error("React.jsx: Static children should always be an array. You are likely explicitly calling React.jsxs or React.jsxDEV. Use the Babel transform instead.");else A(R);if(P.call(f,"key")){R=t(e);var D=Object.keys(f).filter(function(At){return At!=="key"});I=0<D.length?"{key: someKey, "+D.join(": ..., ")+": ...}":"{key: someKey}",ot[R+I]||(D=0<D.length?"{"+D.join(": ..., ")+": ...}":"{}",console.error(`A props object containing a "key" prop is being spread into JSX:
2
+ let props = %s;
3
+ <%s {...props} />
4
+ React keys must be passed directly to JSX without using spread:
5
+ let props = %s;
6
+ <%s key={someKey} {...props} />`,I,R,D,R),ot[R+I]=!0)}if(R=null,w!==void 0&&(o(w),R=""+w),i(f)&&(o(f.key),R=""+f.key),"key"in f){w={};for(var et in f)et!=="key"&&(w[et]=f[et])}else w=f;return R&&s(w,typeof e=="function"?e.displayName||e.name||"Unknown":e),c(e,R,w,a(),q,tt)}function A(e){g(e)?e._store&&(e._store.validated=1):typeof e=="object"&&e!==null&&e.$$typeof===C&&(e._payload.status==="fulfilled"?g(e._payload.value)&&e._payload.value._store&&(e._payload.value._store.validated=1):e._store&&(e._store.validated=1))}function g(e){return typeof e=="object"&&e!==null&&e.$$typeof===m}var d=l,m=Symbol.for("react.transitional.element"),k=Symbol.for("react.portal"),j=Symbol.for("react.fragment"),T=Symbol.for("react.strict_mode"),b=Symbol.for("react.profiler"),$=Symbol.for("react.consumer"),M=Symbol.for("react.context"),y=Symbol.for("react.forward_ref"),O=Symbol.for("react.suspense"),Y=Symbol.for("react.suspense_list"),V=Symbol.for("react.memo"),C=Symbol.for("react.lazy"),E=Symbol.for("react.activity"),v=Symbol.for("react.client.reference"),x=d.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,P=Object.prototype.hasOwnProperty,B=Array.isArray,_=console.createTask?console.createTask:function(){return null};d={react_stack_bottom_frame:function(e){return e()}};var W,N={},F=d.react_stack_bottom_frame.bind(d,u)(),rt=_(n(u)),ot={};L.Fragment=j,L.jsx=function(e,f,w){var I=1e4>x.recentlyCreatedOwnerStacks++;return h(e,f,w,!1,I?Error("react-stack-top-frame"):F,I?_(n(e)):rt)},L.jsxs=function(e,f,w){var I=1e4>x.recentlyCreatedOwnerStacks++;return h(e,f,w,!0,I?Error("react-stack-top-frame"):F,I?_(n(e)):rt)}})()),L}var at;function Tt(){return at||(at=1,process.env.NODE_ENV==="production"?J.exports=_t():J.exports=St()),J.exports}var S=Tt();function jt(t,r){const o=new Uint8Array(r);for(let n=0;n<r;n++)o[n]=t[n*4+3];return o}function $t(t,r,o,n){if(r<=0||o<=0)return{x:0,y:0,w:1,h:1};let a=r,u=o,i=0,s=0,p=!1;for(let c=0;c<o;c++)for(let h=0;h<r;h++)t[c*r+h]>n&&(h<a&&(a=h),h>i&&(i=h),c<u&&(u=c),c>s&&(s=c),p=!0);return p?{x:a/r,y:u/o,w:(i-a+1)/r,h:(s-u+1)/o}:{x:0,y:0,w:1,h:1}}class ht{id;bounds={x:0,y:0,w:1,h:1};src;threshold;alpha=new Uint8Array(0);width=0;height=0;constructor(r,o){this.id=r.id,this.src=r.src,this.threshold=o}async prepare(){const r=new Image;r.crossOrigin="anonymous",r.src=this.src,await new Promise(i=>{r.onload=()=>i(),r.onerror=()=>i()});const o=r.naturalWidth,n=r.naturalHeight;if(o<=0||n<=0)return;const a=document.createElement("canvas");a.width=o,a.height=n;const u=a.getContext("2d",{willReadFrequently:!0});if(u)try{u.drawImage(r,0,0);const i=u.getImageData(0,0,o,n);this.alpha=jt(i.data,o*n),this.width=o,this.height=n,this.bounds=$t(this.alpha,o,n,this.threshold)}catch{this.alpha=new Uint8Array(0)}}hitTest(r,o){if(this.alpha.length===0)return!1;const n=this.bounds;if(r<n.x||r>n.x+n.w||o<n.y||o>n.y+n.h)return!1;const a=Math.min(this.width-1,Math.max(0,Math.floor(r*this.width))),u=Math.min(this.height-1,Math.max(0,Math.floor(o*this.height)));return this.alpha[u*this.width+a]>this.threshold}}class gt{id;bounds;constructor(r){this.id=r.id,this.bounds={...r.bounds}}hitTest(r,o){const n=this.bounds;return r>=n.x&&r<=n.x+n.w&&o>=n.y&&o<=n.y+n.h}}function Pt(t,r,o){let n=!1;for(let a=0,u=o.length-1;a<o.length;u=a++){const i=o[a][0],s=o[a][1],p=o[u][0],c=o[u][1];s>r!=c>r&&t<(p-i)*(r-s)/(c-s)+i&&(n=!n)}return n}class mt{id;bounds;points;constructor(r){this.id=r.id,this.points=r.points;let o=1/0,n=1/0,a=-1/0,u=-1/0;for(const[i,s]of r.points)i<o&&(o=i),i>a&&(a=i),s<n&&(n=s),s>u&&(u=s);this.bounds={x:o,y:n,w:a-o,h:u-n}}hitTest(r,o){const n=this.bounds;return r<n.x||r>n.x+n.w||o<n.y||o>n.y+n.h?!1:Pt(r,o,this.points)}}function bt(t,r){switch(t.type){case"image":return new ht(t,r);case"bbox":return new gt(t);case"polygon":return new mt(t)}}function Mt(t){switch(t.type){case"image":return`${t.id}:image:${t.src}:${t.label??""}`;case"bbox":return`${t.id}:bbox:${t.bounds.x},${t.bounds.y},${t.bounds.w},${t.bounds.h}:${t.label??""}`;case"polygon":return`${t.id}:polygon:${t.points.flat().join(",")}:${t.label??""}`}}function yt(t,r=!0,o=30,n=150){const[a,u]=l.useState(null),[i,s]=l.useState(null),p=l.useRef(null),c=l.useRef([]),[h,A]=l.useState({}),g=Math.min(255,Math.max(0,o)),d=l.useRef(null),m=l.useCallback(()=>{d.current===null&&(d.current=setTimeout(()=>{d.current=null,u(null)},n))},[n]),k=l.useCallback(()=>{d.current!==null&&(clearTimeout(d.current),d.current=null)},[]),j=t.map(Mt).join("|"),T=l.useMemo(()=>t,[j]);l.useEffect(()=>{if(!r){c.current=[];return}let C=!1,E=[];async function v(){const x=[],P={};for(const B of T){const _=bt(B,g);if(_.prepare&&await _.prepare(),C)return;x.push(_),P[_.id]=_.bounds}C||(E=x,c.current=x,A(P))}return v(),()=>{C=!0;for(const x of E)x.dispose?.()}},[T,r,g]);const b=l.useCallback((C,E)=>{const v=c.current;for(let x=v.length-1;x>=0;x--)if(v[x].hitTest(C,E))return v[x].id;return null},[]),$=l.useCallback(C=>{const E=p.current;if(!E)return null;const v=E.getBoundingClientRect(),x=(C.clientX-v.left)/v.width,P=(C.clientY-v.top)/v.height;return x<0||x>1||P<0||P>1?null:{nx:x,ny:P}},[]),M=l.useCallback(C=>{if(!r)return;const E=$(C);if(!E){m();return}const v=b(E.nx,E.ny);if(v===null){if(C.target?.closest('[data-cutout-overlay="true"]')){k();return}m();return}k(),u(v)},[r,$,b,m,k]),y=l.useCallback(()=>{m()},[m]),O=l.useCallback(C=>{if(!r)return;const E=$(C);if(!E){s(null);return}const v=b(E.nx,E.ny);s(v===i||v===null?null:v)},[r,$,b,i]),Y=i??a,V=r?h:{};return l.useEffect(()=>()=>{k()},[k]),{hoveredId:a,selectedId:i,activeId:Y,boundsMap:V,containerRef:p,containerProps:{onPointerMove:M,onPointerLeave:y,onClick:O}}}function K(t,r){return{name:t,css:r}}const it=new Set;function Ot(t){if(!(!t.keyframes?.length||typeof document>"u"))for(const r of t.keyframes){if(it.has(r.name))continue;it.add(r.name);const o=document.createElement("style");o.setAttribute("data-ricut-kf",r.name),o.textContent=`@keyframes ${r.name} {
7
+ ${r.css}
8
+ }`,document.head.appendChild(o)}}const U="all 0.5s cubic-bezier(0.22, 1, 0.36, 1)",vt={name:"elevate",transition:U,mainImageHovered:{filter:"brightness(0.45) saturate(0.7)"},vignetteStyle:{background:"radial-gradient(ellipse at center, transparent 20%, rgba(0,0,0,0.4) 100%)"},cutoutActive:{transform:"scale(1.04) translateY(-6px)",filter:"drop-shadow(0 0 28px rgba(130, 190, 255, 0.5)) drop-shadow(0 16px 48px rgba(0, 0, 0, 0.55))",opacity:1},cutoutInactive:{transform:"scale(1)",filter:"brightness(0.45) saturate(0.6)",opacity:.55},cutoutIdle:{transform:"scale(1)",filter:"drop-shadow(0 1px 4px rgba(0, 0, 0, 0.12))",opacity:1},geometryActive:{fill:"rgba(130, 190, 255, 0.2)",stroke:"rgba(130, 190, 255, 0.9)",strokeWidth:2,glow:"0 0 24px rgba(130, 190, 255, 0.5), 0 0 56px rgba(130, 190, 255, 0.2), 0 12px 40px rgba(0, 0, 0, 0.4)"},geometryInactive:{fill:"rgba(100, 150, 200, 0.06)",stroke:"rgba(100, 150, 200, 0.2)",strokeWidth:1},geometryIdle:{fill:"transparent",stroke:"transparent",strokeWidth:1}},xt={name:"glow",transition:U,mainImageHovered:{filter:"brightness(0.55) saturate(0.8)"},vignetteStyle:{background:"radial-gradient(ellipse at center, transparent 30%, rgba(0,0,0,0.3) 100%)"},cutoutActive:{transform:"scale(1)",filter:"drop-shadow(0 0 20px rgba(255, 200, 100, 0.6)) drop-shadow(0 0 60px rgba(255, 200, 100, 0.25))",opacity:1},cutoutInactive:{transform:"scale(1)",filter:"brightness(0.5) saturate(0.5)",opacity:.5},cutoutIdle:{transform:"scale(1)",filter:"none",opacity:1},geometryActive:{fill:"rgba(255, 200, 100, 0.15)",stroke:"rgba(255, 200, 100, 0.85)",strokeWidth:2,glow:"0 0 20px rgba(255, 200, 100, 0.5), 0 0 56px rgba(255, 200, 100, 0.2)"},geometryInactive:{fill:"rgba(200, 160, 80, 0.05)",stroke:"rgba(200, 160, 80, 0.2)",strokeWidth:1},geometryIdle:{fill:"transparent",stroke:"transparent",strokeWidth:1}},wt={name:"lift",transition:U,mainImageHovered:{filter:"brightness(0.4)"},vignetteStyle:{background:"rgba(0,0,0,0.25)"},cutoutActive:{transform:"scale(1.06) translateY(-10px)",filter:"drop-shadow(0 24px 64px rgba(0, 0, 0, 0.7))",opacity:1},cutoutInactive:{transform:"scale(0.97)",filter:"brightness(0.35)",opacity:.4},cutoutIdle:{transform:"scale(1)",filter:"none",opacity:1},geometryActive:{fill:"rgba(255, 255, 255, 0.1)",stroke:"rgba(255, 255, 255, 0.7)",strokeWidth:2,glow:"0 20px 56px rgba(0, 0, 0, 0.6), 0 0 16px rgba(255, 255, 255, 0.1)"},geometryInactive:{fill:"rgba(255, 255, 255, 0.02)",stroke:"rgba(255, 255, 255, 0.1)",strokeWidth:1},geometryIdle:{fill:"transparent",stroke:"transparent",strokeWidth:1}},kt={name:"subtle",transition:"all 0.3s ease",mainImageHovered:{filter:"brightness(0.7)"},vignetteStyle:{background:"transparent"},cutoutActive:{transform:"scale(1)",filter:"none",opacity:1},cutoutInactive:{transform:"scale(1)",filter:"none",opacity:.35},cutoutIdle:{transform:"scale(1)",filter:"none",opacity:1},geometryActive:{fill:"rgba(255, 255, 255, 0.08)",stroke:"rgba(255, 255, 255, 0.5)",strokeWidth:1},geometryInactive:{fill:"transparent",stroke:"rgba(255, 255, 255, 0.1)",strokeWidth:1},geometryIdle:{fill:"transparent",stroke:"transparent",strokeWidth:1}},ut=K("_ricut-trace-stroke",`from { stroke-dashoffset: 0; }
9
+ to { stroke-dashoffset: -1; }`),lt=K("_ricut-trace-glow",`0% { filter: drop-shadow(-3px -3px 6px rgba(255,255,255,0.6)) drop-shadow(0 0 2px rgba(255,255,255,0.15)); }
10
+ 25% { filter: drop-shadow(3px -3px 6px rgba(255,255,255,0.6)) drop-shadow(0 0 2px rgba(255,255,255,0.15)); }
11
+ 50% { filter: drop-shadow(3px 3px 6px rgba(255,255,255,0.6)) drop-shadow(0 0 2px rgba(255,255,255,0.15)); }
12
+ 75% { filter: drop-shadow(-3px 3px 6px rgba(255,255,255,0.6)) drop-shadow(0 0 2px rgba(255,255,255,0.15)); }
13
+ 100% { filter: drop-shadow(-3px -3px 6px rgba(255,255,255,0.6)) drop-shadow(0 0 2px rgba(255,255,255,0.15)); }`),Et={name:"trace",transition:U,keyframes:[ut,lt],mainImageHovered:{filter:"brightness(0.35) saturate(0.5)"},vignetteStyle:{background:"radial-gradient(ellipse at center, transparent 20%, rgba(0,0,0,0.5) 100%)"},cutoutActive:{transform:"scale(1)",filter:"drop-shadow(-3px -3px 6px rgba(255,255,255,0.6)) drop-shadow(0 0 2px rgba(255,255,255,0.15))",opacity:1,animation:`${lt.name} 3s linear infinite`},cutoutInactive:{transform:"scale(1)",filter:"brightness(0.35) saturate(0.4)",opacity:.4},cutoutIdle:{transform:"scale(1)",filter:"none",opacity:1},geometryActive:{fill:"rgba(255, 255, 255, 0.03)",stroke:"rgba(255, 255, 255, 0.9)",strokeWidth:2.5,strokeDasharray:"0.15 0.85",animation:`${ut.name} 3s linear infinite`,glow:"0 0 10px rgba(255, 255, 255, 0.25)"},geometryInactive:{fill:"transparent",stroke:"rgba(255, 255, 255, 0.15)",strokeWidth:1},geometryIdle:{fill:"transparent",stroke:"transparent",strokeWidth:1}},ct=K("_ricut-shimmer",`0%, 100% {
14
+ filter: brightness(1.05) contrast(1.02)
15
+ drop-shadow(0 0 6px rgba(255, 255, 255, 0.12))
16
+ drop-shadow(0 12px 32px rgba(0, 0, 0, 0.4));
17
+ }
18
+ 18% {
19
+ filter: brightness(1.4) contrast(1.08)
20
+ drop-shadow(0 0 14px rgba(255, 255, 255, 0.4))
21
+ drop-shadow(0 12px 32px rgba(0, 0, 0, 0.4));
22
+ }
23
+ 36% {
24
+ filter: brightness(1.05) contrast(1.02)
25
+ drop-shadow(0 0 6px rgba(255, 255, 255, 0.12))
26
+ drop-shadow(0 12px 32px rgba(0, 0, 0, 0.4));
27
+ }`),Ct={name:"shimmer",transition:U,keyframes:[ct],mainImageHovered:{filter:"brightness(0.35) saturate(0.6)"},vignetteStyle:{background:"radial-gradient(ellipse at center, transparent 20%, rgba(0,0,0,0.5) 100%)"},cutoutActive:{transform:"scale(1.04) translateY(-6px)",filter:"brightness(1.05) contrast(1.02) drop-shadow(0 0 6px rgba(255,255,255,0.12)) drop-shadow(0 12px 32px rgba(0,0,0,0.4))",opacity:1,animation:`${ct.name} 2.4s ease-in-out infinite`},cutoutInactive:{transform:"scale(1)",filter:"brightness(0.35) saturate(0.5)",opacity:.4},cutoutIdle:{transform:"scale(1)",filter:"drop-shadow(0 1px 4px rgba(0, 0, 0, 0.1))",opacity:1},geometryActive:{fill:"rgba(255, 255, 255, 0.1)",stroke:"rgba(255, 255, 255, 0.7)",strokeWidth:2,glow:"0 0 14px rgba(255, 255, 255, 0.35), 0 12px 32px rgba(0, 0, 0, 0.4)"},geometryInactive:{fill:"rgba(255, 255, 255, 0.02)",stroke:"rgba(255, 255, 255, 0.1)",strokeWidth:1},geometryIdle:{fill:"transparent",stroke:"transparent",strokeWidth:1}},H={elevate:vt,glow:xt,lift:wt,subtle:kt,trace:Et,shimmer:Ct},Z=l.createContext(null),It=l.createContext(null);function Q(){const t=l.useContext(It);if(!t)throw new Error("Must be used inside <CutoutViewer>");return t}const G=l.createContext(null);function Wt(){const t=l.useContext(G);if(!t)throw new Error("useCutout must be used inside <CutoutViewer.Cutout>");return t}function Nt({id:t,src:r,label:o,effect:n,children:a,renderLayer:u}){const i=l.useContext(Z),s=Q();if(!i)throw new Error("<CutoutViewer.Cutout> must be used inside <CutoutViewer>");l.useEffect(()=>(i.registerCutout({type:"image",id:t,src:r,label:o}),()=>i.unregisterCutout(t)),[t,r,o,i]);const p=n?typeof n=="string"?H[n]??s.effect:n:s.effect,c=s.activeId===t,h=s.hoveredId===t,A=s.selectedId===t,g={x:0,y:0,w:1,h:1},d=s.boundsMap[t]??g;let m;!s.enabled||!s.isAnyActive&&!s.showAll?m=p.cutoutIdle:s.showAll||c?m=p.cutoutActive:m=p.cutoutInactive;const k=l.useMemo(()=>({id:t,label:o,bounds:d,isActive:c,isHovered:h,isSelected:A,effect:p}),[t,o,d,c,h,A,p]);return S.jsxs(G.Provider,{value:k,children:[S.jsx("div",{"data-cutout-id":t,style:{pointerEvents:"none",position:"absolute",inset:0,zIndex:c?20:10,transition:p.transition,...m},children:u?u({isActive:c,isHovered:h,isSelected:A,bounds:d,effect:p}):S.jsx("img",{src:r,alt:o||t,draggable:!1,style:{width:"100%",height:"100%",objectFit:"fill",userSelect:"none"}})}),a]})}function ft(t){const{filter:r,...o}=t;return o}function Yt({id:t,bounds:r,label:o,effect:n,children:a,renderLayer:u}){const i=l.useContext(Z),s=Q();if(!i)throw new Error("<CutoutViewer.BBoxCutout> must be used inside <CutoutViewer>");const{x:p,y:c,w:h,h:A}=r;l.useEffect(()=>(i.registerCutout({type:"bbox",id:t,bounds:{x:p,y:c,w:h,h:A},label:o}),()=>i.unregisterCutout(t)),[t,p,c,h,A,o,i]);const g=n?typeof n=="string"?H[n]??s.effect:n:s.effect,d=s.activeId===t,m=s.hoveredId===t,k=s.selectedId===t,j={x:0,y:0,w:1,h:1},T=s.boundsMap[t]??j;let b,$;!s.enabled||!s.isAnyActive&&!s.showAll?($={...g.cutoutIdle,filter:"none",opacity:0},b=g.geometryIdle):s.showAll||d?($=ft(g.cutoutActive),b=g.geometryActive):($=ft(g.cutoutInactive),b=g.geometryInactive);const y=b??{fill:"rgba(37, 99, 235, 0.15)",stroke:"rgba(37, 99, 235, 0.6)",strokeWidth:2},O=l.useMemo(()=>({id:t,label:o,bounds:T,isActive:d,isHovered:m,isSelected:k,effect:g}),[t,o,T,d,m,k,g]);return S.jsxs(G.Provider,{value:O,children:[S.jsx("div",{"data-cutout-id":t,style:{pointerEvents:"none",position:"absolute",inset:0,zIndex:d?20:10,transition:g.transition,...$},children:u?u({isActive:d,isHovered:m,isSelected:k,bounds:T,effect:g}):S.jsx("svg",{viewBox:"0 0 1 1",preserveAspectRatio:"none",style:{position:"absolute",inset:0,width:"100%",height:"100%",overflow:"visible",filter:y.glow?`drop-shadow(${y.glow.split(",")[0]?.trim()??""})`:"none"},children:S.jsx("rect",{x:T.x,y:T.y,width:T.w,height:T.h,rx:.004,fill:y.fill,stroke:y.stroke,strokeWidth:(y.strokeWidth??2)*.0015,strokeLinecap:y.strokeDasharray?"round":void 0,strokeDasharray:y.strokeDasharray,pathLength:y.strokeDasharray?1:void 0,style:{transition:g.transition,animation:y.animation}})})}),a]})}function dt(t){const{filter:r,...o}=t;return o}function Dt({id:t,points:r,label:o,effect:n,children:a,renderLayer:u}){const i=l.useContext(Z),s=Q();if(!i)throw new Error("<CutoutViewer.PolygonCutout> must be used inside <CutoutViewer>");const p=r.flat().join(",");l.useEffect(()=>(i.registerCutout({type:"polygon",id:t,points:r,label:o}),()=>i.unregisterCutout(t)),[t,p,o,i]);const c=n?typeof n=="string"?H[n]??s.effect:n:s.effect,h=s.activeId===t,A=s.hoveredId===t,g=s.selectedId===t,d={x:0,y:0,w:1,h:1},m=s.boundsMap[t]??d;let k,j;!s.enabled||!s.isAnyActive&&!s.showAll?(j={...c.cutoutIdle,filter:"none",opacity:0},k=c.geometryIdle):s.showAll||h?(j=dt(c.cutoutActive),k=c.geometryActive):(j=dt(c.cutoutInactive),k=c.geometryInactive);const b=k??{fill:"rgba(37, 99, 235, 0.15)",stroke:"rgba(37, 99, 235, 0.6)",strokeWidth:2},$=l.useMemo(()=>({id:t,label:o,bounds:m,isActive:h,isHovered:A,isSelected:g,effect:c}),[t,o,m,h,A,g,c]);return S.jsxs(G.Provider,{value:$,children:[S.jsx("div",{"data-cutout-id":t,style:{pointerEvents:"none",position:"absolute",inset:0,zIndex:h?20:10,transition:c.transition,...j},children:u?u({isActive:h,isHovered:A,isSelected:g,bounds:m,effect:c}):S.jsx("svg",{viewBox:"0 0 1 1",preserveAspectRatio:"none",style:{position:"absolute",inset:0,width:"100%",height:"100%",overflow:"visible",filter:b.glow?`drop-shadow(${b.glow.split(",")[0]?.trim()??""})`:"none"},children:S.jsx("polygon",{points:r.map(([M,y])=>`${M},${y}`).join(" "),fill:b.fill,stroke:b.stroke,strokeWidth:(b.strokeWidth??2)*.0015,strokeLinejoin:"round",strokeLinecap:b.strokeDasharray?"round":void 0,strokeDasharray:b.strokeDasharray,pathLength:b.strokeDasharray?1:void 0,style:{transition:c.transition,animation:b.animation}})})}),a]})}function Ht(t,r){const{x:o,y:n,w:a,h:u}=r;let i,s;t.includes("left")?(i=`${o*100}%`,s="0"):t.includes("right")?(i=`${(o+a)*100}%`,s="-100%"):(i=`${(o+a/2)*100}%`,s="-50%");let p,c;return t.startsWith("top")?(p=`${n*100}%`,c="-100%"):t.startsWith("bottom")?(p=`${(n+u)*100}%`,c="0"):(p=`${(n+u/2)*100}%`,c="-50%"),{position:"absolute",left:i,top:p,transform:`translate(${s}, ${c})`}}function Rt({placement:t="top-center",children:r,className:o="",style:n}){const a=l.useContext(G),u=Q();if(!a)throw new Error("<CutoutViewer.Overlay> must be used inside <CutoutViewer.Cutout>");const i=u.enabled&&(u.showAll||a.isActive),s=Ht(t,a.bounds);return S.jsx("div",{"data-cutout-overlay":"true",className:o,style:{zIndex:30,transition:a.effect.transition,opacity:i?1:0,pointerEvents:i?"auto":"none",...s,...n},children:r})}function pt(t){switch(t.type){case"image":return`image:${t.src}:${t.label??""}`;case"bbox":return`bbox:${t.bounds.x},${t.bounds.y},${t.bounds.w},${t.bounds.h}:${t.label??""}`;case"polygon":return`polygon:${t.points.flat().join(",")}:${t.label??""}`}}function Vt({mainImage:t,mainImageAlt:r="Main image",effect:o="elevate",enabled:n=!0,showAll:a=!1,alphaThreshold:u=30,hoverLeaveDelay:i=150,children:s,className:p="",style:c,onHover:h,onActiveChange:A,onSelect:g}){const d=typeof o=="string"?H[o]??H.elevate:o;l.useEffect(()=>{Ot(d)},[d]);const[m,k]=l.useState(()=>new Map),j=l.useCallback(_=>{k(W=>{const N=W.get(_.id);if(N&&pt(N)===pt(_))return W;const F=new Map(W);return F.set(_.id,_),F})},[]),T=l.useCallback(_=>{k(W=>{if(!W.has(_))return W;const N=new Map(W);return N.delete(_),N})},[]),b=l.useMemo(()=>({registerCutout:j,unregisterCutout:T}),[j,T]),$=l.useMemo(()=>Array.from(m.values()),[m]),{activeId:M,selectedId:y,hoveredId:O,boundsMap:Y,containerRef:V,containerProps:C}=yt($,n,u,i),E=l.useRef(null),v=l.useRef(null),x=l.useRef(null);l.useEffect(()=>{O!==E.current&&(E.current=O,h?.(O))},[O,h]),l.useEffect(()=>{M!==v.current&&(v.current=M,A?.(M))},[M,A]),l.useEffect(()=>{y!==x.current&&(x.current=y,g?.(y))},[y,g]);const P=a||M!==null,B=l.useMemo(()=>({activeId:M,selectedId:y,hoveredId:O,effect:d,enabled:n,showAll:a,boundsMap:Y,isAnyActive:P}),[M,y,O,d,n,a,Y,P]);return S.jsx(Z.Provider,{value:b,children:S.jsx(It.Provider,{value:B,children:S.jsxs("div",{ref:V,className:p,style:{position:"relative",width:"100%",overflow:"hidden",cursor:P&&n?"pointer":"default",...c},...C,children:[S.jsx("img",{src:t,alt:r,draggable:!1,style:{display:"block",width:"100%",height:"auto",userSelect:"none",transition:d.transition,...P&&n?d.mainImageHovered:{}}}),S.jsx("div",{style:{pointerEvents:"none",position:"absolute",inset:0,transition:d.transition,opacity:P&&n?1:0,...d.vignetteStyle}}),s]})})})}const X=Vt;X.Cutout=Nt;X.BBoxCutout=Yt;X.PolygonCutout=Dt;X.Overlay=Rt;exports.CutoutOverlay=Rt;exports.CutoutViewer=X;exports.ImageHitTestStrategy=ht;exports.PolygonHitTestStrategy=mt;exports.RectHitTestStrategy=gt;exports.createHitTestStrategy=bt;exports.defineKeyframes=K;exports.elevateEffect=vt;exports.glowEffect=xt;exports.hoverEffects=H;exports.liftEffect=wt;exports.shimmerEffect=Ct;exports.subtleEffect=kt;exports.traceEffect=Et;exports.useCutout=Wt;exports.useCutoutHitTest=yt;
@@ -0,0 +1 @@
1
+ export * from "./components/cutout-viewer";