From 0531671d263708f937871c7057777f3acc94edfa Mon Sep 17 00:00:00 2001 From: Mathieu Moran Date: Wed, 16 Sep 2026 09:17:12 -0400 Subject: [PATCH] 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. --- .../dev/quoije/umd/rco/anim/Animator.java | 54 ++++++++++++ .../dev/quoije/umd/rco/anim/FadeAnim.java | 14 ++- .../dev/quoije/umd/rco/anim/MoveToAnim.java | 24 ++++- .../dev/quoije/umd/rco/anim/RecolourAnim.java | 29 ++++-- .../dev/quoije/umd/rco/anim/ResizeAnim.java | 24 ++++- .../dev/quoije/umd/rco/anim/RotateAnim.java | 27 ++++++ .../java/dev/quoije/umd/rco/anim/Tween.java | 37 ++++++++ .../umd/rco/object/BasePositionObject.java | 88 ++++++++++++++++++- .../umd/rco/vsmx/interpreter/VsmxString.java | 11 +++ 9 files changed, 289 insertions(+), 19 deletions(-) create mode 100644 core/src/main/java/dev/quoije/umd/rco/anim/Animator.java create mode 100644 core/src/main/java/dev/quoije/umd/rco/anim/Tween.java diff --git a/core/src/main/java/dev/quoije/umd/rco/anim/Animator.java b/core/src/main/java/dev/quoije/umd/rco/anim/Animator.java new file mode 100644 index 0000000..508c90c --- /dev/null +++ b/core/src/main/java/dev/quoije/umd/rco/anim/Animator.java @@ -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 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 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; + } + } + } +} diff --git a/core/src/main/java/dev/quoije/umd/rco/anim/FadeAnim.java b/core/src/main/java/dev/quoije/umd/rco/anim/FadeAnim.java index 5e31998..96fb4f7 100644 --- a/core/src/main/java/dev/quoije/umd/rco/anim/FadeAnim.java +++ b/core/src/main/java/dev/quoije/umd/rco/anim/FadeAnim.java @@ -18,8 +18,18 @@ public class FadeAnim extends BaseAnim { @Override protected void doPlay(VsmxBaseObject object) { applyTo(ref, target -> { - target.alphaScale.setFloatValue(transparency.getFloatValue()); - target.onDisplayUpdated(); + float ta = transparency.getFloatValue(); + 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(); + })); }); } } diff --git a/core/src/main/java/dev/quoije/umd/rco/anim/MoveToAnim.java b/core/src/main/java/dev/quoije/umd/rco/anim/MoveToAnim.java index b2c8b93..1ba7063 100644 --- a/core/src/main/java/dev/quoije/umd/rco/anim/MoveToAnim.java +++ b/core/src/main/java/dev/quoije/umd/rco/anim/MoveToAnim.java @@ -20,10 +20,26 @@ public class MoveToAnim extends BaseAnim { @Override protected void doPlay(VsmxBaseObject object) { applyTo(ref, target -> { - target.animX = x.getFloatValue(); - target.animY = y.getFloatValue(); - target.animZ = z.getFloatValue(); - target.onDisplayUpdated(); + float tx = x.getFloatValue(); + float ty = y.getFloatValue(); + float tz = z.getFloatValue(); + 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(); + })); }); } } diff --git a/core/src/main/java/dev/quoije/umd/rco/anim/RecolourAnim.java b/core/src/main/java/dev/quoije/umd/rco/anim/RecolourAnim.java index ec45df3..60dbed2 100644 --- a/core/src/main/java/dev/quoije/umd/rco/anim/RecolourAnim.java +++ b/core/src/main/java/dev/quoije/umd/rco/anim/RecolourAnim.java @@ -21,11 +21,30 @@ public class RecolourAnim extends BaseAnim { @Override protected void doPlay(VsmxBaseObject object) { applyTo(ref, target -> { - target.redScale.setFloatValue(red.getFloatValue()); - target.greenScale.setFloatValue(green.getFloatValue()); - target.blueScale.setFloatValue(blue.getFloatValue()); - target.alphaScale.setFloatValue(alpha.getFloatValue()); - target.onDisplayUpdated(); + float tr = red.getFloatValue(); + float tg = green.getFloatValue(); + float tb = blue.getFloatValue(); + float ta = alpha.getFloatValue(); + 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(); + })); }); } } diff --git a/core/src/main/java/dev/quoije/umd/rco/anim/ResizeAnim.java b/core/src/main/java/dev/quoije/umd/rco/anim/ResizeAnim.java index 9d28416..b8fa086 100644 --- a/core/src/main/java/dev/quoije/umd/rco/anim/ResizeAnim.java +++ b/core/src/main/java/dev/quoije/umd/rco/anim/ResizeAnim.java @@ -20,10 +20,26 @@ public class ResizeAnim extends BaseAnim { @Override protected void doPlay(VsmxBaseObject object) { applyTo(ref, target -> { - target.scaleWidth.setFloatValue(width.getFloatValue()); - target.scaleHeight.setFloatValue(height.getFloatValue()); - target.scaleDepth.setFloatValue(depth.getFloatValue()); - target.onDisplayUpdated(); + float tw = width.getFloatValue(); + float th = height.getFloatValue(); + float td = depth.getFloatValue(); + 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(); + })); }); } } diff --git a/core/src/main/java/dev/quoije/umd/rco/anim/RotateAnim.java b/core/src/main/java/dev/quoije/umd/rco/anim/RotateAnim.java index 7824aea..77c2f8e 100644 --- a/core/src/main/java/dev/quoije/umd/rco/anim/RotateAnim.java +++ b/core/src/main/java/dev/quoije/umd/rco/anim/RotateAnim.java @@ -5,6 +5,7 @@ import dev.quoije.umd.rco.ObjectField; import dev.quoije.umd.rco.type.FloatType; import dev.quoije.umd.rco.type.IntType; import dev.quoije.umd.rco.type.ObjectType; +import dev.quoije.umd.rco.vsmx.interpreter.VsmxBaseObject; /** Rotates the referenced object (type 4). */ public class RotateAnim extends BaseAnim { @@ -15,4 +16,30 @@ public class RotateAnim extends BaseAnim { @ObjectField(order = 4) public FloatType x; @ObjectField(order = 5) public FloatType y; @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(); + })); + }); + } } diff --git a/core/src/main/java/dev/quoije/umd/rco/anim/Tween.java b/core/src/main/java/dev/quoije/umd/rco/anim/Tween.java new file mode 100644 index 0000000..39431d6 --- /dev/null +++ b/core/src/main/java/dev/quoije/umd/rco/anim/Tween.java @@ -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; + } +} diff --git a/core/src/main/java/dev/quoije/umd/rco/object/BasePositionObject.java b/core/src/main/java/dev/quoije/umd/rco/object/BasePositionObject.java index 7e9f585..9576136 100644 --- a/core/src/main/java/dev/quoije/umd/rco/object/BasePositionObject.java +++ b/core/src/main/java/dev/quoije/umd/rco/object/BasePositionObject.java @@ -2,6 +2,8 @@ package dev.quoije.umd.rco.object; 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.FloatType; import dev.quoije.umd.rco.type.IntType; @@ -37,6 +39,11 @@ public class BasePositionObject extends BaseObject { public float animY; public float animZ; + /** Rotation angles in radians (the renderer applies rotZ). */ + public float rotX; + public float rotY; + public float rotZ; + @Override public int getX() { int parentX = 0; @@ -133,6 +140,9 @@ public class BasePositionObject extends BaseObject { } public void setScale(VsmxBaseObject object, VsmxBaseObject w, VsmxBaseObject h, VsmxBaseObject d) { + scaleWidth.setFloatValue(w.getFloatValue()); + scaleHeight.setFloatValue(h.getFloatValue()); + scaleDepth.setFloatValue(d.getFloatValue()); onDisplayUpdated(); } @@ -155,22 +165,92 @@ public class BasePositionObject extends BaseObject { } public void setRotate(VsmxBaseObject object, VsmxBaseObject x, VsmxBaseObject y, VsmxBaseObject rotationRads) { + rotX = x.getFloatValue(); + rotY = y.getFloatValue(); + rotZ = rotationRads.getFloatValue(); onDisplayUpdated(); } 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) { - 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) { - 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) { - 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(); + })); } } diff --git a/core/src/main/java/dev/quoije/umd/rco/vsmx/interpreter/VsmxString.java b/core/src/main/java/dev/quoije/umd/rco/vsmx/interpreter/VsmxString.java index 35ffed0..60ffd00 100644 --- a/core/src/main/java/dev/quoije/umd/rco/vsmx/interpreter/VsmxString.java +++ b/core/src/main/java/dev/quoije/umd/rco/vsmx/interpreter/VsmxString.java @@ -34,6 +34,17 @@ public final class VsmxString extends VsmxBaseObject { 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 public boolean equals(VsmxBaseObject value) { return getStringValue().equals(value.getStringValue());