refactor: reorganize package structure and decouple framework packages (#338)

* refactor: reorganize package structure and decouple framework packages

## Package Structure Reorganization
- Reorganized 55 packages into categorized subdirectories:
  - packages/framework/ - Generic framework (Laya/Cocos compatible)
  - packages/engine/ - ESEngine core modules
  - packages/rendering/ - Rendering modules (WASM dependent)
  - packages/physics/ - Physics modules
  - packages/streaming/ - World streaming
  - packages/network-ext/ - Network extensions
  - packages/editor/ - Editor framework and plugins
  - packages/rust/ - Rust WASM engine
  - packages/tools/ - Build tools and SDK

## Framework Package Decoupling
- Decoupled behavior-tree and blueprint packages from ESEngine dependencies
- Created abstracted interfaces (IBTAssetManager, IBehaviorTreeAssetContent)
- ESEngine-specific code moved to esengine/ subpath exports
- Framework packages now usable with Cocos/Laya without ESEngine

## CI Configuration
- Updated CI to only type-check and lint framework packages
- Added type-check:framework and lint:framework scripts

## Breaking Changes
- Package import paths changed due to directory reorganization
- ESEngine integrations now use subpath imports (e.g., '@esengine/behavior-tree/esengine')

* fix: update es-engine file path after directory reorganization

* docs: update README to focus on framework over engine

* ci: only build framework packages, remove Rust/WASM dependencies

* fix: remove esengine subpath from behavior-tree and blueprint builds

ESEngine integration code will only be available in full engine builds.
Framework packages are now purely engine-agnostic.

* fix: move network-protocols to framework, build both in CI

* fix: update workflow paths from packages/core to packages/framework/core

* fix: exclude esengine folder from type-check in behavior-tree and blueprint

* fix: update network tsconfig references to new paths

* fix: add test:ci:framework to only test framework packages in CI

* fix: only build core and math npm packages in CI

* fix: exclude test files from CodeQL and fix string escaping security issue
This commit is contained in:
YHH
2025-12-26 14:50:35 +08:00
committed by GitHub
parent a84ff902e4
commit 155411e743
1936 changed files with 4147 additions and 11578 deletions

View File

@@ -0,0 +1,20 @@
{
"id": "platform-web",
"name": "@esengine/platform-web",
"displayName": "Platform Web",
"description": "Web platform runtime | Web 平台运行时",
"version": "1.0.0",
"category": "Platform",
"icon": "Globe",
"tags": ["platform", "web", "browser", "runtime"],
"isCore": true,
"defaultEnabled": true,
"isEngineModule": true,
"canContainContent": false,
"platforms": ["web"],
"dependencies": ["core", "runtime-core"],
"exports": {},
"outputPath": "dist/index.mjs",
"requiresWasm": false,
"isRuntimeEntry": true
}

View File

@@ -0,0 +1,61 @@
{
"name": "@esengine/platform-web",
"version": "1.0.0",
"description": "Web/H5 Platform Adapter - Browser runtime and asset loading",
"type": "module",
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"files": [
"dist"
],
"scripts": {
"build": "rollup -c",
"build:npm": "npm run build",
"clean": "rimraf dist",
"type-check": "npx tsc --noEmit",
"prepublishOnly": "npm run build"
},
"keywords": [
"ecs",
"web",
"h5",
"platform",
"runtime",
"browser"
],
"author": "yhh",
"license": "MIT",
"dependencies": {
"@esengine/runtime-core": "workspace:*"
},
"devDependencies": {
"@esengine/ecs-framework": "workspace:*",
"@esengine/asset-system": "workspace:*",
"@esengine/engine-core": "workspace:*",
"@esengine/sprite": "workspace:*",
"@esengine/platform-common": "workspace:*",
"@rollup/plugin-commonjs": "^28.0.3",
"@rollup/plugin-node-resolve": "^16.0.1",
"@rollup/plugin-typescript": "^11.1.6",
"rimraf": "^5.0.0",
"rollup": "^4.42.0",
"rollup-plugin-dts": "^6.2.1",
"typescript": "^5.8.3"
},
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "https://github.com/esengine/esengine.git",
"directory": "packages/platform-web"
}
}

View File

@@ -0,0 +1,78 @@
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import typescript from '@rollup/plugin-typescript';
import dts from 'rollup-plugin-dts';
/**
* Platform-web Rollup Configuration
*
* Builds:
* 1. ESM + CJS bundles for editor usage
* 2. TypeScript declarations
*
* All @esengine/* packages are external to avoid bundling.
* Game builds use import maps to resolve modules at runtime.
*/
const external = [
'@esengine/ecs-framework',
'@esengine/runtime-core',
'@esengine/platform-common',
'@esengine/asset-system',
'@esengine/ecs-components',
'@esengine/ecs-engine-bindgen',
'@esengine/tilemap',
'@esengine/fairygui',
'@esengine/behavior-tree',
// Editor packages (should never be in runtime)
'@esengine/editor-core',
'@esengine/fairygui-editor',
'@esengine/tilemap-editor',
'@esengine/behavior-tree-editor',
'@esengine/blueprint-editor',
'@esengine/physics-rapier2d-editor',
// React (editor only)
'react',
'react-dom',
];
export default [
// Main bundle (ESM + CJS)
{
input: 'src/index.ts',
output: [
{
file: 'dist/index.mjs',
format: 'esm',
sourcemap: true
},
{
file: 'dist/index.js',
format: 'cjs',
sourcemap: true
}
],
external,
plugins: [
resolve({
browser: true,
preferBuiltins: false
}),
commonjs(),
typescript({
tsconfig: './tsconfig.json',
declaration: false
})
]
},
// TypeScript declarations
{
input: 'src/index.ts',
output: {
file: 'dist/index.d.ts',
format: 'esm'
},
external,
plugins: [dts()]
}
];

View File

