2019-05-24 17:25:03 +00:00
|
|
|
//! Ruffle web frontend.
|
2019-05-03 02:11:47 +00:00
|
|
|
mod audio;
|
2019-09-01 19:24:04 +00:00
|
|
|
mod navigator;
|
2019-09-17 03:37:11 +00:00
|
|
|
mod render;
|
2019-09-08 18:51:21 +00:00
|
|
|
mod utils;
|
2019-05-03 02:11:47 +00:00
|
|
|
|
2019-09-17 03:37:11 +00:00
|
|
|
use crate::{
|
|
|
|
audio::WebAudioBackend, navigator::WebNavigatorBackend, render::WebCanvasRenderBackend,
|
|
|
|
};
|
2019-05-10 16:06:47 +00:00
|
|
|
use generational_arena::{Arena, Index};
|
2019-04-28 01:15:43 +00:00
|
|
|
use js_sys::Uint8Array;
|
2019-08-20 05:23:02 +00:00
|
|
|
use ruffle_core::{backend::render::RenderBackend, PlayerEvent};
|
2019-05-17 02:14:23 +00:00
|
|
|
use std::{cell::RefCell, error::Error, num::NonZeroI32};
|
2019-08-09 21:50:20 +00:00
|
|
|
use wasm_bindgen::{prelude::*, JsCast, JsValue};
|
2019-08-20 01:17:29 +00:00
|
|
|
use web_sys::{Event, EventTarget, HtmlCanvasElement, MouseEvent};
|
2019-04-28 01:15:43 +00:00
|
|
|
|
2019-05-03 00:17:02 +00:00
|
|
|
thread_local! {
|
2019-05-24 17:25:03 +00:00
|
|
|
/// We store the actual instances of the ruffle core in a static pool.
|
|
|
|
/// This gives us a clear boundary between the JS side and Rust side, avoiding
|
|
|
|
/// issues with lifetimes and type paramters (which cannot be exported with wasm-bindgen).
|
|
|
|
static INSTANCES: RefCell<Arena<RuffleInstance>> = RefCell::new(Arena::new());
|
2019-05-17 02:14:23 +00:00
|
|
|
}
|
|
|
|
|
2019-08-15 20:48:51 +00:00
|
|
|
type AnimationHandler = Closure<dyn FnMut(f64)>;
|
2019-05-17 02:14:23 +00:00
|
|
|
|
2019-05-24 17:25:03 +00:00
|
|
|
struct RuffleInstance {
|
2019-09-01 19:24:04 +00:00
|
|
|
core: ruffle_core::Player<WebAudioBackend, WebCanvasRenderBackend, WebNavigatorBackend>,
|
2019-08-20 01:40:55 +00:00
|
|
|
canvas: HtmlCanvasElement,
|
|
|
|
canvas_width: i32,
|
|
|
|
canvas_height: i32,
|
2019-08-20 05:23:02 +00:00
|
|
|
device_pixel_ratio: f64,
|
2019-08-22 20:28:06 +00:00
|
|
|
timestamp: Option<f64>,
|
2019-05-17 02:14:23 +00:00
|
|
|
animation_handler: Option<AnimationHandler>, // requestAnimationFrame callback
|
|
|
|
animation_handler_id: Option<NonZeroI32>, // requestAnimationFrame id
|
2019-08-09 21:50:20 +00:00
|
|
|
#[allow(dead_code)]
|
2019-08-15 20:48:51 +00:00
|
|
|
click_callback: Option<Closure<dyn FnMut(Event)>>,
|
2019-08-20 01:17:29 +00:00
|
|
|
mouse_move_callback: Option<Closure<dyn FnMut(MouseEvent)>>,
|
|
|
|
mouse_down_callback: Option<Closure<dyn FnMut(MouseEvent)>>,
|
|
|
|
mouse_up_callback: Option<Closure<dyn FnMut(MouseEvent)>>,
|
2019-05-03 00:17:02 +00:00
|
|
|
}
|
|
|
|
|
2019-05-24 17:25:03 +00:00
|
|
|
/// An opaque handle to a `RuffleInstance` inside the pool.
|
|
|
|
///
|
|
|
|
/// This type is exported to JS, and is used to interact with the library.
|
2019-04-28 01:15:43 +00:00
|
|
|
#[wasm_bindgen]
|
2019-05-17 02:14:23 +00:00
|
|
|
#[derive(Clone)]
|
2019-05-24 17:25:03 +00:00
|
|
|
pub struct Ruffle(Index);
|
2019-04-28 01:15:43 +00:00
|
|
|
|
|
|
|
#[wasm_bindgen]
|
2019-05-24 17:25:03 +00:00
|
|
|
impl Ruffle {
|
|
|
|
pub fn new(canvas: HtmlCanvasElement, swf_data: Uint8Array) -> Result<Ruffle, JsValue> {
|
|
|
|
Ruffle::new_internal(canvas, swf_data).map_err(|_| "Error creating player".into())
|
2019-04-28 01:15:43 +00:00
|
|
|
}
|
|
|
|
|
2019-05-17 02:14:23 +00:00
|
|
|
pub fn destroy(&mut self) -> Result<(), JsValue> {
|
|
|
|
// Remove instance from the active list.
|
2019-05-24 17:25:03 +00:00
|
|
|
if let Some(instance) = INSTANCES.with(|instances| {
|
|
|
|
let mut instances = instances.borrow_mut();
|
|
|
|
instances.remove(self.0)
|
2019-05-17 02:14:23 +00:00
|
|
|
}) {
|
|
|
|
// Cancel the animation handler, if it's still active.
|
2019-05-24 17:25:03 +00:00
|
|
|
if let Some(id) = instance.animation_handler_id {
|
2019-05-17 02:14:23 +00:00
|
|
|
if let Some(window) = web_sys::window() {
|
|
|
|
return window.cancel_animation_frame(id.into());
|
|
|
|
}
|
2019-05-06 10:51:09 +00:00
|
|
|
}
|
2019-05-17 02:14:23 +00:00
|
|
|
}
|
2019-05-06 10:51:09 +00:00
|
|
|
|
2019-05-17 02:14:23 +00:00
|
|
|
// Player is dropped at this point.
|
|
|
|
Ok(())
|
2019-04-28 01:15:43 +00:00
|
|
|
}
|
|
|
|
}
|
2019-04-28 06:08:59 +00:00
|
|
|
|
2019-05-24 17:25:03 +00:00
|
|
|
impl Ruffle {
|
2019-08-15 20:48:51 +00:00
|
|
|
fn new_internal(
|
|
|
|
canvas: HtmlCanvasElement,
|
|
|
|
swf_data: Uint8Array,
|
|
|
|
) -> Result<Ruffle, Box<dyn Error>> {
|
2019-04-30 08:53:21 +00:00
|
|
|
console_error_panic_hook::set_once();
|
2019-05-06 10:51:09 +00:00
|
|
|
let _ = console_log::init_with_level(log::Level::Trace);
|
2019-04-30 08:53:21 +00:00
|
|
|
|
2019-04-28 06:08:59 +00:00
|
|
|
let mut data = vec![0; swf_data.length() as usize];
|
|
|
|
swf_data.copy_to(&mut data[..]);
|
|
|
|
|
2019-08-12 01:02:45 +00:00
|
|
|
let window = web_sys::window().ok_or_else(|| "Expected window")?;
|
2019-04-29 20:24:29 +00:00
|
|
|
let renderer = WebCanvasRenderBackend::new(&canvas)?;
|
2019-04-30 08:53:21 +00:00
|
|
|
let audio = WebAudioBackend::new()?;
|
2019-09-01 19:24:04 +00:00
|
|
|
let navigator = WebNavigatorBackend::new();
|
2019-04-28 06:08:59 +00:00
|
|
|
|
2019-09-01 19:24:04 +00:00
|
|
|
let core = ruffle_core::Player::new(renderer, audio, navigator, data)?;
|
2019-04-29 20:24:29 +00:00
|
|
|
|
2019-05-17 02:14:23 +00:00
|
|
|
// Create instance.
|
2019-05-24 17:25:03 +00:00
|
|
|
let instance = RuffleInstance {
|
2019-05-17 02:14:23 +00:00
|
|
|
core,
|
2019-08-20 01:40:55 +00:00
|
|
|
canvas: canvas.clone(),
|
|
|
|
canvas_width: 0, // Intiailize canvas width and height to 0 to force an initial canvas resize.
|
|
|
|
canvas_height: 0,
|
2019-08-20 05:23:02 +00:00
|
|
|
device_pixel_ratio: window.device_pixel_ratio(),
|
2019-05-17 02:14:23 +00:00
|
|
|
animation_handler: None,
|
|
|
|
animation_handler_id: None,
|
2019-08-09 21:50:20 +00:00
|
|
|
click_callback: None,
|
2019-08-20 01:17:29 +00:00
|
|
|
mouse_move_callback: None,
|
|
|
|
mouse_down_callback: None,
|
|
|
|
mouse_up_callback: None,
|
2019-08-22 20:28:06 +00:00
|
|
|
timestamp: None,
|
2019-05-17 02:14:23 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
// Register the instance and create the animation frame closure.
|
2019-05-24 17:25:03 +00:00
|
|
|
let mut ruffle = INSTANCES.with(move |instances| {
|
|
|
|
let mut instances = instances.borrow_mut();
|
|
|
|
let index = instances.insert(instance);
|
|
|
|
let ruffle = Ruffle(index);
|
2019-05-17 02:14:23 +00:00
|
|
|
|
|
|
|
// Create the animation frame closure.
|
|
|
|
{
|
2019-05-24 17:25:03 +00:00
|
|
|
let mut ruffle = ruffle.clone();
|
|
|
|
let instance = instances.get_mut(index).unwrap();
|
2019-05-17 02:14:23 +00:00
|
|
|
instance.animation_handler = Some(Closure::wrap(Box::new(move |timestamp: f64| {
|
2019-05-24 17:25:03 +00:00
|
|
|
ruffle.tick(timestamp);
|
2019-05-17 02:14:23 +00:00
|
|
|
})
|
2019-08-15 20:48:51 +00:00
|
|
|
as Box<dyn FnMut(f64)>));
|
2019-05-17 02:14:23 +00:00
|
|
|
}
|
|
|
|
|
2019-08-20 01:17:29 +00:00
|
|
|
// Create mouse move handler.
|
|
|
|
{
|
|
|
|
let mouse_move_callback = Closure::wrap(Box::new(move |js_event: MouseEvent| {
|
|
|
|
INSTANCES.with(move |instances| {
|
|
|
|
let mut instances = instances.borrow_mut();
|
|
|
|
if let Some(instance) = instances.get_mut(index) {
|
|
|
|
let event = PlayerEvent::MouseMove {
|
2019-08-20 05:23:02 +00:00
|
|
|
x: f64::from(js_event.offset_x()) * instance.device_pixel_ratio,
|
|
|
|
y: f64::from(js_event.offset_y()) * instance.device_pixel_ratio,
|
2019-08-20 01:17:29 +00:00
|
|
|
};
|
|
|
|
instance.core.handle_event(event);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
})
|
|
|
|
as Box<dyn FnMut(MouseEvent)>);
|
|
|
|
let canvas_events: &EventTarget = canvas.as_ref();
|
|
|
|
canvas_events
|
|
|
|
.add_event_listener_with_callback(
|
|
|
|
"mousemove",
|
|
|
|
mouse_move_callback.as_ref().unchecked_ref(),
|
|
|
|
)
|
|
|
|
.unwrap();
|
|
|
|
let instance = instances.get_mut(index).unwrap();
|
|
|
|
instance.mouse_move_callback = Some(mouse_move_callback);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create mouse down handler.
|
|
|
|
{
|
|
|
|
let mouse_down_callback = Closure::wrap(Box::new(move |js_event: MouseEvent| {
|
|
|
|
INSTANCES.with(move |instances| {
|
|
|
|
let mut instances = instances.borrow_mut();
|
|
|
|
if let Some(instance) = instances.get_mut(index) {
|
|
|
|
let event = PlayerEvent::MouseDown {
|
2019-08-20 05:23:02 +00:00
|
|
|
x: f64::from(js_event.offset_x()) * instance.device_pixel_ratio,
|
|
|
|
y: f64::from(js_event.offset_y()) * instance.device_pixel_ratio,
|
2019-08-20 01:17:29 +00:00
|
|
|
};
|
|
|
|
instance.core.handle_event(event);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
})
|
|
|
|
as Box<dyn FnMut(MouseEvent)>);
|
|
|
|
let canvas_events: &EventTarget = canvas.as_ref();
|
|
|
|
canvas_events
|
|
|
|
.add_event_listener_with_callback(
|
|
|
|
"mousedown",
|
|
|
|
mouse_down_callback.as_ref().unchecked_ref(),
|
|
|
|
)
|
|
|
|
.unwrap();
|
|
|
|
let instance = instances.get_mut(index).unwrap();
|
|
|
|
instance.mouse_down_callback = Some(mouse_down_callback);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create mouse up handler.
|
|
|
|
{
|
|
|
|
let mouse_up_callback = Closure::wrap(Box::new(move |js_event: MouseEvent| {
|
|
|
|
INSTANCES.with(move |instances| {
|
|
|
|
let mut instances = instances.borrow_mut();
|
|
|
|
if let Some(instance) = instances.get_mut(index) {
|
|
|
|
let event = PlayerEvent::MouseUp {
|
2019-08-20 05:23:02 +00:00
|
|
|
x: f64::from(js_event.offset_x()) * instance.device_pixel_ratio,
|
|
|
|
y: f64::from(js_event.offset_y()) * instance.device_pixel_ratio,
|
2019-08-20 01:17:29 +00:00
|
|
|
};
|
|
|
|
instance.core.handle_event(event);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
})
|
|
|
|
as Box<dyn FnMut(MouseEvent)>);
|
|
|
|
let canvas_events: &EventTarget = canvas.as_ref();
|
|
|
|
canvas_events
|
|
|
|
.add_event_listener_with_callback(
|
|
|
|
"mouseup",
|
|
|
|
mouse_up_callback.as_ref().unchecked_ref(),
|
|
|
|
)
|
|
|
|
.unwrap();
|
|
|
|
let instance = instances.get_mut(index).unwrap();
|
|
|
|
instance.mouse_up_callback = Some(mouse_up_callback);
|
|
|
|
}
|
|
|
|
|
2019-08-09 21:50:20 +00:00
|
|
|
// Create click event handler.
|
|
|
|
{
|
|
|
|
let click_callback = Closure::wrap(Box::new(move |_| {
|
|
|
|
INSTANCES.with(move |instances| {
|
|
|
|
let mut instances = instances.borrow_mut();
|
|
|
|
if let Some(instance) = instances.get_mut(index) {
|
|
|
|
instance.core.set_is_playing(true);
|
|
|
|
}
|
|
|
|
});
|
2019-08-15 20:48:51 +00:00
|
|
|
}) as Box<dyn FnMut(Event)>);
|
2019-08-09 21:50:20 +00:00
|
|
|
let canvas_events: &EventTarget = canvas.as_ref();
|
|
|
|
canvas_events
|
|
|
|
.add_event_listener_with_callback(
|
|
|
|
"click",
|
|
|
|
click_callback.as_ref().unchecked_ref(),
|
|
|
|
)
|
|
|
|
.unwrap();
|
|
|
|
canvas.style().set_property("cursor", "pointer").unwrap();
|
|
|
|
let instance = instances.get_mut(index).unwrap();
|
|
|
|
instance.click_callback = Some(click_callback);
|
2019-08-20 01:17:29 +00:00
|
|
|
// Do initial render to render "click-to-play".
|
2019-08-09 21:50:20 +00:00
|
|
|
instance.core.render();
|
|
|
|
}
|
|
|
|
|
2019-05-24 17:25:03 +00:00
|
|
|
ruffle
|
2019-05-03 00:17:02 +00:00
|
|
|
});
|
|
|
|
|
2019-08-22 20:28:06 +00:00
|
|
|
// Set initial timestamp and do initial tick to start animation loop.
|
|
|
|
ruffle.tick(0.0);
|
2019-05-17 02:14:23 +00:00
|
|
|
|
2019-05-24 17:25:03 +00:00
|
|
|
Ok(ruffle)
|
2019-05-17 02:14:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
fn tick(&mut self, timestamp: f64) {
|
2019-05-24 17:25:03 +00:00
|
|
|
INSTANCES.with(|instances| {
|
|
|
|
let mut instances = instances.borrow_mut();
|
|
|
|
if let Some(instance) = instances.get_mut(self.0) {
|
2019-08-22 20:28:06 +00:00
|
|
|
// Calculate the dt from last tick.
|
|
|
|
let dt = if let Some(prev_timestamp) = instance.timestamp {
|
|
|
|
instance.timestamp = Some(timestamp);
|
|
|
|
timestamp - prev_timestamp
|
|
|
|
} else {
|
|
|
|
// Store the timestamp from the initial tick.
|
|
|
|
// (I tried to use Performance.now() to get the initial timestamp,
|
|
|
|
// but this didn't seem to be accurate and caused negative dts on
|
|
|
|
// Chrome.)
|
|
|
|
instance.timestamp = Some(timestamp);
|
|
|
|
0.0
|
|
|
|
};
|
2019-08-09 21:50:20 +00:00
|
|
|
|
2019-05-24 17:25:03 +00:00
|
|
|
instance.core.tick(dt);
|
2019-05-17 02:14:23 +00:00
|
|
|
|
2019-08-20 01:40:55 +00:00
|
|
|
// Check for canvas resize.
|
|
|
|
let canvas_width = instance.canvas.client_width();
|
|
|
|
let canvas_height = instance.canvas.client_height();
|
|
|
|
if instance.canvas_width != canvas_width || instance.canvas_height != canvas_height
|
|
|
|
{
|
|
|
|
// If a canvas resizes, it's drawing context will get scaled. You must reset
|
|
|
|
// the width and height attributes of the canvas element to recreate the context.
|
|
|
|
// (NOT the CSS width/height!)
|
|
|
|
instance.canvas_width = canvas_width;
|
|
|
|
instance.canvas_height = canvas_height;
|
2019-08-20 05:23:02 +00:00
|
|
|
|
2019-08-20 01:40:55 +00:00
|
|
|
// The actual viewport is scaled by DPI, bigger than CSS pixels.
|
2019-08-20 05:23:02 +00:00
|
|
|
let viewport_width =
|
|
|
|
(f64::from(canvas_width) * instance.device_pixel_ratio) as u32;
|
|
|
|
let viewport_height =
|
|
|
|
(f64::from(canvas_height) * instance.device_pixel_ratio) as u32;
|
2019-08-20 01:40:55 +00:00
|
|
|
instance.canvas.set_width(viewport_width);
|
|
|
|
instance.canvas.set_height(viewport_height);
|
|
|
|
instance
|
|
|
|
.core
|
|
|
|
.set_viewport_dimensions(viewport_width, viewport_height);
|
|
|
|
instance
|
|
|
|
.core
|
|
|
|
.renderer_mut()
|
|
|
|
.set_viewport_dimensions(viewport_width, viewport_height);
|
|
|
|
|
|
|
|
// Force a re-render if we resize.
|
|
|
|
instance.core.render();
|
|
|
|
}
|
|
|
|
|
2019-05-17 02:14:23 +00:00
|
|
|
// Request next animation frame.
|
2019-05-24 17:25:03 +00:00
|
|
|
if let Some(handler) = &instance.animation_handler {
|
2019-05-17 02:14:23 +00:00
|
|
|
let window = web_sys::window().unwrap();
|
|
|
|
let id = window
|
|
|
|
.request_animation_frame(handler.as_ref().unchecked_ref())
|
|
|
|
.unwrap();
|
2019-05-24 17:25:03 +00:00
|
|
|
instance.animation_handler_id = NonZeroI32::new(id);
|
2019-05-17 02:14:23 +00:00
|
|
|
} else {
|
2019-05-24 17:25:03 +00:00
|
|
|
instance.animation_handler_id = None;
|
2019-05-17 02:14:23 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
2019-04-28 06:08:59 +00:00
|
|
|
}
|
|
|
|
}
|