import { GpuComputeNode, GpuFragmentParams } from '../../gpu/contract'; import { ComputeStep, KitComputePipeline } from '../../gpu/compute'; import { StableFluidsKernels } from '../../gpu/scaffolds/fluids'; import { PointerFrame, PointerLike, PointerVelocityTrackerOptions } from '../../gpu/kit/host/pointer'; /** The GPU device handle the build callback receives, for creating the resources your passes use. */ export type FluidRoot = NonNullable['root']; /** A pass function run once per grid cell, with the cell's column and row. */ export type FluidKernel = (cx: number, cy: number) => void; /** What the renderer reports each frame: the pointer in uv, the frame delta in seconds, the canvas size in pixels. */ export interface FluidFrameParams { pointer?: PointerLike; deltaTime?: number; dimensions?: { width: number; height: number; }; } /** One frame, as every part and `values` sees it: the clamped delta, the clocks, the pointer and your props. */ export interface FluidFrame { /** Seconds since the last frame, never more than `maxDeltaTime` (0.033 by default). */ dt: number; /** Wall-clock milliseconds this frame. */ now: number; /** The simulation's own clock: the sum of every `dt` so far. */ readonly elapsed: number; setElapsed(t: number): void; /** The pointer this frame, when the config declared `pointer`. Null otherwise. */ ptr: PointerFrame | null; /** A numeric prop's current value, or `fallback` when it is not a number this frame. */ num(key: string, fallback: number): number; /** Any prop's current value. */ getCpuValue(key: string): unknown; frameParams: FluidFrameParams; /** Write the per-frame values directly, for a part that rewrites them between passes. */ writeParams(v: unknown): void; } /** What a part receives when it builds its passes: the device, the grid size, the buffers and a pass builder. */ export interface FluidSetupCtx { params: GpuFragmentParams; root: FluidRoot; /** Cells per side of the grid. */ n: number; buffers: { velA: unknown; velB: unknown; dyeA: unknown; dyeB: unknown; pressure: unknown; divergence: unknown; maskBuf?: unknown; }; /** The buffer holding the per-frame values, for a part with its own resources. */ paramsBuffer: unknown; /** The resource group every solver pass runs on. */ fluidBindGroup: unknown; /** Turn a pass function into a runnable pass, on the fluid's resources unless another group is given. */ pass(kernel: FluidKernel, bindGroup?: unknown): KitComputePipeline; } /** Something that injects into the field each frame. Build one with `splat` or `cursorRibbon`. */ export interface FluidEmitter { setup?(ctx: FluidSetupCtx): void; /** Bookkeeping before the idle gate: sample the pointer, drift a color cycle, mark activity. */ tick?(f: FluidFrame): void; /** Return true to skip the whole frame. The settled field stays on screen. */ skip?(f: FluidFrame): boolean; /** Add this frame's injection passes. They run before the solve. */ emit?(f: FluidFrame, nodes: ComputeStep[], values: TParams): void; } /** A pass that runs inside every solve, before it (`pre`) or after it (`post`). Build one with `ambientForce` or `restoreToward`. */ export interface FluidSolveStage { stage: 'pre' | 'post'; kernel: FluidKernel; } /** * A force applied to the whole field before every solve: a turbulence field, a steady wind. * * @example * ```ts * solve: [ambientForce(turbulenceKernel)] * ``` * @see restoreToward, fluidSim */ export declare const ambientForce: (kernel: FluidKernel) => FluidSolveStage; /** * A correction applied after every solve: re-seed a color that the solve slowly blurs away. * * @example * ```ts * solve: [restoreToward(colorRestoreKernel)] * ``` * @see ambientForce, fluidSim */ export declare const restoreToward: (kernel: FluidKernel) => FluidSolveStage; /** What an init part appends with: the frame's pass list, a queued values write, and a full solve. */ export interface FluidInitIo { nodes: ComputeStep[]; writeParams(v: TParams): ComputeStep; solve(nodes: ComputeStep[], jacobiIters: number): void; } /** How the field starts, and when it must start over. Build one with `seededFieldInit`. */ export interface FluidInitPart { setup?(ctx: FluidSetupCtx): void; /** Return true when the field must be (re)seeded this frame. */ stale(f: FluidFrame): boolean; run(f: FluidFrame, io: FluidInitIo): void; } /** What a container (a shape the fluid is confined to) contributes each frame. */ export interface FluidContainerParts { /** The pass that writes the inside/outside mask, run after the values write and before injection. */ maskPass: KitComputePipeline; /** Extra outputs for the fragment. */ outputs?: Record; /** Passes that run first each frame, even on a frame too short to solve. */ preFrame?(frameParams: unknown): ComputeStep[] | null; } /** What a fluid simulation declares: its grid, its solver, its output, its parts and its per-frame values. */ export interface FluidSimConfig { /** Cells per side of the grid. 256 is the usual choice. */ resolution: number; /** The layout naming the fluid's buffers. It must use the solver's entry names. */ layout: unknown; /** The layout of the output pass: the dye buffer read-only plus the output texture. */ outputLayout: unknown; /** The schema of the per-frame values your passes read. The engine owns the buffer. */ paramsSchema: unknown; /** The solver's passes and how many pressure iterations to run per solve. 10 is the norm. */ solver: { kernels: StableFluidsKernels; jacobiIters: number; }; /** The pass that turns dye into the output texture, and the name the fragment reads it under. */ output: { kernel: FluidKernel; key: string; }; /** How the values reach the GPU: written at once (default), or queued in order with the passes. Use `'thunk'` with `seededFieldInit`, whose warm-up queues its own writes. */ write?: 'direct' | 'thunk'; /** Track the pointer for a cursor force. Stroke emitters built with `cursorRibbon` track their own. */ pointer?: PointerVelocityTrackerOptions; /** A shape the fluid is confined to. */ container?: { setup(ctx: FluidSetupCtx): FluidContainerParts; }; /** How the field starts. Omit to start empty. */ init?: FluidInitPart; /** What injects dye and momentum each frame. */ inject?: FluidEmitter[]; /** Passes run before and after every solve. */ solve?: FluidSolveStage[]; /** The values your passes read this frame, built from the clock, the pointer and your props. */ values(f: FluidFrame): TParams; /** Longest frame step accepted, in seconds. Default 0.033. */ maxDeltaTime?: number; } /** * A fluid the engine solves every frame. Spread it into a definition next to a `gpu.fragment` * that samples the output texture under `output.key`. * * The build callback runs once per instance of the layer, with the GPU device, and returns the * grid size, the solver, the output pass, the parts and the `values` function. Everything * created inside it belongs to that instance. Without a GPU nothing runs and the fragment * should fall back to transparent. * * @example * ```ts * ...fluidSim(() => ({resolution: 256, layout: fluidLayout, outputLayout, paramsSchema: FluidParams, solver: {kernels: solverKernels, jacobiIters: 10}, output: {kernel: outputKernel, key: 'smokeTexture'}, pointer: {minDrag: 0.0005}, inject: [splat({kernel: splatKernel})], values: (f) => ({dt: f.dt, emitX: 0.5 * 256, emitY: 256, emitRad: f.num('emitRadius', 0.06) * 256, dyeFade: f.num('dissipation', 1), velFade: 0.2, curlStrength: f.num('detail', 30), colorDecay: 1})})) * ``` * @tip Positions and radii in `values` are in grid cells, not uv: multiply a uv value by `resolution`. * @see splat, cursorRibbon, seededFieldInit, ambientForce, restoreToward, simulate */ export declare function fluidSim(build: (params: GpuFragmentParams, root: FluidRoot) => FluidSimConfig): { compute: GpuComputeNode; }; /** * An emitter at a fixed source: one pass per frame that adds dye and momentum where your pass * function says (a cone from a point, a line along an edge). * * Give it the pass function, or a `setup` that builds the pass on resources of its own. * * @example * ```ts * inject: [splat({kernel: splatKernel})] * ``` * @tip The source position and radius come from `values`, in grid cells. * @see cursorRibbon, fluidSim */ export declare function splat(opts: { kernel: FluidKernel; } | { setup: (ctx: FluidSetupCtx) => KitComputePipeline; }): FluidEmitter; /** This frame's ribbon of stamps along the stroke: spacing, cap, and how each stamp is written. */ export interface CursorRibbonSpec { /** Distance between stamps, in uv. Half the brush radius reads continuous. */ stepSize: number; /** Most stamps in one frame, so a fast flick cannot run away. */ maxSteps: number; /** A per-stamp payload prepared in stamp order, with `t` from 0 to 1 along the stroke. */ prepare?: (t: number) => TPrepared; /** Write the values for one stamp at `posX, posY` (grid cells) right before its pass runs. */ write: (posX: number, posY: number, t: number, prepared: TPrepared) => void; } /** * An emitter that paints along the pointer's stroke: a ribbon of stamps between last frame's * position and this one, so a fast flick leaves a continuous line, not dots. * * It tracks the pointer itself, marks the frame active when `activeWhen` says so, and sleeps * the simulation `fadeSeconds` after the last activity. `ribbon` describes this frame's stamps. * Read the pointer back with `.ptr()` and `.active()` inside `values`. * * @example * ```ts * const strokes = cursorRibbon({activeWhen: (ptr) => ptr.smoothSpeed > 0.005, fadeSeconds: (f) => decayFadeSeconds(255, f.num('dissipation', 0.4)), pass: {kernel: splatKernel}, ribbon: (f, ptr, values) => ({stepSize: Math.max(0.004, f.num('emitRadius', 0.07) * 0.5), maxSteps: 64, write: (emitX, emitY) => f.writeParams({...values, emitX, emitY})})}) * ``` * @tip Raise the tracker's `teleportGuard` when fast flicks should paint: the default treats a quarter-canvas jump as the cursor re-entering and emits nothing. * @see splat, fluidSim */ export declare function cursorRibbon(opts: { /** Pointer tracking: smoothing, the teleport distance, the minimum drag. */ tracker?: PointerVelocityTrackerOptions; /** Whether the stroke emits this frame. A live frame keeps the simulation awake. */ activeWhen: (ptr: PointerFrame, f: FluidFrame) => boolean; /** Seconds after the last activity before the simulation may sleep. */ fadeSeconds: (f: FluidFrame) => number; /** Extra bookkeeping per frame before the idle gate. */ onFrame?: (f: FluidFrame, ptr: PointerFrame) => void; /** The stamp pass: a pass function, or a `setup` that builds one on resources of its own. */ pass: { kernel: FluidKernel; } | { setup: (ctx: FluidSetupCtx) => KitComputePipeline; }; /** This frame's stamps. */ ribbon: (f: FluidFrame, ptr: PointerFrame, values: TParams) => CursorRibbonSpec; }): FluidEmitter & { ptr(): PointerFrame | null; active(): boolean; }; /** * A field that starts full: seed it from a pass function, then run the solve silently * `warm.steps` times so the first visible frame already flows. * * Re-seeds whenever `seed` returns a new value. Each warm step advances a private clock by * `warm.dt` seconds and writes `warm.values(t)`. When it finishes, the simulation's clock * continues from where the warm-up ended. * * @example * ```ts * init: seededFieldInit({kernel: initKernel, seed: (f) => f.num('seed', 0), warm: {steps: 50, jacobiIters: 10, dt: 0.033, startTime: () => Math.random() * 100, values: (t, f) => ({...baseValues(f), time: t})}}) * ``` * @tip A random `startTime` makes two instances of the same layer drift differently. * @see fluidSim, ambientForce */ export declare function seededFieldInit(opts: { kernel: FluidKernel; /** The value whose change forces a re-seed. */ seed: (f: FluidFrame) => number; warm: { /** How many silent solves to run. */ steps: number; jacobiIters: number; /** Simulated seconds per warm step. */ dt: number; /** Where the warm clock starts. Randomize it so two instances differ. */ startTime: () => number; /** The full values for a warm step at simulated time `t`. `t = 0` is the seed write. */ values: (t: number, f: FluidFrame) => TParams; }; }): FluidInitPart; //# sourceMappingURL=fluids.d.ts.map