├── 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 |
--------------------------------------------------------------------------------