feat: 动画系统激励源 & refactor: 部分接口变动

This commit is contained in:
AncTe committed 2026-03-09 13:52:47 +08:00
1 parent 5d6f40a469
commit feb6abb5bb
14 files changed
+447 -181

No files matched your search

+323
View File
@@ -0,0 +1,323 @@
import { logger } from '@motajs/common';
import {
IExcitation,
IExcitable,
IExcitableController,
IExcitationVariator,
ExcitationCurve,
VariatorCurveMode
} from './types';
import { excited } from './utils';
/**
* IExcitation 抽象基类,管理受激励对象的注册、激励与移除。
* 便于拓展不同类型的激励源。
*/
export abstract class ExcitationBase<T> implements IExcitation<T> {
protected readonly excitables: Set<IExcitable<T>> = new Set();
protected destroyed: boolean = false;
/**
* 当前激励负载,由子类实现
*/
abstract payload(): T;
/**
* 激励所有受激励对象
*/
excite(payload: T): void {
this.excitables.forEach(ex => ex.excited(payload));
}
/**
* 添加受激励对象
*/
add(object: IExcitable<T>): IExcitableController<T> | null {
if (this.destroyed) {
logger.error(48, 'add');
return null;
}
this.excitables.add(object);
const controller: IExcitableController<T> = {
excitable: object,
revoke: () => this.remove(object),
excite: (payload: T) => object.excited(payload)
};
return controller;
}
/**
* 移除受激励对象
*/
remove(object: IExcitable<T>): boolean {
if (this.destroyed) {
logger.error(48, 'remove');
return false;
}
return this.excitables.delete(object);
}
/**
* 摧毁激励源,清理所有引用
*/
destroy(): void {
this.destroyed = true;
this.excitables.clear();
}
}
/**
* 基于 requestAnimationFrame 的激励源
* 每帧激励所有对象,payload 为当前时间戳
*/
export class RafExcitation extends ExcitationBase<number> {
private rafId: number = -1;
private now: number = 0;
constructor() {
super();
this.tick = this.tick.bind(this);
this.rafId = requestAnimationFrame(this.tick);
}
payload(): number {
return this.now;
}
/**
* 每帧对所有激励源激励一次
* @param ts 当前时间戳
*/
private tick(ts: number) {
this.now = ts;
this.excite(ts);
if (!this.destroyed) {
this.rafId = requestAnimationFrame(this.tick);
} else {
this.rafId = -1;
}
}
override destroy(): void {
if (this.rafId !== -1) {
cancelAnimationFrame(this.rafId);
this.rafId = -1;
}
super.destroy();
}
}
interface CurveQueue {
/** 速率曲线 */
curve: ExcitationCurve;
/** 变速持续时间 */
time: number;
/** 变速参考模式 */
mode: VariatorCurveMode;
/** 兑现 Promise */
resolve: () => void;
}
interface CurrentCurve {
/** 当前速率曲线 */
curve: ExcitationCurve;
/** 变速时长 */
time: number;
/** 变速参考模式 */
mode: VariatorCurveMode;
/** 起始时间戳 */
startTs: number;
/** 兑现 Promise */
resolve: () => void;
}
/**
* 激励源变速器
* 可对 payload 为 number 的激励源进行变速处理
*/
export class ExcitationVariator
extends ExcitationBase<number>
implements IExcitationVariator
{
/** 当前绑定的激励源 */
source: IExcitation<number> | null = null;
/** 当前速度 */
speed: number = 1;
/** 在源中添加的被激励对象的控制器 */
private sourceController: IExcitableController<number> | null = null;
/** 上一次变速时源的参考时间戳 */
private sourceTs: number = 0;
/** 上一次变速时自身的参考时间戳 */
private selfTs: number = 0;
/** 当前自身时间戳 */
private now: number = 0;
/** 曲线队列 */
private curveQueue: CurveQueue[] = [];
/** 当前执行的曲线 */
private currentCurve: CurrentCurve | null = null;
payload(): number {
return this.now;
}
/**
* 处理源的激励,更新自身时间戳并处理曲线逻辑
*/
excite(payload: number): void {
if (!this.source) return;
// 计算新的自身时间戳
const newSelfTs = this.selfTs + (payload - this.sourceTs) * this.speed;
this.now = newSelfTs;
// 处理曲线执行
if (this.currentCurve) {
// 根据模式计算完成度所用的时间戳
const referenceTs =
this.currentCurve.mode === VariatorCurveMode.SourceRelated
? payload
: newSelfTs;
const elapsed = referenceTs - this.currentCurve.startTs;
if (elapsed >= this.currentCurve.time) {
// 曲线完成,使用 curve(1) 作为最终速率值
this.setSpeed(this.currentCurve.curve(1));
this.currentCurve.resolve();
this.currentCurve = null;
// 执行下一个曲线
if (this.curveQueue.length > 0) {
this.startNextCurve();
}
} else {
// 曲线进行中,更新速度
const progress = elapsed / this.currentCurve.time;
this.setSpeed(this.currentCurve.curve(progress));
}
}
// 激励所有受激励对象
this.excitables.forEach(ex => ex.excited(newSelfTs));
}
bindExcitation(excitation: IExcitation<number>): void {
// 如果已绑定不同的源,先解绑
if (this.source !== null && this.source !== excitation) {
this.unbindExcitation();
}
if (this.source === excitation) {
return;
}
this.source = excitation;
this.sourceTs = excitation.payload();
this.selfTs = this.sourceTs;
this.speed = 1;
// 创建内部激励对象,将源的激励转发给自身
const internalExcitable: IExcitable<number> = excited(payload => {
this.excite(payload);
});
this.sourceController = excitation.add(internalExcitable);
}
unbindExcitation(): void {
if (this.source === null) {
return;
}
// 取消在源上的绑定
if (this.sourceController !== null) {
this.sourceController.revoke();
this.sourceController = null;
}
// 取消曲线执行
this.endAllCurves();
// 重置状态
this.source = null;
this.speed = 1;
this.sourceTs = 0;
this.selfTs = 0;
this.now = 0;
}
setSpeed(speed: number): void {
if (this.source === null) {
logger.error(49, 'set speed');
return;
}
// 更新参考时间戳
this.sourceTs = this.source.payload();
this.selfTs = this.now;
this.speed = speed;
}
curveSpeed(
curve: ExcitationCurve,
time: number,
mode: VariatorCurveMode = VariatorCurveMode.SourceRelated
): Promise<void> {
if (this.source === null) {
logger.error(49, 'curve speed');
return Promise.resolve();
}
return new Promise<void>(resolve => {
this.curveQueue.push({ curve, time, mode, resolve });
// 如果没有正在执行的曲线,立即开始
if (this.currentCurve === null) {
this.startNextCurve();
}
});
}
private startNextCurve(): void {
if (this.curveQueue.length === 0) {
return;
}
const item = this.curveQueue.shift()!;
// 记录起始时间戳
const startTs =
item.mode === VariatorCurveMode.SourceRelated
? this.source!.payload()
: this.now;
this.currentCurve = {
...item,
startTs
};
}
endAllCurves(): void {
if (!this.currentCurve) return;
if (this.curveQueue.length > 0) {
const last = this.curveQueue.at(-1)!;
const speed = last.curve(1);
this.setSpeed(speed);
this.curveQueue = [];
this.currentCurve = null;
} else {
const speed = this.currentCurve.curve(1);
this.setSpeed(speed);
this.currentCurve = null;
}
}
override destroy(): void {
this.unbindExcitation();
super.destroy();
}
}
+2
View File
@@ -1 +1,3 @@
export * from './excitation';
export * from './types';
export * from './utils';
+75 -1
View File
@@ -1,3 +1,24 @@
/**
* 动画速率曲线,输入时间完成度,输出一个值,时间完成度范围在 `0-1` 之间,而输出值没有范围限制。
* 在不同场景下,速率曲线返回值的含义可能不同,有的可能表示动画完成度,有的可能表示绝对的坐标值。
*/
export type ExcitationCurve = (progress: number) => number;
/**
* 二维动画速率曲线,输入时间完成度,输出两个值。可由两个一维曲线组合而成(本质是参数方程)
*/
export type ExcitationCurve2D = (progress: number) => [number, number];
/**
* 三维动画速率曲线,输入时间完成度,输出三个值。可由三个一维曲线组合而成(本质是参数方程)
*/
export type ExcitationCurve3D = (progress: number) => [number, number, number];
/**
* `n` 维动画速率曲线,输入时间完成度,输出三个值。可由 `n` 个一维曲线组合而成(本质是参数方程)
*/
export type GeneralExcitationCurve = (progress: number) => number[];
export interface IExcitable<T> {
/**
* 受到激励源激励
@@ -39,7 +60,7 @@ export interface IExcitation<T> {
* @param object 受激励对象
* @returns 受激励对象控制器
*/
add(object: IExcitable<T>): IExcitableController<T>;
add(object: IExcitable<T>): IExcitableController<T> | null;
/**
* 移除一个受激励对象
@@ -53,3 +74,56 @@ export interface IExcitation<T> {
*/
destroy(): void;
}
export const enum VariatorCurveMode {
/**
* 时间相对于激励源,不受变速器当前速度影响
*/
SourceRelated,
/**
* 时间相对于变速器本身,当变速器本身的速度变慢时,速率曲线的运行速度也变慢
*/
SelfRelated
}
export interface IExcitationVariator extends IExcitation<number> {
/** 变速器当前的速度值 */
readonly speed: number;
/** 当前绑定的激励源 */
readonly source: IExcitation<number> | null;
/**
* 绑定激励源对象,变速器将以此激励源为基础实施变速
* @param excitation 绑定的激励源
*/
bindExcitation(excitation: IExcitation<number>): void;
/**
* 取消绑定当前的激励源
*/
unbindExcitation(): void;
/**
* 修改激励速度
* @param speed 激励速度,值表示当前速度与原速度的比值,1 为原速
*/
setSpeed(speed: number): void;
/**
* 使用速率曲线来修改激励速度,当有速率曲线在执行时,调用此函数将会加入队列,当上一个曲线结束后立刻执行
* @param curve 速率曲线
* @param time 速率曲线实施时间
* @param mode 速率曲线实施模式
*/
curveSpeed(
curve: ExcitationCurve,
time: number,
mode?: VariatorCurveMode
): Promise<void>;
/**
* 终止当前所有速率曲线的执行,最终速度为队列中最后一个曲线的终止速度
*/
endAllCurves(): void;
}
+2 -1
View File
@@ -1,5 +1,6 @@
export * from './utils';
export * from './dirtyTracker';
export * from './hook';
export * from './logger';
export * from './utils';
export * from './types';
+2
View File
@@ -47,6 +47,8 @@
"45": "Cannot add $1 map renderer extension, since $1 already exists for the given state.",
"46": "Cannot execute close door action on $1,$2, since the given position is not empty.",
"47": "Cannot require text area outside the target map.",
"48": "Cannot $1 excitables on destroyed excitation.",
"49": "Cannot $1 on variator without excitation binding.",
"1201": "Floor-damage extension needs 'floor-binder' extension as dependency."
},
"warn": {
File renamed without changes.
+2
View File
@@ -0,0 +1,2 @@
export * from './func';
export * from './types';
+29
View File
@@ -0,0 +1,29 @@
export interface ISearchable4Dir {
/** 获取上侧元素 */
up(): ISearchable4Dir | null;
/** 获取下侧元素 */
down(): ISearchable4Dir | null;
/** 获取左侧元素 */
left(): ISearchable4Dir | null;
/** 获取右侧元素 */
right(): ISearchable4Dir | null;
}
export interface ISearchable8Dir {
/** 获取上侧元素 */
up(): ISearchable8Dir | null;
/** 获取下侧元素 */
down(): ISearchable8Dir | null;
/** 获取左侧元素 */
left(): ISearchable8Dir | null;
/** 获取右侧元素 */
right(): ISearchable8Dir | null;
/** 获取左上元素 */
leftUp(): ISearchable8Dir | null;
/** 获取右上元素 */
rightUp(): ISearchable8Dir | null;
/** 获取左下元素 */
leftDown(): ISearchable8Dir | null;
/** 获取右下元素 */
rightDown(): ISearchable8Dir | null;
}
+1 -39
View File
@@ -1,41 +1,3 @@
{
"name": "@motajs/render",
"version": "0.1.0",
"main": "dist/render.cjs.js",
"module": "dist/render.es.js",
"types": "dist/render.es.d.ts",
"keywords": [
"render",
"game",
"vue",
"graphics"
],
"repository": {
"type": "git",
"url": "https://github.com/motajs/motajs-engine"
},
"author": {
"name": "AncTe",
"email": "1319491857@qq.com"
},
"exports": {
".": {
"import": "./dist/render.es.js",
"require": "./dist/render.cjs.js",
"types": "./dist/render.es.d.ts"
}
},
"files": [
"dist"
],
"dependencies": {
"eventemitter3": "^5.0.1",
"gl-matrix": "^3.4.3",
"lodash-es": "^4.17.21",
"mutate-animate": "^1.4.2"
},
"peerDependencies": {
"vue": "^3.5.13"
},
"license": "LGPL-3.0-only"
"name": "@motajs/render"
}