Skip to content

Commit

Permalink
Rewrite animation engine in Java
Browse files Browse the repository at this point in the history
The animator threads are now daemon threads.
  • Loading branch information
Juuxel committed Aug 1, 2024
1 parent a196c91 commit 4c90666
Show file tree
Hide file tree
Showing 13 changed files with 219 additions and 177 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import juuxel.adorn.util.animation.AnimatedPropertyWrapper;
import juuxel.adorn.util.animation.AnimationEngine;
import juuxel.adorn.util.animation.Interpolator;
import kotlin.Unit;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.Element;
import net.minecraft.util.math.MathHelper;
Expand Down Expand Up @@ -47,9 +46,8 @@ public ScrollEnvelope(int x, int y, int width, int height, SizedElement element,
);
}

private Unit setOffset(double offset) {
private void setOffset(double offset) {
this.offset = MathHelper.clamp(offset, 0.0, heightDifference());
return Unit.INSTANCE;
}

private int heightDifference() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package juuxel.adorn.util.animation;

import org.jetbrains.annotations.Nullable;

import java.util.Objects;

public abstract class AbstractAnimatedProperty<T> {
private final AnimationEngine engine;
private final int duration;
private final Interpolator<T> interpolator;
private @Nullable Task currentTask = null;

protected AbstractAnimatedProperty(AnimationEngine engine, int duration, Interpolator<T> interpolator) {
this.engine = engine;
this.duration = duration;
this.interpolator = interpolator;
}

protected abstract void setRawValue(T value);

public abstract T get();

public synchronized void set(T value) {
T oldValue = get();
var oldTask = currentTask;
if (oldTask != null) engine.remove(oldTask);

if (!Objects.equals(oldValue, value)) {
var task = new Task(oldValue, value);
currentTask = task;
engine.add(task);
}
}

// https://easings.net/#easeOutQuint
private static float ease(float delta) {
return (float) Math.pow(1 - (1 - delta), 5);
}

private class Task implements AnimationTask {
private final T from;
private final T to;
private int age = 0;

private Task(T from, T to) {
this.from = from;
this.to = to;
}

@Override
public boolean isAlive() {
return age < duration;
}

@Override
public void tick() {
age++;
float delta = ease((float) age / (float) duration);
T newValue = interpolator.interpolate(delta, from, to);
setRawValue(newValue);
}

@Override
public void removed() {
synchronized (AbstractAnimatedProperty.this) {
if (currentTask == this) {
currentTask = null;
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package juuxel.adorn.util.animation;

public final class AnimatedProperty<T> extends AbstractAnimatedProperty<T> {
private T value;

public AnimatedProperty(T initial, AnimationEngine engine, int duration, Interpolator<T> interpolator) {
super(engine, duration, interpolator);
value = initial;
}

@Override
public T get() {
return value;
}

@Override
protected void setRawValue(T value) {
this.value = value;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package juuxel.adorn.util.animation;

import java.util.function.Consumer;
import java.util.function.Supplier;

public final class AnimatedPropertyWrapper<T> extends AbstractAnimatedProperty<T> {
private final Supplier<T> getter;
private final Consumer<T> setter;

public AnimatedPropertyWrapper(AnimationEngine engine, int duration, Interpolator<T> interpolator, Supplier<T> getter, Consumer<T> setter) {
super(engine, duration, interpolator);
this.getter = getter;
this.setter = setter;
}

@Override
public T get() {
return getter.get();
}

@Override
protected void setRawValue(T value) {
setter.accept(value);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package juuxel.adorn.util.animation;

import org.jetbrains.annotations.Nullable;

import java.util.ArrayList;
import java.util.List;

public final class AnimationEngine {
private final List<AnimationTask> tasks = new ArrayList<>();
private @Nullable AnimatorThread thread = null;

public void add(AnimationTask task) {
synchronized (tasks) {
tasks.add(task);
}
}

public void remove(AnimationTask task) {
synchronized (tasks) {
tasks.remove(task);
}
}

public void start() {
// Null check to make sure that this function can be called in Screen.init.
// It's also called when the screen is resized, so creating a new thread each time
// 1. leaks animator threads
// 2. causes the animations to speed up unreasonably
if (thread == null) {
var thread = new AnimatorThread();
thread.start();
this.thread = thread;
}
}

public void stop() {
var current = thread;
if (current != null) current.interrupt();
thread = null;
}

private class AnimatorThread extends Thread {
private AnimatorThread() {
super("Adorn animator");
setDaemon(true);
}

@Override
public void run() {
while (!interrupted()) {
synchronized (tasks) {
var iter = tasks.iterator();
while (iter.hasNext()) {
var task = iter.next();
if (task.isAlive()) {
task.tick();
} else {
task.removed();
iter.remove();
}
}
}

try {
// noinspection BusyWait
sleep(10L);
} catch (InterruptedException e) {
break;
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package juuxel.adorn.util.animation;

public interface AnimationTask {
boolean isAlive();
void tick();
default void removed() {
}
}
20 changes: 20 additions & 0 deletions common/src/main/java/juuxel/adorn/util/animation/Interpolator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package juuxel.adorn.util.animation;

import juuxel.adorn.util.Colors;
import juuxel.adorn.util.ColorsKt;
import net.minecraft.util.math.MathHelper;

@FunctionalInterface
public interface Interpolator<T> {
Interpolator<Float> FLOAT = MathHelper::lerp;
Interpolator<Double> DOUBLE = MathHelper::lerp;
Interpolator<Integer> COLOR = (delta, from, to) -> {
float alpha = FLOAT.interpolate(delta, Colors.alphaOf(from), Colors.alphaOf(to));
float red = FLOAT.interpolate(delta, Colors.redOf(from), Colors.redOf(to));
float green = FLOAT.interpolate(delta, Colors.greenOf(from), Colors.greenOf(to));
float blue = FLOAT.interpolate(delta, Colors.blueOf(from), Colors.blueOf(to));
return ColorsKt.color(red, green, blue, alpha);
};

T interpolate(float delta, T from, T to);
}

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

Loading

0 comments on commit 4c90666

Please sign in to comment.