Skip to content

Animations

“Perrier” edited this page May 10, 2026 · 1 revision

Animations

Two interpolators (Tween and Spring), a wide easing library, ARGB-aware color interpolation, and a render-time wrapper that applies them to any component.

import org.triggersstudio.moddinglib.client.ui.animation.Tween;
import org.triggersstudio.moddinglib.client.ui.animation.Spring;
import org.triggersstudio.moddinglib.client.ui.animation.Easing;
import org.triggersstudio.moddinglib.client.ui.animation.ColorTween;
import org.triggersstudio.moddinglib.client.ui.animation.Direction;

Tween

Time-based interpolated value implementing DoubleSupplier. The current value is computed lazily from System.nanoTime() on every getAsDouble() call — there's no global ticker. CPU is zero when nothing reads it.

Builder

Tween fade = Tween.from(0.0).to(1.0)
        .duration(Duration.ofMillis(300))
        .easing(Easing.OUT_CUBIC)
        .play();

Builder methods: to(double), duration(Duration), durationMs(long), easing(DoubleUnaryOperator), loop(), yoyo(), onComplete(Runnable). Finish with .play() (build + start) or .build() (build, leave stopped).

Compact factory

Tween fade = Tween.over(0.0, 1.0, 300, Easing.OUT_CUBIC).play();

Lifecycle

tween.play()      // start (or restart) from time zero
tween.replay()    // alias for play()
tween.stop()      // reset to start, stop the tween
tween.isDone()    // true when the tween reaches its end value (always false for looping)
tween.isStarted() // true once play() was called at least once
tween.onComplete(Runnable callback)

onComplete fires exactly once when the tween reaches its end — looping tweens never fire it; yoyo cycles never fire it on direction reversal. The callback runs on the render thread (it executes inside getAsDouble()).

Loop and yoyo

Tween rotate = Tween.from(0.0).to(360.0).durationMs(800).loop().play();
Tween bounce = Tween.from(0.0).to(20.0).durationMs(500).yoyo().play();

yoyo() implies loop(); the direction alternates every cycle.

Convenience factories

Tween fadeIn  = Tween.fadeIn(300);   // 0 → 1, 300 ms, OUT_CUBIC, started
Tween fadeOut = Tween.fadeOut(300);  // 1 → 0, 300 ms, OUT_CUBIC, started

Spring

Physics-based mass-spring-damper interpolator. Unlike Tween, calling target(...) mid-flight does not restart the animation — the system keeps its current position and velocity and re-aims at the new target. Stays smooth under heavy interaction (drag handles, search-as-you-type, hover scaling).

Integration: semi-implicit Euler at a fixed 1/240 s sub-step. getAsDouble() integrates as many sub-steps as needed to catch up since the last sample. CPU is zero when nothing reads it.

Presets

Spring.smooth(double initial)   // moderate stiffness, near-critical damping. Default.
Spring.snappy(double initial)   // high stiffness, slightly over-damped. Fast, no overshoot.
Spring.bouncy(double initial)   // under-damped. Overshoots and oscillates.
Spring.strong(double initial)   // very stiff. Almost instant.

Builder (advanced)

Spring s = Spring.from(0.0)
        .stiffness(180)
        .damping(20)
        .mass(1.0)
        .restThreshold(0.001)
        .target(100.0)         // set initial target
        .velocity(0)           // set initial velocity
        .onSettled(() -> ...)
        .build();

Mutators

spring.target(double t)        // re-aim, smooth transition. No-op if t == current target.
spring.set(double v)           // snap. Position = velocity = 0, target = v, atRest = true.
spring.onSettled(Runnable)     // replace the rest callback.

Queries

spring.position()
spring.velocity()
spring.target()
spring.isAtRest()
spring.getAsDouble()           // sample now (also drives integration)
Spring xPos = Spring.bouncy(0.0);
Animated(child).translateX(xPos::getAsDouble).build();
xPos.target(200.0);   // glide toward 200, with bouncy overshoot
xPos.target(50.0);    // re-aim mid-flight, smoothly

Easing

Standard easings as DoubleUnaryOperator. Each family exposes IN, OUT, IN_OUT, and OUT_IN variants.

