backend/gsap 教程
本教程介绍如何把场景渲染为 SVG 并用 GSAP 控制其动画:在页面上加载 GSAP,绘制到 <svg> 中,用 GsapPlayer 驱动绘制,并接上播放、暂停、跳转和调速控件。
快速开始
该包需要 js 目标、用于 DOM 的 rabbita,以及页面上的 GSAP 3。像 Canvas 后端那样安装模块,然后声明一个可执行包:
import {
"Luna-Flow/geometry3d/core",
"Luna-Flow/geometry3d/view",
"Luna-Flow/geometry3d/frontend",
"Luna-Flow/geometry3d/backend/gsap",
"moonbit-community/rabbita/dom",
}
supported_targets = "js"
pkgtype(kind: "executable")
页面加载 GSAP,把它发布为 globalThis.gsap,然后才加载 MoonBit 的输出:
<!doctype html>
<svg id="scene" xmlns="http://www.w3.org/2000/svg"></svg>
<script type="module">
import { gsap } from "https://cdn.jsdelivr.net/npm/gsap@3.13.0/+esm";
globalThis.gsap = gsap;
await import("./main.js");
</script>
程序每八秒让圆环旋转一圈,永不停止:
fn main {
let svg = @dom.document().get_element_by_id("scene").to_option().unwrap()
let config = @gsap.GsapSvgRenderConfig::sized(640, 480)
let view = @frontend.RenderView::perspective(
@view.Camera3::default(6.0),
@view.PerspectiveProjection::new(@view.Viewport::new(640, 480), 380.0),
)
let player = @gsap.GsapPlayer::new(8.0, fn(t) {
let angle = t / 8.0 * 2.0 * @math.PI
let scene = @frontend.Scene::single(
@core.torus_mesh(1.7, 0.58, 32, 18),
@core.Transform3::rotation(0.7 * angle, angle, 0.25 * angle),
@frontend.Light::default(),
)
@gsap.render_scene(svg, scene, view, config)
})
player.play()
}
播放器在每个动画帧以时间轴时间调用该函数,后端原地更新多边形。由于每帧只是 t 的函数,动画在 处无缝循环。
常见任务
接上播放控件
每个控件对应一个方法。把播放器保存在一个变量中,并在事件处理器中调用它(这里使用 rabbita 的 add_event_listener):
fn bind_button(id : String, action : () -> Unit) -> Unit {
let element = @dom.document().get_element_by_id(id).to_option().unwrap()
element.add_event_listener("click", fn(_) { action() })
}
fn wire(player : @gsap.GsapPlayer) -> Unit {
bind_button("toggle", fn() {
if player.is_paused() { player.play() } else { player.pause() }
})
bind_button("reverse", fn() { player.reverse() })
bind_button("restart", fn() { player.restart() })
bind_button("half-speed", fn() { player.set_time_scale(0.5) })
bind_button("once", fn() { player.set_repeat(0) })
bind_button("middle", fn() { player.set_progress(0.5) })
}
seek 和 set_progress 会把参数截断到时间轴范围内,set_time_scale 忽略非正因子,set_repeat 把小于 -1 的值视为“无限”。
显示时间轴位置
在帧回调中读取 time() 和 progress(),以更新读数或范围输入。demo_gsap 包用一个小的 JavaScript 函数完成这件事;在 MoonBit 中你可以自己格式化文字:
fn readout(time : Double, duration : Double) -> String {
let tenths = (time * 10.0).round().to_int()
"\{tenths / 10}.\{tenths % 10} / \{duration.to_int()} s"
}
test "readout" {
inspect(readout(3.14159, 8.0), content="3.1 / 8 s")
}
在运行时选择场景
把当前选择保存在一个 Ref 中并在回调中读取;修改之后,重绘当前时刻,使暂停中的播放器也能立即显示新场景:
fn scene_for(kind : String, t : Double) -> @frontend.Scene {
let mesh = if kind == "cube" { @core.cube_mesh(1.2) } else { @core.torus_mesh(1.7, 0.58, 32, 18) }
@frontend.Scene::single(
mesh,
@core.Transform3::rotation(0.5 * t, 0.8 * t, 0.0),
@frontend.Light::default(),
)
}
fn start_with_selection(svg : @dom.Element, view : @frontend.RenderView) -> (@gsap.GsapPlayer, Ref[String]) {
let config = @gsap.GsapSvgRenderConfig::sized(640, 480)
let kind = Ref("torus")
let player = @gsap.GsapPlayer::new(8.0, fn(t) {
@gsap.render_scene(svg, scene_for(kind.val, t), view, config)
})
(player, kind)
}
fn select(player : @gsap.GsapPlayer, kind : Ref[String], value : String, svg : @dom.Element, view : @frontend.RenderView) -> Unit {
kind.val = value
@gsap.render_scene(svg, scene_for(value, player.time()), view, @gsap.GsapSvgRenderConfig::sized(640, 480))
}
进阶
绘制顺序的精确性
多边形按平均深度由远到近排序。单个凸物体(立方体、球、圆柱、圆锥、棱锥)总能被正确绘制;圆环和多物体场景在三角形深度重叠处可能出现短暂的排序错误,相交的物体则永远无法正确处理。GSAP 设计证明了顺序何时是精确的。当精确遮挡比矢量输出更重要时,请使用 Canvas 后端。
为输出设置样式
SVG 是普通的 DOM:可以用 CSS 缩放(width: 100%; height: auto 会保持 viewBox 的宽高比)、添加滤镜,或通过序列化该元素将其导出。后端只触碰它自己的背景矩形和三角形分组。
常见陷阱
- 未加载 GSAP。 缺少 GSAP 时,
GsapPlayer::new会抛出 “geometry3d GSAP backend requires globalThis.gsap”。请像上面的页面那样,在 MoonBit 模块之前导入 GSAP。 - 目标不对。 该包只能为
js构建;请在每个导入它的包中添加supported_targets = "js"。 - 外来的子节点。 对于缺少后端标记节点的
<svg>,首次渲染会替换其所有子节点。请给后端一个专用的<svg>元素。 - 倒放与速度因子。 请使用
reverse(),而不是负的速度因子;set_time_scale会把非正因子替换为1.0。 - 接缝。 浏览器对每个多边形分别做抗锯齿,因此同一面的相邻三角形之间可能出现淡淡的线条。
下一步
- GSAP API 列出了渲染器和播放器的每个方法。
- GSAP 设计分析了画家排序。
- demo_gsap 教程讲解了仓库中完整的播放器页面。