/** * Geometry and timing kernel shared by the two liquid anchor animations: * `drip` (constant-width sheet) and `bead` (width driven by p's velocity). * * Everything here is a pure function of one progress scalar `p` (0 closed, * 1 open) so the visual can be reasoned about — and unit tested — without a * layout engine. jsdom reports every offset as 0, so the component tests feed * metrics in directly rather than measuring. */ /** Progress at which the neck between trigger and panel has finished pinching. */ export declare const DETACH_AT = 0.45; /** How many pixels of panel growth an item fades across. */ export declare const ITEM_FADE_SPAN = 18; /** Below this the neck is not cropping anything a display can resolve. */ export declare const NECK_CLIP_MIN = 0.5; /** * Speed at which the bead pinch saturates, in peel per unit of normalised time. * * The peel leaves the trigger at `2 / DETACH_AT` — about 4.44 — and decays * linearly to 0 at the detach, so a reference of 4 holds the sheet fully drawn * in for roughly the first tenth of the peel and then lets it relax. */ export declare const BEAD_VELOCITY_REF = 4; export interface LiquidMetrics { /** Panel top edge at p=0, in floating-layer local coordinates (trigger centre). */ topStart: number; /** Panel top edge at p=1 — always 0, i.e. where floating-ui placed the panel. */ topEnd: number; /** Panel height at p=0. The drop has to start with *some* body to be torn off. */ seedHeight: number; /** Measured panel height at p=1. */ panelHeight: number; } export interface LiquidGeometry { top: number; height: number; /** Derived from top + height. Never fed back into height. */ bottom: number; } export declare const LIQUID_DEFAULTS: { readonly duration: 260; readonly closeDuration: 150; readonly ease: "linear"; readonly closeEase: "cubic-bezier(0.25, 0.46, 0.45, 0.94)"; readonly gooBlur: 4.5; readonly gooThreshold: 20; readonly gooThresholdOffset: -9; readonly seedHeight: 12; readonly itemSelector: "[data-liquid-item]"; readonly outlineColor: "#dcdfe6"; /** * `bead` only: how far each side may be drawn in at peak speed (px). * * Deep enough that the neck reads as a neck rather than as a slightly inset * copy of the trigger — on the reference 200px panel it necks to 80px, well * under half. Full-width rows cannot survive a pinch this deep on their own, * which is what `neckClipAt` is for. */ readonly beadPinch: 60; readonly beadVelocityRef: 4; }; export declare function clamp01(value: number): number; export declare function easeOutQuad(t: number): number; export declare function easeOutCubic(t: number): number; /** * Resolve an ease string to a timing function. * * The liquid motion is specified as fixed curves, so it takes `linear` or a CSS * `cubic-bezier(...)` and nothing else: GSAP names and every spring formulation * fall back to `fallback` rather than being silently accepted. * * `linear` is a first-class value here, not a fallback: p is meant to advance * linearly so the two per-edge easings are the only shaping in the system. * Compounding a front-loaded master curve on top of them collapses the whole * drop into about two frames at 60Hz. */ export declare function resolveLiquidEase(value: string | undefined, fallback: string): (t: number) => number; /** * Normalise raw measurements into the metrics the geometry runs on. * Clamping the seed here keeps `geometryAt` a plain interpolation. */ export declare function createLiquidMetrics(input: { triggerTop: number; triggerHeight: number; panelHeight: number; seedHeight: number; }): LiquidMetrics; /** * How far the top edge has peeled, 0..1. Reaches 1 at DETACH_AT and stays there. * * This — not `p` — is the drop's actual fall. `p` advances linearly, so its own * derivative is a constant and carries no information; the peel decelerates to a * dead stop at the detach, and it is the faster of the two motions the bead * reports until roughly a third of the way through the timeline. */ export declare function peelAt(p: number): number; /** * The panel is described by two falling edges, not by a box that grows. * * The top edge peels fast and stops at DETACH_AT; the height keeps filling * through the detach and past it. At the moment the neck snaps the panel is * already easeOutCubic(0.45) = 83.4% of its body and still filling. */ export declare function geometryAt(p: number, metrics: LiquidMetrics): LiquidGeometry; /** * d(peel)/dp — the peel's own slope at progress `p`. * * `easeOutQuad(x)` is `2x - x²`, so its slope is `2 - 2x`, and `x = p / DETACH_AT` * contributes the `1 / DETACH_AT` chain factor. Past the detach the peel is * saturated and the slope is exactly 0, which is what retires the pinch. */ export declare function peelSlopeAt(p: number): number; /** * d(fill)/dp — the slope of the body still filling. * * `easeOutCubic(x)` is `1 - (1-x)³`, so its slope is `3(1-x)²`. Unlike the peel * this never saturates: the panel is still growing right up to p = 1, which is * what gives the pinch something to keep reporting after the neck has snapped. */ export declare function fillSlopeAt(p: number): number; /** * Slope of a resolved ease at timeline position `t`, by central difference. * * Every ease reaching this point is either linear or a cubic-bezier — both * smooth enough that a fixed-step difference lands well inside a tenth of a * pixel of pinch. Staying a pure function of `t` is the property that matters: * two machines running at different refresh rates derive the same number for * the same point in the timeline. */ export declare function easeSlopeAt(ease: (t: number) => number, t: number): number; /** * How fast the drop is moving at timeline position `t`, normalised per unit of * timeline so it is independent of duration and refresh rate. * * The drop has two motions and reports whichever is currently faster. The peel * dominates the tear and then stops dead at `DETACH_AT`; the fill is slower but * runs all the way to p = 1. Reporting the peel alone cut the pinch off at 45% * of the timeline — the sheet slammed back to full width while the panel was * still visibly growing. A sheet is under tension for as long as *either* end of * it is still moving. * * Both slopes are closed forms, which is the other half of the point: the frame * loop used to reconstruct this by backward difference across rAF frames, and * differencing has no sample at `t = 0`, so it reported a standing start the drop * never has and popped the sheet from full width to full pinch on frame one. */ export declare function liquidVelocityAt(p: number, t: number, ease: (t: number) => number): number; /** * How hard the sheet is squeezed right now, 0..1. * * This is the whole point of `bead`: the width reports how fast the drop is * falling* rather than how far along it is, so the pinch peaks as the drop * breaks away and relaxes to nothing the moment the fall stops. */ export declare function beadPinchRatio(velocity: number, velocityRef?: number): number; /** * Horizontal span of the sheet, drawn in symmetrically about its own centre so * the bead necks rather than sliding sideways. */ export declare function beadSpanAt(panelWidth: number, maxPinch: number, pinchRatio: number): { x: number; width: number; }; /** * Opacity for one item, keyed to the panel's own growth rather than the clock. * * `hold` is the item's bottom edge in panel-local coordinates; because the * panel's own bottom rises monotonically, an item cannot start appearing until * the panel has fully grown to hold it. */ export declare function itemOpacityAt(bottom: number, hold: number, span?: number): number; /** * `clip-path` that keeps the panel's content inside the necked sheet. * * `itemOpacityAt` is the vertical half of this rule — nothing appears before the * panel has grown to hold it — and a deep pinch makes the horizontal half matter * just as much: rows keep their full width while the sheet does not, so at a * pinch of any depth their text would hang outside the silhouette. * * Clipped rather than faded on purpose. A fade has to be tuned against the pinch * depth and only ever makes the overhang *faint*; clipping to the sheet makes it * impossible, and reads as the neck revealing the content as it relaxes. The goo * threshold sits fractionally outside the rect it is drawn from, so clipping on * the rect edge stays conservative. */ export declare function neckClipAt(pinch: number): string;