use wasm_bindgen::{JsCast, JsValue};
use web_sys::{
WebGl2RenderingContext as Gl,
WebGlBuffer,
WebGlProgram,
WebGlShader,
WebGlUniformLocation,
};
thread_local! {
static FRAME: std::cell::RefCell<u32> = std::cell::RefCell::new(0);
}
const FLOATS_PER_VERT: usize = 5;
enum Batch {
Tri(Vec<f32>),
Line(Vec<f32>),
}
struct State {
ctx: Gl,
canvas: web_sys::OffscreenCanvas,
prog: WebGlProgram,
buf: WebGlBuffer,
u_size: WebGlUniformLocation,
batches: Vec<Batch>,
width: f32,
height: f32,
}
thread_local! {
static STATE: std::cell::RefCell<Option<State>> = std::cell::RefCell::new(None);
}
const VERT: &str = r#"#version 300 es
in vec2 aScreen;
in vec3 aColor;
uniform vec2 uSize;
out vec3 vColor;
void main() {
vec2 ndc = (aScreen / uSize) * 2.0 - 1.0;
ndc.y = -ndc.y;
gl_Position = vec4(ndc, 0.0, 1.0);
vColor = aColor;
}
"#;
const FRAG: &str = r#"#version 300 es
precision mediump float;
in vec3 vColor;
out vec4 fragColor;
void main() {
fragColor = vec4(vColor, 1.0);
}
"#;
fn compile_shader(ctx: &Gl, kind: u32, src: &str) -> WebGlShader {
let sh = ctx.create_shader(kind).expect("create_shader");
ctx.shader_source(&sh, src);
ctx.compile_shader(&sh);
assert!(
ctx.get_shader_parameter(&sh, Gl::COMPILE_STATUS)
.as_bool().unwrap_or(false),
"shader compile: {}",
ctx.get_shader_info_log(&sh).unwrap_or_default()
);
sh
}
fn link_program(ctx: &Gl, vs: &WebGlShader, fs: &WebGlShader) -> WebGlProgram {
let prog = ctx.create_program().expect("create_program");
ctx.attach_shader(&prog, vs);
ctx.attach_shader(&prog, fs);
ctx.link_program(&prog);
assert!(
ctx.get_program_parameter(&prog, Gl::LINK_STATUS)
.as_bool().unwrap_or(false),
"program link: {}",
ctx.get_program_info_log(&prog).unwrap_or_default()
);
prog
}
pub fn init(canvas: web_sys::OffscreenCanvas) {
let ctx: Gl = canvas
.get_context("webgl2")
.expect("get_context call")
.expect("webgl2 unavailable")
.dyn_into::<Gl>()
.expect("cast to WebGl2RenderingContext");
let vs = compile_shader(&ctx, Gl::VERTEX_SHADER, VERT);
let fs = compile_shader(&ctx, Gl::FRAGMENT_SHADER, FRAG);
let prog = link_program(&ctx, &vs, &fs);
let buf = ctx.create_buffer().expect("create_buffer");
ctx.disable(Gl::DEPTH_TEST);
let u_size = ctx.get_uniform_location(&prog, "uSize")
.expect("uniform uSize not found");
let width = canvas.width() as f32;
let height = canvas.height() as f32;
ctx.viewport(0, 0, canvas.width() as i32, canvas.height() as i32);
STATE.with(|cell| {
*cell.borrow_mut() = Some(State {
ctx, canvas, prog, buf, u_size,
batches: Vec::new(),
width, height,
});
});
}
pub fn canvas_size() -> (u32, u32) {
STATE.with(|cell| {
cell.borrow().as_ref()
.map(|s| (s.canvas.width(), s.canvas.height()))
.unwrap_or((800, 600))
})
}
pub fn resize(width: u32, height: u32) {
STATE.with(|cell| {
if let Some(s) = cell.borrow_mut().as_mut() {
s.canvas.set_width(width);
s.canvas.set_height(height);
s.width = width as f32;
s.height = height as f32;
s.ctx.viewport(0, 0, width as i32, height as i32);
}
});
}
#[inline]
fn unpack(color: u32) -> (f32, f32, f32) {
(
((color >> 16) & 0xFF) as f32 / 255.0,
((color >> 8) & 0xFF) as f32 / 255.0,
( color & 0xFF) as f32 / 255.0,
)
}
pub fn push_triangle(
color: u32,
x0: f32, y0: f32,
x1: f32, y1: f32,
x2: f32, y2: f32,
_depth: f32,
) {
STATE.with(|cell| {
if let Some(s) = cell.borrow_mut().as_mut() {
let (r, g, b) = unpack(color);
let verts = [x0,y0,r,g,b, x1,y1,r,g,b, x2,y2,r,g,b];
match s.batches.last_mut() {
Some(Batch::Tri(v)) => v.extend_from_slice(&verts),
_ => s.batches.push(Batch::Tri(verts.to_vec())),
}
}
});
}
pub fn push_line(
color: u32,
x0: f32, y0: f32,
x1: f32, y1: f32,
_depth: f32,
) {
STATE.with(|cell| {
if let Some(s) = cell.borrow_mut().as_mut() {
let (r, g, b) = unpack(color);
let verts = [x0,y0,r,g,b, x1,y1,r,g,b];
match s.batches.last_mut() {
Some(Batch::Line(v)) => v.extend_from_slice(&verts),
_ => s.batches.push(Batch::Line(verts.to_vec())),
}
}
});
}
fn upload_and_draw(
ctx: &Gl,
prog: &WebGlProgram,
buf: &WebGlBuffer,
u_size: &WebGlUniformLocation,
verts: &[f32],
w: f32, h: f32,
mode: u32,
) {
ctx.bind_buffer(Gl::ARRAY_BUFFER, Some(buf));
unsafe {
let arr = js_sys::Float32Array::view(verts);
ctx.buffer_data_with_array_buffer_view(Gl::ARRAY_BUFFER, &arr, Gl::DYNAMIC_DRAW);
}
ctx.use_program(Some(prog));
ctx.uniform2f(Some(u_size), w, h);
let stride = (FLOATS_PER_VERT * 4) as i32;
let loc_screen = ctx.get_attrib_location(prog, "aScreen") as u32;
let loc_color = ctx.get_attrib_location(prog, "aColor") as u32;
ctx.enable_vertex_attrib_array(loc_screen);
ctx.vertex_attrib_pointer_with_i32(loc_screen, 2, Gl::FLOAT, false, stride, 0);
ctx.enable_vertex_attrib_array(loc_color);
ctx.vertex_attrib_pointer_with_i32(loc_color, 3, Gl::FLOAT, false, stride, 8);
ctx.draw_arrays(mode, 0, (verts.len() / FLOATS_PER_VERT) as i32);
}
pub fn flush(fill_r: f32, fill_g: f32, fill_b: f32, width: usize, height: usize) {
STATE.with(|cell| {
let mut opt = cell.borrow_mut();
if let Some(s) = opt.as_mut() {
let ctx = &s.ctx;
ctx.clear_color(fill_r, fill_g, fill_b, 1.0);
ctx.clear(Gl::COLOR_BUFFER_BIT);
let w = width as f32;
let h = height as f32;
let batches = std::mem::take(&mut s.batches);
FRAME.with(|fc| {
let n = *fc.borrow();
if n < 3 {
web_sys::console::log_1(&JsValue::from_str(&format!(
"[ling webgl] frame={n} batches={} canvas={}x{} fill=({fill_r:.2},{fill_g:.2},{fill_b:.2})",
batches.len(), w as u32, h as u32,
)));
}
*fc.borrow_mut() = n + 1;
});
for batch in &batches {
match batch {
Batch::Tri(verts) =>
upload_and_draw(ctx, &s.prog, &s.buf, &s.u_size, verts, w, h, Gl::TRIANGLES),
Batch::Line(verts) =>
upload_and_draw(ctx, &s.prog, &s.buf, &s.u_size, verts, w, h, Gl::LINES),
}
}
ctx.flush();
}
});
}