export interface BlendConfig { /** SVG group in the melt-overlay layer; the engine builds the warped-image * structure inside it (pattern-filled rounded rects run through a * turbulence → displacement → blur → saturation filter chain). Filters on * SVG content render correctly in every browser — CSS url() filters on * HTML are what WebKit breaks. */ host: SVGGElement; /** Melt blur radius in px. */ blur: number; /** Displacement strength of the liquid warp (feDisplacementMap scale). */ warp: number; /** Max px the melted imagery is pulled toward the contact. */ pull: number; /** Distance (px) at which melting starts; defaults from the group's goo blur. */ range?: number; /** Base radius (px) of the melt zone around the contact point. Defaults * from the group's goo blur — the mixing zone matches the bridge width * instead of swallowing the whole element. */ zone?: number; /** 0..1 — two-liquid mixing: the melted image is eroded into noise-shaped * tendrils so the neighbour's liquid (already behind it) shows through the * gaps. Never paints a shape — a painted disc reads as a visible circle. */ mix?: number; /** Px the melted material is drawn toward the NEIGHBOUR's centre at full * contact — the dissolve streams into the other body instead of * scattering symmetrically. Default 25. */ gravity?: number; /** 0..1 — how POINTY the gravity flow is. Each layer's mask is pushed * further along the flow and shrunk, so their union tapers to a tip * instead of stretching as an even blob. Default 0.65. */ taper?: number; /** Noise octaves — higher = finer, more detailed swirls. Default 2. */ detail?: number; /** Multiplier on the noise frequency: <1 = broad lazy swirls, >1 = fine * busy veins. Default 1. */ warpFreq?: number; /** Px/s the noise field drifts, so the liquid visibly churns while held * instead of sitting frozen. 0 = static. Default 26. */ flowSpeed?: number; /** 'fractalNoise' = soft billows (default), 'turbulence' = veinier, * more marbled liquid. */ warpStyle?: 'fractalNoise' | 'turbulence'; /** While false, the melt fades out over `releaseMs` regardless of * proximity — e.g. the moment a drag is released. Default true. */ active?: boolean; /** Structural release time when `active` goes false, ms. Default 240. */ releaseMs?: number; /** Ms the melt takes to EVAPORATE — its opacity fades to zero over this, * independently of `releaseMs`, so the dissolve can leave gradually * instead of blinking out. May exceed `releaseMs` (the melt then lives * until the fade finishes). Defaults to `releaseMs`. */ fadeMs?: number; /** 0..1 — overall dissolve intensity, independent of proximity: a master * ceiling on how far the melt can develop even at full contact. Scales * warp/blur/gravity/mix AND the hole depth together, so a weak strength * reads as a shallower liquid rather than an erased edge with no melt to * justify it. 1 = full strength (default). */ strength?: number; /** How deep this piece may sink into its neighbour before the melt is fully * gone, as a fraction of the smaller body (1 = completely engulfed). * Melting is a SURFACE event: it belongs to the moment two skins meet, and * once a piece is well inside the other there is no seam left to mix at — * it has simply joined. Without this the proximity gap pins to zero on * first overlap and the melt stays at full while the piece buries itself, * which reads as a smear that should have resolved. Default 0.8; raise * toward (or past) 1 to keep melting while deeply overlapped. */ sink?: number; } export interface EvolveOptions { /** Spring driving the liquid mass's centre. Default 320 / 17. */ massStiffness?: number; massDamping?: number; /** Spring driving width/height. Default 170 / 11.5. */ sizeStiffness?: number; sizeDamping?: number; /** Spring driving the corner radius. Default 900 / 60 — stiff and * overdamped, so the element's own border-radius transition timing shows * through instead of the spring imposing its own. Soften it to make the * corners lag the element. */ radiusStiffness?: number; radiusDamping?: number; /** Max content cross-blur during the morph, px. 0 disables. Default 7. */ contentBlur?: number; /** 0..1 — how strongly the blob rounds into a droplet while morphing. Default 1. */ roundness?: number; /** Corner-forming timeline: starts at the very beginning of the morph and * runs droplet-round → target radius over `cornerDuration` ms with * `cornerEase` (any CSS easing: a cubic-bezier(...), a keyword or a * linear(...) list), * after `cornerDelay` ms. No motion gating — tweak duration/easing and it * behaves like a normal animation. Defaults 460 / 0 / smooth. */ cornerDuration?: number; cornerDelay?: number; cornerEase?: string; /** Ms the travel lead takes to ramp in — how EAGERLY the droplet commits to * the destination. 0 leads instantly; it never scales the reach. * Default 90. */ anticipation?: number; /** Px the mass centre leads ahead of the element — how FAR the droplet * travels toward the destination before it inflates. 0 disables. * Default 32. */ travel?: number; } export declare const EVOLVE_DEFAULTS: Required; export interface MoveOptions { /** Spring pulling the liquid surface after the element. Lower stiffness / * damping = a laggier, more rubbery trail. Default 380 / 18. */ stiffness?: number; damping?: number; /** Max axial stretch at speed (0 = rigid). Default 0.18. */ stretch?: number; /** Trailing droplet size as a fraction of the body. 0 disables the tail. * Default 0.46. */ tail?: number; } export declare const MOVE_DEFAULTS: Required; export interface ItemDynamics { /** Liquid surface springs behind size/shape changes and settles like jelly. */ evolve: boolean; /** Surface lags the moving element and stretches with velocity — liquid rubber. */ move: boolean; /** Resolved evolve tuning; falls back to EVOLVE_DEFAULTS. */ evolveOpts?: Required; /** Resolved move tuning; falls back to MOVE_DEFAULTS. */ moveOpts?: Required; } export interface ObservedTarget { target: HTMLElement; blob: SVGRectElement; radius?: number; /** Px to shrink the blob on every side relative to the element — lets an * opaque element (e.g. a round photo) fully cover its own liquid so white * only appears as the merge bridge. */ blobInset?: number; /** Px the blob swells back OUT (beyond blobInset) as the item nears a * neighbour — an opaque element visibly grows a liquid coat that necks * into the other surface, instead of merging invisibly behind itself. */ bridgeGrow?: number; blend?: BlendConfig; dynamics?: ItemDynamics; } /** Shared per-group measurement loop for observe-mode items: mirrors externally * animated elements onto their blobs each frame, then sleeps entirely once * nothing has moved for ~half a second. MutationObserver + transition / * animation events + a slow safety tick wake it, so idle cost is zero. * * Items opted into contact blur additionally get a liquid melt at the point * where they touch a neighbour: their images are re-rendered as SVG shapes * warped by a turbulence displacement field — edges bend and smear like two * materials merging — while the originals' edges dissolve under them. */ export declare class ObserveEngine { private getGroup; /** Goo blur of the owning group; used to derive the default blend range. */ gooBlur: number; private items; private awake; private clean; private raf; private sourcesReady; private mo; private interval; private removeListeners; constructor(getGroup: () => HTMLElement | null); add(t: ObservedTarget): () => void; wake: () => void; dispose(): void; private resolveRadius; /** (Re)build the warped-image SVG structure for a melt item. Two graded * warp layers share ONE noise field (same frequency + seed): a wide gentle * ripple and a tight strong core. Same field, different displacement * scales → the layers align and read as a single liquid getting deeper * toward the contact. The noise frequency is derived from the melt zone, * so several ripple wavelengths fit across it — a fixed low frequency * displaces the whole zone as one chunk, which reads as a shifted ghost * copy instead of liquid. */ /** Resize response. The melt DOM only needs REBUILDING when the set of * images changes; a resize alone just makes the cached corner radii stale. * * Rebuilding on every resize tore down and recreated three turbulence * filters plus a pattern + per photo, and WebKit re-decodes and * re-rasterises all of it synchronously. The pill resizes the moment the * hover gap opens — i.e. exactly as the flight begins — so that landed as * a ~120ms main-thread stall. CSS transitions keep running on the * compositor through a stall, but the silhouette is written from this * loop, so the liquid froze while the content sailed on: the timing * mismatch, and it never showed in Chromium because the rebuild is cheap * enough there to fit in a frame. */ private syncMelt; private refreshMelt; /** Remove all melt traces: hide the warped overlay, restore image masks. */ private clearBlend; private lastNow; /** EMA of the raw frame interval (ms) — the melt write pass consults it to * pick its cadence: every frame while the clock is healthy, backed off to * ~28fps when frames are dropping (WebKit under CPU-raster load is exactly * the case that degrades the clock). Seeded at 60Hz-healthy. */ private frameEma; private loop; private measureAll; /** Effective blob inset: bridgeGrow pulls it toward negative (a visible * liquid coat) as the nearest neighbour approaches. * * Smoothed on a time constant rather than tracking proximity instantly. * The raw value is a function of the dragged neighbour's position, so it * moves as fast as the pointer does and lands on a different value every * frame; the blob grows symmetrically from it, so that per-frame step is * visible on the silhouette's far edge as a size flicker. It stayed small * enough to read as smooth at 60fps, but a frame-rate drop multiplies the * per-frame delta — which is why the pill's left edge flashed in Safari * and not in Chromium. dt-based smoothing makes the growth rate identical * at any frame rate. */ private effectiveInset; private writeBlob; /** Nearest-neighbour proximity → a liquid warp-melt at the contact point. * The strength is ONE smoothed value: fast attack while approaching, and * a gradual `releaseMs` decay whenever the target drops — whether from a * drag release (`active: false`), moving out of range, or absorption. No * path clears instantly. */ private writeBlend; private ensureSources; }