Files
effectcraft/examples/plugins/posterize-bands/src/lib.rs
T
just_anshhandjustanshh 544eebf904 WebAssembly ABI 2: neighbouring frames for effect plug-ins (#2773) (#3329)
* WebAssembly ABI 2: neighbouring frames for effect plug-ins (#2773)

A wasm plug-in can export ec_api_version 2 and ec_render2 to receive the layer's previous and next frames, their layer times and validity flags. Neighbours come from PluginHost::input_at, are resampled onto the current grid, and are zeroed when absent. ABI 1 modules (ec_render) load and render unchanged; the manifest api stays independent. A layer with an ABI 2 plug-in keys the render cache on layer time; the Plug-in Library accepts ABI 1-2 releases. Docs, help text and tests updated.

* Fix ABI 2 neighbour times on NTSC frame boundaries

	ime * fps is often a few ulps off an integer at rates such as 24000/1001. The old boundary test then took the bracket branch and passed the current frame back as prev (or
ext). Treat a near-integer as a frame boundary within 1e-6 frames, and test 2400 frames at 24000/1001: every boundary must get the frames either side. Also document the loader helpers for the docstring coverage check.

---------

Co-authored-by: justanshh <294019246+justanshh@users.noreply.github.com>
2026-11-09 10:49:53 +01:00

94 lines
3.4 KiB
Rust

//! Posterize Bands: an example EffectCraft effect plug-in (WebAssembly ABI 1, see
//! `docs/plugins.md`). It quantizes each pixel's luminance into `levels` bands and mixes a tint
//! colour in by `mix` percent, optionally only above a `threshold`.
//!
//! Build: `cargo build --release --target wasm32-unknown-unknown`, then load
//! `target/wasm32-unknown-unknown/release/posterize_bands.wasm` with Effect ▸ Load Effect Plug-in
//! (or `effect.plugins.load {"path": …}`), or drop it in the settings folder's `Plug-ins`.
use std::cell::RefCell;
/// The manifest: id, name, Effects & Presets category, parameters (flattened in this order:
/// levels, mix, tint r g b a, invert).
const MANIFEST: &str = r#"{
"api": 1,
"id": "org.effectcraft.example.posterize-bands",
"name": "Posterize Bands",
"category": "Stylize",
"version": "1.0.0",
"author": "EffectCraft contributors",
"description": "Quantizes luminance into tinted bands.",
"params": [
{"id": "levels", "name": "Levels", "type": "slider", "default": 4, "min": 2, "max": 64, "sliderMax": 16, "decimals": 0},
{"id": "mix", "name": "Tint Amount", "type": "slider", "default": 50, "min": 0, "max": 100, "decimals": 1},
{"id": "tint", "name": "Tint", "type": "color", "default": [1.0, 0.55, 0.2, 1.0]},
{"id": "invert", "name": "Invert Bands", "type": "checkbox", "default": false}
]
}"#;
thread_local! {
static BUF: RefCell<Vec<u64>> = const { RefCell::new(Vec::new()) };
}
#[unsafe(no_mangle)]
pub extern "C" fn ec_api_version() -> i32 {
1
}
#[unsafe(no_mangle)]
pub extern "C" fn ec_manifest_ptr() -> i32 {
MANIFEST.as_ptr() as i32
}
#[unsafe(no_mangle)]
pub extern "C" fn ec_manifest_len() -> i32 {
MANIFEST.len() as i32
}
/// One reusable, 8-byte-aligned buffer for the parameters and pixels.
#[unsafe(no_mangle)]
pub extern "C" fn ec_alloc(bytes: i32) -> i32 {
BUF.with(|b| {
let mut b = b.borrow_mut();
let words = (bytes.max(0) as usize).div_ceil(8);
if b.len() < words {
b.resize(words, 0);
}
b.as_mut_ptr() as i32
})
}
/// # Safety
/// The host passes pointers into the buffer `ec_alloc` returned.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ec_render(pixels: i32, width: i32, height: i32, params: i32, nparams: i32, _time: f64, _scale: f64) -> i32 {
if nparams < 7 {
return 1;
}
let p = unsafe { std::slice::from_raw_parts(params as *const f64, nparams as usize) };
let px = unsafe { std::slice::from_raw_parts_mut(pixels as *mut [f32; 4], (width.max(0) * height.max(0)) as usize) };
let levels = p[0].round().max(2.0) as f32;
let mix = (p[1] / 100.0).clamp(0.0, 1.0) as f32;
let tint = [p[2] as f32, p[3] as f32, p[4] as f32];
let invert = p[6] != 0.0;
for c in px.iter_mut() {
let a = c[3];
if a <= 0.0 {
continue;
}
// Un-premultiply, band the luminance, re-premultiply.
let (r, g, b) = (c[0] / a, c[1] / a, c[2] / a);
let y = 0.2126 * r + 0.7152 * g + 0.0722 * b;
let mut band = (y * levels).floor().min(levels - 1.0) / (levels - 1.0);
if invert {
band = 1.0 - band;
}
for (i, ch) in [r, g, b].into_iter().enumerate() {
let banded = if y > 0.0 { ch * band / y.max(1e-6) } else { band };
let v = banded * (1.0 - mix) + band * tint[i] * mix;
c[i] = v.max(0.0) * a;
}
}
0
}