A tiny Skia subset ported to MoonBit (port of tiny-skia 0.12.0; see NOTICE)
| 语言 | MoonBit(后端:wasm 与 wasm-gc 并列,两个都验过,默认产物 wasm-gc) |
| 后端承诺 | 两个都承诺:各自 build + check + test 全绿(bash scripts/backend-tier.sh),backend-parity 再跑 CLI 指纹逐字节一致。判据与分级方式见 T0 §3.10 |
| 许可 | BSD-3-Clause(接收自 tiny-skia) |
| 来源 | tiny-skia 0.12.0 的 MoonBit 移植 → NOTICE |
| 测试 | 黑盒 / 白盒分层 → 文档索引 · 现行口径 T0 事实基线 |
数字与复跑命令在 T0 §6.4 实测值清单。
| 能做什么 | 状态 |
|---|---|
| 填充任意路径(直线 / 二次 / 三次贝塞尔),Winding 与 EvenOdd | ✅ |
| fill_rect(与 fill_path 是两个入口,非整数坐标差 1 像素列) | ✅ |
| 抗锯齿(4×4 超采样)· 非恒等变换 · 29 种混合模式 | ✅ |
| 遮罩 Mask(含 apply_mask 与 from_pixmap 的 Alpha / Luminance 两档,上游唯一不经过 Paint 的管线) | ✅ |
| 描边:粗线 / 细线(w < 1px)/ 虚线(Stroke::with_dash) | ✅ |
| 渐变四种形状:线性(两停 + 多停)· 径向(同心 / Strip / Focal)· 扫掠 · 图案 | ✅ |
| 低精度管线(lowp) | ✅ 按上游选路走;选路表 5 项已对齐并在装配层生效(T0 §2.6.3);AA + 图案三张基线逐字节 |
| 大图(任一边长 > 8191,即 8192+) | ✅ 与上游 DrawTiler 同门限、同分块;8191 正常画,8192+ 逐块 subpixmap / submask(T0 §2.6.1) |
| 视图 / 复制 API(PixmapRef · clone_rect · from_bytes) | ✅ PixmapView 的 buffer / data / clone_rect / from_bytes / to_owned + Pixmap::clone_rect + PixmapMut::from_bytes / to_owned |
| PNG 字节编解码(Pixmap / Mask / PixmapView) | 🟡 解 / 编已接(含调色板 PNG);load_png / save_png 要文件系统,wasm / wasm-gc 上不可用 |
| 色彩空间(Paint.with_colorspace / ColorSpace):Linear(默认)· Gamma2 · SimpleSRGB · FullSRGBGamma | ✅ 纯色 / 渐变的展开与压缩都接上了;gamma.png 逐字节已认领(V-5,T6 §1.5.1.2) |
| JPEG | ❌ 不做(上游也没有;无素材、无判据) |
| 能力 | 示例 |
|---|---|
| 标量入口 | quick start |
| 路径填充 | fill a triangle |
| 抗锯齿 · 混合模式 | fill an antialiased rect with Xor |
| 非恒等变换 | fill a rect through a non-identity transform |
| 遮罩 | fill a path through a mask |
| 描边:粗线 | stroke a thick line |
| 描边:虚线 | stroke a dashed line |
| 渐变:线性两停 / 多停 | paint with a linear gradient · paint with a multi-stop linear gradient |
| 渐变:径向 | paint with a radial gradient |
| 渐变:扫掠 | paint with a sweep gradient |
| 图案着色器 | paint with a pattern |
| 色彩空间 | select a color space by name |
| PNG 字节编解码 | png bytes round trip |
///|
test "quick start" {
// 版本号来自 moon.mod,是单一事实源
// 用码点数判长度:`String::length()` 等于已弃用的 `count()`,语义随工具链漂移
let v = @lib.version()
assert_true(v.to_array().length() > 0)
// 通道值钳制:0..=255
assert_eq(@lib.clamp_channel(300), 255)
assert_eq(@lib.clamp_channel(-1), 0)
// 单位区间钳制:[0, 1]
assert_eq(@lib.clamp_unit(2.0), 1.0)
assert_eq(@lib.clamp_unit(-0.5), 0.0)
}///|
test "fill a triangle" {
let pm = @lib.Pixmap::new(64, 64).unwrap()
let paint = @lib.paint_with_rgba8(@lib.paint_new(), 50, 127, 150, 200).with_anti_alias(
false,
)
let pb = @lib.path_builder_new()
@lib.path_builder_move_to(pb, 32.0, 4.0)
@lib.path_builder_line_to(pb, 60.0, 60.0)
@lib.path_builder_line_to(pb, 4.0, 60.0)
@lib.path_builder_close(pb)
let path = @lib.path_builder_finish(pb).unwrap()
let r = pm.fill_path(
path,
paint,
@lib.fill_rule_winding(),
@lib.transform_identity(),
)
assert_true(r is Ok(_))
// 三角形内部不透明,外部透明:像素是**预乘** RGBA
match pm.pixel_at(32, 40) {
Some((red, _, _, alpha)) => {
// premultiply_u8(50, 200):prod = 50*200+128 = 10128;10128 + (10128>>8) = 10167;
// 10167 >> 8 = 39
assert_eq(red, 39)
assert_eq(alpha, 200)
}
None => fail("pixel_at 越界")
}
match pm.pixel_at(0, 0) {
Some((0, 0, 0, 0)) => ()
Some((red, green, blue, alpha)) =>
fail("左上角应为透明,实际 (\{red},\{green},\{blue},\{alpha})")
None => fail("pixel_at 越界")
}
}///|
test "fill an antialiased rect with Xor" {
let pm = @lib.Pixmap::new(16, 16).unwrap()
// 先铺一层不透明底,好让混合模式有东西可混
let bg = @lib.paint_with_rgba8(@lib.paint_new(), 200, 100, 50, 255).with_anti_alias(
false,
)
let full = @lib.rect_from_ltrb(0.0, 0.0, 16.0, 16.0).unwrap()
assert_true(pm.fill_rect(full, bg, @lib.transform_identity()) is Ok(_))
// 再用 Xor 画一个分数边界的矩形(上下边落在半像素上 ⇒ 出覆盖率)
let fg = @lib.paint_with_rgba8(@lib.paint_new(), 20, 210, 160, 255).with_anti_alias(
true,
)
let xor = @lib.paint_with_blend_mode(fg, "Xor").unwrap()
let r = @lib.rect_from_ltrb(2.5, 4.25, 12.5, 11.75).unwrap()
assert_true(pm.fill_rect(r, xor, @lib.transform_identity()) is Ok(_))
// 矩形正中是不透明区:Xor 对「不透明源 + 不透明底」给出 (0,0,0,0)
match pm.pixel_at(7, 8) {
Some((0, 0, 0, 0)) => ()
Some((red, green, blue, alpha)) =>
fail(
"Xor 正中应为全透明,实际 (\{red},\{green},\{blue},\{alpha})",
)
None => fail("pixel_at 越界")
}
// 边界行必须出现**中间覆盖率**(既不是 0 也不是 1)——这就是抗锯齿在起作用
let mut saw_partial = false
for x = 3; x < 12; x = x + 1 {
match pm.pixel_at(x, 4) {
Some((_, _, _, alpha)) =>
if alpha > 0 && alpha < 255 {
saw_partial = true
}
None => fail("pixel_at 越界")
}
}
assert_true(saw_partial)
}///|
test "fill a rect through a non-identity transform" {
let pm = @lib.Pixmap::new(16, 1).unwrap()
let paint = @lib.paint_with_rgba8(@lib.paint_new(), 10, 20, 30, 255).with_anti_alias(
false,
)
// 变换的构造与读数都在公开面:平移 (5,0) 不是恒等变换,带平移、不带缩放
let ts = @lib.transform_from_translate(5.0, 0.0)
assert_true(!@lib.transform_is_identity(ts))
assert_true(@lib.transform_has_translate(ts))
assert_true(!@lib.transform_has_scale(ts))
// 几何先过变换再光栅化:矩形 [0,4)×[0,1) 落到 x ∈ [5,9)
let rect = @lib.rect_from_ltrb(0.0, 0.0, 4.0, 1.0).unwrap()
assert_true(pm.fill_rect(rect, paint, ts) is Ok(_))
match pm.pixel_at(0, 0) {
Some((0, 0, 0, 0)) => ()
Some(_) => fail("fill_rect + from_translate(5,0):x=0 应透明")
None => fail("pixel_at 越界")
}
match pm.pixel_at(6, 0) {
Some((10, 20, 30, 255)) => ()
Some(_) =>
fail("fill_rect + from_translate(5,0):x=6 应为不透明源色")
None => fail("pixel_at 越界")
}
match pm.pixel_at(12, 0) {
Some((0, 0, 0, 0)) => ()
Some(_) =>
fail(
"fill_rect + from_translate(5,0):x=12 在矩形右边界外,应透明",
)
None => fail("pixel_at 越界")
}
}///|
test "fill a path through a mask" {
let pm = @lib.Pixmap::new(32, 32).unwrap()
// 遮罩:只放行左半 `[0,16)`
let mask = @lib.mask_new(32, 32).unwrap()
let half = @lib.path_builder_new()
@lib.path_builder_move_to(half, 0.0, 0.0)
@lib.path_builder_line_to(half, 16.0, 0.0)
@lib.path_builder_line_to(half, 16.0, 32.0)
@lib.path_builder_line_to(half, 0.0, 32.0)
@lib.path_builder_close(half)
let half_path = @lib.path_builder_finish(half).unwrap()
assert_true(
@lib.mask_fill_path(
mask,
half_path,
@lib.fill_rule_winding(),
false,
@lib.transform_identity(),
)
is Ok(_),
)
// mask 里 255 / 0 就是「放行 / 拦住」
assert_eq(@lib.mask_coverage_at(mask, 0, 0), 255)
assert_eq(@lib.mask_coverage_at(mask, 16, 0), 0)
// 用整张图去填,但经过遮罩 ⇒ 只有左半被画
let paint = @lib.paint_with_rgba8(@lib.paint_new(), 50, 127, 150, 255).with_anti_alias(
false,
)
let full = @lib.path_builder_new()
@lib.path_builder_move_to(full, 0.0, 0.0)
@lib.path_builder_line_to(full, 32.0, 0.0)
@lib.path_builder_line_to(full, 32.0, 32.0)
@lib.path_builder_line_to(full, 0.0, 32.0)
@lib.path_builder_close(full)
let full_path = @lib.path_builder_finish(full).unwrap()
assert_true(
@lib.pixmap_fill_path_with_mask(
pm,
full_path,
paint,
@lib.fill_rule_winding(),
@lib.transform_identity(),
mask,
)
is Ok(_),
)
// 遮罩内:不透明源色(a=255 ⇒ 预乘就是自己)
match pm.pixel_at(4, 16) {
Some((10, 20, 30, 255)) => fail("不该是别的颜色")
Some((50, 127, 150, 255)) => ()
Some((red, green, blue, alpha)) =>
fail(
"遮罩内应为 (50,127,150,255),实际 (\{red},\{green},\{blue},\{alpha})",
)
None => fail("pixel_at 越界")
}
// 遮罩外:一个字节都没被碰
match pm.pixel_at(20, 16) {
Some((0, 0, 0, 0)) => ()
Some((red, green, blue, alpha)) =>
fail("遮罩外应透明,实际 (\{red},\{green},\{blue},\{alpha})")
None => fail("pixel_at 越界")
}
}///|
test "stroke a thick line" {
let pm = @lib.Pixmap::new(100, 100).unwrap()
let paint = @lib.paint_with_rgba8(@lib.paint_new(), 50, 127, 150, 200).with_anti_alias(
false,
)
let pb = @lib.path_builder_new()
@lib.path_builder_move_to(pb, 20.0, 50.0)
@lib.path_builder_line_to(pb, 80.0, 50.0)
let path = @lib.path_builder_finish(pb).unwrap()
// w = 10、Butt 端:沿法向各外扩 w/2 = 5 ⇒ 覆盖 x ∈ [20, 80]、y ∈ [45, 55)
// (Butt 不沿切线延长,所以左端就是端点那一格;像素 y 覆盖 [y, y+1) 是半开区间)
let stroke = @lib.stroke_new()
.with_width(10.0)
.with_line_cap(@lib.line_cap_butt())
assert_true(
pm.stroke_path(path, paint, stroke, @lib.transform_identity()) is Ok(_),
)
// 五个「几何算术」点:中线不透明、两端按 Butt 截断、上下边界外各差一格
match pm.pixel_at(50, 50) {
// premultiply_u8:prod = c*200+128,结果 (prod + (prod>>8)) >> 8
// 50 ⇒ 10128 ⇒ 10167 >> 8 = 39 · 127 ⇒ 25528 ⇒ 25627 >> 8 = 100 · 150 ⇒ 30128 ⇒ 30245 >> 8 = 118
Some((39, 100, 118, 200)) => ()
Some((red, green, blue, alpha)) =>
fail(
"中线应为预乘 (39,100,118,200),实际 (\{red},\{green},\{blue},\{alpha})",
)
None => fail("pixel_at 越界")
}
match pm.pixel_at(50, 44) {
Some((0, 0, 0, 0)) => ()
Some((red, green, blue, alpha)) =>
fail(
"y=44 在 w=10 的带外,实际 (\{red},\{green},\{blue},\{alpha})",
)
None => fail("pixel_at 越界")
}
// 半开区间:像素 y 覆盖 [y, y+1),所以带是 y ∈ [45, 55) ⇒ 54 在内、55 在外
match pm.pixel_at(50, 54) {
Some((39, 100, 118, 200)) => ()
Some((red, green, blue, alpha)) =>
fail(
"y=54 在 w=10 的带内,实际 (\{red},\{green},\{blue},\{alpha})",
)
None => fail("pixel_at 越界")
}
match pm.pixel_at(50, 55) {
Some((0, 0, 0, 0)) => ()
Some((red, green, blue, alpha)) =>
fail(
"y=55 在 w=10 的带外,实际 (\{red},\{green},\{blue},\{alpha})",
)
None => fail("pixel_at 越界")
}
// Butt:x = 19 在端点外(不延长),x = 20 在端点上(闭区间)
match pm.pixel_at(19, 50) {
Some((0, 0, 0, 0)) => ()
Some((red, green, blue, alpha)) =>
fail(
"Butt 端外 x=19 应透明,实际 (\{red},\{green},\{blue},\{alpha})",
)
None => fail("pixel_at 越界")
}
match pm.pixel_at(20, 50) {
Some((39, 100, 118, 200)) => ()
Some((red, green, blue, alpha)) =>
fail(
"Butt 端点 x=20 应不透明,实际 (\{red},\{green},\{blue},\{alpha})",
)
None => fail("pixel_at 越界")
}
}///|
test "stroke a dashed line" {
let pm = @lib.Pixmap::new(100, 20).unwrap()
let paint = @lib.paint_with_rgba8(@lib.paint_new(), 0, 0, 0, 255).with_anti_alias(
false,
)
let pb = @lib.path_builder_new()
@lib.path_builder_move_to(pb, 0.0, 10.0)
@lib.path_builder_line_to(pb, 100.0, 10.0)
let path = @lib.path_builder_finish(pb).unwrap()
// dash 数组是「实、虚、实、虚…」,相位 0:实 10、虚 10
let dash = @lib.stroke_dash_new([6.0, 10.0], 0.0).unwrap()
let stroke = @lib.stroke_new().with_width(4.0).with_dash(Some(dash))
assert_true(
pm.stroke_path(path, paint, stroke, @lib.transform_identity()) is Ok(_),
)
// 实段在前 10 像素里 ⇒ x=5 不透明;x=15 落在虚段里 ⇒ 一个字节都没被碰
match pm.pixel_at(5, 10) {
Some((0, 0, 0, 255)) => ()
Some((red, green, blue, alpha)) =>
fail(
"实段 x=5 应为 (0,0,0,255),实际 (\{red},\{green},\{blue},\{alpha})",
)
None => fail("pixel_at 越界")
}
match pm.pixel_at(15, 10) {
Some((0, 0, 0, 0)) => ()
Some((red, green, blue, alpha)) =>
fail("虚段 x=15 应透明,实际 (\{red},\{green},\{blue},\{alpha})")
None => fail("pixel_at 越界")
}
// 非法 dash 是**返回值**不是异常:空数组 / 奇数长度 / 负数都拿 `Err`
assert_true(@lib.stroke_dash_new([], 0.0) is Err(_))
assert_true(@lib.stroke_dash_new([5.0], 0.0) is Err(_))
assert_true(@lib.stroke_dash_new([5.0, -1.0], 0.0) is Err(_))
}///|
test "paint with a linear gradient" {
let pm = @lib.Pixmap::new(8, 1).unwrap()
// 沿 x 从 (0,0) 到 (8,0):红 → 蓝,Pad(范围外复用首末色)
let grad = @lib.paint_with_linear_gradient(
@lib.paint_new(),
0.0,
0.0,
8.0,
0.0,
@lib.Color::from_rgba8(255, 0, 0, 255),
@lib.Color::from_rgba8(0, 0, 255, 255),
@lib.spread_mode_pad(),
@lib.transform_identity(),
).unwrap()
// 挂着色器时它**不是**纯色:`memset2d` 短路与强度归约都以 `is_solid_color` 为前提
assert_true(!grad.is_solid_color())
// 两个不同着色器挂在同一个 Paint 上 ⇒ 后设的生效(上游 `paint.shader = …` 是赋值)
let again = @lib.paint_with_shader(grad, grad.shader().unwrap())
assert_eq(@lib.shader_kind_name(again.shader().unwrap()), "LinearGradient")
// 设纯色会把着色器撤掉(上游 `set_color` 写的是 `shader = SolidColor(c)`)
let solid = grad.with_color_rgba8(0, 200, 0, 255)
assert_true(solid.is_solid_color())
assert_true(solid.shader() is None)
let full = @lib.rect_from_ltrb(0.0, 0.0, 8.0, 1.0).unwrap()
assert_true(pm.fill_rect(full, grad, @lib.transform_identity()) is Ok(_))
// 渐变必须真的铺开:两端各偏向自己那侧
match pm.pixel_at(0, 0) {
Some((red, _, blue, _)) => assert_true(red > 200 && blue < 60)
None => fail("pixel_at 越界")
}
match pm.pixel_at(7, 0) {
Some((red, _, blue, _)) => assert_true(red < 60 && blue > 200)
None => fail("pixel_at 越界")
}
}///|
test "paint with a multi-stop linear gradient" {
let pm = @lib.Pixmap::new(8, 1).unwrap()
// 0.0 红 → 0.5 绿 → 1.0 蓝。三停走 `Gradient`(查表),两停才走
// `EvenlySpaced2StopGradient`(四次 mad)——上游的判据是色标数,不是「有没有中间色」。
let stops : Array[(Float, @lib.Color)] = [
(0.0, @lib.Color::from_rgba8(255, 0, 0, 255)),
(0.5, @lib.Color::from_rgba8(0, 255, 0, 255)),
(1.0, @lib.Color::from_rgba8(0, 0, 255, 255)),
]
let grad = @lib.paint_with_linear_gradient_stops(
@lib.paint_new(),
0.0,
0.0,
8.0,
0.0,
stops,
@lib.spread_mode_pad(),
@lib.transform_identity(),
).unwrap()
let full = @lib.rect_from_ltrb(0.0, 0.0, 8.0, 1.0).unwrap()
assert_true(pm.fill_rect(full, grad, @lib.transform_identity()) is Ok(_))
// 两端是首末色标,中间那停真的参与插值(绿通道在中间最大)
match pm.pixel_at(0, 0) {
Some((red, green, blue, _)) =>
assert_true(red > 200 && green < 60 && blue < 60)
None => fail("pixel_at 越界")
}
match pm.pixel_at(3, 0) {
Some((_, green, _, _)) => assert_true(green > 150)
None => fail("pixel_at 越界")
}
match pm.pixel_at(7, 0) {
Some((red, green, blue, _)) =>
assert_true(red < 60 && green < 60 && blue > 200)
None => fail("pixel_at 越界")
}
// 位置越界**不报错**:上游 `GradientStop::new` 用 `new_clamped` 夹住;空数组才是 `Err`
let clamped : Array[(Float, @lib.Color)] = [
(-1.0, @lib.Color::from_rgba8(255, 0, 0, 255)),
(2.0, @lib.Color::from_rgba8(0, 0, 255, 255)),
]
assert_true(
@lib.paint_with_linear_gradient_stops(
@lib.paint_new(),
0.0,
0.0,
8.0,
0.0,
clamped,
@lib.spread_mode_pad(),
@lib.transform_identity(),
)
is Ok(_),
)
let empty : Array[(Float, @lib.Color)] = []
assert_true(
@lib.paint_with_linear_gradient_stops(
@lib.paint_new(),
0.0,
0.0,
8.0,
0.0,
empty,
@lib.spread_mode_pad(),
@lib.transform_identity(),
)
is Err(_),
)
}///|
test "paint with a radial gradient" {
let pm = @lib.Pixmap::new(8, 1).unwrap()
// 同心径向:(4,0) 为心,r0 = 0 → r1 = 4:红 → 蓝,Pad
let grad = @lib.paint_with_radial_gradient(
@lib.paint_new(),
4.0,
0.0,
0.0,
4.0,
0.0,
4.0,
@lib.Color::from_rgba8(255, 0, 0, 255),
@lib.Color::from_rgba8(0, 0, 255, 255),
@lib.spread_mode_pad(),
@lib.transform_identity(),
).unwrap()
let full = @lib.rect_from_ltrb(0.0, 0.0, 8.0, 1.0).unwrap()
assert_true(pm.fill_rect(full, grad, @lib.transform_identity()) is Ok(_))
// 圆心是首色(t = 0),最外缘是末色(t = 1)
match pm.pixel_at(4, 0) {
Some((red, _, blue, _)) => assert_true(red > 200 && blue < 60)
None => fail("pixel_at 越界")
}
match pm.pixel_at(0, 0) {
Some((red, _, blue, _)) => assert_true(red < 60 && blue > 200)
None => fail("pixel_at 越界")
}
// 半径为负是返回值不是异常:Err(NegativeRadius)
assert_true(
@lib.paint_with_radial_gradient(
@lib.paint_new(),
4.0,
0.0,
-1.0,
4.0,
0.0,
4.0,
@lib.Color::from_rgba8(255, 0, 0, 255),
@lib.Color::from_rgba8(0, 0, 255, 255),
@lib.spread_mode_pad(),
@lib.transform_identity(),
)
is Err(_),
)
}///|
test "paint with a sweep gradient" {
let pm = @lib.Pixmap::new(8, 1).unwrap()
// 圆心 (4,0),红起蓝止;t0/t1 = 起始角/360、终止角/360
let grad = @lib.paint_with_sweep_gradient(
@lib.paint_new(),
4.0,
0.0,
0.0,
360.0,
@lib.Color::from_rgba8(255, 0, 0, 255),
@lib.Color::from_rgba8(0, 0, 255, 255),
@lib.spread_mode_pad(),
@lib.transform_identity(),
).unwrap()
// 挂了着色器就不是纯色;种别名是枚举的契约名
assert_true(!grad.is_solid_color())
assert_eq(@lib.shader_kind_name(grad.shader().unwrap()), "SweepGradient")
// t0/t1 是扫掠独有的几何读数(线性和径向取不到)
match @lib.sweep_gradient_t0_t1(grad.shader().unwrap()) {
Some((t0, t1)) => {
assert_true(t0 == 0.0)
assert_true(t1 == 1.0)
}
None => fail("扫掠着色器应给得出 (t0, t1)")
}
let full = @lib.rect_from_ltrb(0.0, 0.0, 8.0, 1.0).unwrap()
assert_true(pm.fill_rect(full, grad, @lib.transform_identity()) is Ok(_))
// 圆心两侧的角度不同 ⇒ t 不同 ⇒ 颜色不同(不是一张纯色平铺)
match (pm.pixel_at(0, 0), pm.pixel_at(7, 0)) {
(Some((lr, lg, lb, la)), Some((rr, rg, rb, ra))) =>
assert_true(lr != rr || lg != rg || lb != rb || la != ra)
_ => fail("pixel_at 越界")
}
}///|
test "paint with a pattern" {
// 源图 2×1:左像素不透明红,右像素不透明蓝
let src = @lib.Pixmap::new(2, 1).unwrap()
let red = @lib.paint_with_rgba8(@lib.paint_new(), 255, 0, 0, 255)
let blue = @lib.paint_with_rgba8(@lib.paint_new(), 0, 0, 255, 255)
assert_true(
src.fill_rect(
@lib.rect_from_ltrb(0.0, 0.0, 1.0, 1.0).unwrap(),
red,
@lib.transform_identity(),
)
is Ok(_),
)
assert_true(
src.fill_rect(
@lib.rect_from_ltrb(1.0, 0.0, 2.0, 1.0).unwrap(),
blue,
@lib.transform_identity(),
)
is Ok(_),
)
// Repeat + Nearest:源图像素 → 画布走恒等变换,2 像素一循环
let pat = @lib.paint_with_pattern(
@lib.paint_new(),
src,
@lib.spread_mode_repeat(),
@lib.filter_quality_nearest(),
1.0,
@lib.transform_identity(),
).unwrap()
let pm = @lib.Pixmap::new(5, 1).unwrap()
let full = @lib.rect_from_ltrb(0.0, 0.0, 5.0, 1.0).unwrap()
assert_true(pm.fill_rect(full, pat, @lib.transform_identity()) is Ok(_))
// x 偶数落红列、x 奇数落蓝列(不透明源 ⇒ 预乘值就是自己)
assert_eq(pm.pixel_at(0, 0), Some((255, 0, 0, 255)))
assert_eq(pm.pixel_at(1, 0), Some((0, 0, 255, 255)))
assert_eq(pm.pixel_at(4, 0), Some((255, 0, 0, 255)))
}///|
test "select a color space by name" {
// 名字逐字照抄上游 ColorSpace 变体名;`Linear` 是默认档
assert_true(@lib.colorspace_of_name("Linear") is Some(_))
assert_true(@lib.colorspace_of_name("Gamma2") is Some(_))
assert_true(@lib.colorspace_of_name("SimpleSRGB") is Some(_))
assert_true(@lib.colorspace_of_name("FullSRGBGamma") is Some(_))
assert_true(@lib.colorspace_of_name("sRGB") is None)
// setter 同形:合法名字给 Paint,拼错给 None
let base = @lib.paint_with_rgba8(@lib.paint_new(), 50, 100, 150, 255)
assert_true(@lib.paint_with_colorspace(base, "Gamma2") is Some(_))
assert_true(@lib.paint_with_colorspace(base, "bogus") is None)
// 挂上色彩空间之后照常出图
let pm = @lib.Pixmap::new(4, 1).unwrap()
let paint = @lib.paint_with_colorspace(base, "Gamma2").unwrap()
let full = @lib.rect_from_ltrb(0.0, 0.0, 4.0, 1.0).unwrap()
assert_true(pm.fill_rect(full, paint, @lib.transform_identity()) is Ok(_))
}///|
test "png bytes round trip" {
let pm = @lib.Pixmap::new(4, 2).unwrap()
let paint = @lib.paint_with_rgba8(@lib.paint_new(), 50, 127, 150, 200).with_anti_alias(
false,
)
assert_true(
pm.fill_rect(
@lib.rect_from_ltrb(0.0, 0.0, 4.0, 2.0).unwrap(),
paint,
@lib.transform_identity(),
)
is Ok(_),
)
// 编成 PNG 字节再解回来:逐像素一致
let bytes = pm.encode_png().unwrap()
let back = @lib.Pixmap::decode_png(bytes).unwrap()
assert_eq(back.width(), 4)
assert_eq(back.height(), 2)
for y = 0; y < 2; y = y + 1 {
for x = 0; x < 4; x = x + 1 {
assert_eq(back.pixel_at(x, y), pm.pixel_at(x, y))
}
}
// 坏字节是返回值不是异常
assert_true(@lib.Pixmap::decode_png(b"not a png") is Err(_))
}着色器的类型面(调用方只需记两件事):Paint.shader 是 Shader?——一个字段、一个枚举, None 表示纯色,所以「同时挂两个着色器」在类型上写不出来; with_color_rgba8 / with_color 会撤掉着色器(与上游 set_color 同义)。 为什么是这种形状、加成员的判据在哪,见 T0 §2.3。
描边怎么走(只讲路径,不讲状态):粗线铺成外扩轮廓再当填充;细线(w < 1px 或 w = 0) 走 hairline / hairline_aa 的逐段扫描,不走轮廓;dash 在两者之前——先切虚线再判粗细 (与上游同序)。三条路各自的状态与判据在 T0 §2.2。
上面这些代码块都是文档测试:moon test 会真编译、真运行。 三角形那段断言的是预乘值(50*200/255 的整数式舍入),不是「跑一遍抄下来」—— 算式写在注释里,手算可复核;抗锯齿矩形那段断言「边界行出现过中间覆盖率」, 这是抗锯齿唯一能被黑盒测到的特征(全 0 或全 255 说明 AA 没生效)。 新增三块也各有读数:变换判 from_translate(5,0) 把 [0,4) 搬到 [5,9) 的几何算术; 扫掠判 t0/t1 = 0/1 与「圆心两侧颜色不同」(不是纯色平铺); 色彩空间判名字契约——拼错给 None,不静默取默认 Linear。
moon add TryAndRun-TvT/tiny-skia-moon改公共 API 必须同 PR 改这里的示例,否则 moon test 直接变红。
# 环境初始化:一条命令装齐 moon + cc + make + cargo(幂等,装完自检)
bash scripts/setup-toolchain.sh
# 只跑门禁的话,SKIP_RUST=1 可以跳过 cc/cargo;
# 只要 Rust 侧(上游取证)用 bash scripts/setup-rust.sh
#
# 为什么 cc 是必装:缺它时 `cargo build` 连 `crc32fast` 都编不过——
# 那个 crate 的 build script 要先编成宿主机二进制。报错指向 crate、不指向环境。
# 本地全量门禁(push CI 未启用时的等价替代)
bash scripts/gates.sh
# 只跑一段
STAGES="check test" bash scripts/gates.sh
SKIP_SLOW=1 bash scripts/gates.sh
# 工具链适配判据(换了工具链 / 改了 .moonignore 或发布面后先跑这个)
bash scripts/toolchain-check.shbash scripts/lang-probe.sh # MoonBit 语言卡点 + SIMD 能力 + 标量基准 + ⑩/⑪ API 形态陷阱
bash scripts/upstream-ledger.sh # 上游账本:体量 / 测试 / 基线分布 / 结构常量(全部现算)
bash scripts/upstream-semantics.sh # 上游语义:12 组判据逐条打印**上游行号**
python3 scripts/gen-baselines.py --verify-only # 基线解码三判据(需上游已 clone)
bash scripts/determinism.sh # 同命令跑两遍字节必须一样(账本 / 基线 / 指纹自检)
bash scripts/m5-probe.sh # M5 可行性:未实现阶段分批 / 上游 lowp 表里哪些是 null_fn / 基线分布# 两个后端都编(缺省 preferred_target = wasm-gc)
moon build cmd/main --target wasm-gc --release
moon build cmd/main --target wasm --release
moon run cmd/main --target wasm-gc
# 公开面用法示例(纯色/路径填充 · 描边 · 线性渐变,结果打印采样像素)
moon run cmd/examples --target wasm-gc
# 从 JS 调用:cmd/jsapi 导出绑定函数,JS 建画布 → 传参画图 → 像素读回断言
# (真实交互演示,两个后端同一套导出名,详见 cmd/examples/js/README.md)
moon build cmd/jsapi --target wasm && node cmd/examples/js/jsapi_demo.mjs wasm
moon build cmd/jsapi --target wasm-gc && node cmd/examples/js/jsapi_demo.mjs wasm-gc
# JS 加载 wasm / wasm-gc 产物(构建 + Node 运行,含两后端差异分析)
moon build cmd/examples --target wasm-gc --release
node cmd/examples/js/run-gc.mjs
moon build cmd/examples --target wasm --release
node cmd/examples/js/run-wasm.mjs
# 后端等价性判据(声明集合 / 双后端 CLI 输出一致 / 逐后端 moon test)
bash scripts/backend-parity.sh门口在 docs/README.md(按读者路径索引)。 docs/ 只有一篇正文:T0 · 事实基线 ——口径、能力边界、事实基线、改代码前的三张表、缺口与下一步、判据工程、裁决、过程留痕, 以及「注释与死代码的账」(§3.14)和「基线形态与真值来源」(§3.15)。另加一个导航面 docs/README.md。 其余是 archive/ 里的指针(只写「已并入 T0 哪一节」,正文不在了)。 归档外目标用仓库绝对链接(/docs/ 与 /AGENTS.md 不进发布归档)。
| 文档 | 回答什么 |
|---|---|
| T0 · 事实基线 | 唯一口径,全仓唯一的正文:一句话现状 · 能力边界(对着代码核过)· §3 十五节事实基线(上游账本 / 数值一致性 / 对拍口径 / 精度档 / 三张表 / 判据面 / 门禁 / 确定性 / 性能账 / 后端与工具链 / 许可 / 文档纪律 / 环境 / 注释与死代码 / 基线形态,每条一命令)· §4 改代码前的三张表与坑录 · §1 完整缺口清单 · §2 下一步与出口判据 · §3 判据工程十六种形态 · §4 裁决与否掉的方案 · §5 过程留痕 · §6 文档长度与索引(生成物) |
| AGENTS.md | 硬性约定:Git 安全 · 包与文件放置 · 测试铁律 · 文档准入 · 环境初始化(写代码前先读) |
| NOTICE · LICENSE | 上游归属与逐条改动声明(唯一出处;许可义务→判据见 T0 §3.11) |
历史存档:docs/archive/ 里 是指针(已失效,不删文件,只留「已并入哪一篇哪一节」)。篇数不写死,现算:bash scripts/docs-length.sh --print。 lib/** 注释里的 M1 §5.5 这类施工期章节号去那里按编号找—— 读法见 T6 §5.5。 要复盘「当初为什么这么判」才去读它们。
.
├── moon.mod # 模块元数据(版本号单一事实源)
├── moon.pkg # 模块根空包(README 文档测试宿主)
├── lib/
│ ├── moon.pkg # 公共面(导出 API,含路径面的再导)
│ ├── facade.mbt # 公开形态:类型 + 错误
│ ├── path/ # 路径面子模块:Point/Rect/Size/Transform + Path/PathBuilder
│ │ # + 描边与虚线。**不含光栅**,只往下依赖内核
│ └── internal/ # 零分配内核(不 import lib)
│ ├── num/ # 饱和 / 回绕 / 定点 / 谓词 / 通道算术(M1)
│ ├── raster/ # 边 / 扫描线 / 裁剪(M2)+ AlphaRuns / SuperBlitter / AA 矩形(M3)
│ ├── mask/ # 遮罩:覆盖率缓冲 + fill_path / intersect_path(M3.5)
│ ├── pipeline/ # 栅格管线:79 项 Stage 枚举 + 已认领阶段 + blitter(M3/M4/M5)
│ ├── color/ # Color / ColorU8 / PremultipliedColorU8(M2)
│ ├── pixmap/ # Pixmap + SubPixmapMut(不含 PNG 编解码)
│ ├── error/ # KernelError(把上游 Option / panic 收成可穷尽枚举)
│ └── fingerprint/ # SHA-256(只服务确定性判据)
├── lib/baselines/ # 上游像素基线(裸字节)+ 整图对拍脚手架
├── cmd/main/ # CLI 演示入口(构建/运行 smoke 靶子)
├── cmd/fingerprint/ # 确定性指纹入口
├── cmd/examples/ # 示例入口:填充 / 路径 / 描边 / 渐变的公开面用法
│ └── js/ # JS 加载 wasm / wasm-gc 产物的最小示例 + 两后端差异分析
├── scripts/ # 门禁脚本(gates.sh 串成一条链)
├── docs/ # 文档(规格 / 证据 / 过程 / 元)
└── NOTICE / LICENSE # 上游归属与许可正文(随包分发)依 BSD-3-Clause 条件③:不得以 tiny-skia / Skia / 其作者或贡献者名义为本移植背书。
Install
Download zipA tiny Skia subset ported to MoonBit (port of tiny-skia 0.12.0; see NOTICE)