Motion physics and transitions
Choose a motion relationship, distinguish duration curves from spring responses and preserve task state through interruption and reduced motion.
Two different motion contracts
A cubic-bezier easing curve and a damped spring answer different questions. A duration curve maps normalized time to progress; a spring describes the response to displacement and velocity. Do not label a finite easing token as a physics solver.
Material’s motion system includes scheme, spring and customization guidance. This library currently supplies duration/easing tokens for its components. The explorer below teaches an explicitly separate analytic model; it does not silently replace those component animations or implement native Material spring presets.
| Model | Inputs | Completion meaning |
|---|---|---|
| Duration and easing | Start/end values, duration and progress curve | A defined timeline reaches its endpoint |
| Spring response | Initial value/velocity, target, mass, stiffness and damping | A tolerance/horizon policy decides when the response is sufficiently settled |
| Task operation | Validated data/state transition | Actual completion or recovery, independent of presentation |
Schemes, style, speed and application
Start with a coherent default scheme. Customize the scheme when the product’s tasks justify a different character; override an individual element only with a reason and an interruption/reduced-motion contract. A collection of unrelated bounces is not a system.
The model fields here are ordinary mathematical parameters. They are not the published numerical Material token values, Android stiffness constants or a cross-platform unit conversion.
| Decision | Design responsibility | Local implementation boundary |
|---|---|---|
| Standard or expressive scheme | Choose coherent motion character for the task and audience | Record a deliberate scheme; do not infer one from a spring’s damping ratio |
| Style | Determine whether restrained or more noticeable response helps orientation | Overshoot is optional and may be unsuitable for reading or precise controls |
| Speed | Balance immediate response, continuity and understandable arrival | A faster response must not postpone cancellation or necessary feedback |
| Spatial application | Changing position, size or relationships | Preserve layout/focus and consider collision/overshoot |
| Effects application | Changing opacity, color or emphasis | Do not overshoot a bounded opacity or color representation without a valid mapping |
| Component override | An exception for a particular element/task | Record purpose and verify it still belongs to the system |
Spring mechanics and regimes
The unit-step model solves m·x″ + c·x′ + k·(x−1) = 0, starting at x=0 with the entered initial velocity. Its target is x=1. Positive mass and stiffness define the natural angular frequency √(k/m); damping ratio is c/(2√(km)). Time is in seconds and displacement is in model units.
Under-damping can cross the target and oscillate. Critical damping is the boundary between oscillatory and nonoscillatory behavior. Over-damping has two real decay rates and can approach the target slowly. Initial velocity matters in every regime; do not claim that damping alone prevents every target crossing. Zero damping retains ideal oscillation.
The implementation uses analytic under/critical/over-damped responses, ordinary floating-point arithmetic and a small relative tolerance around the critical boundary. That numerical branch is not a formal proof for arbitrary real-number inputs. The editor bounds keep this tool’s inputs and work finite.
| Parameter | Change to inspect | Constraint |
|---|---|---|
| Mass | The response changes with inertia relative to other parameters | Use a consistent unit system; it is not a CSS length |
| Stiffness | Restoring response grows relative to mass | A large value can require sufficient temporal sampling |
| Damping | Energy loss and response character change | Damping zero is not a settled animation |
| Initial velocity | An interrupted response may already be moving | Resetting it can create a discontinuity |
| Horizon | A bounded observation/playback interval | Ending playback does not prove convergence |
Captured response explorer
Capture a model, inspect its curve, regime, parameters and equivalent samples, and play the time cursor. Change draft settings while retaining the existing capture; a replay captures the new valid settings. Empty or out-of-bound fields cannot replace the captured model.
Stop retains the current time and curve. Reset moves the cursor to the captured start without rewriting draft parameters. Operating-system reduced motion displays the endpoint without cursor animation; enabling it during playback stops movement and preserves the response data.
The plot’s vertical scale adapts to the captured trajectory. It is not a component displacement in CSS pixels. Full parameters and all sampled values are available as JSON independently of the graphic. Displayed decimals are rounded for readability; the JSON retains the computed floating-point values.
This dimensionless unit-step model moves from x=0 toward x=1. Its parameters are model units, not Android spring tokens or a physical device calibration.
Mass 0.1–10; stiffness 1–1000; damping 0–100; initial velocity −5–5; preview 0.1–8 seconds. Draft changes do not rewrite the captured curve. An empty field is invalid.
Draft parameters are within the model bounds.
Reduced motion not requested: cursor playback available.
Response over 4 seconds; dashed line is target x=1. The vertical scale adapts to the captured samples. Equivalent values are below.
t=0.000 s; x=0.0000; velocity=0.0000 model units/s
- Captured mass
- 1
- Captured stiffness
- 100
- Captured damping
- 20
- Captured initial velocity
- 0
- Captured preview seconds
- 4
- Captured regime
- critical
- Damping ratio
- 1.0000
- Natural angular frequency
- 10.0000 rad/s
- Endpoint and velocity
- x=1.0000; v=0.0000
Positive damping approaches the target, but this chosen horizon may end while displacement or velocity remains.
| Seconds | Position | Velocity |
|---|---|---|
| 0.000 | 0.000000 | 0.000000 |
| 1.000 | 0.999501 | 0.004540 |
| 2.000 | 1.000000 | 0.000000 |
| 3.000 | 1.000000 | 0.000000 |
| 4.000 | 1.000000 | 0.000000 |
Full captured parameters and 241 samples
{
"parameters": {
"mass": 1,
"stiffness": 100,
"damping": 20,
"initialVelocity": 0,
"duration": 4
},
"samples": [
{
"time": 0,
"position": 0,
"velocity": 0
},
{
"time": 0.016666666666666666,
"position": 0.012437987627616942,
"velocity": 1.410802874817691
},
{
"time": 0.03333333333333333,
"position": 0.04462491923494771,
"velocity": 2.38843770191263
},
{
"time": 0.05,
"position": 0.09020401043104986,
"velocity": 3.032653298563167
},
{
"time": 0.06666666666666667,
"position": 0.1443048016123467,
"velocity": 3.4227807935506123
},
{
"time": 0.08333333333333333,
"position": 0.20323661773702328,
"velocity": 3.621651737558985
},
{
"time": 0.1,
"position": 0.26424111765711533,
"velocity": 3.6787944117144233
},
{
"time": 0.11666666666666667,
"position": 0.3252930148517049,
"velocity": 3.6330376123369743
},
{
"time": 0.13333333333333333,
"position": 0.38494001106330433,
"velocity": 3.514628508209689
},
{
"time": 0.15,
"position": 0.44217459962892547,
"velocity": 3.346952402226447
},
{
"time": 0.16666666666666666,
"position": 0.49633172576650164,
"velocity": 3.147926713959364
},
{
"time": 0.18333333333333332,
"position": 0.547007386107534,
"velocity": 2.931128678127721
},
{
"time": 0.2,
"position": 0.5939941502901619,
"velocity": 2.706705664732254
},
{
"time": 0.21666666666666667,
"position": 0.637230327356489,
"velocity": 2.482108286508234
},
{
"time": 0.23333333333333334,
"position": 0.6767601071186498,
"velocity": 2.2626792501694513
},
{
"time": 0.25,
"position": 0.7127025048163542,
"velocity": 2.05212496559747
},
{
"time": 0.26666666666666666,
"position": 0.7452273455163944,
"velocity": 1.852892032608041
},
{
"time": 0.2833333333333333,
"position": 0.7745368587040187,
"velocity": 1.666466696535514
},
{
"time": 0.3,
"position": 0.8008517265285442,
"velocity": 1.4936120510359183
},
{
"time": 0.31666666666666665,
"position": 0.8244006520446816,
"velocity": 1.334555044460419
},
{
"time": 0.3333333333333333,
"position": 0.8454126954952396,
"velocity": 1.18913311157508
},
{
"time": 0.35,
"position": 0.8641117745995668,
"velocity": 1.0569084197811476
},
{
"time": 0.36666666666666664,
"position": 0.8807128450362988,
"velocity": 0.9372562175719379
},
{
"time": 0.38333333333333336,
"position": 0.8954193748557834,
"velocity": 0.8294325442472353
},
{
"time": 0.4,
"position": 0.9084218055563291,
"velocity": 0.7326255555493671
},
{
"time": 0.4166666666666667,
"position": 0.9198967564051186,
"velocity": 0.6459938999587215
},
{
"time": 0.43333333333333335,
"position": 0.9300067800696482,
"velocity": 0.5686949119341081
},
{
"time": 0.45,
"position": 0.9389005190396673,
"velocity": 0.49990484422090375
},
{
"time": 0.4666666666666667,
"position": 0.9467131455415272,
"velocity": 0.43883291906977634
},
{
"time": 0.48333333333333334,
"position": 0.9535669942158791,
"velocity": 0.38473061935414504
},
{
"time": 0.5,
"position": 0.9595723180054871,
"velocity": 0.33689734995427334
},
{
"time": 0.5166666666666667,
"position": 0.9648281145122877,
"velocity": 0.2946833648970491
},
{
"time": 0.5333333333333333,
"position": 0.9694229833724008,
"velocity": 0.25749066633767687
},
{
"time": 0.55,
"position": 0.9734359856499836,
"velocity": 0.22477242911552367
},
{
"time": 0.5666666666666667,
"position": 0.9769374844235683,
"velocity": 0.19603138239966977
},
{
"time": 0.5833333333333334,
"position": 0.9799899520854091,
"velocity": 0.17081748219772755
},
{
"time": 0.6,
"position": 0.9826487347633355,
"velocity": 0.14872513059998152
},
{
"time": 0.6166666666666667,
"position": 0.9849627680037535,
"velocity": 0.12939013578165567
},
{
"time": 0.6333333333333333,
"position": 0.9869752406646145,
"velocity": 0.11248655789651067
},
{
"time": 0.65,
"position": 0.9887242060526682,
"velocity": 0.0977235475435422
},
{
"time": 0.6666666666666666,
"position": 0.9902431408563948,
"velocity": 0.08484225342265393
},
{
"time": 0.6833333333333333,
"position": 0.9915614535000543,
"velocity": 0.073612852446335
},
{
"time": 0.7,
"position": 0.9927049442755639,
"velocity": 0.06383173758881613
},
{
"time": 0.7166666666666667,
"position": 0.9936962200773563,
"velocity": 0.05531888503544485
},
{
"time": 0.7333333333333333,
"position": 0.9945550668344385,
"velocity": 0.04791541185694123
},
{
"time": 0.75,
"position": 0.9952987828537434,
"velocity": 0.041481327761087525
},
{
"time": 0.7666666666666667,
"position": 0.9959424762990092,
"velocity": 0.03589347889337957
},
{
"time": 0.7833333333333333,
"position": 0.9964993299607008,
"velocity": 0.031043677706992633
},
{
"time": 0.8,
"position": 0.9969808363488774,
"velocity": 0.026837010232200947
},
{
"time": 0.8166666666666667,
"position": 0.9973970059808868,
"velocity": 0.02319031035209929
},
{
"time": 0.8333333333333334,
"position": 0.9977565515534179,
"velocity": 0.020030789701626174
},
{
"time": 0.85,
"position": 0.9980670504943989,
"velocity": 0.017294811365904755
},
{
"time": 0.8666666666666667,
"position": 0.9983350881923788,
"velocity": 0.014926795516603523
},
{
"time": 0.8833333333333333,
"position": 0.9985663840051096,
"velocity": 0.012878245377829906
},
{
"time": 0.9,
"position": 0.9987659019591332,
"velocity": 0.011106882367801161
},
{
"time": 0.9166666666666666,
"position": 0.9989379478710443,
"velocity": 0.009575879851239652
},
{
"time": 0.9333333333333333,
"position": 0.9990862544505146,
"velocity": 0.00825318560825575
},
{
"time": 0.95,
"position": 0.9992140557861792,
"velocity": 0.007110923839331556
},
{
"time": 0.9666666666666667,
"position": 0.999324152468538,
"velocity": 0.006124868253874099
},
{
"time": 0.9833333333333333,
"position": 0.999418968469177,
"velocity": 0.005273978510547581
},
{
"time": 1,
"position": 0.9995006007726127,
"velocity": 0.0045399929762484845
},
{
"time": 1.0166666666666666,
"position": 0.9995708626454496,
"velocity": 0.003907071436951528
},
{
"time": 1.0333333333333334,
"position": 0.9996313213267097,
"velocity": 0.003361482021176028
},
{
"time": 1.05,
"position": 0.999683330832478,
"velocity": 0.002891327181723452
},
{
"time": 1.0666666666666667,
"position": 0.9997280604866657,
"velocity": 0.0024863041219132815
},
{
"time": 1.0833333333333333,
"position": 0.9997665197169967,
"velocity": 0.002137495548621671
},
{
"time": 1.1,
"position": 0.999799579590517,
"velocity": 0.0018371870869270225
},
{
"time": 1.1166666666666667,
"position": 0.9998279915053286,
"velocity": 0.0015787081017779704
},
{
"time": 1.1333333333333333,
"position": 0.999852403404142,
"velocity": 0.0013562930430185496
},
{
"time": 1.15,
"position": 0.9998733738300171,
"velocity": 0.0011649607638425318
},
{
"time": 1.1666666666666667,
"position": 0.9998913841046995,
"velocity": 0.001000409561978433
},
{
"time": 1.1833333333333333,
"position": 0.9999068488747217,
"velocity": 0.0008589259603580048
},
{
"time": 1.2,
"position": 0.9999201252394068,
"velocity": 0.0007373054823993851
},
{
"time": 1.2166666666666666,
"position": 0.9999315206476326,
"velocity": 0.000632783888964304
},
{
"time": 1.2333333333333334,
"position": 0.999941299726267,
"velocity": 0.0005429775320301175
},
{
"time": 1.25,
"position": 0.9999496901821769,
"velocity": 0.0004658316465098339
},
{
"time": 1.2666666666666666,
"position": 0.9999568879013293,
"velocity": 0.00039957554865489625
},
{
"time": 1.2833333333333334,
"position": 0.9999630613524066,
"velocity": 0.0003426838391185822
},
{
"time": 1.3,
"position": 0.9999683553883023,
"velocity": 0.00029384282290753703
},
{
"time": 1.3166666666666667,
"position": 0.9999728945265841,
"velocity": 0.00025192145880690317
},
{
"time": 1.3333333333333333,
"position": 0.9999767857793102,
"velocity": 0.000215946238975015
},
{
"time": 1.35,
"position": 0.9999801210932474,
"velocity": 0.00018507947666185137
},
{
"time": 1.3666666666666667,
"position": 0.9999829794534211,
"velocity": 0.00015860054766690405
},
{
"time": 1.3833333333333333,
"position": 0.9999854286958548,
"velocity": 0.000135889690342527
},
{
"time": 1.4,
"position": 0.9999875270692135,
"velocity": 0.00011641402067449951
},
{
"time": 1.4166666666666667,
"position": 0.9999893245797227,
"velocity": 0.00009971546412940662
},
{
"time": 1.4333333333333333,
"position": 0.9999908641491025,
"velocity": 0.00008540034534599335
},
{
"time": 1.45,
"position": 0.9999921826112302,
"velocity": 0.00007313041107234377
},
{
"time": 1.4666666666666666,
"position": 0.9999933115697568,
"velocity": 0.0000626150916387356
},
{
"time": 1.4833333333333334,
"position": 0.9999942781358823,
"velocity": 0.000053604832260615056
},
{
"time": 1.5,
"position": 0.999995105562872,
"velocity": 0.00004588534807527387
},
{
"time": 1.5166666666666666,
"position": 0.9999958137916302,
"velocity": 0.000039272676459141435
},
{
"time": 1.5333333333333334,
"position": 0.9999964199196866,
"velocity": 0.00003360891722789959
},
{
"time": 1.55,
"position": 0.9999969386042517,
"velocity": 0.000028758566120547662
},
{
"time": 1.5666666666666667,
"position": 0.9999973824085318,
"velocity": 0.000024605359801931022
},
{
"time": 1.5833333333333333,
"position": 0.9999977620992254,
"velocity": 0.000021049561740806517
},
{
"time": 1.6,
"position": 0.9999980869020297,
"velocity": 0.00001800562795508146
},
{
"time": 1.6166666666666667,
"position": 0.9999983647210354,
"velocity": 0.00001540019995716804
},
{
"time": 1.6333333333333333,
"position": 0.9999986023270787,
"velocity": 0.000013170379450449392
},
{
"time": 1.65,
"position": 0.9999988055194091,
"velocity": 0.000011262245570952535
},
{
"time": 1.6666666666666667,
"position": 0.9999989792644283,
"velocity": 0.000009629580865698555
},
{
"time": 1.6833333333333333,
"position": 0.9999991278147283,
"velocity": 0.000008232776863381885
},
{
"time": 1.7,
"position": 0.9999992548112107,
"velocity": 0.000007037894121934784
},
{
"time": 1.7166666666666666,
"position": 0.9999993633706722,
"velocity": 0.000006015855116285164
},
{
"time": 1.7333333333333334,
"position": 0.9999994561609168,
"velocity": 0.000005141751332180654
},
{
"time": 1.75,
"position": 0.9999995354651562,
"velocity": 0.000004394248522551968
},
{
"time": 1.7666666666666666,
"position": 0.9999996032372193,
"velocity": 0.0000037550763172437145
},
{
"time": 1.7833333333333334,
"position": 0.9999996611488745,
"velocity": 0.000003208590303381589
},
{
"time": 1.8,
"position": 0.9999997106303848,
"velocity": 0.000002741396354048273
},
{
"time": 1.8166666666666667,
"position": 0.9999997529052591,
"velocity": 0.000002342028413590876
},
{
"time": 1.8333333333333333,
"position": 0.9999997890200246,
"velocity": 0.000002000672180190056
},
{
"time": 1.85,
"position": 0.9999998198697316,
"velocity": 0.00000170892818746456
},
{
"time": 1.8666666666666667,
"position": 0.9999998462197977,
"velocity": 0.000001459608700378282
},
{
"time": 1.8833333333333333,
"position": 0.9999998687247154,
"velocity": 0.0000012465636268826435
},
{
"time": 1.9,
"position": 0.9999998879440712,
"velocity": 0.000001064531323132081
},
{
"time": 1.9166666666666667,
"position": 0.99999990435626,
"velocity": 9.090107519260264e-7
},
{
"time": 1.9333333333333333,
"position": 0.9999999183702255,
"velocity": 7.761519543582365e-7
},
{
"time": 1.95,
"position": 0.9999999303355097,
"velocity": 6.626622248015388e-7
},
{
"time": 1.9666666666666666,
"position": 0.9999999405508535,
"velocity": 5.657257490790038e-7
},
{
"time": 1.9833333333333334,
"position": 0.9999999492715563,
"velocity": 4.82934783386067e-7
},
{
"time": 2,
"position": 0.9999999567157739,
"velocity": 4.122307244877116e-7
},
{
"time": 2.0166666666666666,
"position": 0.9999999630699055,
"velocity": 3.518536561732436e-7
},
{
"time": 2.033333333333333,
"position": 0.999999968493203,
"velocity": 3.0029915829722897e-7
},
{
"time": 2.05,
"position": 0.9999999731217134,
"velocity": 2.5628133760928223e-7
},
{
"time": 2.066666666666667,
"position": 0.9999999770716497,
"velocity": 2.1870118797463882e-7
},
{
"time": 2.0833333333333335,
"position": 0.9999999804422748,
"velocity": 1.8661951495201458e-7
},
{
"time": 2.1,
"position": 0.999999983318367,
"velocity": 1.5923376898615005e-7
},
{
"time": 2.1166666666666667,
"position": 0.9999999857723276,
"velocity": 1.358582252372474e-7
},
{
"time": 2.1333333333333333,
"position": 0.999999987865983,
"velocity": 1.1590702849955578e-7
},
{
"time": 2.15,
"position": 0.9999999896521254,
"velocity": 9.88796906410248e-8
},
{
"time": 2.1666666666666665,
"position": 0.9999999911758296,
"velocity": 8.434868714543002e-8
},
{
"time": 2.183333333333333,
"position": 0.9999999924755782,
"velocity": 7.194885005010016e-8
},
{
"time": 2.2,
"position": 0.9999999935842234,
"velocity": 6.136829804311634e-8
},
{
"time": 2.216666666666667,
"position": 0.9999999945298085,
"velocity": 5.2340681741601525e-8
},
{
"time": 2.2333333333333334,
"position": 0.9999999953362705,
"velocity": 4.463855410064601e-8
},
{
"time": 2.25,
"position": 0.9999999960240399,
"velocity": 3.806770325884043e-8
},
{
"time": 2.2666666666666666,
"position": 0.999999996610553,
"velocity": 3.246230856247972e-8
},
{
"time": 2.283333333333333,
"position": 0.9999999971106902,
"velocity": 2.768080058520724e-8
},
{
"time": 2.3,
"position": 0.9999999975371489,
"velocity": 2.3602323152914347e-8
},
{
"time": 2.316666666666667,
"position": 0.9999999979007641,
"velocity": 2.012371010675897e-8
},
{
"time": 2.3333333333333335,
"position": 0.9999999982107802,
"velocity": 1.715690214339223e-8
},
{
"time": 2.35,
"position": 0.9999999984750846,
"velocity": 1.4626739863833289e-8
},
{
"time": 2.3666666666666667,
"position": 0.9999999987004059,
"velocity": 1.246907840073669e-8
},
{
"time": 2.3833333333333333,
"position": 0.9999999988924844,
"velocity": 1.0629176900849464e-8
},
{
"time": 2.4,
"position": 0.9999999990562164,
"velocity": 9.060322906269834e-9
},
{
"time": 2.4166666666666665,
"position": 0.9999999991957784,
"velocity": 7.722657468444174e-9
},
{
"time": 2.433333333333333,
"position": 0.9999999993147328,
"velocity": 6.582171783004745e-9
},
{
"time": 2.45,
"position": 0.9999999994161176,
"velocity": 5.609850371831605e-9
},
{
"time": 2.466666666666667,
"position": 0.9999999995025238,
"velocity": 4.780939464154334e-9
},
{
"time": 2.4833333333333334,
"position": 0.9999999995761611,
"velocity": 4.074322329378578e-9
},
{
"time": 2.5,
"position": 0.9999999996389135,
"velocity": 3.4719859662410053e-9
},
{
"time": 2.5166666666666666,
"position": 0.9999999996923875,
"velocity": 2.958565820629696e-9
},
{
"time": 2.533333333333333,
"position": 0.9999999997379532,
"velocity": 2.5209571434136007e-9
},
{
"time": 2.55,
"position": 0.9999999997767782,
"velocity": 2.147983257389505e-9
},
{
"time": 2.566666666666667,
"position": 0.9999999998098584,
"velocity": 1.830112419631953e-9
},
{
"time": 2.5833333333333335,
"position": 0.9999999998380427,
"velocity": 1.559216176919723e-9
},
{
"time": 2.6,
"position": 0.9999999998620546,
"velocity": 1.3283631472964645e-9
},
{
"time": 2.6166666666666667,
"position": 0.999999999882511,
"velocity": 1.131643045693126e-9
},
{
"time": 2.6333333333333333,
"position": 0.9999999998999375,
"velocity": 9.640165277073149e-10
},
{
"time": 2.65,
"position": 0.9999999999147825,
"velocity": 8.211870717612837e-10
},
{
"time": 2.6666666666666665,
"position": 0.9999999999274277,
"velocity": 6.994916719118143e-10
},
{
"time": 2.683333333333333,
"position": 0.9999999999381989,
"velocity": 5.958075852176972e-10
},
{
"time": 2.7,
"position": 0.9999999999473732,
"velocity": 5.074727804655524e-10
},
{
"time": 2.716666666666667,
"position": 0.9999999999551872,
"velocity": 4.3221807920119237e-10
},
{
"time": 2.7333333333333334,
"position": 0.9999999999618423,
"velocity": 3.681092739547595e-10
},
{
"time": 2.75,
"position": 0.9999999999675102,
"velocity": 3.1349775958719774e-10
},
{
"time": 2.7666666666666666,
"position": 0.9999999999723371,
"velocity": 2.6697842806825847e-10
},
{
"time": 2.783333333333333,
"position": 0.9999999999764477,
"velocity": 2.2735376006064614e-10
},
{
"time": 2.8,
"position": 0.9999999999799483,
"velocity": 1.9360320299432567e-10
},
{
"time": 2.816666666666667,
"position": 0.999999999982929,
"velocity": 1.6485705876983542e-10
},
{
"time": 2.8333333333333335,
"position": 0.9999999999854672,
"velocity": 1.403742181629597e-10
},
{
"time": 2.85,
"position": 0.9999999999876283,
"velocity": 1.1952317626381702e-10
},
{
"time": 2.8666666666666667,
"position": 0.9999999999894684,
"velocity": 1.0176584630532461e-10
},
{
"time": 2.8833333333333333,
"position": 0.9999999999910352,
"velocity": 8.664376009871404e-11
},
{
"time": 2.9,
"position": 0.999999999992369,
"velocity": 7.376630377393076e-11
},
{
"time": 2.9166666666666665,
"position": 0.9999999999935046,
"velocity": 6.28006891381515e-11
},
{
"time": 2.933333333333333,
"position": 0.9999999999944714,
"velocity": 5.346340501264443e-11
},
{
"time": 2.95,
"position": 0.9999999999952944,
"velocity": 4.5512930494160694e-11
},
{
"time": 2.966666666666667,
"position": 0.9999999999959951,
"velocity": 3.874352415760013e-11
},
{
"time": 2.9833333333333334,
"position": 0.9999999999965915,
"velocity": 3.297993057949833e-11
},
{
"time": 3,
"position": 0.9999999999970991,
"velocity": 2.8072868906520524e-11
},
{
"time": 3.0166666666666666,
"position": 0.9999999999975313,
"velocity": 2.389518810847863e-11
},
{
"time": 3.033333333333333,
"position": 0.9999999999978991,
"velocity": 2.0338590544145965e-11
},
{
"time": 3.05,
"position": 0.9999999999982122,
"velocity": 1.7310839959529804e-11
},
{
"time": 3.066666666666667,
"position": 0.9999999999984787,
"velocity": 1.4733382398675727e-11
},
{
"time": 3.0833333333333335,
"position": 0.9999999999987054,
"velocity": 1.253931904927307e-11
},
{
"time": 3.1,
"position": 0.9999999999988984,
"velocity": 1.0671679036256928e-11
},
{
"time": 3.1166666666666667,
"position": 0.9999999999990626,
"velocity": 9.081947844100996e-12
},
{
"time": 3.1333333333333333,
"position": 0.9999999999992024,
"velocity": 7.728813587010423e-12
},
{
"time": 3.15,
"position": 0.9999999999993214,
"velocity": 6.5770989216846905e-12
},
{
"time": 3.1666666666666665,
"position": 0.9999999999994227,
"velocity": 5.596851151326639e-12
},
{
"time": 3.183333333333333,
"position": 0.9999999999995088,
"velocity": 4.7625671229337815e-12
},
{
"time": 3.2,
"position": 0.9999999999995821,
"velocity": 4.052532975710136e-12
},
{
"time": 3.216666666666667,
"position": 0.9999999999996445,
"velocity": 3.4482617446190228e-12
},
{
"time": 3.2333333333333334,
"position": 0.9999999999996976,
"velocity": 2.934014334689795e-12
},
{
"time": 3.25,
"position": 0.9999999999997427,
"velocity": 2.496391522690681e-12
},
{
"time": 3.2666666666666666,
"position": 0.9999999999997811,
"velocity": 2.123986467781465e-12
},
{
"time": 3.283333333333333,
"position": 0.9999999999998138,
"velocity": 1.8070887683251036e-12
},
{
"time": 3.3,
"position": 0.9999999999998416,
"velocity": 1.5374324278841213e-12
},
{
"time": 3.316666666666667,
"position": 0.9999999999998652,
"velocity": 1.3079812234250699e-12
},
{
"time": 3.3333333333333335,
"position": 0.9999999999998854,
"velocity": 1.112745931788333e-12
},
{
"time": 3.35,
"position": 0.9999999999999025,
"velocity": 9.466286911837297e-13
},
{
"time": 3.3666666666666667,
"position": 0.9999999999999171,
"velocity": 8.052904738483495e-13
},
{
"time": 3.3833333333333333,
"position": 0.9999999999999295,
"velocity": 6.850382419616285e-13
},
{
"time": 3.4,
"position": 0.99999999999994,
"velocity": 5.827288667242844e-13
},
{
"time": 3.4166666666666665,
"position": 0.9999999999999489,
"velocity": 4.956873231907199e-13
},
{
"time": 3.433333333333333,
"position": 0.9999999999999566,
"velocity": 4.2163704209865333e-13
},
{
"time": 3.45,
"position": 0.9999999999999631,
"velocity": 3.5864061402622656e-13
},
{
"time": 3.466666666666667,
"position": 0.9999999999999686,
"velocity": 3.0504930879209445e-13
},
{
"time": 3.4833333333333334,
"position": 0.9999999999999734,
"velocity": 2.594601009728445e-13
},
{
"time": 3.5,
"position": 0.9999999999999774,
"velocity": 2.2067908660514462e-13
},
{
"time": 3.5166666666666666,
"position": 0.9999999999999807,
"velocity": 1.8769034156193613e-13
},
{
"time": 3.533333333333333,
"position": 0.9999999999999836,
"velocity": 1.5962941299990137e-13
},
{
"time": 3.55,
"position": 0.999999999999986,
"velocity": 1.3576075529744831e-13
},
{
"time": 3.566666666666667,
"position": 0.9999999999999881,
"velocity": 1.1545852413032922e-13
},
{
"time": 3.5833333333333335,
"position": 0.9999999999999899,
"velocity": 9.819022940056385e-14
},
{
"time": 3.6,
"position": 0.9999999999999915,
"velocity": 8.350282188876851e-14
},
{
"time": 3.6166666666666667,
"position": 0.9999999999999927,
"velocity": 7.101085165334993e-14
},
{
"time": 3.6333333333333333,
"position": 0.9999999999999938,
"velocity": 6.038638998238894e-14
},
{
"time": 3.65,
"position": 0.9999999999999947,
"velocity": 5.1350452504284355e-14
},
{
"time": 3.6666666666666665,
"position": 0.9999999999999956,
"velocity": 4.366570006457927e-14
},
{
"time": 3.683333333333333,
"position": 0.9999999999999962,
"velocity": 3.713022718699053e-14
},
{
"time": 3.7,
"position": 0.9999999999999968,
"velocity": 3.157227621525304e-14
},
{
"time": 3.716666666666667,
"position": 0.9999999999999972,
"velocity": 2.6845739310623742e-14
},
{
"time": 3.7333333333333334,
"position": 0.9999999999999977,
"velocity": 2.2826330980928316e-14
},
{
"time": 3.75,
"position": 0.999999999999998,
"velocity": 1.9408331271757006e-14
},
{
"time": 3.7666666666666666,
"position": 0.9999999999999983,
"velocity": 1.6501814610974977e-14
},
{
"time": 3.783333333333333,
"position": 0.9999999999999986,
"velocity": 1.403029194924417e-14
},
{
"time": 3.8,
"position": 0.9999999999999988,
"velocity": 1.1928704609782512e-14
},
{
"time": 3.816666666666667,
"position": 0.999999999999999,
"velocity": 1.0141717429473833e-14
},
{
"time": 3.8333333333333335,
"position": 0.9999999999999991,
"velocity": 8.622266578613423e-15
},
{
"time": 3.85,
"position": 0.9999999999999992,
"velocity": 7.330324090652841e-15
},
{
"time": 3.8666666666666667,
"position": 0.9999999999999993,
"velocity": 6.231846788835621e-15
},
{
"time": 3.8833333333333333,
"position": 0.9999999999999994,
"velocity": 5.297882110529085e-15
},
{
"time": 3.9,
"position": 0.9999999999999996,
"velocity": 4.503807427476157e-15
},
{
"time": 3.9166666666666665,
"position": 0.9999999999999996,
"velocity": 3.828682947647515e-15
},
{
"time": 3.933333333333333,
"position": 0.9999999999999997,
"velocity": 3.2547012525864414e-15
},
{
"time": 3.95,
"position": 0.9999999999999997,
"velocity": 2.7667190503366147e-15
},
{
"time": 3.966666666666667,
"position": 0.9999999999999998,
"velocity": 2.3518588739914028e-15
},
{
"time": 3.9833333333333334,
"position": 0.9999999999999998,
"velocity": 1.9991702855839287e-15
},
{
"time": 4,
"position": 0.9999999999999998,
"velocity": 1.6993417021166355e-15
}
]
}Initial captured model. Change settings and run to capture another response.
Playback inactive. The curve is sampled at up to 60 intervals per second; the cursor evaluates the captured analytic response at elapsed browser time. This plot does not animate a production component, preserve retargeted velocity or measure frame-rate performance.
Settling and duration selection
Choose a settling policy from the property and perceptual context: residual displacement, residual velocity and a maximum observation interval have different jobs. A timer reaching four seconds is not sufficient evidence that a spring settled. Measure the final values and make the stopping rule explicit.
For duration/easing transitions, use the committed token scale and a suitable entering, exiting or continuing curve. Distance, context and repetition affect what feels understandable; do not create one arbitrary duration for every overlay and navigation change. Use the existing Motion chapter’s actual token tables and replay lab.
- Keep the actual data transition independent of animation-end events.
- Avoid hiding useful results while an optional flourish finishes.
- Compare repeated actions, slow hardware, enlarged text and reduced motion.
- For a production spring renderer, define sampling/integration, completion tolerances and behavior when the tab is backgrounded.
- This explorer samples its curve at up to 60 intervals per second and evaluates the cursor at elapsed browser time; it does not certify a display frame rate.
Transition patterns and identity
Choose a pattern from the relationship rather than drawing every arrival from a screen edge. A decorative morph must not imply two records are the same object. Coordinate entering and exiting regions without leaving two competing primary tasks or a focusable hidden surface.
Navigation, Dialog/Drawer, Layout compositions and Skeleton examples supply working task/state patterns. Applying an animation still requires their existing semantics and focus contract; motion cannot repair an inaccessible interaction model.
| Pattern | Use | Preserve |
|---|---|---|
| Container transform | A clearly related object expands into a detailed context | Object identity, readable labels and a usable return path |
| Forward/backward | A task moves deeper into or returns through a hierarchy | Actual history/destination truth and focus restoration |
| Lateral | Sibling activities change within one level | Selected destination and shared context |
| Top-level | Independent primary destinations change | Consistent navigation, current location and relevant retained state |
| Enter/exit within bounds | A contained surface or item appears/disappears | Reading order and the associated control relationship |
| Enter/exit beyond bounds | A temporary surface originates outside the current region | Safe area, unobscured focus and reachable dismissal |
| Skeleton replacement | Known content structure waits for real data | Stable geometry and truthful loading/empty/error outcomes |
Interruption and retargeting
- Give the latest action authority. Cancel or supersede presentation work without duplicating the operation.
- A retargeted spring needs the current value and velocity in a consistent coordinate system if continuity is intended. Restarting from zero is a different behavior.
- When geometry changes, define how a model value maps to the new surface. A changing plot scale is not proof of a continuous pixel trajectory.
- Keep element identity stable while choosing new targets; do not attach a previous response to an unrelated record.
- Restore focus when a region disappears and keep cancellation reachable while presentation is active.
- The existing duration lab cancels/replays a visual transition. This spring explorer replaces a captured unit-step playback; it does not implement continuous component retargeting.
Reduced motion and accessibility
Keep the information and task outcome available without movement. Reduced motion is not a reason to remove the selected state, result or recovery operation. Avoid large-field zoom, parallax and compulsory oscillation around reading content.
Check the preference at the beginning and respond to changes while playing. A CSS media query cannot cancel an unrelated JavaScript frame loop by itself. Provide a reachable stop path for optional ongoing playback and avoid announcing every visual frame.
- The plot is decorative because model characteristics, samples and full JSON provide the equivalent information.
- Cursor readout updates without an automatic live channel; the status describes explicit start/stop/reset/end events.
- A preview horizon is bounded to eight seconds, and replay supersedes the old frame callback.
- No preference changes rewrite the captured source values or establish a physical performance claim.
Analytic model source
export interface SpringParameters {
mass:number;
stiffness:number;
damping:number;
initialVelocity:number;
duration:number;
}
export interface SpringSample {time:number;position:number;velocity:number}
export const initialSpring:SpringParameters={mass:1,stiffness:100,damping:20,initialVelocity:0,duration:4};
export function validateSpring(parameters:SpringParameters):string[]{
const errors:string[]=[];
if(!Number.isFinite(parameters.mass)||parameters.mass<.1||parameters.mass>10)errors.push('Mass must be 0.1–10.');
if(!Number.isFinite(parameters.stiffness)||parameters.stiffness<1||parameters.stiffness>1000)errors.push('Stiffness must be 1–1000.');
if(!Number.isFinite(parameters.damping)||parameters.damping<0||parameters.damping>100)errors.push('Damping must be 0–100.');
if(!Number.isFinite(parameters.initialVelocity)||Math.abs(parameters.initialVelocity)>5)errors.push('Initial velocity must be −5–5.');
if(!Number.isFinite(parameters.duration)||parameters.duration<.1||parameters.duration>8)errors.push('Preview duration must be 0.1–8 seconds.');
return errors;
}
export function springCharacteristics(parameters:SpringParameters){
const errors=validateSpring(parameters);if(errors.length)throw new RangeError(errors.join(' '));
const naturalFrequency=Math.sqrt(parameters.stiffness/parameters.mass);
const alpha=parameters.damping/(2*parameters.mass);
const discriminant=alpha*alpha-naturalFrequency*naturalFrequency;
const tolerance=1e-10*Math.max(1,alpha*alpha,naturalFrequency*naturalFrequency);
const regime=Math.abs(discriminant)<=tolerance?'critical':discriminant<0?'underdamped':'overdamped';
return {naturalFrequency,alpha,discriminant,regime,dampingRatio:alpha/naturalFrequency};
}
// Unit-step response of m*x'' + c*x' + k*(x-1) = 0, x(0)=0.
// Time is seconds, and the initial velocity is in model displacement units/second.
export function springResponse(parameters:SpringParameters,time:number):SpringSample{
const {naturalFrequency,alpha,discriminant,regime}=springCharacteristics(parameters);
if(!Number.isFinite(time)||time<0||time>parameters.duration)throw new RangeError('Time must be within the captured preview duration.');
const displacement=-1,initialVelocity=parameters.initialVelocity;
let offset:number,velocity:number;
if(regime==='critical'){
const coefficient=initialVelocity+alpha*displacement;
const decay=Math.exp(-alpha*time);
offset=decay*(displacement+coefficient*time);
velocity=decay*(coefficient-alpha*(displacement+coefficient*time));
}else if(regime==='underdamped'){
const frequency=Math.sqrt(-discriminant),coefficient=(initialVelocity+alpha*displacement)/frequency;
const cosine=Math.cos(frequency*time),sine=Math.sin(frequency*time),decay=Math.exp(-alpha*time);
offset=decay*(displacement*cosine+coefficient*sine);
velocity=decay*((-alpha*displacement+coefficient*frequency)*cosine+(-alpha*coefficient-displacement*frequency)*sine);
}else{
const root=Math.sqrt(discriminant),fast=-alpha-root;
// This equivalent slow root avoids subtracting nearly equal positive values.
const slow=-(naturalFrequency*naturalFrequency)/(alpha+root);
const first=(initialVelocity-slow*displacement)/(fast-slow),second=displacement-first;
offset=first*Math.exp(fast*time)+second*Math.exp(slow*time);
velocity=first*fast*Math.exp(fast*time)+second*slow*Math.exp(slow*time);
}
return {time,position:1+offset,velocity};
}
export function sampleSpring(parameters:SpringParameters):SpringSample[]{
const errors=validateSpring(parameters);if(errors.length)throw new RangeError(errors.join(' '));
const intervals=Math.floor(parameters.duration*60);
return Array.from({length:intervals+1},(_,index)=>springResponse(parameters,index===intervals?parameters.duration:index/intervals*parameters.duration));
}
Compiled explorer source
'use client';
import {useEffect,useId,useRef,useState} from 'react';
import {Button} from '@mobtranslate/ui';
import {CodeBlock} from '../components/code-block';
import {initialSpring,sampleSpring,springCharacteristics,springResponse,validateSpring,type SpringParameters} from '../lib/spring-response';
import './spring-response.css';
const fields=[
{key:'mass',label:'Mass',min:.1,max:10,step:.1},
{key:'stiffness',label:'Stiffness',min:1,max:1000,step:1},
{key:'damping',label:'Damping',min:0,max:100,step:1},
{key:'initialVelocity',label:'Initial velocity',min:-5,max:5,step:.1},
{key:'duration',label:'Preview seconds',min:.1,max:8,step:.1},
] as const;
const makeModel=(parameters:SpringParameters)=>{
const samples=sampleSpring(parameters);
const minimum=Math.min(0,...samples.map(sample=>sample.position)),maximum=Math.max(1,...samples.map(sample=>sample.position));
const padding=(maximum-minimum)*.1;
return {parameters:{...parameters},samples,minimum:minimum-padding,maximum:maximum+padding,characteristics:springCharacteristics(parameters)};
};
export function SpringResponseExample(){
const id=useId();
const [editor,setEditor]=useState<Record<keyof SpringParameters,string>>({mass:'1',stiffness:'100',damping:'20',initialVelocity:'0',duration:'4'});
const [model,setModel]=useState(()=>makeModel(initialSpring));
const [ready,setReady]=useState(false),[running,setRunning]=useState(false),[reduced,setReduced]=useState(false),[status,setStatus]=useState('Initial captured model. Change settings and run to capture another response.');
const frame=useRef<number|null>(null),epoch=useRef(0),reducedPreference=useRef(false);
const currentModel=useRef(model),playing=useRef(false);
const cursor=useRef<SVGLineElement>(null),readout=useRef<HTMLSpanElement>(null);
const parameters=Object.fromEntries(fields.map(field=>[field.key,editor[field.key].trim()===''?NaN:Number(editor[field.key])])) as unknown as SpringParameters;
const errors=validateSpring(parameters);
const valid=errors.length===0;
const stopFrame=()=>{epoch.current++;if(frame.current!==null)cancelAnimationFrame(frame.current);frame.current=null;playing.current=false;setRunning(false);};
const showTime=(captured:ReturnType<typeof makeModel>,time:number)=>{
const point=springResponse(captured.parameters,time);
const x=10+980*time/captured.parameters.duration;
cursor.current?.setAttribute('x1',String(x));cursor.current?.setAttribute('x2',String(x));
if(readout.current)readout.current.textContent=`t=${point.time.toFixed(3)} s; x=${point.position.toFixed(4)}; velocity=${point.velocity.toFixed(4)} model units/s`;
};
useEffect(()=>{
const query=matchMedia('(prefers-reduced-motion: reduce)');
const update=()=>{
reducedPreference.current=query.matches;setReduced(query.matches);
if(query.matches&&playing.current){epoch.current++;if(frame.current!==null)cancelAnimationFrame(frame.current);frame.current=null;playing.current=false;setRunning(false);
const captured=currentModel.current,time=captured.parameters.duration,point=springResponse(captured.parameters,time),x=990;
cursor.current?.setAttribute('x1',String(x));cursor.current?.setAttribute('x2',String(x));
if(readout.current)readout.current.textContent=`t=${time.toFixed(3)} s; x=${point.position.toFixed(4)}; velocity=${point.velocity.toFixed(4)} model units/s`;
setStatus('Reduced motion is now enabled. Showing the captured endpoint without moving the cursor.');
}
};
update();setReady(true);query.addEventListener('change',update);
return()=>{epoch.current++;if(frame.current!==null)cancelAnimationFrame(frame.current);query.removeEventListener('change',update);};
},[]);
const run=()=>{
if(!valid)return;stopFrame();const captured=makeModel(parameters);currentModel.current=captured;setModel(captured);showTime(captured,0);
if(reducedPreference.current){showTime(captured,captured.parameters.duration);setStatus('Reduced motion: captured the response and displayed its endpoint without cursor animation.');return;}
const token=epoch.current,started=performance.now();playing.current=true;setRunning(true);setStatus('Captured response playback started. The time horizon is not a settling guarantee.');
const advance=(now:number)=>{
if(token!==epoch.current)return;
const time=Math.min(captured.parameters.duration,Math.max(0,(now-started)/1000));showTime(captured,time);
if(time>=captured.parameters.duration){frame.current=null;playing.current=false;setRunning(false);setStatus('Captured response reached its preview horizon. Inspect the endpoint and residual velocity; playback completion is not convergence.');}
else frame.current=requestAnimationFrame(advance);
};frame.current=requestAnimationFrame(advance);
};
const y=(position:number)=>110-100*(position-model.minimum)/(model.maximum-model.minimum);
const path=model.samples.map((point,index)=>`${index?'L':'M'}${(10+980*point.time/model.parameters.duration).toFixed(3)},${y(point.position).toFixed(3)}`).join(' ');
const endpoint=model.samples[model.samples.length-1];
const rows=[0,.25,.5,.75,1].map(fraction=>springResponse(model.parameters,model.parameters.duration*fraction));
return <div className="space-y-4" data-spring-ready={ready}>
<p>This dimensionless unit-step model moves from x=0 toward x=1. Its parameters are model units, not Android spring tokens or a physical device calibration.</p>
<fieldset className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3"><legend className="font-semibold mb-3">Draft model parameters</legend>{fields.map(field=><label key={field.key} htmlFor={`${id}-${field.key}`}>{field.label}<input id={`${id}-${field.key}`} data-spring-field={field.key} type="number" min={field.min} max={field.max} step="any" className="mt-input mt-2 w-full" value={editor[field.key]} disabled={!ready} aria-invalid={!Number.isFinite(parameters[field.key])||parameters[field.key]<field.min||parameters[field.key]>field.max} aria-describedby={`${id}-help ${id}-errors`} onChange={event=>setEditor(current=>({...current,[field.key]:event.target.value}))} /></label>)}</fieldset>
<p id={`${id}-help`}>Mass 0.1–10; stiffness 1–1000; damping 0–100; initial velocity −5–5; preview 0.1–8 seconds. Draft changes do not rewrite the captured curve. An empty field is invalid.</p>
<p id={`${id}-errors`} data-spring-errors>{errors.length?errors.join(' '):'Draft parameters are within the model bounds.'}</p>
<div className="spring-response-actions flex flex-wrap gap-3"><Button type="button" data-spring-run disabled={!ready||!valid} onClick={run}>Capture and play response</Button><Button type="button" variant="outline" data-spring-stop disabled={!ready} onClick={()=>{stopFrame();setStatus('Playback stopped. The captured response and current time remain available.');}}>Stop playback</Button><Button type="button" variant="outline" data-spring-reset disabled={!ready} onClick={()=>{stopFrame();showTime(currentModel.current,0);setStatus('Cursor reset to the captured response’s start. Draft parameters are unchanged.');}}>Reset cursor</Button></div>
<p data-spring-preference>{reduced?'Reduced motion enabled: no animated cursor.':'Reduced motion not requested: cursor playback available.'}</p>
<div className="border border-[var(--color-muted-foreground)] rounded-md p-3" data-spring-plot>
<svg viewBox="0 0 1000 120" preserveAspectRatio="none" className="w-full h-44" aria-hidden="true" focusable="false">
<line x1="10" x2="990" y1={y(1)} y2={y(1)} stroke="var(--color-muted-foreground)" strokeDasharray="5 5" vectorEffect="non-scaling-stroke" />
<path d={path} fill="none" stroke="var(--color-foreground)" strokeWidth="2" vectorEffect="non-scaling-stroke" />
<line ref={cursor} x1="10" x2="10" y1="0" y2="120" stroke="var(--color-foreground)" strokeWidth="2" vectorEffect="non-scaling-stroke" />
</svg>
<p>Response over {model.parameters.duration} {model.parameters.duration===1?'second':'seconds'}; dashed line is target x=1. The vertical scale adapts to the captured samples. Equivalent values are below.</p>
</div>
<p data-spring-live aria-live="off"><span ref={readout}>t=0.000 s; x=0.0000; velocity=0.0000 model units/s</span></p>
<dl className="grid gap-3 sm:grid-cols-2" data-spring-captured>{fields.map(field=><div key={field.key}><dt>Captured {field.label.toLowerCase()}</dt><dd data-spring-parameter={field.key}>{model.parameters[field.key]}</dd></div>)}<div><dt>Captured regime</dt><dd data-spring-regime>{model.characteristics.regime}</dd></div><div><dt>Damping ratio</dt><dd>{model.characteristics.dampingRatio.toFixed(4)}</dd></div><div><dt>Natural angular frequency</dt><dd>{model.characteristics.naturalFrequency.toFixed(4)} rad/s</dd></div><div><dt>Endpoint and velocity</dt><dd data-spring-endpoint>x={endpoint.position.toFixed(4)}; v={endpoint.velocity.toFixed(4)}</dd></div></dl>
<p>{model.parameters.damping===0?'Zero damping retains oscillation in this ideal model. A bounded playback does not make it settle.':'Positive damping approaches the target, but this chosen horizon may end while displacement or velocity remains.'}</p>
<div className="doc-table" role="region" tabIndex={0} aria-label="Captured spring response samples"><table><thead><tr><th scope="col">Seconds</th><th scope="col">Position</th><th scope="col">Velocity</th></tr></thead><tbody>{rows.map(point=><tr key={point.time}><th scope="row">{point.time.toFixed(3)}</th><td>{point.position.toFixed(6)}</td><td>{point.velocity.toFixed(6)}</td></tr>)}</tbody></table></div>
<details data-spring-full><summary className="min-h-11 cursor-pointer">Full captured parameters and {model.samples.length} samples</summary><CodeBlock language="json" code={JSON.stringify({parameters:model.parameters,samples:model.samples},null,2)} /></details>
<p role="status" data-spring-status>{status}</p>
<p className="text-sm">{running?'Playback active.':'Playback inactive.'} The curve is sampled at up to 60 intervals per second; the cursor evaluates the captured analytic response at elapsed browser time. This plot does not animate a production component, preserve retargeted velocity or measure frame-rate performance.</p>
</div>;
}
Scoped control reflow
The explorer’s action labels wrap and grow at enlarged text sizes. Their target height remains at least 44 CSS px without forcing the page wider than its viewport.
.spring-response-actions .mt-btn {
min-block-size:44px;
block-size:auto;
max-inline-size:100%;
white-space:normal;
overflow-wrap:anywhere;
}
Verification and failure cases
- Check analytic reference cases: initial conditions, critical response, known real roots and undamped oscillation.
- Exercise every regime, velocity sign, extreme valid inputs, blank fields and out-of-bound values without nonfinite output.
- Change draft parameters and verify the old capture remains unchanged until an explicit valid run.
- Replay rapidly, stop and reset; verify the last operation owns the cursor/readout and no stale callback updates it.
- Enable reduced motion before and during playback; verify stationary cursor behavior with complete data available.
- Inspect both themes, 320px reflow, keyboard controls, expanded full JSON and nonannouncing frame updates.
- Do not claim native preset parity, production component integration, continuous retargeting, physical calibration or device performance from this numerical plot.