@@ -0,0 +1,129 @@
/**
* Browser Asset Reader
* 浏览器资产读取器
*
* Implements IAssetReader interface for browser environment.
* Uses fetch API to load assets from web server.
*/
/**
* Browser Asset Reader for loading assets via fetch API
* 通过 fetch API 加载资产的浏览器读取器
*/
export class BrowserAssetReader {
private _baseUrl: string;
private _audioContext: AudioContext | null = null;
constructor(baseUrl: string = '/assets') {
this._baseUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
}
/**
* Resolve asset path to URL
* 将资产路径解析为 URL
*/
private _resolveUrl(absolutePath: string): string {
// Handle absolute Windows paths (e.g., F:\TowerECS\assets\...)
if (/^[A-Za-z]:[\\/]/.test(absolutePath)) {
const normalized = absolutePath.replace(/\\/g, '/');
const assetsIndex = normalized.toLowerCase().indexOf('/assets/');
if (assetsIndex >= 0) {
return `${this._baseUrl}${normalized.substring(assetsIndex + 7)}`;
}
const filename = normalized.split('/').pop();
return `${this._baseUrl}/${filename}`;
}
// Handle relative paths
if (absolutePath.startsWith('./') || absolutePath.startsWith('../')) {
return absolutePath;
}
if (absolutePath.startsWith('/assets/')) {
return absolutePath;
}
if (absolutePath.startsWith('assets/')) {
return `/${absolutePath}`;
}
return `${this._baseUrl}/${absolutePath}`;
}
/**
* Read text file
* 读取文本文件
*/
async readText(absolutePath: string): Promise<string> {
const url = this._resolveUrl(absolutePath);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to load text: ${url} (${response.status})`);
}
return response.text();
}
/**
* Read binary file
* 读取二进制文件
*/
async readBinary(absolutePath: string): Promise<ArrayBuffer> {
const url = this._resolveUrl(absolutePath);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to load binary: ${url} (${response.status})`);
}
return response.arrayBuffer();
}
/**
* Load image
* 加载图片
*/
async loadImage(absolutePath: string): Promise<HTMLImageElement> {
const url = this._resolveUrl(absolutePath);
return new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = () => resolve(img);
img.onerror = () => reject(new Error(`Failed to load image: ${url}`));
img.src = url;
});
}
/**
* Load audio
* 加载音频
*/
async loadAudio(absolutePath: string): Promise<AudioBuffer> {
if (!this._audioContext) {
this._audioContext = new AudioContext();
}
const url = this._resolveUrl(absolutePath);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to load audio: ${url} (${response.status})`);
}
const arrayBuffer = await response.arrayBuffer();
return this._audioContext.decodeAudioData(arrayBuffer);
}
/**
* Check if file exists
* 检查文件是否存在
*/
async exists(absolutePath: string): Promise<boolean> {
const url = this._resolveUrl(absolutePath);
try {
const response = await fetch(url, { method: 'HEAD' });
return response.ok;
} catch {
return false;
}
}
/**
* Get base URL
* 获取基础 URL
*/
get baseUrl(): string {
return this._baseUrl;
}
}

View File

@@ -0,0 +1,427 @@
/**
* Browser Runtime
* 浏览器运行时
*
* Lightweight runtime for web game builds.
* Uses dynamic plugin loading via import maps.
*
* 轻量级 Web 游戏运行时。
* 通过 import maps 动态加载插件。
*/
import { Core } from '@esengine/ecs-framework';
import {
GameRuntime,
createGameRuntime,
BrowserPlatformAdapter,
runtimePluginManager,
BrowserFileSystemService,
RuntimeSceneManager,
RuntimeSceneManagerToken,
type IRuntimePlugin,
type IRuntimeSceneManager
} from '@esengine/runtime-core';
import { isValidGUID, setGlobalAssetFileLoader, type IAssetManager, type IAssetFileLoader } from '@esengine/asset-system';
import { BrowserAssetReader } from './BrowserAssetReader';
import { WebInputSubsystem } from './subsystems/WebInputSubsystem';
/**
* Runtime configuration
* 运行时配置
*/
export interface RuntimeConfig {
/** Canvas element ID */
canvasId: string;
/** Canvas width (defaults to window.innerWidth) */
width?: number;
/** Canvas height (defaults to window.innerHeight) */
height?: number;
/** Asset catalog file URL (defaults to '/asset-catalog.json') */
assetCatalogUrl?: string;
/** Asset base URL (defaults to '/assets') */
assetBaseUrl?: string;
}
/**
* Browser Runtime
* 浏览器运行时
*
* Main entry point for running games in browser.
* Supports dynamic plugin registration.
*/
export class BrowserRuntime {
private _runtime: GameRuntime | null = null;
private _canvasId: string;
private _width: number;
private _height: number;
private _assetCatalogUrl: string;
private _assetBaseUrl: string;
private _fileSystem: BrowserFileSystemService | null = null;
private _assetReader: BrowserAssetReader | null = null;
private _sceneManager: RuntimeSceneManager | null = null;
private _initialized = false;
constructor(config: RuntimeConfig) {
this._canvasId = config.canvasId;
this._width = config.width ?? window.innerWidth;
this._height = config.height ?? window.innerHeight;
this._assetCatalogUrl = config.assetCatalogUrl ?? '/asset-catalog.json';
this._assetBaseUrl = config.assetBaseUrl ?? '/assets';
}
/**
* Register a plugin dynamically
* 动态注册插件
*
* Call this before initialize() to register plugins.
*/
registerPlugin(plugin: IRuntimePlugin): void {
if (plugin) {
runtimePluginManager.register(plugin);
runtimePluginManager.enable(plugin.manifest.id);
console.log(`[Runtime] Registered plugin: ${plugin.manifest.id}`);
}
}
/**
* Register multiple plugins
* 注册多个插件
*/
registerPlugins(plugins: IRuntimePlugin[]): void {
for (const plugin of plugins) {
this.registerPlugin(plugin);
}
}
/**
* Initialize the runtime
* 初始化运行时
*
* @param wasmModule - Optional WASM module (from es_engine.js)
*/
async initialize(wasmModule?: unknown): Promise<void> {
if (this._initialized) {
console.warn('[Runtime] Already initialized');
return;
}
// Initialize browser file system service
this._fileSystem = new BrowserFileSystemService({
baseUrl: this._assetBaseUrl,
catalogUrl: this._assetCatalogUrl,
enableCache: true
});
await this._fileSystem.initialize();
// Initialize asset reader
this._assetReader = new BrowserAssetReader(this._assetBaseUrl);
// Create browser platform adapter
// 创建输入子系统,用于处理键盘、鼠标、触摸事件
// Create input subsystem for keyboard, mouse, touch events
// 使用 'direct' 模式,因为独立 Web 构建不使用 Tauri 代理
// Use 'direct' mode since standalone web builds don't use Tauri proxy
const platform = new BrowserPlatformAdapter({
wasmModule: wasmModule ?? undefined,
inputSubsystemFactory: () => new WebInputSubsystem(),
assetBaseUrl: this._assetBaseUrl,
pathResolveMode: 'direct'
});
// Create game runtime
this._runtime = createGameRuntime({
platform,
canvasId: this._canvasId,
width: this._width,
height: this._height,
autoStartRenderLoop: false
});
await this._runtime.initialize();
// Register file system service
const IFileSystemServiceKey = Symbol.for('IFileSystemService');
if (!Core.services.isRegistered(IFileSystemServiceKey)) {
Core.services.registerInstance(IFileSystemServiceKey, this._fileSystem);
}
// Set asset reader for AssetManager
// 设置资产读取器
if (this._assetReader && this._runtime.assetManager) {
this._runtime.assetManager.setReader(this._assetReader);
// Initialize AssetManager with catalog from BrowserFileSystemService
// 使用 BrowserFileSystemService 的 catalog 初始化 AssetManager
// Catalog format is now unified - no conversion needed
// 目录格式已统一 - 无需转换
if (this._fileSystem?.catalog) {
const catalog = this._fileSystem.catalog;
this._runtime.assetManager.initializeFromCatalog(catalog);
}
// Set global asset file loader for UI atlas and other subsystems
// 设置全局资产文件加载器供 UI 图集和其他子系统使用
const assetFileLoader: IAssetFileLoader = {
loadImage: (assetPath: string) => this._assetReader!.loadImage(assetPath),
loadText: (assetPath: string) => this._assetReader!.readText(assetPath),
loadBinary: (assetPath: string) => this._assetReader!.readBinary(assetPath),
exists: (assetPath: string) => this._assetReader!.exists(assetPath)
};
setGlobalAssetFileLoader(assetFileLoader);
}
// Disable editor mode (hides grid, gizmos, axis indicator)
// 禁用编辑器模式隐藏网格、gizmos、坐标轴指示器
this._runtime.setEditorMode(false);
// Set up asset path resolver for rendering system
// 为渲染系统设置资产路径解析器
this._setupAssetPathResolver();
// Initialize scene manager
// 初始化场景管理器
this._initializeSceneManager();
this._initialized = true;
console.log('[Runtime] Initialized');
}
/**
* Initialize the runtime scene manager
* 初始化运行时场景管理器
*/
private _initializeSceneManager(): void {
if (!this._runtime) return;
// Create scene manager with scene loader
// 使用场景加载器创建场景管理器
this._sceneManager = new RuntimeSceneManager(
(url: string) => this._runtime!.loadSceneFromUrl(url),
'./scenes'
);
// Auto-discover scenes from catalog
// 从目录自动发现场景
// scenes 是运行时扩展字段,不在 IAssetCatalog 接口中
// scenes is a runtime extension field, not in IAssetCatalog interface
const catalog = this._fileSystem?.catalog as { scenes?: Array<{ name: string; path: string }> } | null;
if (catalog?.scenes) {
const scenes = catalog.scenes.map((scene) => ({
name: scene.name,
path: `./scenes/${scene.name}.ecs`
}));
this._sceneManager.registerScenes(scenes);
}
// Register scene manager as a service
// 注册场景管理器为服务
const serviceRegistry = this._runtime.getServiceRegistry();
if (serviceRegistry) {
serviceRegistry.register(RuntimeSceneManagerToken, this._sceneManager);
}
// Also register in Core.services for global access (systems can access it)
// 同时注册到 Core.services 供全局访问(系统可以访问)
// RuntimeSceneManager 实现了 IService 接口(有 dispose 方法)
// RuntimeSceneManager implements IService interface (has dispose method)
const GlobalSceneManagerKey = Symbol.for('@esengine/service:runtimeSceneManager');
if (!Core.services.isRegistered(GlobalSceneManagerKey)) {
Core.services.registerInstance(GlobalSceneManagerKey, this._sceneManager);
}
console.log('[Runtime] Scene manager initialized');
}
/**
* Set up asset path resolver for the render system
* 为渲染系统设置资产路径解析器
*
* This enables GUID-to-URL resolution for textures in Web runtime.
* 这使得 Web 运行时能够将 GUID 解析为纹理 URL。
*/
private _setupAssetPathResolver(): void {
const renderSystem = this._runtime?.renderSystem;
const assetManager = this._runtime?.assetManager;
if (!renderSystem || !assetManager) return;
const assetBaseUrl = this._assetBaseUrl;
renderSystem.setAssetPathResolver((guidOrPath: string): string => {
// Skip if already a valid URL
// 如果已经是有效的 URL 则跳过
if (!guidOrPath || guidOrPath.startsWith('http') || guidOrPath.startsWith('data:') || guidOrPath.startsWith('/')) {
return guidOrPath;
}
// Check if this is a GUID using the unified validation function
// 使用统一的验证函数检查是否为 GUID
if (isValidGUID(guidOrPath)) {
// Get metadata from runtime's asset manager
// 从运行时资产管理器获取元数据
const metadata = assetManager.getDatabase().getMetadata(guidOrPath);
if (metadata?.path) {
// Construct full URL: baseUrl + relative path
// 构建完整 URLbaseUrl + 相对路径
let relativePath = metadata.path.replace(/\\/g, '/');
// Remove 'assets/' prefix if present to avoid duplication
// 移除 'assets/' 前缀以避免重复assetBaseUrl 已包含 /assets
if (relativePath.startsWith('assets/')) {
relativePath = relativePath.substring(7);
}
return `${assetBaseUrl}/${relativePath}`;
}
// GUID not found in catalog, return original
// 目录中未找到 GUID返回原值
console.warn(`[BrowserRuntime] GUID not found in asset catalog: ${guidOrPath}`);
return guidOrPath;
}
// Not a GUID, treat as relative path
// 不是 GUID视为相对路径
return `${assetBaseUrl}/${guidOrPath}`;
});
console.log('[Runtime] Asset path resolver configured');
}
/**
* Load a scene from URL
* 从 URL 加载场景
*
* @param sceneUrl 场景 URL 或名称 | Scene URL or name
*/
async loadScene(sceneUrl: string): Promise<void> {
if (!this._runtime) {
throw new Error('Runtime not initialized. Call initialize() first.');
}
// Use scene manager if available for proper tracking
// 如果可用,使用场景管理器进行正确跟踪
if (this._sceneManager) {
await this._sceneManager.loadSceneByPath(sceneUrl);
} else {
await this._runtime.loadSceneFromUrl(sceneUrl);
}
}
/**
* Load a scene from data object (for single-file mode)
* 从数据对象加载场景(用于单文件模式)
*/
async loadSceneFromData(sceneData: unknown): Promise<void> {
if (!this._runtime) {
throw new Error('Runtime not initialized. Call initialize() first.');
}
await this._runtime.loadSceneFromData(sceneData);
}
/**
* Start the game loop
* 启动游戏循环
*/
start(): void {
this._runtime?.start();
}
/**
* Stop the game loop
* 停止游戏循环
*/
stop(): void {
this._runtime?.stop();
}
/**
* Handle window resize
* 处理窗口大小变化
*/
handleResize(width: number, height: number): void {
this._runtime?.resize(width, height);
}
/**
* Get the underlying GameRuntime
* 获取底层 GameRuntime
*/
get gameRuntime(): GameRuntime | null {
return this._runtime;
}
/**
* Get the asset manager
* 获取资产管理器
*
* @returns The asset manager instance, or null if not initialized
*/
get assetManager(): IAssetManager | null {
return this._runtime?.assetManager ?? null;
}
/**
* Get the scene manager
* 获取场景管理器
*
* Use this to load scenes, check available scenes, and listen to scene events.
* 使用它来加载场景、检查可用场景和监听场景事件。
*
* @example
* ```typescript
* // Load a scene by name
* await runtime.sceneManager?.loadScene('Level1');
*
* // Get list of available scenes
* const scenes = runtime.sceneManager?.availableScenes;
*
* // Listen to scene load events
* runtime.sceneManager?.onLoadComplete((sceneName) => {
* console.log(`Scene loaded: ${sceneName}`);
* });
* ```
*
* @returns The scene manager instance, or null if not initialized
*/
get sceneManager(): IRuntimeSceneManager | null {
return this._sceneManager;
}
/**
* Check if runtime is initialized
* 检查运行时是否已初始化
*/
get isInitialized(): boolean {
return this._initialized;
}
}
/**
* Create a browser runtime instance
* 创建浏览器运行时实例
*/
export function create(config: RuntimeConfig): BrowserRuntime {
return new BrowserRuntime(config);
}
/**
* Default export for convenient usage
* 默认导出,便于使用
*
* Usage in game HTML:
* ```js
* const ECSRuntime = (await import('@esengine/platform-web')).default;
* const runtime = ECSRuntime.create({ canvasId: 'game-canvas' });
* ```
*/
export default {
create,
BrowserRuntime,
BrowserAssetReader,
// Re-export useful types from dependencies
Core,
GameRuntime,
createGameRuntime,
BrowserPlatformAdapter,
runtimePluginManager
};

View File

@@ -0,0 +1,254 @@
/**
* Rust 引擎桥接层
* 负责在 Web 环境中初始化和管理 Rust WASM 引擎
*/
import type { IPlatformCanvas, CanvasContextAttributes } from '@esengine/platform-common';
import { WebCanvasSubsystem } from './subsystems/WebCanvasSubsystem';
/**
* 引擎配置
*/
export interface EngineBridgeConfig {
wasmPath: string;
canvasId?: string;
canvasWidth?: number;
canvasHeight?: number;
contextAttributes?: CanvasContextAttributes;
}
/**
* GameEngine WASM 模块导出接口
*/
interface GameEngineExports {
memory: WebAssembly.Memory;
new: (canvasIdPtr: number, canvasIdLen: number) => number;
fromExternal: (glContext: any, width: number, height: number) => any;
clear: (engine: any, r: number, g: number, b: number, a: number) => void;
render: (engine: any) => void;
width: (engine: any) => number;
height: (engine: any) => number;
submitSpriteBatch: (
engine: any,
transforms: any,
textureIds: any,
uvs: any,
colors: any
) => void;
loadTexture: (engine: any, id: number, urlPtr: number, urlLen: number) => void;
isKeyDown: (engine: any, keyCodePtr: number, keyCodeLen: number) => boolean;
updateInput: (engine: any) => void;
}
/**
* 引擎桥接层
* 将 Web 平台能力桥接到 Rust WASM 引擎
*/
export class EngineBridge {
private _canvasSubsystem: WebCanvasSubsystem;
private _canvas: IPlatformCanvas;
private _gl: WebGL2RenderingContext | null = null;
private _wasmModule: WebAssembly.Module | null = null;
private _wasmInstance: WebAssembly.Instance | null = null;
private _gameEngine: any = null;
private _config: EngineBridgeConfig;
constructor(config: EngineBridgeConfig) {
this._config = config;
this._canvasSubsystem = new WebCanvasSubsystem();
const width = config.canvasWidth ?? window.innerWidth;
const height = config.canvasHeight ?? window.innerHeight;
if (config.canvasId) {
const existingCanvas = document.getElementById(config.canvasId) as HTMLCanvasElement;
if (existingCanvas) {
existingCanvas.width = width;
existingCanvas.height = height;
this._canvas = this._wrapExistingCanvas(existingCanvas);
} else {
this._canvas = this._canvasSubsystem.createCanvas(width, height);
}
} else {
this._canvas = this._canvasSubsystem.createCanvas(width, height);
}
}
private _wrapExistingCanvas(canvas: HTMLCanvasElement): IPlatformCanvas {
return {
width: canvas.width,
height: canvas.height,
getContext: (type: string, attrs: any) => canvas.getContext(type, attrs as WebGLContextAttributes),
toDataURL: () => canvas.toDataURL(),
toTempFilePath: () => {
throw new Error('Not supported');
}
} as IPlatformCanvas;
}
/**
* 初始化引擎
*/
async initialize(): Promise<void> {
this._gl = this._getWebGLContext();
if (!this._gl) {
throw new Error('无法获取 WebGL2 上下文');
}
const imports = this._createWASMImports();
const response = await fetch(this._config.wasmPath);
const buffer = await response.arrayBuffer();
const result = await WebAssembly.instantiate(buffer, imports);
this._wasmModule = result.module;
this._wasmInstance = result.instance;
const exports = this._wasmInstance.exports as unknown as GameEngineExports;
if (typeof exports.fromExternal === 'function') {
this._gameEngine = exports.fromExternal(
this._gl,
this._canvas.width,
this._canvas.height
);
}
}
/**
* 获取 WebGL2 上下文
*/
private _getWebGLContext(): WebGL2RenderingContext | null {
const attrs = this._config.contextAttributes ?? {
alpha: false,
antialias: false,
depth: false,
stencil: false,
premultipliedAlpha: true,
preserveDrawingBuffer: false
};
return this._canvas.getContext('webgl2', attrs) as WebGL2RenderingContext | null;
}
/**
* 创建 WASM 导入对象
*/
private _createWASMImports(): WebAssembly.Imports {
return {
env: {
memory: new WebAssembly.Memory({ initial: 256, maximum: 16384 }),
platform_log: (ptr: number, len: number) => {
const message = this._readString(ptr, len);
console.log('[Engine]', message);
},
platform_error: (ptr: number, len: number) => {
const message = this._readString(ptr, len);
console.error('[Engine]', message);
},
platform_now: () => {
return performance.now();
}
},
wbg: {}
};
}
/**
* 从 WASM 内存读取字符串
*/
private _readString(ptr: number, len: number): string {
if (!this._wasmInstance) return '';
const memory = this._wasmInstance.exports.memory as WebAssembly.Memory;
const bytes = new Uint8Array(memory.buffer, ptr, len);
return new TextDecoder().decode(bytes);
}
/**
* 获取 Canvas
*/
get canvas(): IPlatformCanvas {
return this._canvas;
}
/**
* 获取 WebGL 上下文
*/
get gl(): WebGL2RenderingContext | null {
return this._gl;
}
/**
* 获取 WASM 实例
*/
get wasmInstance(): WebAssembly.Instance | null {
return this._wasmInstance;
}
/**
* 获取 GameEngine 实例
*/
get gameEngine(): any {
return this._gameEngine;
}
/**
* 清屏
*/
clear(r: number, g: number, b: number, a: number): void {
if (this._gl) {
this._gl.clearColor(r, g, b, a);
this._gl.clear(this._gl.COLOR_BUFFER_BIT);
}
}
/**
* 渲染一帧
*/
render(): void {
if (this._wasmInstance && this._gameEngine) {
const exports = this._wasmInstance.exports as unknown as GameEngineExports;
if (exports.render) {
exports.render(this._gameEngine);
}
}
}
/**
* 获取画布宽度
*/
get width(): number {
return this._canvas.width;
}
/**
* 获取画布高度
*/
get height(): number {
return this._canvas.height;
}
/**
* 调整画布大小
*/
resize(width: number, height: number): void {
this._canvas.width = width;
this._canvas.height = height;
if (this._gl) {
this._gl.viewport(0, 0, width, height);
}
}
/**
* 销毁引擎
*/
dispose(): void {
this._gameEngine = null;
this._wasmInstance = null;
this._wasmModule = null;
this._gl = null;
}
}

View File

@@ -0,0 +1,82 @@
/**
* @esengine/platform-web
*
* Web/H5 Platform Adapter
* Web/H5 平台适配器
*
* Provides browser-specific implementations:
* - BrowserRuntime: Main entry point for game builds
* - BrowserAssetReader: Asset loading via fetch API
* - Web subsystems: Canvas, Input, Storage, WASM
*
* @packageDocumentation
*/
// ============================================
// Runtime (main entry for game builds)
// ============================================
export {
BrowserRuntime,
create,
type RuntimeConfig
} from './BrowserRuntime';
// Default export for convenient usage in game builds
export { default } from './BrowserRuntime';
// Asset reader
export { BrowserAssetReader } from './BrowserAssetReader';
// Re-export scene manager for convenience
// 重新导出场景管理器以方便使用
export {
RuntimeSceneManager,
RuntimeSceneManagerToken,
type IRuntimeSceneManager,
type SceneInfo,
type SceneLoadOptions
} from '@esengine/runtime-core';
// ============================================
// Web Platform Subsystems
// ============================================
export { WebCanvasSubsystem } from './subsystems/WebCanvasSubsystem';
export { WebInputSubsystem } from './subsystems/WebInputSubsystem';
export { WebStorageSubsystem } from './subsystems/WebStorageSubsystem';
export { WebWASMSubsystem } from './subsystems/WebWASMSubsystem';
// ============================================
// Web-specific Systems
// ============================================
export { Canvas2DRenderSystem } from './systems/Canvas2DRenderSystem';
// ============================================
// Platform Detection Utilities
// ============================================
/**
* Check if running in web browser
* 检查是否在浏览器中运行
*/
export function isWebPlatform(): boolean {
return typeof window !== 'undefined' && typeof document !== 'undefined';
}
/**
* Get canvas element by ID
* 根据 ID 获取 canvas 元素
*/
export function getWebCanvas(canvasId: string): HTMLCanvasElement | null {
return document.getElementById(canvasId) as HTMLCanvasElement | null;
}
/**
* Create a new canvas element
* 创建新的 canvas 元素
*/
export function createWebCanvas(width: number, height: number): HTMLCanvasElement {
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
return canvas;
}

View File

@@ -0,0 +1,174 @@
/**
* Web 平台 Canvas 子系统
*/
import type {
IPlatformCanvasSubsystem,
IPlatformCanvas,
IPlatformImage,
TempFilePathOptions,
CanvasContextAttributes
} from '@esengine/platform-common';
/**
* Web Canvas 包装
*/
class WebCanvas implements IPlatformCanvas {
private _canvas: HTMLCanvasElement;
constructor(canvas: HTMLCanvasElement) {
this._canvas = canvas;
}
get width(): number {
return this._canvas.width;
}
set width(value: number) {
this._canvas.width = value;
}
get height(): number {
return this._canvas.height;
}
set height(value: number) {
this._canvas.height = value;
}
getContext(
contextType: '2d' | 'webgl' | 'webgl2',
contextAttributes?: CanvasContextAttributes
): RenderingContext | null {
const attrs: WebGLContextAttributes | undefined = contextAttributes ? {
alpha: typeof contextAttributes.alpha === 'number'
? contextAttributes.alpha > 0
: contextAttributes.alpha,
antialias: contextAttributes.antialias,
depth: contextAttributes.depth,
stencil: contextAttributes.stencil,
premultipliedAlpha: contextAttributes.premultipliedAlpha,
preserveDrawingBuffer: contextAttributes.preserveDrawingBuffer,
failIfMajorPerformanceCaveat: contextAttributes.failIfMajorPerformanceCaveat,
powerPreference: contextAttributes.powerPreference
} : undefined;
return this._canvas.getContext(contextType, attrs);
}
toDataURL(): string {
return this._canvas.toDataURL();
}
toTempFilePath(_options: TempFilePathOptions): void {
throw new Error('toTempFilePath is not supported on Web platform');
}
getNativeCanvas(): HTMLCanvasElement {
return this._canvas;
}
}
/**
* Web Image 包装
*/
class WebImage implements IPlatformImage {
private _image: HTMLImageElement;
constructor() {
this._image = new Image();
}
get src(): string {
return this._image.src;
}
set src(value: string) {
this._image.src = value;
}
get width(): number {
return this._image.width;
}
get height(): number {
return this._image.height;
}
get onload(): (() => void) | null {
return this._image.onload as (() => void) | null;
}
set onload(value: (() => void) | null) {
this._image.onload = value;
}
get onerror(): ((error: any) => void) | null {
return this._image.onerror as ((error: any) => void) | null;
}
set onerror(value: ((error: any) => void) | null) {
this._image.onerror = value;
}
getNativeImage(): HTMLImageElement {
return this._image;
}
}
/**
* Web 平台 Canvas 子系统实现
*/
export class WebCanvasSubsystem implements IPlatformCanvasSubsystem {
private _mainCanvas: WebCanvas | null = null;
createCanvas(width?: number, height?: number): IPlatformCanvas {
const canvas = document.createElement('canvas');
if (width !== undefined) {
canvas.width = width;
}
if (height !== undefined) {
canvas.height = height;
}
const wrappedCanvas = new WebCanvas(canvas);
if (!this._mainCanvas) {
this._mainCanvas = wrappedCanvas;
}
return wrappedCanvas;
}
createImage(): IPlatformImage {
return new WebImage();
}
createImageData(width: number, height: number): ImageData {
return new ImageData(width, height);
}
getScreenWidth(): number {
return window.screen.width;
}
getScreenHeight(): number {
return window.screen.height;
}
getDevicePixelRatio(): number {
return window.devicePixelRatio || 1;
}
getMainCanvas(): IPlatformCanvas | null {
return this._mainCanvas;
}
getWindowWidth(): number {
return window.innerWidth;
}
getWindowHeight(): number {
return window.innerHeight;
}
}

View File

@@ -0,0 +1,336 @@
/**
* Web 平台输入子系统
* Web platform input subsystem
*/
import type {
IPlatformInputSubsystem,
TouchHandler,
TouchEvent,
KeyboardHandler,
KeyboardEventInfo,
MouseHandler,
MouseEventInfo,
WheelHandler,
WheelEventInfo
} from '@esengine/platform-common';
import { MouseButton } from '@esengine/platform-common';
/**
* Web 平台输入子系统实现
* Web platform input subsystem implementation
*/
export class WebInputSubsystem implements IPlatformInputSubsystem {
// ========== Touch handlers ==========
private _touchStartHandlers: Map<TouchHandler, (e: globalThis.TouchEvent) => void> = new Map();
private _touchMoveHandlers: Map<TouchHandler, (e: globalThis.TouchEvent) => void> = new Map();
private _touchEndHandlers: Map<TouchHandler, (e: globalThis.TouchEvent) => void> = new Map();
private _touchCancelHandlers: Map<TouchHandler, (e: globalThis.TouchEvent) => void> = new Map();
// ========== Keyboard handlers ==========
private _keyDownHandlers: Map<KeyboardHandler, (e: globalThis.KeyboardEvent) => void> = new Map();
private _keyUpHandlers: Map<KeyboardHandler, (e: globalThis.KeyboardEvent) => void> = new Map();
// ========== Mouse handlers ==========
private _mouseMoveHandlers: Map<MouseHandler, (e: globalThis.MouseEvent) => void> = new Map();
private _mouseDownHandlers: Map<MouseHandler, (e: globalThis.MouseEvent) => void> = new Map();
private _mouseUpHandlers: Map<MouseHandler, (e: globalThis.MouseEvent) => void> = new Map();
private _wheelHandlers: Map<WheelHandler, (e: globalThis.WheelEvent) => void> = new Map();
// ========== Touch events ==========
onTouchStart(handler: TouchHandler): void {
const nativeHandler = (e: globalThis.TouchEvent) => {
handler(this.convertTouchEvent(e));
};
this._touchStartHandlers.set(handler, nativeHandler);
window.addEventListener('touchstart', nativeHandler);
}
onTouchMove(handler: TouchHandler): void {
const nativeHandler = (e: globalThis.TouchEvent) => {
handler(this.convertTouchEvent(e));
};
this._touchMoveHandlers.set(handler, nativeHandler);
window.addEventListener('touchmove', nativeHandler);
}
onTouchEnd(handler: TouchHandler): void {
const nativeHandler = (e: globalThis.TouchEvent) => {
handler(this.convertTouchEvent(e));
};
this._touchEndHandlers.set(handler, nativeHandler);
window.addEventListener('touchend', nativeHandler);
}
onTouchCancel(handler: TouchHandler): void {
const nativeHandler = (e: globalThis.TouchEvent) => {
handler(this.convertTouchEvent(e));
};
this._touchCancelHandlers.set(handler, nativeHandler);
window.addEventListener('touchcancel', nativeHandler);
}
offTouchStart(handler: TouchHandler): void {
const nativeHandler = this._touchStartHandlers.get(handler);
if (nativeHandler) {
window.removeEventListener('touchstart', nativeHandler);
this._touchStartHandlers.delete(handler);
}
}
offTouchMove(handler: TouchHandler): void {
const nativeHandler = this._touchMoveHandlers.get(handler);
if (nativeHandler) {
window.removeEventListener('touchmove', nativeHandler);
this._touchMoveHandlers.delete(handler);
}
}
offTouchEnd(handler: TouchHandler): void {
const nativeHandler = this._touchEndHandlers.get(handler);
if (nativeHandler) {
window.removeEventListener('touchend', nativeHandler);
this._touchEndHandlers.delete(handler);
}
}
offTouchCancel(handler: TouchHandler): void {
const nativeHandler = this._touchCancelHandlers.get(handler);
if (nativeHandler) {
window.removeEventListener('touchcancel', nativeHandler);
this._touchCancelHandlers.delete(handler);
}
}
supportsPressure(): boolean {
return typeof Touch !== 'undefined' && 'force' in Touch.prototype;
}
// ========== Keyboard events ==========
onKeyDown(handler: KeyboardHandler): void {
const nativeHandler = (e: globalThis.KeyboardEvent) => {
handler(this.convertKeyboardEvent(e));
};
this._keyDownHandlers.set(handler, nativeHandler);
window.addEventListener('keydown', nativeHandler);
}
onKeyUp(handler: KeyboardHandler): void {
const nativeHandler = (e: globalThis.KeyboardEvent) => {
handler(this.convertKeyboardEvent(e));
};
this._keyUpHandlers.set(handler, nativeHandler);
window.addEventListener('keyup', nativeHandler);
}
offKeyDown(handler: KeyboardHandler): void {
const nativeHandler = this._keyDownHandlers.get(handler);
if (nativeHandler) {
window.removeEventListener('keydown', nativeHandler);
this._keyDownHandlers.delete(handler);
}
}
offKeyUp(handler: KeyboardHandler): void {
const nativeHandler = this._keyUpHandlers.get(handler);
if (nativeHandler) {
window.removeEventListener('keyup', nativeHandler);
this._keyUpHandlers.delete(handler);
}
}
// ========== Mouse events ==========
onMouseMove(handler: MouseHandler): void {
const nativeHandler = (e: globalThis.MouseEvent) => {
handler(this.convertMouseEvent(e));
};
this._mouseMoveHandlers.set(handler, nativeHandler);
window.addEventListener('mousemove', nativeHandler);
}
onMouseDown(handler: MouseHandler): void {
const nativeHandler = (e: globalThis.MouseEvent) => {
handler(this.convertMouseEvent(e));
};
this._mouseDownHandlers.set(handler, nativeHandler);
window.addEventListener('mousedown', nativeHandler);
}
onMouseUp(handler: MouseHandler): void {
const nativeHandler = (e: globalThis.MouseEvent) => {
handler(this.convertMouseEvent(e));
};
this._mouseUpHandlers.set(handler, nativeHandler);
window.addEventListener('mouseup', nativeHandler);
}
onWheel(handler: WheelHandler): void {
const nativeHandler = (e: globalThis.WheelEvent) => {
handler(this.convertWheelEvent(e));
};
this._wheelHandlers.set(handler, nativeHandler);
window.addEventListener('wheel', nativeHandler);
}
offMouseMove(handler: MouseHandler): void {
const nativeHandler = this._mouseMoveHandlers.get(handler);
if (nativeHandler) {
window.removeEventListener('mousemove', nativeHandler);
this._mouseMoveHandlers.delete(handler);
}
}
offMouseDown(handler: MouseHandler): void {
const nativeHandler = this._mouseDownHandlers.get(handler);
if (nativeHandler) {
window.removeEventListener('mousedown', nativeHandler);
this._mouseDownHandlers.delete(handler);
}
}
offMouseUp(handler: MouseHandler): void {
const nativeHandler = this._mouseUpHandlers.get(handler);
if (nativeHandler) {
window.removeEventListener('mouseup', nativeHandler);
this._mouseUpHandlers.delete(handler);
}
}
offWheel(handler: WheelHandler): void {
const nativeHandler = this._wheelHandlers.get(handler);
if (nativeHandler) {
window.removeEventListener('wheel', nativeHandler);
this._wheelHandlers.delete(handler);
}
}
// ========== Capability queries ==========
supportsKeyboard(): boolean {
return true;
}
supportsMouse(): boolean {
// 检测是否有鼠标设备 | Check if mouse device exists
return window.matchMedia('(pointer: fine)').matches;
}
// ========== Event converters ==========
private convertTouchEvent(e: globalThis.TouchEvent): TouchEvent {
const convertTouch = (touch: globalThis.Touch) => ({
identifier: touch.identifier,
x: touch.clientX,
y: touch.clientY,
force: (touch as any).force
});
return {
touches: Array.from(e.touches).map(convertTouch),
changedTouches: Array.from(e.changedTouches).map(convertTouch),
timeStamp: e.timeStamp
};
}
private convertKeyboardEvent(e: globalThis.KeyboardEvent): KeyboardEventInfo {
return {
code: e.code,
key: e.key,
altKey: e.altKey,
ctrlKey: e.ctrlKey,
shiftKey: e.shiftKey,
metaKey: e.metaKey,
repeat: e.repeat,
timeStamp: e.timeStamp
};
}
private convertMouseEvent(e: globalThis.MouseEvent): MouseEventInfo {
return {
x: e.clientX,
y: e.clientY,
movementX: e.movementX,
movementY: e.movementY,
button: e.button as MouseButton,
buttons: e.buttons,
altKey: e.altKey,
ctrlKey: e.ctrlKey,
shiftKey: e.shiftKey,
metaKey: e.metaKey,
timeStamp: e.timeStamp
};
}
private convertWheelEvent(e: globalThis.WheelEvent): WheelEventInfo {
return {
x: e.clientX,
y: e.clientY,
deltaX: e.deltaX,
deltaY: e.deltaY,
deltaZ: e.deltaZ,
timeStamp: e.timeStamp
};
}
/**
* 销毁并移除所有事件监听器
* Dispose and remove all event listeners
*/
dispose(): void {
// 清理触摸事件 | Clean up touch events
this._touchStartHandlers.forEach((handler) => {
window.removeEventListener('touchstart', handler);
});
this._touchStartHandlers.clear();
this._touchMoveHandlers.forEach((handler) => {
window.removeEventListener('touchmove', handler);
});
this._touchMoveHandlers.clear();
this._touchEndHandlers.forEach((handler) => {
window.removeEventListener('touchend', handler);
});
this._touchEndHandlers.clear();
this._touchCancelHandlers.forEach((handler) => {
window.removeEventListener('touchcancel', handler);
});
this._touchCancelHandlers.clear();
// 清理键盘事件 | Clean up keyboard events
this._keyDownHandlers.forEach((handler) => {
window.removeEventListener('keydown', handler);
});
this._keyDownHandlers.clear();
this._keyUpHandlers.forEach((handler) => {
window.removeEventListener('keyup', handler);
});
this._keyUpHandlers.clear();
// 清理鼠标事件 | Clean up mouse events
this._mouseMoveHandlers.forEach((handler) => {
window.removeEventListener('mousemove', handler);
});
this._mouseMoveHandlers.clear();
this._mouseDownHandlers.forEach((handler) => {
window.removeEventListener('mousedown', handler);
});
this._mouseDownHandlers.clear();
this._mouseUpHandlers.forEach((handler) => {
window.removeEventListener('mouseup', handler);
});
this._mouseUpHandlers.clear();
this._wheelHandlers.forEach((handler) => {
window.removeEventListener('wheel', handler);
});
this._wheelHandlers.clear();
}
}

View File

@@ -0,0 +1,77 @@
/**
* Web 平台存储子系统
*/
import type {
IPlatformStorageSubsystem,
StorageInfo
} from '@esengine/platform-common';
/**
* Web 平台存储子系统实现
*/
export class WebStorageSubsystem implements IPlatformStorageSubsystem {
getStorageSync<T = any>(key: string): T | undefined {
try {
const value = localStorage.getItem(key);
if (value === null) {
return undefined;
}
return JSON.parse(value) as T;
} catch {
return undefined;
}
}
setStorageSync<T = any>(key: string, value: T): void {
localStorage.setItem(key, JSON.stringify(value));
}
removeStorageSync(key: string): void {
localStorage.removeItem(key);
}
clearStorageSync(): void {
localStorage.clear();
}
getStorageInfoSync(): StorageInfo {
const keys: string[] = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key) {
keys.push(key);
}
}
let currentSize = 0;
for (const key of keys) {
const value = localStorage.getItem(key);
if (value) {
currentSize += key.length + value.length;
}
}
return {
keys,
currentSize: Math.ceil(currentSize / 1024),
limitSize: 5 * 1024
};
}
async getStorage<T = any>(key: string): Promise<T | undefined> {
return this.getStorageSync<T>(key);
}
async setStorage<T = any>(key: string, value: T): Promise<void> {
this.setStorageSync(key, value);
}
async removeStorage(key: string): Promise<void> {
this.removeStorageSync(key);
}
async clearStorage(): Promise<void> {
this.clearStorageSync();
}
}

View File

@@ -0,0 +1,44 @@
/**
* Web 平台 WASM 子系统
*/
import type {
IPlatformWASMSubsystem,
IWASMInstance,
WASMImports,
WASMExports
} from '@esengine/platform-common';
/**
* Web 平台 WASM 子系统实现
*/
export class WebWASMSubsystem implements IPlatformWASMSubsystem {
async instantiate(path: string, imports?: WASMImports): Promise<IWASMInstance> {
const response = await fetch(path);
const buffer = await response.arrayBuffer();
const result = await WebAssembly.instantiate(buffer, imports);
return {
exports: result.instance.exports as WASMExports
};
}
isSupported(): boolean {
return typeof WebAssembly !== 'undefined';
}
createMemory(initial: number, maximum?: number): WebAssembly.Memory {
return new WebAssembly.Memory({
initial,
maximum
});
}
createTable(initial: number, maximum?: number): WebAssembly.Table {
return new WebAssembly.Table({
element: 'anyfunc',
initial,
maximum
});
}
}

View File

@@ -0,0 +1,79 @@
/**
* Canvas 2D Render System
* Canvas 2D 渲染系统
*/
import { EntitySystem, Matcher, ECSSystem, Core } from '@esengine/ecs-framework';
import { TransformComponent } from '@esengine/engine-core';
import { SpriteComponent } from '@esengine/sprite';
@ECSSystem('Canvas2DRender', { updateOrder: 1000 })
export class Canvas2DRenderSystem extends EntitySystem {
private ctx: CanvasRenderingContext2D;
private canvas: HTMLCanvasElement;
private textureCache = new Map<string, HTMLImageElement>();
constructor(ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement) {
super(Matcher.empty());
this.ctx = ctx;
this.canvas = canvas;
}
async loadTexture(path: string): Promise<void> {
if (this.textureCache.has(path)) return;
try {
const img = new Image();
img.crossOrigin = 'anonymous';
const urlPath = `/asset?path=${encodeURIComponent(path)}`;
await new Promise<void>((resolve, reject) => {
img.onload = () => resolve();
img.onerror = () => reject(new Error(`Failed to load: ${path}`));
img.src = urlPath;
});
this.textureCache.set(path, img);
} catch (error) {
console.warn('Texture load failed:', path, error);
}
}
update(): void {
this.ctx.fillStyle = '#1a1a1a';
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
if (!Core.scene) return;
for (const entity of Core.scene.entities.buffer) {
if (!entity.enabled) continue;
const transform = entity.getComponent(TransformComponent) as TransformComponent | null;
const sprite = entity.getComponent(SpriteComponent) as SpriteComponent | null;
if (!transform || !sprite) continue;
this.ctx.save();
// 使用世界变换(由 TransformSystem 计算,考虑父级变换)
const x = (transform.worldPosition.x || 0) + this.canvas.width / 2;
const y = this.canvas.height / 2 - (transform.worldPosition.y || 0);
const width = (sprite.width || 64) * (transform.worldScale.x || 1);
const height = (sprite.height || 64) * (transform.worldScale.y || 1);
const rotation = -(transform.worldRotation.z || 0) * Math.PI / 180;
this.ctx.translate(x, y);
this.ctx.rotate(rotation);
const texture = this.textureCache.get(sprite.textureGuid || '');
if (texture) {
this.ctx.drawImage(texture, -width / 2, -height / 2, width, height);
} else {
this.ctx.fillStyle = sprite.color || '#ffffff';
this.ctx.fillRect(-width / 2, -height / 2, width, height);
}
this.ctx.restore();
}
}
}

View File

@@ -0,0 +1,37 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"lib": [
"ES2020",
"DOM"
],
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"strictNullChecks": true,
"noImplicitAny": true,
"moduleResolution": "bundler",
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": false
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"dist"
],
"references": [
{
"path": "../../engine/platform-common"
}
]
}