backend/canvas API

パッケージ Luna-Flow/geometry3d/backend/canvas は、フロントエンドの DrawList を 2D コンテキストで HTML <canvas> に描画します。描画リストをフロントエンドのソフトウェア深度バッファにラスタライズし、各ピクセルの輝度を単一の前景色の濃淡段階に量子化し、同じ濃淡が続く水平ランを fillRect で塗ります。対応ターゲットは js のみで、moonbit-community/rabbita の DOM バインディングを使います。

import {
  "Luna-Flow/geometry3d/frontend",
  "Luna-Flow/geometry3d/backend/canvas",
  "moonbit-community/rabbita/dom",
}

supported_targets = "js"

モジュールに moonbit-community/rabbita を追加してください(moon add moonbit-community/rabbita@0.12.4)。シェーディングとランの結合は Canvas の設計で、これを使ったページの作り方は Canvas チュートリアルで説明しています。

設定

CanvasColor

CanvasColor は各チャネルが [0,255][0, 255] の RGB 色です。

pub struct CanvasColor {
  red : Int
  green : Int
  blue : Int
}

CanvasColor::rgb

CanvasColor::rgb(r, g, b) は各チャネルを [0,255][0, 255] にクランプして色を作ります。

pub fn CanvasColor::rgb(Int, Int, Int) -> Self

CanvasRenderConfig

CanvasRenderConfig は、キャンバスのサイズ、背景色、前景色、濃淡の段階数をまとめたものです。

pub struct CanvasRenderConfig {
  width : Int
  height : Int
  background_color : CanvasColor
  foreground_color : CanvasColor
  shade_levels : Int
}

フィールドはパッケージ外からは読み取り専用なので、設定は以下のコンストラクタから得ます。描画のたびに設定は正規化されます。正でないサイズは 640 × 480 になり、チャネルはクランプされ、shade_levels は [2,256][2, 256] にクランプされます。

CanvasRenderConfig::default, CanvasRenderConfig::sized

CanvasRenderConfig::default() は 640 × 480、CanvasRenderConfig::sized(w, h) は指定したサイズを使います(正でない値は 640 または 480 になります)。どちらも背景色 rgb(7, 12, 22)、前景色 rgb(112, 226, 255)、256 段階の濃淡を使います。

pub fn CanvasRenderConfig::default() -> Self
pub fn CanvasRenderConfig::sized(Int, Int) -> Self

default は通常のコンストラクタであり、Default トレイトの実装ではありません。

test "canvas config" {
  let config = @canvas.CanvasRenderConfig::sized(320, 0)
  debug_inspect((config.width, config.height, config.shade_levels), content="(320, 480, 256)")
  let c = @canvas.CanvasColor::rgb(-5, 128, 300)
  debug_inspect([c.red, c.green, c.blue], content="[0, 128, 255]")
}

描画

render_draw_list

render_draw_list(context, list, config) は描画リストを 2D コンテキストに描きます。

pub fn render_draw_list(@dom.CanvasRenderingContext2D, @frontend.DrawList, CanvasRenderConfig) -> Unit

まず設定された領域全体を背景色で塗り、@frontend.draw_list_to_luma で設定サイズのままリストをラスタライズし、覆われた各ピクセルについて濃淡段階数 LL のもとで q=round⁡(clamp⁡(I) (L−1))q = \operatorname{round}(\operatorname{clamp}(I)\,(L - 1)) を計算します。次に、qq が等しい覆われたピクセルの極大な水平ランごとに、高さ 1 ピクセルの fillRect を色 round⁡(fg⋅q/(L−1))\operatorname{round}(\mathit{fg} \cdot q / (L - 1)) で 1 回呼びます。塗りつぶしスタイルは、連続するランの間で濃淡が変わるときだけ設定します。輝度 0 の覆われたピクセルは背景色ではなく黒で塗られます。描画リストは設定サイズと同じビューポートに投影されている必要があります。

render_scene

render_scene(context, scene, view, config) は @frontend.build_draw_list と render_draw_list を続けて実行します。

pub fn render_scene(@dom.CanvasRenderingContext2D, @frontend.Scene, @frontend.RenderView, CanvasRenderConfig) -> Unit

render_canvas

render_canvas(canvas, list, config) は canvas 要素の width と height 属性を設定サイズにし、2D コンテキストを取得して render_draw_list を呼びます。

pub fn render_canvas(@dom.HTMLCanvasElement, @frontend.DrawList, CanvasRenderConfig) -> Unit

要素に 2D コンテキストがない場合は中断(abort)します。

fn draw_cube_on(canvas : @dom.HTMLCanvasElement, angle : Double) -> Unit {
  let config = @canvas.CanvasRenderConfig::sized(320, 240)
  let view = @frontend.RenderView::perspective(
    @view.Camera3::default(4.5),
    @view.PerspectiveProjection::new(@view.Viewport::new(320, 240), 200.0),
  )
  let scene = @frontend.Scene::single(
    @core.cube_mesh(1.0),
    @core.Transform3::rotation(0.4, angle, 0.0),
    @frontend.Light::default(),
  )
  @canvas.render_canvas(canvas, @frontend.build_draw_list(scene, view), config)
}