backend/canvas API

包 Luna-Flow/geometry3d/backend/canvas 使用 2D 上下文把前端的 DrawList 绘制到 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 是 RGB 颜色,各通道取值于 [0,255][0, 255]。

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 trait 的实现。

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 光栅化列表,并对每个被覆盖的像素计算明暗级别 q=round⁡(clamp⁡(I) (L−1))q = \operatorname{round}(\operatorname{clamp}(I)\,(L - 1)),其中 LL 为明暗级数。然后把每段 qq 相同的极大水平覆盖像素段用一次高 1 像素的 fillRect 绘制,颜色为 round⁡(fg⋅q/(L−1))\operatorname{round}(\mathit{fg} \cdot q / (L - 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 上下文,它会中止。

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)
}