Fix menu push, focus scale, and tweened animations

- Implement String.substring so mm_on_push can dispatch the pressed button
  (Enter did nothing before).
- setScale now writes scaleWidth/scaleHeight/scaleDepth (focused item was
  never enlarged); setRotate stores rotation angles.
- animPos/animScale/animColor/animRotate and the animation-table records
  (move/resize/recolour/fade/rotate) now tween over their duration via a
  60fps Animator thread instead of jumping to the final state.
This commit is contained in:
2026-09-16 09:17:12 -04:00
parent 9f0b25acfc
commit 0531671d26
9 changed files with 289 additions and 19 deletions
@@ -0,0 +1,54 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.quoije.umd.rco.anim;
import java.util.ArrayList;
import java.util.List;
/**
* Runs tween animations on a single daemon thread, applying progress each
* frame. Tweens only mutate object state and request a repaint, so they never
* re-enter the VSMX interpreter.
*/
public final class Animator {
private static final Animator INSTANCE = new Animator();
private final List<Tween> tweens = new ArrayList<>();
private Animator() {
Thread thread = new Thread(this::run, "umd-animator");
thread.setDaemon(true);
thread.start();
}
public static Animator getInstance() {
return INSTANCE;
}
/** Registers a tween; it starts on the next animation tick. */
public void add(Tween tween) {
synchronized (tweens) {
tweens.add(tween);
}
}
private void run() {
while (true) {
long now = System.nanoTime();
List<Tween> done = new ArrayList<>();
synchronized (tweens) {
for (Tween tween : tweens) {
if (tween.step(now)) {
done.add(tween);
}
}
tweens.removeAll(done);
}
try {
Thread.sleep(16); // ~60 fps
} catch (InterruptedException e) {
return;
}
}
}
}
@@ -18,8 +18,18 @@ public class FadeAnim extends BaseAnim {
@Override @Override
protected void doPlay(VsmxBaseObject object) { protected void doPlay(VsmxBaseObject object) {
applyTo(ref, target -> { applyTo(ref, target -> {
target.alphaScale.setFloatValue(transparency.getFloatValue()); float ta = transparency.getFloatValue();
target.onDisplayUpdated(); long millis = (long) duration.getFloatValue();
if (millis <= 0) {
target.alphaScale.setFloatValue(ta);
target.onDisplayUpdated();
return;
}
float sa = target.alphaScale.getFloatValue();
Animator.getInstance().add(new Tween(millis, p -> {
target.alphaScale.setFloatValue(sa + (ta - sa) * p);
target.onDisplayUpdated();
}));
}); });
} }
} }
@@ -20,10 +20,26 @@ public class MoveToAnim extends BaseAnim {
@Override @Override
protected void doPlay(VsmxBaseObject object) { protected void doPlay(VsmxBaseObject object) {
applyTo(ref, target -> { applyTo(ref, target -> {
target.animX = x.getFloatValue(); float tx = x.getFloatValue();
target.animY = y.getFloatValue(); float ty = y.getFloatValue();
target.animZ = z.getFloatValue(); float tz = z.getFloatValue();
target.onDisplayUpdated(); long millis = (long) duration.getFloatValue();
if (millis <= 0) {
target.animX = tx;
target.animY = ty;
target.animZ = tz;
target.onDisplayUpdated();
return;
}
float sx = target.animX;
float sy = target.animY;
float sz = target.animZ;
Animator.getInstance().add(new Tween(millis, p -> {
target.animX = sx + (tx - sx) * p;
target.animY = sy + (ty - sy) * p;
target.animZ = sz + (tz - sz) * p;
target.onDisplayUpdated();
}));
}); });
} }
} }
@@ -21,11 +21,30 @@ public class RecolourAnim extends BaseAnim {
@Override @Override
protected void doPlay(VsmxBaseObject object) { protected void doPlay(VsmxBaseObject object) {
applyTo(ref, target -> { applyTo(ref, target -> {
target.redScale.setFloatValue(red.getFloatValue()); float tr = red.getFloatValue();
target.greenScale.setFloatValue(green.getFloatValue()); float tg = green.getFloatValue();
target.blueScale.setFloatValue(blue.getFloatValue()); float tb = blue.getFloatValue();
target.alphaScale.setFloatValue(alpha.getFloatValue()); float ta = alpha.getFloatValue();
target.onDisplayUpdated(); long millis = (long) duration.getFloatValue();
if (millis <= 0) {
target.redScale.setFloatValue(tr);
target.greenScale.setFloatValue(tg);
target.blueScale.setFloatValue(tb);
target.alphaScale.setFloatValue(ta);
target.onDisplayUpdated();
return;
}
float sr = target.redScale.getFloatValue();
float sg = target.greenScale.getFloatValue();
float sb = target.blueScale.getFloatValue();
float sa = target.alphaScale.getFloatValue();
Animator.getInstance().add(new Tween(millis, p -> {
target.redScale.setFloatValue(sr + (tr - sr) * p);
target.greenScale.setFloatValue(sg + (tg - sg) * p);
target.blueScale.setFloatValue(sb + (tb - sb) * p);
target.alphaScale.setFloatValue(sa + (ta - sa) * p);
target.onDisplayUpdated();
}));
}); });
} }
} }
@@ -20,10 +20,26 @@ public class ResizeAnim extends BaseAnim {
@Override @Override
protected void doPlay(VsmxBaseObject object) { protected void doPlay(VsmxBaseObject object) {
applyTo(ref, target -> { applyTo(ref, target -> {
target.scaleWidth.setFloatValue(width.getFloatValue()); float tw = width.getFloatValue();
target.scaleHeight.setFloatValue(height.getFloatValue()); float th = height.getFloatValue();
target.scaleDepth.setFloatValue(depth.getFloatValue()); float td = depth.getFloatValue();
target.onDisplayUpdated(); long millis = (long) duration.getFloatValue();
if (millis <= 0) {
target.scaleWidth.setFloatValue(tw);
target.scaleHeight.setFloatValue(th);
target.scaleDepth.setFloatValue(td);
target.onDisplayUpdated();
return;
}
float sw = target.scaleWidth.getFloatValue();
float sh = target.scaleHeight.getFloatValue();
float sd = target.scaleDepth.getFloatValue();
Animator.getInstance().add(new Tween(millis, p -> {
target.scaleWidth.setFloatValue(sw + (tw - sw) * p);
target.scaleHeight.setFloatValue(sh + (th - sh) * p);
target.scaleDepth.setFloatValue(sd + (td - sd) * p);
target.onDisplayUpdated();
}));
}); });
} }
} }
@@ -5,6 +5,7 @@ import dev.quoije.umd.rco.ObjectField;
import dev.quoije.umd.rco.type.FloatType; import dev.quoije.umd.rco.type.FloatType;
import dev.quoije.umd.rco.type.IntType; import dev.quoije.umd.rco.type.IntType;
import dev.quoije.umd.rco.type.ObjectType; import dev.quoije.umd.rco.type.ObjectType;
import dev.quoije.umd.rco.vsmx.interpreter.VsmxBaseObject;
/** Rotates the referenced object (type 4). */ /** Rotates the referenced object (type 4). */
public class RotateAnim extends BaseAnim { public class RotateAnim extends BaseAnim {
@@ -15,4 +16,30 @@ public class RotateAnim extends BaseAnim {
@ObjectField(order = 4) public FloatType x; @ObjectField(order = 4) public FloatType x;
@ObjectField(order = 5) public FloatType y; @ObjectField(order = 5) public FloatType y;
@ObjectField(order = 6) public FloatType z; @ObjectField(order = 6) public FloatType z;
@Override
protected void doPlay(VsmxBaseObject object) {
applyTo(ref, target -> {
float tx = x.getFloatValue();
float ty = y.getFloatValue();
float tz = z.getFloatValue();
long millis = (long) duration.getFloatValue();
if (millis <= 0) {
target.rotX = tx;
target.rotY = ty;
target.rotZ = tz;
target.onDisplayUpdated();
return;
}
float sx = target.rotX;
float sy = target.rotY;
float sz = target.rotZ;
Animator.getInstance().add(new Tween(millis, p -> {
target.rotX = sx + (tx - sx) * p;
target.rotY = sy + (ty - sy) * p;
target.rotZ = sz + (tz - sz) * p;
target.onDisplayUpdated();
}));
});
}
} }
@@ -0,0 +1,37 @@
// SPDX-License-Identifier: GPL-3.0-or-later
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.
*/
public final class Tween {
/** Receives progress 0..1 on every animation tick. */
@FunctionalInterface
public interface ProgressCallback {
void apply(float progress);
}
private final long startNanos;
private final long durationNanos;
private final ProgressCallback callback;
private boolean finished;
public Tween(long durationMillis, ProgressCallback callback) {
this.startNanos = System.nanoTime();
this.durationNanos = Math.max(1L, durationMillis) * 1_000_000L;
this.callback = callback;
}
/** Advances the tween and reports whether it has finished. */
boolean step(long nowNanos) {
float progress = (nowNanos - startNanos) / (float) durationNanos;
if (progress >= 1f) {
progress = 1f;
finished = true;
}
callback.apply(progress);
return finished;
}
}
@@ -2,6 +2,8 @@
package dev.quoije.umd.rco.object; package dev.quoije.umd.rco.object;
import dev.quoije.umd.rco.ObjectField; import dev.quoije.umd.rco.ObjectField;
import dev.quoije.umd.rco.anim.Animator;
import dev.quoije.umd.rco.anim.Tween;
import dev.quoije.umd.rco.type.EventType; import dev.quoije.umd.rco.type.EventType;
import dev.quoije.umd.rco.type.FloatType; import dev.quoije.umd.rco.type.FloatType;
import dev.quoije.umd.rco.type.IntType; import dev.quoije.umd.rco.type.IntType;
@@ -37,6 +39,11 @@ public class BasePositionObject extends BaseObject {
public float animY; public float animY;
public float animZ; public float animZ;
/** Rotation angles in radians (the renderer applies rotZ). */
public float rotX;
public float rotY;
public float rotZ;
@Override @Override
public int getX() { public int getX() {
int parentX = 0; int parentX = 0;
@@ -133,6 +140,9 @@ public class BasePositionObject extends BaseObject {
} }
public void setScale(VsmxBaseObject object, VsmxBaseObject w, VsmxBaseObject h, VsmxBaseObject d) { public void setScale(VsmxBaseObject object, VsmxBaseObject w, VsmxBaseObject h, VsmxBaseObject d) {
scaleWidth.setFloatValue(w.getFloatValue());
scaleHeight.setFloatValue(h.getFloatValue());
scaleDepth.setFloatValue(d.getFloatValue());
onDisplayUpdated(); onDisplayUpdated();
} }
@@ -155,22 +165,92 @@ public class BasePositionObject extends BaseObject {
} }
public void setRotate(VsmxBaseObject object, VsmxBaseObject x, VsmxBaseObject y, VsmxBaseObject rotationRads) { public void setRotate(VsmxBaseObject object, VsmxBaseObject x, VsmxBaseObject y, VsmxBaseObject rotationRads) {
rotX = x.getFloatValue();
rotY = y.getFloatValue();
rotZ = rotationRads.getFloatValue();
onDisplayUpdated(); onDisplayUpdated();
} }
public void animColor(VsmxBaseObject object, VsmxBaseObject red, VsmxBaseObject green, VsmxBaseObject blue, VsmxBaseObject alpha, VsmxBaseObject duration) { public void animColor(VsmxBaseObject object, VsmxBaseObject red, VsmxBaseObject green, VsmxBaseObject blue, VsmxBaseObject alpha, VsmxBaseObject duration) {
setColor(object, red, green, blue, alpha); long millis = duration.getIntValue();
float sr = redScale.getFloatValue();
float sg = greenScale.getFloatValue();
float sb = blueScale.getFloatValue();
float sa = alphaScale.getFloatValue();
float tr = red.getFloatValue();
float tg = green.getFloatValue();
float tb = blue.getFloatValue();
float ta = alpha.getFloatValue();
if (millis <= 0) {
setColor(object, red, green, blue, alpha);
return;
}
Animator.getInstance().add(new Tween(millis, p -> {
redScale.setFloatValue(sr + (tr - sr) * p);
greenScale.setFloatValue(sg + (tg - sg) * p);
blueScale.setFloatValue(sb + (tb - sb) * p);
alphaScale.setFloatValue(sa + (ta - sa) * p);
onDisplayUpdated();
}));
} }
public void animScale(VsmxBaseObject object, VsmxBaseObject w, VsmxBaseObject h, VsmxBaseObject d, VsmxBaseObject duration) { public void animScale(VsmxBaseObject object, VsmxBaseObject w, VsmxBaseObject h, VsmxBaseObject d, VsmxBaseObject duration) {
setScale(object, w, h, d); long millis = duration.getIntValue();
float sw = scaleWidth.getFloatValue();
float sh = scaleHeight.getFloatValue();
float sd = scaleDepth.getFloatValue();
float tw = w.getFloatValue();
float th = h.getFloatValue();
float td = d.getFloatValue();
if (millis <= 0) {
setScale(object, w, h, d);
return;
}
Animator.getInstance().add(new Tween(millis, p -> {
scaleWidth.setFloatValue(sw + (tw - sw) * p);
scaleHeight.setFloatValue(sh + (th - sh) * p);
scaleDepth.setFloatValue(sd + (td - sd) * p);
onDisplayUpdated();
}));
} }
public void animPos(VsmxBaseObject object, VsmxBaseObject x, VsmxBaseObject y, VsmxBaseObject z, VsmxBaseObject duration) { public void animPos(VsmxBaseObject object, VsmxBaseObject x, VsmxBaseObject y, VsmxBaseObject z, VsmxBaseObject duration) {
setPos(object, x, y, z); long millis = duration.getIntValue();
float sx = posX.getFloatValue();
float sy = posY.getFloatValue();
float sz = posZ.getFloatValue();
float tx = x.getFloatValue();
float ty = y.getFloatValue();
float tz = z.getFloatValue();
if (millis <= 0) {
setPos(object, x, y, z);
return;
}
Animator.getInstance().add(new Tween(millis, p -> {
posX.setFloatValue(sx + (tx - sx) * p);
posY.setFloatValue(sy + (ty - sy) * p);
posZ.setFloatValue(sz + (tz - sz) * p);
onDisplayUpdated();
}));
} }
public void animRotate(VsmxBaseObject object, VsmxBaseObject x, VsmxBaseObject y, VsmxBaseObject rotationRads, VsmxBaseObject duration) { public void animRotate(VsmxBaseObject object, VsmxBaseObject x, VsmxBaseObject y, VsmxBaseObject rotationRads, VsmxBaseObject duration) {
setRotate(object, x, y, rotationRads); long millis = duration.getIntValue();
float sx = rotX;
float sy = rotY;
float sz = rotZ;
float tx = x.getFloatValue();
float ty = y.getFloatValue();
float tz = rotationRads.getFloatValue();
if (millis <= 0) {
setRotate(object, x, y, rotationRads);
return;
}
Animator.getInstance().add(new Tween(millis, p -> {
rotX = sx + (tx - sx) * p;
rotY = sy + (ty - sy) * p;
rotZ = sz + (tz - sz) * p;
onDisplayUpdated();
}));
} }
} }
@@ -34,6 +34,17 @@ public final class VsmxString extends VsmxBaseObject {
return super.getPropertyValue(name); return super.getPropertyValue(name);
} }
public VsmxBaseObject substring(VsmxBaseObject object, VsmxBaseObject beginIndex) {
int begin = Math.max(0, Math.min(beginIndex.getIntValue(), value.length()));
return new VsmxString(interpreter, value.substring(begin));
}
public VsmxBaseObject substring(VsmxBaseObject object, VsmxBaseObject beginIndex, VsmxBaseObject endIndex) {
int begin = Math.max(0, Math.min(beginIndex.getIntValue(), value.length()));
int end = Math.max(begin, Math.min(endIndex.getIntValue(), value.length()));
return new VsmxString(interpreter, value.substring(begin, end));
}
@Override @Override
public boolean equals(VsmxBaseObject value) { public boolean equals(VsmxBaseObject value) {
return getStringValue().equals(value.getStringValue()); return getStringValue().equals(value.getStringValue());