Family Constants
Linear LINEAR
Sine IN_SINE, OUT_SINE, IN_OUT_SINE, OUT_IN_SINE
Quad (power 2) IN_QUAD, OUT_QUAD, IN_OUT_QUAD, OUT_IN_QUAD
Cubic (power 3) IN_CUBIC, OUT_CUBIC, IN_OUT_CUBIC, OUT_IN_CUBIC
Quart (power 4) IN_QUART, OUT_QUART, IN_OUT_QUART, OUT_IN_QUART
Quint (power 5) IN_QUINT, OUT_QUINT, IN_OUT_QUINT, OUT_IN_QUINT
Expo IN_EXPO, OUT_EXPO, IN_OUT_EXPO, OUT_IN_EXPO
Circ IN_CIRC, OUT_CIRC, IN_OUT_CIRC, OUT_IN_CIRC
Back (overshoot) IN_BACK, OUT_BACK, IN_OUT_BACK, OUT_IN_BACK
Elastic IN_ELASTIC, OUT_ELASTIC, IN_OUT_ELASTIC, OUT_IN_ELASTIC
Bounce IN_BOUNCE, OUT_BOUNCE, IN_OUT_BOUNCE, OUT_IN_BOUNCE
Bezier (CSS-style) BEZIER_IN, BEZIER_OUT, BEZIER_IN_OUT, BEZIER_OUT_IN
Steps STEPS_START (steps(1, true)), STEPS_END (steps(1, false))

Parametric factories

Easing.bezier(double cp1x, double cp1y, double cp2x, double cp2y)  // CSS cubic-bezier
Easing.steps(int n, boolean jumpStart)                              // stair-step
Easing.power(double exponent)                                       // arbitrary exponent (sugar for inPower)
Easing.inPower(double p)
Easing.outPower(double p)
Easing.inOutPower(double p)
Easing.outInPower(double p)

Composition

Easing.combine(in, out)   // first half "in" rescaled, second half "out" rescaled
Easing.outIn(inCurve)     // build OUT_IN from an IN curve
Easing.reverse(inCurve)   // 1 - in(1 - t)

ColorTween

ARGB color interpolator. Mirrors Tween's lifecycle (play / stop / replay, loop / yoyo, onComplete) but exposes getAsInt() instead of getAsDouble().

Builder

ColorTween glow = ColorTween.from(0xFF_22_22_22)
        .to(0xFF_00_AA_FF)
        .durationMs(400)
        .easing(Easing.IN_OUT_CUBIC)
        .play();

Compact factory

ColorTween glow = ColorTween.over(0xFF_22_22_22, 0xFF_00_AA_FF, 400, Easing.IN_OUT_CUBIC).play();

Sampling

int argb = glow.getAsInt();    // raw int, ARGB-packed
Integer boxed = glow.get();    // boxed variant

Plug into the animated builder via backgroundColor(IntSupplier):

Animated(child).backgroundColor(glow::getAsInt).build();

AnimatedComponent (and the Animated builder)

Render-time wrapper around any UIComponent. Suppliers are read on every frame and applied as transform / paint overrides. Layout is unchanged — animations are visual-only, so a fading-out child still occupies its layout slot.

Fluent builder

Animated(child)
    .opacity(DoubleSupplier)
    .translateX(DoubleSupplier)
    .translateY(DoubleSupplier)
    .translate(DoubleSupplier x, DoubleSupplier y)
    .scale(DoubleSupplier)
    .backgroundColor(IntSupplier)
    .build();
Tween appear = Tween.over(0.0, 1.0, 400, Easing.OUT_CUBIC).play();
Spring xPos = Spring.smooth(0.0);

UIComponent animated = Animated(card)
        .opacity(appear)
        .translateX(xPos)
        .build();

xPos.target(120);   // glide

Convenience shortcuts

FadeIn(UIComponent child, long durationMs)               // 0 → 1 alpha, OUT_CUBIC
FadeOut(UIComponent child, long durationMs)              // 1 → 0 alpha, OUT_CUBIC
SlideIn(UIComponent child, Direction from, long durationMs)

SlideIn ramps a 100 px translation from the requested side to zero, with Easing.OUT_CUBIC. Use the Animated builder if you need a different offset, easing, or to combine slide with fade.

Direction

public enum Direction { LEFT, TOP, RIGHT, BOTTOM }

SlideIn(child, Direction.LEFT, 300) slides the child in from the left.

Clone this wiki locally