| 1 | // Copyright © SixtyFPS GmbH <info@slint.dev> |
| 2 | // SPDX-License-Identifier: MIT |
| 3 | |
| 4 | #[cfg (target_arch = "wasm32" )] |
| 5 | use wasm_bindgen::prelude::*; |
| 6 | |
| 7 | slint::include_modules!(); |
| 8 | |
| 9 | use glow::HasContext; |
| 10 | |
| 11 | struct EGLUnderlay { |
| 12 | gl: glow::Context, |
| 13 | program: glow::Program, |
| 14 | effect_time_location: glow::UniformLocation, |
| 15 | rotation_time_location: glow::UniformLocation, |
| 16 | vbo: glow::Buffer, |
| 17 | vao: glow::VertexArray, |
| 18 | start_time: web_time::Instant, |
| 19 | } |
| 20 | |
| 21 | impl EGLUnderlay { |
| 22 | fn new(gl: glow::Context) -> Self { |
| 23 | unsafe { |
| 24 | let program = gl.create_program().expect("Cannot create program" ); |
| 25 | |
| 26 | let (vertex_shader_source, fragment_shader_source) = ( |
| 27 | r#"#version 100 |
| 28 | attribute vec2 position; |
| 29 | varying vec2 frag_position; |
| 30 | void main() { |
| 31 | frag_position = position; |
| 32 | gl_Position = vec4(position, 0.0, 1.0); |
| 33 | }"# , |
| 34 | r#"#version 100 |
| 35 | precision mediump float; |
| 36 | varying vec2 frag_position; |
| 37 | uniform float effect_time; |
| 38 | uniform float rotation_time; |
| 39 | |
| 40 | float roundRectDistance(vec2 pos, vec2 rect_size, float radius) |
| 41 | { |
| 42 | vec2 q = abs(pos) - rect_size + radius; |
| 43 | return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - radius; |
| 44 | } |
| 45 | |
| 46 | void main() { |
| 47 | vec2 size = vec2(0.4, 0.5) + 0.2 * cos(effect_time / 500. + vec2(0.3, 0.2)); |
| 48 | float radius = 0.5 * sin(effect_time / 300.); |
| 49 | float a = rotation_time / 800.0; |
| 50 | float d = roundRectDistance(mat2(cos(a), -sin(a), sin(a), cos(a)) * frag_position, size, radius); |
| 51 | vec3 col = (d > 0.0) ? vec3(sin(d * 0.2), 0.4 * cos(effect_time / 1000.0 + d * 0.8), sin(d * 1.2)) : vec3(0.2 * cos(d * 0.1), 0.17 * sin(d * 0.4), 0.96 * abs(sin(effect_time / 500. - d * 0.9))); |
| 52 | col *= 0.8 + 0.5 * sin(50.0 * d); |
| 53 | col = mix(col, vec3(0.9), 1.0 - smoothstep(0.0, 0.03, abs(d) )); |
| 54 | gl_FragColor = vec4(col, 1.0); |
| 55 | }"# , |
| 56 | ); |
| 57 | |
| 58 | let shader_sources = [ |
| 59 | (glow::VERTEX_SHADER, vertex_shader_source), |
| 60 | (glow::FRAGMENT_SHADER, fragment_shader_source), |
| 61 | ]; |
| 62 | |
| 63 | let mut shaders = Vec::with_capacity(shader_sources.len()); |
| 64 | |
| 65 | for (shader_type, shader_source) in shader_sources.iter() { |
| 66 | let shader = gl.create_shader(*shader_type).expect("Cannot create shader" ); |
| 67 | gl.shader_source(shader, shader_source); |
| 68 | gl.compile_shader(shader); |
| 69 | if !gl.get_shader_compile_status(shader) { |
| 70 | panic!("{}" , gl.get_shader_info_log(shader)); |
| 71 | } |
| 72 | gl.attach_shader(program, shader); |
| 73 | shaders.push(shader); |
| 74 | } |
| 75 | |
| 76 | gl.link_program(program); |
| 77 | if !gl.get_program_link_status(program) { |
| 78 | panic!("{}" , gl.get_program_info_log(program)); |
| 79 | } |
| 80 | |
| 81 | for shader in shaders { |
| 82 | gl.detach_shader(program, shader); |
| 83 | gl.delete_shader(shader); |
| 84 | } |
| 85 | |
| 86 | let effect_time_location = gl.get_uniform_location(program, "effect_time" ).unwrap(); |
| 87 | let rotation_time_location = gl.get_uniform_location(program, "rotation_time" ).unwrap(); |
| 88 | let position_location = gl.get_attrib_location(program, "position" ).unwrap(); |
| 89 | |
| 90 | let vbo = gl.create_buffer().expect("Cannot create buffer" ); |
| 91 | gl.bind_buffer(glow::ARRAY_BUFFER, Some(vbo)); |
| 92 | |
| 93 | let vertices = [-1.0f32, 1.0f32, -1.0f32, -1.0f32, 1.0f32, 1.0f32, 1.0f32, -1.0f32]; |
| 94 | |
| 95 | gl.buffer_data_u8_slice(glow::ARRAY_BUFFER, vertices.align_to().1, glow::STATIC_DRAW); |
| 96 | |
| 97 | let vao = gl.create_vertex_array().expect("Cannot create vertex array" ); |
| 98 | gl.bind_vertex_array(Some(vao)); |
| 99 | gl.enable_vertex_attrib_array(position_location); |
| 100 | gl.vertex_attrib_pointer_f32(position_location, 2, glow::FLOAT, false, 8, 0); |
| 101 | |
| 102 | gl.bind_buffer(glow::ARRAY_BUFFER, None); |
| 103 | gl.bind_vertex_array(None); |
| 104 | |
| 105 | Self { |
| 106 | gl, |
| 107 | program, |
| 108 | effect_time_location, |
| 109 | rotation_time_location, |
| 110 | vbo, |
| 111 | vao, |
| 112 | start_time: web_time::Instant::now(), |
| 113 | } |
| 114 | } |
| 115 | } |
| 116 | } |
| 117 | |
| 118 | impl Drop for EGLUnderlay { |
| 119 | fn drop(&mut self) { |
| 120 | unsafe { |
| 121 | self.gl.delete_program(self.program); |
| 122 | self.gl.delete_vertex_array(self.vao); |
| 123 | self.gl.delete_buffer(self.vbo); |
| 124 | } |
| 125 | } |
| 126 | } |
| 127 | |
| 128 | impl EGLUnderlay { |
| 129 | fn render(&mut self, rotation_enabled: bool) { |
| 130 | unsafe { |
| 131 | let gl = &self.gl; |
| 132 | |
| 133 | gl.use_program(Some(self.program)); |
| 134 | |
| 135 | // Retrieving the buffer with glow only works with native builds right now. For WASM this requires https://github.com/grovesNL/glow/pull/190 |
| 136 | // That means we can't properly restore the vao/vbo, but this is okay for now as this only works with femtovg, which doesn't rely on |
| 137 | // these bindings to persist across frames. |
| 138 | #[cfg (not(target_arch = "wasm32" ))] |
| 139 | let old_buffer = |
| 140 | std::num::NonZeroU32::new(gl.get_parameter_i32(glow::ARRAY_BUFFER_BINDING) as u32) |
| 141 | .map(glow::NativeBuffer); |
| 142 | #[cfg (target_arch = "wasm32" )] |
| 143 | let old_buffer = None; |
| 144 | |
| 145 | gl.bind_buffer(glow::ARRAY_BUFFER, Some(self.vbo)); |
| 146 | |
| 147 | #[cfg (not(target_arch = "wasm32" ))] |
| 148 | let old_vao = |
| 149 | std::num::NonZeroU32::new(gl.get_parameter_i32(glow::VERTEX_ARRAY_BINDING) as u32) |
| 150 | .map(glow::NativeVertexArray); |
| 151 | #[cfg (target_arch = "wasm32" )] |
| 152 | let old_vao = None; |
| 153 | |
| 154 | gl.bind_vertex_array(Some(self.vao)); |
| 155 | |
| 156 | let elapsed = self.start_time.elapsed().as_millis() as f32; |
| 157 | gl.uniform_1_f32(Some(&self.effect_time_location), elapsed); |
| 158 | gl.uniform_1_f32( |
| 159 | Some(&self.rotation_time_location), |
| 160 | if rotation_enabled { elapsed } else { 0.0 }, |
| 161 | ); |
| 162 | |
| 163 | gl.draw_arrays(glow::TRIANGLE_STRIP, 0, 4); |
| 164 | |
| 165 | gl.bind_buffer(glow::ARRAY_BUFFER, old_buffer); |
| 166 | gl.bind_vertex_array(old_vao); |
| 167 | gl.use_program(None); |
| 168 | } |
| 169 | } |
| 170 | } |
| 171 | |
| 172 | #[cfg_attr (target_arch = "wasm32" , wasm_bindgen(start))] |
| 173 | pub fn main() { |
| 174 | // This provides better error messages in debug mode. |
| 175 | // It's disabled in release mode so it doesn't bloat up the file size. |
| 176 | #[cfg (all(debug_assertions, target_arch = "wasm32" ))] |
| 177 | console_error_panic_hook::set_once(); |
| 178 | |
| 179 | slint::BackendSelector::new() |
| 180 | .require_opengl_es() |
| 181 | .select() |
| 182 | .expect("Unable to create Slint backend with OpenGL ES renderer" ); |
| 183 | |
| 184 | let app = App::new().unwrap(); |
| 185 | |
| 186 | let mut underlay = None; |
| 187 | |
| 188 | let app_weak = app.as_weak(); |
| 189 | |
| 190 | app.window() |
| 191 | .set_rendering_notifier(move |state, graphics_api| { |
| 192 | // eprintln!("rendering state {:#?}", state); |
| 193 | |
| 194 | match state { |
| 195 | slint::RenderingState::RenderingSetup => { |
| 196 | let context = match graphics_api { |
| 197 | #[cfg (not(target_arch = "wasm32" ))] |
| 198 | slint::GraphicsAPI::NativeOpenGL { get_proc_address } => unsafe { |
| 199 | glow::Context::from_loader_function_cstr(|s| get_proc_address(s)) |
| 200 | }, |
| 201 | #[cfg (target_arch = "wasm32" )] |
| 202 | slint::GraphicsAPI::WebGL { canvas_element_id, context_type } => { |
| 203 | use wasm_bindgen::JsCast; |
| 204 | |
| 205 | let canvas = web_sys::window() |
| 206 | .unwrap() |
| 207 | .document() |
| 208 | .unwrap() |
| 209 | .get_element_by_id(canvas_element_id) |
| 210 | .unwrap() |
| 211 | .dyn_into::<web_sys::HtmlCanvasElement>() |
| 212 | .unwrap(); |
| 213 | |
| 214 | let webgl1_context = canvas |
| 215 | .get_context(context_type) |
| 216 | .unwrap() |
| 217 | .unwrap() |
| 218 | .dyn_into::<web_sys::WebGl2RenderingContext>() |
| 219 | .unwrap(); |
| 220 | |
| 221 | glow::Context::from_webgl2_context(webgl1_context) |
| 222 | } |
| 223 | _ => return, |
| 224 | }; |
| 225 | underlay = Some(EGLUnderlay::new(context)) |
| 226 | } |
| 227 | slint::RenderingState::BeforeRendering => { |
| 228 | if let (Some(underlay), Some(app)) = (underlay.as_mut(), app_weak.upgrade()) { |
| 229 | underlay.render(app.get_rotation_enabled()); |
| 230 | app.window().request_redraw(); |
| 231 | } |
| 232 | } |
| 233 | slint::RenderingState::AfterRendering => {} |
| 234 | slint::RenderingState::RenderingTeardown => { |
| 235 | drop(underlay.take()); |
| 236 | } |
| 237 | _ => {} |
| 238 | } |
| 239 | }) |
| 240 | .expect("Unable to set rendering notifier" ); |
| 241 | |
| 242 | app.run().unwrap(); |
| 243 | } |
| 244 | |