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>

プログラムはトーラスを 8 秒に 1 回転させ、それをずっと続けます。

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 で継ぎ目なくループします。

よくある作業

再生コントロールをつなぐ

各コントロールは 1 つのメソッドに対応します。プレーヤーを変数に保持し、イベントハンドラから呼び出します(ここでは 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 チュートリアルで説明しています。