From fd8f58c6c28cc59b02f0b319d26a9aa68308afd7 Mon Sep 17 00:00:00 2001 From: Mike Welsh Date: Fri, 24 May 2019 10:25:03 -0700 Subject: [PATCH] Work --- core/Cargo.toml | 1 + core/src/shape_utils.rs | 634 +++++++++++++++++- desktop/Cargo.toml | 4 +- desktop/src/render.rs | 1368 ++++++++++++++++++++++++--------------- web/Cargo.toml | 1 + web/demo/www/index.js | 12 +- web/src/lib.rs | 76 ++- web/src/render.rs | 320 ++++++++- web/src/shape_utils.rs | 566 ---------------- 9 files changed, 1841 insertions(+), 1141 deletions(-) delete mode 100644 web/src/shape_utils.rs diff --git a/core/Cargo.toml b/core/Cargo.toml index 1ae3a9eb3..bad84f152 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -6,6 +6,7 @@ edition = "2018" [dependencies] bitstream-io = "0.8.2" +fnv = "1.0.3" gc = "0.3.3" gc_derive = "0.3.2" generational-arena = "0.2.2" diff --git a/core/src/shape_utils.rs b/core/src/shape_utils.rs index 4d0b33895..3f4a2afb7 100644 --- a/core/src/shape_utils.rs +++ b/core/src/shape_utils.rs @@ -1,6 +1,8 @@ -pub fn calculate_shape_bounds(shape_records: &[swf::ShapeRecord]) -> swf::Rectangle { - use swf::Twips; +use fnv::FnvHashMap; +use std::num::NonZeroU32; +use swf::{FillStyle, LineStyle, ShapeRecord, Twips}; +pub fn calculate_shape_bounds(shape_records: &[swf::ShapeRecord]) -> swf::Rectangle { let mut bounds = swf::Rectangle { x_min: Twips::new(std::i32::MAX), y_min: Twips::new(std::i32::MAX), @@ -55,3 +57,631 @@ pub fn calculate_shape_bounds(shape_records: &[swf::ShapeRecord]) -> swf::Rectan } bounds } + +/// Converts an SWF shape into a list of paths for easy conversion in the rendering backend. +/// Each path represents either a fill or a stroke, and they will be in drawing order from back-to-front. +pub fn swf_shape_to_paths<'a>(shape: &'a swf::Shape) -> Vec> { + ShapeConverter::from_shape(shape).into_commands() +} + +/// `DrawPath` represents a solid fill or a stroke. +/// Fills are always closed paths, while strokes may be open or closed. +/// Closed paths will have the first point equal to the last point. +#[derive(Debug, PartialEq)] +pub enum DrawPath<'a> { + Stroke { + style: &'a LineStyle, + is_closed: bool, + commands: Vec, + }, + Fill { + style: &'a FillStyle, + commands: Vec, + }, +} + +/// `DrawCommands` trace the outline of a path. +/// Fills follow the even-odd fill rule, with opposite winding for holes. +#[derive(Debug, PartialEq)] +pub enum DrawCommand { + MoveTo { + x: Twips, + y: Twips, + }, + LineTo { + x: Twips, + y: Twips, + }, + CurveTo { + x1: Twips, + y1: Twips, + x2: Twips, + y2: Twips, + }, +} + +#[derive(Debug, Copy, Clone)] +struct Point { + x: Twips, + y: Twips, + is_bezier_control: bool, +} + +/// A path segment is a series of edges linked togerther. +/// Fill paths are directed, because the winding determines the fill-rule. +/// Stroke paths are undirected. +#[derive(Debug)] +struct PathSegment { + pub points: Vec, +} + +impl PathSegment { + fn new(start: (Twips, Twips)) -> Self { + Self { + points: vec![Point { + x: start.0, + y: start.1, + is_bezier_control: false, + }], + } + } + + /// Flips the direction of the path segment. + /// Flash fill paths are dual-sided, with fill style 1 indicating the positive side + /// and fill style 0 indicating the negative. We have to flip fill style 0 paths + /// in order to link them to fill style 1 paths. + fn flip(&mut self) { + self.points.reverse(); + } + + /// Adds an edge to the end of the path segment. + fn add_point(&mut self, point: Point) { + self.points.push(point); + } + + fn is_empty(&self) -> bool { + self.points.len() <= 1 + } + + fn start(&self) -> (Twips, Twips) { + let pt = &self.points.first().unwrap(); + (pt.x, pt.y) + } + + fn end(&self) -> (Twips, Twips) { + let pt = &self.points.last().unwrap(); + (pt.x, pt.y) + } + + fn is_closed(&self) -> bool { + self.start() == self.end() + } + + /// Attemps to merge another path segment. + /// One path's start must meet the other path's end. + /// Returns true if the merge is successful. + fn try_merge(&mut self, other: &mut PathSegment, directed: bool) -> bool { + // Note that the merge point will be duplicated, so we want to slice it off one end. [1..] + if other.end() == self.start() { + std::mem::swap(&mut self.points, &mut other.points); + self.points.extend_from_slice(&other.points[1..]); + true + } else if self.end() == other.start() { + self.points.extend_from_slice(&other.points[1..]); + true + } else if !directed && self.end() == other.end() { + other.flip(); + self.points.extend_from_slice(&other.points[1..]); + true + } else if !directed && self.start() == other.start() { + other.flip(); + std::mem::swap(&mut self.points, &mut other.points); + self.points.extend_from_slice(&other.points[1..]); + true + } else { + false + } + } + + fn into_draw_commands(self) -> impl Iterator { + assert!(self.points.len() > 1); + let mut i = self.points.into_iter(); + let first = i.next().unwrap(); + std::iter::once(DrawCommand::MoveTo { + x: first.x, + y: first.y, + }) + .chain(std::iter::from_fn(move || match i.next() { + Some(Point { + is_bezier_control: false, + x, + y, + }) => Some(DrawCommand::LineTo { x, y }), + Some(Point { + is_bezier_control: true, + x, + y, + }) => { + let end = i.next().expect("Bezier without endpoint"); + Some(DrawCommand::CurveTo { + x1: x, + y1: y, + x2: end.x, + y2: end.y, + }) + } + None => None, + })) + } +} + +/// The internal path structure used by ShapeConverter. +/// +/// Each path is uniquely identified by its fill/stroke style. But Flash gives +/// the path edges as an "edge soup" -- they can arrive in an arbitrary order. +/// We have to link the edges together for each path. This structure contains +/// a list of path segment, and each time a path segment is added, it will try +/// to merge it with an existing segment. +#[derive(Debug)] +struct PendingPath { + /// The list of path segments for this fill/stroke. + /// For fills, this should turn into a list of closed paths when the shape is complete. + /// Strokes may or may not be closed. + segments: Vec, +} + +impl PendingPath { + fn new() -> Self { + Self { segments: vec![] } + } + + fn merge_path(&mut self, mut new_segment: PathSegment, directed: bool) { + if !new_segment.is_empty() { + if let Some(i) = self + .segments + .iter_mut() + .position(|segment| segment.try_merge(&mut new_segment, directed)) + { + new_segment = self.segments.swap_remove(i); + self.merge_path(new_segment, directed); + } else { + // Couldn't merge the segment any further to an existing segment. Add it to list. + self.segments.push(new_segment); + } + } + } + + fn into_draw_commands(self) -> impl Iterator { + self.segments + .into_iter() + .map(PathSegment::into_draw_commands) + .flatten() + } +} + +/// `PendingPathMap` maps from style IDs to the path associated with that style. +/// Each path is uniquely identified by its style ID (until the style list changes). +/// Style IDs tend to be sequential, so we just use a `Vec`. +#[derive(Debug)] +pub struct PendingPathMap(FnvHashMap); + +impl PendingPathMap { + fn new() -> Self { + Self(FnvHashMap::default()) + } + + fn merge_path(&mut self, path: ActivePath, directed: bool) { + let pending_path = self.0.entry(path.style_id).or_insert_with(PendingPath::new); + pending_path.merge_path(path.segment, directed); + } +} + +#[derive(Debug)] +pub struct ActivePath { + style_id: NonZeroU32, + segment: PathSegment, +} + +impl ActivePath { + fn new(style_id: NonZeroU32, start: (Twips, Twips)) -> Self { + Self { + style_id, + segment: PathSegment::new(start), + } + } + + fn add_point(&mut self, point: Point) { + self.segment.add_point(point) + } + + fn flip(&mut self) { + self.segment.flip() + } +} + +pub struct ShapeConverter<'a> { + // SWF shape commands. + iter: std::slice::Iter<'a, swf::ShapeRecord>, + + // Pen position. + x: Twips, + y: Twips, + + // Fill styles and line styles. + // These change from StyleChangeRecords, and a flush occurs when these change. + fill_styles: &'a [swf::FillStyle], + line_styles: &'a [swf::LineStyle], + + fill_style0: Option, + fill_style1: Option, + line_style: Option, + + // Paths. These get flushed when the shape is complete + // and for each new layer. + fills: PendingPathMap, + strokes: PendingPathMap, + + // Output. + commands: Vec>, +} + +impl<'a> ShapeConverter<'a> { + const DEFAULT_CAPACITY: usize = 512; + + fn from_shape(shape: &'a swf::Shape) -> Self { + ShapeConverter { + iter: shape.shape.iter(), + + x: Twips::new(0), + y: Twips::new(0), + + fill_styles: &shape.styles.fill_styles, + line_styles: &shape.styles.line_styles, + + fill_style0: None, + fill_style1: None, + line_style: None, + + fills: PendingPathMap::new(), + strokes: PendingPathMap::new(), + + commands: Vec::with_capacity(Self::DEFAULT_CAPACITY), + } + } + + fn into_commands(mut self) -> Vec> { + while let Some(record) = self.iter.next() { + match record { + ShapeRecord::StyleChange(style_change) => { + if let Some((x, y)) = style_change.move_to { + self.x = x; + self.y = y; + // We've lifted the pen, so we're starting a new path. + // Flush the previous path. + self.flush_paths(); + } + + if let Some(ref styles) = style_change.new_styles { + // A new style list is also used to indicate a new drawing layer. + self.flush_layer(); + self.fill_styles = &styles.fill_styles[..]; + self.line_styles = &styles.line_styles[..]; + } + + if let Some(fs) = style_change.fill_style_1 { + if let Some(path) = self.fill_style1.take() { + self.fills.merge_path(path, true); + } + + self.fill_style1 = if fs != 0 { + let id = NonZeroU32::new(fs).unwrap(); + Some(ActivePath::new(id, (self.x, self.y))) + } else { + None + } + } + + if let Some(fs) = style_change.fill_style_0 { + if let Some(mut path) = self.fill_style0.take() { + if !path.segment.is_empty() { + path.flip(); + self.fills.merge_path(path, true); + } + } + + self.fill_style0 = if fs != 0 { + let id = NonZeroU32::new(fs).unwrap(); + Some(ActivePath::new(id, (self.x, self.y))) + } else { + None + } + } + + if let Some(ls) = style_change.line_style { + if let Some(path) = self.line_style.take() { + self.strokes.merge_path(path, false); + } + + self.line_style = if ls != 0 { + let id = NonZeroU32::new(ls).unwrap(); + Some(ActivePath::new(id, (self.x, self.y))) + } else { + None + } + } + } + + ShapeRecord::StraightEdge { delta_x, delta_y } => { + self.x += *delta_x; + self.y += *delta_y; + + self.visit_point(Point { + x: self.x, + y: self.y, + is_bezier_control: false, + }); + } + + ShapeRecord::CurvedEdge { + control_delta_x, + control_delta_y, + anchor_delta_x, + anchor_delta_y, + } => { + let x1 = self.x + *control_delta_x; + let y1 = self.y + *control_delta_y; + + self.visit_point(Point { + x: x1, + y: y1, + is_bezier_control: true, + }); + + let x2 = x1 + *anchor_delta_x; + let y2 = y1 + *anchor_delta_y; + + self.visit_point(Point { + x: x2, + y: y2, + is_bezier_control: false, + }); + + self.x = x2; + self.y = y2; + } + } + } + + // Flush any open paths. + self.flush_layer(); + self.commands + } + + /// Adds a point to the current path for the active fills/strokes. + fn visit_point(&mut self, point: Point) { + if let Some(path) = &mut self.fill_style0 { + path.add_point(point) + } + + if let Some(path) = &mut self.fill_style1 { + path.add_point(point) + } + + if let Some(path) = &mut self.line_style { + path.add_point(point) + } + } + + /// When the pen jumps to a new position, we reset the active path. + fn flush_paths(&mut self) { + // Move the current paths to the active list. + if let Some(path) = self.fill_style1.take() { + self.fill_style1 = Some(ActivePath::new(path.style_id, (self.x, self.y))); + self.fills.merge_path(path, true); + } + + if let Some(mut path) = self.fill_style0.take() { + self.fill_style0 = Some(ActivePath::new(path.style_id, (self.x, self.y))); + if !path.segment.is_empty() { + path.flip(); + self.fills.merge_path(path, true); + } + } + + if let Some(path) = self.line_style.take() { + self.line_style = Some(ActivePath::new(path.style_id, (self.x, self.y))); + self.strokes.merge_path(path, false); + } + } + + /// When a new layer starts, all paths are flushed and turned into drawing commands. + fn flush_layer(self: &mut Self) { + self.flush_paths(); + self.fill_style0 = None; + self.fill_style1 = None; + self.line_style = None; + + //let fills = std::mem::replace(&mut self.fills.0, FnvHashMap::default()); + //let strokes = std::mem::replace(&mut self.strokes.0, FnvHashMap::default()); + + // Draw fills, and then strokes. + for (style_id, path) in self.fills.0.drain() { + assert!(style_id.get() > 0); + let style = &self.fill_styles[style_id.get() as usize - 1]; + self.commands.push(DrawPath::Fill { + style, + commands: path.into_draw_commands().collect(), + }); + } + + // Strokes are drawn last because they always appear on top of fills in the same layer. + // Because path segments can either be open or closed, we convert each stroke segment into + // a separate draw command. + // TODO(Herschel): Open strokes could be grouped together into a single path. + for (style_id, path) in self.strokes.0.drain() { + assert!(style_id.get() > 0); + let style = &self.line_styles[style_id.get() as usize - 1]; + for segment in path.segments { + self.commands.push(DrawPath::Stroke { + style, + is_closed: segment.is_closed(), + commands: segment.into_draw_commands().collect(), + }); + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const FILL_STYLES: [FillStyle; 1] = [FillStyle::Color(swf::Color { + r: 255, + g: 0, + b: 0, + a: 255, + })]; + + const LINE_STYLES: [LineStyle; 0] = []; + + /// Convenience method to quickly make a shape, + fn build_shape(records: Vec) -> swf::Shape { + let bounds = calculate_shape_bounds(&records[..]); + swf::Shape { + version: 2, + id: 1, + shape_bounds: bounds.clone(), + edge_bounds: bounds, + has_fill_winding_rule: false, + has_non_scaling_strokes: false, + has_scaling_strokes: true, + styles: swf::ShapeStyles { + fill_styles: FILL_STYLES.to_vec(), + line_styles: LINE_STYLES.to_vec(), + }, + shape: records, + } + } + + /// A simple solid square. + #[test] + fn basic_shape() { + let shape = build_shape(vec![ + ShapeRecord::StyleChange(swf::StyleChangeData { + move_to: Some((Twips::from_pixels(100.0), Twips::from_pixels(100.0))), + fill_style_0: None, + fill_style_1: Some(1), + line_style: None, + new_styles: None, + }), + ShapeRecord::StraightEdge { + delta_x: Twips::from_pixels(100.0), + delta_y: Twips::from_pixels(0.0), + }, + ShapeRecord::StraightEdge { + delta_x: Twips::from_pixels(0.0), + delta_y: Twips::from_pixels(100.0), + }, + ShapeRecord::StraightEdge { + delta_x: Twips::from_pixels(-100.0), + delta_y: Twips::from_pixels(0.0), + }, + ShapeRecord::StraightEdge { + delta_x: Twips::from_pixels(0.0), + delta_y: Twips::from_pixels(-100.0), + }, + ]); + let commands = swf_shape_to_paths(&shape); + let expected = vec![DrawPath::Fill { + style: &FILL_STYLES[0], + commands: vec![ + DrawCommand::MoveTo { + x: Twips::from_pixels(100.0), + y: Twips::from_pixels(100.0), + }, + DrawCommand::LineTo { + x: Twips::from_pixels(200.0), + y: Twips::from_pixels(100.0), + }, + DrawCommand::LineTo { + x: Twips::from_pixels(200.0), + y: Twips::from_pixels(200.0), + }, + DrawCommand::LineTo { + x: Twips::from_pixels(100.0), + y: Twips::from_pixels(200.0), + }, + DrawCommand::LineTo { + x: Twips::from_pixels(100.0), + y: Twips::from_pixels(100.0), + }, + ], + }]; + assert_eq!(commands, expected); + } + + /// A solid square with one edge flipped (fillstyle0 instead of fillstyle1). + #[test] + fn flipped_edges() { + let shape = build_shape(vec![ + ShapeRecord::StyleChange(swf::StyleChangeData { + move_to: Some((Twips::from_pixels(100.0), Twips::from_pixels(100.0))), + fill_style_0: None, + fill_style_1: Some(1), + line_style: None, + new_styles: None, + }), + ShapeRecord::StraightEdge { + delta_x: Twips::from_pixels(100.0), + delta_y: Twips::from_pixels(0.0), + }, + ShapeRecord::StraightEdge { + delta_x: Twips::from_pixels(0.0), + delta_y: Twips::from_pixels(100.0), + }, + ShapeRecord::StraightEdge { + delta_x: Twips::from_pixels(-100.0), + delta_y: Twips::from_pixels(0.0), + }, + ShapeRecord::StyleChange(swf::StyleChangeData { + move_to: Some((Twips::from_pixels(100.0), Twips::from_pixels(100.0))), + fill_style_0: Some(1), + fill_style_1: Some(0), + line_style: None, + new_styles: None, + }), + ShapeRecord::StraightEdge { + delta_x: Twips::from_pixels(0.0), + delta_y: Twips::from_pixels(100.0), + }, + ]); + let commands = swf_shape_to_paths(&shape); + let expected = vec![DrawPath::Fill { + style: &FILL_STYLES[0], + commands: vec![ + DrawCommand::MoveTo { + x: Twips::from_pixels(100.0), + y: Twips::from_pixels(200.0), + }, + DrawCommand::LineTo { + x: Twips::from_pixels(100.0), + y: Twips::from_pixels(100.0), + }, + DrawCommand::LineTo { + x: Twips::from_pixels(200.0), + y: Twips::from_pixels(100.0), + }, + DrawCommand::LineTo { + x: Twips::from_pixels(200.0), + y: Twips::from_pixels(200.0), + }, + DrawCommand::LineTo { + x: Twips::from_pixels(100.0), + y: Twips::from_pixels(200.0), + }, + ], + }]; + assert_eq!(commands, expected); + } +} diff --git a/desktop/Cargo.toml b/desktop/Cargo.toml index 82c12ad9b..3f969253f 100644 --- a/desktop/Cargo.toml +++ b/desktop/Cargo.toml @@ -11,8 +11,10 @@ glutin = "0.20" env_logger = "0.6.1" generational-arena = "0.2.2" image = "0.21.1" +inflate = "0.4.5" +jpeg-decoder = "0.1.15" log = "0.4" -lyon = "0.13.1" +lyon = { git = "https://github.com/nical/lyon", branch = "tess-flattening" } minimp3 = { git = "https://github.com/germangb/minimp3-rs" } structopt = "0.2.15" winit = "0.19.1" diff --git a/desktop/src/render.rs b/desktop/src/render.rs index 6373b8b9f..f5e889d04 100644 --- a/desktop/src/render.rs +++ b/desktop/src/render.rs @@ -8,9 +8,12 @@ use glium::{ }; use glutin::WindowedContext; use lyon::tessellation::geometry_builder::{BuffersBuilder, VertexBuffers}; -use lyon::{path::PathEvent, tessellation, tessellation::FillTessellator}; +use lyon::{ + path::PathEvent, tessellation, tessellation::FillTessellator, tessellation::StrokeTessellator, +}; use ruffle_core::backend::render::swf::{self, FillStyle, LineStyle}; use ruffle_core::backend::render::{BitmapHandle, Color, RenderBackend, ShapeHandle, Transform}; +use ruffle_core::shape_utils::{DrawCommand, DrawPath}; use std::collections::{HashMap, VecDeque}; use swf::Twips; @@ -19,8 +22,9 @@ pub struct GliumRenderBackend { target: Option, shader_program: glium::Program, gradient_shader_program: glium::Program, + bitmap_shader_program: glium::Program, meshes: Vec, - textures: Vec, + textures: Vec<(swf::CharacterId, Texture)>, movie_width: f32, movie_height: f32, } @@ -60,10 +64,25 @@ impl GliumRenderBackend { }, )?; + let bitmap_shader_program = glium::Program::new( + &display, + ProgramCreationInput::SourceCode { + vertex_shader: TEXTURE_VERTEX_SHADER, + fragment_shader: BITMAP_FRAGMENT_SHADER, + geometry_shader: None, + tessellation_control_shader: None, + tessellation_evaluation_shader: None, + transform_feedback_varyings: None, + outputs_srgb: true, + uses_point_size: false, + }, + )?; + Ok(GliumRenderBackend { display, shader_program, gradient_shader_program, + bitmap_shader_program, target: None, meshes: vec![], textures: vec![], @@ -78,232 +97,553 @@ impl GliumRenderBackend { fn register_shape_internal(&mut self, shape: &swf::Shape) -> ShapeHandle { let handle = ShapeHandle(self.meshes.len()); + let paths = ruffle_core::shape_utils::swf_shape_to_paths(shape); - use lyon::tessellation::FillOptions; + use lyon::tessellation::{FillOptions, StrokeOptions}; let mut mesh = Mesh { draws: vec![] }; - let mut vertices: Vec = vec![]; - let mut indices: Vec = vec![]; + //let mut vertices: Vec = vec![]; + //let mut indices: Vec = vec![]; - let paths = swf_shape_to_lyon_paths(shape); let mut fill_tess = FillTessellator::new(); - + let mut stroke_tess = StrokeTessellator::new(); let mut lyon_mesh: VertexBuffers<_, u32> = VertexBuffers::new(); - for (cmd, path) in paths.clone() { - let color = match cmd { - PathCommandType::Fill(FillStyle::Color(color)) => [ - f32::from(color.r) / 255.0, - f32::from(color.g) / 255.0, - f32::from(color.b) / 255.0, - f32::from(color.a) / 255.0, - ], - PathCommandType::Fill(_) => continue, - PathCommandType::Stroke(_) => continue, - }; - let vertex_ctor = move |vertex: tessellation::FillVertex| Vertex { - position: [vertex.position.x, vertex.position.y], - color, - }; - let mut buffers_builder = BuffersBuilder::new(&mut lyon_mesh, vertex_ctor); - - if let Err(e) = fill_tess.tessellate_path( - path.into_iter(), - &FillOptions::even_odd(), - &mut buffers_builder, - ) { - log::error!("Tessellation failure: {:?}", e); - self.meshes.push(mesh); - return handle; + fn flush_draw( + draw: DrawType, + mesh: &mut Mesh, + lyon_mesh: &mut VertexBuffers, + display: &Display, + ) { + if lyon_mesh.vertices.is_empty() { + return; } - let vert_offset = vertices.len() as u32; - vertices.extend(lyon_mesh.vertices.iter()); - indices.extend(lyon_mesh.indices.iter().map(|&n| n + vert_offset)); - } + let vertex_buffer = glium::VertexBuffer::new(display, &lyon_mesh.vertices[..]).unwrap(); - let vertex_buffer = glium::VertexBuffer::new(&self.display, &vertices[..]).unwrap(); - let index_buffer = glium::IndexBuffer::new( - &self.display, - glium::index::PrimitiveType::TrianglesList, - &indices[..], - ) - .unwrap(); - - mesh.draws.push(Draw { - draw_type: DrawType::Color, - vertex_buffer, - index_buffer, - }); - - fn swf_to_gl_matrix(m: swf::Matrix) -> [[f32; 3]; 3] { - let tx = m.translate_x.get() as f32; - let ty = m.translate_y.get() as f32; - let det = m.scale_x * m.scale_y - m.rotate_skew_1 * m.rotate_skew_0; - let mut a = m.scale_y / det; - let mut b = -m.rotate_skew_1 / det; - let mut c = -(tx * m.scale_y - m.rotate_skew_1 * ty) / det; - let mut d = -m.rotate_skew_0 / det; - let mut e = m.scale_x / det; - let mut f = (tx * m.rotate_skew_0 - m.scale_x * ty) / det; - - a *= 20.0 / 32768.0; - b *= 20.0 / 32768.0; - d *= 20.0 / 32768.0; - e *= 20.0 / 32768.0; - - c /= 32768.0; - f /= 32768.0; - c += 0.5; - f += 0.5; - [[a, d, 0.0], [b, e, 0.0], [c, f, 1.0]] - } - - for (cmd, path) in paths { - let mut lyon_mesh: VertexBuffers<_, u32> = VertexBuffers::new(); - if let PathCommandType::Stroke(_) = cmd { - continue; - } - let gradient_uniforms = match cmd { - PathCommandType::Fill(FillStyle::LinearGradient(gradient)) => { - let mut colors: Vec<[f32; 4]> = Vec::with_capacity(8); - let mut ratios: Vec = Vec::with_capacity(8); - for (i, record) in gradient.records.iter().enumerate() { - colors.push([ - record.color.r as f32 / 255.0, - record.color.g as f32 / 255.0, - record.color.b as f32 / 255.0, - record.color.a as f32 / 255.0, - ]); - ratios.push(record.ratio as f32 / 255.0); - } - - GradientUniforms { - gradient_type: 0, - ratios, - colors, - num_colors: gradient.records.len() as u32, - matrix: swf_to_gl_matrix(gradient.matrix.clone()), - repeat_mode: 0, - focal_point: 0.0, - } - } - PathCommandType::Fill(FillStyle::RadialGradient(gradient)) => { - let mut colors: Vec<[f32; 4]> = Vec::with_capacity(8); - let mut ratios: Vec = Vec::with_capacity(8); - for (i, record) in gradient.records.iter().enumerate() { - colors.push([ - record.color.r as f32 / 255.0, - record.color.g as f32 / 255.0, - record.color.b as f32 / 255.0, - record.color.a as f32 / 255.0, - ]); - ratios.push(record.ratio as f32 / 255.0); - } - - GradientUniforms { - gradient_type: 1, - ratios, - colors, - num_colors: gradient.records.len() as u32, - matrix: swf_to_gl_matrix(gradient.matrix.clone()), - repeat_mode: 0, - focal_point: 0.0, - } - } - PathCommandType::Fill(FillStyle::FocalGradient { - gradient, - focal_point, - }) => { - let mut colors: Vec<[f32; 4]> = Vec::with_capacity(8); - let mut ratios: Vec = Vec::with_capacity(8); - for (i, record) in gradient.records.iter().enumerate() { - colors.push([ - record.color.r as f32 / 255.0, - record.color.g as f32 / 255.0, - record.color.b as f32 / 255.0, - record.color.a as f32 / 255.0, - ]); - ratios.push(record.ratio as f32 / 255.0); - } - - GradientUniforms { - gradient_type: 2, - ratios, - colors, - num_colors: gradient.records.len() as u32, - matrix: swf_to_gl_matrix(gradient.matrix.clone()), - repeat_mode: 0, - focal_point, - } - } - // PathCommandType::Fill(FillStyle::Bitmap { - // id, - // matrix, - // is_repeating, - // is_smoothed, - // }) => { - // let mut colors = [[0.0; 4]; 8]; - // let mut ratios = [0.0; 8]; - // for (i, record) in gradient.records.iter().enumerate() { - // colors[i] = [ - // record.color.r as f32 / 255.0, - // record.color.g as f32 / 255.0, - // record.color.b as f32 / 255.0, - // record.color.a as f32 / 255.0, - // ]; - // ratios[i] = record.ratio as f32 / 255.0; - // } - - // GradientUniforms { - // gradient_type: 0, - // ratios, - // colors, - // num_colors: gradient.records.len() as u32, - // matrix: swf_to_gl_matrix(gradient.matrix.clone()), - // repeat_mode: 0, - // focal_point: 0.0, - // } - // } - PathCommandType::Fill(_) => continue, - PathCommandType::Stroke(_) => continue, - }; - - let vertex_ctor = move |vertex: tessellation::FillVertex| Vertex { - position: [vertex.position.x, vertex.position.y], - color: [0.0, 0.0, 0.0, 0.0], - }; - - let mut buffers_builder = BuffersBuilder::new(&mut lyon_mesh, vertex_ctor); - if let Err(e) = fill_tess.tessellate_path( - path.into_iter(), - &FillOptions::even_odd(), - &mut buffers_builder, - ) { - log::error!("Tessellation failure: {:?}", e); - self.meshes.push(mesh); - return handle; - } - - let vertex_buffer = - glium::VertexBuffer::new(&self.display, &lyon_mesh.vertices[..]).unwrap(); let index_buffer = glium::IndexBuffer::new( - &self.display, + display, glium::index::PrimitiveType::TrianglesList, &lyon_mesh.indices[..], ) .unwrap(); mesh.draws.push(Draw { - draw_type: DrawType::LinearGradient(gradient_uniforms), + draw_type: draw, vertex_buffer, index_buffer, }); + + *lyon_mesh = VertexBuffers::new(); } + for path in paths { + match path { + DrawPath::Fill { style, commands } => match style { + FillStyle::Color(color) => { + let color = [ + f32::from(color.r) / 255.0, + f32::from(color.g) / 255.0, + f32::from(color.b) / 255.0, + f32::from(color.a) / 255.0, + ]; + + let vertex_ctor = move |vertex: tessellation::FillVertex| Vertex { + position: [vertex.position.x, vertex.position.y], + color, + }; + let mut buffers_builder = BuffersBuilder::new(&mut lyon_mesh, vertex_ctor); + + if let Err(e) = fill_tess.tessellate_path( + ruffle_path_to_lyon_path(commands, true), + &FillOptions::even_odd(), + &mut buffers_builder, + ) { + println!("Failure"); + log::error!("Tessellation failure: {:?}", e); + self.meshes.push(mesh); + return handle; + } + } + FillStyle::LinearGradient(gradient) => { + flush_draw(DrawType::Color, &mut mesh, &mut lyon_mesh, &self.display); + + let vertex_ctor = move |vertex: tessellation::FillVertex| Vertex { + position: [vertex.position.x, vertex.position.y], + color: [1.0, 1.0, 1.0, 1.0], + }; + let mut buffers_builder = BuffersBuilder::new(&mut lyon_mesh, vertex_ctor); + + if let Err(e) = fill_tess.tessellate_path( + ruffle_path_to_lyon_path(commands, true), + &FillOptions::even_odd(), + &mut buffers_builder, + ) { + println!("Failure"); + log::error!("Tessellation failure: {:?}", e); + self.meshes.push(mesh); + return handle; + } + + let mut colors: Vec<[f32; 4]> = Vec::with_capacity(8); + let mut ratios: Vec = Vec::with_capacity(8); + for (i, record) in gradient.records.iter().enumerate() { + colors.push([ + record.color.r as f32 / 255.0, + record.color.g as f32 / 255.0, + record.color.b as f32 / 255.0, + record.color.a as f32 / 255.0, + ]); + ratios.push(record.ratio as f32 / 255.0); + } + + let uniforms = GradientUniforms { + gradient_type: 0, + ratios, + colors, + num_colors: gradient.records.len() as u32, + matrix: swf_to_gl_matrix(gradient.matrix.clone()), + repeat_mode: 0, + focal_point: 0.0, + }; + + flush_draw( + DrawType::Gradient(uniforms), + &mut mesh, + &mut lyon_mesh, + &self.display, + ); + } + FillStyle::RadialGradient(gradient) => { + flush_draw(DrawType::Color, &mut mesh, &mut lyon_mesh, &self.display); + + let vertex_ctor = move |vertex: tessellation::FillVertex| Vertex { + position: [vertex.position.x, vertex.position.y], + color: [1.0, 1.0, 1.0, 1.0], + }; + let mut buffers_builder = BuffersBuilder::new(&mut lyon_mesh, vertex_ctor); + + if let Err(e) = fill_tess.tessellate_path( + ruffle_path_to_lyon_path(commands, true), + &FillOptions::even_odd(), + &mut buffers_builder, + ) { + println!("Failure"); + log::error!("Tessellation failure: {:?}", e); + self.meshes.push(mesh); + return handle; + } + + let mut colors: Vec<[f32; 4]> = Vec::with_capacity(8); + let mut ratios: Vec = Vec::with_capacity(8); + for (i, record) in gradient.records.iter().enumerate() { + colors.push([ + record.color.r as f32 / 255.0, + record.color.g as f32 / 255.0, + record.color.b as f32 / 255.0, + record.color.a as f32 / 255.0, + ]); + ratios.push(record.ratio as f32 / 255.0); + } + + let uniforms = GradientUniforms { + gradient_type: 1, + ratios, + colors, + num_colors: gradient.records.len() as u32, + matrix: swf_to_gl_matrix(gradient.matrix.clone()), + repeat_mode: 0, + focal_point: 0.0, + }; + + flush_draw( + DrawType::Gradient(uniforms), + &mut mesh, + &mut lyon_mesh, + &self.display, + ); + } + FillStyle::FocalGradient { + gradient, + focal_point, + } => { + flush_draw(DrawType::Color, &mut mesh, &mut lyon_mesh, &self.display); + + let vertex_ctor = move |vertex: tessellation::FillVertex| Vertex { + position: [vertex.position.x, vertex.position.y], + color: [1.0, 1.0, 1.0, 1.0], + }; + let mut buffers_builder = BuffersBuilder::new(&mut lyon_mesh, vertex_ctor); + + if let Err(e) = fill_tess.tessellate_path( + ruffle_path_to_lyon_path(commands, true), + &FillOptions::even_odd(), + &mut buffers_builder, + ) { + println!("Failure"); + log::error!("Tessellation failure: {:?}", e); + self.meshes.push(mesh); + return handle; + } + + let mut colors: Vec<[f32; 4]> = Vec::with_capacity(8); + let mut ratios: Vec = Vec::with_capacity(8); + for (i, record) in gradient.records.iter().enumerate() { + colors.push([ + record.color.r as f32 / 255.0, + record.color.g as f32 / 255.0, + record.color.b as f32 / 255.0, + record.color.a as f32 / 255.0, + ]); + ratios.push(record.ratio as f32 / 255.0); + } + + let uniforms = GradientUniforms { + gradient_type: 1, + ratios, + colors, + num_colors: gradient.records.len() as u32, + matrix: swf_to_gl_matrix(gradient.matrix.clone()), + repeat_mode: 0, + focal_point: *focal_point, + }; + + flush_draw( + DrawType::Gradient(uniforms), + &mut mesh, + &mut lyon_mesh, + &self.display, + ); + } + FillStyle::Bitmap { id, matrix, .. } => { + flush_draw(DrawType::Color, &mut mesh, &mut lyon_mesh, &self.display); + + let vertex_ctor = move |vertex: tessellation::FillVertex| Vertex { + position: [vertex.position.x, vertex.position.y], + color: [1.0, 1.0, 1.0, 1.0], + }; + let mut buffers_builder = BuffersBuilder::new(&mut lyon_mesh, vertex_ctor); + + if let Err(e) = fill_tess.tessellate_path( + ruffle_path_to_lyon_path(commands, true), + &FillOptions::even_odd(), + &mut buffers_builder, + ) { + println!("Failure"); + log::error!("Tessellation failure: {:?}", e); + self.meshes.push(mesh); + return handle; + } + + let texture = &self + .textures + .iter() + .find(|(other_id, _tex)| *other_id == *id) + .unwrap() + .1; + + let uniforms = BitmapUniforms { + matrix: swf_bitmap_to_gl_matrix( + matrix.clone(), + texture.width, + texture.height, + ), + id: *id, + }; + + flush_draw( + DrawType::Bitmap(uniforms), + &mut mesh, + &mut lyon_mesh, + &self.display, + ); + } + }, + DrawPath::Stroke { + style, + commands, + is_closed, + } => { + let color = [ + f32::from(style.color.r) / 255.0, + f32::from(style.color.g) / 255.0, + f32::from(style.color.b) / 255.0, + f32::from(style.color.a) / 255.0, + ]; + + let vertex_ctor = move |vertex: tessellation::StrokeVertex| Vertex { + position: [vertex.position.x, vertex.position.y], + color, + }; + let mut buffers_builder = BuffersBuilder::new(&mut lyon_mesh, vertex_ctor); + + // TODO(Herschel): 0 width indicates "hairline". + let width = if style.width.to_pixels() >= 1.0 { + style.width.to_pixels() as f32 + } else { + 1.0 + }; + + let mut options = StrokeOptions::default() + .with_line_width(width) + .with_line_join(match style.join_style { + swf::LineJoinStyle::Round => tessellation::LineJoin::Round, + swf::LineJoinStyle::Bevel => tessellation::LineJoin::Bevel, + swf::LineJoinStyle::Miter(_) => tessellation::LineJoin::MiterClip, + }) + .with_start_cap(match style.start_cap { + swf::LineCapStyle::None => tessellation::LineCap::Butt, + swf::LineCapStyle::Round => tessellation::LineCap::Round, + swf::LineCapStyle::Square => tessellation::LineCap::Square, + }) + .with_end_cap(match style.end_cap { + swf::LineCapStyle::None => tessellation::LineCap::Butt, + swf::LineCapStyle::Round => tessellation::LineCap::Round, + swf::LineCapStyle::Square => tessellation::LineCap::Square, + }); + + if let swf::LineJoinStyle::Miter(limit) = style.join_style { + options = options.with_miter_limit(limit); + } + + if let Err(e) = stroke_tess.tessellate_path( + ruffle_path_to_lyon_path(commands, is_closed), + &options, + &mut buffers_builder, + ) { + log::error!("Tessellation failure: {:?}", e); + self.meshes.push(mesh); + return handle; + } + } + _ => (), + } + } + + flush_draw(DrawType::Color, &mut mesh, &mut lyon_mesh, &self.display); + self.meshes.push(mesh); + //lyon_mesh.vertices + + // for cmd in cmds { + // let color = match cmd { + // PathCommandType::Fill(FillStyle::Color(color)) => [ + // f32::from(color.r) / 255.0, + // f32::from(color.g) / 255.0, + // f32::from(color.b) / 255.0, + // f32::from(color.a) / 255.0, + // ], + // PathCommandType::Fill(_) => continue, + // PathCommandType::Stroke(_) => continue, + // }; + // let vertex_ctor = move |vertex: tessellation::FillVertex| Vertex { + // position: [vertex.position.x, vertex.position.y], + // color, + // }; + + // let mut buffers_builder = BuffersBuilder::new(&mut lyon_mesh, vertex_ctor); + + // if let Err(e) = fill_tess.tessellate_path( + // path.into_iter(), + // &FillOptions::even_odd(), + // &mut buffers_builder, + // ) { + // log::error!("Tessellation failure: {:?}", e); + // self.meshes.push(mesh); + // return handle; + // } + + // let vert_offset = vertices.len() as u32; + // vertices.extend(lyon_mesh.vertices.iter()); + // indices.extend(lyon_mesh.indices.iter().map(|&n| n + vert_offset)); + // } + + // let vertex_buffer = glium::VertexBuffer::new(&self.display, &vertices[..]).unwrap(); + // let index_buffer = glium::IndexBuffer::new( + // &self.display, + // glium::index::PrimitiveType::TrianglesList, + // &indices[..], + // ) + // .unwrap(); + + // mesh.draws.push(Draw { + // draw_type: DrawType::Color, + // vertex_buffer, + // index_buffer, + // }); + + // fn swf_to_gl_matrix(m: swf::Matrix) -> [[f32; 3]; 3] { + // let tx = m.translate_x.get() as f32; + // let ty = m.translate_y.get() as f32; + // let det = m.scale_x * m.scale_y - m.rotate_skew_1 * m.rotate_skew_0; + // let mut a = m.scale_y / det; + // let mut b = -m.rotate_skew_1 / det; + // let mut c = -(tx * m.scale_y - m.rotate_skew_1 * ty) / det; + // let mut d = -m.rotate_skew_0 / det; + // let mut e = m.scale_x / det; + // let mut f = (tx * m.rotate_skew_0 - m.scale_x * ty) / det; + + // a *= 20.0 / 32768.0; + // b *= 20.0 / 32768.0; + // d *= 20.0 / 32768.0; + // e *= 20.0 / 32768.0; + + // c /= 32768.0; + // f /= 32768.0; + // c += 0.5; + // f += 0.5; + // [[a, d, 0.0], [b, e, 0.0], [c, f, 1.0]] + // } + + // for (cmd, path) in paths { + // let mut lyon_mesh: VertexBuffers<_, u32> = VertexBuffers::new(); + // if let PathCommandType::Stroke(_) = cmd { + // continue; + // } + // let gradient_uniforms = match cmd { + // PathCommandType::Fill(FillStyle::LinearGradient(gradient)) => { + // let mut colors: Vec<[f32; 4]> = Vec::with_capacity(8); + // let mut ratios: Vec = Vec::with_capacity(8); + // for (i, record) in gradient.records.iter().enumerate() { + // colors.push([ + // record.color.r as f32 / 255.0, + // record.color.g as f32 / 255.0, + // record.color.b as f32 / 255.0, + // record.color.a as f32 / 255.0, + // ]); + // ratios.push(record.ratio as f32 / 255.0); + // } + + // GradientUniforms { + // gradient_type: 0, + // ratios, + // colors, + // num_colors: gradient.records.len() as u32, + // matrix: swf_to_gl_matrix(gradient.matrix.clone()), + // repeat_mode: 0, + // focal_point: 0.0, + // } + // } + // PathCommandType::Fill(FillStyle::RadialGradient(gradient)) => { + // let mut colors: Vec<[f32; 4]> = Vec::with_capacity(8); + // let mut ratios: Vec = Vec::with_capacity(8); + // for (i, record) in gradient.records.iter().enumerate() { + // colors.push([ + // record.color.r as f32 / 255.0, + // record.color.g as f32 / 255.0, + // record.color.b as f32 / 255.0, + // record.color.a as f32 / 255.0, + // ]); + // ratios.push(record.ratio as f32 / 255.0); + // } + + // GradientUniforms { + // gradient_type: 1, + // ratios, + // colors, + // num_colors: gradient.records.len() as u32, + // matrix: swf_to_gl_matrix(gradient.matrix.clone()), + // repeat_mode: 0, + // focal_point: 0.0, + // } + // } + // PathCommandType::Fill(FillStyle::FocalGradient { + // gradient, + // focal_point, + // }) => { + // let mut colors: Vec<[f32; 4]> = Vec::with_capacity(8); + // let mut ratios: Vec = Vec::with_capacity(8); + // for (i, record) in gradient.records.iter().enumerate() { + // colors.push([ + // record.color.r as f32 / 255.0, + // record.color.g as f32 / 255.0, + // record.color.b as f32 / 255.0, + // record.color.a as f32 / 255.0, + // ]); + // ratios.push(record.ratio as f32 / 255.0); + // } + + // GradientUniforms { + // gradient_type: 2, + // ratios, + // colors, + // num_colors: gradient.records.len() as u32, + // matrix: swf_to_gl_matrix(gradient.matrix.clone()), + // repeat_mode: 0, + // focal_point, + // } + // } + // // PathCommandType::Fill(FillStyle::Bitmap { + // // id, + // // matrix, + // // is_repeating, + // // is_smoothed, + // // }) => { + // // let mut colors = [[0.0; 4]; 8]; + // // let mut ratios = [0.0; 8]; + // // for (i, record) in gradient.records.iter().enumerate() { + // // colors[i] = [ + // // record.color.r as f32 / 255.0, + // // record.color.g as f32 / 255.0, + // // record.color.b as f32 / 255.0, + // // record.color.a as f32 / 255.0, + // // ]; + // // ratios[i] = record.ratio as f32 / 255.0; + // // } + + // // GradientUniforms { + // // gradient_type: 0, + // // ratios, + // // colors, + // // num_colors: gradient.records.len() as u32, + // // matrix: swf_to_gl_matrix(gradient.matrix.clone()), + // // repeat_mode: 0, + // // focal_point: 0.0, + // // } + // // } + // PathCommandType::Fill(_) => continue, + // PathCommandType::Stroke(_) => continue, + // }; + + // let vertex_ctor = move |vertex: tessellation::FillVertex| Vertex { + // position: [vertex.position.x, vertex.position.y], + // color: [0.0, 0.0, 0.0, 0.0], + // }; + + // let mut buffers_builder = BuffersBuilder::new(&mut lyon_mesh, vertex_ctor); + // if let Err(e) = fill_tess.tessellate_path( + // path.into_iter(), + // &FillOptions::even_odd(), + // &mut buffers_builder, + // ) { + // log::error!("Tessellation failure: {:?}", e); + // self.meshes.push(mesh); + // return handle; + // } + + // let vertex_buffer = + // glium::VertexBuffer::new(&self.display, &lyon_mesh.vertices[..]).unwrap(); + // let index_buffer = glium::IndexBuffer::new( + // &self.display, + // glium::index::PrimitiveType::TrianglesList, + // &lyon_mesh.indices[..], + // ) + // .unwrap(); + + // mesh.draws.push(Draw { + // draw_type: DrawType::LinearGradient(gradient_uniforms), + // vertex_buffer, + // index_buffer, + // }); + // } + + // self.meshes.push(mesh); + + // handle + // } + handle } } @@ -343,10 +683,32 @@ impl RenderBackend for GliumRenderBackend { fn register_bitmap_jpeg( &mut self, - _id: swf::CharacterId, + id: swf::CharacterId, mut data: &[u8], mut jpeg_tables: &[u8], ) -> BitmapHandle { + // SWF19 p.138: + // "Before version 8 of the SWF file format, SWF files could contain an erroneous header of 0xFF, 0xD9, 0xFF, 0xD8 before the JPEG SOI marker." + // Slice off these bytes if necessary.` + if &data[0..4] == [0xFF, 0xD9, 0xFF, 0xD8] { + data = &data[4..]; + } + + if !jpeg_tables.is_empty() { + if &jpeg_tables[0..4] == [0xFF, 0xD9, 0xFF, 0xD8] { + jpeg_tables = &jpeg_tables[4..]; + } + + let mut full_jpeg = jpeg_tables[..jpeg_tables.len() - 2].to_vec(); + full_jpeg.extend_from_slice(&data[2..]); + + self.register_bitmap_jpeg_2(id, &full_jpeg[..]) + } else { + self.register_bitmap_jpeg_2(id, &data[..]) + } + } + + fn register_bitmap_jpeg_2(&mut self, id: swf::CharacterId, mut data: &[u8]) -> BitmapHandle { // SWF19 p.138: // "Before version 8 of the SWF file format, SWF files could contain an erroneous header of 0xFF, 0xD9, 0xFF, 0xD8 before the JPEG SOI marker." // Slice off these bytes if necessary.` @@ -354,30 +716,116 @@ impl RenderBackend for GliumRenderBackend { data = &data[4..]; } - if jpeg_tables[0..4] == [0xFF, 0xD9, 0xFF, 0xD8] { - jpeg_tables = &jpeg_tables[4..]; - } + let mut decoder = jpeg_decoder::Decoder::new(data); + decoder.read_info().unwrap(); + let metadata = decoder.info().unwrap(); + let decoded_data = decoder.decode().expect("failed to decode image"); + + let image = glium::texture::RawImage2d::from_raw_rgb( + decoded_data, + (metadata.width.into(), metadata.height.into()), + ); - let full_jpeg = ruffle_core::backend::render::glue_swf_jpeg_to_tables(jpeg_tables, data); - let image = image::load(std::io::Cursor::new(&full_jpeg[..]), image::JPEG) - .unwrap() - .to_rgba(); - let image_dimensions = image.dimensions(); - let image = - glium::texture::RawImage2d::from_raw_rgba_reversed(&image.into_raw(), image_dimensions); let texture = glium::texture::Texture2d::new(&self.display, image).unwrap(); let handle = BitmapHandle(self.textures.len()); - self.textures.push(texture); + self.textures.push(( + id, + Texture { + texture, + width: metadata.width.into(), + height: metadata.height.into(), + }, + )); + handle } - fn register_bitmap_jpeg_2(&mut self, id: swf::CharacterId, data: &[u8]) -> BitmapHandle { - unimplemented!() - } - fn register_bitmap_png(&mut self, swf_tag: &swf::DefineBitsLossless) -> BitmapHandle { - unimplemented!() + use std::io::{Read, Write}; + + use inflate::inflate_bytes_zlib; + let mut decoded_data = inflate_bytes_zlib(&swf_tag.data).unwrap(); + match (swf_tag.version, swf_tag.format) { + (1, swf::BitmapFormat::Rgb15) => unimplemented!("15-bit PNG"), + (1, swf::BitmapFormat::Rgb32) => { + let mut i = 0; + while i < decoded_data.len() { + decoded_data[i] = decoded_data[i + 1]; + decoded_data[i + 1] = decoded_data[i + 2]; + decoded_data[i + 2] = decoded_data[i + 3]; + decoded_data[i + 3] = 0xff; + i += 4; + } + } + (2, swf::BitmapFormat::Rgb32) => { + let mut i = 0; + while i < decoded_data.len() { + let alpha = decoded_data[i]; + decoded_data[i] = decoded_data[i + 1]; + decoded_data[i + 1] = decoded_data[i + 2]; + decoded_data[i + 2] = decoded_data[i + 3]; + decoded_data[i + 3] = alpha; + i += 4; + } + } + (2, swf::BitmapFormat::ColorMap8) => { + let mut i = 0; + let padded_width = (swf_tag.width + 0b11) & !0b11; + + let mut palette = Vec::with_capacity(swf_tag.num_colors as usize + 1); + for _ in 0..swf_tag.num_colors + 1 { + palette.push(Color { + r: decoded_data[i], + g: decoded_data[i + 1], + b: decoded_data[i + 2], + a: decoded_data[i + 3], + }); + i += 4; + } + let mut out_data = vec![]; + for _ in 0..swf_tag.height { + for _ in 0..swf_tag.width { + let entry = decoded_data[i] as usize; + if entry < palette.len() { + let color = &palette[entry]; + out_data.push(color.r); + out_data.push(color.g); + out_data.push(color.b); + out_data.push(color.a); + } else { + out_data.push(0); + out_data.push(0); + out_data.push(0); + out_data.push(0); + } + i += 1; + } + i += (padded_width - swf_tag.width) as usize; + } + decoded_data = out_data; + } + _ => unimplemented!(), + } + + let image = glium::texture::RawImage2d::from_raw_rgba( + decoded_data, + (swf_tag.width as u32, swf_tag.height as u32), + ); + + let texture = glium::texture::Texture2d::new(&self.display, image).unwrap(); + + let handle = BitmapHandle(self.textures.len()); + self.textures.push(( + swf_tag.id, + Texture { + texture, + width: swf_tag.width.into(), + height: swf_tag.height.into(), + }, + )); + + handle } fn begin_frame(&mut self) { @@ -457,8 +905,8 @@ impl RenderBackend for GliumRenderBackend { ) .unwrap(); } - DrawType::LinearGradient(gradient_uniforms) => { - let uniforms = AllUniforms { + DrawType::Gradient(gradient_uniforms) => { + let uniforms = GradientUniformsFull { view_matrix, world_matrix, mult_color, @@ -476,11 +924,44 @@ impl RenderBackend for GliumRenderBackend { ) .unwrap(); } + DrawType::Bitmap(bitmap_uniforms) => { + let texture = &self + .textures + .iter() + .find(|(id, _tex)| *id == bitmap_uniforms.id) + .unwrap() + .1; + + let uniforms = BitmapUniformsFull { + view_matrix, + world_matrix, + mult_color, + add_color, + matrix: bitmap_uniforms.matrix, + texture: &texture.texture, + }; + + target + .draw( + &draw.vertex_buffer, + &draw.index_buffer, + &self.bitmap_shader_program, + &uniforms, + &draw_parameters, + ) + .unwrap(); + } } } } } +struct Texture { + width: u32, + height: u32, + texture: glium::Texture2d, +} + #[derive(Copy, Clone, Debug)] struct Vertex { position: [f32; 2], @@ -507,7 +988,7 @@ impl Uniforms for GradientUniforms { "u_gradient_type", UniformValue::SignedInt(self.gradient_type), ); - for i in 0..8 { + for i in 0..self.num_colors as usize { visit( &format!("u_ratios[{}]", i)[..], UniformValue::Float(self.ratios[i]), @@ -524,7 +1005,7 @@ impl Uniforms for GradientUniforms { } #[derive(Clone, Debug)] -struct AllUniforms { +struct GradientUniformsFull { world_matrix: [[f32; 4]; 4], view_matrix: [[f32; 4]; 4], mult_color: [f32; 4], @@ -532,7 +1013,7 @@ struct AllUniforms { gradient: GradientUniforms, } -impl Uniforms for AllUniforms { +impl Uniforms for GradientUniformsFull { fn visit_values<'a, F: FnMut(&str, UniformValue<'a>)>(&'a self, mut visit: F) { visit("world_matrix", UniformValue::Mat4(self.world_matrix)); visit("view_matrix", UniformValue::Mat4(self.view_matrix)); @@ -542,6 +1023,39 @@ impl Uniforms for AllUniforms { } } +#[derive(Clone, Debug)] +struct BitmapUniforms { + matrix: [[f32; 3]; 3], + id: swf::CharacterId, +} + +impl Uniforms for BitmapUniforms { + fn visit_values<'a, F: FnMut(&str, UniformValue<'a>)>(&'a self, mut visit: F) { + visit("u_matrix", UniformValue::Mat3(self.matrix)); + } +} + +#[derive(Clone, Debug)] +struct BitmapUniformsFull<'a> { + world_matrix: [[f32; 4]; 4], + view_matrix: [[f32; 4]; 4], + mult_color: [f32; 4], + add_color: [f32; 4], + matrix: [[f32; 3]; 3], + texture: &'a glium::Texture2d, +} + +impl<'a> Uniforms for BitmapUniformsFull<'a> { + fn visit_values<'v, F: FnMut(&str, UniformValue<'v>)>(&'v self, mut visit: F) { + visit("world_matrix", UniformValue::Mat4(self.world_matrix)); + visit("view_matrix", UniformValue::Mat4(self.view_matrix)); + visit("mult_color", UniformValue::Vec4(self.mult_color)); + visit("add_color", UniformValue::Vec4(self.add_color)); + visit("u_matrix", UniformValue::Mat3(self.matrix)); + visit("u_texture", UniformValue::Texture2d(self.texture, None)); + } +} + const VERTEX_SHADER: &str = r#" #version 140 @@ -658,6 +1172,22 @@ const GRADIENT_FRAGMENT_SHADER: &str = r#" } "#; +const BITMAP_FRAGMENT_SHADER: &str = r#" +#version 140 + uniform vec4 mult_color; + uniform vec4 add_color; + + in vec2 frag_uv; + out vec4 out_color; + + uniform sampler2D u_texture; + + void main() { + vec4 color = texture(u_texture, frag_uv); + out_color = mult_color * color + add_color; + } +"#; + struct Mesh { draws: Vec, } @@ -670,325 +1200,101 @@ struct Draw { enum DrawType { Color, - LinearGradient(GradientUniforms), + Gradient(GradientUniforms), + Bitmap(BitmapUniforms), } fn point(x: Twips, y: Twips) -> lyon::math::Point { lyon::math::Point::new(x.to_pixels() as f32, y.to_pixels() as f32) } -fn swf_shape_to_lyon_paths( - shape: &swf::Shape, -) -> Vec<(PathCommandType, Vec)> { - let cmds = get_paths(shape); - let mut out_paths = vec![]; - let mut prev = point(Default::default(), Default::default()); +fn ruffle_path_to_lyon_path( + commands: Vec, + mut is_closed: bool, +) -> impl Iterator { use lyon::geom::{LineSegment, QuadraticBezierSegment}; - for cmd in cmds { - if let PathCommandType::Fill(_fill_style) = &cmd.command_type { - let mut out_path = vec![]; - for path in cmd.paths { - out_path.push(PathEvent::MoveTo(point(path.start.0, path.start.1))); - prev = point(path.start.0, path.start.1); - for edge in path.edges { - let out_cmd = match edge { - PathEdge::Straight(x, y) => { - let cmd = PathEvent::Line(LineSegment { - from: prev, - to: point(x, y), - }); - prev = point(x, y); - cmd - } - PathEdge::Bezier(x1, y1, x2, y2) => { - let cmd = PathEvent::Quadratic(QuadraticBezierSegment { - from: prev, - ctrl: point(x1, y1), - to: point(x2, y2), - }); - prev = point(x2, y2); - cmd - } - }; - out_path.push(out_cmd); - } - } - out_path.push(PathEvent::Close(LineSegment { - from: prev, - to: prev, - })); - out_paths.push((cmd.command_type.clone(), out_path)); + let mut cur = lyon::math::Point::new(0.0, 0.0); + let mut i = commands.into_iter(); + std::iter::from_fn(move || match i.next() { + Some(DrawCommand::MoveTo { x, y }) => { + cur = point(x, y); + Some(PathEvent::MoveTo(cur)) } - } - out_paths -} - -fn get_paths(shape: &swf::Shape) -> impl Iterator { - let mut x = Twips::new(0); - let mut y = Twips::new(0); - - let mut fill_styles = &shape.styles.fill_styles; - let mut line_styles = &shape.styles.line_styles; - let mut fill_style_0 = 0; - let mut fill_style_1 = 0; - let mut line_style = 0; - - let mut paths: HashMap = HashMap::new(); - let mut strokes: HashMap = HashMap::new(); - - let mut out = vec![]; - - for record in &shape.shape { - use swf::ShapeRecord::*; - match record { - StyleChange(style_change) => { - if let Some((move_x, move_y)) = style_change.move_to { - x = move_x; - y = move_y; - } - - if let Some(i) = style_change.fill_style_0 { - fill_style_0 = i; - } - - if let Some(i) = style_change.fill_style_1 { - fill_style_1 = i; - } - - if let Some(i) = style_change.line_style { - line_style = i; - } - - if let Some(ref new_styles) = style_change.new_styles { - for (id, paths) in paths { - let mut out_paths = vec![]; - for path in paths.open_paths { - out_paths.push(path) - } - out.push(PathCommand { - command_type: paths.command_type.clone(), - paths: out_paths, - }) - } - for (id, paths) in strokes { - for path in paths.open_paths { - out.push(PathCommand { - command_type: paths.command_type.clone(), - paths: vec![path], - }) - } - } - paths = HashMap::new(); - strokes = HashMap::new(); - fill_styles = &new_styles.fill_styles; - line_styles = &new_styles.line_styles; - } - } - - StraightEdge { delta_x, delta_y } => { - if fill_style_0 != 0 { - let path = paths.entry(fill_style_0).or_insert_with(|| { - PendingPaths::new(PathCommandType::Fill( - fill_styles[fill_style_0 as usize - 1].clone(), - )) - }); - path.add_edge((x + *delta_x, y + *delta_y), PathEdge::Straight(x, y)); - } - - if fill_style_1 != 0 { - let path = paths.entry(fill_style_1).or_insert_with(|| { - PendingPaths::new(PathCommandType::Fill( - fill_styles[fill_style_1 as usize - 1].clone(), - )) - }); - path.add_edge((x, y), PathEdge::Straight(x + *delta_x, y + *delta_y)); - } - - if line_style != 0 { - let path = strokes.entry(line_style).or_insert_with(|| { - PendingPaths::new(PathCommandType::Stroke( - line_styles[line_style as usize - 1].clone(), - )) - }); - path.add_edge((x, y), PathEdge::Straight(x + *delta_x, y + *delta_y)); - } - - x += *delta_x; - y += *delta_y; - } - - CurvedEdge { - control_delta_x, - control_delta_y, - anchor_delta_x, - anchor_delta_y, - } => { - if fill_style_0 != 0 { - let path = paths.entry(fill_style_0).or_insert_with(|| { - PendingPaths::new(PathCommandType::Fill( - fill_styles[fill_style_0 as usize - 1].clone(), - )) - }); - path.add_edge( - ( - x + *control_delta_x + *anchor_delta_x, - y + *control_delta_y + *anchor_delta_y, - ), - PathEdge::Bezier(x + *control_delta_x, y + *control_delta_y, x, y), - ); - } - - if fill_style_1 != 0 { - let path = paths.entry(fill_style_1).or_insert_with(|| { - PendingPaths::new(PathCommandType::Fill( - fill_styles[fill_style_1 as usize - 1].clone(), - )) - }); - path.add_edge( - (x, y), - PathEdge::Bezier( - x + *control_delta_x, - y + *control_delta_y, - x + *control_delta_x + *anchor_delta_x, - y + *control_delta_y + *anchor_delta_y, - ), - ); - } - - if line_style != 0 { - let path = strokes.entry(line_style).or_insert_with(|| { - PendingPaths::new(PathCommandType::Stroke( - line_styles[line_style as usize - 1].clone(), - )) - }); - path.add_edge( - (x, y), - PathEdge::Bezier( - x + *control_delta_x, - y + *control_delta_y, - x + *control_delta_x + *anchor_delta_x, - y + *control_delta_y + *anchor_delta_y, - ), - ); - } - - x += *control_delta_x + *anchor_delta_x; - y += *control_delta_y + *anchor_delta_y; + Some(DrawCommand::LineTo { x, y }) => { + let next = point(x, y); + let cmd = PathEvent::Line(LineSegment { + from: cur, + to: next, + }); + cur = next; + Some(cmd) + } + Some(DrawCommand::CurveTo { x1, y1, x2, y2 }) => { + let next = point(x2, y2); + let cmd = PathEvent::Quadratic(QuadraticBezierSegment { + from: cur, + ctrl: point(x1, y1), + to: next, + }); + cur = next; + Some(cmd) + } + None => { + if is_closed { + is_closed = false; + Some(PathEvent::Close(LineSegment { from: cur, to: cur })) + } else { + None } } - } - - for (id, paths) in paths { - if paths.open_paths.is_empty() { - continue; - } - let mut out_paths = vec![]; - for path in paths.open_paths { - out_paths.push(path) - } - out.push(PathCommand { - command_type: paths.command_type.clone(), - paths: out_paths, - }) - } - for (id, paths) in strokes { - if paths.open_paths.is_empty() { - continue; - } - for path in paths.open_paths { - out.push(PathCommand { - command_type: paths.command_type.clone(), - paths: vec![path], - }) - } - } - out.into_iter() + }) } -#[derive(Debug)] -pub struct PathCommand { - command_type: PathCommandType, - paths: Vec, +fn swf_to_gl_matrix(m: swf::Matrix) -> [[f32; 3]; 3] { + let tx = m.translate_x.get() as f32; + let ty = m.translate_y.get() as f32; + let det = m.scale_x * m.scale_y - m.rotate_skew_1 * m.rotate_skew_0; + let mut a = m.scale_y / det; + let mut b = -m.rotate_skew_1 / det; + let mut c = -(tx * m.scale_y - m.rotate_skew_1 * ty) / det; + let mut d = -m.rotate_skew_0 / det; + let mut e = m.scale_x / det; + let mut f = (tx * m.rotate_skew_0 - m.scale_x * ty) / det; + + a *= 20.0 / 32768.0; + b *= 20.0 / 32768.0; + d *= 20.0 / 32768.0; + e *= 20.0 / 32768.0; + + c /= 32768.0; + f /= 32768.0; + c += 0.5; + f += 0.5; + [[a, d, 0.0], [b, e, 0.0], [c, f, 1.0]] } -#[derive(Clone, Debug)] -enum PathCommandType { - Fill(FillStyle), - Stroke(LineStyle), -} - -struct PendingPaths { - command_type: PathCommandType, - open_paths: Vec, -} - -impl PendingPaths { - fn new(command_type: PathCommandType) -> PendingPaths { - Self { - command_type, - open_paths: vec![], - } - } - - fn add_edge(&mut self, start: (Twips, Twips), edge: PathEdge) { - let new_path = Path { - start, - end: match edge { - PathEdge::Straight(x, y) => (x, y), - PathEdge::Bezier(_cx, _cy, ax, ay) => (ax, ay), - }, - - edges: { - let mut edges = VecDeque::new(); - edges.push_back(edge); - edges - }, - }; - - self.merge_subpath(new_path); - } - - fn merge_subpath(&mut self, mut path: Path) { - let mut path_index = None; - for (i, other) in self.open_paths.iter_mut().enumerate() { - if path.end == other.start { - other.start = path.start; - for edge in path.edges.iter().rev() { - other.edges.push_front(*edge); - } - path_index = Some(i); - break; - } - - if other.end == path.start { - other.end = path.end; - other.edges.append(&mut path.edges); - - path_index = Some(i); - break; - } - } - - if let Some(i) = path_index { - let path = self.open_paths.swap_remove(i); - self.merge_subpath(path); - } else { - self.open_paths.push(path); - } - } -} - -#[derive(Debug)] -struct Path { - start: (Twips, Twips), - end: (Twips, Twips), - - edges: VecDeque, -} - -#[derive(Copy, Clone, Debug)] -enum PathEdge { - Straight(Twips, Twips), - Bezier(Twips, Twips, Twips, Twips), +fn swf_bitmap_to_gl_matrix(m: swf::Matrix, bitmap_width: u32, bitmap_height: u32) -> [[f32; 3]; 3] { + let bitmap_width = bitmap_width as f32; + let bitmap_height = bitmap_height as f32; + + let tx = m.translate_x.get() as f32; + let ty = m.translate_y.get() as f32; + let det = m.scale_x * m.scale_y - m.rotate_skew_1 * m.rotate_skew_0; + let mut a = m.scale_y / det; + let mut b = -m.rotate_skew_1 / det; + let mut c = -(tx * m.scale_y - m.rotate_skew_1 * ty) / det; + let mut d = -m.rotate_skew_0 / det; + let mut e = m.scale_x / det; + let mut f = (tx * m.rotate_skew_0 - m.scale_x * ty) / det; + + a *= 20.0 / bitmap_width; + b *= 20.0 / bitmap_width; + d *= 20.0 / bitmap_height; + e *= 20.0 / bitmap_height; + + c /= bitmap_width; + f /= bitmap_height; + + [[a, d, 0.0], [b, e, 0.0], [c, f, 1.0]] } diff --git a/web/Cargo.toml b/web/Cargo.toml index 8d176f98d..62650a27a 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -16,6 +16,7 @@ byteorder = "1.3.1" console_error_panic_hook = { version = "0.1.1", optional = true } console_log = { version = "0.1", optional = true } ruffle_core = { path = "../core" } +fnv = "1.0.3" generational-arena = "0.2.2" inflate = "0.4.5" jpeg-decoder = "0.1.15" diff --git a/web/demo/www/index.js b/web/demo/www/index.js index 3f8a8e9e8..dffb4e607 100644 --- a/web/demo/www/index.js +++ b/web/demo/www/index.js @@ -1,4 +1,4 @@ -import { Player } from "../../pkg/ruffle"; +import { Ruffle } from "../../pkg/ruffle"; let sampleFileInput = document.getElementById("sample-file"); if (sampleFileInput) { @@ -10,7 +10,7 @@ if (localFileInput) { localFileInput.addEventListener("change", localFileSelected, false); } -let player; +let ruffle; if (window.location.search && window.location.search != "") { let urlParams = new URLSearchParams(window.location.search); @@ -54,13 +54,13 @@ let timestamp = 0; let animationHandler; function playSwf(swfData) { - if (player) { - player.destroy(); - player = null; + if (ruffle) { + ruffle.destroy(); + ruffle = null; } let canvas = document.getElementById("player"); if (swfData && canvas) { - player = Player.new(canvas, new Uint8Array(swfData)); + ruffle = Ruffle.new(canvas, new Uint8Array(swfData)); } } \ No newline at end of file diff --git a/web/src/lib.rs b/web/src/lib.rs index 2cbb43702..9e273072e 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -1,45 +1,51 @@ +//! Ruffle web frontend. mod audio; mod render; -mod shape_utils; use crate::{audio::WebAudioBackend, render::WebCanvasRenderBackend}; use generational_arena::{Arena, Index}; use js_sys::Uint8Array; use std::{cell::RefCell, error::Error, num::NonZeroI32}; -use wasm_bindgen::{prelude::*, JsCast, JsValue}; +use wasm_bindgen::{prelude::*, JsValue}; use web_sys::HtmlCanvasElement; thread_local! { - static PLAYERS: RefCell> = RefCell::new(Arena::new()); + /// 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> = RefCell::new(Arena::new()); } type AnimationHandler = Closure; -struct PlayerInstance { +struct RuffleInstance { core: ruffle_core::Player, timestamp: f64, animation_handler: Option, // requestAnimationFrame callback animation_handler_id: Option, // requestAnimationFrame id } +/// An opaque handle to a `RuffleInstance` inside the pool. +/// +/// This type is exported to JS, and is used to interact with the library. #[wasm_bindgen] #[derive(Clone)] -pub struct Player(Index); +pub struct Ruffle(Index); #[wasm_bindgen] -impl Player { - pub fn new(canvas: HtmlCanvasElement, swf_data: Uint8Array) -> Result { - Player::new_internal(canvas, swf_data).map_err(|_| "Error creating player".into()) +impl Ruffle { + pub fn new(canvas: HtmlCanvasElement, swf_data: Uint8Array) -> Result { + Ruffle::new_internal(canvas, swf_data).map_err(|_| "Error creating player".into()) } pub fn destroy(&mut self) -> Result<(), JsValue> { // Remove instance from the active list. - if let Some(player_instance) = PLAYERS.with(|players| { - let mut players = players.borrow_mut(); - players.remove(self.0) + if let Some(instance) = INSTANCES.with(|instances| { + let mut instances = instances.borrow_mut(); + instances.remove(self.0) }) { // Cancel the animation handler, if it's still active. - if let Some(id) = player_instance.animation_handler_id { + if let Some(id) = instance.animation_handler_id { if let Some(window) = web_sys::window() { return window.cancel_animation_frame(id.into()); } @@ -51,8 +57,8 @@ impl Player { } } -impl Player { - fn new_internal(canvas: HtmlCanvasElement, swf_data: Uint8Array) -> Result> { +impl Ruffle { + fn new_internal(canvas: HtmlCanvasElement, swf_data: Uint8Array) -> Result> { console_error_panic_hook::set_once(); let _ = console_log::init_with_level(log::Level::Trace); @@ -83,7 +89,7 @@ impl Player { .now(); // Create instance. - let instance = PlayerInstance { + let instance = RuffleInstance { core, animation_handler: None, animation_handler_id: None, @@ -91,47 +97,49 @@ impl Player { }; // Register the instance and create the animation frame closure. - let mut player = PLAYERS.with(move |players| { - let mut players = players.borrow_mut(); - let index = players.insert(instance); - let player = Player(index); + let mut ruffle = INSTANCES.with(move |instances| { + let mut instances = instances.borrow_mut(); + let index = instances.insert(instance); + let ruffle = Ruffle(index); // Create the animation frame closure. { - let mut player = player.clone(); - let instance = players.get_mut(index).unwrap(); + let mut ruffle = ruffle.clone(); + let instance = instances.get_mut(index).unwrap(); instance.animation_handler = Some(Closure::wrap(Box::new(move |timestamp: f64| { - player.tick(timestamp); + ruffle.tick(timestamp); }) as Box)); } - player + ruffle }); // Do an initial tick to start the animation loop. - player.tick(timestamp); + ruffle.tick(timestamp); - Ok(player) + Ok(ruffle) } fn tick(&mut self, timestamp: f64) { - PLAYERS.with(|players| { - let mut players = players.borrow_mut(); - if let Some(player_instance) = players.get_mut(self.0) { - let dt = timestamp - player_instance.timestamp; - player_instance.timestamp = timestamp; - player_instance.core.tick(dt); + use wasm_bindgen::JsCast; + + INSTANCES.with(|instances| { + let mut instances = instances.borrow_mut(); + if let Some(instance) = instances.get_mut(self.0) { + let dt = timestamp - instance.timestamp; + instance.timestamp = timestamp; + instance.core.tick(dt); // Request next animation frame. - if let Some(handler) = &player_instance.animation_handler { + if let Some(handler) = &instance.animation_handler { let window = web_sys::window().unwrap(); let id = window .request_animation_frame(handler.as_ref().unchecked_ref()) .unwrap(); - player_instance.animation_handler_id = NonZeroI32::new(id); + instance.animation_handler_id = NonZeroI32::new(id); } else { - player_instance.animation_handler_id = None; + instance.animation_handler_id = None; } } }); diff --git a/web/src/render.rs b/web/src/render.rs index 98f05bf31..ac9ee455e 100644 --- a/web/src/render.rs +++ b/web/src/render.rs @@ -127,7 +127,7 @@ impl RenderBackend for WebCanvasRenderBackend { } use url::percent_encoding::{utf8_percent_encode, DEFAULT_ENCODE_SET}; - let svg = crate::shape_utils::swf_shape_to_svg(&shape, &bitmaps); + let svg = swf_shape_to_svg(&shape, &bitmaps); let svg_encoded = format!( "data:image/svg+xml,{}", @@ -392,3 +392,321 @@ impl RenderBackend for WebCanvasRenderBackend { self.context.set_global_alpha(1.0); } } + +fn swf_shape_to_svg(shape: &swf::Shape, bitmaps: &HashMap) -> String { + use ruffle_core::matrix::Matrix; + use ruffle_core::shape_utils::{DrawPath, DrawCommand, swf_shape_to_paths}; + use svg::Document; + use svg::node::element::{ + path::Data, Definitions, Image, LinearGradient, Path as SvgPath, Pattern, RadialGradient, Stop, + }; + use swf::{FillStyle, LineCapStyle, LineJoinStyle}; + use fnv::FnvHashSet; + + // 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(), + ), + ) + .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 svg_paths = vec![]; + let paths = swf_shape_to_paths(shape); + for path in paths { + match path { + DrawPath::Fill { style, commands } => { + let mut svg_path = SvgPath::new(); + + svg_path = svg_path.set( + "fill", + match style { + FillStyle::Color(Color { r, g, b, a }) => { + format!("rgba({},{},{},{})", r, g, b, f32::from(*a) / 255.0) + } + FillStyle::LinearGradient(gradient) => { + let matrix: Matrix = Matrix::from(gradient.matrix.clone()); + let shift = Matrix { + a: 32768.0 / width, + d: 32768.0 / height, + tx: -16384.0, + ty: -16384.0, + ..Default::default() + }; + let gradient_matrix = 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, + gradient_matrix.ty + ), + ); + for record in &gradient.records { + let stop = Stop::new() + .set("offset", format!("{}%", f32::from(record.ratio) / 2.55)) + .set( + "stop-color", + format!( + "rgba({},{},{},{})", + record.color.r, + record.color.g, + record.color.b, + f32::from(record.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 matrix = Matrix::from(gradient.matrix.clone()); + let shift = Matrix { + a: 32768.0 / width, + d: 32768.0 / height, + tx: -16384.0, + ty: -16384.0, + ..Default::default() + }; + let gradient_matrix = matrix * shift; + + let mut svg_gradient = RadialGradient::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, + gradient_matrix.ty + ), + ); + for record in &gradient.records { + let stop = Stop::new() + .set("offset", format!("{}%", f32::from(record.ratio) / 2.55)) + .set( + "stop-color", + format!( + "rgba({},{},{},{})", + record.color.r, record.color.g, record.color.b, record.color.a + ), + ); + 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 matrix = Matrix::from(gradient.matrix.clone()); + let shift = Matrix { + a: 32768.0 / width, + d: 32768.0 / height, + tx: -16384.0, + ty: -16384.0, + ..Default::default() + }; + let gradient_matrix = matrix * shift; + + let mut svg_gradient = RadialGradient::new() + .set("id", format!("f{}", num_defs)) + .set("fx", -focal_point) + .set("gradientUnits", "userSpaceOnUse") + .set( + "gradientTransform", + format!( + "matrix({} {} {} {} {} {})", + gradient_matrix.a, + gradient_matrix.b, + gradient_matrix.c, + gradient_matrix.d, + gradient_matrix.tx, + gradient_matrix.ty + ), + ); + for record in &gradient.records { + let stop = Stop::new() + .set("offset", format!("{}%", f32::from(record.ratio) / 2.55)) + .set( + "stop-color", + format!( + "rgba({},{},{},{})", + record.color.r, record.color.g, record.color.b, record.color.a + ), + ); + 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, .. } => { + let (bitmap_data, bitmap_width, bitmap_height) = + bitmaps.get(&id).unwrap_or(&("", 0, 0)); + + if !bitmap_defs.contains(&id) { + let image = Image::new() + .set("width", *bitmap_width) + .set("height", *bitmap_height) + .set("xlink:href", *bitmap_data); + + let bitmap_pattern = Pattern::new() + .set("id", format!("b{}", id)) + .set("width", *bitmap_width) + .set("height", *bitmap_height) + .set("patternUnits", "userSpaceOnUse") + .add(image); + + defs = defs.add(bitmap_pattern); + bitmap_defs.insert(*id); + } + let a = Matrix::from(matrix.clone()); + let bitmap_matrix = a; + + let svg_pattern = Pattern::new() + .set("id", format!("f{}", num_defs)) + .set("xlink:href", format!("#b{}", id)) + .set( + "patternTransform", + format!( + "matrix({} {} {} {} {} {})", + bitmap_matrix.a, + bitmap_matrix.b, + bitmap_matrix.c, + bitmap_matrix.d, + bitmap_matrix.tx, + bitmap_matrix.ty + ), + ); + + defs = defs.add(svg_pattern); + + let fill_id = format!("url(#f{})", num_defs); + num_defs += 1; + fill_id + } + }); + + 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 } => { + 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", style.width.get()) + .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 num_defs > 0 { + document = document.add(defs); + } + + for svg_path in svg_paths { + document = document.add(svg_path); + } + + document.to_string() +} diff --git a/web/src/shape_utils.rs b/web/src/shape_utils.rs deleted file mode 100644 index 3ab021cca..000000000 --- a/web/src/shape_utils.rs +++ /dev/null @@ -1,566 +0,0 @@ -use ruffle_core::backend::render::swf::{self, CharacterId, Color, FillStyle, LineStyle, Shape}; -use ruffle_core::matrix::Matrix; -use std::collections::{HashMap, HashSet, VecDeque}; -use svg::node::element::{ - path::Data, Definitions, Image, LinearGradient, Path as SvgPath, Pattern, RadialGradient, Stop, -}; -use svg::Document; -use swf::Twips; - -pub fn swf_shape_to_svg(shape: &Shape, bitmaps: &HashMap) -> String { - // 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(), - ), - ) - .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 = HashSet::::new(); - - let mut defs = Definitions::new(); - let mut num_defs = 0; - - let mut svg_paths = vec![]; - let (paths, strokes) = swf_shape_to_paths(shape); - for path in paths { - let mut svg_path = SvgPath::new(); - - svg_path = svg_path.set( - "fill", - match path.fill_style { - FillStyle::Color(Color { r, g, b, a }) => { - format!("rgba({},{},{},{})", r, g, b, f32::from(a) / 255.0) - } - FillStyle::LinearGradient(gradient) => { - let matrix = Matrix::from(gradient.matrix); - let shift = Matrix { - a: 32768.0 / width, - d: 32768.0 / height, - tx: -16384.0, - ty: -16384.0, - ..Default::default() - }; - let gradient_matrix = 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, - gradient_matrix.ty - ), - ); - for record in &gradient.records { - let stop = Stop::new() - .set("offset", format!("{}%", f32::from(record.ratio) / 2.55)) - .set( - "stop-color", - format!( - "rgba({},{},{},{})", - record.color.r, - record.color.g, - record.color.b, - f32::from(record.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 matrix = Matrix::from(gradient.matrix); - let shift = Matrix { - a: 32768.0 / width, - d: 32768.0 / height, - tx: -16384.0, - ty: -16384.0, - ..Default::default() - }; - let gradient_matrix = matrix * shift; - - let mut svg_gradient = RadialGradient::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, - gradient_matrix.ty - ), - ); - for record in &gradient.records { - let stop = Stop::new() - .set("offset", format!("{}%", f32::from(record.ratio) / 2.55)) - .set( - "stop-color", - format!( - "rgba({},{},{},{})", - record.color.r, record.color.g, record.color.b, record.color.a - ), - ); - 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 matrix = Matrix::from(gradient.matrix); - let shift = Matrix { - a: 32768.0 / width, - d: 32768.0 / height, - tx: -16384.0, - ty: -16384.0, - ..Default::default() - }; - let gradient_matrix = matrix * shift; - - let mut svg_gradient = RadialGradient::new() - .set("id", format!("f{}", num_defs)) - .set("fx", -focal_point) - .set("gradientUnits", "userSpaceOnUse") - .set( - "gradientTransform", - format!( - "matrix({} {} {} {} {} {})", - gradient_matrix.a, - gradient_matrix.b, - gradient_matrix.c, - gradient_matrix.d, - gradient_matrix.tx, - gradient_matrix.ty - ), - ); - for record in &gradient.records { - let stop = Stop::new() - .set("offset", format!("{}%", f32::from(record.ratio) / 2.55)) - .set( - "stop-color", - format!( - "rgba({},{},{},{})", - record.color.r, record.color.g, record.color.b, record.color.a - ), - ); - 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, .. } => { - let (bitmap_data, bitmap_width, bitmap_height) = - bitmaps.get(&id).unwrap_or(&("", 0, 0)); - - if !bitmap_defs.contains(&id) { - let image = Image::new() - .set("width", *bitmap_width) - .set("height", *bitmap_height) - .set("xlink:href", *bitmap_data); - - let bitmap_pattern = Pattern::new() - .set("id", format!("b{}", id)) - .set("width", *bitmap_width) - .set("height", *bitmap_height) - .set("patternUnits", "userSpaceOnUse") - .add(image); - - defs = defs.add(bitmap_pattern); - bitmap_defs.insert(id); - } - let a = Matrix::from(matrix); - let mut bitmap_matrix = a; - - let svg_pattern = Pattern::new() - .set("id", format!("f{}", num_defs)) - .set("xlink:href", format!("#b{}", id)) - .set( - "patternTransform", - format!( - "matrix({} {} {} {} {} {})", - bitmap_matrix.a, - bitmap_matrix.b, - bitmap_matrix.c, - bitmap_matrix.d, - bitmap_matrix.tx, - bitmap_matrix.ty - ), - ); - - defs = defs.add(svg_pattern); - - let fill_id = format!("url(#f{})", num_defs); - num_defs += 1; - fill_id - } - }, - ); - - let mut data = Data::new(); - for subpath in &path.subpaths { - //svg_paths.push_str(&format!("M{} {}", subpath.start.0, subpath.start.1)); - data = data.move_to((subpath.start.0.get(), subpath.start.1.get())); - - for edge in &subpath.edges { - match edge { - SubpathEdge::Straight(x, y) => { - data = data.line_to((x.get(), y.get())); - } - SubpathEdge::Bezier(cx, cy, ax, ay) => { - data = data.quadratic_curve_to((cx.get(), cy.get(), ax.get(), ay.get())); - } - } - } - } - - svg_path = svg_path.set("d", data); - svg_paths.push(svg_path); - } - - use swf::{LineCapStyle, LineJoinStyle}; - for stroke in strokes { - let mut svg_path = SvgPath::new(); - let line_style = stroke.line_style.unwrap(); - svg_path = svg_path - .set("fill", "none") - .set( - "stroke", - format!( - "rgba({},{},{},{})", - line_style.color.r, line_style.color.g, line_style.color.b, line_style.color.a - ), - ) - .set("stroke-width", line_style.width.get()) - .set( - "stroke-linecap", - match line_style.start_cap { - LineCapStyle::Round => "round", - LineCapStyle::Square => "square", - LineCapStyle::None => "butt", - }, - ) - .set( - "stroke-linejoin", - match line_style.join_style { - LineJoinStyle::Round => "round", - LineJoinStyle::Bevel => "bevel", - LineJoinStyle::Miter(_) => "miter", - }, - ); - - if let LineJoinStyle::Miter(miter_limit) = line_style.join_style { - svg_path = svg_path.set("stroke-miterlimit", miter_limit); - } - - let mut data = Data::new(); - for subpath in &stroke.subpaths { - data = data.move_to((subpath.start.0.get(), subpath.start.1.get())); - - for edge in &subpath.edges { - match edge { - SubpathEdge::Straight(x, y) => { - data = data.line_to((x.get(), y.get())); - } - SubpathEdge::Bezier(cx, cy, ax, ay) => { - data = data.quadratic_curve_to((cx.get(), cy.get(), ax.get(), ay.get())); - } - } - } - } - - svg_path = svg_path.set("d", data); - svg_paths.push(svg_path); - } - - if num_defs > 0 { - document = document.add(defs); - } - - for svg_path in svg_paths { - document = document.add(svg_path); - } - - document.to_string() -} - -struct Stroke { - path: Path, - line_style: LineStyle, -} - -// TODO(Herschel): Iterater-ize this. -pub fn swf_shape_to_paths(shape: &Shape) -> (Vec, Vec) { - let mut layers = vec![]; - let mut paths = HashMap::::new(); - let mut stroke_paths = HashMap::::new(); - - let mut x = Twips::new(0); - let mut y = Twips::new(0); - - let mut fill_style_0 = 0; - let mut fill_style_1 = 0; - let mut line_style = 0; - let mut fill_styles = &shape.styles.fill_styles; - let mut line_styles = &shape.styles.line_styles; - for record in &shape.shape { - use swf::ShapeRecord::*; - match record { - StyleChange(style_change) => { - if let Some((move_x, move_y)) = style_change.move_to { - x = move_x; - y = move_y; - } - - if let Some(i) = style_change.fill_style_0 { - fill_style_0 = i; - } - - if let Some(i) = style_change.fill_style_1 { - fill_style_1 = i; - } - - if let Some(i) = style_change.line_style { - line_style = i; - } - - if let Some(ref new_styles) = style_change.new_styles { - // TODO - layers.push((paths, stroke_paths)); - paths = HashMap::new(); - stroke_paths = HashMap::new(); - fill_styles = &new_styles.fill_styles; - line_styles = &new_styles.line_styles; - } - } - - StraightEdge { delta_x, delta_y } => { - if fill_style_0 != 0 { - let path = paths.entry(fill_style_0).or_insert_with(|| { - Path::new(fill_styles[fill_style_0 as usize - 1].clone()) - }); - path.add_edge((x + *delta_x, y + *delta_y), SubpathEdge::Straight(x, y)); - } - - if fill_style_1 != 0 { - let path = paths.entry(fill_style_1).or_insert_with(|| { - Path::new(fill_styles[fill_style_1 as usize - 1].clone()) - }); - path.add_edge((x, y), SubpathEdge::Straight(x + *delta_x, y + *delta_y)); - } - - if line_style != 0 { - let path = stroke_paths.entry(line_style).or_insert_with(|| { - Path::new_stroke(line_styles[line_style as usize - 1].clone()) - }); - path.add_edge((x, y), SubpathEdge::Straight(x + *delta_x, y + *delta_y)); - } - - x += *delta_x; - y += *delta_y; - } - - CurvedEdge { - control_delta_x, - control_delta_y, - anchor_delta_x, - anchor_delta_y, - } => { - if fill_style_0 != 0 { - let path = paths.entry(fill_style_0).or_insert_with(|| { - Path::new(fill_styles[fill_style_0 as usize - 1].clone()) - }); - path.add_edge( - ( - x + *control_delta_x + *anchor_delta_x, - y + *control_delta_y + *anchor_delta_y, - ), - SubpathEdge::Bezier(x + *control_delta_x, y + *control_delta_y, x, y), - ); - } - - if fill_style_1 != 0 { - let path = paths.entry(fill_style_1).or_insert_with(|| { - Path::new(fill_styles[fill_style_1 as usize - 1].clone()) - }); - path.add_edge( - (x, y), - SubpathEdge::Bezier( - x + *control_delta_x, - y + *control_delta_y, - x + *control_delta_x + *anchor_delta_x, - y + *control_delta_y + *anchor_delta_y, - ), - ); - } - - if line_style != 0 { - let path = stroke_paths.entry(line_style).or_insert_with(|| { - Path::new_stroke(line_styles[line_style as usize - 1].clone()) - }); - path.add_edge( - (x, y), - SubpathEdge::Bezier( - x + *control_delta_x, - y + *control_delta_y, - x + *control_delta_x + *anchor_delta_x, - y + *control_delta_y + *anchor_delta_y, - ), - ); - } - - x += *control_delta_x + *anchor_delta_x; - y += *control_delta_y + *anchor_delta_y; - } - } - } - - layers.push((paths, stroke_paths)); - - let mut out_paths = vec![]; - let mut out_strokes = vec![]; - for (paths, strokes) in layers { - for (_, path) in paths { - out_paths.push(path); - } - - for (_, stroke) in strokes { - out_strokes.push(stroke); - } - } - - (out_paths, out_strokes) -} - -pub struct Path { - fill_style: FillStyle, - line_style: Option, - subpaths: Vec, -} - -impl Path { - fn new(fill_style: FillStyle) -> Path { - Path { - fill_style, - line_style: None, - subpaths: vec![], - } - } - - fn new_stroke(line_style: LineStyle) -> Path { - Path { - fill_style: FillStyle::Color(Color { - r: 0, - g: 0, - b: 0, - a: 0, - }), - line_style: Some(line_style), - subpaths: vec![], - } - } - - fn add_edge(&mut self, start: (Twips, Twips), edge: SubpathEdge) { - let new_subpath = Subpath { - start, - end: match edge { - SubpathEdge::Straight(x, y) => (x, y), - SubpathEdge::Bezier(_cx, _cy, ax, ay) => (ax, ay), - }, - - edges: { - let mut edges = VecDeque::new(); - edges.push_back(edge); - edges - }, - }; - - self.merge_subpath(new_subpath); - } - - fn merge_subpath(&mut self, mut subpath: Subpath) { - let mut subpath_index = None; - for (i, other) in self.subpaths.iter_mut().enumerate() { - if subpath.end == other.start { - other.start = subpath.start; - for edge in subpath.edges.iter().rev() { - other.edges.push_front(*edge); - } - subpath_index = Some(i); - break; - } - - if other.end == subpath.start { - other.end = subpath.end; - other.edges.append(&mut subpath.edges); - - subpath_index = Some(i); - break; - } - } - - if let Some(i) = subpath_index { - let subpath = self.subpaths.swap_remove(i); - self.merge_subpath(subpath); - } else { - self.subpaths.push(subpath); - } - } -} - -struct Subpath { - start: (Twips, Twips), - end: (Twips, Twips), - - edges: VecDeque, -} - -#[derive(Copy, Clone)] -enum SubpathEdge { - Straight(Twips, Twips), - Bezier(Twips, Twips, Twips, Twips), -}