Files
esengine/packages/ui/src/systems/UIAnimationSystem.ts
YHH cabb625a17 Feature/UI input system fix (#243)
* feat(ui): 实现编辑器预览模式下的 UI 输入系统

* feat(platform-web): 为浏览器运行时添加 UI 输入系统绑定
2025-11-27 22:31:05 +08:00

283 lines
9.1 KiB
TypeScript

import { EntitySystem, Matcher, Entity, Time, ECSSystem } from '@esengine/ecs-framework';
import { UIProgressBarComponent } from '../components/widgets/UIProgressBarComponent';
import { UISliderComponent } from '../components/widgets/UISliderComponent';
import { UIButtonComponent } from '../components/widgets/UIButtonComponent';
/**
* 缓动函数类型
* Easing function type
*/
export type EasingFunction = (t: number) => number;
/**
* 预定义缓动函数
* Predefined easing functions
*/
export const Easing = {
linear: (t: number) => t,
// Quad
easeInQuad: (t: number) => t * t,
easeOutQuad: (t: number) => t * (2 - t),
easeInOutQuad: (t: number) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t,
// Cubic
easeInCubic: (t: number) => t * t * t,
easeOutCubic: (t: number) => (--t) * t * t + 1,
easeInOutCubic: (t: number) => t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1,
// Quart
easeInQuart: (t: number) => t * t * t * t,
easeOutQuart: (t: number) => 1 - (--t) * t * t * t,
easeInOutQuart: (t: number) => t < 0.5 ? 8 * t * t * t * t : 1 - 8 * (--t) * t * t * t,
// Quint
easeInQuint: (t: number) => t * t * t * t * t,
easeOutQuint: (t: number) => 1 + (--t) * t * t * t * t,
easeInOutQuint: (t: number) => t < 0.5 ? 16 * t * t * t * t * t : 1 + 16 * (--t) * t * t * t * t,
// Sine
easeInSine: (t: number) => 1 - Math.cos(t * Math.PI / 2),
easeOutSine: (t: number) => Math.sin(t * Math.PI / 2),
easeInOutSine: (t: number) => -(Math.cos(Math.PI * t) - 1) / 2,
// Expo
easeInExpo: (t: number) => t === 0 ? 0 : Math.pow(2, 10 * (t - 1)),
easeOutExpo: (t: number) => t === 1 ? 1 : 1 - Math.pow(2, -10 * t),
easeInOutExpo: (t: number) => {
if (t === 0) return 0;
if (t === 1) return 1;
if (t < 0.5) return Math.pow(2, 20 * t - 10) / 2;
return (2 - Math.pow(2, -20 * t + 10)) / 2;
},
// Circ
easeInCirc: (t: number) => 1 - Math.sqrt(1 - t * t),
easeOutCirc: (t: number) => Math.sqrt(1 - (--t) * t),
easeInOutCirc: (t: number) => t < 0.5
? (1 - Math.sqrt(1 - 4 * t * t)) / 2
: (Math.sqrt(1 - Math.pow(-2 * t + 2, 2)) + 1) / 2,
// Back
easeInBack: (t: number) => {
const c1 = 1.70158;
const c3 = c1 + 1;
return c3 * t * t * t - c1 * t * t;
},
easeOutBack: (t: number) => {
const c1 = 1.70158;
const c3 = c1 + 1;
return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);
},
easeInOutBack: (t: number) => {
const c1 = 1.70158;
const c2 = c1 * 1.525;
return t < 0.5
? (Math.pow(2 * t, 2) * ((c2 + 1) * 2 * t - c2)) / 2
: (Math.pow(2 * t - 2, 2) * ((c2 + 1) * (t * 2 - 2) + c2) + 2) / 2;
},
// Elastic
easeInElastic: (t: number) => {
if (t === 0) return 0;
if (t === 1) return 1;
return -Math.pow(2, 10 * t - 10) * Math.sin((t * 10 - 10.75) * ((2 * Math.PI) / 3));
},
easeOutElastic: (t: number) => {
if (t === 0) return 0;
if (t === 1) return 1;
return Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * ((2 * Math.PI) / 3)) + 1;
},
easeInOutElastic: (t: number) => {
if (t === 0) return 0;
if (t === 1) return 1;
const c5 = (2 * Math.PI) / 4.5;
return t < 0.5
? -(Math.pow(2, 20 * t - 10) * Math.sin((20 * t - 11.125) * c5)) / 2
: (Math.pow(2, -20 * t + 10) * Math.sin((20 * t - 11.125) * c5)) / 2 + 1;
},
// Bounce
easeInBounce: (t: number) => 1 - Easing.easeOutBounce(1 - t),
easeOutBounce: (t: number) => {
const n1 = 7.5625;
const d1 = 2.75;
if (t < 1 / d1) {
return n1 * t * t;
} else if (t < 2 / d1) {
return n1 * (t -= 1.5 / d1) * t + 0.75;
} else if (t < 2.5 / d1) {
return n1 * (t -= 2.25 / d1) * t + 0.9375;
} else {
return n1 * (t -= 2.625 / d1) * t + 0.984375;
}
},
easeInOutBounce: (t: number) => t < 0.5
? (1 - Easing.easeOutBounce(1 - 2 * t)) / 2
: (1 + Easing.easeOutBounce(2 * t - 1)) / 2,
// 简化别名
easeIn: (t: number) => t * t,
easeOut: (t: number) => t * (2 - t),
easeInOut: (t: number) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t
};
/**
* 缓动函数名称映射
* Easing function name mapping
*/
export type EasingName = keyof typeof Easing;
/**
* UI 动画系统
* UI Animation System - Handles value interpolation and animations
*/
@ECSSystem('UIAnimation')
export class UIAnimationSystem extends EntitySystem {
constructor() {
// 匹配有任何动画组件的实体
super(Matcher.empty().any(UIButtonComponent, UIProgressBarComponent, UISliderComponent));
}
/**
* 获取缓动函数
* Get easing function by name
*/
public getEasingFunction(name: string): EasingFunction {
return (Easing as Record<string, EasingFunction>)[name] ?? Easing.linear;
}
protected process(entities: readonly Entity[]): void {
const dt = Time.deltaTime;
for (const entity of entities) {
// 处理进度条动画
this.updateProgressBar(entity, dt);
// 处理滑块动画
this.updateSlider(entity, dt);
// 处理按钮颜色动画
this.updateButtonColor(entity, dt);
}
}
/**
* 更新进度条动画
* Update progress bar animation
*/
private updateProgressBar(entity: Entity, dt: number): void {
const progress = entity.getComponent(UIProgressBarComponent);
if (!progress) return;
// 如果目标值和显示值不同,进行插值
if (progress.displayValue !== progress.targetValue) {
const easingFn = this.getEasingFunction(progress.easing);
const range = progress.maxValue - progress.minValue;
const speed = range / progress.transitionDuration;
const diff = progress.targetValue - progress.displayValue;
const direction = Math.sign(diff);
const step = Math.min(Math.abs(diff), speed * dt);
progress.displayValue += direction * step;
// 接近目标时直接设置
if (Math.abs(progress.displayValue - progress.targetValue) < 0.01) {
progress.displayValue = progress.targetValue;
}
progress.value = progress.displayValue;
}
}
/**
* 更新滑块动画
* Update slider animation
*/
private updateSlider(entity: Entity, dt: number): void {
const slider = entity.getComponent(UISliderComponent);
if (!slider) return;
// 如果正在拖拽,直接设置(不做动画)
if (slider.dragging) {
slider.displayValue = slider.targetValue;
slider.value = slider.targetValue;
return;
}
// 平滑插值
if (slider.displayValue !== slider.targetValue) {
const range = slider.maxValue - slider.minValue;
const speed = range / slider.transitionDuration;
const diff = slider.targetValue - slider.displayValue;
const direction = Math.sign(diff);
const step = Math.min(Math.abs(diff), speed * dt);
slider.displayValue += direction * step;
if (Math.abs(slider.displayValue - slider.targetValue) < 0.01) {
slider.displayValue = slider.targetValue;
}
slider.value = slider.displayValue;
}
}
/**
* 更新按钮颜色动画
* Update button color animation
*/
private updateButtonColor(entity: Entity, dt: number): void {
const button = entity.getComponent(UIButtonComponent);
if (!button) return;
if (button.currentColor !== button.targetColor) {
// 颜色插值
button.currentColor = this.lerpColor(
button.currentColor,
button.targetColor,
Math.min(1, dt / button.transitionDuration)
);
}
}
/**
* 颜色线性插值
* Linear interpolate between two colors
*/
private lerpColor(from: number, to: number, t: number): number {
const fromR = (from >> 16) & 0xFF;
const fromG = (from >> 8) & 0xFF;
const fromB = from & 0xFF;
const toR = (to >> 16) & 0xFF;
const toG = (to >> 8) & 0xFF;
const toB = to & 0xFF;
const r = Math.round(fromR + (toR - fromR) * t);
const g = Math.round(fromG + (toG - fromG) * t);
const b = Math.round(fromB + (toB - fromB) * t);
return (r << 16) | (g << 8) | b;
}
/**
* 数值线性插值
* Linear interpolate between two values
*/
public lerp(from: number, to: number, t: number): number {
return from + (to - from) * t;
}
/**
* 应用缓动的插值
* Interpolate with easing
*/
public ease(from: number, to: number, t: number, easing: EasingName = 'linear'): number {
const easingFn = this.getEasingFunction(easing);
return this.lerp(from, to, easingFn(t));
}
}