sugarcraft / sugar-spring
Damped spring physics for PHP TUIs — spring core ported from charmbracelet/harmonica, extended with projectile simulation, easing/bezier timing, spring chains, collections and reduced-motion awareness.
Requires
- php: ^8.3
- sugarcraft/sugar-colorprofile: dev-master
- sugarcraft/sugar-core: dev-master
Requires (Dev)
- phpunit/phpunit: ^10.5
- sugarcraft/sugar-testing: dev-master
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-05 01:02:44 UTC
README
SugarSpring
Formerly published as
sugarcraft/honey-bounce(renamed 2026-10). Namespace changed:SugarCraft\Bounce\→SugarCraft\Spring\.
sugar-spring — damped-spring physics, projectile simulation, and easing/timing curves for animation, for PHP 8.3+. Pure math; no terminal dependency.
composer require sugarcraft/sugar-spring
I want to…
| Goal | Reach for |
|---|---|
| Move a value to a target with physical feel | Spring |
| Know when an animation is actually done | Spring::settled() / SpringChain::isComplete() / SpringCollection::settled() |
| Sequence staggered animations | SpringChain |
| Animate many values at once | SpringCollection |
| Skip the math, pick a feel | Spring::fromPreset() |
| Use physical units (tension/friction/mass) | SpringConfig |
| Arcs, bouncing balls, particles | Projectile + Vector / Point |
| CSS-compatible timing curves | CubicBezier |
| Named Penner curves without control points | Easing enum |
| Honour reduced-motion | automatic for spring-driven types (see Reduced motion) |
Spring
Damped harmonic oscillator (Ryan-Juckett's algorithm, spring physics
part 1 / part
2). Choose
dampingRatio: < 1 oscillates, = 1 is critical (no overshoot, fastest
convergence), > 1 is over-damped. Both arguments must produce a converging
spring: angularFrequency must be at least 1e-9 and dampingRatio must be
greater than 0 — a zero-damping spring oscillates forever and a negative one
diverges, so the constructor refuses both instead of letting your frame loop
run until the user closes the window.
use SugarCraft\Spring\Spring; $spring = new Spring( deltaTime: Spring::fps(60), // 1/60 of a second angularFrequency: 6.0, // rad/sec dampingRatio: 1.0, // critical ); $pos = 0.0; $vel = 0.0; $target = 100.0; $frame = 0; while (!Spring::settled($pos, $vel, $target) && $frame < 600) { [$pos, $vel] = $spring->update($pos, $vel, $target); printf("frame %2d pos=%.2f vel=%.2f\n", $frame, $pos, $vel); $frame++; }
Spring::fps(float $n) returns 1.0 / $n for the deltaTime — pair with the
same $n per-second simulation cadence. Spring::settled($pos, $vel, $target, $threshold) is the stop predicate: position within the threshold (default
Spring::SETTLING_THRESHOLD = 1e-4) and velocity below it. Drive loops by
settled() rather than by a fixed frame count — the count that finishes one
spring takes seven seconds on another preset (see
Spring presets).
Driving a frame loop
A spring bakes its deltaTime in at construction, so the simulation only
matches wall-clock when you actually call update() at that rate. Two
honest patterns:
-
Fixed cadence (terminals, replays). Build one spring with
Spring::fps(60)and tick it once per 60 Hz frame — with sugar-core programs, once per tick command yourProgram::subscriptions()emits (Subscriptions::withTick()/Cmd::tick()):use SugarCraft\Spring\Spring; $spring = new Spring(Spring::fps(60), 6.0, 1.0); $pos = 0.0; $vel = 0.0; foreach ([10.0, 60.0, 100.0] as $target) { // one frame per tick do { [$pos, $vel] = $spring->update($pos, $vel, $target); // render $pos this frame } while (!Spring::settled($pos, $vel, $target)); }
If the loop drifts slower than 60 Hz the motion slows with it. That is a feature for deterministic replays; for wall-clock motion measure the real frame time instead:
use SugarCraft\Spring\Spring; // A real loop measures with hrtime(true); the scripted array keeps the // example deterministic. Clamping bounds a stall instead of teleporting. $frameTimes = [0.016, 0.033, 0.017, 0.021, 0.016]; $targets = [10.0, 60.0, 100.0, 100.0, 100.0]; $pos = 0.0; $vel = 0.0; foreach ($frameTimes as $index => $deltaTime) { $spring = new Spring($deltaTime, 6.0, 1.0); // coeffs are cheap (~1 µs) [$pos, $vel] = $spring->update($pos, $vel, $targets[$index]); // render $pos this frame }
Rebuilding the spring per frame (above) keeps wall-clock motion honest under jitter; if the integer-rendered position is unchanged from last frame, skip the repaint — animating a value that rounds to the same cell is repaints without information.
Reduced motion
Spring-driven animation honours a reduce-motion request automatically:
Spring::update() — and everything built from springs, SpringChain and
SpringCollection — snaps to $target (returning [<target>, 0.0]) when the
signal is on. The signal is environment-only: REDUCE_MOTION or
PREFERS_REDUCED_MOTION, looked up through
SugarCraft\ColorProfile\Probe::reducedMotion(). The lookup treats 0,
false, no and off (case-insensitively) as off; any other non-empty
value — 1 among them — as on. There is no terminal query or
accessibility-daemon channel, and the value is consulted per call: flipping
the env var mid-session changes behaviour on the next frame. When the host
sets one of the variables this supports the WCAG 2.3.3 (Animation from
Interactions) reduced-motion
guideline
— a math library can support conformance, not satisfy it for your whole app.
Projectile, the Easing curves and CubicBezier deliberately ignore the
signal: they are pure functions of time with no target to snap to. Gate
them app-side by short-circuiting normalized time to the end of the motion:
use SugarCraft\ColorProfile\Probe; use SugarCraft\Spring\Easing\Easing; $frame = 12; // 12th frame of a 60-frame motion $t = Probe::reducedMotion() ? 1.0 : $frame / 60.0; // t = 1.0 is "already there" $progress = Easing::CubicOut->ease($t);
Two more knobs: the constructor's fourth parameter
?bool $reducedMotionOverride pins the behaviour for one spring —
pass false to animate even when the env var is set, true to snap even
when it is not (null, the default, defers to the environment).
Spring::fromPreset(), SpringConfig::spring() and
SpringConfig::springAt60Fps() take the same override as their final optional
parameter.
use SugarCraft\Spring\Spring; // With REDUCE_MOTION=1 the spring skips animation entirely: putenv('REDUCE_MOTION=1'); $spring = new Spring(deltaTime: Spring::fps(60), angularFrequency: 6.0, dampingRatio: 1.0); [$pos, $vel] = $spring->update(0.0, 0.0, 100.0); // returns [100.0, 0.0] // Pin one spring to ignore the environment (e.g. a required progress cue): $alwaysAnimates = new Spring( deltaTime: Spring::fps(60), angularFrequency: 6.0, dampingRatio: 1.0, reducedMotionOverride: false, );
Spring presets
Spring::fromPreset(SpringPreset $preset, ?float $deltaTime = null) constructs
a spring from a named preset at 60 fps (override the frame time as needed).
Five presets are available, tuned by hand against the react-spring-era folk
tables (they are not literal UIKit values — UIViewPropertyLiteral documents
duration/damping, not tension triples):
| Preset | Tension | Friction | Mass | ζ | Overshoot | Settle at 60 fps |
|---|---|---|---|---|---|---|
Gentle |
100 | 10 | 1 | 0.50 | ~16% | 174 frames (~2.9 s) |
Wobbly |
180 | 12 | 1 | 0.45 | ~21% | 141 frames (~2.4 s) |
Stiff |
500 | 20 | 1 | 0.45 | ~21% | 94 frames (~1.6 s) |
Slow |
50 | 6 | 1 | 0.42 | ~23% | 292 frames (~4.9 s) |
Molasses |
30 | 4 | 1 | 0.37 | ~29% | 439 frames (~7.3 s) |
Every preset is under-damped (ζ < 1) — they all overshoot a little; that is
the family's character. Settle frames are measured for a 0 → 100 move at the
default 1e-4 threshold. A preset critical on demand does not exist because
none was tuned; build one with dampingRatio: 1.0 (the flagship example
above) or new SpringConfig(...) with friction 2 * sqrt(tension * mass).
use SugarCraft\Spring\{Spring, SpringPreset}; $spring = Spring::fromPreset(SpringPreset::Wobbly); // With custom frame rate $spring60 = Spring::fromPreset(SpringPreset::Stiff, Spring::fps(60)); $spring30 = Spring::fromPreset(SpringPreset::Gentle, Spring::fps(30));
SpringConfig
SpringConfig accepts physical parameters (tension / friction / mass — all
finite and greater than zero) and derives the angularFrequency and
dampingRatio consumed by Spring:
angularFrequency = sqrt(tension / mass)
dampingRatio = friction / (2 * sqrt(tension * mass))
use SugarCraft\Spring\{SpringConfig, Spring}; $config = new SpringConfig(tension: 180.0, friction: 12.0, mass: 1.0); $spring = $config->springAt60Fps(); // or ->spring($deltaTime) echo $config->angularFrequency; // 13.416... (public readonly)
Both SpringConfig::spring() and SpringConfig::springAt60Fps() return
a pre-wired Spring instance ready to drive update() calls and accept the
optional reducedMotionOverride described above.
Projectile
Newtonian-physics simulator for arcs / bouncing balls / particle effects.
use SugarCraft\Spring\{Point, Projectile, Spring, Vector}; $p = Projectile::new( deltaTime: Spring::fps(60), position: Point::zero(), velocity: new Vector(5.0, 10.0), // up-and-to-the-right (Y-up) acceleration: Projectile::gravity(), // (0, -9.81) — pulls down ); for ($i = 0; $i < 60; $i++) { $p = $p->update(); echo sprintf("t=%2d pos=(%.1f, %.1f)\n", $i, $p->position->x, $p->position->y); }
Gravity constants: Projectile::GRAVITY (9.81) and
Projectile::TERMINAL_GRAVITY (53.0). Helper factories
Projectile::gravity() and Projectile::terminalGravity() return Y-up
Vector instances ((0, -9.81, 0) / (0, -53, 0)) ready to drop into
the constructor; gravityYDown() / terminalGravityYDown() keep the
legacy Y-down sign reachable.
SugarCraft\Spring\Gravity exposes the same vectors as static
accessors at the package level — Gravity::standard(),
Gravity::terminal(), Gravity::standardYDown(),
Gravity::terminalYDown() — so gravity presets read
uniformly across call sites.
Damping-ratio regimes
The dampingRatio argument to Spring picks one of three classical
behaviours:
- Under-damped (
0 < ζ < 1) — oscillates around the target, amplitudes decaying each cycle. Good picks for "bouncy" feel. - Critically-damped (
ζ = 1) — fastest convergence with no overshoot. The default for "snap to value" animations. - Over-damped (
ζ > 1) — converges without overshoot but slower than critical. Good picks for slow, weighty motion.
ζ <= 0 is refused at construction: zero damping is a pure oscillator that
never settles, and negative damping diverges — neither belongs in a frame
loop, so the constructor throws instead of hanging it.
Coordinate systems
Both Vector and Point are 3D (x, y, z) — the constructor's
$z defaults to 0.0 so existing 2D call sites still compile
unchanged. Use the third dimension when a scene needs a Z
axis (parallax / depth-shaded particle systems). Both types refuse
non-finite components, and Vector::normalize() refuses zero and
overflowed (INF) lengths rather than quietly returning garbage.
The Y-axis convention is Y-up by default: Gravity::standard() returns (0, -9.81, 0) so increasing
Y means "up the screen". Terminal renderers usually grow downward —
flip to Gravity::standardYDown() (or its Projectile::gravityYDown()
alias) when you want gravity to pull toward the bottom of the grid
without manually negating every coordinate.
Projectile::update() returns a new Projectile instance each
call — update() never mutates the receiver. The hazard is discarding
the result: read state from whatever you last assigned ($p = $p->update();),
not from the pre-update $p. The position / velocity / acceleration
properties and their identically-named accessor methods expose the same
Vectors; pick either spelling, just be consistent.
When is it done? (the completion contract)
A spring has finished when Spring::settled($pos, $vel, $target, $threshold)
says so — position near the target and velocity near zero, both required:
a position-only check lies exactly on under-damped presets, which cross the
target at full speed and come back. Chain and collection expose the same
contract at their own level: SpringChain::isComplete() and
SpringCollection::settled(). Loops in this README stop on those predicates,
not on frame counts; the $frame < 600 guards shown are escape hatches for
paths you can't otherwise bound, and presets are documented so you can size
them (Molasses alone needs 439).
For terminal grids the default 1e-4 threshold is finer than one cell:
a spring has usually stopped changing the frame long before it has stopped
settling. Pass a cell-sized threshold (0.5) to settled() when the
consumer renders at cell resolution, and skip the repaint when the rounded
cell value is unchanged from last frame. There is no value→cell helper
tree-wide yet; 0.5 on a unit-per-cell mapping is the hand-carried rule of
thumb.
SpringChain
Sequence multiple springs so that one spring's settle triggers the next. Useful for staggered animations where each stage must complete before the next begins.
use SugarCraft\Spring\Spring; use SugarCraft\Spring\SpringChain; use SugarCraft\Spring\SpringPreset; $chain = (new SpringChain([])) ->withStage(Spring::fromPreset(SpringPreset::Gentle), 0.0, 0.0, 50.0) ->withStage(Spring::fromPreset(SpringPreset::Wobbly), 0.0, 0.0, 100.0) ->withStage(Spring::fromPreset(SpringPreset::Stiff), 0.0, 0.0, 75.0); while (!$chain->isComplete()) { $chain = $chain->tick(); $positions = $chain->currentPositions(); // this frame's positions // render $positions }
tick() returns the advanced chain — reassign it, or the run replays the
same frame forever. Positions are read back with currentPositions(), which
always reflects the chain you call it on (the previous three-tuple return
carried a copy that lagged one frame behind; that shape is gone). Each tick()
advances only the active stage. When that stage reaches its target — position
within SpringChain::SETTLING_THRESHOLD (5e-4) and velocity within it —
the chain activates the next stage. The threshold is deliberately looser than
a lone spring's Spring::SETTLING_THRESHOLD (1e-4) so stages hand off while
still visually moving; a settled stage hands off up to 5e-4 short of its
target by that design. isComplete() returns true when all stages have
settled. Under reduced motion every stage snaps, so the chain completes within
one tick per stage. Note withStage() restarts the new chain at stage 0:
build the full stage list before the first tick(). The constructor accepts
[Spring, float, float, float] stages and refuses malformed shapes and an
out-of-range active index at the boundary.
SpringCollection
Keep many named springs animating at once. Like every other type here,
the collection is immutable: withSpring(), without(), withTarget()
and tick() each return a new collection, so the carry-the-result
assignment is mandatory:
use SugarCraft\Spring\Spring; use SugarCraft\Spring\SpringCollection; $group = (new SpringCollection()) ->withSpring('menu', Spring::fromPreset(\SugarCraft\Spring\SpringPreset::Stiff), 0.0, 0.0, 40.0) ->withSpring('panel', Spring::fromPreset(\SugarCraft\Spring\SpringPreset::Molasses), 0.0, 0.0, 90.0); $frame = 0; while (!$group->settled() && $frame < 600) { // settled(): every member within Spring::SETTLING_THRESHOLD $group = $group->tick(); // tick(): self — NOT a position map $frame++; } echo sprintf("menu=%.2f panel=%.2f\n", $group->get('menu'), $group->get('panel'));
Read with get($id) / all() / target($id) / velocity($id) /
allVelocities() / has($id); ask "still animating?" with
settled($threshold = Spring::SETTLING_THRESHOLD) (it needs velocities, which
is why they are exposed). Unknown ids are refused by get() / target() /
velocity() / withTarget() with an OutOfBoundsException naming the id,
and the constructor refuses misaligned or orphaned id planes. The earlier
in-place add() / remove() / setTarget() mutators are deprecated
shims that emit a visible E_USER_DEPRECATED and will be dropped; migrate to
the with*() forms above (the shims mutate the receiver and return void,
so $c = $c->add(...) assigns null).
Easing
SugarCraft\Spring\Easing\Easing provides named easing curves via their
ease(float $t): float method — apply to any normalized time value in
[0.0, 1.0]:
use SugarCraft\Spring\Easing\Easing; $ease = Easing::ElasticOut; for ($f = 0; $f <= 60; $f++) { $t = $f / 60.0; echo sprintf("frame %2d t=%.3f eased=%.3f\n", $f, $t, $ease->ease($t)); }
Honesty about the range: ease() assumes t in [0, 1] but does not
enforce it (out-of-domain t extrapolates), and several curves intentionally
leave [0, 1] in the output — Elastic* and Back* overshoot,
Bounce* stays inside. Six of the sixteen cases exit [0, 1] somewhere, by
design. If you map an eased value to cells ((int) round($eased * $cols)),
clamp before indexing — a rubber-band curve is one frame away from a negative
index.
Curves implement SugarCraft\Spring\Easing\EasingFunction, the extension
seam: write your own ease() and pass it anywhere an easing function is
consumed.
CubicBezier
CubicBezier implements the CSS cubic-bezier() easing algorithm
(Newton-Raphson root-finding with a bounded binary-search fallback). Construct
via static factory methods: 26 named presets in total — the five CSS
transition-timing-function keywords (linear, ease, ease-in,
ease-out, ease-in-out, which subsume the arbitrary cubic-bezier()
form) plus 21 Penner-style curves (Sine/Quad/Cubic/Quart/Quint/Expo/Circ in
In/Out/InOut) approximated with the
easings.net table expressed as bezier control points.
The Penner rows are named for the easing family, not for a CSS standard list:
use SugarCraft\Spring\Easing\CubicBezier; // CSS named easings $ease = CubicBezier::ease(); // 0.25, 0.10, 0.25, 1.00 $easeIn = CubicBezier::easeIn(); // 0.42, 0.00, 1.00, 1.00 $easeOut = CubicBezier::easeOut(); // 0.00, 0.00, 0.58, 1.00 $easeInOut = CubicBezier::easeInOut(); // 0.42, 0.00, 0.58, 1.00 $linear = CubicBezier::linear(); // 0.00, 0.00, 1.00, 1.00 // Sine $easeInSine = CubicBezier::easeInSine(); $easeOutSine = CubicBezier::easeOutSine(); $easeInOutSine = CubicBezier::easeInOutSine(); // Quadratic $easeInQuad = CubicBezier::easeInQuad(); $easeOutQuad = CubicBezier::easeOutQuad(); $easeInOutQuad = CubicBezier::easeInOutQuad(); // Cubic $easeInCubic = CubicBezier::easeInCubic(); $easeOutCubic = CubicBezier::easeOutCubic(); $easeInOutCubic = CubicBezier::easeInOutCubic(); // Quartic / Quintic / Exponential / Circular $easeInQuart = CubicBezier::easeInQuart(); $easeOutQuart = CubicBezier::easeOutQuart(); $easeInOutQuart = CubicBezier::easeInOutQuart(); $easeInQuint = CubicBezier::easeInQuint(); $easeOutQuint = CubicBezier::easeOutQuint(); $easeInOutQuint = CubicBezier::easeInOutQuint(); $easeInExpo = CubicBezier::easeInExpo(); $easeOutExpo = CubicBezier::easeOutExpo(); $easeInOutExpo = CubicBezier::easeInOutExpo(); $easeInCirc = CubicBezier::easeInCirc(); $easeOutCirc = CubicBezier::easeOutCirc(); $easeInOutCirc = CubicBezier::easeInOutCirc(); for ($f = 0; $f <= 60; $f++) { $t = $f / 60.0; echo sprintf("frame %2d eased=%.4f\n", $f, $easeInOutCubic->evaluate($t)); }
CubicBezier::evaluate(float $t): float takes t in [0, 1] and returns the
eased progress. The x-mapping is solved to [0, 1] monotonicity (control
x-values are range-checked); the y-output lands exactly on (0, 1) for the
presets but can overshoot outside that interval between endpoints for the
elastic- and back-flavoured control points — same honesty rule as Easing.
Public API
Spring—__construct($dt, $ω, $ζ, ?bool $reducedMotionOverride = null)(refuses non-finite inputs,dt < 0,ω < 1e-9,ζ <= 0and coefficient overflow) /update($pos, $pos, $target)/fps(float)/fromPreset(SpringPreset, ?float $dt = null, ?bool $reducedMotionOverride = null)/ staticsettled($pos, $vel, $target, $threshold = SETTLING_THRESHOLD).const SETTLING_THRESHOLD = 1e-4.update()rejects non-finite state and short-circuits to[$target, 0.0]when the env-driven reduced-motion lookup — or the per-instance override — resolves totrue.SpringChain—__construct($stages, int $activeIndex = 0)/new($stages)/withStage(Spring, $pos, $vel, $target)(build-time only; returns a chain restarted at stage 0) /tick(): self(reassign or the run never advances) /currentPositions(): list<float>/isComplete(): bool/activeStage(): int/const SETTLING_THRESHOLD = 5e-4. Under reduced motion every stage snaps (one tick per stage).SpringCollection—__construct($springs, $positions, $velocities, $targets)/withSpring($id, Spring, $pos, $vel, $target)/without($id)/withTarget($id, $target)(unknown ids refused, named in the error) /tick(): self(new collection; positions map viaall()) /get($id): float/has($id): bool/all(): array<string,float>/target($id): float/velocity($id): float/allVelocities()/settled($threshold = Spring::SETTLING_THRESHOLD): bool. The in-placeadd()/remove()/setTarget()mutators are deprecatedE_USER_DEPRECATEDshims slated for removal — use thewith*()forms.SpringPreset—Gentle/Wobbly/Stiff/Slow/Molasses.resolve()returns aSpringConfig.SpringConfig—__construct(tension, friction, mass)(all finite and positive) with public readonly$angularFrequency/$dampingRatio/spring(float $dt = 1/60, ?bool $reducedMotionOverride = null)/springAt60Fps(?bool $reducedMotionOverride = null).Projectile—Projectile::new(...)/update()/position()/velocity()/acceleration()(identical->positionetc. properties exist) /gravity()/terminalGravity()/gravityYDown()/terminalGravityYDown()/GRAVITY/TERMINAL_GRAVITY.Gravity— package-level static accessors:standard(),terminal(),standardYDown(),terminalYDown().Vector— immutable 3D vector (finite components enforced) withadd/sub/scale/length/lengthSquared/dot/cross/lerp/normalize/jsonSerialize/__toString/Vector::zero().Point— immutable 3D point (finite components enforced) withadd(Vector)/distance/__toString/jsonSerialize/Point::zero().Easing— enum implementingEasingFunction(the extension seam) withease(float $t): float. Cases:Linear,QuadraticIn/Out/InOut,CubicIn/Out/InOut,ElasticIn/Out/InOut,BounceIn/Out/InOut,BackIn/Out/InOut.CubicBezier—evaluate(float $t): float. 26 static factories: the five CSS timing keywords (linear,ease,easeIn,easeOut,easeInOut) and 21 Penner-style curves (easeIn/Out/InOut×Sine,Quad,Cubic,Quart,Quint,Expo,Circ) expressed as bezier control points.
Exception messages are translated through SugarCraft\Spring\Lang (the
sugarcraft i18n registry, lang/<code>.php files, 16 locales). Set the
locale with the shared core machinery — SugarCraft\Core\I18n\T::setLocale():
use SugarCraft\Core\I18n\T; T::setLocale('de'); // every Lang::t() lookup, this lib's messages included, // now resolves German (exact → base → en fallback).
Test
cd sugar-spring && composer install && vendor/bin/phpunit
Snapshot tests
Numeric trajectory output is pinned as golden files (JSON) in
tests/fixtures/, using sugar-testing's GoldenFile::save()/load()
with plain assertEquals equality in tests/GoldenRenderTest.php —
sugar-testing's assertGolden* helper family is ANSI/cell-grid specific
and not used here. The golden tests clear REDUCE_MOTION /
PREFERS_REDUCED_MOTION and pin serialize_precision in setUp(), so host
accessibility settings and ini defaults cannot change the comparison. Any
change to the physics output must be intentional — re-record the fixtures with:
UPDATE_GOLDENS=1 vendor/bin/phpunit
(there is no --update-golden phpunit flag).
Demos
Projectile motion
Spring physics
Particle field
Accessor deprecation (2026-10-24)
Per the house bare-accessor convention ("Bare accessors (no get")),
SpringCollection::getTarget() was renamed to its bare twin target($id) —
the slot was unclaimed and the getter now mirrors the collection's existing
bare get() / all() / has() readers. The fluent withTarget() builder
and the deprecated in-place setTarget() shim are untouched; a bare
target() coexists with both cleanly. The legacy spelling stays a
byte-identical one-line BC forwarder marked @deprecated with REMOVED-IN: 2.0.0; it will be removed in sugar-spring v2.0.0.
| Namespace | Class | Deprecated | Use instead |
|---|---|---|---|
Spring |
SpringCollection |
getTarget() |
target() |
tests/AccessorDeprecationTest.php pins BC identity on a live collection
(seeded spring, after retarget, original untouched), the forwarder shape
(single return line + markers), a discovery-first src/ census with an
inverse leg that reddens any unmarked public get*(), and the README table
agreement. tick() reads $this->targets directly, so there were no internal
call sites to switch; the existing SpringCollectionTest needles stay green
through the forwarder.
Credits & inspiration
The spring core mirrors charmbracelet/harmonica after Ryan Juckett's spring physics articles; preset values descend from the react-spring-era folk tables and are tuned by hand; the bezier control points follow easings.net. The reduced-motion lookup rides sugar-colorprofile's Probe. Originally inspired by the Go Charm ecosystem; SugarCraft is developed as a native PHP project.


