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 的函数,动画在 t=8t = 8 处无缝循环。

常见任务

接上播放控件

每个控件对应一个方法。把播放器保存在一个变量中,并在事件处理器中调用它(这里使用 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。
  • 接缝。 浏览器对每个多边形分别做抗锯齿,因此同一面的相邻三角形之间可能出现淡淡的线条。

下一步