sloshgen accepts a JSON recipe and produces a self-contained .slosh.json
asset. All numbers must be finite. Unknown recipe fields fail rather than being
silently ignored. Input errors are FormatException with the field name; CLI
usage errors exit 64 and generation errors exit 1. Failed generation preserves
an existing output. SVG and recipe files are limited to 1 MiB each.
Required fields:
| Field | Contract |
|---|---|
version |
Integer 1 |
shape |
Exactly one of polygon, pathData, or svg; see below |
pivot |
[x,y], in source viewBox coordinates |
duration |
Seconds, 0.01–60 |
fill |
0–1, fraction of the enclosed area |
loop |
Boolean; requests validated seamless repetition |
rotation |
2–1000 keys, increasing times spanning exactly 0 to duration |
layers |
1–4 ordered layer objects with unique IDs |
Optional liquid parameters: damping (ratio, default 0.8, 0.001–10),
naturalFrequency (Hz, default 2, 0.05–20), response (default 0.5, 0–10),
and quality (object, defaults below). A rotation key has time in seconds,
angle in clockwise radians (−1000 to 1000, never wrapped), and optional
easing: linear (default) or smooth (cubic smoothstep). Easing belongs to the
segment starting at that key. At interior keys the following segment supplies
derivatives; the last key uses the preceding segment's terminal derivatives.
Each layer requires an id of 1–64 characters. Optional values:
| Field | Default | Range / units |
|---|---|---|
color |
0xff38bdf8 | Unsigned integer ARGB, 0–4294967295 (JSON uses decimal) |
opacity |
1 | 0–1, multiplied by color alpha |
amplitude |
0.015 | 0–0.25 normalized distance |
frequency |
1 | 0–32 cycles across swept station interval |
speed |
1 | −32 to 32 temporal cycles/second |
phase |
0 | Radians, absolute value <= 1e6 |
A loop with decorative waves requires speed * duration to be an integer for
each nonzero-amplitude layer. Use different phase, frequency and amplitude for
layered depth. Each layer retains the same nominal fill amount.
- Polygon:
{"polygon":[[0,0],[100,0],[100,100],[0,100]],"viewBox":[0,0,100,100]}. Polygon lists implicitly close; repeating the first point at the end is also accepted. SVG contours require explicitZ. - Raw path:
{"pathData":"M0 0 H100 V100 H0 Z","viewBox":[0,0,100,100]}. - SVG file:
{"svg":"../shapes/pathverse.svg","clipPathId":"clippath"}. Relative paths resolve against the recipe file.pathIdcan select a path by its own ID.clipPathIdselects paths below that clipPath. Exactly one path must result. An explicit recipe viewBox overrides the SVG viewBox.
Supported SVG commands: M/L/H/V/C/S/Q/T/Z, absolute and relative, repeated arguments, and implicit line segments following M. Arcs, multiple subpaths, holes, nested SVG coordinate systems, objectBoundingBox clips, and transforms on the selected path or its ancestors are rejected. CSS transforms are rejected. This is contour extraction, not SVG scene rendering: fills, strokes, gradients, and unrelated painted paths are not imported. A single contour inside a clipPath is selectable, but arbitrary scene clipping/compositing is not applied.
Curves flatten within geometry tolerance. Contours must be simple and have
positive area after normalization. Up to 4096 normalized vertices are accepted.
Coordinates are bounded to ±1e6; normalized swept radius is limited to 100.
Normalization subtracts the viewBox origin and divides both axes by its larger
dimension, preserving aspect ratio. The pivot follows the same transform.
| Field | Default | Accepted range / units |
|---|---|---|
stations |
32 | Integer 8–256 |
hz |
240 | Integer 30–960; duration * hz <= 20000 |
geometry |
0.0005 | 1e-8–0.05 normalized distance |
surface |
0.002 | 1e-8–0.1 normalized displacement |
area |
0.005 | 1e-8–0.1 absolute fraction of container area |
rotation |
0.001 | 1e-8–0.1 radians |
seamPosition |
0.002 | 1e-8–0.1 normalized distance |
seamVelocity |
0.02 | 1e-8–1 normalized distance/second |
seamRotationVelocity |
0.01 | 1e-8–1 radians/second |
warmup |
20 | Integer 1–100 repeated cycles |
maxBytes |
2097152 | Integer 128–16777216 encoded UTF-8 bytes |
All failures are explicit, including nonconvergent loops and unattainable
spatial, area, or output budgets. Lower tolerances may increase file size and
generation time. Increasing keyframes cannot repair insufficient spatial
resolution. Validation samples the requested dense grid and independently
integrated midpoints at twice hz, plus authored keyframe times. Reported
maximum errors are sampled observations, not continuous mathematical bounds.
Required version-1 fields:
| Field | Representation |
|---|---|
version, model |
1, "surface-modes-1" |
duration, fill |
Same units/ranges as recipe |
loopSafe |
Boolean, set only after generation seam validation |
polygon |
3–4096 normalized [x,y] vertices, implicitly closed |
pivot |
Normalized [x,y] |
bounds |
[left,top,width,height], enclosing the whole swept vessel |
stations |
8–256 strictly increasing normalized x coordinates covering bounds |
layers |
1–4 objects containing id, decimal ARGB color, opacity |
frames |
2–10000 objects containing time, angle, surfaces |
Frame times strictly increase and include 0 and duration. surfaces contains
one y-coordinate array per layer, with exactly one value per station. For fill
0 or 1 every layer's surface array is empty; the player draws nothing or the
whole vessel. The player enforces encoded size before decoding, validates all
required fields, finite coordinates, counts, simple geometry and swept bounds,
and constructs immutable data. It rejects unknown versions/models.
At time t, interpolate unwrapped angle and corresponding y values linearly between neighboring timestamps. Draw piecewise-linear surfaces in the fixed presentation frame (x right, y down), closing below swept bounds, clipped to the vessel rotated around its pivot. Scale all geometry uniformly. Keep container and surface on the same clock. Unrelated ancestor rotation rotates the entire presentation and does not change the baked gravity direction.
The asset contains no source paths, timestamps or machine metadata. Generation uses stable JSON field ordering and deterministic integration. Canonical bytes are covered by repeated-generation tests. Generator/player interoperability is covered by checked-in assets and dense numeric references.
Two damped modes use frequencies omega and 1.6 * omega, where
omega = 2*pi*naturalFrequency. For each rotating vertex offset (x,y),
wall horizontal velocity is -angularVelocity*y and acceleration is
-angularAcceleration*y-angularVelocity^2*x. The bounded drive is
clamp(0.6*velocity+0.04*acceleration,-5,5). Average it for mode 0 and project
onto sin(pi*x/radius) for mode 1. Modal acceleration is
0.06*response*drive*omega^2 - 2*damping*omega*v - omega^2*q.
Semi-implicit integration substeps are bounded by both the validation cadence
and omega*20*max(1,damping). The surface combines a linear tilt mode, a sine
mode, and each layer's decorative sine wave. A bracketed height solve corrects
clipped area to a tighter internal tolerance before keyframe compression.
The model is stylized. It does not conserve separate trapped pools, simulate spilling or droplets, or reproduce full centrifugal fluid dynamics. Translation, animated scale, live gesture physics, and true 3D are outside version 1.
Version 2 is the opt-in contour format included in the initial package release. The current development status is in the implementation checkpoint. The input controls below are implemented; complete example acceptance is tracked separately.
Common shape, viewBox, pivot, duration, rotation, fill, layers and wave-response
fields retain their version-1 conventions. policy is required: surface
uses a pool, droplet uses rounded bodies, and hybrid permits both. emission
requires hybrid; surface rejects droplet controls and explicit seeds.
Unknown fields fail. Old experimental parcels, maxParcels, grid/density
controls, contactDamping and transitions are retired and rejected.
| Setting | Default | Meaning / bounds |
|---|---|---|
wall.friction |
0.5 | Artistic tangential entrainment, 0..1 |
droplet.adhesion |
0.25 | Wall retention, 0..1 |
droplet.viscosity |
0.2 | Deformation damping, 0..1 |
droplet.cohesion |
0.5 | Resistance to stretch/split, 0..1 |
droplet.gravity |
1 | Normalized distance/s² toward positive y, 0..10 |
emission.enabled |
false | Hybrid only |
emission.side |
auto | Presentation left/right or automatic contact |
emission.threshold |
0.5 | Minimum abs(angularVelocity) * friction, 0..20 /s |
emission.cooldown |
0.25 | Seconds between eligible pickups, 0.05..10 |
emission.dropShare |
0.02 | Fraction of initial total liquid per pickup, >0..0.25 |
emission.minDropShare |
0.005 | Minimum fraction of initial total, >0..dropShare |
emission.styleId |
droplet default | Optional valid named style |
quality.maxDroplets |
8 | Active attached/free bodies, 1..32 |
quality.contourPoints |
32 | Samples per simple loop, 16..128 |
quality.relativeArea |
0.05 | Approximate total visible area error, 0.001..0.1 |
Use inherited bounded quality.hz, duration, layers and byte controls. Cap total frames at 10000, serialized coordinates at 2 million, and active vertices at 4096. Named styles are bounded to 32, with 1..64 character IDs. Keep split/merge/event history bounded to 256 events per bake. These limits bound the product of contours, layers and samples. Failed CLI generation preserves an existing output.
Rotation is unwrapped radians, positive clockwise in screen coordinates. Negative
angular velocity is counterclockwise. Pickup left/right is relative to the
presentation pivot; all sides require a wetted, upward-moving wall contact.
Clockwise motion lifts the left wall; counterclockwise lifts the right wall.
An incompatible explicit side suppresses pickup instead of launching a drop.
Zero friction or stationary motion produces no wall pickup.
A picked-up bead begins submerged, clings to the inner wall and travels with it.
Friction controls tangential carry/slip; adhesion controls retention against
gravity. When the inward gravity load exceeds retention, the bead releases with
its carried velocity and falls back into the pool while rotation continues.
The emission name is retained for this pickup configuration. Diagnostics record
pickup, release, absorption and suppression; contour split/merge events also
represent visible separation from or reconnection with the pool.
Version 2 optionally accepts translation alongside rotation:
"translation": [
{"time": 0, "offset": [0, 0], "easing": "smooth"},
{"time": 0.12, "offset": [0.1, 0.035], "easing": "smooth"},
{"time": 3, "offset": [0, 0]}
]Supply 2..1000 strictly increasing keys spanning zero to duration. Offsets are
finite source distances in presentation axes (x right, y down), normalized by
the viewBox scale. Translation applies after rotation about the source pivot.
Omitting the track means zero translation. Outgoing easing is linear (default)
or smooth cubic smoothstep, using the same convention as rotation.
Rapid changes in linear or angular velocity accumulate transient droplet strain. Sufficient stretch causes necking and breakup; contacting fragments recombine. Steady translation or rotation does not continuously add breakup strain. Integration stops at keys in either track, and a linear velocity discontinuity is counted once. Smooth tracks contribute finite velocity changes over each step. This is a bounded animation model, not a fluid solver.
Optional initialState.seeds contains 1..32 circles with a unique id, center: [x,y], optional velocity: [x,y] (default zero), positive share and optional
styleId. Shares sum to one within 1e-10. Centers are source coordinates,
normalized by the viewBox and transformed by the initial rotation. Velocity is
in source units/second in presentation space and is divided by the viewBox
scale. Radius follows the assigned area. Circles must fit and not overlap.
Default hybrid/surface initialization is settled pooled water; default droplet
initialization is a settled rounded body when it fits. Empty/full states reject
explicit seeds. Initial seed count must respect maxDroplets.
appearance.styles maps 1..64-character IDs to {color?, opacity?} with at most
32 entries. Input color accepts #RRGGBB or an unsigned ARGB integer. Opacity
is 0..1, default 1. appearance.poolStyle and appearance.dropletStyle name
optional defaults. emission.styleId and each seed's styleId can override the
droplet default. All references must exist. Omitted appearance inherits layers.
A split inherits parent style. A merged drop uses the larger contributor's style (equal amounts choose the lexically smaller stable body ID). Absorption transitions to the pool style. Styles represent presentation, not pigment mixing. Recoloring must preserve geometry, amounts and event timing.
The Flutter Slosh and SloshPainter accept styleColors and styleOpacities
maps in addition to the existing layer colors/opacities. Color precedence:
runtime style override, baked style color, runtime layer override, baked layer
color. Resolved style opacity multiplies resolved layer opacity.
Assets use version: 2, model: hybrid-contours-1, policy, state
(empty, full, liquid), and common vessel/rotation/appearance metadata.
bodies records IDs, amounts, parent IDs and model history. segments covers
zero through duration without gaps; each segment fixes its contour layout and
stores at least two ordered frames including its endpoints. A layout contains
id, body, layer, hole, parent, points, optional styleId, and optional
endStyleId. Each frame has time, angle, interleaved x/y coordinates rows
and optional normalized offset: [x,y] (default zero), plus optional per-contour
styleMix values in 0..1. Contours already include the world offset. The player
translates the shell and clip once, without translating liquid coordinates a
second time. Empty/full states use the same pose without contour payload.
Fixed bounds enclose the swept translated vessel and prevent fit-scale jitter.
Offset scalars count toward payload budgets. Motion keys survive compression;
loop checks include translated endpoint position and velocity. Holes share their exterior's
styles and mix. Missing mix is zero; nonzero mix requires an end style.
Adjacent segments contain outgoing and incoming geometry at the same timestamp. Exact boundary sampling selects incoming geometry; duration selects the last frame. No cross-segment point interpolation occurs. Optional style transitions are sampled on the same geometry, so reverse seeks need no event replay. Missing style metadata preserves existing contour-fixture layer appearance.
Generation preserves scalar liquid amounts; visible area has an approximate relative total-area budget. Optional emissions that cannot fit or exceed the active cap are suppressed before debit and reported. Existing droplets are not deleted to meet a budget. Unsupported geometry, mandatory event or output budget failures reject generation, preserving an existing output file. A requested loop must pass endpoint checks; a one-shot recipe need not close.