Files
timi-fx/src/main/java/com/imyeyu/fx/utils/AnimationRenderer.java
2025-07-14 14:12:45 +08:00

356 lines
7.5 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package com.imyeyu.fx.utils;
import javafx.animation.AnimationTimer;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.beans.property.ReadOnlyIntegerProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.util.Duration;
import com.imyeyu.fx.task.RunAsyncScheduled;
import com.imyeyu.java.bean.Callback;
import com.imyeyu.java.bean.CallbackArg;
import java.util.Iterator;
import java.util.LinkedList;
/**
* 动画帧渲染器,可控帧率,在 JVM 启动参数含<b>-Djavafx.animation.fullspeed=true</b>时,预设 FPS
* 才可以突破屏幕刷新率。
*
* <pre>
* Box box = new Box(128, 128, 128);
* box.setDrawMode(DrawMode.LINE);
* box.setCullFace(CullFace.BACK);
* box.setMaterial(new PhongMaterial(RED));
* box.setRotationAxis(new Point3D(0, 64, 0));
*
* // 以 240 FPS 每秒 90 度旋转一个 3D 立方体
* AnimationRenderer renderer = new AnimationRenderer(240);
* renderer.addRenderCallback(deltaSecond -> {
* box.setRotate(box.getRotate() + 90 * deltaSecond);
* });
* </pre>
*
* @author 夜雨
* @since 2023-03-08 09:48
*/
public class AnimationRenderer {
/** 渲染回调 */
protected final LinkedList<CallbackArg<Double>> renderCallbacks;
/** 动画渲染队列 */
protected final LinkedList<Animation> animations;
/** 渲染器 */
protected final AnimationTimer timer;
/** 平均帧生成时间 */
protected final DoubleProperty mpf;
/** 平均帧率 */
protected final IntegerProperty fps;
/** 状态计时器 */
protected final RunAsyncScheduled<?> statusTimer;
/** 当前帧 */
private double nowNanos;
/** 上一帧 */
private double lastNanos;
/** 累计帧差(纳秒) */
private double deltaNanos;
/** 累计帧差(秒) */
private double deltaSecond;
/** 当前帧差 */
private double betweenNanos;
/** 标准帧生成时间(纳秒) */
private double NPF;
/** 预设渲染帧率 */
private int prefFPS;
/** 默认构造60 FPS */
public AnimationRenderer() {
this(60);
}
/**
* 标准构造
*
* @param prefFPS 预设帧率
*/
public AnimationRenderer(int prefFPS) {
setPrefFPS(prefFPS);
fps = new SimpleIntegerProperty(0);
mpf = new SimpleDoubleProperty(0);
renderCallbacks = new LinkedList<>();
animations = new LinkedList<>();
timer = new AnimationTimer() {
@Override
public void handle(long now) {
nowNanos = now;
if (0 < lastNanos) {
// 计算帧差
deltaNanos += betweenNanos = nowNanos - lastNanos;
// 累计帧差大于最小帧生成时间(足够渲染下一帧)
if (NPF <= deltaNanos) {
deltaSecond = deltaNanos * 1E-9;
synchronized (renderCallbacks) {
for (CallbackArg<Double> renderCallback : renderCallbacks) {
renderCallback.handler(deltaSecond);
}
}
synchronized (animations) {
// 动画
Iterator<Animation> iterator = animations.iterator();
while (iterator.hasNext()) {
Animation next = iterator.next();
if (next.diedAt < millis()) {
next.callback.handler(deltaSecond, 1);
if (next.onFinishedEvent != null) {
next.onFinishedEvent.handler();
}
iterator.remove();
} else {
next.callback.handler(deltaSecond, (millis() - next.startAt) / next.ttl);
}
}
}
// 消耗剩余帧差
deltaNanos = deltaNanos % NPF;
}
}
lastNanos = nowNanos;
}
};
// 状态计算
statusTimer = RunAsyncScheduled.finish(Duration.seconds(1), new Callback() {
long total = 0, old;
{
renderCallbacks.add(nowNanos -> total++);
}
@Override
public void handler() {
// 帧生成时间
mpf.set(betweenNanos * 1E-6);
// 帧率
fps.set((int) (total - old));
old = total;
}
});
}
/** 启动 */
public void start() {
nowNanos = lastNanos = 0;
deltaNanos = NPF;
timer.start();
}
/** 停止 */
public void stop() {
timer.stop();
}
/**
* 添加渲染动画
*
* @param duration 持续时间
* @param callback 动画回调
*/
public void render(Duration duration, AnimationCallback callback) {
render(duration, callback, null);
}
/**
* 添加渲染动画
*
* @param duration 持续时间
* @param callback 动画回调
* @param onFinishedEvent 动画完成回调
*/
public void render(Duration duration, AnimationCallback callback, Callback onFinishedEvent) {
synchronized (animations) {
Animation animation = new Animation();
animation.startAt = nowNanos * 1E-6;
animation.diedAt = animation.startAt + duration.toMillis();
animation.ttl = duration.toMillis();
animation.callback = callback;
animation.onFinishedEvent = onFinishedEvent;
animations.add(animation);
}
}
/**
* 添加渲染回调
*
* @param callback 回调
*/
public void addRenderCallback(CallbackArg<Double> callback) {
synchronized (renderCallbacks) {
renderCallbacks.add(callback);
}
}
/**
* 移除渲染回调
*
* @param callback 回调
*/
public void removeRenderCallback(CallbackArg<Double> callback) {
synchronized (renderCallbacks) {
renderCallbacks.remove(callback);
}
}
/**
* 获取预设 FPS
*
* @return 预设 FPS
*/
public int getPrefFPS() {
return prefFPS;
}
/**
* 预设 FPS渲染器会尽量匹配此帧率渲染可能会突破少许系统资源紧张时实际渲染帧率会低于预设
*
* @param prefFPS FPS 取值范围 [1, 1000]
*/
public void setPrefFPS(int prefFPS) {
if (prefFPS < 1) {
throw new IllegalArgumentException("pref fps can not less then 1");
}
this.prefFPS = prefFPS;
this.NPF = 1E9 / prefFPS;
}
/**
* 获取帧生成时间(毫秒)
*
* @return 帧生成时间
*/
public double getMPF() {
return mpf.get();
}
/**
* 获取帧生成时间属性(毫秒)
*
* @return 帧生成时间属性(毫秒)
*/
public ReadOnlyDoubleProperty mpfProperty() {
return mpf;
}
/**
* 获取当前渲染 FPS
*
* @return FPS
*/
public int getFPS() {
return fps.get();
}
/**
* 获取当前渲染 FPS 属性
*
* @return FPS 属性
*/
public ReadOnlyIntegerProperty fpsProperty() {
return fps;
}
/**
* 获取累计帧差(纳秒)
*
* @return 累计帧差(纳秒)
*/
public double deltaNanos() {
return deltaNanos;
}
/**
* 获取累计帧差(毫秒)
*
* @return 累计帧差(毫秒)
*/
public double deltaMillis() {
return deltaNanos * 1E-6;
}
/**
* 获取累计帧差(秒)
*
* @return 累计帧差(秒)
*/
public double deltaSecond() {
return deltaNanos * 1E-9;
}
/** @return 当前帧(纳秒) */
public double nanos() {
return nowNanos;
}
/** @return 当前帧(毫秒) */
public double millis() {
return nowNanos * 1E-6;
}
/** @return 当前帧(秒) */
public double second() {
return nowNanos * 1E-9;
}
/**
* 一次性动画
*
* @author 夜雨
* @since 2023-05-16 16:02
*/
public static class Animation {
double diedAt;
double startAt;
double ttl;
AnimationCallback callback;
Callback onFinishedEvent;
}
/**
* 一次性动画回调
*
* @author 夜雨
* @since 2023-05-14 10:10
*/
public interface AnimationCallback {
/**
* 处理器
*
* @param deltaSecond 帧差
* @param percent 动画进度百分比,取值范围 [0, 1]
*/
void handler(double deltaSecond, double percent);
}
}