Files
esengine/packages/rendering/fairygui/src/display/MovieClip.ts

421 lines
12 KiB
TypeScript
Raw Normal View History

feat(fairygui): FairyGUI 完整集成 (#314) * feat(fairygui): FairyGUI ECS 集成核心架构 实现 FairyGUI 的 ECS 原生集成,完全替代旧 UI 系统: 核心类: - GObject: UI 对象基类,支持变换、可见性、关联、齿轮 - GComponent: 容器组件,管理子对象和控制器 - GRoot: 根容器,管理焦点、弹窗、输入分发 - GGroup: 组容器,支持水平/垂直布局 抽象层: - DisplayObject: 显示对象基类 - EventDispatcher: 事件分发 - Timer: 计时器 - Stage: 舞台,管理输入和缩放 布局系统: - Relations: 约束关联管理 - RelationItem: 24 种关联类型 基础设施: - Controller: 状态控制器 - Transition: 过渡动画 - ScrollPane: 滚动面板 - UIPackage: 包管理 - ByteBuffer: 二进制解析 * refactor(ui): 删除旧 UI 系统,使用 FairyGUI 替代 * feat(fairygui): 实现 UI 控件 - 添加显示类:Image、TextField、Graph - 添加基础控件:GImage、GTextField、GGraph - 添加交互控件:GButton、GProgressBar、GSlider - 更新 IRenderCollector 支持 Graph 渲染 - 扩展 Controller 添加 selectedPageId - 添加 STATE_CHANGED 事件类型 * feat(fairygui): 现代化架构重构 - 增强 EventDispatcher 支持类型安全、优先级和传播控制 - 添加 PropertyBinding 响应式属性绑定系统 - 添加 ServiceContainer 依赖注入容器 - 添加 UIConfig 全局配置系统 - 添加 UIObjectFactory 对象工厂 - 实现 RenderBridge 渲染桥接层 - 实现 Canvas2DBackend 作为默认渲染后端 - 扩展 IRenderCollector 支持更多图元类型 * feat(fairygui): 九宫格渲染和资源加载修复 - 修复 FGUIUpdateSystem 支持路径和 GUID 两种加载方式 - 修复 GTextInput 同时设置 _displayObject 和 _textField - 实现九宫格渲染展开为 9 个子图元 - 添加 sourceWidth/sourceHeight 用于九宫格计算 - 添加 DOMTextRenderer 文本渲染层(临时方案) * fix(fairygui): 修复 GGraph 颜色读取 * feat(fairygui): 虚拟节点 Inspector 和文本渲染支持 * fix(fairygui): 编辑器状态刷新和遗留引用修复 - 修复切换 FGUI 包后组件列表未刷新问题 - 修复切换组件后 viewport 未清理旧内容问题 - 修复虚拟节点在包加载后未刷新问题 - 重构为事件驱动架构,移除轮询机制 - 修复 @esengine/ui 遗留引用,统一使用 @esengine/fairygui * fix: 移除 tsconfig 中的 @esengine/ui 引用
2025-12-22 10:52:54 +08:00
import { Image } from './Image';
import { Timer } from '../core/Timer';
import { FGUIEvents } from '../events/Events';
import type { IRenderCollector } from '../render/IRenderCollector';
/**
* Frame data for movie clip animation
*
*/
export interface IFrame {
/** Additional delay for this frame | 该帧额外延迟 */
addDelay: number;
/** Texture ID for this frame | 该帧的纹理 ID */
texture?: string | number | null;
}
/**
* Simple callback handler
*
*/
export type SimpleHandler = (() => void) | { run: () => void };
/**
* MovieClip
*
* Animated sprite display object with frame-based animation.
*
*
*
* Features:
* - Frame-by-frame animation
* - Swing (ping-pong) mode
* - Time scale control
* - Play range and loop control
*/
export class MovieClip extends Image {
/** Frame interval in milliseconds | 帧间隔(毫秒) */
public interval: number = 0;
/** Swing mode (ping-pong) | 摆动模式 */
public swing: boolean = false;
/** Delay between loops | 循环间延迟 */
public repeatDelay: number = 0;
/** Time scale multiplier | 时间缩放 */
public timeScale: number = 1;
private _playing: boolean = true;
private _frameCount: number = 0;
private _frames: IFrame[] = [];
private _frame: number = 0;
private _start: number = 0;
private _end: number = 0;
private _times: number = 0;
private _endAt: number = 0;
private _status: number = 0; // 0-none, 1-next loop, 2-ending, 3-ended
private _frameElapsed: number = 0;
private _reversed: boolean = false;
private _repeatedCount: number = 0;
private _endHandler: SimpleHandler | null = null;
private _isOnStage: boolean = false;
private _lastTime: number = 0;
constructor() {
super();
this.touchable = false;
this.setPlaySettings();
// Subscribe to stage lifecycle events
// 订阅舞台生命周期事件
this.on(FGUIEvents.ADDED_TO_STAGE, this.onAddToStage, this);
this.on(FGUIEvents.REMOVED_FROM_STAGE, this.onRemoveFromStage, this);
}
/**
* Get animation frames
*
*/
public get frames(): IFrame[] {
return this._frames;
}
/**
* Set animation frames
*
*/
public set frames(value: IFrame[]) {
this._frames = value;
this.scaleByTile = false;
this.scale9Grid = null;
if (this._frames && this._frames.length > 0) {
this._frameCount = this._frames.length;
if (this._end === -1 || this._end > this._frameCount - 1) {
this._end = this._frameCount - 1;
}
if (this._endAt === -1 || this._endAt > this._frameCount - 1) {
this._endAt = this._frameCount - 1;
}
if (this._frame < 0 || this._frame > this._frameCount - 1) {
this._frame = this._frameCount - 1;
}
this._frameElapsed = 0;
this._repeatedCount = 0;
this._reversed = false;
} else {
this._frameCount = 0;
}
this.drawFrame();
this.checkTimer();
}
/**
* Get frame count
*
*/
public get frameCount(): number {
return this._frameCount;
}
/**
* Get current frame index
*
*/
public get frame(): number {
return this._frame;
}
/**
* Set current frame index
*
*/
public set frame(value: number) {
if (this._frame !== value) {
if (this._frames && value >= this._frameCount) {
value = this._frameCount - 1;
}
this._frame = value;
this._frameElapsed = 0;
this.drawFrame();
}
}
/**
* Get playing state
*
*/
public get playing(): boolean {
return this._playing;
}
/**
* Set playing state
*
*/
public set playing(value: boolean) {
if (this._playing !== value) {
this._playing = value;
this.checkTimer();
}
}
/**
* Rewind to first frame
*
*/
public rewind(): void {
this._frame = 0;
this._frameElapsed = 0;
this._reversed = false;
this._repeatedCount = 0;
this.drawFrame();
}
/**
* Sync status from another MovieClip
* MovieClip
*/
public syncStatus(anotherMc: MovieClip): void {
this._frame = anotherMc._frame;
this._frameElapsed = anotherMc._frameElapsed;
this._reversed = anotherMc._reversed;
this._repeatedCount = anotherMc._repeatedCount;
this.drawFrame();
}
/**
* Advance animation by time
*
*
* @param timeInMilliseconds Time to advance |
*/
public advance(timeInMilliseconds: number): void {
const beginFrame = this._frame;
const beginReversed = this._reversed;
const backupTime = timeInMilliseconds;
while (true) {
let tt = this.interval + this._frames[this._frame].addDelay;
if (this._frame === 0 && this._repeatedCount > 0) {
tt += this.repeatDelay;
}
if (timeInMilliseconds < tt) {
this._frameElapsed = 0;
break;
}
timeInMilliseconds -= tt;
if (this.swing) {
if (this._reversed) {
this._frame--;
if (this._frame <= 0) {
this._frame = 0;
this._repeatedCount++;
this._reversed = !this._reversed;
}
} else {
this._frame++;
if (this._frame > this._frameCount - 1) {
this._frame = Math.max(0, this._frameCount - 2);
this._repeatedCount++;
this._reversed = !this._reversed;
}
}
} else {
this._frame++;
if (this._frame > this._frameCount - 1) {
this._frame = 0;
this._repeatedCount++;
}
}
// Completed one round
if (this._frame === beginFrame && this._reversed === beginReversed) {
const roundTime = backupTime - timeInMilliseconds;
timeInMilliseconds -= Math.floor(timeInMilliseconds / roundTime) * roundTime;
}
}
this.drawFrame();
}
/**
* Set play settings
*
*
* @param start Start frame |
* @param end End frame (-1 for last) | -1
* @param times Loop times (0 for infinite) | 0
* @param endAt Stop at frame (-1 for end) | -1
* @param endHandler Callback on end |
*/
public setPlaySettings(
start: number = 0,
end: number = -1,
times: number = 0,
endAt: number = -1,
endHandler: SimpleHandler | null = null
): void {
this._start = start;
this._end = end;
if (this._end === -1 || this._end > this._frameCount - 1) {
this._end = this._frameCount - 1;
}
this._times = times;
this._endAt = endAt;
if (this._endAt === -1) {
this._endAt = this._end;
}
this._status = 0;
this._endHandler = endHandler;
this.frame = start;
}
/**
* Called when added to stage
*
*/
public onAddToStage(): void {
this._isOnStage = true;
this._lastTime = Timer.time;
this.checkTimer();
}
/**
* Called when removed from stage
*
*/
public onRemoveFromStage(): void {
this._isOnStage = false;
this.checkTimer();
}
/**
* Update animation (called each frame)
*
*/
public update(): void {
if (!this._playing || this._frameCount === 0 || this._status === 3) {
return;
}
const currentTime = Timer.time;
let dt = currentTime - this._lastTime;
this._lastTime = currentTime;
if (dt > 100) {
dt = 100;
}
if (this.timeScale !== 1) {
dt *= this.timeScale;
}
this._frameElapsed += dt;
let tt = this.interval + this._frames[this._frame].addDelay;
if (this._frame === 0 && this._repeatedCount > 0) {
tt += this.repeatDelay;
}
if (this._frameElapsed < tt) {
return;
}
this._frameElapsed -= tt;
if (this._frameElapsed > this.interval) {
this._frameElapsed = this.interval;
}
if (this.swing) {
if (this._reversed) {
this._frame--;
if (this._frame <= 0) {
this._frame = 0;
this._repeatedCount++;
this._reversed = !this._reversed;
}
} else {
this._frame++;
if (this._frame > this._frameCount - 1) {
this._frame = Math.max(0, this._frameCount - 2);
this._repeatedCount++;
this._reversed = !this._reversed;
}
}
} else {
this._frame++;
if (this._frame > this._frameCount - 1) {
this._frame = 0;
this._repeatedCount++;
}
}
if (this._status === 1) {
// New loop
this._frame = this._start;
this._frameElapsed = 0;
this._status = 0;
} else if (this._status === 2) {
// Ending
this._frame = this._endAt;
this._frameElapsed = 0;
this._status = 3; // Ended
// Play end callback
if (this._endHandler) {
const handler = this._endHandler;
this._endHandler = null;
if (typeof handler === 'function') {
handler();
} else {
handler.run();
}
}
} else {
if (this._frame === this._end) {
if (this._times > 0) {
this._times--;
if (this._times === 0) {
this._status = 2; // Ending
} else {
this._status = 1; // New loop
}
} else {
this._status = 1; // New loop
}
}
}
this.drawFrame();
}
private drawFrame(): void {
if (this._frameCount > 0 && this._frame < this._frames.length) {
const frame = this._frames[this._frame];
this.texture = frame.texture ?? null;
} else {
this.texture = null;
}
}
private checkTimer(): void {
if (this._playing && this._frameCount > 0 && this._isOnStage) {
Timer.add(this.update, this);
} else {
Timer.remove(this.update, this);
}
}
public collectRenderData(collector: IRenderCollector): void {
super.collectRenderData(collector);
}
}