├── Cargo.toml ├── README.md ├── index.html └── src ├── data.rs └── lib.rs /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wasm-pong" 3 | version = "0.2.0" 4 | authors = ["Teemu Erkkola "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | wasm-bindgen = "0.2.43" 12 | js-sys = "0.3.20" 13 | gloo = "0.3.0" 14 | 15 | [dependencies.web-sys] 16 | version = "0.3.20" 17 | features = [ 18 | 'AudioContext', 19 | 'AudioBuffer', 20 | 'AudioNode', 21 | 'AudioBufferSourceNode', 22 | 'AudioDestinationNode', 23 | 'Document', 24 | 'Element', 25 | 'HtmlCanvasElement', 26 | 'KeyboardEvent', 27 | 'Node', 28 | 'WebGlBuffer', 29 | 'WebGlRenderingContext', 30 | 'WebGlProgram', 31 | 'WebGlShader', 32 | 'WebGlUniformLocation', 33 | 'WebGlTexture', 34 | 'Window', 35 | ] 36 | 37 | [profile.release] 38 | opt-level = 'z' 39 | lto = true 40 | panic = 'abort' 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wasm-pong-rs 2 | WebAssembly Pong in Rust 3 | 4 | ## Compiling 5 | 6 | Use [wasm-pack](https://github.com/rustwasm/wasm-pack): `wasm-pack build --target=web` 7 | 8 | ## Running 9 | 10 | Open `index.html`. 11 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 |
10 | 0 11 | 0 12 |
13 | 14 |
15 | 16 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/data.rs: -------------------------------------------------------------------------------- 1 | pub const VERTEX_SHADER: &str = 2 | "attribute vec2 a_position; 3 | attribute vec2 a_texcoord; 4 | uniform vec4 u_offset; 5 | varying mediump vec2 v_texcoord; 6 | void main() { 7 | gl_Position = vec4(a_position, 1.0, 1.0) + u_offset; 8 | v_texcoord = a_texcoord; 9 | }"; 10 | 11 | pub const FRAGMENT_SHADER: &str = 12 | "precision mediump float; 13 | varying mediump vec2 v_texcoord; 14 | uniform sampler2D u_sampler; 15 | uniform float u_opacity; 16 | void main() { 17 | vec4 color = texture2D(u_sampler, v_texcoord); 18 | color.a *= u_opacity; 19 | gl_FragColor = color; 20 | }"; 21 | 22 | pub const BALL_VERTICES: [f32; 2*3*4] = [ 23 | -0.05,-0.05,0.0,0.0, 0.05,0.05,1.0,1.0, -0.05,0.05,0.0,1.0, 24 | -0.05,-0.05,0.0,0.0, 0.05,-0.05,1.0,0.0, 0.05,0.05,1.0,1.0 25 | ]; 26 | 27 | pub const BALL_TEXTURE: [u8; 4*4*4] = [ 28 | 0x00,0x00,0x00,0x00, 0xDD,0x00,0x00,0xFF, 0xDD,0x00,0x00,0xFF, 0x00,0x00,0x00,0x00, 29 | 0xDD,0x00,0x00,0xFF, 0xFF,0x00,0x00,0xFF, 0xFF,0x44,0x44,0xFF, 0xDD,0x00,0x00,0xFF, 30 | 0xDD,0x00,0x00,0xFF, 0xFF,0x44,0x44,0xFF, 0xFF,0x88,0x88,0xFF, 0xDD,0x00,0x00,0xFF, 31 | 0x00,0x00,0x00,0x00, 0xDD,0x00,0x00,0xFF, 0xDD,0x00,0x00,0xFF, 0x00,0x00,0x00,0x00 32 | ]; 33 | pub const BALL_TAIL_VERTICES: [f32; 2*3*4] = [ 34 | -0.04,-0.04,0.0,0.0, 0.04,0.04,1.0,1.0, -0.04,0.04,0.0,1.0, 35 | -0.04,-0.04,0.0,0.0, 0.04,-0.04,1.0,0.0, 0.04,0.04,1.0,1.0 36 | ]; 37 | 38 | pub const BALL_TAIL_TEXTURE: [u8; 4*4*4] = [ 39 | 0x00,0x00,0x00,0x00, 0xFF,0x00,0x00,0xCC, 0xFF,0x00,0x00,0xCC, 0x00,0x00,0x00,0x00, 40 | 0xFF,0x00,0x00,0xCC, 0xFF,0x00,0x00,0xCC, 0xFF,0x00,0x00,0xCC, 0xFF,0x00,0x00,0xCC, 41 | 0xFF,0x00,0x00,0xCC, 0xFF,0x00,0x00,0xCC, 0xFF,0x00,0x00,0xCC, 0xFF,0x00,0x00,0xCC, 42 | 0x00,0x00,0x00,0x00, 0xFF,0x00,0x00,0xCC, 0xFF,0x00,0x00,0xCC, 0x00,0x00,0x00,0x00 43 | ]; 44 | 45 | pub const PADDLE_VERTICES: [f32; 2*3*4] = [ 46 | -0.05,-0.20,0.0,0.0, 0.05,0.20,1.0,1.0, -0.05,0.20,0.0,1.0, 47 | -0.05,-0.20,0.0,0.0, 0.05,-0.20,1.0,0.0, 0.05,0.20,1.0,1.0 48 | ]; 49 | 50 | pub const PADDLE_TEXTURE: [u8; 8*8*4] = [ 51 | 0x00,0xDD,0x00,0xFF, 0x00,0xEE,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xEE,0x00,0xFF, 0x00,0xDD,0x00,0xFF, 52 | 0x00,0xDD,0x00,0xFF, 0x00,0xEE,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xEE,0x00,0xFF, 0x00,0xDD,0x00,0xFF, 53 | 0x00,0xDD,0x00,0xFF, 0x00,0xEE,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xEE,0x00,0xFF, 0x00,0xDD,0x00,0xFF, 54 | 0x00,0xDD,0x00,0xFF, 0x00,0xEE,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xEE,0x00,0xFF, 0x00,0xDD,0x00,0xFF, 55 | 0x00,0xDD,0x00,0xFF, 0x00,0xEE,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xEE,0x00,0xFF, 0x00,0xDD,0x00,0xFF, 56 | 0x00,0xDD,0x00,0xFF, 0x00,0xEE,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xEE,0x00,0xFF, 0x00,0xDD,0x00,0xFF, 57 | 0x00,0xDD,0x00,0xFF, 0x00,0xEE,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xEE,0x00,0xFF, 0x00,0xDD,0x00,0xFF, 58 | 0x00,0xDD,0x00,0xFF, 0x00,0xEE,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xEE,0x00,0xFF, 0x00,0xDD,0x00,0xFF, 59 | ]; 60 | 61 | pub const FIELD_VERTICES: [f32; 2*3*4] = [ 62 | -1.0,-1.0,0.0,0.0, 1.0, 1.0,1.0,1.0, -1.0,1.0,0.0,1.0, 63 | -1.0,-1.0,0.0,0.0, 1.0,-1.0,1.0,0.0, 1.0,1.0,1.0,1.0 64 | ]; 65 | 66 | pub const FIELD_TEXTURE: [u8; 8*8*4] = [ 67 | 0x33,0x33,0x33,0xFF, 0x22,0x22,0x22,0xFF, 0x11,0x11,0x11,0xFF, 0x66,0x66,0x66,0xFF, 0x55,0x55,0x55,0xFF, 0x11,0x11,0x11,0xFF, 0x22,0x22,0x22,0xFF, 0x33,0x33,0x33,0xFF, 68 | 0x33,0x33,0x33,0xFF, 0x22,0x22,0x22,0xFF, 0x11,0x11,0x11,0xFF, 0x55,0x55,0x55,0xFF, 0x66,0x66,0x66,0xFF, 0x11,0x11,0x11,0xFF, 0x22,0x22,0x22,0xFF, 0x33,0x33,0x33,0xFF, 69 | 0x33,0x33,0x33,0xFF, 0x22,0x22,0x22,0xFF, 0x11,0x11,0x11,0xFF, 0x66,0x66,0x66,0xFF, 0x55,0x55,0x55,0xFF, 0x11,0x11,0x11,0xFF, 0x22,0x22,0x22,0xFF, 0x33,0x33,0x33,0xFF, 70 | 0x33,0x33,0x33,0xFF, 0x22,0x22,0x22,0xFF, 0x11,0x11,0x11,0xFF, 0x55,0x55,0x55,0xFF, 0x66,0x66,0x66,0xFF, 0x11,0x11,0x11,0xFF, 0x22,0x22,0x22,0xFF, 0x33,0x33,0x33,0xFF, 71 | 0x33,0x33,0x33,0xFF, 0x22,0x22,0x22,0xFF, 0x11,0x11,0x11,0xFF, 0x66,0x66,0x66,0xFF, 0x55,0x55,0x55,0xFF, 0x11,0x11,0x11,0xFF, 0x22,0x22,0x22,0xFF, 0x33,0x33,0x33,0xFF, 72 | 0x33,0x33,0x33,0xFF, 0x22,0x22,0x22,0xFF, 0x11,0x11,0x11,0xFF, 0x55,0x55,0x55,0xFF, 0x66,0x66,0x66,0xFF, 0x11,0x11,0x11,0xFF, 0x22,0x22,0x22,0xFF, 0x33,0x33,0x33,0xFF, 73 | 0x33,0x33,0x33,0xFF, 0x22,0x22,0x22,0xFF, 0x11,0x11,0x11,0xFF, 0x66,0x66,0x66,0xFF, 0x55,0x55,0x55,0xFF, 0x11,0x11,0x11,0xFF, 0x22,0x22,0x22,0xFF, 0x33,0x33,0x33,0xFF, 74 | 0x33,0x33,0x33,0xFF, 0x22,0x22,0x22,0xFF, 0x11,0x11,0x11,0xFF, 0x55,0x55,0x55,0xFF, 0x66,0x66,0x66,0xFF, 0x11,0x11,0x11,0xFF, 0x22,0x22,0x22,0xFF, 0x33,0x33,0x33,0xFF, 75 | ]; 76 | 77 | pub const SPARK_VERTICES: [f32; 2*3*4] = [ 78 | -0.01,-0.01,0.0,0.0, 0.01, 0.01,1.0,1.0,-0.01,0.01,0.0,1.0, 79 | -0.01,-0.01,0.0,0.0, 0.01,-0.01,1.0,0.0, 0.01,0.01,1.0,1.0 80 | ]; 81 | 82 | pub const SPARK_TEXTURE: [u8; 4*4*4] = [ 83 | 0x00,0x00,0x00,0x00, 0xFF,0xFF,0x00,0xFF, 0xFF,0xFF,0x00,0xFF, 0x00,0x00,0x00,0x00, 84 | 0xFF,0xFF,0x00,0xFF, 0xFF,0xFF,0x88,0xFF, 0xFF,0xFF,0x88,0xFF, 0xFF,0xFF,0x00,0xFF, 85 | 0xFF,0xFF,0x00,0xFF, 0xFF,0xFF,0x88,0xFF, 0xFF,0xFF,0x88,0xFF, 0xFF,0xFF,0x00,0xFF, 86 | 0x00,0x00,0x00,0x00, 0xFF,0xFF,0x00,0xFF, 0xFF,0xFF,0x00,0xFF, 0x00,0x00,0x00,0x00, 87 | ]; 88 | 89 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | mod data; 2 | 3 | use js_sys::WebAssembly; 4 | use wasm_bindgen::prelude::*; 5 | use wasm_bindgen::JsCast; 6 | use web_sys::{ 7 | WebGlProgram, WebGlUniformLocation, WebGlRenderingContext, WebGlShader, 8 | WebGlTexture, WebGlBuffer, AudioContext, AudioBuffer, HtmlCanvasElement, 9 | KeyboardEvent, Event 10 | }; 11 | use gloo::{ 12 | render::{request_animation_frame, AnimationFrame}, 13 | events::EventListener 14 | }; 15 | 16 | // Convenience alias for referring to OpenGL constants 17 | type GL = WebGlRenderingContext; 18 | 19 | const PADDLE_SPEED: f32 = 0.001; 20 | const BALL_SPEED: f32 = 0.0012; 21 | 22 | const AUDIO_BUFFER_SIZE: usize = 8192; 23 | type WebGlVertexArray = i32; 24 | 25 | struct RenderContext { 26 | gl: WebGlRenderingContext, 27 | program: WebGlProgram, 28 | position: WebGlVertexArray, 29 | texcoord: WebGlVertexArray, 30 | offset: WebGlUniformLocation, 31 | sampler: WebGlUniformLocation, 32 | opacity: WebGlUniformLocation, 33 | } 34 | 35 | #[derive(Clone)] 36 | struct Vec2 { 37 | x: f32, 38 | y: f32 39 | } 40 | 41 | struct Model { 42 | vertex_buffer: WebGlBuffer, 43 | num_vertices: u32, 44 | texture: WebGlTexture, 45 | extent: Vec2 46 | } 47 | 48 | struct Ball { 49 | position: Vec2, 50 | velocity: Vec2 51 | } 52 | 53 | struct Paddle { 54 | position: Vec2, 55 | up: bool, 56 | down: bool, 57 | } 58 | 59 | struct Particle { 60 | position: Vec2, 61 | velocity: Vec2, 62 | acceleration: Vec2, 63 | life: i32, 64 | total_life: i32 65 | } 66 | 67 | struct ParticleSystem { 68 | max_particles: usize, 69 | particles: Vec, 70 | } 71 | 72 | struct Pong { 73 | ctx: RenderContext, 74 | audio_ctx: AudioContext, 75 | audio_buffer: AudioBuffer, 76 | 77 | request_animation_frame_handle: AnimationFrame, 78 | _key_down_event_listener_handle: EventListener, 79 | _key_up_event_listener_handle: EventListener, 80 | 81 | timestamp: i32, 82 | 83 | ball_model: Model, 84 | ball_tail_model: Model, 85 | paddle_model: Model, 86 | spark_model: Model, 87 | field_model: Model, 88 | 89 | beep: Vec, 90 | boop: Vec, 91 | bloop: Vec, 92 | 93 | ball: Ball, 94 | left: Paddle, 95 | right: Paddle, 96 | 97 | ball_tail: ParticleSystem, 98 | sparks: ParticleSystem, 99 | 100 | left_score: u32, 101 | right_score: u32 102 | } 103 | 104 | static mut PONG: Option = None; 105 | 106 | #[wasm_bindgen(start)] 107 | pub fn start() -> Result<(), JsValue> { 108 | let window = web_sys::window().unwrap(); 109 | let document = window.document().unwrap(); 110 | let canvas: HtmlCanvasElement = document.get_element_by_id("canvas").unwrap().dyn_into()?; 111 | let ctx_options = js_sys::Object::new(); 112 | js_sys::Reflect::set(&ctx_options, &"alpha".into(), &false.into()).unwrap(); 113 | let gl: WebGlRenderingContext = canvas 114 | .get_context_with_context_options("webgl", &ctx_options)?.unwrap().dyn_into()?; 115 | 116 | gl.clear_color(0.1, 0.1, 0.1, 1.0); 117 | gl.enable(GL::DEPTH_TEST); 118 | gl.enable(GL::BLEND); 119 | gl.depth_func(GL::LEQUAL); 120 | gl.blend_func(GL::SRC_ALPHA, GL::ONE_MINUS_SRC_ALPHA); 121 | gl.clear(GL::COLOR_BUFFER_BIT | GL::DEPTH_BUFFER_BIT); 122 | 123 | let vert_shader = compile_shader(&gl, GL::VERTEX_SHADER, data::VERTEX_SHADER)?; 124 | let frag_shader = compile_shader(&gl, GL::FRAGMENT_SHADER, data::FRAGMENT_SHADER)?; 125 | let program = link_program(&gl, &vert_shader, &frag_shader)?; 126 | gl.use_program(Some(&program)); 127 | 128 | let ctx = RenderContext::new(gl, program); 129 | 130 | let ball_texture = ctx.load_texture(&data::BALL_TEXTURE, 4, 4); 131 | let ball_tail_texture = ctx.load_texture(&data::BALL_TAIL_TEXTURE, 4, 4); 132 | let spark_texture = ctx.load_texture(&data::SPARK_TEXTURE, 4, 4); 133 | let paddle_texture = ctx.load_texture(&data::PADDLE_TEXTURE, 8, 8); 134 | let field_texture = ctx.load_texture(&data::FIELD_TEXTURE, 8, 8); 135 | 136 | let ball_model = Model::new(&ctx, &data::BALL_VERTICES, ball_texture); 137 | let ball_tail_model = Model::new(&ctx, &data::BALL_TAIL_VERTICES, ball_tail_texture); 138 | let spark_model = Model::new(&ctx, &data::SPARK_VERTICES, spark_texture); 139 | let paddle_model = Model::new(&ctx, &data::PADDLE_VERTICES, paddle_texture); 140 | let field_model = Model::new(&ctx, &data::FIELD_VERTICES, field_texture); 141 | 142 | let mut beep: Vec = Vec::with_capacity(AUDIO_BUFFER_SIZE); 143 | let mut boop: Vec = Vec::with_capacity(AUDIO_BUFFER_SIZE); 144 | let mut bloop: Vec = Vec::with_capacity(AUDIO_BUFFER_SIZE); 145 | 146 | for i in 0..AUDIO_BUFFER_SIZE { 147 | let sq64 = if i/64 % 2 == 0 { 0.1 } else { -0.1 }; 148 | let sq128 = if i/128 % 2 == 0 { 0.1 } else { -0.1 }; 149 | beep.push(sq64); 150 | boop.push(sq128); 151 | bloop.push(sq64 + sq128); 152 | } 153 | 154 | let audio_ctx = AudioContext::new().unwrap(); 155 | let audio_buffer = audio_ctx.create_buffer( 156 | 1, (audio_ctx.sample_rate() * 2.0) as u32, audio_ctx.sample_rate()).unwrap(); 157 | 158 | let request_animation_frame_handle = request_animation_frame(on_animation_frame); 159 | 160 | let _key_down_event_listener_handle = EventListener::new(&document, "keydown", |e: &Event| { 161 | if let Some(e) = e.dyn_ref::() { 162 | on_key(e.key_code(), true); 163 | } 164 | }); 165 | 166 | let _key_up_event_listener_handle = EventListener::new(&document, "keyup", |e: &Event| { 167 | if let Some(e) = e.dyn_ref::() { 168 | on_key(e.key_code(), false); 169 | } 170 | }); 171 | 172 | unsafe { 173 | PONG = Some(Pong { 174 | ctx, audio_ctx, audio_buffer, 175 | timestamp: 0, 176 | request_animation_frame_handle, 177 | _key_down_event_listener_handle, 178 | _key_up_event_listener_handle, 179 | 180 | ball_model, ball_tail_model, paddle_model, spark_model, field_model, 181 | beep, boop, bloop, 182 | 183 | ball: Ball { 184 | position: Vec2::zero(), 185 | velocity: Vec2::new(1.0, 1.0), 186 | }, 187 | left: Paddle { 188 | position: Vec2::new(-0.9, 0.0), 189 | up: false, down: false 190 | }, 191 | right: Paddle { 192 | position: Vec2::new(0.9, 0.0), 193 | up: false, down: false 194 | }, 195 | ball_tail: ParticleSystem::new(100), 196 | sparks: ParticleSystem::new(100), 197 | 198 | left_score: 0, 199 | right_score: 0 200 | }); 201 | } 202 | 203 | Ok(()) 204 | } 205 | 206 | pub fn on_animation_frame(timestamp: f64) { 207 | let timestamp = timestamp as i32; 208 | let pong = unsafe { PONG.as_mut().unwrap() }; 209 | let delta = match pong.timestamp { 210 | 0 => 1, 211 | x => timestamp - x 212 | } as f32; 213 | pong.timestamp = timestamp; 214 | 215 | let left = &mut pong.left; 216 | let right = &mut pong.right; 217 | 218 | let left_direction = if left.up == left.down { 0. } else if left.up { 1. } else { -1. }; 219 | let right_direction = if right.up == right.down { 0. } else if right.up { 1. } else { -1. }; 220 | left.position.y = clamp(left.position.y + left_direction * PADDLE_SPEED * delta, -0.8, 0.8); 221 | right.position.y = clamp(right.position.y + right_direction * PADDLE_SPEED * delta, -0.8, 0.8); 222 | 223 | let ball = &mut pong.ball; 224 | ball.position.x += ball.velocity.x * delta * BALL_SPEED; 225 | if collide(&ball.position, &pong.ball_model.extent, 226 | &left.position, &pong.paddle_model.extent) 227 | || collide(&ball.position, &pong.ball_model.extent, 228 | &right.position, &pong.paddle_model.extent) { 229 | ball.velocity.x = -ball.velocity.x; 230 | ball.position.x += ball.velocity.x * delta * BALL_SPEED; 231 | play_audio(&pong.beep); 232 | create_sparks(&mut pong.sparks, 233 | (if ball.velocity.x > 0. { -1. } else { 1. }) 234 | * pong.ball_model.extent.x + ball.position.x, 235 | ball.position.y, 2. * ball.velocity.x, 0.); 236 | } else if ball.position.x.abs() > 1.05 { 237 | if ball.position.x > 0.0 { 238 | pong.left_score += 1; 239 | } else { 240 | pong.right_score += 1; 241 | } 242 | 243 | ball.position.x = 0.0; 244 | ball.velocity.x = (1 - 2 * (timestamp % 2)) as f32; 245 | ball.velocity.y = (1 - 2 * ((timestamp/7) % 2)) as f32; 246 | play_audio(&pong.bloop); 247 | set_score(pong.left_score, pong.right_score); 248 | } 249 | 250 | ball.position.y += ball.velocity.y * delta as f32 * BALL_SPEED; 251 | if collide(&ball.position, &pong.ball_model.extent, 252 | &left.position, &pong.paddle_model.extent) 253 | || collide(&ball.position, &pong.ball_model.extent, 254 | &right.position, &pong.paddle_model.extent) { 255 | ball.velocity.y = -ball.velocity.y; 256 | ball.position.y += ball.velocity.y * delta as f32 * BALL_SPEED; 257 | play_audio(&pong.beep); 258 | create_sparks(&mut pong.sparks, ball.position.x, 259 | (if ball.velocity.y > 0. { -1. } else { 1. }) 260 | * pong.ball_model.extent.y + ball.position.y, 261 | 0., 2. * ball.velocity.y); 262 | } else if ball.position.y > 0.95 { 263 | ball.velocity.y = -ball.velocity.y.abs(); 264 | play_audio(&pong.boop); 265 | create_sparks(&mut pong.sparks, ball.position.x, 266 | pong.ball_model.extent.y + ball.position.y, 267 | 0., 2. * ball.velocity.y); 268 | } else if ball.position.y < -0.95 { 269 | ball.velocity.y = ball.velocity.y.abs(); 270 | play_audio(&pong.boop); 271 | create_sparks(&mut pong.sparks, ball.position.x, 272 | -pong.ball_model.extent.y + ball.position.y, 273 | 0., 2. * ball.velocity.y); 274 | } 275 | 276 | pong.ball_tail.add(ball.position.clone(), Vec2::zero(), Vec2::zero(), 1000); 277 | pong.ball_tail.update(delta); 278 | pong.sparks.update(delta); 279 | 280 | pong.ctx.gl.clear(GL::COLOR_BUFFER_BIT | GL::DEPTH_BUFFER_BIT); 281 | pong.ctx.gl.use_program(Some(&pong.ctx.program)); 282 | pong.ctx.gl.enable_vertex_attrib_array(pong.ctx.position as u32); 283 | pong.ctx.gl.enable_vertex_attrib_array(pong.ctx.texcoord as u32); 284 | 285 | pong.field_model.pre_render(&pong.ctx); 286 | pong.field_model.render(&Vec2 {x: 0.0, y: 0.0}, &pong.ctx); 287 | 288 | pong.ball_tail.render(&pong.ball_tail_model, &pong.ctx); 289 | pong.ball_model.pre_render(&pong.ctx); 290 | pong.ball_model.render(&ball.position, &pong.ctx); 291 | 292 | pong.paddle_model.pre_render(&pong.ctx); 293 | pong.paddle_model.render(&left.position, &pong.ctx); 294 | pong.paddle_model.render(&right.position, &pong.ctx); 295 | 296 | pong.sparks.render(&pong.spark_model, &pong.ctx); 297 | 298 | pong.request_animation_frame_handle = request_animation_frame(on_animation_frame); 299 | } 300 | 301 | pub fn on_key(key: u32, state: bool) { 302 | const KEY_UP: u32 = 38; 303 | const KEY_DOWN: u32 = 40; 304 | const KEY_A: u32 = 65; 305 | const KEY_Z: u32 = 90; 306 | 307 | let pong = unsafe { PONG.as_mut().unwrap() }; 308 | 309 | match key { 310 | KEY_UP => pong.right.up = state, 311 | KEY_DOWN => pong.right.down = state, 312 | KEY_A => pong.left.up = state, 313 | KEY_Z => pong.left.down = state, 314 | _ => () 315 | }; 316 | } 317 | 318 | impl Vec2 { 319 | fn zero() -> Vec2 { 320 | Vec2 { x: 0.0, y: 0.0 } 321 | } 322 | fn new(x: f32, y: f32) -> Vec2 { 323 | Vec2 { x, y } 324 | } 325 | } 326 | 327 | impl RenderContext { 328 | fn new(gl: WebGlRenderingContext, program: WebGlProgram) -> RenderContext { 329 | let position = gl.get_attrib_location(&program, "a_position"); 330 | let texcoord = gl.get_attrib_location(&program, "a_texcoord"); 331 | let offset = gl.get_uniform_location(&program, "u_offset").unwrap(); 332 | let sampler = gl.get_uniform_location(&program, "u_sampler").unwrap(); 333 | let opacity = gl.get_uniform_location(&program, "u_opacity").unwrap(); 334 | RenderContext { 335 | gl, program, position, texcoord, 336 | offset, sampler, opacity 337 | } 338 | } 339 | fn load_texture(&self, data: &[u8], width: i32, height: i32) -> WebGlTexture { 340 | let texture = self.gl.create_texture().unwrap(); 341 | self.gl.active_texture(GL::TEXTURE0); 342 | self.gl.bind_texture(GL::TEXTURE_2D, Some(&texture)); 343 | self.gl.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_u8_array( 344 | GL::TEXTURE_2D, 0, GL::RGBA as i32, 345 | width, height, 0, GL::RGBA, 346 | GL::UNSIGNED_BYTE, 347 | Some(data)).unwrap(); 348 | self.gl.tex_parameteri(GL::TEXTURE_2D, GL::TEXTURE_MAG_FILTER, GL::NEAREST as i32); 349 | self.gl.tex_parameteri(GL::TEXTURE_2D, GL::TEXTURE_MIN_FILTER, GL::NEAREST as i32); 350 | texture 351 | } 352 | } 353 | 354 | impl Model { 355 | fn new(ctx: &RenderContext, vertices: &[f32], texture: WebGlTexture) -> Model { 356 | let vertex_buffer = ctx.gl.create_buffer().unwrap(); 357 | ctx.gl.bind_buffer(WebGlRenderingContext::ARRAY_BUFFER, Some(&vertex_buffer)); 358 | 359 | // FIXME: Hack to fill a vertex buffer 360 | let memory_buffer = wasm_bindgen::memory() 361 | .dyn_into::().unwrap() 362 | .buffer(); 363 | let vertices_location = vertices.as_ptr() as u32 / 4; 364 | let vertex_array = js_sys::Float32Array::new(&memory_buffer) 365 | .subarray(vertices_location, vertices_location + vertices.len() as u32); 366 | 367 | ctx.gl.bind_buffer(WebGlRenderingContext::ARRAY_BUFFER, Some(&vertex_buffer)); 368 | ctx.gl.buffer_data_with_array_buffer_view(WebGlRenderingContext::ARRAY_BUFFER, 369 | &vertex_array, 370 | WebGlRenderingContext::STATIC_DRAW); 371 | 372 | let mut x: f32 = 0.0; 373 | let mut y: f32 = 0.0; 374 | 375 | for xs in vertices.chunks(4) { 376 | x = x.max(xs.get(0).unwrap().abs()); 377 | y = y.max(xs.get(1).unwrap().abs()); 378 | } 379 | 380 | Model { 381 | vertex_buffer, 382 | num_vertices: vertices.len() as u32, 383 | texture, 384 | extent: Vec2 { x: x * 0.9, y: y * 0.9 } 385 | } 386 | } 387 | fn pre_render(&self, ctx: &RenderContext) { 388 | ctx.gl.bind_buffer(GL::ARRAY_BUFFER, Some(&self.vertex_buffer)); 389 | ctx.gl.active_texture(GL::TEXTURE0); 390 | ctx.gl.bind_texture(GL::TEXTURE_2D, Some(&self.texture)); 391 | ctx.gl.vertex_attrib_pointer_with_i32(ctx.position as u32, 2, GL::FLOAT, false, 16, 0); 392 | ctx.gl.vertex_attrib_pointer_with_i32(ctx.texcoord as u32, 2, GL::FLOAT, false, 16, 8); 393 | ctx.gl.uniform1i(Some(&ctx.sampler), 0); 394 | ctx.gl.uniform1f(Some(&ctx.opacity), 1.0); 395 | } 396 | fn render(&self, pos: &Vec2, ctx: &RenderContext) { 397 | ctx.gl.uniform4f(Some(&ctx.offset), pos.x, pos.y, 0.0, 0.0); 398 | ctx.gl.draw_arrays(GL::TRIANGLES, 0, self.num_vertices as i32 / 4); 399 | } 400 | fn render_particle(&self, pos: &Vec2, opacity: f32, ctx: &RenderContext) { 401 | ctx.gl.uniform4f(Some(&ctx.offset), pos.x, pos.y, 0.0, 0.0); 402 | ctx.gl.uniform1f(Some(&ctx.opacity), opacity); 403 | ctx.gl.draw_arrays(GL::TRIANGLES, 0, self.num_vertices as i32 / 4); 404 | } 405 | } 406 | 407 | 408 | 409 | impl ParticleSystem { 410 | fn new(max_particles: usize) -> ParticleSystem { 411 | ParticleSystem { 412 | max_particles, 413 | particles: Vec::with_capacity(max_particles) 414 | } 415 | } 416 | fn render(&self, model: &Model, ctx: &RenderContext) { 417 | model.pre_render(ctx); 418 | for particle in self.particles.iter() { 419 | model.render_particle(&particle.position, 420 | particle.life as f32 / particle.total_life as f32, 421 | ctx); 422 | } 423 | } 424 | fn add(&mut self, position: Vec2, velocity: Vec2, acceleration: Vec2, life: i32) { 425 | if self.particles.len() < self.max_particles { 426 | self.particles.push(Particle { position, velocity, acceleration, life, total_life: life }); 427 | } 428 | } 429 | fn update(&mut self, delta: f32) { 430 | for p in self.particles.iter_mut() { 431 | p.life -= delta as i32; 432 | p.velocity.x += p.acceleration.x * delta / 1000.0; 433 | p.velocity.y += p.acceleration.y * delta / 1000.0; 434 | p.position.x += p.velocity.x * delta / 1000.0; 435 | p.position.y += p.velocity.y * delta / 1000.0; 436 | } 437 | self.particles.retain(|p| p.life > 0); 438 | } 439 | } 440 | 441 | fn compile_shader(ctx: &WebGlRenderingContext, shader_type: u32, source: &str) -> Result { 442 | let shader = ctx.create_shader(shader_type) 443 | .ok_or_else(|| String::from("Unable to create shader object"))?; 444 | ctx.shader_source(&shader, source); 445 | ctx.compile_shader(&shader); 446 | 447 | let ok = ctx.get_shader_parameter(&shader, GL::COMPILE_STATUS) 448 | .as_bool() 449 | .unwrap_or(false); 450 | if ok { 451 | Ok(shader) 452 | } else { 453 | Err(ctx.get_shader_info_log(&shader) 454 | .unwrap_or_else(|| String::from("Unknown error creating shader"))) 455 | } 456 | } 457 | 458 | fn link_program(ctx: &WebGlRenderingContext, vert_shader: &WebGlShader, frag_shader: &WebGlShader) -> Result { 459 | let program = ctx.create_program().ok_or_else(|| String::from("Unable to create shader object"))?; 460 | 461 | ctx.attach_shader(&program, vert_shader); 462 | ctx.attach_shader(&program, frag_shader); 463 | ctx.link_program(&program); 464 | 465 | let ok = ctx.get_program_parameter(&program, GL::LINK_STATUS) 466 | .as_bool() 467 | .unwrap_or(false); 468 | if ok { 469 | Ok(program) 470 | } else { 471 | Err(ctx.get_program_info_log(&program) 472 | .unwrap_or_else(|| String::from("Unknown error creating program object"))) 473 | } 474 | } 475 | fn collide(p1: &Vec2, e1: &Vec2, p2: &Vec2, e2: &Vec2) -> bool { 476 | (if p1.x < p2.x { p2.x - p1.x } else { p1.x - p2.x }) < e1.x + e2.x && 477 | (if p1.y < p2.y { p2.y - p1.y } else { p1.y - p2.y }) < e1.y + e2.y 478 | } 479 | 480 | fn clamp(x: f32, min: f32, max: f32) -> f32 { 481 | x.max(min).min(max) 482 | } 483 | 484 | fn play_audio(sample: &[f32]) { 485 | let pong = unsafe { PONG.as_mut().unwrap() }; 486 | let ctx = &pong.audio_ctx; 487 | let buffer = &pong.audio_buffer; 488 | 489 | let source = ctx.create_buffer_source().unwrap(); 490 | 491 | // FIXME: copy_to_channel requires a mutable reference for some reason 492 | let mut_sample = unsafe {(sample as *const [f32] as *mut [f32]).as_mut().unwrap()}; 493 | 494 | buffer.copy_to_channel(mut_sample, 0).unwrap(); 495 | source.set_buffer(Some(&buffer)); 496 | source.connect_with_audio_node(&ctx.destination()).unwrap(); 497 | ctx.resume().unwrap(); 498 | source.start().unwrap(); 499 | } 500 | 501 | fn set_score(left: u32, right: u32) { 502 | let document = web_sys::window().unwrap().document().unwrap(); 503 | document.get_element_by_id("score_left").unwrap() 504 | .set_text_content(Some(&left.to_string())); 505 | document.get_element_by_id("score_right").unwrap() 506 | .set_text_content(Some(&right.to_string())); 507 | } 508 | 509 | fn create_sparks(ps: &mut ParticleSystem, x: f32, y: f32, dx: f32, dy: f32) { 510 | for i in 0..4 { 511 | let i = i as f32; 512 | let ddx = (i + 1.0) * dx / 10.0; 513 | let ddy = (i + 1.0) * dy / 10.0; 514 | ps.add(Vec2::new(x, y), Vec2::new(dy + ddx, -dx + ddy), Vec2::zero(), 100); 515 | ps.add(Vec2::new(x, y), Vec2::new(-dy + ddx, dx + ddy), Vec2::zero(), 100); 516 | } 517 | } 518 | 519 | 520 | --------------------------------------------------------------------------------