use ruffle_core::backend::render::{ swf::{self, CharacterId, GradientInterpolation, GradientSpread}, Bitmap, BitmapFormat, BitmapHandle, BitmapInfo, Color, JpegTagFormat, Letterbox, RenderBackend, ShapeHandle, Transform, }; use ruffle_core::color_transform::ColorTransform; use ruffle_core::shape_utils::{DistilledShape, DrawCommand}; use ruffle_core::swf::Matrix; use ruffle_web_common::JsResult; use std::collections::HashMap; use std::convert::TryInto; use wasm_bindgen::{JsCast, JsValue}; use web_sys::{ CanvasGradient, CanvasPattern, CanvasRenderingContext2d, Element, HtmlCanvasElement, HtmlImageElement, Path2d, SvgsvgElement, }; type Error = Box; 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, id_to_bitmap: HashMap, viewport_width: u32, viewport_height: u32, use_color_transform_hack: bool, pixelated_property_value: &'static str, deactivating_mask: bool, bitmap_registry: HashMap, } /// Canvas-drawable shape data extracted from an SWF file. struct ShapeData(Vec); struct CanvasColor(String, u8, u8, u8, u8); /// Convert an f32 to a u8, clamping all out-of-range values to the `u8` range. fn clamped_u8_color(v: f32) -> u8 { if v < 0.0 { 0 } else if v > 255.0 { 255 } else { v as u8 } } impl CanvasColor { /// Apply a color transformation to this color. fn color_transform(&self, cxform: &ColorTransform) -> CanvasColor { let CanvasColor(_, r, g, b, a) = self; let r = clamped_u8_color(*r as f32 * cxform.r_mult + (cxform.r_add * 255.0)); let g = clamped_u8_color(*g as f32 * cxform.g_mult + (cxform.g_add * 255.0)); let b = clamped_u8_color(*b as f32 * cxform.b_mult + (cxform.b_add * 255.0)); let a = clamped_u8_color(*a as f32 * cxform.a_mult + (cxform.a_add * 255.0)); let colstring = format!("rgba({},{},{},{})", r, g, b, f32::from(a) / 255.0); CanvasColor(colstring, r, g, b, a) } } /// An individual command to be drawn to the canvas. enum CanvasDrawCommand { /// A command to draw a path stroke with a given style. Stroke { path: Path2d, line_width: f64, stroke_style: CanvasColor, line_cap: String, line_join: String, miter_limit: f64, }, /// A command to fill a path with a given style. Fill { path: Path2d, fill_style: CanvasFillStyle, }, /// A command to draw a particular image (such as an SVG) DrawImage { image: HtmlImageElement, x_min: f64, y_min: f64, }, } enum CanvasFillStyle { Color(CanvasColor), #[allow(dead_code)] Gradient(CanvasGradient), Pattern(CanvasPattern), } impl CanvasFillStyle { /// Attempt to apply a color transformation to this fill style. fn color_transform(&self, cxform: &ColorTransform) -> Option { match self { Self::Color(cc) => Some(Self::Color(cc.color_transform(cxform))), _ => None, } } } #[allow(dead_code)] struct BitmapData { image: HtmlImageElement, width: u32, height: u32, data: String, } impl WebCanvasRenderBackend { pub fn new(canvas: &HtmlCanvasElement) -> Result> { // Request the CanvasRenderingContext2d. // Disable alpha for possible speedup. // TODO: Allow user to enable transparent background (transparent wmode in legacy Flash). let context_options = js_sys::Object::new(); let _ = js_sys::Reflect::set( &context_options, &"alpha".into(), &wasm_bindgen::JsValue::FALSE, ); let context: CanvasRenderingContext2d = canvas .get_context_with_context_options("2d", &context_options) .into_js_result()? .ok_or("Could not create context")? .dyn_into() .map_err(|_| "Expected CanvasRenderingContext2d")?; let window = web_sys::window().unwrap(); let document = window.document().unwrap(); // Create a color matrix filter to handle Flash color effects. // We may have a previous instance if this canvas was re-used, so remove it. if let Ok(Some(element)) = canvas.query_selector("#_svgfilter") { element.remove(); } let svg = document .create_element_ns(Some("http://www.w3.org/2000/svg"), "svg") .map_err(|_| "Couldn't make SVG")?; svg.set_id("_svgfilter"); svg.set_attribute("width", "0") .map_err(|_| "Couldn't make SVG")?; svg.set_attribute("height", "0") .map_err(|_| "Couldn't make SVG")?; svg.set_attribute_ns( Some("http://www.w3.org/2000/xmlns/"), "xmlns:xlink", "http://www.w3.org/1999/xlink", ) .map_err(|_| "Couldn't make SVG")?; let filter = document .create_element_ns(Some("http://www.w3.org/2000/svg"), "filter") .map_err(|_| "Couldn't make SVG filter")?; filter .set_attribute("id", "_cm") .map_err(|_| "Couldn't make SVG filter")?; filter .set_attribute("color-interpolation-filters", "sRGB") .map_err(|_| "Couldn't make SVG filter")?; let color_matrix = document .create_element_ns(Some("http://www.w3.org/2000/svg"), "feColorMatrix") .map_err(|_| "Couldn't make SVG feColorMatrix element")?; color_matrix .set_attribute("type", "matrix") .map_err(|_| "Couldn't make SVG feColorMatrix element")?; color_matrix .set_attribute("values", "1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0") .map_err(|_| "Couldn't make SVG feColorMatrix element")?; filter .append_child(&color_matrix) .map_err(|_| "append_child failed")?; svg.append_child(&filter) .map_err(|_| "append_child failed")?; canvas .append_child(&svg) .map_err(|_| "append_child failed")?; // Check if we are on Firefox to use the color transform hack. // TODO: We could turn this into a general util function to detect browser // type, version, OS, etc. let is_firefox = window .navigator() .user_agent() .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![], bitmaps: vec![], id_to_bitmap: HashMap::new(), viewport_width: 0, viewport_height: 0, use_color_transform_hack: is_firefox, deactivating_mask: false, // For rendering non-smoothed bitmaps. // crisp-edges works in Firefox, pixelated works in Chrome (and others)? pixelated_property_value: if is_firefox { "crisp-edges" } else { "pixelated" }, bitmap_registry: HashMap::new(), }; Ok(renderer) } /// Converts an RGBA image into a PNG encoded as a base64 data URI. fn bitmap_to_png_data_uri(bitmap: Bitmap) -> Result> { use png::Encoder; let mut png_data: Vec = vec![]; { let mut encoder = Encoder::new(&mut png_data, bitmap.width, bitmap.height); encoder.set_depth(png::BitDepth::Eight); let data = match bitmap.data { BitmapFormat::Rgba(mut data) => { ruffle_core::backend::render::unmultiply_alpha_rgba(&mut data[..]); encoder.set_color(png::ColorType::RGBA); data } BitmapFormat::Rgb(data) => { encoder.set_color(png::ColorType::RGB); data } }; let mut writer = encoder.write_header()?; writer.write_image_data(&data)?; } Ok(format!( "data:image/png;base64,{}", &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()) } } #[allow(clippy::float_cmp)] #[inline] fn set_transform(&mut self, matrix: &Matrix) { self.context .set_transform( matrix.a.into(), matrix.b.into(), matrix.c.into(), matrix.d.into(), matrix.tx.to_pixels(), matrix.ty.to_pixels(), ) .unwrap(); } #[allow(clippy::float_cmp)] #[inline] fn set_color_filter(&self, transform: &Transform) { let color_transform = &transform.color_transform; if color_transform.r_mult == 1.0 && color_transform.g_mult == 1.0 && color_transform.b_mult == 1.0 && color_transform.r_add == 0.0 && color_transform.g_add == 0.0 && color_transform.b_add == 0.0 && color_transform.a_add == 0.0 { self.context.set_global_alpha(color_transform.a_mult.into()); } else { // TODO HACK: Firefox is having issues with additive alpha in color transforms (see #38). // Hack this away and just use multiplicative (not accurate in many cases, but won't look awful). let (a_mult, a_add) = if self.use_color_transform_hack && color_transform.a_add != 0.0 { (color_transform.a_mult + color_transform.a_add, 0.0) } else { (color_transform.a_mult, color_transform.a_add) }; let matrix_str = format!( "{} 0 0 0 {} 0 {} 0 0 {} 0 0 {} 0 {} 0 0 0 {} {}", color_transform.r_mult, color_transform.r_add, color_transform.g_mult, color_transform.g_add, color_transform.b_mult, color_transform.b_add, a_mult, a_add ); self.color_matrix .set_attribute("values", &matrix_str) .unwrap(); self.context.set_filter("url('#_cm')"); } } #[inline] fn clear_color_filter(&self) { self.context.set_filter("none"); self.context.set_global_alpha(1.0); } fn register_bitmap_pure_jpeg( &mut self, id: CharacterId, data: &[u8], ) -> Result { //TODO: self.bitmap_registry.insert(id, Bitmap { width: 123, height: 123, data: BitmapFormat::Rgb(vec![]) }); let data = ruffle_core::backend::render::remove_invalid_jpeg_data(data); let mut decoder = jpeg_decoder::Decoder::new(&data[..]); decoder.read_info()?; let metadata = decoder.info().ok_or("Expected JPEG metadata")?; let image = HtmlImageElement::new().into_js_result()?; let jpeg_encoded = format!("data:image/jpeg;base64,{}", &base64::encode(&data[..])); image.set_src(&jpeg_encoded); let handle = BitmapHandle(self.bitmaps.len()); self.bitmaps.push(BitmapData { image, width: metadata.width.into(), height: metadata.height.into(), data: jpeg_encoded, }); self.id_to_bitmap.insert(id, handle); Ok(BitmapInfo { handle, width: metadata.width, height: metadata.height, }) } fn register_bitmap_raw( &mut self, id: CharacterId, bitmap: Bitmap, ) -> Result { self.bitmap_registry.insert(id, bitmap.clone()); let (width, height) = (bitmap.width, bitmap.height); let png = Self::bitmap_to_png_data_uri(bitmap)?; let image = HtmlImageElement::new().unwrap(); image.set_src(&png); let handle = BitmapHandle(self.bitmaps.len()); self.bitmaps.push(BitmapData { image, width, height, data: png, }); self.id_to_bitmap.insert(id, handle); Ok(BitmapInfo { handle, width: width.try_into().expect("JPEG dimensions too large"), height: height.try_into().expect("JPEG dimensions too large"), }) } } impl RenderBackend for WebCanvasRenderBackend { fn set_viewport_dimensions(&mut self, width: u32, height: u32) { self.viewport_width = width; self.viewport_height = height; } fn register_shape(&mut self, shape: DistilledShape) -> ShapeHandle { let handle = ShapeHandle(self.shapes.len()); let mut bitmaps = HashMap::new(); for (id, handle) in &self.id_to_bitmap { let bitmap_data = &self.bitmaps[handle.0]; bitmaps.insert( *id, (&bitmap_data.data[..], bitmap_data.width, bitmap_data.height), ); } let data = swf_shape_to_canvas_commands( &shape, &bitmaps, self.pixelated_property_value, &self.context, ) .unwrap_or_else(|| swf_shape_to_svg(shape, &bitmaps, self.pixelated_property_value)); self.shapes.push(data); handle } fn replace_shape(&mut self, shape: DistilledShape, handle: ShapeHandle) { let mut bitmaps = HashMap::new(); for (id, handle) in &self.id_to_bitmap { let bitmap_data = &self.bitmaps[handle.0]; bitmaps.insert( *id, (&bitmap_data.data[..], bitmap_data.width, bitmap_data.height), ); } let data = swf_shape_to_canvas_commands( &shape, &bitmaps, self.pixelated_property_value, &self.context, ) .unwrap_or_else(|| swf_shape_to_svg(shape, &bitmaps, self.pixelated_property_value)); self.shapes[handle.0] = data; } fn register_glyph_shape(&mut self, glyph: &swf::Glyph) -> ShapeHandle { let shape = ruffle_core::shape_utils::swf_glyph_to_shape(glyph); self.register_shape((&shape).into()) } fn register_bitmap_jpeg( &mut self, id: CharacterId, data: &[u8], jpeg_tables: Option<&[u8]>, ) -> Result { let data = ruffle_core::backend::render::glue_tables_to_jpeg(data, jpeg_tables); self.register_bitmap_pure_jpeg(id, &data) } fn register_bitmap_jpeg_2( &mut self, id: CharacterId, data: &[u8], ) -> Result { if ruffle_core::backend::render::determine_jpeg_tag_format(data) == JpegTagFormat::Jpeg { self.register_bitmap_pure_jpeg(id, data) } else { let bitmap = ruffle_core::backend::render::decode_define_bits_jpeg(data, None)?; self.register_bitmap_raw(id, bitmap) } } fn register_bitmap_jpeg_3( &mut self, id: swf::CharacterId, jpeg_data: &[u8], alpha_data: &[u8], ) -> Result { let bitmap = ruffle_core::backend::render::decode_define_bits_jpeg(jpeg_data, Some(alpha_data))?; self.register_bitmap_raw(id, bitmap) } fn register_bitmap_png( &mut self, swf_tag: &swf::DefineBitsLossless, ) -> Result { let bitmap = ruffle_core::backend::render::decode_define_bits_lossless(swf_tag)?; self.bitmap_registry.insert(swf_tag.id, bitmap.clone()); let png = Self::bitmap_to_png_data_uri(bitmap)?; let image = HtmlImageElement::new().unwrap(); image.set_src(&png); let handle = BitmapHandle(self.bitmaps.len()); self.bitmaps.push(BitmapData { image, width: swf_tag.width.into(), height: swf_tag.height.into(), data: png, }); self.id_to_bitmap.insert(swf_tag.id, handle); Ok(BitmapInfo { handle, width: swf_tag.width, height: swf_tag.height, }) } fn begin_frame(&mut self, clear: Color) { // Reset canvas transform in case it was left in a dirty state. self.context.reset_transform().unwrap(); let width = self.canvas.width(); let height = self.canvas.height(); let color = format!("rgb({}, {}, {})", clear.r, clear.g, clear.b); self.context.set_fill_style(&color.into()); self.context .fill_rect(0.0, 0.0, width.into(), height.into()); self.deactivating_mask = false; } fn end_frame(&mut self) { // Noop } fn render_bitmap(&mut self, bitmap: BitmapHandle, transform: &Transform) { if self.deactivating_mask { return; } self.set_transform(&transform.matrix); self.set_color_filter(transform); if let Some(bitmap) = self.bitmaps.get(bitmap.0) { let _ = self .context .draw_image_with_html_image_element(&bitmap.image, 0.0, 0.0); } self.clear_color_filter(); } fn render_shape(&mut self, shape: ShapeHandle, transform: &Transform) { if self.deactivating_mask { return; } self.set_transform(&transform.matrix); if let Some(shape) = self.shapes.get(shape.0) { for command in shape.0.iter() { match command { CanvasDrawCommand::Fill { path, fill_style } => { let xformed_fill_style = fill_style.color_transform(&transform.color_transform); if xformed_fill_style.is_none() { self.set_color_filter(transform); } match xformed_fill_style.as_ref().unwrap_or(fill_style) { CanvasFillStyle::Color(CanvasColor(color, ..)) => { self.context.set_fill_style(&JsValue::from_str(&color)) } CanvasFillStyle::Gradient(grad) => self.context.set_fill_style(grad), CanvasFillStyle::Pattern(patt) => self.context.set_fill_style(patt), }; self.context.fill_with_path_2d(&path); if xformed_fill_style.is_none() { self.clear_color_filter(); } } CanvasDrawCommand::Stroke { path, line_width, stroke_style, line_cap, line_join, miter_limit, } => { let xformed_stroke_style = stroke_style.color_transform(&transform.color_transform); self.context.set_line_width(*line_width); self.context.set_line_cap(&line_cap); self.context.set_line_join(&line_join); self.context.set_miter_limit(*miter_limit); self.context .set_stroke_style(&JsValue::from_str(&xformed_stroke_style.0)); self.context.stroke_with_path(&path); } CanvasDrawCommand::DrawImage { image, x_min, y_min, } => { self.set_color_filter(transform); let _ = self .context .draw_image_with_html_image_element(&image, *x_min, *y_min); self.clear_color_filter(); } } } } } fn draw_rect(&mut self, color: Color, matrix: &Matrix) { if self.deactivating_mask { return; } self.set_transform(matrix); self.clear_color_filter(); self.context.set_fill_style( &format!( "rgba({},{},{},{})", color.r, color.g, color.b, f32::from(color.a) / 255.0 ) .into(), ); self.context.fill_rect(0.0, 0.0, 1.0, 1.0); self.clear_color_filter(); } fn draw_letterbox(&mut self, letterbox: Letterbox) { self.context.reset_transform().unwrap(); self.context.set_fill_style(&"black".into()); match letterbox { Letterbox::None => (), Letterbox::Letterbox(margin_height) => { self.context .fill_rect(0.0, 0.0, self.viewport_width.into(), margin_height.into()); self.context.fill_rect( 0.0, (self.viewport_height as f32 - margin_height).into(), self.viewport_width.into(), self.viewport_height.into(), ); } Letterbox::Pillarbox(margin_width) => { self.context .fill_rect(0.0, 0.0, margin_width.into(), self.viewport_height.into()); self.context.fill_rect( (self.viewport_width as f32 - margin_width).into(), 0.0, margin_width.into(), self.viewport_height.into(), ); } } } 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 deactivate_mask(&mut self) { self.deactivating_mask = true; } fn pop_mask(&mut self) { self.deactivating_mask = false; 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. maskee_context.reset_transform().warn_on_error(); self.context.reset_transform().warn_on_error(); // We draw the masker onto the maskee using the "destination-in" blend mode. // This will filter out pixels where the maskee alpha == 0. maskee_context .set_global_composite_operation("destination-in") .unwrap(); // Force alpha to 100% for the mask art, because Flash ignores alpha in masks. // Otherwise canvas blend modes will draw the masked clip as transparent. // TODO: Doesn't work on Safari because it doesn't support context.filter. self.color_matrix .set_attribute( "values", &"1.0 0 0 0 0 0 1.0 0 0 0 0 0 1.0 0 0 0 0 0 256.0 0", ) .warn_on_error(); maskee_context.set_filter("url('#_cm')"); maskee_context .draw_image_with_html_canvas_element(&masker_canvas, 0.0, 0.0) .unwrap(); maskee_context .set_global_composite_operation("source-over") .unwrap(); maskee_context.set_filter("none"); // 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(&maskee_canvas, 0.0, 0.0) .unwrap(); } fn get_bitmap_pixels(&mut self, bitmap: BitmapHandle) -> (u32, u32, Vec) { log::error!("Get bitmap pixels canvas {:?}", bitmap); if let Some((id, _texture)) = self.id_to_bitmap.iter().find(|(_k, v)| v.0 == bitmap.0) { if let Some(bitmap) = self.bitmap_registry.get(id) { log::error!("Found bitmap = {:?}", bitmap); let data = match &bitmap.data { BitmapFormat::Rgb(x) => { x.chunks_exact(3).map(|chunk| { let r = chunk[0]; let g = chunk[1]; let b = chunk[2]; (0xFF << 24) | ((r as u32) << 16) | ((g as u32) << 8) | (b as u32) }).collect() } BitmapFormat::Rgba(x) => { x.chunks_exact(4).map(|chunk| { let r = chunk[0]; let g = chunk[1]; let b = chunk[2]; //TODO: check this order, assuming because rgb_a let a = chunk[3]; ((a as u32) << 24) | ((r as u32) << 16) | ((g as u32) << 8) | (b as u32) }).collect() } }; (bitmap.width, bitmap.height, data) } else { log::error!("Failed to find bitmap {} in registry", id); (0, 0, vec![0]) } } else { log::error!("Failed to find bitmap {:?} in id_to_bitmap", bitmap); (0, 0, vec![1]) } } fn register_bitmap_raw(&mut self, width: u32, height: u32, rgba: Vec) -> BitmapHandle { self.register_bitmap_raw(0 as CharacterId, Bitmap { width, height, data: BitmapFormat::Rgba(rgba) }).unwrap().handle } } #[allow(clippy::cognitive_complexity)] fn swf_shape_to_svg( shape: DistilledShape, bitmaps: &HashMap, pixelated_property_value: &str, ) -> ShapeData { use fnv::FnvHashSet; use ruffle_core::shape_utils::DrawPath; use svg::node::element::{ path::Data, Definitions, Filter, Image, LinearGradient, Path as SvgPath, Pattern, RadialGradient, Stop, }; use svg::Document; use swf::{FillStyle, LineCapStyle, LineJoinStyle}; // Some browsers will vomit if you try to load/draw an image with 0 width/height. // TODO(Herschel): Might be better to just return None in this case and skip // rendering altogether. let (width, height) = ( f32::max( (shape.shape_bounds.x_max - shape.shape_bounds.x_min).to_pixels() as f32, 1.0, ), f32::max( (shape.shape_bounds.y_max - shape.shape_bounds.y_min).to_pixels() as f32, 1.0, ), ); let mut document = Document::new() .set("width", width) .set("height", height) .set( "viewBox", ( shape.shape_bounds.x_min.get(), shape.shape_bounds.y_min.get(), (shape.shape_bounds.x_max - shape.shape_bounds.x_min).get(), (shape.shape_bounds.y_max - shape.shape_bounds.y_min).get(), ), ) // preserveAspectRatio must be off or Firefox will fudge with the dimensions when we draw an image onto canvas. .set("preserveAspectRatio", "none") .set("xmlns:xlink", "http://www.w3.org/1999/xlink"); let width = (shape.shape_bounds.x_max - shape.shape_bounds.x_min).get() as f32; let height = (shape.shape_bounds.y_max - shape.shape_bounds.y_min).get() as f32; let mut bitmap_defs: FnvHashSet = FnvHashSet::default(); let mut defs = Definitions::new(); let mut num_defs = 0; let mut has_linear_rgb_gradient = false; let mut svg_paths = vec![]; for path in shape.paths { match path { DrawPath::Fill { style, commands } => { let mut svg_path = SvgPath::new(); let fill = match style { FillStyle::Color(Color { r, g, b, a }) => { format!("rgba({},{},{},{})", r, g, b, f32::from(*a) / 255.0) } FillStyle::LinearGradient(gradient) => { let shift = Matrix { a: 32768.0 / width, d: 32768.0 / height, tx: swf::Twips::new(-16384), ty: swf::Twips::new(-16384), ..Default::default() }; let gradient_matrix = gradient.matrix * shift; let mut svg_gradient = LinearGradient::new() .set("id", format!("f{}", num_defs)) .set("gradientUnits", "userSpaceOnUse") .set( "gradientTransform", format!( "matrix({} {} {} {} {} {})", gradient_matrix.a, gradient_matrix.b, gradient_matrix.c, gradient_matrix.d, gradient_matrix.tx.get(), gradient_matrix.ty.get() ), ); svg_gradient = match gradient.spread { GradientSpread::Pad => svg_gradient, // default GradientSpread::Reflect => svg_gradient.set("spreadMethod", "reflect"), GradientSpread::Repeat => svg_gradient.set("spreadMethod", "repeat"), }; if gradient.interpolation == GradientInterpolation::LinearRGB { has_linear_rgb_gradient = true; svg_path = svg_path.set("filter", "url('#_linearrgb')"); } for record in &gradient.records { let color = if gradient.interpolation == GradientInterpolation::LinearRGB { srgb_to_linear(record.color.clone()) } else { record.color.clone() }; let stop = Stop::new() .set("offset", format!("{}%", f32::from(record.ratio) / 2.55)) .set( "stop-color", format!( "rgba({},{},{},{})", color.r, color.g, color.b, f32::from(color.a) / 255.0 ), ); svg_gradient = svg_gradient.add(stop); } defs = defs.add(svg_gradient); let fill_id = format!("url(#f{})", num_defs); num_defs += 1; fill_id } FillStyle::RadialGradient(gradient) => { let shift = Matrix { a: 32768.0, d: 32768.0, ..Default::default() }; let gradient_matrix = gradient.matrix * shift; let mut svg_gradient = RadialGradient::new() .set("id", format!("f{}", num_defs)) .set("gradientUnits", "userSpaceOnUse") .set("cx", "0") .set("cy", "0") .set("r", "0.5") .set( "gradientTransform", format!( "matrix({} {} {} {} {} {})", gradient_matrix.a, gradient_matrix.b, gradient_matrix.c, gradient_matrix.d, gradient_matrix.tx.get(), gradient_matrix.ty.get() ), ); svg_gradient = match gradient.spread { GradientSpread::Pad => svg_gradient, // default GradientSpread::Reflect => svg_gradient.set("spreadMethod", "reflect"), GradientSpread::Repeat => svg_gradient.set("spreadMethod", "repeat"), }; if gradient.interpolation == GradientInterpolation::LinearRGB { has_linear_rgb_gradient = true; svg_path = svg_path.set("filter", "url('#_linearrgb')"); } for record in &gradient.records { let color = if gradient.interpolation == GradientInterpolation::LinearRGB { srgb_to_linear(record.color.clone()) } else { record.color.clone() }; let stop = Stop::new() .set("offset", format!("{}%", f32::from(record.ratio) / 2.55)) .set( "stop-color", format!( "rgba({},{},{},{})", color.r, color.g, color.b, f32::from(color.a) / 255.0 ), ); svg_gradient = svg_gradient.add(stop); } defs = defs.add(svg_gradient); let fill_id = format!("url(#f{})", num_defs); num_defs += 1; fill_id } FillStyle::FocalGradient { gradient, focal_point, } => { let shift = Matrix { a: 32768.0, d: 32768.0, ..Default::default() }; let gradient_matrix = gradient.matrix * shift; let mut svg_gradient = RadialGradient::new() .set("id", format!("f{}", num_defs)) .set("fx", focal_point / 2.0) .set("gradientUnits", "userSpaceOnUse") .set("cx", "0") .set("cy", "0") .set("r", "0.5") .set( "gradientTransform", format!( "matrix({} {} {} {} {} {})", gradient_matrix.a, gradient_matrix.b, gradient_matrix.c, gradient_matrix.d, gradient_matrix.tx.get(), gradient_matrix.ty.get() ), ); svg_gradient = match gradient.spread { GradientSpread::Pad => svg_gradient, // default GradientSpread::Reflect => svg_gradient.set("spreadMethod", "reflect"), GradientSpread::Repeat => svg_gradient.set("spreadMethod", "repeat"), }; if gradient.interpolation == GradientInterpolation::LinearRGB { has_linear_rgb_gradient = true; svg_path = svg_path.set("filter", "url('#_linearrgb')"); } for record in &gradient.records { let color = if gradient.interpolation == GradientInterpolation::LinearRGB { srgb_to_linear(record.color.clone()) } else { record.color.clone() }; let stop = Stop::new() .set("offset", format!("{}%", f32::from(record.ratio) / 2.55)) .set( "stop-color", format!( "rgba({},{},{},{})", color.r, color.g, color.b, f32::from(color.a) / 255.0 ), ); svg_gradient = svg_gradient.add(stop); } defs = defs.add(svg_gradient); let fill_id = format!("url(#f{})", num_defs); num_defs += 1; fill_id } FillStyle::Bitmap { id, matrix, is_smoothed, is_repeating, } => { let (bitmap_data, bitmap_width, bitmap_height) = bitmaps.get(&id).unwrap_or(&("", 0, 0)); if !bitmap_defs.contains(&id) { let mut image = Image::new() .set("width", *bitmap_width) .set("height", *bitmap_height) .set("xlink:href", *bitmap_data); if !*is_smoothed { image = image.set("image-rendering", pixelated_property_value); } let mut bitmap_pattern = Pattern::new() .set("id", format!("b{}", id)) .set("patternUnits", "userSpaceOnUse"); if !*is_repeating { bitmap_pattern = bitmap_pattern .set("width", *bitmap_width) .set("height", *bitmap_height); } else { bitmap_pattern = bitmap_pattern .set("width", *bitmap_width) .set("height", *bitmap_height) .set( "viewBox", format!("0 0 {} {}", bitmap_width, bitmap_height), ); } bitmap_pattern = bitmap_pattern.add(image); defs = defs.add(bitmap_pattern); bitmap_defs.insert(*id); } let svg_pattern = Pattern::new() .set("id", format!("f{}", num_defs)) .set("xlink:href", format!("#b{}", id)) .set( "patternTransform", format!( "matrix({} {} {} {} {} {})", matrix.a, matrix.b, matrix.c, matrix.d, matrix.tx.get(), matrix.ty.get() ), ); defs = defs.add(svg_pattern); let fill_id = format!("url(#f{})", num_defs); num_defs += 1; fill_id } }; svg_path = svg_path.set("fill", fill); let mut data = Data::new(); for command in commands { data = match command { DrawCommand::MoveTo { x, y } => data.move_to((x.get(), y.get())), DrawCommand::LineTo { x, y } => data.line_to((x.get(), y.get())), DrawCommand::CurveTo { x1, y1, x2, y2 } => { data.quadratic_curve_to((x1.get(), y1.get(), x2.get(), y2.get())) } }; } svg_path = svg_path.set("d", data); svg_paths.push(svg_path); } DrawPath::Stroke { style, commands, is_closed, } => { // Flash always renders strokes with a minimum width of 1 pixel (20 twips). // Additionally, many SWFs use the "hairline" stroke setting, which sets the stroke's width // to 1 twip. Because of the minimum, this will effectively make the stroke nearly-always render // as 1 pixel wide. // SVG doesn't have a minimum and can render strokes at fractional widths, so these hairline // strokes end up rendering very faintly if we use the actual width of 1 twip. // Therefore, we clamp the stroke width to 1 pixel (20 twips). This won't be 100% accurate // if the shape is scaled, but it looks much closer to the Flash Player. let stroke_width = std::cmp::max(style.width.get(), 20); let mut svg_path = SvgPath::new(); svg_path = svg_path .set("fill", "none") .set( "stroke", format!( "rgba({},{},{},{})", style.color.r, style.color.g, style.color.b, style.color.a ), ) .set("stroke-width", stroke_width) .set( "stroke-linecap", match style.start_cap { LineCapStyle::Round => "round", LineCapStyle::Square => "square", LineCapStyle::None => "butt", }, ) .set( "stroke-linejoin", match style.join_style { LineJoinStyle::Round => "round", LineJoinStyle::Bevel => "bevel", LineJoinStyle::Miter(_) => "miter", }, ); if let LineJoinStyle::Miter(miter_limit) = style.join_style { svg_path = svg_path.set("stroke-miterlimit", miter_limit); } let mut data = Data::new(); for command in commands { data = match command { DrawCommand::MoveTo { x, y } => data.move_to((x.get(), y.get())), DrawCommand::LineTo { x, y } => data.line_to((x.get(), y.get())), DrawCommand::CurveTo { x1, y1, x2, y2 } => { data.quadratic_curve_to((x1.get(), y1.get(), x2.get(), y2.get())) } }; } if is_closed { data = data.close(); } svg_path = svg_path.set("d", data); svg_paths.push(svg_path); } } } // If this shape contains a gradient in linear RGB space, add a filter to do the color space adjustment. // We have to use a filter because browser don't seem to implement the `color-interpolation` SVG property. if has_linear_rgb_gradient { // Add a filter to convert from linear space to sRGB space. let mut filter = Filter::new(); filter = filter.set("id", "_linearrgb"); filter = filter.set("color-interpolation-filters", "sRGB"); let text = svg::node::Text::new( r#" "#, ); filter = filter.add(text); defs = defs.add(filter); num_defs += 1; } if num_defs > 0 { document = document.add(defs); } for svg_path in svg_paths { document = document.add(svg_path); } use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC}; let svg = document.to_string(); let svg_encoded = format!( "data:image/svg+xml,{}", utf8_percent_encode(&svg, NON_ALPHANUMERIC) ); let image = HtmlImageElement::new().unwrap(); image.set_src(&svg_encoded); let mut data = ShapeData(vec![]); data.0.push(CanvasDrawCommand::DrawImage { image, x_min: shape.shape_bounds.x_min.to_pixels(), y_min: shape.shape_bounds.y_min.to_pixels(), }); data } /// Convert a series of `DrawCommands` to a `Path2d` shape. /// /// The path can be optionally closed by setting `is_closed` to `true`. /// /// The resulting path is in the shape's own coordinate space and needs to be /// transformed to fit within the shape's bounds. fn draw_commands_to_path2d(commands: &[DrawCommand], is_closed: bool) -> Path2d { let path = Path2d::new().unwrap(); for command in commands { match command { DrawCommand::MoveTo { x, y } => path.move_to(x.get().into(), y.get().into()), DrawCommand::LineTo { x, y } => path.line_to(x.get().into(), y.get().into()), DrawCommand::CurveTo { x1, y1, x2, y2 } => path.quadratic_curve_to( x1.get().into(), y1.get().into(), x2.get().into(), y2.get().into(), ), }; } if is_closed { path.close_path(); } path } fn swf_shape_to_canvas_commands( shape: &DistilledShape, bitmaps: &HashMap, _pixelated_property_value: &str, context: &CanvasRenderingContext2d, ) -> Option { use ruffle_core::shape_utils::DrawPath; use swf::{FillStyle, LineCapStyle, LineJoinStyle}; // Some browsers will vomit if you try to load/draw an image with 0 width/height. // TODO(Herschel): Might be better to just return None in this case and skip // rendering altogether. let (_width, _height) = ( f32::max( (shape.shape_bounds.x_max - shape.shape_bounds.x_min).get() as f32, 1.0, ), f32::max( (shape.shape_bounds.y_max - shape.shape_bounds.y_min).get() as f32, 1.0, ), ); let mut canvas_data = ShapeData(vec![]); let matrix_factory: SvgsvgElement = web_sys::window() .expect("window") .document() .expect("document") .create_element_ns(Some("http://www.w3.org/2000/svg"), "svg") .expect("create_element on svg") .dyn_into::() .expect("an actual SVG element"); let bounds_viewbox_matrix = matrix_factory.create_svg_matrix(); bounds_viewbox_matrix.set_a(1.0 / 20.0); bounds_viewbox_matrix.set_d(1.0 / 20.0); for path in &shape.paths { match path { DrawPath::Fill { style, commands } => { let fill_style = match style { FillStyle::Color(Color { r, g, b, a }) => CanvasFillStyle::Color(CanvasColor( format!("rgba({},{},{},{})", r, g, b, f32::from(*a) / 255.0), *r, *g, *b, *a, )), FillStyle::LinearGradient(_gradient) => return None, FillStyle::RadialGradient(_gradient) => return None, FillStyle::FocalGradient { .. } => return None, FillStyle::Bitmap { id, matrix, is_smoothed, is_repeating, } => { let (bitmap_data, bitmap_width, bitmap_height) = bitmaps.get(&id).unwrap_or(&("", 0, 0)); let image = HtmlImageElement::new_with_width_and_height( *bitmap_width, *bitmap_height, ) .expect("html image element"); if !*is_smoothed { //image = image.set("image-rendering", pixelated_property_value); } let repeat = if !*is_repeating { "no-repeat" } else { "repeat" }; let bitmap_pattern = context .create_pattern_with_html_image_element(&image, repeat) .expect("pattern creation success")?; // Set source below the pattern creation because otherwise the bitmap gets screwed up // when cached? (Issue #412) image.set_src(*bitmap_data); let a = *matrix; let matrix = matrix_factory.create_svg_matrix(); matrix.set_a(a.a); matrix.set_b(a.b); matrix.set_c(a.c); matrix.set_d(a.d); matrix.set_e(a.tx.get() as f32); matrix.set_f(a.ty.get() as f32); bitmap_pattern.set_transform(&matrix); CanvasFillStyle::Pattern(bitmap_pattern) } }; let path = Path2d::new().unwrap(); path.add_path_with_transformation( &draw_commands_to_path2d(&commands, false), &bounds_viewbox_matrix, ); canvas_data .0 .push(CanvasDrawCommand::Fill { path, fill_style }); } DrawPath::Stroke { style, commands, is_closed, } => { // Flash always renders strokes with a minimum width of 1 pixel (20 twips). // Additionally, many SWFs use the "hairline" stroke setting, which sets the stroke's width // to 1 twip. Because of the minimum, this will effectively make the stroke nearly-always render // as 1 pixel wide. // SVG doesn't have a minimum and can render strokes at fractional widths, so these hairline // strokes end up rendering very faintly if we use the actual width of 1 twip. // Therefore, we clamp the stroke width to 1 pixel (20 twips). This won't be 100% accurate // if the shape is scaled, but it looks much closer to the Flash Player. let line_width = std::cmp::max(style.width.get(), 20); let stroke_style = CanvasColor( format!( "rgba({},{},{},{})", style.color.r, style.color.g, style.color.b, style.color.a ), style.color.r, style.color.g, style.color.b, style.color.a, ); let line_cap = match style.start_cap { LineCapStyle::Round => "round", LineCapStyle::Square => "square", LineCapStyle::None => "butt", }; let (line_join, miter_limit) = match style.join_style { LineJoinStyle::Round => ("round", 999_999.0), LineJoinStyle::Bevel => ("bevel", 999_999.0), LineJoinStyle::Miter(ml) => ("miter", ml), }; let path = Path2d::new().unwrap(); path.add_path_with_transformation( &draw_commands_to_path2d(&commands, *is_closed), &bounds_viewbox_matrix, ); canvas_data.0.push(CanvasDrawCommand::Stroke { path, line_width: line_width as f64 / 20.0, stroke_style, line_cap: line_cap.to_string(), line_join: line_join.to_string(), miter_limit: miter_limit as f64 / 20.0, }); } } } Some(canvas_data) } /// Converts an SWF color from sRGB space to linear color space. pub fn srgb_to_linear(mut color: swf::Color) -> swf::Color { fn to_linear_channel(n: u8) -> u8 { let mut n = f32::from(n) / 255.0; n = if n <= 0.04045 { n / 12.92 } else { f32::powf((n + 0.055) / 1.055, 2.4) }; (n.max(0.0).min(1.0) * 255.0).round() as u8 } color.r = to_linear_channel(color.r); color.g = to_linear_channel(color.g); color.b = to_linear_channel(color.b); color }