From d86bbc3740b88c3bf5cfdd03c8ef8563876cca70 Mon Sep 17 00:00:00 2001 From: Will Anderson Date: Tue, 28 Apr 2026 14:51:24 -0500 Subject: [PATCH] add canvas_image builtin for PNG rendering with alpha blending MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- Cargo.lock | 60 +++++++++++++++++++++++++++++++++++- Cargo.toml | 1 + bin/el/Cargo.toml | 1 + bin/el/src/main.rs | 44 ++++++++++++++++++++++++++ crates/el-types/src/types.rs | 1 + 5 files changed, 106 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index e3d7d65..b5182f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", ] diff --git a/Cargo.toml b/Cargo.toml index 309dcf3..a571fc2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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"] } diff --git a/bin/el/Cargo.toml b/bin/el/Cargo.toml index 8a15fb7..7546103 100644 --- a/bin/el/Cargo.toml +++ b/bin/el/Cargo.toml @@ -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 } diff --git a/bin/el/src/main.rs b/bin/el/src/main.rs index 6c6114f..df89b42 100644 --- a/bin/el/src/main.rs +++ b/bin/el/src/main.rs @@ -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, } } diff --git a/crates/el-types/src/types.rs b/crates/el-types/src/types.rs index cb21d15..d820092 100644 --- a/crates/el-types/src/types.rs +++ b/crates/el-types/src/types.rs @@ -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()));