Work
This commit is contained in:
parent
1521fb0bc1
commit
fd8f58c6c2
|
@ -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"
|
||||
|
|
|
@ -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<DrawPath<'a>> {
|
||||
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<DrawCommand>,
|
||||
},
|
||||
Fill {
|
||||
style: &'a FillStyle,
|
||||
commands: Vec<DrawCommand>,
|
||||
},
|
||||
}
|
||||
|
||||
/// `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<Point>,
|
||||
}
|
||||
|
||||
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<Item = DrawCommand> {
|
||||
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<PathSegment>,
|
||||
}
|
||||
|
||||
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<Item = DrawCommand> {
|
||||
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<NonZeroU32, PendingPath>);
|
||||
|
||||
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<ActivePath>,
|
||||
fill_style1: Option<ActivePath>,
|
||||
line_style: Option<ActivePath>,
|
||||
|
||||
// Paths. These get flushed when the shape is complete
|
||||
// and for each new layer.
|
||||
fills: PendingPathMap,
|
||||
strokes: PendingPathMap,
|
||||
|
||||
// Output.
|
||||
commands: Vec<DrawPath<'a>>,
|
||||
}
|
||||
|
||||
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<DrawPath<'a>> {
|
||||
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<ShapeRecord>) -> 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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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"
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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<Arena<PlayerInstance>> = 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<Arena<RuffleInstance>> = RefCell::new(Arena::new());
|
||||
}
|
||||
|
||||
type AnimationHandler = Closure<FnMut(f64)>;
|
||||
|
||||
struct PlayerInstance {
|
||||
struct RuffleInstance {
|
||||
core: ruffle_core::Player,
|
||||
timestamp: f64,
|
||||
animation_handler: Option<AnimationHandler>, // requestAnimationFrame callback
|
||||
animation_handler_id: Option<NonZeroI32>, // 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, JsValue> {
|
||||
Player::new_internal(canvas, swf_data).map_err(|_| "Error creating player".into())
|
||||
impl Ruffle {
|
||||
pub fn new(canvas: HtmlCanvasElement, swf_data: Uint8Array) -> Result<Ruffle, JsValue> {
|
||||
Ruffle::new_internal(canvas, swf_data).map_err(|_| "Error creating player".into())
|
||||
}
|
||||
|
||||
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<Player, Box<Error>> {
|
||||
impl Ruffle {
|
||||
fn new_internal(canvas: HtmlCanvasElement, swf_data: Uint8Array) -> Result<Ruffle, Box<Error>> {
|
||||
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<FnMut(f64)>));
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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<CharacterId, (&str, u32, u32)>) -> 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<CharacterId> = 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()
|
||||
}
|
||||
|
|
|
@ -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<CharacterId, (&str, u32, u32)>) -> 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::<CharacterId>::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<Path>, Vec<Path>) {
|
||||
let mut layers = vec![];
|
||||
let mut paths = HashMap::<u32, Path>::new();
|
||||
let mut stroke_paths = HashMap::<u32, Path>::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<LineStyle>,
|
||||
subpaths: Vec<Subpath>,
|
||||
}
|
||||
|
||||
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<SubpathEdge>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
enum SubpathEdge {
|
||||
Straight(Twips, Twips),
|
||||
Bezier(Twips, Twips, Twips, Twips),
|
||||
}
|
Loading…
Reference in New Issue