Files
endorphin-tessera-gui/docs/illustrated-manuscript.md
T

112 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Endor 脚本:龙与文字实时绕排
完整程序:[examples/illustrated-manuscript.endor](../examples/illustrated-manuscript.endor)。它通过普通 Page、源 measure/paint、托管事件及 owner-scoped Task 工作。Tessera 没有 Dragon / Flame / Manuscript 专用 Rust 类型、专用启动参数或动画循环;Rust 只补齐可被其他脚本复用的图形与文字能力。
## 运行
在仓库根目录启动普通应用:
```sh
cargo run --release --locked
```
另开终端,在同一个仓库根目录提交原始 Endor 文件:
```sh
curl --request PUT --header 'Content-Type: text/plain; charset=utf-8' \
--data-binary @examples/illustrated-manuscript.endor \
http://127.0.0.1:4737/v1/namespaces/app.main/code
```
这是普通源码提交,没有预编译场景或原生替身。资源路径目前相对于应用工作目录;通过以上根目录启动方式使用 `examples/assets/manuscript/`。从其他目录启动时,修改脚本的 `assets` 路径为绝对路径。图片、字体、正文都在示例目录,不编入宿主。素材与字体许可见 [NOTICE](../examples/assets/manuscript/NOTICE.md)。
移动鼠标让龙跟随,按住左键喷火,松开停止。离开窗口、取消指针或失焦也会停止喷火。空闲两秒回到卷曲姿势。原生窗口缩放触发普通布局和 paint;窄窗口切换到脚本预备的小字号。关闭 Page 会取消其定时任务,不留下应用级动画循环。
## 脚本对象分工
| 对象 | 状态与责任 |
| --- | --- |
| `Segment` | 一节身体的位置、角度及共享图片引用 |
| `FireArt` | 一张火焰图及其原始轴角;这些校准值是示例素材数据 |
| `Flame` | 一个粒子的位置、速度、尺寸、年龄及 FireArt 引用 |
| `Dragon` | 身体链、火焰集合、目标点、输入状态、时钟样本和随机种子;实现追随、恢复、生成/更新/删除粒子及绘图描述 |
| `Manuscript : RectBlock` | 普通视觉子类;组合预处理文字、首字母图片、龙和 Page,覆盖 measure/paint,通过 onMount/onUnmount 管理 Task |
| `Page` / `Task` / `Event` / `Mount` | 沿用 Tessera 已有托管体系:Page 是任务 owner,Task 调用脚本 tick,Event 带局部指针位置,Mount 提供当前出现的几何 |
这些是同一 VM 中的普通对象与引用。身体并不是多个重量远程 actor;图片被多个出现/粒子共享,临时 PaintCommand 只描述本帧绘制。
## 从输入到一帧
1. `view.on("pointermove", dragon.pointer)` 显式订阅连续输入。宿主把原生指针转换成 Event,再交给普通传播链;脚本读取 `event.mount.bounds()`,将目标点换算到自己的 700 单位画布。按下/松开只改变脚本 `pressed`。
2. `onMount` 用 `ui.after(page,"animation",80,self.tick)` 创建 Page 所有的任务。`tick` 调用脚本 `Dragon.step()`,再安排下一个任务。80 ms 是脚本参数;宿主不知道这是龙的动画。
3. `step` 先采样通用单调时钟。头向目标移动,其余 19 节依次跟随上一节,保持间距 30,并把相邻转角差限制在 ±0.25 弧度。空闲时由脚本计算卷曲目标并插值回去。
4. 粒子在脚本中更新位置、速度与年龄,倒序删除超过 12 步的粒子。每步最多产生 1 个、总数最多 12 个,最长边仅 12–20 个画布单位。随机变化由脚本自己的确定性种子和数学函数产生。
5. 普通属性变更使源 paint 的已观察依赖失效。`paint` 从当前几何生成图像绘制命令,再由脚本选择哪些图片形成避让区,交给通用文字流布局。
6. 正常文字使用缓存整词形状;喷火时脚本请求字素粒度,并在 `heat` 中按粒子距离修改临时文字命令的位置、角度、透明度和颜色。没有把热力规则藏进宿主。
7. 返回的 DisplayList 经过现有只读 paint 校验、Frame 发布和 CPU 栅格化,在普通窗口中呈现。VM 对象不进入窗口线程。
paint 内创建/修改的是临时返回对象,不能写已有龙状态或读取文件/时钟。资源加载、时钟采样和状态更新发生在初始化、事件或任务中。
## 绕排与预处理
脚本初始化时用 `prepareText` 准备两套正文和提示文字。字体/字号/行高/颜色/设备分辨率属于准备参数;每帧只传新几何与避让区,复用字形形状和透明图像。
`Dragon.artwork` 返回普通图像命令。脚本循环调用通用 `imageExclusion(command,padding)`,由图片 alpha 轮廓产生保守凸包,并给首字母添加矩形避让。`flowText` 对整个行带求交、合并遮挡区间,再依次填充剩余的左右片段;同一个文字 cursor 跨片段继续,不复制或跳过词。单词装不下所有可用片段时才退回字素边界。
当前提供面向本示例的从左到右文字流,不声称完整支持所有文字系统的双向段落。字素绘制会改变部分字偶距;这是喷火效果的取舍。正文超出当前页的部分留在准备数据中,当前示例保持单页,未增加滚动文档业务。
## 火焰角度与尺寸
火焰 PNG 自身方向各不相同;把它们一律当作朝右图片会错位。脚本在 `FireArt.axis` 中记录每张图片原始轴角,绘制时使用:
```endor
image = p.art.image
imageScale = p.size * scale / math.max(image.width, image.height)
angle = math.atan2(p.vy, p.vx) - p.art.axis
drawing = sprite(image, ox + p.x * scale, oy + p.y * scale, imageScale, angle, 0.5, 0.5)
```
角度以弧度表示,在屏幕坐标中正方向为顺时针;原始素材校准值与 PNG 一起属于应用数据。尺寸按最长边归一化,窄高的 PNG 不会因宽度较小而放大。舌头同样通过局部 anchor 定位,使其偏移随头部一起旋转。
## 可复用宿主接口
入口是 `g = ui.graphics`,数学入口是 `g.math`。宿主实现位于 `src/bridge/graphics.rs`、`src/render/image.rs` 和 `src/render/text_flow.rs`。
| 方法 | 行为 |
| --- | --- |
| `loadImage(path)` | 读取有界普通 PNG 文件,返回共享不可变资源及 width/height |
| `loadFont(path,family)` | 注册开放字体,验证指定 family;相同资源复用,冲突不替换既有字体 |
| `readText(path)` / `textSlice(text,start,end)` / `string(value)` | 初始化用有界 UTF-8 资源读取、按字符切片及标量文本转换 |
| `prepareText(text,font,size,lineHeight,color,rasterScale)` | 生成与障碍无关的缓存文字资源 |
| `sprite(image,x,y,width,height,angle,anchorX,anchorY,opacity)` | 返回普通 PaintCommand;旋转围绕原点,anchor 决定图片局部偏移 |
| `imageExclusion(command,padding)` | 图像实际 alpha 轮廓的保守凸包,应用该命令的平移、缩放与旋转 |
| `circleExclusion(x,y,r)` / `rectExclusion(x,y,w,h)` | 通用几何避让区 |
| `flowText(prepared,x,y,w,h,exclusions,letterMode)` | 生成可检查/修改的普通文字图像命令列表 |
| `math.float/sin/cos/sqrt/atan2/abs/min/max/clamp/floor/now` | 通用实数与图形数学;float 是显式二进制浮点入口,now 仅在事件/任务中采样 |
| `ObjectList.append/remove/extend/added` | 通用托管集合编辑/批量追加/持久添加;paint 只能编辑本次创建的临时集合 |
PNG 单张最多 4M 像素,文件最多 4 MiB;准备文字最多 16 KiB、缓存最多 8M 像素;最多 512 个避让区、1024 行和 10000 条绘图命令。资源读取不允许在 paint/layout 中执行。图形边界显式窄化数字,源语言的精确数值表示不会被改写。
原生连续指针输入需显式订阅。生产端在有界队列中只合并相同窗口/session/目标的相邻绝对移动采样,按下/松开/取消及目标变化都是顺序边界。后台 relay 直接把内部 Input envelope 送进同一 VM worker 队列并等待完成,窗口线程不等待;不绕行 HTTP。worker 等既有刷新结束后才重验 Mount/session 并更新连续事件版本,避免动画发布新帧时丢掉移动或松开。外部 HTTP 无法创建内部 envelope,继续严格校验帧版本。
## Aster 语言扩展
为使上述逻辑留在源码中,独立 Aster 分支实现了真实四则运算、非字面量负号、六种比较、if/else 与 while,并同步 ASDL、AST 生成产物、名称分析、bytecode 验证与 VM。整数/十进制/有理数保持精确;有 BinaryFloat 操作数才选择浮点算术,混合数值比较避免大整数丢精度。循环沿用同一累计指令限制与协作暂停机制。不是在 Tessera 中解释另一个小语言。
## 验证与范围
```sh
cargo test --locked
cargo clippy --locked --all-targets -- -D warnings
cargo fmt --all -- --check
TESSERA_MANUSCRIPT_SNAPSHOTS=/absolute/output/path \
cargo test --release --locked manuscript_script_snapshots -- --nocapture
```
快照测试加载实际 Endor 文件,经 VM、事件、源 paint 和软件渲染器输出 endor-rest/crossing/fire/narrow 等 PNG。覆盖持续喷火达到上限、角度校准、最长边尺寸、字形呈现、宽窄窗口,以及任务确实调用脚本和关闭 Page 后停止。另一项集成测试验证指针驱动头部移动、取消与粒子回收。不是用 Rust 动画或手工绘图替代源码执行。
当前采用既有 CPU 后端;无 GPU 实现,也未达到稳定 60 FPS。参考本机 release 样本,16 次持续喷火的 VM 更新与完整 Frame 刷新约 1.4–1.5 秒(不含 OS 呈现),不能把 80 ms 定时参数当作实际帧率。性能结果属于脚本通路而非之前已移除的原生原型。
本次还补上了指针类型的 HTTP 白名单。最初只测 VM 分发遗漏了真实 worker 入口,随后通过真实 HTTP 回归复现并修复 `unsupported event kind`;动画期间的过期事件竞争则通过内部原生 envelope 解决。测试分别覆盖 HTTP 的坐标/类型/严格版本、原生 envelope 换帧后仍投递且旧会话不穿透,以及生产端合并不跨越按下/松开。最终运行窗口已由用户确认“持续正常,松开也正常”。系统自动事件投递/截图权限未开启,因此最终展示图来自实际脚本的 VM→Frame→软件渲染快照,原生交互验证来自用户操作。