Stop previous stream when a new one starts; ease animations

Starting a movie now closes the current decode engine and bumps a playback
generation so the stale video/audio threads stop themselves (they were
overlapping the movie's canvas and audio). PlaybackEngine.close is idempotent.
Animation records now honor their accelMode (linear/accel/decel easing).
This commit is contained in:
2026-09-16 12:20:47 -04:00
parent 0531671d26
commit ba21426b77
9 changed files with 103 additions and 20 deletions
@@ -66,6 +66,7 @@ public final class PlaybackEngine implements AutoCloseable {
private volatile boolean eof = false;
private volatile IOException failure = null;
private volatile boolean closed = false;
private final Thread decodeThread;
private PlaybackEngine(UmdIsoReader iso, String streamName, int audioSubstream) throws IOException {
@@ -254,6 +255,10 @@ public final class PlaybackEngine implements AutoCloseable {
@Override
public void close() {
if (closed) {
return;
}
closed = true;
decodeThread.interrupt();
try {
decodeThread.join(1000);
@@ -26,7 +26,7 @@ public class FadeAnim extends BaseAnim {
return;
}
float sa = target.alphaScale.getFloatValue();
Animator.getInstance().add(new Tween(millis, p -> {
Animator.getInstance().add(new Tween(millis, accelMode.getIntValue(), p -> {
target.alphaScale.setFloatValue(sa + (ta - sa) * p);
target.onDisplayUpdated();
}));
@@ -34,7 +34,7 @@ public class MoveToAnim extends BaseAnim {
float sx = target.animX;
float sy = target.animY;
float sz = target.animZ;
Animator.getInstance().add(new Tween(millis, p -> {
Animator.getInstance().add(new Tween(millis, accelMode.getIntValue(), p -> {
target.animX = sx + (tx - sx) * p;
target.animY = sy + (ty - sy) * p;
target.animZ = sz + (tz - sz) * p;
@@ -38,7 +38,7 @@ public class RecolourAnim extends BaseAnim {
float sg = target.greenScale.getFloatValue();
float sb = target.blueScale.getFloatValue();
float sa = target.alphaScale.getFloatValue();
Animator.getInstance().add(new Tween(millis, p -> {
Animator.getInstance().add(new Tween(millis, accelMode.getIntValue(), p -> {
target.redScale.setFloatValue(sr + (tr - sr) * p);
target.greenScale.setFloatValue(sg + (tg - sg) * p);
target.blueScale.setFloatValue(sb + (tb - sb) * p);
@@ -34,7 +34,7 @@ public class ResizeAnim extends BaseAnim {
float sw = target.scaleWidth.getFloatValue();
float sh = target.scaleHeight.getFloatValue();
float sd = target.scaleDepth.getFloatValue();
Animator.getInstance().add(new Tween(millis, p -> {
Animator.getInstance().add(new Tween(millis, accelMode.getIntValue(), p -> {
target.scaleWidth.setFloatValue(sw + (tw - sw) * p);
target.scaleHeight.setFloatValue(sh + (th - sh) * p);
target.scaleDepth.setFloatValue(sd + (td - sd) * p);
@@ -34,7 +34,7 @@ public class RotateAnim extends BaseAnim {
float sx = target.rotX;
float sy = target.rotY;
float sz = target.rotZ;
Animator.getInstance().add(new Tween(millis, p -> {
Animator.getInstance().add(new Tween(millis, accelMode.getIntValue(), p -> {
target.rotX = sx + (tx - sx) * p;
target.rotY = sy + (ty - sy) * p;
target.rotZ = sz + (tz - sz) * p;
@@ -3,10 +3,17 @@ package dev.quoije.umd.rco.anim;
/**
* A single interpolation from 0..1 over a duration in milliseconds. The
* callback receives linear progress each frame until completion.
* callback receives eased progress each frame until completion.
*/
public final class Tween {
/** Linear progress (no easing). */
public static final int EASE_LINEAR = 0;
/** Accelerate: slow start, fast finish. */
public static final int EASE_IN = 1;
/** Decelerate: fast start, slow finish. */
public static final int EASE_OUT = 2;
/** Receives progress 0..1 on every animation tick. */
@FunctionalInterface
public interface ProgressCallback {
@@ -15,12 +22,18 @@ public final class Tween {
private final long startNanos;
private final long durationNanos;
private final int accelMode;
private final ProgressCallback callback;
private boolean finished;
public Tween(long durationMillis, ProgressCallback callback) {
this(durationMillis, EASE_LINEAR, callback);
}
public Tween(long durationMillis, int accelMode, ProgressCallback callback) {
this.startNanos = System.nanoTime();
this.durationNanos = Math.max(1L, durationMillis) * 1_000_000L;
this.accelMode = accelMode;
this.callback = callback;
}
@@ -31,7 +44,15 @@ public final class Tween {
progress = 1f;
finished = true;
}
callback.apply(progress);
callback.apply(ease(progress, accelMode));
return finished;
}
private static float ease(float p, int accelMode) {
return switch (accelMode) {
case EASE_IN -> p * p;
case EASE_OUT -> 1f - (1f - p) * (1f - p);
default -> p;
};
}
}