Feature/UI input system fix (#243)
* feat(ui): 实现编辑器预览模式下的 UI 输入系统 * feat(platform-web): 为浏览器运行时添加 UI 输入系统绑定
This commit is contained in:
@@ -9,7 +9,7 @@ import { Core, Scene, Entity, SceneSerializer } from '@esengine/ecs-framework';
|
||||
import { TransformComponent, SpriteComponent, SpriteAnimatorComponent, SpriteAnimatorSystem } from '@esengine/ecs-components';
|
||||
import { TilemapComponent, TilemapRenderingSystem } from '@esengine/tilemap';
|
||||
import { BehaviorTreeExecutionSystem } from '@esengine/behavior-tree';
|
||||
import { UIRenderDataProvider, invalidateUIRenderCaches } from '@esengine/ui';
|
||||
import { UIRenderDataProvider, invalidateUIRenderCaches, UIInputSystem } from '@esengine/ui';
|
||||
import * as esEngine from '@esengine/engine';
|
||||
import {
|
||||
AssetManager,
|
||||
@@ -37,6 +37,7 @@ export class EngineService {
|
||||
private tilemapSystem: TilemapRenderingSystem | null = null;
|
||||
private behaviorTreeSystem: BehaviorTreeExecutionSystem | null = null;
|
||||
private uiRenderProvider: UIRenderDataProvider | null = null;
|
||||
private uiInputSystem: UIInputSystem | null = null;
|
||||
private initialized = false;
|
||||
private modulesInitialized = false;
|
||||
private running = false;
|
||||
@@ -244,6 +245,7 @@ export class EngineService {
|
||||
this.tilemapSystem = context.tilemapSystem as TilemapRenderingSystem | undefined ?? null;
|
||||
this.behaviorTreeSystem = context.behaviorTreeSystem as BehaviorTreeExecutionSystem | undefined ?? null;
|
||||
this.uiRenderProvider = context.uiRenderProvider as UIRenderDataProvider | undefined ?? null;
|
||||
this.uiInputSystem = context.uiInputSystem as UIInputSystem | undefined ?? null;
|
||||
|
||||
// 设置 UI 渲染数据提供者到 EngineRenderSystem
|
||||
// Set UI render data provider to EngineRenderSystem
|
||||
@@ -276,12 +278,19 @@ export class EngineService {
|
||||
pluginManager.clearSceneSystems();
|
||||
}
|
||||
|
||||
// Unbind UI input system before clearing
|
||||
// 清理前解绑 UI 输入系统
|
||||
if (this.uiInputSystem) {
|
||||
this.uiInputSystem.unbind();
|
||||
}
|
||||
|
||||
// 清空本地引用(系统的实际清理由场景管理)
|
||||
// Clear local references (actual system cleanup is managed by scene)
|
||||
this.animatorSystem = null;
|
||||
this.tilemapSystem = null;
|
||||
this.behaviorTreeSystem = null;
|
||||
this.uiRenderProvider = null;
|
||||
this.uiInputSystem = null;
|
||||
this.modulesInitialized = false;
|
||||
}
|
||||
|
||||
@@ -362,6 +371,15 @@ export class EngineService {
|
||||
this.renderSystem.setPreviewMode(true);
|
||||
}
|
||||
|
||||
// Bind UI input system to canvas for event handling
|
||||
// 绑定 UI 输入系统到 canvas 以处理事件
|
||||
if (this.uiInputSystem && this.canvasId) {
|
||||
const canvas = document.getElementById(this.canvasId) as HTMLCanvasElement;
|
||||
if (canvas) {
|
||||
this.uiInputSystem.bindToCanvas(canvas);
|
||||
}
|
||||
}
|
||||
|
||||
// Enable animator system and start auto-play animations
|
||||
// 启用动画系统并启动自动播放的动画
|
||||
if (this.animatorSystem) {
|
||||
@@ -435,6 +453,12 @@ export class EngineService {
|
||||
this.renderSystem.setPreviewMode(false);
|
||||
}
|
||||
|
||||
// Unbind UI input system from canvas
|
||||
// 从 canvas 解绑 UI 输入系统
|
||||
if (this.uiInputSystem) {
|
||||
this.uiInputSystem.unbind();
|
||||
}
|
||||
|
||||
// Disable animator system and stop all animations
|
||||
// 禁用动画系统并停止所有动画
|
||||
if (this.animatorSystem) {
|
||||
|
||||
@@ -9,7 +9,7 @@ import { EngineBridge, EngineRenderSystem, CameraSystem } from '@esengine/ecs-en
|
||||
import { TransformComponent, SpriteAnimatorSystem, CoreRuntimeModule } from '@esengine/ecs-components';
|
||||
import type { SystemContext, IPluginLoader, IRuntimeModuleLoader, PluginDescriptor } from '@esengine/ecs-components';
|
||||
// Import from /runtime entry points to avoid editor dependencies (React, etc.)
|
||||
import { UIRuntimeModule, UIRenderDataProvider } from '@esengine/ui/runtime';
|
||||
import { UIRuntimeModule, UIRenderDataProvider, UIInputSystem } from '@esengine/ui/runtime';
|
||||
import { TilemapRuntimeModule, TilemapRenderingSystem } from '@esengine/tilemap/runtime';
|
||||
import { BehaviorTreeRuntimeModule, BehaviorTreeExecutionSystem } from '@esengine/behavior-tree/runtime';
|
||||
|
||||
@@ -33,6 +33,8 @@ export interface RuntimeModuleConfig {
|
||||
enabledPlugins?: string[];
|
||||
/** 是否为编辑器模式 */
|
||||
isEditor?: boolean;
|
||||
/** Canvas ID 用于 UI 输入绑定 */
|
||||
canvasId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -352,6 +354,15 @@ export function createRuntimeSystems(
|
||||
|
||||
scene.addSystem(renderSystem);
|
||||
|
||||
// 绑定 UIInputSystem 到 canvas(用于 UI 交互)
|
||||
// Bind UIInputSystem to canvas (for UI interaction)
|
||||
if (config?.canvasId && context.uiInputSystem) {
|
||||
const canvas = document.getElementById(config.canvasId) as HTMLCanvasElement;
|
||||
if (canvas) {
|
||||
(context.uiInputSystem as UIInputSystem).bindToCanvas(canvas);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
cameraSystem,
|
||||
animatorSystem: context.animatorSystem as SpriteAnimatorSystem | undefined,
|
||||
|
||||
@@ -21,8 +21,10 @@ class BrowserRuntime {
|
||||
private animationId: number | null = null;
|
||||
private assetManager: AssetManager;
|
||||
private engineIntegration: EngineIntegration;
|
||||
private canvasId: string;
|
||||
|
||||
constructor(config: RuntimeConfig) {
|
||||
this.canvasId = config.canvasId;
|
||||
if (!Core.Instance) {
|
||||
Core.create();
|
||||
}
|
||||
@@ -58,8 +60,10 @@ class BrowserRuntime {
|
||||
// 初始化模块系统
|
||||
await initializeRuntime(Core);
|
||||
|
||||
// 创建运行时系统
|
||||
this.systems = createRuntimeSystems(Core.scene!, this.bridge);
|
||||
// 创建运行时系统(传入 canvasId 用于 UI 输入绑定)
|
||||
this.systems = createRuntimeSystems(Core.scene!, this.bridge, {
|
||||
canvasId: this.canvasId
|
||||
});
|
||||
}
|
||||
|
||||
async loadScene(sceneUrl: string): Promise<void> {
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
} from './components';
|
||||
import { UILayoutSystem } from './systems/UILayoutSystem';
|
||||
import { UIInputSystem } from './systems/UIInputSystem';
|
||||
import { UIAnimationSystem } from './systems/UIAnimationSystem';
|
||||
import { UIRenderDataProvider } from './systems/UIRenderDataProvider';
|
||||
import {
|
||||
UIRenderBeginSystem,
|
||||
@@ -52,6 +53,9 @@ export class UIRuntimeModule implements IRuntimeModuleLoader {
|
||||
const layoutSystem = new UILayoutSystem();
|
||||
scene.addSystem(layoutSystem);
|
||||
|
||||
const animationSystem = new UIAnimationSystem();
|
||||
scene.addSystem(animationSystem);
|
||||
|
||||
const renderBeginSystem = new UIRenderBeginSystem();
|
||||
scene.addSystem(renderBeginSystem);
|
||||
|
||||
@@ -81,6 +85,7 @@ export class UIRuntimeModule implements IRuntimeModuleLoader {
|
||||
|
||||
const uiRenderProvider = new UIRenderDataProvider();
|
||||
const inputSystem = new UIInputSystem();
|
||||
inputSystem.setLayoutSystem(layoutSystem);
|
||||
scene.addSystem(inputSystem);
|
||||
|
||||
context.uiLayoutSystem = layoutSystem;
|
||||
|
||||
@@ -52,12 +52,16 @@ export class UIScrollViewComponent extends Component {
|
||||
* 内容宽度
|
||||
* Content width
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Content Width', min: 0 })
|
||||
public contentWidth: number = 0;
|
||||
|
||||
/**
|
||||
* 内容高度
|
||||
* Content height
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Content Height', min: 0 })
|
||||
public contentHeight: number = 0;
|
||||
|
||||
// ===== 滚动配置 Scroll Configuration =====
|
||||
|
||||
@@ -135,8 +135,8 @@ export type EasingName = keyof typeof Easing;
|
||||
@ECSSystem('UIAnimation')
|
||||
export class UIAnimationSystem extends EntitySystem {
|
||||
constructor() {
|
||||
// 匹配任何可能有动画的组件
|
||||
super(Matcher.empty());
|
||||
// 匹配有任何动画组件的实体
|
||||
super(Matcher.empty().any(UIButtonComponent, UIProgressBarComponent, UISliderComponent));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,6 +3,8 @@ import { UITransformComponent } from '../components/UITransformComponent';
|
||||
import { UIInteractableComponent } from '../components/UIInteractableComponent';
|
||||
import { UIButtonComponent } from '../components/widgets/UIButtonComponent';
|
||||
import { UISliderComponent } from '../components/widgets/UISliderComponent';
|
||||
import { UIScrollViewComponent } from '../components/widgets/UIScrollViewComponent';
|
||||
import type { UILayoutSystem } from './UILayoutSystem';
|
||||
|
||||
/**
|
||||
* 鼠标按钮
|
||||
@@ -58,6 +60,23 @@ export class UIInputSystem extends EntitySystem {
|
||||
private lastClickEntity: Entity | null = null;
|
||||
private doubleClickThreshold: number = 300; // ms
|
||||
|
||||
// ===== 滚轮状态 Wheel State =====
|
||||
|
||||
private wheelDeltaX: number = 0;
|
||||
private wheelDeltaY: number = 0;
|
||||
private hasWheelEvent: boolean = false;
|
||||
|
||||
// ===== ScrollView 拖拽状态 ScrollView Drag State =====
|
||||
|
||||
private scrollViewDragTarget: Entity | null = null;
|
||||
private scrollViewDragStartX: number = 0;
|
||||
private scrollViewDragStartY: number = 0;
|
||||
private scrollViewDragStartScrollX: number = 0;
|
||||
private scrollViewDragStartScrollY: number = 0;
|
||||
private scrollViewLastDragX: number = 0;
|
||||
private scrollViewLastDragY: number = 0;
|
||||
private scrollViewLastDragTime: number = 0;
|
||||
|
||||
// ===== 事件监听器 Event Listeners =====
|
||||
|
||||
private canvas: HTMLCanvasElement | null = null;
|
||||
@@ -66,6 +85,11 @@ export class UIInputSystem extends EntitySystem {
|
||||
private boundMouseUp: (e: MouseEvent) => void;
|
||||
private boundWheel: (e: WheelEvent) => void;
|
||||
|
||||
// ===== UI 布局系统引用 UI Layout System Reference =====
|
||||
// 用于获取 UI 画布尺寸以进行坐标转换
|
||||
// Used to get UI canvas size for coordinate conversion
|
||||
private layoutSystem: UILayoutSystem | null = null;
|
||||
|
||||
constructor() {
|
||||
super(Matcher.empty().all(UITransformComponent, UIInteractableComponent));
|
||||
|
||||
@@ -75,6 +99,17 @@ export class UIInputSystem extends EntitySystem {
|
||||
this.boundWheel = this.onWheel.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 UI 布局系统引用
|
||||
* Set UI layout system reference
|
||||
*
|
||||
* 用于获取 UI 画布尺寸以进行坐标转换
|
||||
* Used to get UI canvas size for coordinate conversion
|
||||
*/
|
||||
public setLayoutSystem(layoutSystem: UILayoutSystem): void {
|
||||
this.layoutSystem = layoutSystem;
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定到 Canvas 元素
|
||||
* Bind to canvas element
|
||||
@@ -128,7 +163,30 @@ export class UIInputSystem extends EntitySystem {
|
||||
|
||||
private onMouseMove(e: MouseEvent): void {
|
||||
const rect = this.canvas!.getBoundingClientRect();
|
||||
this.setMousePosition(e.clientX - rect.left, e.clientY - rect.top);
|
||||
// 屏幕坐标(CSS 像素,左上角为 0,0,Y 向下)
|
||||
const screenX = e.clientX - rect.left;
|
||||
const screenY = e.clientY - rect.top;
|
||||
|
||||
// 获取 UI 画布尺寸
|
||||
const uiCanvasSize = this.layoutSystem?.getCanvasSize() ?? { width: 1920, height: 1080 };
|
||||
|
||||
// 转换为 UI 世界坐标(中心为 0,0,Y 向上)
|
||||
// UI 坐标系:左上角是 (-width/2, +height/2),右下角是 (+width/2, -height/2)
|
||||
// 屏幕坐标系:左上角是 (0, 0),右下角是 (canvasWidth, canvasHeight)
|
||||
const canvasWidth = rect.width;
|
||||
const canvasHeight = rect.height;
|
||||
|
||||
// 屏幕坐标归一化到 [0, 1]
|
||||
const normalizedX = screenX / canvasWidth;
|
||||
const normalizedY = screenY / canvasHeight;
|
||||
|
||||
// 转换为 UI 世界坐标
|
||||
// X: 0 -> -uiCanvasWidth/2, 1 -> +uiCanvasWidth/2
|
||||
// Y: 0 -> +uiCanvasHeight/2 (顶部), 1 -> -uiCanvasHeight/2 (底部)
|
||||
const worldX = (normalizedX - 0.5) * uiCanvasSize.width;
|
||||
const worldY = (0.5 - normalizedY) * uiCanvasSize.height;
|
||||
|
||||
this.setMousePosition(worldX, worldY);
|
||||
}
|
||||
|
||||
private onMouseDown(e: MouseEvent): void {
|
||||
@@ -139,11 +197,17 @@ export class UIInputSystem extends EntitySystem {
|
||||
this.setMouseButton(e.button as MouseButton, false);
|
||||
}
|
||||
|
||||
private onWheel(_e: WheelEvent): void {
|
||||
// TODO: 处理滚轮事件
|
||||
private onWheel(e: WheelEvent): void {
|
||||
e.preventDefault();
|
||||
this.wheelDeltaX = e.deltaX;
|
||||
this.wheelDeltaY = e.deltaY;
|
||||
this.hasWheelEvent = true;
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// 如果没有绑定 canvas,不处理输入
|
||||
if (!this.canvas) return;
|
||||
|
||||
const dt = Time.deltaTime;
|
||||
|
||||
// 按 zIndex 从高到低排序,确保上层元素优先处理
|
||||
@@ -214,6 +278,7 @@ export class UIInputSystem extends EntitySystem {
|
||||
// 处理特殊控件
|
||||
this.handleSlider(entity);
|
||||
this.handleButton(entity, interactable);
|
||||
this.handleScrollView(entity, transform);
|
||||
|
||||
// 阻止事件传递到下层
|
||||
if (interactable.blockEvents) {
|
||||
@@ -230,6 +295,9 @@ export class UIInputSystem extends EntitySystem {
|
||||
if (interactable.pressed && !this.mouseButtons[MouseButton.Left]) {
|
||||
interactable.pressed = false;
|
||||
}
|
||||
|
||||
// 即使鼠标不在元素上,也需要更新按钮的目标颜色(恢复到 normal)
|
||||
this.handleButton(entity, interactable);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,6 +359,13 @@ export class UIInputSystem extends EntitySystem {
|
||||
this.lastClickTime = 0;
|
||||
} else {
|
||||
interactable.onClick?.();
|
||||
|
||||
// 如果是按钮,也调用按钮的 onClick
|
||||
const button = entity.getComponent(UIButtonComponent);
|
||||
if (button && !button.disabled) {
|
||||
button.onClick?.();
|
||||
}
|
||||
|
||||
this.lastClickEntity = entity;
|
||||
this.lastClickTime = now;
|
||||
}
|
||||
@@ -344,8 +419,11 @@ export class UIInputSystem extends EntitySystem {
|
||||
const button = entity.getComponent(UIButtonComponent);
|
||||
if (!button || button.disabled) return;
|
||||
|
||||
// 更新目标颜色
|
||||
button.targetColor = button.getStateColor(interactable.getState());
|
||||
// 更新目标颜色和当前颜色
|
||||
const stateColor = button.getStateColor(interactable.getState());
|
||||
button.targetColor = stateColor;
|
||||
// 直接设置 currentColor 以便立即看到效果(动画系统会平滑过渡)
|
||||
button.currentColor = stateColor;
|
||||
|
||||
// 处理长按
|
||||
if (interactable.pressed) {
|
||||
@@ -365,6 +443,220 @@ export class UIInputSystem extends EntitySystem {
|
||||
}
|
||||
}
|
||||
|
||||
private handleScrollView(entity: Entity, transform: UITransformComponent): void {
|
||||
const scrollView = entity.getComponent(UIScrollViewComponent);
|
||||
if (!scrollView) return;
|
||||
|
||||
const viewportWidth = transform.computedWidth;
|
||||
const viewportHeight = transform.computedHeight;
|
||||
const maxScrollX = scrollView.getMaxScrollX(viewportWidth);
|
||||
const maxScrollY = scrollView.getMaxScrollY(viewportHeight);
|
||||
|
||||
// 处理滚轮事件
|
||||
if (this.hasWheelEvent) {
|
||||
let deltaX = 0;
|
||||
let deltaY = 0;
|
||||
|
||||
if (scrollView.verticalScroll && maxScrollY > 0) {
|
||||
deltaY = this.wheelDeltaY * (scrollView.wheelSpeed / 40);
|
||||
}
|
||||
if (scrollView.horizontalScroll && maxScrollX > 0) {
|
||||
deltaX = this.wheelDeltaX * (scrollView.wheelSpeed / 40);
|
||||
}
|
||||
|
||||
if (deltaX !== 0 || deltaY !== 0) {
|
||||
if (scrollView.smoothScroll) {
|
||||
scrollView.targetScrollX = this.clampScroll(scrollView.targetScrollX + deltaX, maxScrollX);
|
||||
scrollView.targetScrollY = this.clampScroll(scrollView.targetScrollY + deltaY, maxScrollY);
|
||||
} else {
|
||||
scrollView.scrollX = this.clampScroll(scrollView.scrollX + deltaX, maxScrollX);
|
||||
scrollView.scrollY = this.clampScroll(scrollView.scrollY + deltaY, maxScrollY);
|
||||
scrollView.targetScrollX = scrollView.scrollX;
|
||||
scrollView.targetScrollY = scrollView.scrollY;
|
||||
}
|
||||
scrollView.velocityX = 0;
|
||||
scrollView.velocityY = 0;
|
||||
}
|
||||
|
||||
this.hasWheelEvent = false;
|
||||
this.wheelDeltaX = 0;
|
||||
this.wheelDeltaY = 0;
|
||||
}
|
||||
|
||||
// 处理内容拖拽滚动
|
||||
const isLeftPressed = this.mouseButtons[MouseButton.Left]!;
|
||||
const wasLeftPressed = this.prevMouseButtons[MouseButton.Left]!;
|
||||
|
||||
if (isLeftPressed && !wasLeftPressed && !scrollView.dragging) {
|
||||
// 开始拖拽
|
||||
scrollView.dragging = true;
|
||||
scrollView.dragStartScrollX = scrollView.scrollX;
|
||||
scrollView.dragStartScrollY = scrollView.scrollY;
|
||||
this.scrollViewDragTarget = entity;
|
||||
this.scrollViewDragStartX = this.mouseX;
|
||||
this.scrollViewDragStartY = this.mouseY;
|
||||
this.scrollViewDragStartScrollX = scrollView.scrollX;
|
||||
this.scrollViewDragStartScrollY = scrollView.scrollY;
|
||||
this.scrollViewLastDragX = this.mouseX;
|
||||
this.scrollViewLastDragY = this.mouseY;
|
||||
this.scrollViewLastDragTime = performance.now();
|
||||
scrollView.velocityX = 0;
|
||||
scrollView.velocityY = 0;
|
||||
}
|
||||
|
||||
if (scrollView.dragging && this.scrollViewDragTarget === entity) {
|
||||
if (isLeftPressed) {
|
||||
// 拖拽中
|
||||
const dragDeltaX = this.mouseX - this.scrollViewDragStartX;
|
||||
const dragDeltaY = this.mouseY - this.scrollViewDragStartY;
|
||||
|
||||
let newScrollX = this.scrollViewDragStartScrollX - dragDeltaX;
|
||||
let newScrollY = this.scrollViewDragStartScrollY - dragDeltaY;
|
||||
|
||||
if (scrollView.horizontalScroll) {
|
||||
if (scrollView.elasticBounds) {
|
||||
newScrollX = this.applyElasticBounds(newScrollX, maxScrollX, scrollView.elasticity);
|
||||
} else {
|
||||
newScrollX = this.clampScroll(newScrollX, maxScrollX);
|
||||
}
|
||||
scrollView.scrollX = newScrollX;
|
||||
scrollView.targetScrollX = newScrollX;
|
||||
}
|
||||
|
||||
if (scrollView.verticalScroll) {
|
||||
if (scrollView.elasticBounds) {
|
||||
newScrollY = this.applyElasticBounds(newScrollY, maxScrollY, scrollView.elasticity);
|
||||
} else {
|
||||
newScrollY = this.clampScroll(newScrollY, maxScrollY);
|
||||
}
|
||||
scrollView.scrollY = newScrollY;
|
||||
scrollView.targetScrollY = newScrollY;
|
||||
}
|
||||
|
||||
// 计算速度(用于惯性)
|
||||
const now = performance.now();
|
||||
const timeDelta = (now - this.scrollViewLastDragTime) / 1000;
|
||||
if (timeDelta > 0) {
|
||||
scrollView.velocityX = (this.scrollViewLastDragX - this.mouseX) / timeDelta;
|
||||
scrollView.velocityY = (this.scrollViewLastDragY - this.mouseY) / timeDelta;
|
||||
}
|
||||
this.scrollViewLastDragX = this.mouseX;
|
||||
this.scrollViewLastDragY = this.mouseY;
|
||||
this.scrollViewLastDragTime = now;
|
||||
} else {
|
||||
// 结束拖拽
|
||||
scrollView.dragging = false;
|
||||
this.scrollViewDragTarget = null;
|
||||
|
||||
// 应用惯性
|
||||
if (!scrollView.inertia) {
|
||||
scrollView.velocityX = 0;
|
||||
scrollView.velocityY = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新惯性滚动
|
||||
this.updateScrollViewInertia(scrollView, maxScrollX, maxScrollY);
|
||||
|
||||
// 平滑滚动到目标位置
|
||||
this.updateScrollViewSmooth(scrollView);
|
||||
}
|
||||
|
||||
private clampScroll(value: number, max: number): number {
|
||||
return Math.max(0, Math.min(max, value));
|
||||
}
|
||||
|
||||
private applyElasticBounds(value: number, max: number, elasticity: number): number {
|
||||
if (value < 0) {
|
||||
return value * elasticity;
|
||||
}
|
||||
if (value > max) {
|
||||
return max + (value - max) * elasticity;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private updateScrollViewInertia(scrollView: UIScrollViewComponent, maxScrollX: number, maxScrollY: number): void {
|
||||
if (scrollView.dragging) return;
|
||||
|
||||
const dt = Time.deltaTime;
|
||||
const deceleration = scrollView.decelerationRate;
|
||||
const minVelocity = 10;
|
||||
|
||||
// 应用减速
|
||||
if (Math.abs(scrollView.velocityX) > minVelocity || Math.abs(scrollView.velocityY) > minVelocity) {
|
||||
scrollView.velocityX *= Math.pow(deceleration, dt * 60);
|
||||
scrollView.velocityY *= Math.pow(deceleration, dt * 60);
|
||||
|
||||
// 更新滚动位置
|
||||
if (scrollView.horizontalScroll) {
|
||||
scrollView.scrollX += scrollView.velocityX * dt;
|
||||
scrollView.targetScrollX = scrollView.scrollX;
|
||||
}
|
||||
if (scrollView.verticalScroll) {
|
||||
scrollView.scrollY += scrollView.velocityY * dt;
|
||||
scrollView.targetScrollY = scrollView.scrollY;
|
||||
}
|
||||
|
||||
// 停止条件
|
||||
if (Math.abs(scrollView.velocityX) < minVelocity) scrollView.velocityX = 0;
|
||||
if (Math.abs(scrollView.velocityY) < minVelocity) scrollView.velocityY = 0;
|
||||
}
|
||||
|
||||
// 弹回边界
|
||||
if (scrollView.elasticBounds && !scrollView.dragging) {
|
||||
const bounceSpeed = 8;
|
||||
|
||||
if (scrollView.scrollX < 0) {
|
||||
scrollView.scrollX += (0 - scrollView.scrollX) * bounceSpeed * dt;
|
||||
scrollView.targetScrollX = scrollView.scrollX;
|
||||
if (Math.abs(scrollView.scrollX) < 0.5) scrollView.scrollX = 0;
|
||||
} else if (scrollView.scrollX > maxScrollX) {
|
||||
scrollView.scrollX += (maxScrollX - scrollView.scrollX) * bounceSpeed * dt;
|
||||
scrollView.targetScrollX = scrollView.scrollX;
|
||||
if (Math.abs(scrollView.scrollX - maxScrollX) < 0.5) scrollView.scrollX = maxScrollX;
|
||||
}
|
||||
|
||||
if (scrollView.scrollY < 0) {
|
||||
scrollView.scrollY += (0 - scrollView.scrollY) * bounceSpeed * dt;
|
||||
scrollView.targetScrollY = scrollView.scrollY;
|
||||
if (Math.abs(scrollView.scrollY) < 0.5) scrollView.scrollY = 0;
|
||||
} else if (scrollView.scrollY > maxScrollY) {
|
||||
scrollView.scrollY += (maxScrollY - scrollView.scrollY) * bounceSpeed * dt;
|
||||
scrollView.targetScrollY = scrollView.scrollY;
|
||||
if (Math.abs(scrollView.scrollY - maxScrollY) < 0.5) scrollView.scrollY = maxScrollY;
|
||||
}
|
||||
} else {
|
||||
// 硬边界限制
|
||||
scrollView.scrollX = this.clampScroll(scrollView.scrollX, maxScrollX);
|
||||
scrollView.scrollY = this.clampScroll(scrollView.scrollY, maxScrollY);
|
||||
}
|
||||
}
|
||||
|
||||
private updateScrollViewSmooth(scrollView: UIScrollViewComponent): void {
|
||||
if (scrollView.dragging) return;
|
||||
if (scrollView.velocityX !== 0 || scrollView.velocityY !== 0) return;
|
||||
|
||||
const dt = Time.deltaTime;
|
||||
const smoothSpeed = 1 / Math.max(0.01, scrollView.smoothScrollDuration);
|
||||
|
||||
const diffX = scrollView.targetScrollX - scrollView.scrollX;
|
||||
const diffY = scrollView.targetScrollY - scrollView.scrollY;
|
||||
|
||||
if (Math.abs(diffX) > 0.5) {
|
||||
scrollView.scrollX += diffX * smoothSpeed * dt;
|
||||
} else {
|
||||
scrollView.scrollX = scrollView.targetScrollX;
|
||||
}
|
||||
|
||||
if (Math.abs(diffY) > 0.5) {
|
||||
scrollView.scrollY += diffY * smoothSpeed * dt;
|
||||
} else {
|
||||
scrollView.scrollY = scrollView.targetScrollY;
|
||||
}
|
||||
}
|
||||
|
||||
private updateCursor(hoveredEntity: Entity | null): void {
|
||||
if (!this.canvas) return;
|
||||
|
||||
|
||||
16821
pnpm-lock.yaml
generated
16821
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user