feat(editor): 添加 ECS UI 系统和编辑器更新优化 (#238)
This commit is contained in:
282
packages/ui/src/systems/UIAnimationSystem.ts
Normal file
282
packages/ui/src/systems/UIAnimationSystem.ts
Normal file
@@ -0,0 +1,282 @@
|
||||
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());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓动函数
|
||||
* 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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user