@kurot/core 2.0.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/README.md +11 -2
- package/dist/kurot/player/canvas/CanvasRenderer.d.ts.map +1 -1
- package/dist/kurot/player/canvas/CanvasRenderer.js +7 -2
- package/dist/kurot/player/canvas/CanvasRenderer.js.map +1 -1
- package/dist/kurot/text/LineBreaks.d.ts +17 -0
- package/dist/kurot/text/LineBreaks.d.ts.map +1 -0
- package/dist/kurot/text/LineBreaks.js +37 -0
- package/dist/kurot/text/LineBreaks.js.map +1 -0
- package/dist/kurot/text/TextField.d.ts.map +1 -1
- package/dist/kurot/text/TextField.js +11 -118
- package/dist/kurot/text/TextField.js.map +1 -1
- package/dist/kurot/text/TextLineLayout.d.ts +18 -0
- package/dist/kurot/text/TextLineLayout.d.ts.map +1 -0
- package/dist/kurot/text/TextLineLayout.js +141 -0
- package/dist/kurot/text/TextLineLayout.js.map +1 -0
- package/dist/kurot/text/WordWrap.d.ts +5 -7
- package/dist/kurot/text/WordWrap.d.ts.map +1 -1
- package/dist/kurot/text/WordWrap.js +5 -7
- package/dist/kurot/text/WordWrap.js.map +1 -1
- package/docs/ai-context.md +226 -0
- package/docs/architecture.md +371 -0
- package/docs/filters.md +232 -0
- package/docs/resource.md +333 -0
- package/docs/text-layout.md +59 -0
- package/package.json +5 -1
package/docs/filters.md
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# GPU filters
|
|
2
|
+
|
|
3
|
+
Kurot executes `displayObject.filters` in array order. Each filter reads the
|
|
4
|
+
previous filter's image. WebGL 1 and WebGL 2 are supported; shader sources are
|
|
5
|
+
compiled verbatim, with no automatic GLSL translation.
|
|
6
|
+
|
|
7
|
+
## Built-in effects
|
|
8
|
+
|
|
9
|
+
- `ColorMatrixFilter`: color-matrix transform with premultiplied-alpha handling.
|
|
10
|
+
- `BlurFilter(x, y, quality)`: non-negative logical-pixel radii; `quality` is an
|
|
11
|
+
integer from 1 to 16. Each quality step adds one horizontal/vertical pair,
|
|
12
|
+
using radius / sqrt(quality) to approximately preserve variance. The kernel
|
|
13
|
+
remains the existing triangular kernel. Large radii use a reduced-resolution
|
|
14
|
+
image so the per-axis shader radius stays within 32 physical pixels, then
|
|
15
|
+
resample back. This trades fine detail for wider blur; it is not bokeh DoF.
|
|
16
|
+
- `GlowFilter` / `DropShadowFilter`: the existing radial alpha-sampling shader.
|
|
17
|
+
`knockout=false` preserves the original. `hideObject` hides the original for
|
|
18
|
+
shadows. Their `quality` field remains serialized metadata; it does not
|
|
19
|
+
change this shader's fixed sample count.
|
|
20
|
+
- `BloomFilter`: LDR brightness extraction, blur, and original-image composition.
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { BloomFilter, BlurFilter } from '@kurot/core';
|
|
24
|
+
|
|
25
|
+
const blur = new BlurFilter(12, 12, 2);
|
|
26
|
+
blur.resolution = 0.5;
|
|
27
|
+
background.filters = [blur];
|
|
28
|
+
|
|
29
|
+
const bloom = new BloomFilter({
|
|
30
|
+
threshold: 0.7,
|
|
31
|
+
intensity: 1,
|
|
32
|
+
blur: 12,
|
|
33
|
+
blurScale: 0.5,
|
|
34
|
+
});
|
|
35
|
+
world.filters = [bloom];
|
|
36
|
+
bloom.intensity = 0.8;
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`Filter.resolution` specifies physical pixels per logical unit. `undefined`
|
|
40
|
+
inherits the parent. A filter chain uses the minimum of the parent resolution
|
|
41
|
+
and its explicit filter resolutions, additionally limited by GPU texture size.
|
|
42
|
+
`BloomFilter.blurScale` reduces only extraction/blur; the original is retained
|
|
43
|
+
at the effect's full resolution.
|
|
44
|
+
|
|
45
|
+
Filtering includes the rendered descendants. Padding from consecutive filters
|
|
46
|
+
is added; descendant effect extents are included without changing layout bounds.
|
|
47
|
+
A mask/scroll clip on the filtered object clips its final result. Layered
|
|
48
|
+
containers provide control over whether a mask clips before or after another
|
|
49
|
+
effect. A lone ColorMatrix on an unmasked leaf with inherited resolution retains
|
|
50
|
+
its inline fast path; groups and combinations use offscreen composition.
|
|
51
|
+
|
|
52
|
+
## CustomFilter
|
|
53
|
+
|
|
54
|
+
Use `CustomFilter.from()` to provide explicit backend variants. Omit a vertex
|
|
55
|
+
source to use Kurot's standard filter quad. A missing backend throws a descriptive
|
|
56
|
+
error rather than silently drawing the unfiltered image.
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { CustomFilter } from '@kurot/core';
|
|
60
|
+
|
|
61
|
+
const dissolve = CustomFilter.from({
|
|
62
|
+
webgl1: {
|
|
63
|
+
fragment: `precision mediump float;
|
|
64
|
+
varying vec2 vTextureCoord;
|
|
65
|
+
uniform sampler2D uSampler;
|
|
66
|
+
uniform float threshold;
|
|
67
|
+
void main() {
|
|
68
|
+
gl_FragColor = texture2D(uSampler, vTextureCoord)
|
|
69
|
+
* step(threshold, vTextureCoord.x);
|
|
70
|
+
}`,
|
|
71
|
+
},
|
|
72
|
+
webgl2: {
|
|
73
|
+
fragment: `#version 300 es
|
|
74
|
+
precision mediump float;
|
|
75
|
+
in vec2 vTextureCoord;
|
|
76
|
+
uniform sampler2D uSampler;
|
|
77
|
+
uniform float threshold;
|
|
78
|
+
out vec4 result;
|
|
79
|
+
void main() {
|
|
80
|
+
result = texture(uSampler, vTextureCoord)
|
|
81
|
+
* step(threshold, vTextureCoord.x);
|
|
82
|
+
}`,
|
|
83
|
+
},
|
|
84
|
+
uniforms: { threshold: 0 },
|
|
85
|
+
padding: 0,
|
|
86
|
+
});
|
|
87
|
+
actor.filters = [dissolve];
|
|
88
|
+
dissolve.setUniform('threshold', 0.5);
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The existing `new CustomFilter(vertex, fragment, uniforms)` constructor submits
|
|
92
|
+
that exact source pair on either backend; the caller is responsible for source
|
|
93
|
+
compatibility. An empty vertex source selects the backend's standard vertex.
|
|
94
|
+
The source-pair key distinguishes vertex/fragment boundaries. Compiled programs
|
|
95
|
+
are cached by context and actual source, so uniforms are never part of the key.
|
|
96
|
+
Do not generate new shader text every frame; update uniforms instead.
|
|
97
|
+
|
|
98
|
+
Supported uniforms: `float`, `int`, `bool`, floating/integer/boolean vectors,
|
|
99
|
+
`mat2`, `mat3`, `mat4`, and their numeric arrays. Values are numbers, booleans,
|
|
100
|
+
numeric arrays, `Float32Array`, or `Int32Array`. Matrices use column-major order.
|
|
101
|
+
Active uniforms must be supplied with the correct component count; missing,
|
|
102
|
+
non-finite, or incompatible values throw before drawing. Unsigned uniforms,
|
|
103
|
+
non-square matrices, sampler arrays and uniform blocks are not part of this API.
|
|
104
|
+
|
|
105
|
+
`setUniform()` / `setTexture()` invalidate attached objects. After direct edits
|
|
106
|
+
to an array, `filter.uniforms`, or binding descriptor, call `filter.invalidate()`.
|
|
107
|
+
Built-in effect setters invalidate their users as well.
|
|
108
|
+
|
|
109
|
+
### Coordinates and automatic uniforms
|
|
110
|
+
|
|
111
|
+
The default vertex accepts `aVertexPosition` (vec2), `aTextureCoord` (vec2), and
|
|
112
|
+
`aColor` (vec4), with `projectionVector` (vec2). Custom vertex programs must use
|
|
113
|
+
this attribute layout. Vertex displacement needs sufficient `padding` and does
|
|
114
|
+
not change scene geometry, layout or hit testing.
|
|
115
|
+
|
|
116
|
+
`vTextureCoord` uses **bottom-left framebuffer UVs**: the top is y=1. It spans
|
|
117
|
+
the entire padded input, not just the unpadded object. Input/output colors are
|
|
118
|
+
premultiplied RGBA. `vColor` is neutral in custom intermediate passes because
|
|
119
|
+
subtree vertices have already received world alpha/tint. For effects that need
|
|
120
|
+
straight RGB, divide by alpha only when alpha is positive, then premultiply the
|
|
121
|
+
output again.
|
|
122
|
+
|
|
123
|
+
| Reserved uniform | GLSL type | Value |
|
|
124
|
+
| --- | --- | --- |
|
|
125
|
+
| `uSampler` | sampler2D | Previous pass image, texture unit 0 |
|
|
126
|
+
| `projectionVector` | vec2 | Quad projection, managed by the renderer |
|
|
127
|
+
| `uTextureSize` | vec2 | Input width/height in physical pixels |
|
|
128
|
+
| `uInputSize` | vec4 | Input width, height, inverse width, inverse height |
|
|
129
|
+
| `uInputClamp` | vec4 | Input texel-center UV min/max |
|
|
130
|
+
| `uOutputSize` | vec4 | Output width, height, inverse width, inverse height |
|
|
131
|
+
| `uResolution` | float | Requested output pixels per logical unit for this pass |
|
|
132
|
+
|
|
133
|
+
Only declare the uniforms needed by the shader. They cannot be overridden in
|
|
134
|
+
`uniforms` or auxiliary bindings. Small textures may round to one physical pixel.
|
|
135
|
+
Use actual `uInputSize` / `uOutputSize` when exact texel spacing matters.
|
|
136
|
+
|
|
137
|
+
UV distortion should clamp to `uInputClamp`. A sample outside [0,1] otherwise
|
|
138
|
+
uses clamp-to-edge sampling, not automatic transparent border behavior.
|
|
139
|
+
|
|
140
|
+
## Auxiliary images
|
|
141
|
+
|
|
142
|
+
`textures` maps sampler names to full-image `BitmapData` resources. The caller
|
|
143
|
+
retains ownership. Cropped/rotated atlas `Texture` objects are not accepted.
|
|
144
|
+
Auxiliary uploads have their own nearest/linear sampling state, so they do not
|
|
145
|
+
change the same image's sprite sampling.
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
filter.setTexture('uMap', { source: mapBitmapData, smoothing: false });
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Declare `uniform sampler2D uMap`. Optionally declare `uMapMatrix` (mat3) and
|
|
152
|
+
`uMapClamp` (vec4). The default matrix converts bottom-left filter UV to top-left
|
|
153
|
+
image UV. For example, in GLSL 1.00:
|
|
154
|
+
|
|
155
|
+
```glsl
|
|
156
|
+
vec2 mapUV = (uMapMatrix * vec3(vTextureCoord, 1.0)).xy;
|
|
157
|
+
vec2 displacement = texture2D(uMap, clamp(mapUV, uMapClamp.xy, uMapClamp.zw)).rg * 2.0 - 1.0;
|
|
158
|
+
vec2 uv = vTextureCoord + displacement * amplitude / uTextureSize;
|
|
159
|
+
gl_FragColor = texture2D(uSampler, clamp(uv, uInputClamp.xy, uInputClamp.zw));
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
This maps the image across the padded input. `transform: [a,b,c,d,tx,ty]` can
|
|
163
|
+
replace the affine mapping; it is expressed directly in normalized UV units.
|
|
164
|
+
The default is `[1,0,0,-1,0,1]`.
|
|
165
|
+
|
|
166
|
+
After editing canvas/video content, call `BitmapData.invalidate(source)` so the
|
|
167
|
+
next pass uploads the new pixels. Call `filter.invalidate()` when application
|
|
168
|
+
scheduling also needs an explicit invalidation. Disposed or unavailable sources
|
|
169
|
+
throw instead of sampling stale pixels. Texture units are checked against the
|
|
170
|
+
context's fragment-stage limit, including the primary input.
|
|
171
|
+
|
|
172
|
+
## Multi-pass effects
|
|
173
|
+
|
|
174
|
+
`MultiPassFilter` accepts 1–32 immutable pass descriptions. A pass reads the
|
|
175
|
+
previous output by default. `input: 'original'` reads the input to this effect;
|
|
176
|
+
`input: 0` reads the first pass output. Additional `textures` references use the
|
|
177
|
+
same notation and are supported by CustomFilter passes only. Forward references,
|
|
178
|
+
cycles and nested MultiPassFilter instances are rejected.
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
import { MultiPassFilter } from '@kurot/core';
|
|
182
|
+
|
|
183
|
+
const effect = new MultiPassFilter([
|
|
184
|
+
{ filter: extractHighlights, scale: 0.5 },
|
|
185
|
+
{ filter: blurHighlights, scale: 0.5 },
|
|
186
|
+
{ filter: combine, textures: { uOriginal: 'original' } },
|
|
187
|
+
]);
|
|
188
|
+
world.filters = [effect];
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Here `combine` declares `uniform sampler2D uOriginal`. Borrowed framebuffer
|
|
192
|
+
inputs share the primary input's bottom-left UV convention; their automatic
|
|
193
|
+
`<name>Matrix` is identity. Primary and additional inputs may differ in pixel
|
|
194
|
+
size but cover the same logical rectangle. `scale` is relative to the enclosing
|
|
195
|
+
effect resolution, not the previous pass. Per-pass filter `resolution` can
|
|
196
|
+
further reduce its output resolution. Update child filter uniforms to animate;
|
|
197
|
+
replace the MultiPassFilter to change its pass graph.
|
|
198
|
+
|
|
199
|
+
GPU targets are never exposed to application code. The renderer preserves inputs
|
|
200
|
+
until their consumers finish, prohibits framebuffer feedback and recycles owned
|
|
201
|
+
targets even after errors. The idle intermediate pool is limited to 16 targets
|
|
202
|
+
and 64 MiB. Active pass images, subtree capture buffers and auxiliary images are
|
|
203
|
+
not included in that idle limit. Player's framebuffer pool counters report this
|
|
204
|
+
intermediate pool. `Player.destroy()` releases idle effect targets and auxiliary
|
|
205
|
+
uploads; caller image objects remain intact. Context restoration recreates
|
|
206
|
+
programs, intermediate targets and auxiliary uploads.
|
|
207
|
+
|
|
208
|
+
## Backend and caching boundaries
|
|
209
|
+
|
|
210
|
+
CustomFilter, MultiPassFilter and BloomFilter are GPU effects. Canvas 2D skips
|
|
211
|
+
them; it continues to approximate supported built-in effects through its existing
|
|
212
|
+
CSS/pixel paths. Its filter combinations and Blur quality are not pixel-equivalent
|
|
213
|
+
to WebGL.
|
|
214
|
+
|
|
215
|
+
`cacheAsTexture` and `RenderTexture` still rasterize through Canvas 2D. Do not use
|
|
216
|
+
them to capture or cache these GPU effects. For post-processing use filter chains
|
|
217
|
+
or MultiPassFilter, which retain GPU images.
|
|
218
|
+
|
|
219
|
+
## Validation
|
|
220
|
+
|
|
221
|
+
Run from the repository root:
|
|
222
|
+
|
|
223
|
+
```sh
|
|
224
|
+
pnpm --dir packages/core build
|
|
225
|
+
pnpm --dir packages/core test
|
|
226
|
+
pnpm --dir packages/core test:visual
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
The browser suite includes deterministic scene screenshots and explicit pixel
|
|
230
|
+
assertions on both WebGL versions, resource restoration, error recovery, filter
|
|
231
|
+
ordering, masks, high-resolution inputs, large-radius blur and Bloom. It is a
|
|
232
|
+
Chromium baseline, not a guarantee for every physical GPU or mobile browser.
|
package/docs/resource.md
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
# 资源管理器 (Resource Manager)
|
|
2
|
+
|
|
3
|
+
## 概述
|
|
4
|
+
|
|
5
|
+
Kurot 资源管理器是从 Egret RES 模块现代化迁移而来的异步资源加载与缓存系统。它提供:
|
|
6
|
+
|
|
7
|
+
- **async/await API** — 所有加载操作返回 Promise
|
|
8
|
+
- **类型安全** — `ResourceType` 枚举 + 泛型 `get<T>()`
|
|
9
|
+
- **配置驱动** — 兼容 Egret `resource.json` 格式
|
|
10
|
+
- **按组加载** — 并发控制(默认 2 线程)、自动重试(默认 3 次)
|
|
11
|
+
- **可扩展** — 自定义 Analyzer 支持新资源类型
|
|
12
|
+
- **零外部依赖** — 仅依赖 core 内部的 `net/` 和 `events/`
|
|
13
|
+
|
|
14
|
+
## 目录结构
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
packages/core/src/kurot/resource/
|
|
18
|
+
├── index.ts — 统一导出
|
|
19
|
+
├── Resource.ts — 资源管理器主类(单例 + 便捷实例)
|
|
20
|
+
├── ResourceConfig.ts — 配置文件解析
|
|
21
|
+
├── ResourceLoader.ts — 队列加载器(并发 + 重试)
|
|
22
|
+
├── ResourceItem.ts — 资源项定义 + ResourceType 枚举
|
|
23
|
+
├── ResourceEvent.ts — 事件类型定义
|
|
24
|
+
└── analyzers/
|
|
25
|
+
├── index.ts
|
|
26
|
+
├── AnalyzerBase.ts — 解析器抽象基类
|
|
27
|
+
├── ImageAnalyzer.ts — 图片 → Texture
|
|
28
|
+
├── JsonAnalyzer.ts — JSON 文件 → object
|
|
29
|
+
├── TextAnalyzer.ts — 纯文本 → string
|
|
30
|
+
├── SoundAnalyzer.ts — 音频 → HTMLAudioElement
|
|
31
|
+
└── SheetAnalyzer.ts — 精灵表 JSON + 图片 → SpriteSheet
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## 快速开始
|
|
35
|
+
|
|
36
|
+
### 1. 准备配置文件
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
// resource.json
|
|
40
|
+
{
|
|
41
|
+
"resources": [
|
|
42
|
+
{ "name": "bg", "type": "image", "url": "assets/bg.png" },
|
|
43
|
+
{ "name": "config", "type": "json", "url": "config.json" },
|
|
44
|
+
{ "name": "hero", "type": "sheet", "url": "hero.json" }
|
|
45
|
+
],
|
|
46
|
+
"groups": [
|
|
47
|
+
{ "name": "preload", "keys": "bg,config" },
|
|
48
|
+
{ "name": "game", "keys": "hero" }
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### 2. 加载与使用
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
import { resource, ResourceType } from '@kurot/core';
|
|
57
|
+
|
|
58
|
+
// 加载配置
|
|
59
|
+
await resource.loadConfig('resource.json', 'assets/');
|
|
60
|
+
|
|
61
|
+
// 按组加载
|
|
62
|
+
await resource.loadGroup('preload');
|
|
63
|
+
|
|
64
|
+
// 获取缓存的资源(泛型)
|
|
65
|
+
const texture = resource.get<Texture>('bg');
|
|
66
|
+
|
|
67
|
+
// 异步加载单个资源
|
|
68
|
+
const data = await resource.load<object>('config');
|
|
69
|
+
|
|
70
|
+
// 监听加载进度
|
|
71
|
+
await resource.loadGroup('game', 0, (loaded, total) => {
|
|
72
|
+
console.log(`进度: ${loaded}/${total}`);
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## API 参考
|
|
77
|
+
|
|
78
|
+
### Resource 主类
|
|
79
|
+
|
|
80
|
+
#### 配置
|
|
81
|
+
|
|
82
|
+
| 方法 | 说明 |
|
|
83
|
+
| -------------------------- | -------------------------------------------------- |
|
|
84
|
+
| `loadConfig(url, folder?)` | 加载并解析 resource.json,`folder` 为资源 URL 前缀 |
|
|
85
|
+
| `addResource(def)` | 动态添加资源定义 `{ name, url, type }` |
|
|
86
|
+
|
|
87
|
+
#### 加载
|
|
88
|
+
|
|
89
|
+
| 方法 | 说明 |
|
|
90
|
+
| ----------------------------------------- | -------------------------------------------- |
|
|
91
|
+
| `loadGroup(name, priority?, onProgress?)` | 按组加载,返回 Promise。已加载的资源自动跳过 |
|
|
92
|
+
| `load<T>(name)` | 异步加载单个资源,返回缓存数据 |
|
|
93
|
+
|
|
94
|
+
#### 缓存
|
|
95
|
+
|
|
96
|
+
| 方法 | 说明 |
|
|
97
|
+
| ----------------- | ---------------------------------------- |
|
|
98
|
+
| `get<T>(name)` | 同步获取缓存资源,未加载返回 `undefined` |
|
|
99
|
+
| `hasRes(name)` | 检查资源是否已缓存 |
|
|
100
|
+
| `hasGroup(name)` | 检查组是否存在 |
|
|
101
|
+
| `getGroupNames()` | 获取所有组名 |
|
|
102
|
+
|
|
103
|
+
#### 销毁
|
|
104
|
+
|
|
105
|
+
| 方法 | 说明 |
|
|
106
|
+
| -------------------- | ---------------------- |
|
|
107
|
+
| `destroy(name)` | 销毁单个资源,释放缓存 |
|
|
108
|
+
| `destroyGroup(name)` | 销毁整组资源 |
|
|
109
|
+
| `destroyAll()` | 销毁所有已加载资源 |
|
|
110
|
+
|
|
111
|
+
#### 事件
|
|
112
|
+
|
|
113
|
+
| 方法 | 说明 |
|
|
114
|
+
| ---------------------- | -------------------------------------- |
|
|
115
|
+
| `on(type, listener)` | 监听资源事件 |
|
|
116
|
+
| `off(type, listener)` | 移除事件监听 |
|
|
117
|
+
| `onProgress(callback)` | 便捷进度监听 `(loaded, total) => void` |
|
|
118
|
+
|
|
119
|
+
#### 扩展
|
|
120
|
+
|
|
121
|
+
| 方法 | 说明 |
|
|
122
|
+
| ---------------------------------- | ------------------------ |
|
|
123
|
+
| `registerAnalyzer(type, analyzer)` | 注册自定义资源类型解析器 |
|
|
124
|
+
|
|
125
|
+
### ResourceType 枚举
|
|
126
|
+
|
|
127
|
+
| 值 | 说明 |
|
|
128
|
+
| --------- | --------------------------------- |
|
|
129
|
+
| `'image'` | 图片资源,生成 `Texture` |
|
|
130
|
+
| `'json'` | JSON 文件,解析为 `object` |
|
|
131
|
+
| `'text'` | 纯文本文件,返回 `string` |
|
|
132
|
+
| `'sound'` | 音频文件,返回 `HTMLAudioElement` |
|
|
133
|
+
| `'sheet'` | 精灵表,返回 `SpriteSheet` |
|
|
134
|
+
|
|
135
|
+
### ResourceEventType 事件类型
|
|
136
|
+
|
|
137
|
+
| 事件 | 触发时机 |
|
|
138
|
+
| ------------------- | ------------------------ |
|
|
139
|
+
| `CONFIG_COMPLETE` | 配置文件加载完成 |
|
|
140
|
+
| `CONFIG_LOAD_ERROR` | 配置文件加载失败 |
|
|
141
|
+
| `GROUP_COMPLETE` | 资源组全部加载完成 |
|
|
142
|
+
| `GROUP_PROGRESS` | 资源组中单个资源加载完成 |
|
|
143
|
+
| `GROUP_LOAD_ERROR` | 资源组中有资源加载失败 |
|
|
144
|
+
| `ITEM_LOAD_ERROR` | 单个资源加载失败 |
|
|
145
|
+
|
|
146
|
+
### ResourceEvent 接口
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
interface ResourceEvent {
|
|
150
|
+
type: ResourceEventType;
|
|
151
|
+
groupName: string;
|
|
152
|
+
item?: ResourceItem;
|
|
153
|
+
itemsLoaded: number;
|
|
154
|
+
itemsTotal: number;
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## 内置解析器
|
|
159
|
+
|
|
160
|
+
### ImageAnalyzer
|
|
161
|
+
|
|
162
|
+
加载图片并创建 `Texture` 对象。销毁时自动调用 `texture.dispose()`。
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
const texture = resource.get<Texture>('bg');
|
|
166
|
+
// texture 可直接用于 Bitmap 显示对象
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### JsonAnalyzer
|
|
170
|
+
|
|
171
|
+
通过 `HttpRequest` 加载 JSON 文件并解析为对象。
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
const config = resource.get<Record<string, unknown>>('config');
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### TextAnalyzer
|
|
178
|
+
|
|
179
|
+
通过 `HttpRequest` 加载纯文本文件。
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
const text = resource.get<string>('readme');
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### SoundAnalyzer
|
|
186
|
+
|
|
187
|
+
加载音频文件,返回 `HTMLAudioElement`。销毁时自动 `pause()` 并释放 `src`。
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
const audio = resource.get<HTMLAudioElement>('bgm');
|
|
191
|
+
audio.play();
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### SheetAnalyzer
|
|
195
|
+
|
|
196
|
+
加载精灵表(TexturePacker JSON 格式),两步加载:
|
|
197
|
+
|
|
198
|
+
1. 加载 JSON 配置获取帧信息和图片路径
|
|
199
|
+
2. 加载关联图片,切割为子纹理
|
|
200
|
+
|
|
201
|
+
支持三种获取方式:
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
// 获取整个 SpriteSheet
|
|
205
|
+
const sheet = resource.get<SpriteSheet>('hero');
|
|
206
|
+
|
|
207
|
+
// 通过子键名直接获取子纹理
|
|
208
|
+
const frame = resource.get<Texture>('hero_idle_01');
|
|
209
|
+
|
|
210
|
+
// 通过点号语法获取
|
|
211
|
+
const frame2 = resource.get<Texture>('hero.idle_01');
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## 自定义解析器
|
|
215
|
+
|
|
216
|
+
继承 `AnalyzerBase` 实现自定义资源类型:
|
|
217
|
+
|
|
218
|
+
```typescript
|
|
219
|
+
import { AnalyzerBase } from '@kurot/core';
|
|
220
|
+
import { ResourceItem } from '@kurot/core';
|
|
221
|
+
|
|
222
|
+
class XmlAnalyzer extends AnalyzerBase {
|
|
223
|
+
public loadFile(item: ResourceItem): Promise<ResourceItem> {
|
|
224
|
+
if (this.fileDic.has(item.name)) {
|
|
225
|
+
item.loaded = true;
|
|
226
|
+
return Promise.resolve(item);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return fetch(item.url)
|
|
230
|
+
.then(res => res.text())
|
|
231
|
+
.then(text => {
|
|
232
|
+
const parser = new DOMParser();
|
|
233
|
+
const doc = parser.parseFromString(text, 'text/xml');
|
|
234
|
+
this.fileDic.set(item.name, doc);
|
|
235
|
+
item.loaded = true;
|
|
236
|
+
return item;
|
|
237
|
+
})
|
|
238
|
+
.catch(() => {
|
|
239
|
+
item.loaded = false;
|
|
240
|
+
return item;
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// 注册
|
|
246
|
+
resource.registerAnalyzer('xml', new XmlAnalyzer());
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## 队列加载器 (ResourceLoader)
|
|
250
|
+
|
|
251
|
+
`ResourceLoader` 是内部使用的队列加载器,支持:
|
|
252
|
+
|
|
253
|
+
- **并发控制** — `threadCount`(默认 2)限制同时加载数
|
|
254
|
+
- **自动重试** — `retryCount`(默认 3)次重试后报告失败
|
|
255
|
+
- **回调通知** — `onComplete` / `onError` / `onProgress`
|
|
256
|
+
|
|
257
|
+
通常不需要直接使用 `ResourceLoader`,通过 `Resource.loadGroup()` 即可间接使用。
|
|
258
|
+
|
|
259
|
+
## 配置文件格式
|
|
260
|
+
|
|
261
|
+
### 标准格式(2.0.0)
|
|
262
|
+
|
|
263
|
+
```json
|
|
264
|
+
{
|
|
265
|
+
"resources": [
|
|
266
|
+
{
|
|
267
|
+
"name": "资源唯一标识",
|
|
268
|
+
"type": "image|json|text|sound|sheet",
|
|
269
|
+
"url": "相对或绝对路径",
|
|
270
|
+
"subkeys": { "frameName": { "scale9grid": "1,1,2,2" }, "ordinaryFrame": {} }
|
|
271
|
+
}
|
|
272
|
+
],
|
|
273
|
+
"groups": [
|
|
274
|
+
{
|
|
275
|
+
"name": "组名",
|
|
276
|
+
"keys": "逗号分隔的资源名称"
|
|
277
|
+
}
|
|
278
|
+
]
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
当前源码的 `subkeys` 为对象,旧字符串需要在 Editor 中显式刷新图集后再使用。
|
|
283
|
+
`scale9grid` 是 Image 编译默认值,Core 不会自动把它应用到任意 Bitmap。
|
|
284
|
+
参见 [资源默认九宫格契约](../../ui-document/docs/resource-nine-slice.md)。
|
|
285
|
+
|
|
286
|
+
### URL 解析规则
|
|
287
|
+
|
|
288
|
+
- 包含 `://` 的 URL 视为绝对路径,不做处理
|
|
289
|
+
- 其他 URL 会在前面拼接 `loadConfig()` 的 `folder` 参数
|
|
290
|
+
|
|
291
|
+
### 动态创建组
|
|
292
|
+
|
|
293
|
+
```typescript
|
|
294
|
+
import { resource, ResourceConfig } from '@kurot/core';
|
|
295
|
+
|
|
296
|
+
// 通过 ResourceConfig 的 createGroup 方法
|
|
297
|
+
// 注意:这是底层 API,通常通过配置文件定义组
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
## 与 Egret RES 的对比
|
|
301
|
+
|
|
302
|
+
| 维度 | Egret RES | Kurot Resource |
|
|
303
|
+
| ---------- | ----------------------- | ----------------------------------- |
|
|
304
|
+
| API 风格 | 回调 `compFunc(data)` | `async/await` + Promise |
|
|
305
|
+
| 模块化 | 全局 `RES.xxx` | ES Module 导入 |
|
|
306
|
+
| 事件系统 | `egret.EventDispatcher` | 轻量 `on/off` 模式 |
|
|
307
|
+
| 类型安全 | 大量 `any` | 泛型 `get<T>()` |
|
|
308
|
+
| 版本控制 | `VersionController` | 剔除(构建工具处理) |
|
|
309
|
+
| XML 解析 | 内置 | 剔除(KUI XML compiler 处理) |
|
|
310
|
+
| 国际化 | 内置 | 剔除(不属于资源加载器职责) |
|
|
311
|
+
| 解析器类型 | 8 种 | 5 种(Image/Json/Text/Sound/Sheet) |
|
|
312
|
+
| 代码量 | ~3,000 行 | ~1,400 行 |
|
|
313
|
+
|
|
314
|
+
### 剔除的功能
|
|
315
|
+
|
|
316
|
+
以下旧版功能被有意剔除:
|
|
317
|
+
|
|
318
|
+
- **VersionController** — CDN 版本控制由现代构建工具(Vite/Webpack)处理
|
|
319
|
+
- **i18n 国际化** — 不属于资源加载器的职责
|
|
320
|
+
- **XML/Bin 分析器** — KUI XML 由 compiler 预处理,Binary 格式初期不需要
|
|
321
|
+
- **AnimationAnalyzer** — 初期不需要,后续按需添加
|
|
322
|
+
- **FontAnalyzer** — 初期不需要,后续按需添加
|
|
323
|
+
- **同步 getRes() 的复杂 sheet 子键查找** — 简化为点号语法
|
|
324
|
+
|
|
325
|
+
## 注意事项
|
|
326
|
+
|
|
327
|
+
1. **并发 `loadGroup` 调用会排队串行执行** — 内部共享一个 `ResourceLoader` 实例,同时调用多个 `loadGroup`(同组或不同组)不会互相覆盖回调或丢失进度;每次调用仍各自独立 resolve/reject,只是底层批次按调用顺序依次执行,不会真正并行加载。
|
|
328
|
+
|
|
329
|
+
2. **`onProgress` 监听器不会自动移除** — 每次 `onProgress()` 调用会添加一个永久监听器。如需一次性监听,请使用 `loadGroup` 的 `onProgress` 参数。
|
|
330
|
+
|
|
331
|
+
3. **Sheet 子键冲突** — 不同精灵表的子纹理名称不应重复,后加载的不会覆盖先加载的同名子键。
|
|
332
|
+
|
|
333
|
+
4. **资源 URL 需要同源或 CORS** — 图片和音频加载受浏览器同源策略限制。
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Text layout
|
|
2
|
+
|
|
3
|
+
`TextField.wordWrap = true` uses Unicode 17.0 UAX #14 line-break opportunities
|
|
4
|
+
from `@cto.af/linebreak`. Available width determines which opportunity is used.
|
|
5
|
+
Font family, size, bold and italic are measured through the same Canvas font
|
|
6
|
+
configuration used for rendering. Rich-text style boundaries do not create
|
|
7
|
+
additional wrap opportunities.
|
|
8
|
+
|
|
9
|
+
- Latin and Cyrillic text prefer word boundaries. CJK text observes opening
|
|
10
|
+
and closing punctuation rules rather than treating dictionary words as units.
|
|
11
|
+
- Thai, Lao and Khmer add dictionary boundaries through the browser's
|
|
12
|
+
`Intl.Segmenter`. The default Unicode rules remain independently testable.
|
|
13
|
+
- When an alphabetic word is wider than the entire line, emergency wrapping
|
|
14
|
+
relaxes UAX #14's alphabetic rule LB28. Non-breaking spaces, narrow
|
|
15
|
+
non-breaking spaces, word joiners and punctuation protection remain in force.
|
|
16
|
+
Protected sequences can therefore exceed the field width.
|
|
17
|
+
- Ordinary spaces and tabs at a wrapped line's end are omitted from drawing
|
|
18
|
+
and width measurement. They are consumed on that line, so they never become
|
|
19
|
+
indentation on the next automatic line. Explicit indentation after a hard
|
|
20
|
+
separator is retained. Other space characters keep their Unicode behavior.
|
|
21
|
+
- The source string is unchanged. Line `charNum` counts consumed UTF-16 code
|
|
22
|
+
units, including unpainted spaces and both characters of CRLF. Painting,
|
|
23
|
+
input caret and hit testing use these source offsets.
|
|
24
|
+
- CRLF, CR, LF, VT, FF, NEL, line separator and paragraph separator force line
|
|
25
|
+
breaks. A trailing hard separator retains a final empty line.
|
|
26
|
+
|
|
27
|
+
`wordWrap = false` retains character-based wrapping. Single-line INPUT fields
|
|
28
|
+
do not wrap to available width; multiline INPUT and dynamic text do. An
|
|
29
|
+
unconstrained field measures its hard-separated lines without automatic wrapping.
|
|
30
|
+
|
|
31
|
+
`WordWrap.tokenize()` remains a public word-segmentation utility; it is not the
|
|
32
|
+
TextField line-break algorithm. `splitGraphemes()` is likewise independent.
|
|
33
|
+
|
|
34
|
+
## Validation
|
|
35
|
+
|
|
36
|
+
`test/LineBreaks.test.ts` verifies all 19,338 official Unicode 17.0 line-break
|
|
37
|
+
cases against the untailored profile. The compressed fixture includes its
|
|
38
|
+
source URL, SHA-256 and Unicode license. Dictionary and emergency tailoring,
|
|
39
|
+
multilingual width fitting, rich text, input offsets and rendering are tested
|
|
40
|
+
separately in `test/TextWrapping.test.ts`.
|
|
41
|
+
|
|
42
|
+
The dependency bundles its Unicode property tables and uses no network or Node
|
|
43
|
+
runtime APIs. Both WebGL text rasterization and Canvas rendering share this
|
|
44
|
+
layout. Core 2.0.1 is sufficient for existing UI 3.x / ui-runtime 0.7 consumers;
|
|
45
|
+
no CLI XML-format change is involved.
|
|
46
|
+
|
|
47
|
+
## Consumer adoption for 2.0.1
|
|
48
|
+
|
|
49
|
+
UI 3.0.0, game 2.0.0 and ui-runtime 0.7.0 already declare Core `^2.0.0` in their
|
|
50
|
+
peer/development ranges. Repository examples and `kurot-project-kui` use the
|
|
51
|
+
same compatible range; their lockfiles need an update to pick up this fix.
|
|
52
|
+
CLI templates use `latest` and do not need a range change. Editor currently
|
|
53
|
+
pins the published Core 2.0.0 and builds its 0.4.6 trial with local 2.0.1 through
|
|
54
|
+
`KUROT_CORE_PATH`; update that pin and lockfile after Core 2.0.1 is published.
|
|
55
|
+
No dependant package is automatically bumped by this Core release preparation.
|
|
56
|
+
|
|
57
|
+
References: [UAX #14 revision 55](https://www.unicode.org/reports/tr14/tr14-55.html),
|
|
58
|
+
[official corpus](https://www.unicode.org/Public/17.0.0/ucd/auxiliary/LineBreakTest.txt),
|
|
59
|
+
[@cto.af/linebreak](https://github.com/cto-af/linebreak).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kurot/core",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "Modern rewrite of the Egret game engine — WebGL multi-texture batching, instruction-driven render pipeline, strict TypeScript, Egret-compatible API",
|
|
5
5
|
"author": "Kelvin <kyia.x52@gmail.com>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
},
|
|
17
17
|
"files": [
|
|
18
18
|
"dist/**/*",
|
|
19
|
+
"docs/**/*",
|
|
19
20
|
"CHANGELOG.md",
|
|
20
21
|
"LICENSE"
|
|
21
22
|
],
|
|
@@ -65,5 +66,8 @@
|
|
|
65
66
|
"packageManager": "pnpm@10.33.0",
|
|
66
67
|
"publishConfig": {
|
|
67
68
|
"access": "public"
|
|
69
|
+
},
|
|
70
|
+
"dependencies": {
|
|
71
|
+
"@cto.af/linebreak": "4.0.3"
|
|
68
72
|
}
|
|
69
73
|
}
|