add canvas_image builtin for PNG rendering with alpha blending

Registers canvas_image(path, x, y, w, h) in the type system and
implements it in the interpreter using the image crate — scales to
exact dimensions via Lanczos3 and alpha-composites onto the pixmap.
This commit is contained in:
Will Anderson
2026-04-28 14:51:24 -05:00
parent 18b60e3bf1
commit d86bbc3740
5 changed files with 106 additions and 1 deletions
Generated
+59 -1
View File
@@ -272,6 +272,12 @@ dependencies = [
"syn",
]
[[package]]
name = "byteorder-lite"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
[[package]]
name = "bytes"
version = "1.11.1"
@@ -672,6 +678,7 @@ dependencies = [
"fontdue",
"hex",
"hmac",
"image",
"reqwest",
"serde_json",
"sha2",
@@ -1421,6 +1428,19 @@ dependencies = [
"icu_properties",
]
[[package]]
name = "image"
version = "0.25.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
dependencies = [
"bytemuck",
"byteorder-lite",
"moxcms",
"num-traits",
"png 0.18.1",
]
[[package]]
name = "indexmap"
version = "2.14.0"
@@ -1676,6 +1696,16 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "moxcms"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b"
dependencies = [
"num-traits",
"pxfm",
]
[[package]]
name = "native-tls"
version = "0.2.18"
@@ -1735,6 +1765,15 @@ dependencies = [
"memoffset",
]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "num_enum"
version = "0.7.6"
@@ -1897,6 +1936,19 @@ dependencies = [
"miniz_oxide",
]
[[package]]
name = "png"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
dependencies = [
"bitflags 2.11.1",
"crc32fast",
"fdeflate",
"flate2",
"miniz_oxide",
]
[[package]]
name = "polling"
version = "3.11.0"
@@ -1969,6 +2021,12 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "pxfm"
version = "0.1.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f"
[[package]]
name = "quick-xml"
version = "0.39.2"
@@ -2641,7 +2699,7 @@ dependencies = [
"bytemuck",
"cfg-if",
"log",
"png",
"png 0.17.16",
"tiny-skia-path",
]
+1
View File
@@ -62,3 +62,4 @@ winit = { version = "0.29", default-features = false, features = ["rwh_05"
softbuffer = "0.3"
tiny-skia = "0.11"
fontdue = "0.8"
image = { version = "0.25", default-features = false, features = ["png"] }
+1
View File
@@ -38,3 +38,4 @@ winit = { version = "0.29", default-features = false, features = ["rwh_05"
softbuffer = "0.3"
tiny-skia = "0.11"
fontdue = "0.8"
image = { workspace = true }
+44
View File
@@ -4724,6 +4724,50 @@ fn dispatch_builtin(
unreachable!()
}
"canvas_image" => {
// canvas_image(path: String, x: Int, y: Int, w: Int, h: Int) -> Void
// Draws a PNG image scaled to w×h at (x, y) with alpha blending.
let draw_h = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as i32, _ => 0 };
let draw_w = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as i32, _ => 0 };
let dy = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as i32, _ => 0 };
let dx = match stack.pop().unwrap_or(Value::Nil) { Value::Int(n) => n as i32, _ => 0 };
let path = match stack.pop().unwrap_or(Value::Nil) { Value::Str(s) => s, _ => { stack.push(Value::Nil); return BuiltinResult::Handled; } };
if draw_w <= 0 || draw_h <= 0 { stack.push(Value::Nil); return BuiltinResult::Handled; }
use image::GenericImageView;
if let Ok(img) = image::open(&path) {
let img = img.resize_exact(draw_w as u32, draw_h as u32, image::imageops::FilterType::Lanczos3);
let rgba = img.to_rgba8();
CANVAS.with(|cv| {
let mut cv = cv.borrow_mut();
if let Some(px) = cv.pixmap.as_mut() {
let pw = px.width() as i32;
let ph = px.height() as i32;
let data = px.data_mut();
for row in 0..draw_h {
for col in 0..draw_w {
let ix = dx + col;
let iy = dy + row;
if ix < 0 || iy < 0 || ix >= pw || iy >= ph { continue; }
let p = rgba.get_pixel(col as u32, row as u32);
let sa = p[3] as u32;
if sa == 0 { continue; }
let ia = 255 - sa;
let idx = (iy as usize * pw as usize + ix as usize) * 4;
data[idx] = ((ia * data[idx] as u32 + sa * p[0] as u32) / 255) as u8;
data[idx + 1] = ((ia * data[idx+1] as u32 + sa * p[1] as u32) / 255) as u8;
data[idx + 2] = ((ia * data[idx+2] as u32 + sa * p[2] as u32) / 255) as u8;
data[idx + 3] = ((ia * data[idx+3] as u32 + sa * sa) / 255) as u8;
}
}
}
});
}
stack.push(Value::Nil);
BuiltinResult::Handled
}
_ => BuiltinResult::NotBuiltin,
}
}
+1
View File
@@ -278,6 +278,7 @@ impl TypeEnv {
env.functions.insert("canvas_events".into(), str_fn(vec![], s.clone()));
env.functions.insert("canvas_swap".into(), str_fn(vec![], Type::Void));
env.functions.insert("canvas_run_loop".into(), str_fn(vec![s.clone()], Type::Void));
env.functions.insert("canvas_image".into(), str_fn(vec![s.clone(), i.clone(), i.clone(), i.clone(), i.clone()], Type::Void));
env.functions.insert("state_set".into(), str_fn(vec![s.clone(), s.clone()], Type::Void));
env.functions.insert("state_get".into(), str_fn(vec![s.clone()], s.clone()));