From ce9a0a8ef6996b63604910d88a69a0bfd70f6ed9 Mon Sep 17 00:00:00 2001 From: Mike Welsh Date: Sun, 8 Sep 2019 11:51:21 -0700 Subject: [PATCH] web: Initial masking support Also add web/src/utils.rs for some methods to ease management of errors for wasm_bindgen. --- web/src/lib.rs | 1 + web/src/render.rs | 100 ++++++++++++++++++++++++++++++++++++++++++++-- web/src/utils.rs | 35 ++++++++++++++++ 3 files changed, 132 insertions(+), 4 deletions(-) create mode 100644 web/src/utils.rs diff --git a/web/src/lib.rs b/web/src/lib.rs index ff2180232..2fa04b7f3 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -1,6 +1,7 @@ //! Ruffle web frontend. mod audio; mod render; +mod utils; use crate::{audio::WebAudioBackend, render::WebCanvasRenderBackend}; use generational_arena::{Arena, Index}; diff --git a/web/src/render.rs b/web/src/render.rs index 1849ca9a8..2da50a84f 100644 --- a/web/src/render.rs +++ b/web/src/render.rs @@ -1,3 +1,4 @@ +use crate::utils::JsResult; use ruffle_core::backend::render::{ swf, swf::CharacterId, BitmapHandle, Color, Letterbox, RenderBackend, ShapeHandle, Transform, }; @@ -8,6 +9,9 @@ use web_sys::{CanvasRenderingContext2d, Element, HtmlCanvasElement, HtmlImageEle pub struct WebCanvasRenderBackend { canvas: HtmlCanvasElement, context: CanvasRenderingContext2d, + root_canvas: HtmlCanvasElement, + render_targets: Vec<(HtmlCanvasElement, CanvasRenderingContext2d)>, + cur_render_target: usize, color_matrix: Element, shapes: Vec, bitmaps: Vec, @@ -45,7 +49,7 @@ impl WebCanvasRenderBackend { ); let context: CanvasRenderingContext2d = canvas .get_context_with_context_options("2d", &context_options) - .map_err(|_| "Could not create context")? + .into_js_result()? .ok_or("Could not create context")? .dyn_into() .map_err(|_| "Expected CanvasRenderingContext2d")?; @@ -119,8 +123,12 @@ impl WebCanvasRenderBackend { .map(|s| s.contains("Firefox")) .unwrap_or(false); + let render_targets = vec![(canvas.clone(), context.clone())]; let renderer = Self { canvas: canvas.clone(), + root_canvas: canvas.clone(), + render_targets, + cur_render_target: 0, color_matrix, context, shapes: vec![], @@ -161,6 +169,57 @@ impl WebCanvasRenderBackend { &base64::encode(&png_data[..]) )) } + + // Pushes a fresh canvas onto the stack to use as a render target. + fn push_render_target(&mut self) { + self.cur_render_target += 1; + if self.cur_render_target >= self.render_targets.len() { + // Create offscreen canvas to use as the render target. + let window = web_sys::window().unwrap(); + let document = window.document().unwrap(); + let canvas: HtmlCanvasElement = document + .create_element("canvas") + .unwrap() + .dyn_into() + .unwrap(); + let context: CanvasRenderingContext2d = canvas + .get_context("2d") + .unwrap() + .unwrap() + .dyn_into() + .unwrap(); + canvas + .style() + .set_property("display", "none") + .warn_on_error(); + self.root_canvas.append_child(&canvas).warn_on_error(); + self.render_targets.push((canvas, context)); + } + + let (canvas, context) = &self.render_targets[self.cur_render_target]; + canvas.set_width(self.viewport_width); + canvas.set_height(self.viewport_height); + self.canvas = canvas.clone(); + self.context = context.clone(); + let width = self.canvas.width(); + let height = self.canvas.height(); + self.context + .clear_rect(0.0, 0.0, width.into(), height.into()); + } + + fn pop_render_target(&mut self) -> (HtmlCanvasElement, CanvasRenderingContext2d) { + if self.cur_render_target > 0 { + let out = (self.canvas.clone(), self.context.clone()); + self.cur_render_target -= 1; + let (canvas, context) = &self.render_targets[self.cur_render_target]; + self.canvas = canvas.clone(); + self.context = context.clone(); + out + } else { + log::error!("Render target stack underflow"); + (self.canvas.clone(), self.context.clone()) + } + } } impl RenderBackend for WebCanvasRenderBackend { @@ -441,9 +500,42 @@ impl RenderBackend for WebCanvasRenderBackend { } } - fn push_mask(&mut self) {} - fn activate_mask(&mut self) {} - fn pop_mask(&mut self) {} + fn push_mask(&mut self) { + // In the canvas backend, masks are implemented using two render targets. + // We render the masker clips to the first render target. + self.push_render_target(); + } + fn activate_mask(&mut self) { + // We render the maskee clips to the second render target. + self.push_render_target(); + } + fn pop_mask(&mut self) { + let (maskee_canvas, _maskee_context) = self.pop_render_target(); + let (masker_canvas, masker_context) = self.pop_render_target(); + + // We have to be sure to reset the transforms here so that + // the texture is drawn starting from the upper-left corner. + masker_context.reset_transform().warn_on_error(); + self.context.reset_transform().warn_on_error(); + + // We draw the maskee onto the masker using the "source-in" blend mode. + // This will draw the clips in pixels only where the masker alpha > 0. + masker_context + .set_global_composite_operation("source-in") + .unwrap(); + masker_context + .draw_image_with_html_canvas_element(&maskee_canvas, 0.0, 0.0) + .unwrap(); + masker_context + .set_global_composite_operation("source-over") + .unwrap(); + + // Finally, we draw the finalized masked onto the main canvas. + self.context.reset_transform().warn_on_error(); + self.context + .draw_image_with_html_canvas_element(&masker_canvas, 0.0, 0.0) + .unwrap(); + } } fn swf_shape_to_svg( diff --git a/web/src/utils.rs b/web/src/utils.rs new file mode 100644 index 000000000..c5fcc9428 --- /dev/null +++ b/web/src/utils.rs @@ -0,0 +1,35 @@ +//! Utility functions for the web backend. +use wasm_bindgen::JsValue; + +#[derive(Debug)] +pub struct JsError { + value: JsValue, +} + +impl std::fmt::Display for JsError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + write!(f, "JS exception: {:?}", self) + } +} + +impl std::error::Error for JsError {} + +pub trait JsResult { + /// Converts a `JsValue` into a standard `Error`. + fn warn_on_error(&self); + fn into_js_result(self) -> Result; +} + +impl JsResult for Result { + #[inline] + fn warn_on_error(&self) { + if let Err(value) = self { + log::warn!("Unexpected JavaScript error: {:?}", value); + } + } + + #[inline] + fn into_js_result(self) -> Result { + self.map_err(|value| JsError { value }) + } +} \ No newline at end of file