From 876fd9f9773af5ddfadada6aac4b01023faf0c15 Mon Sep 17 00:00:00 2001 From: ImBit <151650938+ImBit@users.noreply.github.com> Date: Mon, 27 Jul 2026 17:05:05 +0100 Subject: [PATCH 1/6] :art: Add more generic animations. --- .../bits/mc/animation/Animations.java | 85 +++++++++++++++++-- 1 file changed, 80 insertions(+), 5 deletions(-) diff --git a/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/Animations.java b/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/Animations.java index fcbe56e2..cb4f83d1 100644 --- a/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/Animations.java +++ b/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/Animations.java @@ -14,11 +14,11 @@ public final class Animations { private Animations() {} - public static Animation floating() { - return floating(40, 0.5f); + //region Generic transformations + public static Animation translation(float x, float y, float z) { + return Animation.constant(AnimationPose.builder().translation(x, y, z).build()); } - public static Animation xTranslation(float distance) { return Animation.constant(AnimationPose.builder().translateX(distance).build()); } @@ -31,6 +31,47 @@ public static Animation zTranslation(float distance) { return Animation.constant(AnimationPose.builder().translateZ(distance).build()); } + public static Animation scale(float scale) { + return Animation.constant(AnimationPose.builder().scale(scale).build()); + } + + public static Animation scale(float x, float y, float z) { + return Animation.constant(AnimationPose.builder().scale(x, y, z).build()); + } + + public static Animation xScale(float scale) { + return Animation.constant(AnimationPose.builder().scaleX(scale).build()); + } + + public static Animation yScale(float scale) { + return Animation.constant(AnimationPose.builder().scaleY(scale).build()); + } + + public static Animation zScale(float scale) { + return Animation.constant(AnimationPose.builder().scaleZ(scale).build()); + } + + public static Animation rotation(float x, float y, float z) { + return Animation.constant(AnimationPose.builder().rotateX(x).rotateY(y).rotateZ(z).build()); + } + + public static Animation xRotation(float angle) { + return Animation.constant(AnimationPose.builder().rotateX(angle).build()); + } + + public static Animation yRotation(float angle) { + return Animation.constant(AnimationPose.builder().rotateY(angle).build()); + } + + public static Animation zRotation(float angle) { + return Animation.constant(AnimationPose.builder().rotateZ(angle).build()); + } + //endregion + + //region Floating + public static Animation floating() { + return floating(40, 0.5f); + } public static Animation floating(int duration, float height) { return Animation.of() @@ -40,6 +81,8 @@ public static Animation floating(int duration, float height) { .keyframe(1.00f, AnimationPose.builder().translateY(height).build(), Easings.IN_OUT_SIN) .build(); } + //endregion + public static Animation spin(int duration) { return Animation.of() @@ -98,21 +141,53 @@ public static Animation wiggleZ(int duration, float amplitude) { } public static Animation zoomIn(int duration) { + return zoomIn(duration, 0f); + } + + public static Animation zoomIn(int duration, float startScale) { return Animation.of() .duration(duration) - .keyframe(0.00f, AnimationPose.builder().scale(0).build(), Easings.OUT_BACK) + .keyframe(0.00f, AnimationPose.builder().scale(startScale).build(), Easings.OUT_BACK) .keyframe(1.00f, AnimationPose.builder().scale(1f).build(), Easings.OUT_BACK) .build(); } public static Animation zoomOut(int duration) { + return zoomOut(duration, 0f); + } + + public static Animation zoomOut(int duration, float endScale) { return Animation.of() .duration(duration) .keyframe(0.00f, AnimationPose.builder().scale(1f).build(), Easings.IN_BACK) - .keyframe(1.00f, AnimationPose.builder().scale(0).build(), Easings.IN_BACK) + .keyframe(1.00f, AnimationPose.builder().scale(endScale).build(), Easings.IN_BACK) + .build(); + } + + public static Animation moveUp(int duration, float distance) { + return Animation.of() + .duration(duration) + .keyframe(0.00f, AnimationPose.builder().translateY(-distance).build(), Easings.OUT_BACK) + .keyframe(1.00f, AnimationPose.builder().translateY(0).build(), Easings.OUT_BACK) .build(); } + public static Animation moveUp(int duration) { + return moveUp(duration, 1.0f); + } + + public static Animation moveDown(int duration, float distance) { + return Animation.of() + .duration(duration) + .keyframe(0.00f, AnimationPose.builder().translateY(0).build(), Easings.IN_BACK) + .keyframe(1.00f, AnimationPose.builder().translateY(-distance).build(), Easings.IN_BACK) + .build(); + } + + public static Animation moveDown(int duration) { + return moveDown(duration, 1.0f); + } + //region Composite Animations public static Animation floatSpin() { From 9d1b453b5d669a06a0da4521f6d162c02db6e486 Mon Sep 17 00:00:00 2001 From: ImBit <151650938+ImBit@users.noreply.github.com> Date: Mon, 27 Jul 2026 22:43:03 +0100 Subject: [PATCH 2/6] :art: Improve exception logging. --- .../main/java/xyz/bitsquidd/bits/log/BasicLogger.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/api/src/main/java/xyz/bitsquidd/bits/log/BasicLogger.java b/api/src/main/java/xyz/bitsquidd/bits/log/BasicLogger.java index 56480e95..fe10f96e 100644 --- a/api/src/main/java/xyz/bitsquidd/bits/log/BasicLogger.java +++ b/api/src/main/java/xyz/bitsquidd/bits/log/BasicLogger.java @@ -88,10 +88,13 @@ public void exceptionInternal(final String msg, final Throwable throwable) { return; } - StackTraceElement[] stackTrace = throwable.getStackTrace(); + Throwable real = throwable; + while (real.getCause() != null && real.getCause() != real) real = real.getCause(); + + StackTraceElement[] stackTrace = real.getStackTrace(); String header; if (stackTrace.length == 0) { - header = String.format("%s |-> Exception (no stack trace) - %s", msg, throwable.getMessage()); + header = String.format("%s |-> Exception (no stack trace) - %s", msg, real.getMessage()); } else { StackTraceElement origin = stackTrace[0]; header = String.format( @@ -101,13 +104,13 @@ public void exceptionInternal(final String msg, final Throwable throwable) { origin.getMethodName(), origin.getFileName(), origin.getLineNumber(), - throwable.getMessage() + real.getMessage() ); } System.out.println(LogType.ERROR.format(header)); if (flags.logExtendedError()) { - for (StackTraceElement element : throwable.getStackTrace()) { + for (StackTraceElement element : real.getStackTrace()) { System.out.println(PrettyLogLevel.RED.formatMessage("\tat " + element)); } } From 0d6b03e35b4f79366ee53b93a2db3a811e211e33 Mon Sep 17 00:00:00 2001 From: ImBit <151650938+ImBit@users.noreply.github.com> Date: Wed, 29 Jul 2026 17:43:19 +0100 Subject: [PATCH 3/6] :art: Additive animations 2. --- .../bits/mc/animation/newe/Animation.java | 13 +++ .../bits/mc/animation/newe/AnimationData.java | 13 +++ .../mc/animation/newe/AnimationKeyframe.java | 57 ++++++++++ .../mc/animation/newe/AnimationPoseNew.java | 38 +++++++ .../mc/animation/newe/AnimationTimeline.java | 105 ++++++++++++++++++ 5 files changed, 226 insertions(+) create mode 100644 minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/newe/Animation.java create mode 100644 minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/newe/AnimationData.java create mode 100644 minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/newe/AnimationKeyframe.java create mode 100644 minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/newe/AnimationPoseNew.java create mode 100644 minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/newe/AnimationTimeline.java diff --git a/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/newe/Animation.java b/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/newe/Animation.java new file mode 100644 index 00000000..b2428807 --- /dev/null +++ b/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/newe/Animation.java @@ -0,0 +1,13 @@ +/* + * This file is part of a Bit libraries package. + * Licensed under the GNU Lesser General Public License v3.0. + * + * Copyright (c) 2023-2026 ImBit + */ + +package xyz.bitsquidd.bits.mc.animation.newe; + +public interface Animation { + + +} diff --git a/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/newe/AnimationData.java b/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/newe/AnimationData.java new file mode 100644 index 00000000..50253870 --- /dev/null +++ b/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/newe/AnimationData.java @@ -0,0 +1,13 @@ +/* + * This file is part of a Bit libraries package. + * Licensed under the GNU Lesser General Public License v3.0. + * + * Copyright (c) 2023-2026 ImBit + */ + +package xyz.bitsquidd.bits.mc.animation.newe; + +public record AnimationData( + long currentTick +) { +} diff --git a/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/newe/AnimationKeyframe.java b/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/newe/AnimationKeyframe.java new file mode 100644 index 00000000..c36b5422 --- /dev/null +++ b/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/newe/AnimationKeyframe.java @@ -0,0 +1,57 @@ +/* + * This file is part of a Bit libraries package. + * Licensed under the GNU Lesser General Public License v3.0. + * + * Copyright (c) 2023-2026 ImBit + */ + +package xyz.bitsquidd.bits.mc.animation.newe; + +import org.joml.Quaternionf; +import org.joml.Vector3f; + +import java.util.function.Function; + + +public sealed interface AnimationKeyframe { + + void applyTo(AnimationPoseNew pose, AnimationData data, float proportion); + + record Translation( + Function translation + ) implements AnimationKeyframe { + + @Override + public void applyTo(AnimationPoseNew pose, AnimationData data, float proportion) { + Vector3f effective = translation.apply(data).lerp(new Vector3f(), 1 - proportion); + pose.translation().add(effective); // Translation combines additively + } + + } + + record Rotation( + Function rotation + ) implements AnimationKeyframe { + + @Override + public void applyTo(AnimationPoseNew pose, AnimationData data, float proportion) { + Quaternionf effective = rotation.apply(data).slerp(new Quaternionf(), 1 - proportion); + pose.rotation().mul(effective); // Rotation combines multiplicatively + } + + } + + record Scale( + Function scale + ) implements AnimationKeyframe { + + @Override + public void applyTo(AnimationPoseNew pose, AnimationData data, float proportion) { + Vector3f effective = scale.apply(data).lerp(new Vector3f(1), 1 - proportion); + pose.scale().mul(effective); // Scale combines multiplicatively + } + + } + + +} diff --git a/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/newe/AnimationPoseNew.java b/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/newe/AnimationPoseNew.java new file mode 100644 index 00000000..132466b3 --- /dev/null +++ b/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/newe/AnimationPoseNew.java @@ -0,0 +1,38 @@ +/* + * This file is part of a Bit libraries package. + * Licensed under the GNU Lesser General Public License v3.0. + * + * Copyright (c) 2023-2026 ImBit + */ + +package xyz.bitsquidd.bits.mc.animation.newe; + +import org.joml.Quaternionf; +import org.joml.Vector3f; + + +public record AnimationPoseNew( + Vector3f translation, + Quaternionf rotation, + Vector3f scale +) { + + public static AnimationPoseNew identity() { + return new AnimationPoseNew( + new Vector3f(0), // No translation + new Quaternionf(), // No rotation + new Vector3f(1) // No scaling + ); + } + + public void add(AnimationKeyframe added) { + Vector3f addedTranslation = added.translation(); + Quaternionf addedRotation = added.rotation(); + Vector3f addedScale = added.scale(); + + if (addedTranslation != null) translation.add(addedTranslation); // Translation combines additively + if (addedRotation != null) rotation.mul(addedRotation); // Rotation combines multiplicatively + if (addedScale != null) scale.mul(addedScale); // Scale combines multiplicatively + } + +} diff --git a/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/newe/AnimationTimeline.java b/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/newe/AnimationTimeline.java new file mode 100644 index 00000000..e5894784 --- /dev/null +++ b/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/newe/AnimationTimeline.java @@ -0,0 +1,105 @@ +/* + * This file is part of a Bit libraries package. + * Licensed under the GNU Lesser General Public License v3.0. + * + * Copyright (c) 2023-2026 ImBit + */ + +package xyz.bitsquidd.bits.mc.animation.newe; + +import xyz.bitsquidd.bits.lifecycle.builder.Buildable; +import xyz.bitsquidd.bits.util.math.easing.Easing; +import xyz.bitsquidd.bits.wrapper.collection.pair.Pair; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + + +public class AnimationTimeline { + private record KeyframeRecord( + List> keyframes + // Keyframe and its proportion + ) {} + + private final List baked; + + private AnimationTimeline(List baked) { + this.baked = baked; + } + + + public void mutate(AnimationPoseNew pose, AnimationData data) { + int effectiveIndex = (int)data.currentTick() % baked.size(); + KeyframeRecord record = baked.get(effectiveIndex); + record.keyframes().forEach(pair -> pair.getFirst().applyTo(pose, data, pair.getSecond())); + } + + + public static final class Builder implements Buildable { + private final long ticks; + private final List entries = new ArrayList<>(); + + public Builder(long ticks) { + this.ticks = ticks; + } + + private record TimelineEntry( + long effectiveTick, + AnimationKeyframe frame, + Easing easing + ) {} + + + public Builder add(float proportion, AnimationKeyframe frame, Easing easing) { + entries.add(new TimelineEntry((long)(proportion * ticks), frame, easing)); + return this; + } + + + @Override + public AnimationTimeline build() { + if (entries.isEmpty()) throw new IllegalStateException("Cannot build an empty AnimationTimeline"); + + List sorted = new ArrayList<>(entries); + sorted.sort(Comparator.comparingLong(TimelineEntry::effectiveTick)); + + TimelineEntry first = sorted.getFirst(); + TimelineEntry last = sorted.getLast(); + + List baked = new ArrayList<>((int)ticks); + int segment = 0; + + for (long t = 0; t < ticks; t++) { + if (t <= first.effectiveTick()) { + baked.add(new KeyframeRecord(List.of(Pair.immutable(first.frame(), 1f)))); + continue; + } + if (t >= last.effectiveTick()) { + baked.add(new KeyframeRecord(List.of(Pair.immutable(last.frame(), 1f)))); + continue; + } + + while (segment < sorted.size() - 2 && t > sorted.get(segment + 1).effectiveTick()) { + segment++; + } + + TimelineEntry from = sorted.get(segment); + TimelineEntry to = sorted.get(segment + 1); + long span = to.effectiveTick() - from.effectiveTick(); + float local = span == 0 ? 1f : (t - from.effectiveTick()) / (float)span; + float eased = to.easing().progress(local); + + baked.add(new KeyframeRecord(List.of( + Pair.immutable(from.frame(), 1f - eased), + Pair.immutable(to.frame(), eased) + ))); + } + + return new AnimationTimeline(baked); + } + + } + + +} From c4cdf6ce4a273a611baed5d9f2d83b422315ab5e Mon Sep 17 00:00:00 2001 From: ImBit <151650938+ImBit@users.noreply.github.com> Date: Wed, 29 Jul 2026 19:30:04 +0100 Subject: [PATCH 4/6] :sparkles: Stable consumable animations. --- .../bits/mc/animation/AnimationPlayer.java | 36 ++-- .../bits/mc/animation/PaperDisplay.java | 56 +++++ .../bits/mc/animation/Animatable.java | 24 +++ .../bits/mc/animation/Animation.java | 197 ++++++++++++++++++ .../animation/{newe => }/AnimationData.java | 2 +- .../bits/mc/animation/AnimationKeyframe.java | 157 ++++++++++++++ .../bits/mc/animation/AnimationPose.java | 158 -------------- .../bits/mc/animation/AnimationPoseNew.java | 28 +++ ...opMode.java => AnimationProgressMode.java} | 5 +- .../bits/mc/animation/Animations.java | 149 ++++--------- .../bitsquidd/bits/mc/animation/Keyframe.java | 26 --- .../bits/mc/animation/impl/Animation.java | 45 ---- .../mc/animation/impl/BasicAnimation.java | 128 ------------ .../mc/animation/impl/CompositeAnimation.java | 35 ---- .../bits/mc/animation/impl/package-info.java | 4 - .../bits/mc/animation/newe/Animation.java | 13 -- .../mc/animation/newe/AnimationKeyframe.java | 57 ----- .../mc/animation/newe/AnimationPoseNew.java | 38 ---- .../mc/animation/newe/AnimationTimeline.java | 105 ---------- 19 files changed, 524 insertions(+), 739 deletions(-) create mode 100644 minecraft/paper/src/main/java/xyz/bitsquidd/bits/mc/animation/PaperDisplay.java create mode 100644 minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/Animatable.java create mode 100644 minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/Animation.java rename minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/{newe => }/AnimationData.java (82%) create mode 100644 minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/AnimationKeyframe.java delete mode 100644 minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/AnimationPose.java create mode 100644 minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/AnimationPoseNew.java rename minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/{AnimationLoopMode.java => AnimationProgressMode.java} (90%) delete mode 100644 minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/Keyframe.java delete mode 100644 minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/impl/Animation.java delete mode 100644 minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/impl/BasicAnimation.java delete mode 100644 minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/impl/CompositeAnimation.java delete mode 100644 minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/impl/package-info.java delete mode 100644 minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/newe/Animation.java delete mode 100644 minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/newe/AnimationKeyframe.java delete mode 100644 minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/newe/AnimationPoseNew.java delete mode 100644 minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/newe/AnimationTimeline.java diff --git a/minecraft/paper/src/main/java/xyz/bitsquidd/bits/mc/animation/AnimationPlayer.java b/minecraft/paper/src/main/java/xyz/bitsquidd/bits/mc/animation/AnimationPlayer.java index c547fc02..4cb666c6 100644 --- a/minecraft/paper/src/main/java/xyz/bitsquidd/bits/mc/animation/AnimationPlayer.java +++ b/minecraft/paper/src/main/java/xyz/bitsquidd/bits/mc/animation/AnimationPlayer.java @@ -10,32 +10,26 @@ import org.bukkit.scheduler.BukkitTask; import org.jetbrains.annotations.Nullable; -import xyz.bitsquidd.bits.mc.animation.impl.Animation; import xyz.bitsquidd.bits.paper.util.bukkit.runnable.Runnables; import java.util.function.Consumer; -// TODO -// Refine animation system. -public abstract class AnimationPlayer { +public final class AnimationPlayer { private final Animation animation; - private int currentTick = 0; - private @Nullable Consumer> onComplete; + private long currentTick = 0; + private @Nullable Consumer> onComplete; private @Nullable BukkitTask ticker; public AnimationPlayer(Animation animation) { this.animation = animation; } - public final void play(D display) { - ticker = Runnables.cleanup(ticker); - ticker = Runnables.timer(() -> tick(display, currentTick++), 0, 1); - } - - public final void pause() { + public final void play(A animatable) { ticker = Runnables.cleanup(ticker); + currentTick = 0; + ticker = Runnables.timer(() -> tick(animatable, currentTick++), 0, 1); } public final void stop() { @@ -44,21 +38,23 @@ public final void stop() { if (onComplete != null) onComplete.accept(this); } - public final AnimationPlayer onComplete(Consumer> callback) { + public final AnimationPlayer onComplete(Consumer> callback) { this.onComplete = callback; return this; } - public final void tick(D display, int tick) { - if (animation.isFinished(tick)) { + public final void tick(A animatable, long tick) { + AnimationData data = new AnimationData(tick); + + if (animation.isFinished(data)) { stop(); return; } - AnimationPose pose = animation.evaluate(tick); - applyPose(display, pose); + // Fresh identity every tick: animation.mutate() computes this tick's complete snapshot, + AnimationPoseNew pose = AnimationPoseNew.identity(); + animation.mutate(pose, data); + animatable.applyPose(pose); } - protected abstract void applyPose(D display, AnimationPose pose); - -} \ No newline at end of file +} diff --git a/minecraft/paper/src/main/java/xyz/bitsquidd/bits/mc/animation/PaperDisplay.java b/minecraft/paper/src/main/java/xyz/bitsquidd/bits/mc/animation/PaperDisplay.java new file mode 100644 index 00000000..e29c7cdf --- /dev/null +++ b/minecraft/paper/src/main/java/xyz/bitsquidd/bits/mc/animation/PaperDisplay.java @@ -0,0 +1,56 @@ +/* + * This file is part of a Bit libraries package. + * Licensed under the GNU Lesser General Public License v3.0. + * + * Copyright (c) 2023-2026 ImBit + */ + +package xyz.bitsquidd.bits.mc.animation; + +import org.bukkit.entity.Display; + +import org.bukkit.util.Transformation; +import org.joml.Quaternionf; +import org.joml.Vector3f; + + +public class PaperDisplay implements Animatable { + private final Display display; + + private PaperDisplay(Display display) { + this.display = display; + } + + public static PaperDisplay of(Display display) { + return new PaperDisplay(display); + } + + + @Override + public Vector3f getTranslation() { + return display.getTransformation().getTranslation(); + } + + @Override + public Quaternionf getRotation() { + return display.getTransformation().getLeftRotation(); + } + + @Override + public Vector3f getScale() { + return display.getTransformation().getScale(); + } + + @Override + public void applyPose(AnimationPoseNew pose) { + display.setTransformation( + new Transformation( + pose.translation(), + pose.rotation(), + pose.scale(), + new Quaternionf() // No right rotation, very few situations require this. + ) + ); + } + +} diff --git a/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/Animatable.java b/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/Animatable.java new file mode 100644 index 00000000..9519c0d7 --- /dev/null +++ b/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/Animatable.java @@ -0,0 +1,24 @@ +/* + * This file is part of a Bit libraries package. + * Licensed under the GNU Lesser General Public License v3.0. + * + * Copyright (c) 2023-2026 ImBit + */ + +package xyz.bitsquidd.bits.mc.animation; + +import org.joml.Quaternionf; +import org.joml.Vector3f; + + +public interface Animatable { + Vector3f getTranslation(); + + Quaternionf getRotation(); + + Vector3f getScale(); + + + void applyPose(AnimationPoseNew pose); + +} diff --git a/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/Animation.java b/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/Animation.java new file mode 100644 index 00000000..4d2fb7c1 --- /dev/null +++ b/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/Animation.java @@ -0,0 +1,197 @@ +/* + * This file is part of a Bit libraries package. + * Licensed under the GNU Lesser General Public License v3.0. + * + * Copyright (c) 2023-2026 ImBit + */ + +package xyz.bitsquidd.bits.mc.animation;/* + * This file is part of a Bit libraries package. + * Licensed under the GNU Lesser General Public License v3.0. + * + * Copyright (c) 2023-2026 ImBit + */ + + +import xyz.bitsquidd.bits.lifecycle.builder.Buildable; +import xyz.bitsquidd.bits.util.math.easing.Easing; +import xyz.bitsquidd.bits.wrapper.collection.pair.Pair; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + + +public sealed interface Animation { + + void mutate(AnimationPoseNew pose, AnimationData data); + + boolean isFinished(AnimationData data); + + default Animation and(Animation other) { + return new Compound(List.of(this, other)); + } + + + static Basic.Builder basic(long ticks) { + return new Basic.Builder(ticks); + } + + + final class Basic implements Animation { + private record KeyframeRecord( + // Keyframe and its proportion + List> keyframes + ) {} + + private final List baked; + private final long loops; + private final AnimationProgressMode loopMode; + + private Basic(List baked, long loops, AnimationProgressMode loopMode) { + this.baked = baked; + this.loops = loops; + this.loopMode = loopMode; + } + + + @Override + public void mutate(AnimationPoseNew pose, AnimationData data) { + int size = baked.size(); + long tick = data.currentTick(); + + float progress = loopMode.transform((float)tick / size); + int effectiveIndex = Math.min(size - 1, Math.round(progress * size)); + + KeyframeRecord record = baked.get(effectiveIndex); + record.keyframes().forEach(pair -> pair.getFirst().applyTo(pose, data, pair.getSecond())); + } + + @Override + public boolean isFinished(AnimationData data) { + return loops > 0 && data.currentTick() >= (baked.size() * loops); + } + + + public static final class Builder implements Buildable { + private final long ticks; + private final List entries = new ArrayList<>(); + + private long loops = -1; + private AnimationProgressMode loopMode = AnimationProgressMode.STRAIGHT; + + private Builder(long ticks) { + this.ticks = ticks; + } + + private record TimelineEntry( + long effectiveTick, + AnimationKeyframe frame, + Easing easing + ) {} + + + public Builder keyframe(float proportion, AnimationKeyframe frame, Easing easing) { + entries.add(new TimelineEntry((long)(proportion * ticks), frame, easing)); + return this; + } + + public Builder loops(long loops) { + this.loops = loops; + return this; + } + + public Builder loop(AnimationProgressMode loopMode) { + this.loopMode = loopMode; + return this; + } + + + @Override + public Basic build() { + if (entries.isEmpty()) throw new IllegalStateException("Cannot build an empty Animation.Basic"); + + List sorted = new ArrayList<>(entries); + sorted.sort(Comparator.comparingLong(TimelineEntry::effectiveTick)); + + TimelineEntry first = sorted.getFirst(); + TimelineEntry last = sorted.getLast(); + + List baked = new ArrayList<>((int)ticks); + int segment = 0; + + for (long t = 0; t < ticks; t++) { + if (t <= first.effectiveTick()) { + baked.add(new KeyframeRecord(List.of(Pair.immutable(first.frame(), 1f)))); + continue; + } + if (t >= last.effectiveTick()) { + baked.add(new KeyframeRecord(List.of(Pair.immutable(last.frame(), 1f)))); + continue; + } + + while (segment < sorted.size() - 2 && t > sorted.get(segment + 1).effectiveTick()) { + segment++; + } + + TimelineEntry from = sorted.get(segment); + TimelineEntry to = sorted.get(segment + 1); + long span = to.effectiveTick() - from.effectiveTick(); + float local = span == 0 ? 1f : (t - from.effectiveTick()) / (float)span; + float eased = to.easing().progress(local); + + baked.add(new KeyframeRecord(List.of( + Pair.immutable(from.frame(), 1f - eased), + Pair.immutable(to.frame(), eased) + ))); + } + + return new Basic(baked, loops, loopMode); + } + + } + + } + + final class Compound implements Animation { + private final List animations; + + private Compound(List animations) { + this.animations = animations; + } + + @Override + public void mutate(AnimationPoseNew pose, AnimationData data) { + for (Animation animation : animations) { + if (animation.isFinished(data)) continue; + animation.mutate(pose, data); + } + } + + @Override + public boolean isFinished(AnimationData data) { + return animations.stream().allMatch(a -> a.isFinished(data)); + } + + } + + final class Constant implements Animation { + private final AnimationKeyframe frame; + + private Constant(AnimationKeyframe frame) { + this.frame = frame; + } + + @Override + public void mutate(AnimationPoseNew pose, AnimationData data) { + frame.applyTo(pose, data, 1f); + } + + @Override + public boolean isFinished(AnimationData data) { + return false; + } + + } + +} diff --git a/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/newe/AnimationData.java b/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/AnimationData.java similarity index 82% rename from minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/newe/AnimationData.java rename to minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/AnimationData.java index 50253870..004508d4 100644 --- a/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/newe/AnimationData.java +++ b/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/AnimationData.java @@ -5,7 +5,7 @@ * Copyright (c) 2023-2026 ImBit */ -package xyz.bitsquidd.bits.mc.animation.newe; +package xyz.bitsquidd.bits.mc.animation; public record AnimationData( long currentTick diff --git a/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/AnimationKeyframe.java b/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/AnimationKeyframe.java new file mode 100644 index 00000000..442973fb --- /dev/null +++ b/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/AnimationKeyframe.java @@ -0,0 +1,157 @@ +/* + * This file is part of a Bit libraries package. + * Licensed under the GNU Lesser General Public License v3.0. + * + * Copyright (c) 2023-2026 ImBit + */ + +package xyz.bitsquidd.bits.mc.animation; + +import org.joml.Quaternionf; +import org.joml.Vector3f; + +import java.util.function.Function; + + +public sealed interface AnimationKeyframe { + + void applyTo(AnimationPoseNew pose, AnimationData data, float proportion); + + default AnimationKeyframe and(AnimationKeyframe other) { + return new Combined(this, other); + } + + final class Combined implements AnimationKeyframe { + private final AnimationKeyframe[] keyframes; + + public Combined(AnimationKeyframe... keyframes) { + this.keyframes = keyframes; + } + + @Override + public void applyTo(AnimationPoseNew pose, AnimationData data, float proportion) { + for (AnimationKeyframe keyframe : keyframes) { + keyframe.applyTo(pose, data, proportion); + } + } + + } + + + record Translation( + Function translation + ) implements AnimationKeyframe { + + public static Translation empty() { + return new Translation(d -> new Vector3f()); + } + + public static Translation of(Vector3f translation) { + return new Translation(d -> translation); + } + + public static Translation of(float x, float y, float z) { + return new Translation(d -> new Vector3f(x, y, z)); + } + + public static Translation x(float x) { + return new Translation(d -> new Vector3f(x, 0, 0)); + } + + public static Translation y(float y) { + return new Translation(d -> new Vector3f(0, y, 0)); + } + + public static Translation z(float z) { + return new Translation(d -> new Vector3f(0, 0, z)); + } + + + @Override + public void applyTo(AnimationPoseNew pose, AnimationData data, float proportion) { + Vector3f effective = translation.apply(data).lerp(new Vector3f(), 1 - proportion); + pose.translation().add(effective); // Translation combines additively + } + + } + + record Rotation( + Function rotation + ) implements AnimationKeyframe { + + public static Rotation empty() { + return new Rotation(d -> new Quaternionf()); + } + + public static Rotation of(Quaternionf rotation) { + return new Rotation(d -> rotation); + } + + public static Rotation of(float x, float y, float z, float w) { + return new Rotation(d -> new Quaternionf(x, y, z, w)); + } + + public static Rotation x(float x) { + return new Rotation(d -> new Quaternionf().rotateX(x)); + } + + public static Rotation y(float y) { + return new Rotation(d -> new Quaternionf().rotateY(y)); + } + + public static Rotation z(float z) { + return new Rotation(d -> new Quaternionf().rotateZ(z)); + } + + + @Override + public void applyTo(AnimationPoseNew pose, AnimationData data, float proportion) { + Quaternionf effective = rotation.apply(data).slerp(new Quaternionf(), 1 - proportion); + pose.rotation().mul(effective); // Rotation combines multiplicatively + } + + } + + record Scale( + Function scale + ) implements AnimationKeyframe { + + public static Scale empty() { + return new Scale(d -> new Vector3f(1)); + } + + public static Scale of(Vector3f scale) { + return new Scale(d -> scale); + } + + public static Scale of(float x, float y, float z) { + return new Scale(d -> new Vector3f(x, y, z)); + } + + public static Scale of(float s) { + return new Scale(d -> new Vector3f(s, s, s)); + } + + public static Scale x(float x) { + return new Scale(d -> new Vector3f(x, 1, 1)); + } + + public static Scale y(float y) { + return new Scale(d -> new Vector3f(1, y, 1)); + } + + public static Scale z(float z) { + return new Scale(d -> new Vector3f(1, 1, z)); + } + + + @Override + public void applyTo(AnimationPoseNew pose, AnimationData data, float proportion) { + Vector3f effective = scale.apply(data).lerp(new Vector3f(1), 1 - proportion); + pose.scale().mul(effective); // Scale combines multiplicatively + } + + } + + +} diff --git a/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/AnimationPose.java b/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/AnimationPose.java deleted file mode 100644 index a22dfc8e..00000000 --- a/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/AnimationPose.java +++ /dev/null @@ -1,158 +0,0 @@ -/* - * This file is part of a Bit libraries package. - * Licensed under the GNU Lesser General Public License v3.0. - * - * Copyright (c) 2023-2026 ImBit - */ - -package xyz.bitsquidd.bits.mc.animation;/* - * This file is part of a Bit libraries package. - * Licensed under the GNU Lesser General Public License v3.0. - * - * Copyright (c) 2023-2026 ImBit - */ - -import org.joml.Quaternionf; -import org.joml.Vector3f; - -import xyz.bitsquidd.bits.lifecycle.builder.Buildable; - - -/** - * Represents the pose of an animation at a specific point in time, consisting of translation, rotation, and scale. - *

- * Each component is optional, allowing for partial poses that can be merged with others. - * For example, null translation means "don't change translation", and thus will be ignored when merging with other poses. - * - * @since 0.0.13 - */ -public record AnimationPose( - Vector3f translation, - Quaternionf rotation, - Vector3f scale -) { - - public static AnimationPose identity() { - return new AnimationPose(new Vector3f(), new Quaternionf(), new Vector3f(1)); - } - - public static AnimationPose add(AnimationPose a, AnimationPose b) { - return new AnimationPose( - new Vector3f(a.translation).add(b.translation), // Translation combines additively - new Quaternionf(a.rotation).mul(b.rotation), // Rotation combines multiplicatively - new Vector3f(a.scale).mul(b.scale) // Scale combines multiplicatively - ); - } - - public static AnimationPose multiply(AnimationPose pose, float scalar) { - return new AnimationPose( - new Vector3f(pose.translation).mul(scalar), // Scale translation linearly - new Quaternionf(pose.rotation).slerp(new Quaternionf(), scalar), // Slerp towards identity rotation - new Vector3f(1).lerp(pose.scale, scalar) // Lerp scale towards 1 - ); - } - - public static AnimationPose lerp(AnimationPose from, AnimationPose to, float progress) { - return new AnimationPose( - new Vector3f(from.translation).lerp(to.translation, progress), // Lerp translation linearly - new Quaternionf(from.rotation).slerp(to.rotation, progress), // Slerp rotation - new Vector3f(from.scale).lerp(to.scale, progress) // Lerp scale linearly - ); - } - - - public static Builder builder() { - return new Builder(); - } - - public static final class Builder implements Buildable { - private Vector3f translation = new Vector3f(); - private Quaternionf rotation = new Quaternionf(); - private Vector3f scale = new Vector3f(1); - - private Builder() {} - - public Builder translation(float x, float y, float z) { - this.translation = new Vector3f(x, y, z); - return this; - } - - public Builder translation(Vector3f v) { - return translation(v.x, v.y, v.z); - } - - public Builder translateX(float x) { - translation.x = x; - return this; - } - - public Builder translateY(float y) { - translation.y = y; - return this; - } - - public Builder translateZ(float z) { - translation.z = z; - return this; - } - - - public Builder rotation(Quaternionf rotation) { - this.rotation = new Quaternionf(rotation); - return this; - } - - - public Builder rotateX(float degrees) { - rotation.rotateX((float)Math.toRadians(degrees)); - return this; - } - - public Builder rotateY(float degrees) { - rotation.rotateY((float)Math.toRadians(degrees)); - return this; - } - - public Builder rotateZ(float degrees) { - rotation.rotateZ((float)Math.toRadians(degrees)); - return this; - } - - - public Builder scale(float x, float y, float z) { - this.scale = new Vector3f(x, y, z); - return this; - } - - public Builder scale(Vector3f v) { - return scale(v.x, v.y, v.z); - } - - public Builder scale(float uniform) { - return scale(uniform, uniform, uniform); - } - - public Builder scaleX(float x) { - scale.x = x; - return this; - } - - public Builder scaleY(float y) { - scale.y = y; - return this; - } - - public Builder scaleZ(float z) { - scale.z = z; - return this; - } - - - @Override - public AnimationPose build() { - return new AnimationPose(translation, rotation, scale); - } - - } - -} \ No newline at end of file diff --git a/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/AnimationPoseNew.java b/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/AnimationPoseNew.java new file mode 100644 index 00000000..6a485fe9 --- /dev/null +++ b/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/AnimationPoseNew.java @@ -0,0 +1,28 @@ +/* + * This file is part of a Bit libraries package. + * Licensed under the GNU Lesser General Public License v3.0. + * + * Copyright (c) 2023-2026 ImBit + */ + +package xyz.bitsquidd.bits.mc.animation; + +import org.joml.Quaternionf; +import org.joml.Vector3f; + + +public record AnimationPoseNew( + Vector3f translation, + Quaternionf rotation, + Vector3f scale +) { + + public static AnimationPoseNew identity() { + return new AnimationPoseNew( + new Vector3f(0), // No translation + new Quaternionf(), // No rotation + new Vector3f(1) // No scaling + ); + } + +} diff --git a/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/AnimationLoopMode.java b/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/AnimationProgressMode.java similarity index 90% rename from minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/AnimationLoopMode.java rename to minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/AnimationProgressMode.java index 5c4214da..c042db39 100644 --- a/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/AnimationLoopMode.java +++ b/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/AnimationProgressMode.java @@ -11,12 +11,13 @@ import java.util.function.Function; + /** * Defines how an animation should play. * * @since 0.0.13 */ -public enum AnimationLoopMode { +public enum AnimationProgressMode { // Loops back to the start STRAIGHT(progress -> progress % 1), // Reverses direction each cycle @@ -33,7 +34,7 @@ public enum AnimationLoopMode { private final Function progressTransformer; - AnimationLoopMode(Function progressTransformer) { + AnimationProgressMode(Function progressTransformer) { this.progressTransformer = progressTransformer; } diff --git a/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/Animations.java b/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/Animations.java index cb4f83d1..5fb647b2 100644 --- a/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/Animations.java +++ b/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/Animations.java @@ -7,136 +7,75 @@ package xyz.bitsquidd.bits.mc.animation; -import xyz.bitsquidd.bits.mc.animation.impl.Animation; +import org.joml.Quaternionf; + import xyz.bitsquidd.bits.util.math.easing.Easings; public final class Animations { private Animations() {} - //region Generic transformations - public static Animation translation(float x, float y, float z) { - return Animation.constant(AnimationPose.builder().translation(x, y, z).build()); - } - - public static Animation xTranslation(float distance) { - return Animation.constant(AnimationPose.builder().translateX(distance).build()); - } - - public static Animation yTranslation(float height) { - return Animation.constant(AnimationPose.builder().translateY(height).build()); - } - - public static Animation zTranslation(float distance) { - return Animation.constant(AnimationPose.builder().translateZ(distance).build()); - } - - public static Animation scale(float scale) { - return Animation.constant(AnimationPose.builder().scale(scale).build()); - } - - public static Animation scale(float x, float y, float z) { - return Animation.constant(AnimationPose.builder().scale(x, y, z).build()); - } - - public static Animation xScale(float scale) { - return Animation.constant(AnimationPose.builder().scaleX(scale).build()); - } - - public static Animation yScale(float scale) { - return Animation.constant(AnimationPose.builder().scaleY(scale).build()); - } - - public static Animation zScale(float scale) { - return Animation.constant(AnimationPose.builder().scaleZ(scale).build()); - } - - public static Animation rotation(float x, float y, float z) { - return Animation.constant(AnimationPose.builder().rotateX(x).rotateY(y).rotateZ(z).build()); - } - - public static Animation xRotation(float angle) { - return Animation.constant(AnimationPose.builder().rotateX(angle).build()); - } - - public static Animation yRotation(float angle) { - return Animation.constant(AnimationPose.builder().rotateY(angle).build()); - } - - public static Animation zRotation(float angle) { - return Animation.constant(AnimationPose.builder().rotateZ(angle).build()); - } - //endregion - //region Floating public static Animation floating() { return floating(40, 0.5f); } public static Animation floating(int duration, float height) { - return Animation.of() - .duration(duration) - .loop(AnimationLoopMode.PING_PONG) - .keyframe(0.00f, AnimationPose.builder().translateY(0f).build(), Easings.IN_OUT_SIN) - .keyframe(1.00f, AnimationPose.builder().translateY(height).build(), Easings.IN_OUT_SIN) + return Animation.basic(duration) + .loop(AnimationProgressMode.PING_PONG) + .keyframe(0.00f, AnimationKeyframe.Translation.y(0f), Easings.IN_OUT_SIN) + .keyframe(1.00f, AnimationKeyframe.Translation.y(height), Easings.IN_OUT_SIN) .build(); } //endregion public static Animation spin(int duration) { - return Animation.of() - .duration(duration) - .loop(AnimationLoopMode.STRAIGHT) - .keyframe(0.00f, AnimationPose.builder().rotateY(0f).build(), Easings.IN_OUT_SIN.blend(Easings.LINEAR, 0.5f)) - .keyframe(0.50f, AnimationPose.builder().rotateY(180f).build(), Easings.IN_OUT_SIN.blend(Easings.LINEAR, 0.5f)) - .keyframe(1.00f, AnimationPose.builder().rotateY(360f).build(), Easings.IN_OUT_SIN.blend(Easings.LINEAR, 0.5f)) + return Animation.basic(duration) + .loop(AnimationProgressMode.STRAIGHT) + .keyframe(0.00f, AnimationKeyframe.Rotation.empty(), Easings.IN_OUT_SIN.blend(Easings.LINEAR, 0.5f)) + .keyframe(1.00f, new AnimationKeyframe.Rotation(d -> new Quaternionf().rotateY((float)(Math.PI * 2f))), Easings.IN_OUT_SIN.blend(Easings.LINEAR, 0.5f)) .build(); } public static Animation pulse(int duration, float minScale, float maxScale) { - return Animation.of() - .duration(duration) - .loop(AnimationLoopMode.PING_PONG) - .keyframe(0.00f, AnimationPose.builder().scale(minScale).build(), Easings.IN_OUT_SIN) - .keyframe(1.00f, AnimationPose.builder().scale(maxScale).build(), Easings.IN_OUT_SIN) + return Animation.basic(duration) + .loop(AnimationProgressMode.PING_PONG) + .keyframe(0.00f, AnimationKeyframe.Scale.of(minScale), Easings.IN_OUT_SIN) + .keyframe(1.00f, AnimationKeyframe.Scale.of(maxScale), Easings.IN_OUT_SIN) .build(); } public static Animation swayX(int duration, float angle) { - return Animation.of() - .duration(duration) - .loop(AnimationLoopMode.PING_PONG) - .keyframe(0.00f, AnimationPose.builder().rotateX(-angle).build(), Easings.IN_OUT_SIN) - .keyframe(1.00f, AnimationPose.builder().rotateX(angle).build(), Easings.IN_OUT_SIN) + return Animation.basic(duration) + .loop(AnimationProgressMode.PING_PONG) + .keyframe(0.00f, AnimationKeyframe.Rotation.x(-angle), Easings.IN_OUT_SIN) + .keyframe(1.00f, AnimationKeyframe.Rotation.x(angle), Easings.IN_OUT_SIN) .build(); } public static Animation swayZ(int duration, float angle) { - return Animation.of() - .duration(duration) - .loop(AnimationLoopMode.PING_PONG) - .keyframe(0.00f, AnimationPose.builder().rotateZ(-angle).build(), Easings.IN_OUT_SIN) - .keyframe(1.00f, AnimationPose.builder().rotateZ(angle).build(), Easings.IN_OUT_SIN) + return Animation.basic(duration) + .loop(AnimationProgressMode.PING_PONG) + .keyframe(0.00f, AnimationKeyframe.Rotation.z(-angle), Easings.IN_OUT_SIN) + .keyframe(1.00f, AnimationKeyframe.Rotation.z(angle), Easings.IN_OUT_SIN) .build(); } public static Animation wiggleX(int duration, float amplitude) { - return Animation.of() - .duration(duration) - .loop(AnimationLoopMode.PING_PONG) - .keyframe(0.00f, AnimationPose.builder().translateX(-amplitude).build(), Easings.IN_OUT_SIN) - .keyframe(1.00f, AnimationPose.builder().translateX(amplitude).build(), Easings.IN_OUT_SIN) + return Animation.basic(duration) + .loop(AnimationProgressMode.PING_PONG) + .keyframe(0.00f, AnimationKeyframe.Translation.x(-amplitude), Easings.IN_OUT_SIN) + .keyframe(1.00f, AnimationKeyframe.Translation.x(amplitude), Easings.IN_OUT_SIN) .build(); } public static Animation wiggleZ(int duration, float amplitude) { - return Animation.of() - .duration(duration) - .loop(AnimationLoopMode.PING_PONG) - .keyframe(0.00f, AnimationPose.builder().translateZ(-amplitude).build(), Easings.IN_OUT_SIN) - .keyframe(1.00f, AnimationPose.builder().translateZ(amplitude).build(), Easings.IN_OUT_SIN) + return Animation.basic(duration) + .loop(AnimationProgressMode.PING_PONG) + .keyframe(0.00f, AnimationKeyframe.Translation.z(-amplitude), Easings.IN_OUT_SIN) + .keyframe(1.00f, AnimationKeyframe.Translation.z(amplitude), Easings.IN_OUT_SIN) .build(); } @@ -145,10 +84,9 @@ public static Animation zoomIn(int duration) { } public static Animation zoomIn(int duration, float startScale) { - return Animation.of() - .duration(duration) - .keyframe(0.00f, AnimationPose.builder().scale(startScale).build(), Easings.OUT_BACK) - .keyframe(1.00f, AnimationPose.builder().scale(1f).build(), Easings.OUT_BACK) + return Animation.basic(duration) + .keyframe(0.00f, AnimationKeyframe.Scale.of(startScale), Easings.OUT_BACK) + .keyframe(1.00f, AnimationKeyframe.Scale.empty(), Easings.OUT_BACK) .build(); } @@ -157,18 +95,16 @@ public static Animation zoomOut(int duration) { } public static Animation zoomOut(int duration, float endScale) { - return Animation.of() - .duration(duration) - .keyframe(0.00f, AnimationPose.builder().scale(1f).build(), Easings.IN_BACK) - .keyframe(1.00f, AnimationPose.builder().scale(endScale).build(), Easings.IN_BACK) + return Animation.basic(duration) + .keyframe(0.00f, AnimationKeyframe.Scale.empty(), Easings.IN_BACK) + .keyframe(1.00f, AnimationKeyframe.Scale.of(endScale), Easings.IN_BACK) .build(); } public static Animation moveUp(int duration, float distance) { - return Animation.of() - .duration(duration) - .keyframe(0.00f, AnimationPose.builder().translateY(-distance).build(), Easings.OUT_BACK) - .keyframe(1.00f, AnimationPose.builder().translateY(0).build(), Easings.OUT_BACK) + return Animation.basic(duration) + .keyframe(0.00f, AnimationKeyframe.Translation.y(-distance), Easings.OUT_BACK) + .keyframe(1.00f, AnimationKeyframe.Translation.empty(), Easings.OUT_BACK) .build(); } @@ -177,10 +113,9 @@ public static Animation moveUp(int duration) { } public static Animation moveDown(int duration, float distance) { - return Animation.of() - .duration(duration) - .keyframe(0.00f, AnimationPose.builder().translateY(0).build(), Easings.IN_BACK) - .keyframe(1.00f, AnimationPose.builder().translateY(-distance).build(), Easings.IN_BACK) + return Animation.basic(duration) + .keyframe(0.00f, AnimationKeyframe.Translation.empty(), Easings.IN_BACK) + .keyframe(1.00f, AnimationKeyframe.Translation.y(-distance), Easings.IN_BACK) .build(); } diff --git a/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/Keyframe.java b/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/Keyframe.java deleted file mode 100644 index 3fec8ce8..00000000 --- a/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/Keyframe.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * This file is part of a Bit libraries package. - * Licensed under the GNU Lesser General Public License v3.0. - * - * Copyright (c) 2023-2026 ImBit - */ - -package xyz.bitsquidd.bits.mc.animation; - -import xyz.bitsquidd.bits.util.math.easing.Easing; - -public record Keyframe( - float progress, - AnimationPose pose, - Easing easing -) { - - public static Keyframe of(float progress, AnimationPose pose, Easing easing) { - return new Keyframe(progress, pose, easing); - } - - public static Keyframe of(float progress, AnimationPose pose) { - return new Keyframe(progress, pose, new Easing.Linear()); - } - -} \ No newline at end of file diff --git a/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/impl/Animation.java b/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/impl/Animation.java deleted file mode 100644 index 233a6f4c..00000000 --- a/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/impl/Animation.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * This file is part of a Bit libraries package. - * Licensed under the GNU Lesser General Public License v3.0. - * - * Copyright (c) 2023-2026 ImBit - */ - -package xyz.bitsquidd.bits.mc.animation.impl; - -import xyz.bitsquidd.bits.mc.animation.AnimationPose; - -public abstract class Animation { - - public abstract AnimationPose evaluate(int ticks); - - public abstract boolean isFinished(int ticks); - - - public final Animation and(Animation... others) { - Animation[] all = new Animation[others.length + 1]; - all[0] = this; - System.arraycopy(others, 0, all, 1, others.length); - return new CompositeAnimation(all); - } - - - public static BasicAnimation.Builder of() { - return new BasicAnimation.Builder(); - } - - public static Animation constant(AnimationPose pose) { - return new Animation() { - @Override - public AnimationPose evaluate(int ticks) { - return pose; - } - - @Override - public boolean isFinished(int ticks) { - return false; - } - }; - } - -} \ No newline at end of file diff --git a/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/impl/BasicAnimation.java b/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/impl/BasicAnimation.java deleted file mode 100644 index abc7feaf..00000000 --- a/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/impl/BasicAnimation.java +++ /dev/null @@ -1,128 +0,0 @@ -/* - * This file is part of a Bit libraries package. - * Licensed under the GNU Lesser General Public License v3.0. - * - * Copyright (c) 2023-2026 ImBit - */ - -package xyz.bitsquidd.bits.mc.animation.impl; - -import com.google.common.collect.ImmutableList; -import org.jetbrains.annotations.Nullable; - -import xyz.bitsquidd.bits.lifecycle.builder.Buildable; -import xyz.bitsquidd.bits.mc.animation.AnimationLoopMode; -import xyz.bitsquidd.bits.mc.animation.AnimationPose; -import xyz.bitsquidd.bits.mc.animation.Keyframe; -import xyz.bitsquidd.bits.util.math.easing.Easing; - -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; - -public final class BasicAnimation extends Animation { - private final ImmutableList keyframes; - private final int durationTicks; - private final int framerate; - private final AnimationLoopMode animationLoopMode; - private final int loopCount; - private final int delayTicks; - - private BasicAnimation(Builder builder) { - this.keyframes = ImmutableList.copyOf(builder.keyframes.stream() - .sorted(Comparator.comparingDouble(Keyframe::progress)) - .toList()); - this.durationTicks = builder.durationTicks; - this.framerate = builder.framerate; - this.animationLoopMode = builder.loopMode; - this.loopCount = builder.loopCount == null ? 1 : builder.loopCount; - this.delayTicks = builder.delayTicks; - } - - @Override - public AnimationPose evaluate(int ticks) { - if (isFinished(ticks)) return AnimationPose.identity(); - if (keyframes.isEmpty()) return AnimationPose.identity(); - if (ticks < delayTicks) return AnimationPose.identity(); - if (ticks % framerate != 0) return AnimationPose.identity(); - if (keyframes.size() == 1) return keyframes.getFirst().pose(); - - float progress = animationLoopMode.transform((float)(ticks - delayTicks) / (float)durationTicks); - - AnimationPose result = AnimationPose.identity(); - for (int i = 0; i < keyframes.size() - 1; i++) { - Keyframe kf = keyframes.get(i); - Keyframe next = keyframes.get(i + 1); - if (progress < kf.progress()) break; - - float intervalLength = next.progress() - kf.progress(); - float localProgress = Math.min((progress - kf.progress()) / intervalLength, 1f); - float easedProgress = kf.easing().progress(localProgress); - - result = AnimationPose.lerp(kf.pose(), next.pose(), easedProgress); - } - return result; - } - - @Override - public boolean isFinished(int ticks) { - if (loopCount < 0) return false; // infinite - return ticks >= delayTicks + (durationTicks * loopCount); - } - - public static class Builder implements Buildable { - private final List keyframes = new ArrayList<>(); - private int durationTicks = 20; - private int framerate = 1; - private AnimationLoopMode loopMode = AnimationLoopMode.STRAIGHT; - private @Nullable Integer loopCount = null; - private int delayTicks = 0; - - public Builder duration(int ticks) { - this.durationTicks = ticks; - return this; - } - - public Builder framerate(int ticks) { - this.framerate = ticks; - return this; - } - - public Builder loop(AnimationLoopMode mode) { - this.loopMode = mode; - if (loopCount == null) this.loopCount = -1; - return this; - } - - public Builder loopCount(int count) { - this.loopCount = count; - return this; - } - - public Builder delay(int ticks) { - this.delayTicks = ticks; - return this; - } - - public Builder keyframe(Keyframe keyframe) { - this.keyframes.add(keyframe); - return this; - } - - public Builder keyframe(float progress, AnimationPose pose) { - return keyframe(Keyframe.of(progress, pose)); - } - - public Builder keyframe(float progress, AnimationPose pose, Easing easing) { - return keyframe(Keyframe.of(progress, pose, easing)); - } - - @Override - public BasicAnimation build() { - if (keyframes.isEmpty()) throw new IllegalStateException("Animation requires at least 1 keyframe"); - return new BasicAnimation(this); - } - - } - -} \ No newline at end of file diff --git a/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/impl/CompositeAnimation.java b/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/impl/CompositeAnimation.java deleted file mode 100644 index 95590daf..00000000 --- a/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/impl/CompositeAnimation.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * This file is part of a Bit libraries package. - * Licensed under the GNU Lesser General Public License v3.0. - * - * Copyright (c) 2023-2026 ImBit - */ - -package xyz.bitsquidd.bits.mc.animation.impl; - -import xyz.bitsquidd.bits.mc.animation.AnimationPose; - -import java.util.Arrays; - -public final class CompositeAnimation extends Animation { - private final Animation[] animations; - - public CompositeAnimation(Animation... animations) { - this.animations = animations; - } - - @Override - public AnimationPose evaluate(int ticks) { - AnimationPose result = AnimationPose.identity(); - for (Animation animation : animations) { - result = AnimationPose.add(result, animation.evaluate(ticks)); - } - return result; - } - - @Override - public boolean isFinished(int ticks) { - return Arrays.stream(animations).allMatch(a -> a.isFinished(ticks)); - } - -} \ No newline at end of file diff --git a/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/impl/package-info.java b/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/impl/package-info.java deleted file mode 100644 index 67f48dda..00000000 --- a/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/impl/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -@NotNullByDefault -package xyz.bitsquidd.bits.mc.animation.impl; - -import org.jetbrains.annotations.NotNullByDefault; \ No newline at end of file diff --git a/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/newe/Animation.java b/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/newe/Animation.java deleted file mode 100644 index b2428807..00000000 --- a/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/newe/Animation.java +++ /dev/null @@ -1,13 +0,0 @@ -/* - * This file is part of a Bit libraries package. - * Licensed under the GNU Lesser General Public License v3.0. - * - * Copyright (c) 2023-2026 ImBit - */ - -package xyz.bitsquidd.bits.mc.animation.newe; - -public interface Animation { - - -} diff --git a/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/newe/AnimationKeyframe.java b/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/newe/AnimationKeyframe.java deleted file mode 100644 index c36b5422..00000000 --- a/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/newe/AnimationKeyframe.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * This file is part of a Bit libraries package. - * Licensed under the GNU Lesser General Public License v3.0. - * - * Copyright (c) 2023-2026 ImBit - */ - -package xyz.bitsquidd.bits.mc.animation.newe; - -import org.joml.Quaternionf; -import org.joml.Vector3f; - -import java.util.function.Function; - - -public sealed interface AnimationKeyframe { - - void applyTo(AnimationPoseNew pose, AnimationData data, float proportion); - - record Translation( - Function translation - ) implements AnimationKeyframe { - - @Override - public void applyTo(AnimationPoseNew pose, AnimationData data, float proportion) { - Vector3f effective = translation.apply(data).lerp(new Vector3f(), 1 - proportion); - pose.translation().add(effective); // Translation combines additively - } - - } - - record Rotation( - Function rotation - ) implements AnimationKeyframe { - - @Override - public void applyTo(AnimationPoseNew pose, AnimationData data, float proportion) { - Quaternionf effective = rotation.apply(data).slerp(new Quaternionf(), 1 - proportion); - pose.rotation().mul(effective); // Rotation combines multiplicatively - } - - } - - record Scale( - Function scale - ) implements AnimationKeyframe { - - @Override - public void applyTo(AnimationPoseNew pose, AnimationData data, float proportion) { - Vector3f effective = scale.apply(data).lerp(new Vector3f(1), 1 - proportion); - pose.scale().mul(effective); // Scale combines multiplicatively - } - - } - - -} diff --git a/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/newe/AnimationPoseNew.java b/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/newe/AnimationPoseNew.java deleted file mode 100644 index 132466b3..00000000 --- a/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/newe/AnimationPoseNew.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * This file is part of a Bit libraries package. - * Licensed under the GNU Lesser General Public License v3.0. - * - * Copyright (c) 2023-2026 ImBit - */ - -package xyz.bitsquidd.bits.mc.animation.newe; - -import org.joml.Quaternionf; -import org.joml.Vector3f; - - -public record AnimationPoseNew( - Vector3f translation, - Quaternionf rotation, - Vector3f scale -) { - - public static AnimationPoseNew identity() { - return new AnimationPoseNew( - new Vector3f(0), // No translation - new Quaternionf(), // No rotation - new Vector3f(1) // No scaling - ); - } - - public void add(AnimationKeyframe added) { - Vector3f addedTranslation = added.translation(); - Quaternionf addedRotation = added.rotation(); - Vector3f addedScale = added.scale(); - - if (addedTranslation != null) translation.add(addedTranslation); // Translation combines additively - if (addedRotation != null) rotation.mul(addedRotation); // Rotation combines multiplicatively - if (addedScale != null) scale.mul(addedScale); // Scale combines multiplicatively - } - -} diff --git a/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/newe/AnimationTimeline.java b/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/newe/AnimationTimeline.java deleted file mode 100644 index e5894784..00000000 --- a/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/newe/AnimationTimeline.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - * This file is part of a Bit libraries package. - * Licensed under the GNU Lesser General Public License v3.0. - * - * Copyright (c) 2023-2026 ImBit - */ - -package xyz.bitsquidd.bits.mc.animation.newe; - -import xyz.bitsquidd.bits.lifecycle.builder.Buildable; -import xyz.bitsquidd.bits.util.math.easing.Easing; -import xyz.bitsquidd.bits.wrapper.collection.pair.Pair; - -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; - - -public class AnimationTimeline { - private record KeyframeRecord( - List> keyframes - // Keyframe and its proportion - ) {} - - private final List baked; - - private AnimationTimeline(List baked) { - this.baked = baked; - } - - - public void mutate(AnimationPoseNew pose, AnimationData data) { - int effectiveIndex = (int)data.currentTick() % baked.size(); - KeyframeRecord record = baked.get(effectiveIndex); - record.keyframes().forEach(pair -> pair.getFirst().applyTo(pose, data, pair.getSecond())); - } - - - public static final class Builder implements Buildable { - private final long ticks; - private final List entries = new ArrayList<>(); - - public Builder(long ticks) { - this.ticks = ticks; - } - - private record TimelineEntry( - long effectiveTick, - AnimationKeyframe frame, - Easing easing - ) {} - - - public Builder add(float proportion, AnimationKeyframe frame, Easing easing) { - entries.add(new TimelineEntry((long)(proportion * ticks), frame, easing)); - return this; - } - - - @Override - public AnimationTimeline build() { - if (entries.isEmpty()) throw new IllegalStateException("Cannot build an empty AnimationTimeline"); - - List sorted = new ArrayList<>(entries); - sorted.sort(Comparator.comparingLong(TimelineEntry::effectiveTick)); - - TimelineEntry first = sorted.getFirst(); - TimelineEntry last = sorted.getLast(); - - List baked = new ArrayList<>((int)ticks); - int segment = 0; - - for (long t = 0; t < ticks; t++) { - if (t <= first.effectiveTick()) { - baked.add(new KeyframeRecord(List.of(Pair.immutable(first.frame(), 1f)))); - continue; - } - if (t >= last.effectiveTick()) { - baked.add(new KeyframeRecord(List.of(Pair.immutable(last.frame(), 1f)))); - continue; - } - - while (segment < sorted.size() - 2 && t > sorted.get(segment + 1).effectiveTick()) { - segment++; - } - - TimelineEntry from = sorted.get(segment); - TimelineEntry to = sorted.get(segment + 1); - long span = to.effectiveTick() - from.effectiveTick(); - float local = span == 0 ? 1f : (t - from.effectiveTick()) / (float)span; - float eased = to.easing().progress(local); - - baked.add(new KeyframeRecord(List.of( - Pair.immutable(from.frame(), 1f - eased), - Pair.immutable(to.frame(), eased) - ))); - } - - return new AnimationTimeline(baked); - } - - } - - -} From b305e6e26057706f4012aeb18ceac5ef3f8f9c29 Mon Sep 17 00:00:00 2001 From: ImBit <151650938+ImBit@users.noreply.github.com> Date: Wed, 29 Jul 2026 21:06:53 +0100 Subject: [PATCH 5/6] :art: Further animation refinements. --- .../bits/mc/animation/PaperDisplay.java | 17 ------ .../bits/mc/animation/Animatable.java | 10 ---- .../bits/mc/animation/Animation.java | 47 +++++++++++---- .../bits/mc/animation/AnimationKeyframe.java | 58 +++++++++++-------- .../bits/mc/animation/Animations.java | 37 ++++++++---- 5 files changed, 94 insertions(+), 75 deletions(-) diff --git a/minecraft/paper/src/main/java/xyz/bitsquidd/bits/mc/animation/PaperDisplay.java b/minecraft/paper/src/main/java/xyz/bitsquidd/bits/mc/animation/PaperDisplay.java index e29c7cdf..84cc2247 100644 --- a/minecraft/paper/src/main/java/xyz/bitsquidd/bits/mc/animation/PaperDisplay.java +++ b/minecraft/paper/src/main/java/xyz/bitsquidd/bits/mc/animation/PaperDisplay.java @@ -8,10 +8,8 @@ package xyz.bitsquidd.bits.mc.animation; import org.bukkit.entity.Display; - import org.bukkit.util.Transformation; import org.joml.Quaternionf; -import org.joml.Vector3f; public class PaperDisplay implements Animatable { @@ -26,21 +24,6 @@ public static PaperDisplay of(Display display) { } - @Override - public Vector3f getTranslation() { - return display.getTransformation().getTranslation(); - } - - @Override - public Quaternionf getRotation() { - return display.getTransformation().getLeftRotation(); - } - - @Override - public Vector3f getScale() { - return display.getTransformation().getScale(); - } - @Override public void applyPose(AnimationPoseNew pose) { display.setTransformation( diff --git a/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/Animatable.java b/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/Animatable.java index 9519c0d7..03691ff3 100644 --- a/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/Animatable.java +++ b/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/Animatable.java @@ -7,18 +7,8 @@ package xyz.bitsquidd.bits.mc.animation; -import org.joml.Quaternionf; -import org.joml.Vector3f; - public interface Animatable { - Vector3f getTranslation(); - - Quaternionf getRotation(); - - Vector3f getScale(); - - void applyPose(AnimationPoseNew pose); } diff --git a/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/Animation.java b/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/Animation.java index 4d2fb7c1..4486342d 100644 --- a/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/Animation.java +++ b/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/Animation.java @@ -5,13 +5,7 @@ * Copyright (c) 2023-2026 ImBit */ -package xyz.bitsquidd.bits.mc.animation;/* - * This file is part of a Bit libraries package. - * Licensed under the GNU Lesser General Public License v3.0. - * - * Copyright (c) 2023-2026 ImBit - */ - +package xyz.bitsquidd.bits.mc.animation; import xyz.bitsquidd.bits.lifecycle.builder.Buildable; import xyz.bitsquidd.bits.util.math.easing.Easing; @@ -29,7 +23,18 @@ public sealed interface Animation { boolean isFinished(AnimationData data); default Animation and(Animation other) { - return new Compound(List.of(this, other)); + List flattened = new ArrayList<>(); + if (this instanceof Compound c) { + flattened.addAll(c.animations); + } else { + flattened.add(this); + } + if (other instanceof Compound c) { + flattened.addAll(c.animations); + } else { + flattened.add(other); + } + return new Compound(flattened); } @@ -37,6 +42,10 @@ static Basic.Builder basic(long ticks) { return new Basic.Builder(ticks); } + static Constant constant(AnimationKeyframe frame) { + return new Constant(frame); + } + final class Basic implements Animation { private record KeyframeRecord( @@ -60,11 +69,27 @@ public void mutate(AnimationPoseNew pose, AnimationData data) { int size = baked.size(); long tick = data.currentTick(); - float progress = loopMode.transform((float)tick / size); - int effectiveIndex = Math.min(size - 1, Math.round(progress * size)); + int effectiveIndex = switch (loopMode) { + case STRAIGHT -> (int)(tick % size); + case PING_PONG -> { + long cycle = tick / size; + int cycleTick = (int)(tick % size); + yield (cycle % 2 == 1) ? size - 1 - cycleTick : cycleTick; + } + }; KeyframeRecord record = baked.get(effectiveIndex); - record.keyframes().forEach(pair -> pair.getFirst().applyTo(pose, data, pair.getSecond())); + List> keyframes = record.keyframes(); + + // Crossfade tick: blend the two active keyframes directly rather than composing two independently-faded values, which is not equivalent for rotation. + if (keyframes.size() == 2) { + AnimationKeyframe from = keyframes.get(0).getFirst(); + AnimationKeyframe to = keyframes.get(1).getFirst(); + float proportion = keyframes.get(1).getSecond(); + from.applyBlended(pose, data, to, proportion); + } else { + keyframes.forEach(pair -> pair.getFirst().applyTo(pose, data, pair.getSecond())); + } } @Override diff --git a/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/AnimationKeyframe.java b/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/AnimationKeyframe.java index 442973fb..1cc7807b 100644 --- a/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/AnimationKeyframe.java +++ b/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/AnimationKeyframe.java @@ -17,27 +17,11 @@ public sealed interface AnimationKeyframe { void applyTo(AnimationPoseNew pose, AnimationData data, float proportion); - default AnimationKeyframe and(AnimationKeyframe other) { - return new Combined(this, other); + default void applyBlended(AnimationPoseNew pose, AnimationData data, AnimationKeyframe other, float proportion) { + this.applyTo(pose, data, 1f - proportion); + other.applyTo(pose, data, proportion); } - final class Combined implements AnimationKeyframe { - private final AnimationKeyframe[] keyframes; - - public Combined(AnimationKeyframe... keyframes) { - this.keyframes = keyframes; - } - - @Override - public void applyTo(AnimationPoseNew pose, AnimationData data, float proportion) { - for (AnimationKeyframe keyframe : keyframes) { - keyframe.applyTo(pose, data, proportion); - } - } - - } - - record Translation( Function translation ) implements AnimationKeyframe { @@ -91,16 +75,16 @@ public static Rotation of(float x, float y, float z, float w) { return new Rotation(d -> new Quaternionf(x, y, z, w)); } - public static Rotation x(float x) { - return new Rotation(d -> new Quaternionf().rotateX(x)); + public static Rotation x(float degrees) { + return new Rotation(d -> new Quaternionf().rotateX((float)Math.toRadians(degrees))); } - public static Rotation y(float y) { - return new Rotation(d -> new Quaternionf().rotateY(y)); + public static Rotation y(float degrees) { + return new Rotation(d -> new Quaternionf().rotateY((float)Math.toRadians(degrees))); } - public static Rotation z(float z) { - return new Rotation(d -> new Quaternionf().rotateZ(z)); + public static Rotation z(float degrees) { + return new Rotation(d -> new Quaternionf().rotateZ((float)Math.toRadians(degrees))); } @@ -110,6 +94,18 @@ public void applyTo(AnimationPoseNew pose, AnimationData data, float proportion) pose.rotation().mul(effective); // Rotation combines multiplicatively } + @Override + public void applyBlended(AnimationPoseNew pose, AnimationData data, AnimationKeyframe other, float proportion) { + if (other instanceof Rotation(Function otherRotation)) { + Quaternionf from = this.rotation.apply(data); + Quaternionf to = otherRotation.apply(data); + Quaternionf blended = new Quaternionf(from).slerp(to, proportion); + pose.rotation().mul(blended); + } else { + AnimationKeyframe.super.applyBlended(pose, data, other, proportion); + } + } + } record Scale( @@ -151,6 +147,18 @@ public void applyTo(AnimationPoseNew pose, AnimationData data, float proportion) pose.scale().mul(effective); // Scale combines multiplicatively } + @Override + public void applyBlended(AnimationPoseNew pose, AnimationData data, AnimationKeyframe other, float proportion) { + if (other instanceof Scale(Function otherScale)) { + Vector3f from = this.scale.apply(data); + Vector3f to = otherScale.apply(data); + Vector3f blended = new Vector3f(from).lerp(to, proportion); + pose.scale().mul(blended); + } else { + AnimationKeyframe.super.applyBlended(pose, data, other, proportion); + } + } + } diff --git a/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/Animations.java b/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/Animations.java index 5fb647b2..86cda4b4 100644 --- a/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/Animations.java +++ b/minecraft/src/main/java/xyz/bitsquidd/bits/mc/animation/Animations.java @@ -7,8 +7,6 @@ package xyz.bitsquidd.bits.mc.animation; -import org.joml.Quaternionf; - import xyz.bitsquidd.bits.util.math.easing.Easings; @@ -34,7 +32,8 @@ public static Animation spin(int duration) { return Animation.basic(duration) .loop(AnimationProgressMode.STRAIGHT) .keyframe(0.00f, AnimationKeyframe.Rotation.empty(), Easings.IN_OUT_SIN.blend(Easings.LINEAR, 0.5f)) - .keyframe(1.00f, new AnimationKeyframe.Rotation(d -> new Quaternionf().rotateY((float)(Math.PI * 2f))), Easings.IN_OUT_SIN.blend(Easings.LINEAR, 0.5f)) + .keyframe(0.50f, AnimationKeyframe.Rotation.y(180f), Easings.IN_OUT_SIN.blend(Easings.LINEAR, 0.5f)) + .keyframe(1.00f, AnimationKeyframe.Rotation.y(360f), Easings.IN_OUT_SIN.blend(Easings.LINEAR, 0.5f)) .build(); } @@ -79,30 +78,43 @@ public static Animation wiggleZ(int duration, float amplitude) { .build(); } + public static Animation zoomIn(int duration, float startScale, float endScale) { + return Animation.basic(duration) + .loops(1) + .keyframe(0.00f, AnimationKeyframe.Scale.of(startScale), Easings.OUT_BACK) + .keyframe(1.00f, AnimationKeyframe.Scale.of(endScale), Easings.OUT_BACK) + .build(); + } + + public static Animation zoomIn(int duration, float startScale) { + return zoomIn(duration, startScale, 1f); + } + public static Animation zoomIn(int duration) { return zoomIn(duration, 0f); } - public static Animation zoomIn(int duration, float startScale) { + + public static Animation zoomOut(int duration, float startScale, float endScale) { return Animation.basic(duration) - .keyframe(0.00f, AnimationKeyframe.Scale.of(startScale), Easings.OUT_BACK) - .keyframe(1.00f, AnimationKeyframe.Scale.empty(), Easings.OUT_BACK) + .loops(1) + .keyframe(0.00f, AnimationKeyframe.Scale.of(startScale), Easings.IN_BACK) + .keyframe(1.00f, AnimationKeyframe.Scale.of(endScale), Easings.IN_BACK) .build(); } + public static Animation zoomOut(int duration, float startScale) { + return zoomOut(duration, startScale, 0f); + } + public static Animation zoomOut(int duration) { return zoomOut(duration, 0f); } - public static Animation zoomOut(int duration, float endScale) { - return Animation.basic(duration) - .keyframe(0.00f, AnimationKeyframe.Scale.empty(), Easings.IN_BACK) - .keyframe(1.00f, AnimationKeyframe.Scale.of(endScale), Easings.IN_BACK) - .build(); - } public static Animation moveUp(int duration, float distance) { return Animation.basic(duration) + .loops(1) .keyframe(0.00f, AnimationKeyframe.Translation.y(-distance), Easings.OUT_BACK) .keyframe(1.00f, AnimationKeyframe.Translation.empty(), Easings.OUT_BACK) .build(); @@ -114,6 +126,7 @@ public static Animation moveUp(int duration) { public static Animation moveDown(int duration, float distance) { return Animation.basic(duration) + .loops(1) .keyframe(0.00f, AnimationKeyframe.Translation.empty(), Easings.IN_BACK) .keyframe(1.00f, AnimationKeyframe.Translation.y(-distance), Easings.IN_BACK) .build(); From 5abfcbfe025657a579926f45ba36c80f5f0223d0 Mon Sep 17 00:00:00 2001 From: ImBit <151650938+ImBit@users.noreply.github.com> Date: Thu, 30 Jul 2026 12:42:12 +0100 Subject: [PATCH 6/6] :art: Enrich Locations API --- .../bits/paper/location/Locations.java | 58 +++++++++++++++++-- 1 file changed, 53 insertions(+), 5 deletions(-) diff --git a/minecraft/paper/src/main/java/xyz/bitsquidd/bits/paper/location/Locations.java b/minecraft/paper/src/main/java/xyz/bitsquidd/bits/paper/location/Locations.java index 8b582755..86d41663 100644 --- a/minecraft/paper/src/main/java/xyz/bitsquidd/bits/paper/location/Locations.java +++ b/minecraft/paper/src/main/java/xyz/bitsquidd/bits/paper/location/Locations.java @@ -9,12 +9,14 @@ import org.bukkit.Location; import org.bukkit.World; +import org.jetbrains.annotations.Nullable; import xyz.bitsquidd.bits.paper.location.wrapper.BlockPos; import xyz.bitsquidd.bits.paper.location.wrapper.Locatable; import java.util.Collection; + /** * Suite of location-related utility methods. * @@ -23,23 +25,68 @@ public final class Locations { private Locations() {} + + //region Utils + public static boolean isSameWorld(Location... locations) { + if (locations.length == 0) return false; + World world = locations[0].getWorld(); + + for (Location location : locations) { + if (!location.getWorld().equals(world)) return false; + } + return true; + } + + //endregion + + + //region Distance + /** * Safe distance check between two locations. * Returns false if either location is null or they are in different worlds. * * @since 0.0.11 */ - public static boolean isWithinDistance(Location loc1, Location loc2, double distance) { + public static boolean isWithinDistance(@Nullable Location loc1, @Nullable Location loc2, double distance) { + return isWithinDistanceSq(loc1, loc2, distance * distance); + } + + public static boolean isWithinDistanceSq(@Nullable Location loc1, @Nullable Location loc2, double distanceSq) { if (loc1 == null || loc2 == null) return false; + return getDistanceSq(loc1, loc2) <= distanceSq; + } - World world1 = loc1.getWorld(); - World world2 = loc2.getWorld(); + public static double getDistance(Location location1, Location location2) { + if (!isSameWorld(location1, location2)) return Double.MAX_VALUE; + return Math.sqrt(getDistanceSq(location1, location2)); + } - if (world1 == null || !world1.equals(world2)) return false; + public static double getDistanceSq(Location location1, Location location2) { + if (!isSameWorld(location1, location2)) return Double.MAX_VALUE; - return loc1.distance(loc2) <= distance; + double dx = location1.getX() - location2.getX(); + double dy = location1.getY() - location2.getY(); + double dz = location1.getZ() - location2.getZ(); + return dx * dx + dy * dy + dz * dz; } + public static double getHorizontalDistance(Location location1, Location location2) { + return Math.sqrt(getHorizontalDistanceSq(location1, location2)); + } + + public static double getHorizontalDistanceSq(Location location1, Location location2) { + if (!isSameWorld(location1, location2)) return Double.MAX_VALUE; + + double dx = location1.getX() - location2.getX(); + double dz = location1.getZ() - location2.getZ(); + return dx * dx + dz * dz; + } + //endregion + + + //region Collection Operations + /** * Calculates the midpoint BlockPos coordinates from a collection of {@link Locatable}s. * @@ -85,5 +132,6 @@ public static BlockPos getMaxLocation(Collection locatables locatables.stream().mapToDouble(l -> l.asVector().getZ()).max().orElseThrow(() -> new IllegalArgumentException("Error computing max z")) ); } + //endregion } \ No newline at end of file