[WIP] Initial support for rendering graphic symbols from swf files.
This commit is contained in:
parent
966c836d4b
commit
5d698998e9
|
@ -10,9 +10,11 @@ members = [
|
|||
"examples/canvas_moire",
|
||||
"examples/canvas_text",
|
||||
"examples/lottie_basic",
|
||||
"examples/swf_basic",
|
||||
"geometry",
|
||||
"gl",
|
||||
"gpu",
|
||||
"flash",
|
||||
"lottie",
|
||||
"renderer",
|
||||
"simd",
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
[package]
|
||||
name = "swf_basic"
|
||||
version = "0.1.0"
|
||||
authors = ["Jon Hardie <jon@hardiesoft.com>"]
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
gl = "0.6"
|
||||
sdl2 = "0.32"
|
||||
sdl2-sys = "0.32"
|
||||
|
||||
swf-parser = "0.7.0"
|
||||
swf-tree = "0.7.0"
|
||||
|
||||
[dependencies.pathfinder_flash]
|
||||
path = "../../flash"
|
||||
|
||||
[dependencies.pathfinder_geometry]
|
||||
path = "../../geometry"
|
||||
|
||||
[dependencies.pathfinder_gl]
|
||||
path = "../../gl"
|
||||
|
||||
[dependencies.pathfinder_gpu]
|
||||
path = "../../gpu"
|
||||
|
||||
[dependencies.pathfinder_renderer]
|
||||
path = "../../renderer"
|
|
@ -0,0 +1,136 @@
|
|||
// pathfinder/examples/swf_basic/src/main.rs
|
||||
//
|
||||
// Copyright © 2019 The Pathfinder Project Developers.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
||||
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
|
||||
// option. This file may not be copied, modified, or distributed
|
||||
// except according to those terms.
|
||||
|
||||
use pathfinder_geometry::basic::vector::{Vector2F, Vector2I};
|
||||
use pathfinder_geometry::basic::rect::RectF;
|
||||
use pathfinder_gl::{GLDevice, GLVersion};
|
||||
use pathfinder_gpu::resources::FilesystemResourceLoader;
|
||||
use pathfinder_gpu::{ClearParams, Device};
|
||||
use pathfinder_renderer::concurrent::rayon::RayonExecutor;
|
||||
use pathfinder_renderer::concurrent::scene_proxy::SceneProxy;
|
||||
use pathfinder_renderer::gpu::renderer::{DestFramebuffer, Renderer};
|
||||
use pathfinder_renderer::options::{RenderOptions, RenderTransform};
|
||||
use sdl2::event::Event;
|
||||
use sdl2::keyboard::Keycode;
|
||||
use sdl2::video::GLProfile;
|
||||
use pathfinder_renderer::scene::Scene;
|
||||
use pathfinder_flash::{draw_paths_into_scene, process_swf_tags};
|
||||
use std::env;
|
||||
use std::fs::read;
|
||||
use pathfinder_geometry::basic::transform2d::Transform2DF;
|
||||
|
||||
fn main() {
|
||||
let swf_bytes;
|
||||
if let Some(path) = env::args().skip(1).next() {
|
||||
match read(path) {
|
||||
Ok(bytes) => {
|
||||
swf_bytes = bytes;
|
||||
},
|
||||
Err(e) => panic!(e)
|
||||
}
|
||||
} else {
|
||||
// NOTE(jon): This is a version of the ghostscript tiger graphic flattened to a single
|
||||
// layer with no overlapping shapes. This is how artwork is 'natively' created in the Flash
|
||||
// authoring tool when an artist just draws directly onto the canvas (without 'object' mode
|
||||
// turned on, which is the default).
|
||||
// Subsequent shapes with different fills will knock out existing fills where they overlap.
|
||||
// A downside of this in current pathfinder is that cracks are visible between shape fills -
|
||||
// especially obvious if you set the context clear color to #ff00ff or similar.
|
||||
|
||||
// Common speculation as to why the swf format stores vector graphics in this way says that
|
||||
// it is to save on file-size bytes, however in the case of our tiger, it results in a
|
||||
// larger file than the layered version, since the overlapping shapes and strokes create
|
||||
// a lot more geometry. I think a more likely explanation for the choice is that it was
|
||||
// done to reduce overdraw in the software rasterizer running on late 90's era hardware?
|
||||
// Indeed, this mode gives pathfinders' occlusion culling pass nothing to do!
|
||||
let default_tiger = include_bytes!("../swf/tiger-flat.swf");
|
||||
|
||||
// NOTE(jon): This is a version of the same graphic cut and pasted into the Flash authoring
|
||||
// tool from the SVG version loaded in Illustrator. When layered graphics are pasted
|
||||
// into Flash, by default they retain their layering, expressed as groups.
|
||||
// They are still presented as being on a single timeline layer.
|
||||
// They will be drawn back to front in much the same way as the SVG version.
|
||||
|
||||
//let default_tiger = include_bytes!("../tiger.swf");
|
||||
swf_bytes = Vec::from(&default_tiger[..]);
|
||||
}
|
||||
|
||||
let (_, movie): (_, swf_tree::Movie) = swf_parser::parsers::movie::parse_movie(&swf_bytes[..]).unwrap();
|
||||
|
||||
// Set up SDL2.
|
||||
let sdl_context = sdl2::init().unwrap();
|
||||
let video = sdl_context.video().unwrap();
|
||||
|
||||
// Make sure we have at least a GL 3.0 context. Pathfinder requires this.
|
||||
let gl_attributes = video.gl_attr();
|
||||
gl_attributes.set_context_profile(GLProfile::Core);
|
||||
gl_attributes.set_context_version(3, 3);
|
||||
|
||||
// process swf scene
|
||||
// TODO(jon): Since swf is a streaming format, this really wants to be a lazy iterator over
|
||||
// swf frames eventually.
|
||||
let (library, stage) = process_swf_tags(&movie);
|
||||
|
||||
// Open a window.
|
||||
let window_size = Vector2I::new(stage.width(), stage.height());
|
||||
let window = video.window("Minimal example", window_size.x() as u32, window_size.y() as u32)
|
||||
.opengl()
|
||||
.allow_highdpi()
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let pixel_size = Vector2I::new(
|
||||
window.drawable_size().0 as i32,
|
||||
window.drawable_size().1 as i32
|
||||
);
|
||||
let device_pixel_ratio = pixel_size.x() as f32 / window_size.x() as f32;
|
||||
|
||||
// Create the GL context, and make it current.
|
||||
let gl_context = window.gl_create_context().unwrap();
|
||||
gl::load_with(|name| video.gl_get_proc_address(name) as *const _);
|
||||
window.gl_make_current(&gl_context).unwrap();
|
||||
|
||||
// Create a Pathfinder renderer.
|
||||
let mut renderer = Renderer::new(GLDevice::new(GLVersion::GL3, 0),
|
||||
&FilesystemResourceLoader::locate(),
|
||||
DestFramebuffer::full_window(pixel_size));
|
||||
// Clear to swf stage background color.
|
||||
let mut scene = Scene::new();
|
||||
scene.set_view_box(RectF::new(
|
||||
Vector2F::default(),
|
||||
Vector2F::new(
|
||||
stage.width() as f32 * device_pixel_ratio,
|
||||
stage.height() as f32 * device_pixel_ratio)
|
||||
));
|
||||
draw_paths_into_scene(&library, &mut scene);
|
||||
|
||||
// Render the canvas to screen.
|
||||
renderer.device.clear(&ClearParams {
|
||||
color: Some(stage.background_color()),
|
||||
..ClearParams::default()
|
||||
});
|
||||
let scene = SceneProxy::from_scene(scene, RayonExecutor);
|
||||
let mut render_options = RenderOptions::default();
|
||||
let scale_transform = Transform2DF::from_scale(
|
||||
Vector2F::new(device_pixel_ratio, device_pixel_ratio)
|
||||
);
|
||||
render_options.transform = RenderTransform::Transform2D(scale_transform);
|
||||
scene.build_and_render(&mut renderer, render_options);
|
||||
|
||||
window.gl_swap_window();
|
||||
// Wait for a keypress.
|
||||
let mut event_pump = sdl_context.event_pump().unwrap();
|
||||
loop {
|
||||
match event_pump.wait_event() {
|
||||
Event::Quit {..} | Event::KeyDown { keycode: Some(Keycode::Escape), .. } => return,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,21 @@
|
|||
[package]
|
||||
name = "pathfinder_flash"
|
||||
version = "0.1.0"
|
||||
authors = ["Jon Hardie <jon@hardiesoft.com>"]
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
swf-parser = "0.7.0"
|
||||
swf-tree = "0.7.0"
|
||||
|
||||
[dependencies.pathfinder_geometry]
|
||||
path = "../geometry"
|
||||
|
||||
[dependencies.pathfinder_renderer]
|
||||
path = "../renderer"
|
||||
|
||||
[dependencies.pathfinder_gl]
|
||||
path = "../gl"
|
||||
|
||||
[dependencies.pathfinder_gpu]
|
||||
path = "../gpu"
|
|
@ -0,0 +1,207 @@
|
|||
// pathfinder/flash/src/lib.rs
|
||||
//
|
||||
// Copyright © 2019 The Pathfinder Project Developers.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
||||
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
|
||||
// option. This file may not be copied, modified, or distributed
|
||||
// except according to those terms.
|
||||
|
||||
use std::ops::Add;
|
||||
use pathfinder_geometry::color::{ColorU, ColorF};
|
||||
use pathfinder_geometry::outline::{Outline, Contour};
|
||||
use pathfinder_geometry::basic::vector::Vector2F;
|
||||
use pathfinder_geometry::stroke::{OutlineStrokeToFill, StrokeStyle};
|
||||
use pathfinder_renderer::scene::{PathObject, Scene};
|
||||
|
||||
use swf_tree;
|
||||
use swf_tree::tags::SetBackgroundColor;
|
||||
use swf_tree::{Tag, SRgb8, Movie};
|
||||
|
||||
use crate::shapes::{GraphicLayers, PaintOrLine};
|
||||
|
||||
mod shapes;
|
||||
|
||||
type SymbolId = u16;
|
||||
|
||||
// In swf, most values are specified in a fixed point format known as "twips" or twentieths of
|
||||
// a pixel. We store twips in their integer form, as if we were to convert them to floating point
|
||||
// at the beginning of the pipeline it's easy to start running into precision errors when we add
|
||||
// coordinate deltas and then try and compare coords for equality.
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
struct Twips(i32);
|
||||
|
||||
impl Twips {
|
||||
// Divide twips by 20 to get the f32 value, just to be used once all processing
|
||||
// of the swf coords is completed and we want to output.
|
||||
fn as_f32(&self) -> f32 {
|
||||
self.0 as f32 / 20.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Add for Twips {
|
||||
type Output = Twips;
|
||||
fn add(self, rhs: Twips) -> Self {
|
||||
Twips(self.0 + rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
struct Point2<T> {
|
||||
x: T,
|
||||
y: T
|
||||
}
|
||||
|
||||
impl Point2<Twips> {
|
||||
fn as_f32(self: Point2<Twips>) -> Point2<f32> {
|
||||
Point2 {
|
||||
x: self.x.as_f32(),
|
||||
y: self.y.as_f32(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Add for Point2<Twips> {
|
||||
type Output = Self;
|
||||
fn add(self, rhs: Self) -> Self {
|
||||
Point2 { x: self.x + rhs.x, y: self.y + rhs.y }
|
||||
}
|
||||
}
|
||||
|
||||
enum Symbol {
|
||||
Graphic(GraphicLayers),
|
||||
// Timeline, // TODO(jon)
|
||||
}
|
||||
|
||||
pub struct Stage {
|
||||
// TODO(jon): Support some kind of lazy frames iterator.
|
||||
// frames: Timeline,
|
||||
background_color: SRgb8,
|
||||
width: i32,
|
||||
height: i32,
|
||||
}
|
||||
|
||||
impl Stage {
|
||||
pub fn width(&self) -> i32 {
|
||||
self.width
|
||||
}
|
||||
|
||||
pub fn height(&self) -> i32 {
|
||||
self.height
|
||||
}
|
||||
|
||||
pub fn background_color(&self) -> ColorF {
|
||||
ColorU {
|
||||
r: self.background_color.r,
|
||||
g: self.background_color.g,
|
||||
b: self.background_color.b,
|
||||
a: 255,
|
||||
}.to_f32()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub struct SymbolLibrary(Vec<Symbol>);
|
||||
|
||||
impl SymbolLibrary {
|
||||
fn add_symbol(&mut self, symbol: Symbol) {
|
||||
self.0.push(symbol);
|
||||
}
|
||||
|
||||
fn symbols(&self) -> &Vec<Symbol> {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process_swf_tags(movie: &Movie) -> (SymbolLibrary, Stage) {
|
||||
let mut symbol_library = SymbolLibrary(Vec::new());
|
||||
let stage_width = Twips(movie.header.frame_size.x_max);
|
||||
let stage_height = Twips(movie.header.frame_size.y_max);
|
||||
// let num_frames = movie.header.frame_count;
|
||||
|
||||
let mut stage = Stage {
|
||||
// frames: Timeline(Vec::new()), // TODO(jon)
|
||||
background_color: SRgb8 {
|
||||
r: 255,
|
||||
g: 255,
|
||||
b: 255
|
||||
},
|
||||
width: stage_width.as_f32() as i32,
|
||||
height: stage_height.as_f32() as i32,
|
||||
};
|
||||
|
||||
for tag in &movie.tags {
|
||||
match tag {
|
||||
Tag::SetBackgroundColor(SetBackgroundColor { color }) => {
|
||||
stage.background_color = *color;
|
||||
},
|
||||
Tag::DefineShape(shape) => {
|
||||
symbol_library.add_symbol(Symbol::Graphic(shapes::decode_shape(&shape)));
|
||||
// We will assume that symbol ids just go up, and are 1 based.
|
||||
let symbol_id: SymbolId = shape.id;
|
||||
debug_assert!(symbol_id as usize == symbol_library.0.len());
|
||||
}
|
||||
_ => ()
|
||||
}
|
||||
}
|
||||
(symbol_library, stage)
|
||||
}
|
||||
|
||||
#[allow(irrefutable_let_patterns)]
|
||||
pub fn draw_paths_into_scene(library: &SymbolLibrary, scene: &mut Scene) {
|
||||
for symbol in library.symbols() {
|
||||
// NOTE: Right now symbols only contain graphics.
|
||||
if let Symbol::Graphic(graphic) = symbol {
|
||||
for style_layer in graphic.layers() {
|
||||
debug_assert_ne!(style_layer.shapes().len(), 0);
|
||||
let mut path = Outline::new();
|
||||
let paint_id = scene.push_paint(&style_layer.fill());
|
||||
|
||||
for shape in style_layer.shapes() {
|
||||
let mut contour = Contour::new();
|
||||
let Point2 { x, y } = shape.outline.first().unwrap().from.as_f32();
|
||||
contour.push_endpoint(Vector2F::new(x, y));
|
||||
for segment in &shape.outline {
|
||||
let Point2 { x, y } = segment.to.as_f32();
|
||||
match segment.ctrl {
|
||||
Some(ctrl) => {
|
||||
let Point2 { x: ctrl_x, y: ctrl_y } = ctrl.as_f32();
|
||||
contour.push_quadratic(
|
||||
Vector2F::new(ctrl_x, ctrl_y),
|
||||
Vector2F::new(x, y)
|
||||
);
|
||||
}
|
||||
None => {
|
||||
contour.push_endpoint(Vector2F::new(x, y));
|
||||
},
|
||||
}
|
||||
}
|
||||
if shape.is_closed() {
|
||||
// NOTE: I'm not sure if this really does anything in this context,
|
||||
// since all our closed shapes already have coincident start and end points.
|
||||
contour.close();
|
||||
}
|
||||
path.push_contour(contour);
|
||||
}
|
||||
|
||||
if let PaintOrLine::Line(line) = style_layer.kind() {
|
||||
let mut stroke_to_fill = OutlineStrokeToFill::new(&path, StrokeStyle {
|
||||
line_width: line.width.as_f32(),
|
||||
line_cap: line.cap,
|
||||
line_join: line.join,
|
||||
});
|
||||
stroke_to_fill.offset();
|
||||
path = stroke_to_fill.into_outline();
|
||||
}
|
||||
|
||||
scene.push_path(PathObject::new(
|
||||
path,
|
||||
paint_id,
|
||||
String::new()
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,616 @@
|
|||
use pathfinder_renderer::paint::Paint;
|
||||
use pathfinder_geometry::stroke::{LineJoin, LineCap};
|
||||
use crate::{Twips, Point2};
|
||||
use std::mem;
|
||||
use std::cmp::Ordering;
|
||||
use swf_tree::{
|
||||
FillStyle,
|
||||
StraightSRgba8,
|
||||
LineStyle,
|
||||
fill_styles,
|
||||
JoinStyle,
|
||||
CapStyle,
|
||||
join_styles,
|
||||
ShapeRecord,
|
||||
shape_records,
|
||||
Vector2D
|
||||
};
|
||||
use pathfinder_geometry::color::ColorU;
|
||||
use swf_tree::tags::DefineShape;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(crate) struct LineSegment {
|
||||
pub(crate) from: Point2<Twips>,
|
||||
pub(crate) to: Point2<Twips>,
|
||||
pub(crate) ctrl: Option<Point2<Twips>>,
|
||||
}
|
||||
|
||||
impl LineSegment {
|
||||
fn reverse(&mut self) {
|
||||
let tmp = self.from;
|
||||
self.from = self.to;
|
||||
self.to = tmp;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Debug)]
|
||||
pub(crate) enum LineDirection {
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
impl LineDirection {
|
||||
fn reverse(&mut self) {
|
||||
*self = match self {
|
||||
LineDirection::Right => LineDirection::Left,
|
||||
LineDirection::Left => LineDirection::Right
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct Shape {
|
||||
pub(crate) outline: Vec<LineSegment>, // Could be Vec<(start, end)>
|
||||
direction: LineDirection,
|
||||
reversed: bool,
|
||||
}
|
||||
|
||||
impl Shape {
|
||||
pub fn new_with_direction(direction: LineDirection) -> Shape {
|
||||
Shape {
|
||||
direction,
|
||||
outline: Vec::new(),
|
||||
reversed: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn prepend_shape(&mut self, shape: &mut Shape) {
|
||||
shape.append_shape(&self);
|
||||
mem::swap(&mut self.outline, &mut shape.outline);
|
||||
}
|
||||
|
||||
fn append_shape(&mut self, shape: &Shape) {
|
||||
self.outline.extend_from_slice(&shape.outline);
|
||||
}
|
||||
|
||||
fn add_line_segment(&mut self, segment: LineSegment) {
|
||||
self.outline.push(segment);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn len(&self) -> usize {
|
||||
self.outline.len()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn first(&self) -> &LineSegment {
|
||||
&self.outline.first().unwrap()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn last(&self) -> &LineSegment {
|
||||
&self.outline.last().unwrap()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn comes_before(&self, other: &Shape) -> bool {
|
||||
self.last().to == other.first().from
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn comes_after(&self, other: &Shape) -> bool {
|
||||
self.first().from == other.last().to
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn is_closed(&self) -> bool {
|
||||
self.len() > 1 && self.comes_after(self)
|
||||
}
|
||||
|
||||
fn reverse(&mut self) {
|
||||
self.reversed = !self.reversed;
|
||||
self.direction.reverse();
|
||||
for segment in &mut self.outline {
|
||||
segment.reverse();
|
||||
}
|
||||
self.outline.reverse();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct SwfLineStyle {
|
||||
color: Paint,
|
||||
pub(crate) width: Twips,
|
||||
pub(crate) join: LineJoin,
|
||||
pub(crate) cap: LineCap,
|
||||
}
|
||||
|
||||
pub(crate) enum PaintOrLine {
|
||||
Paint(Paint),
|
||||
Line(SwfLineStyle),
|
||||
}
|
||||
|
||||
pub(crate) struct StyleLayer {
|
||||
fill: PaintOrLine,
|
||||
// TODO(jon): Maybe shapes are actually slices into a single buffer, then we don't
|
||||
// need to realloc anything, we're just shuffling shapes around?
|
||||
shapes: Vec<Shape>,
|
||||
}
|
||||
|
||||
impl StyleLayer {
|
||||
pub(crate) fn kind(&self) -> &PaintOrLine {
|
||||
&self.fill
|
||||
}
|
||||
|
||||
fn is_fill(&self) -> bool {
|
||||
match &self.fill {
|
||||
PaintOrLine::Paint(_) => true,
|
||||
PaintOrLine::Line(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn fill(&self) -> Paint {
|
||||
match &self.fill {
|
||||
PaintOrLine::Paint(paint) => *paint,
|
||||
PaintOrLine::Line(line) => line.color,
|
||||
}
|
||||
}
|
||||
|
||||
fn push_new_shape(&mut self, direction: LineDirection) {
|
||||
if let Some(prev_shape) = self.shapes.last_mut() {
|
||||
// Check that the previous shape was actually used, otherwise reuse it.
|
||||
if prev_shape.len() != 0 {
|
||||
self.shapes.push(Shape::new_with_direction(direction))
|
||||
} else {
|
||||
prev_shape.direction = direction;
|
||||
}
|
||||
} else {
|
||||
self.shapes.push(Shape::new_with_direction(direction))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn shapes(&self) -> &Vec<Shape> {
|
||||
&self.shapes
|
||||
}
|
||||
|
||||
fn shapes_mut(&mut self) -> &mut Vec<Shape> {
|
||||
&mut self.shapes
|
||||
}
|
||||
|
||||
fn current_shape_mut(&mut self) -> &mut Shape {
|
||||
self.shapes.last_mut().unwrap()
|
||||
}
|
||||
|
||||
fn consolidate_edges(&mut self) {
|
||||
// Reverse left fill shape fragments in place.
|
||||
{
|
||||
self.shapes
|
||||
.iter_mut()
|
||||
.filter(|frag| frag.direction == LineDirection::Left)
|
||||
.for_each(|frag| frag.reverse());
|
||||
}
|
||||
|
||||
// Sort shapes into [closed...open]
|
||||
if self.is_fill() {
|
||||
// I think sorting is only necessary when we want to have closed shapes,
|
||||
// lines don't really need this?
|
||||
self.shapes.sort_unstable_by(|a, b| {
|
||||
match (a.is_closed(), b.is_closed()) {
|
||||
(true, true) | (false, false) => Ordering::Equal,
|
||||
(true, false) => Ordering::Less,
|
||||
(false, true) => Ordering::Greater,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// A cursor at the index of the first unclosed shape, if any.
|
||||
let first_open_index = self.shapes
|
||||
.iter()
|
||||
.position(|frag| !frag.is_closed());
|
||||
|
||||
if let Some(first_open_index) = first_open_index {
|
||||
if self.shapes.len() - first_open_index >= 2 {
|
||||
// TODO(jon): This might be sped up by doing it in a way that we don't have
|
||||
// to allocate more vecs?
|
||||
// Also, maybe avoid path reversal, and just flag the path as reversed and iterate it
|
||||
// backwards.
|
||||
let unmatched_pieces = find_matches(first_open_index, &mut self.shapes, false);
|
||||
if let Some(mut unmatched_pieces) = unmatched_pieces {
|
||||
if self.is_fill() {
|
||||
// If they didn't match before, they're probably parts of inner shapes
|
||||
// and should be reversed again so they have correct winding
|
||||
let unclosed = find_matches(0, &mut unmatched_pieces, true);
|
||||
// If it's a shape we should always be able to close it.
|
||||
debug_assert!(unclosed.is_none());
|
||||
}
|
||||
for dropped in &mut unmatched_pieces {
|
||||
dropped.reverse();
|
||||
}
|
||||
self.shapes.extend_from_slice(&unmatched_pieces);
|
||||
}
|
||||
// FIXME(jon): Sometimes we don't get the correct winding of internal closed shapes,
|
||||
// need to figure out why this happens.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn get_new_styles<'a>(
|
||||
fills: &'a Vec<FillStyle>,
|
||||
lines: &'a Vec<LineStyle>
|
||||
) -> impl Iterator<Item=PaintOrLine> + 'a {
|
||||
// This enforces the order that fills and line groupings are added in.
|
||||
// Fills always come first.
|
||||
fills.iter().filter_map(|fill_style| {
|
||||
match fill_style {
|
||||
FillStyle::Solid(
|
||||
fill_styles::Solid {
|
||||
color: StraightSRgba8 {
|
||||
r,
|
||||
g,
|
||||
b,
|
||||
a
|
||||
}
|
||||
}
|
||||
) => {
|
||||
Some(PaintOrLine::Paint(Paint {
|
||||
color: ColorU {
|
||||
r: *r,
|
||||
g: *g,
|
||||
b: *b,
|
||||
a: *a
|
||||
}
|
||||
}))
|
||||
},
|
||||
_ => unimplemented!("Unimplemented fill style")
|
||||
}
|
||||
}).chain(
|
||||
lines.iter().filter_map(|LineStyle {
|
||||
width,
|
||||
fill,
|
||||
join,
|
||||
start_cap,
|
||||
end_cap: _,
|
||||
/*
|
||||
TODO(jon): Handle these cases?
|
||||
pub no_h_scale: bool,
|
||||
pub no_v_scale: bool,
|
||||
pub no_close: bool,
|
||||
pub pixel_hinting: bool,
|
||||
*/
|
||||
..
|
||||
}| {
|
||||
if let FillStyle::Solid(fill_styles::Solid {
|
||||
color: StraightSRgba8 {
|
||||
r,
|
||||
g,
|
||||
b,
|
||||
a
|
||||
}
|
||||
}) = fill {
|
||||
// NOTE: PathFinder doesn't support different cap styles for start and end of
|
||||
// strokes, so lets assume that they're always the same for the inputs we care about.
|
||||
// Alternately, we split a line in two with a diff cap style for each.
|
||||
// assert_eq!(start_cap, end_cap);
|
||||
Some(PaintOrLine::Line(SwfLineStyle {
|
||||
width: Twips(*width as i32),
|
||||
color: Paint { color: ColorU { r: *r, g: *g, b: *b, a: *a } },
|
||||
join: match join {
|
||||
JoinStyle::Bevel => LineJoin::Bevel,
|
||||
JoinStyle::Round => LineJoin::Round,
|
||||
JoinStyle::Miter(join_styles::Miter { limit }) => {
|
||||
LineJoin::Miter(*limit as f32)
|
||||
},
|
||||
},
|
||||
cap: match start_cap {
|
||||
CapStyle::None => LineCap::Butt,
|
||||
CapStyle::Square => LineCap::Square,
|
||||
CapStyle::Round => LineCap::Round,
|
||||
},
|
||||
}))
|
||||
} else {
|
||||
unimplemented!("unimplemented line fill style");
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn decode_shape(shape: &DefineShape) -> GraphicLayers {
|
||||
let DefineShape {
|
||||
shape,
|
||||
// id,
|
||||
// has_fill_winding, NOTE(jon): Could be important for some inputs?
|
||||
// has_non_scaling_strokes,
|
||||
// has_scaling_strokes,
|
||||
..
|
||||
} = shape;
|
||||
let mut graphic = GraphicLayers::new();
|
||||
let mut current_line_style = None;
|
||||
let mut current_left_fill = None;
|
||||
let mut current_right_fill = None;
|
||||
let mut prev_pos = None;
|
||||
|
||||
let mut some_fill_set = false;
|
||||
let mut both_fills_set;
|
||||
let mut both_fills_same = false;
|
||||
let mut both_fills_set_and_same = false;
|
||||
|
||||
// Create style groups for initially specified fills and lines.
|
||||
for fills_or_line in get_new_styles(&shape.initial_styles.fill, &shape.initial_styles.line) {
|
||||
match fills_or_line {
|
||||
PaintOrLine::Paint(fill) => graphic.begin_fill_style(fill),
|
||||
PaintOrLine::Line(line) => graphic.begin_line_style(line),
|
||||
}
|
||||
}
|
||||
|
||||
for record in &shape.records {
|
||||
match record {
|
||||
ShapeRecord::StyleChange(
|
||||
shape_records::StyleChange {
|
||||
move_to,
|
||||
new_styles,
|
||||
line_style,
|
||||
left_fill,
|
||||
right_fill,
|
||||
}
|
||||
) => {
|
||||
// Start a whole new style grouping.
|
||||
if let Some(new_style) = new_styles {
|
||||
// Consolidate current style grouping and begin a new one.
|
||||
graphic.end_style_group();
|
||||
graphic.begin_style_group();
|
||||
for fills_or_line in get_new_styles(&new_style.fill, &new_style.line) {
|
||||
match fills_or_line {
|
||||
PaintOrLine::Paint(fill) => graphic.begin_fill_style(fill),
|
||||
PaintOrLine::Line(line) => graphic.begin_line_style(line),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there's a change in right fill
|
||||
if let Some(fill_id) = right_fill {
|
||||
if *fill_id == 0 {
|
||||
current_right_fill = None;
|
||||
} else {
|
||||
current_right_fill = Some(*fill_id);
|
||||
graphic
|
||||
.with_fill_style_mut(*fill_id)
|
||||
.unwrap()
|
||||
.push_new_shape(LineDirection::Right);
|
||||
}
|
||||
}
|
||||
// If there's a change in left fill
|
||||
if let Some(fill_id) = left_fill {
|
||||
if *fill_id == 0 {
|
||||
current_left_fill = None;
|
||||
} else {
|
||||
current_left_fill = Some(*fill_id);
|
||||
graphic
|
||||
.with_fill_style_mut(*fill_id)
|
||||
.unwrap()
|
||||
.push_new_shape(LineDirection::Left);
|
||||
}
|
||||
}
|
||||
|
||||
some_fill_set = current_left_fill.is_some() || current_right_fill.is_some();
|
||||
both_fills_set = current_left_fill.is_some() && current_right_fill.is_some();
|
||||
both_fills_same = current_left_fill == current_right_fill;
|
||||
both_fills_set_and_same = both_fills_set && both_fills_same;
|
||||
|
||||
// If there's a change in line style
|
||||
if let Some(style_id) = line_style {
|
||||
if *style_id == 0 {
|
||||
current_line_style = None;
|
||||
} else {
|
||||
current_line_style = Some(*style_id);
|
||||
graphic
|
||||
.with_line_style_mut(*style_id)
|
||||
.unwrap()
|
||||
.push_new_shape(LineDirection::Right);
|
||||
}
|
||||
}
|
||||
|
||||
// Move to, start new shape fragments with the current styles.
|
||||
if let Some(Vector2D { x, y }) = move_to {
|
||||
let to: Point2<Twips> = Point2 { x: Twips(*x), y: Twips(*y) };
|
||||
prev_pos = Some(to);
|
||||
|
||||
// If we didn't start a new shape for the current fill due to a fill
|
||||
// style change earlier, we definitely want to start a new shape now,
|
||||
// since each move_to command indicates a new shape fragment.
|
||||
if let Some(current_right_fill) = current_right_fill {
|
||||
graphic
|
||||
.with_fill_style_mut(current_right_fill)
|
||||
.unwrap()
|
||||
.push_new_shape(LineDirection::Right);
|
||||
}
|
||||
if let Some(current_left_fill) = current_left_fill {
|
||||
graphic
|
||||
.with_fill_style_mut(current_left_fill)
|
||||
.unwrap()
|
||||
.push_new_shape(LineDirection::Left);
|
||||
}
|
||||
if let Some(current_line_style) = current_line_style {
|
||||
// TODO(jon): Does the direction of this line depend on the current
|
||||
// fill directions?
|
||||
graphic
|
||||
.with_line_style_mut(current_line_style)
|
||||
.unwrap()
|
||||
.push_new_shape(LineDirection::Right);
|
||||
}
|
||||
}
|
||||
},
|
||||
ShapeRecord::Edge(
|
||||
shape_records::Edge {
|
||||
delta,
|
||||
control_delta,
|
||||
}
|
||||
) => {
|
||||
let from = prev_pos.unwrap();
|
||||
let to = Point2 {
|
||||
x: from.x + Twips(delta.x),
|
||||
y: from.y + Twips(delta.y)
|
||||
};
|
||||
prev_pos = Some(to);
|
||||
let new_segment = LineSegment {
|
||||
from,
|
||||
to,
|
||||
ctrl: control_delta.map(|Vector2D { x, y }| {
|
||||
Point2 {
|
||||
x: from.x + Twips(x),
|
||||
y: from.y + Twips(y),
|
||||
}
|
||||
}),
|
||||
};
|
||||
if some_fill_set && !both_fills_same {
|
||||
for fill_id in [
|
||||
current_right_fill,
|
||||
current_left_fill
|
||||
].iter() {
|
||||
if let Some(fill_id) = fill_id {
|
||||
graphic
|
||||
.with_fill_style_mut(*fill_id)
|
||||
.unwrap()
|
||||
.current_shape_mut()
|
||||
.add_line_segment(new_segment);
|
||||
}
|
||||
}
|
||||
} else if both_fills_set_and_same {
|
||||
for (fill_id, direction) in [
|
||||
(current_right_fill, LineDirection::Right),
|
||||
(current_left_fill, LineDirection::Left)
|
||||
].iter() {
|
||||
// NOTE: If both left and right fill are set the same,
|
||||
// then we don't record the edge as part of the current shape;
|
||||
// it's will just be an internal stroke inside an otherwise solid
|
||||
// shape, and recording these edges as part of the shape means that
|
||||
// we can't determine the closed shape outline later.
|
||||
if let Some(fill_id) = fill_id {
|
||||
graphic
|
||||
.with_fill_style_mut(*fill_id)
|
||||
.unwrap()
|
||||
.push_new_shape(*direction);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(current_line_style) = current_line_style {
|
||||
graphic
|
||||
.with_line_style_mut(current_line_style)
|
||||
.unwrap()
|
||||
.current_shape_mut()
|
||||
.add_line_segment(new_segment);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// NOTE: Consolidate current group of styles, joining edges of shapes/strokes where
|
||||
// possible and forming closed shapes. In swf, all filled shapes should always be closed,
|
||||
// so there will always be a solution for joining shape line segments together so that
|
||||
// the start point and end point are coincident.
|
||||
graphic.end_style_group();
|
||||
graphic
|
||||
}
|
||||
|
||||
fn find_matches(
|
||||
mut first_open_index: usize,
|
||||
shapes: &mut Vec<Shape>,
|
||||
reverse: bool
|
||||
) -> Option<Vec<Shape>> {
|
||||
let mut dropped_pieces = None;
|
||||
while first_open_index < shapes.len() {
|
||||
// Take the last unclosed value, and try to join it onto
|
||||
// one of the other unclosed values.
|
||||
let mut last = shapes.pop().unwrap();
|
||||
if reverse {
|
||||
last.reverse();
|
||||
}
|
||||
let mut found_match = false;
|
||||
for i in first_open_index..shapes.len() {
|
||||
let fragment = &mut shapes[i];
|
||||
debug_assert!(!fragment.is_closed());
|
||||
if last.comes_after(fragment) {
|
||||
// NOTE(jon): We do realloc quite a bit here, I wonder if it's worth trying
|
||||
// to avoid that? Could do it with another level of indirection, where an outline
|
||||
// is a list of fragments.
|
||||
|
||||
// println!("app ({}, {})", last.reversed, fragment.reversed);
|
||||
fragment.append_shape(&last);
|
||||
found_match = true;
|
||||
} else if last.comes_before(fragment) {
|
||||
// println!("pre ({}, {})", last.reversed, fragment.reversed);
|
||||
fragment.prepend_shape(&mut last);
|
||||
found_match = true;
|
||||
}
|
||||
if found_match {
|
||||
if fragment.is_closed() {
|
||||
// Move the shape that was just closed to the left side of the current slice,
|
||||
// and advance the cursor.
|
||||
shapes.swap(first_open_index, i);
|
||||
first_open_index += 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !found_match {
|
||||
// Have we tried matching a reversed version of this segment?
|
||||
// move last back onto the array, it will never be closed, presumably because
|
||||
// it's a set of line segments rather than a shape that needs to be closed.
|
||||
let dropped_pieces: &mut Vec<Shape> = dropped_pieces.get_or_insert(Vec::new());
|
||||
dropped_pieces.push(last);
|
||||
}
|
||||
}
|
||||
dropped_pieces
|
||||
}
|
||||
|
||||
pub(crate) struct GraphicLayers {
|
||||
style_layers: Vec<StyleLayer>,
|
||||
base_layer_offset: usize,
|
||||
stroke_layer_offset: Option<usize>,
|
||||
}
|
||||
|
||||
impl GraphicLayers {
|
||||
fn new() -> GraphicLayers {
|
||||
GraphicLayers { style_layers: Vec::new(), stroke_layer_offset: None, base_layer_offset: 0 }
|
||||
}
|
||||
|
||||
fn begin_style_group(&mut self) {
|
||||
self.stroke_layer_offset = None;
|
||||
self.base_layer_offset = self.style_layers.len();
|
||||
}
|
||||
|
||||
fn begin_fill_style(&mut self, fill: Paint) {
|
||||
self.style_layers.push(StyleLayer { fill: PaintOrLine::Paint(fill), shapes: Vec::new() })
|
||||
}
|
||||
|
||||
fn begin_line_style(&mut self, line: SwfLineStyle) {
|
||||
if self.stroke_layer_offset.is_none() {
|
||||
self.stroke_layer_offset = Some(self.style_layers.len());
|
||||
}
|
||||
self.style_layers.push(StyleLayer { fill: PaintOrLine::Line(line), shapes: Vec::new() })
|
||||
}
|
||||
|
||||
fn with_fill_style_mut(&mut self, fill_id: usize) -> Option<&mut StyleLayer> {
|
||||
self.style_layers.get_mut(self.base_layer_offset + fill_id - 1)
|
||||
}
|
||||
|
||||
fn with_line_style_mut(&mut self, line_id: usize) -> Option<&mut StyleLayer> {
|
||||
self.style_layers.get_mut((self.stroke_layer_offset.unwrap() + line_id) - 1)
|
||||
}
|
||||
|
||||
pub(crate) fn layers(&self) -> &Vec<StyleLayer> {
|
||||
&self.style_layers
|
||||
}
|
||||
|
||||
fn end_style_group(&mut self) {
|
||||
for style_layer in &mut self.style_layers[self.base_layer_offset..] {
|
||||
// There can be an unused style group at the end of each layer, which we should remove.
|
||||
if let Some(last) = style_layer.shapes().last() {
|
||||
if last.len() == 0 {
|
||||
style_layer.shapes_mut().pop();
|
||||
}
|
||||
}
|
||||
style_layer.consolidate_edges();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
struct PlacementInfo {
|
||||
symbol_id: u32,
|
||||
translate_x: Twips,
|
||||
translate_y: Twips,
|
||||
}
|
||||
|
||||
struct Timeline(Vec<Frame>);
|
||||
|
||||
impl Timeline {
|
||||
fn first(&self) -> &Frame {
|
||||
&self.0[0]
|
||||
}
|
||||
|
||||
fn last(&self) -> &Frame {
|
||||
&self.0[self.0.len() - 1]
|
||||
}
|
||||
|
||||
fn first_mut(&mut self) -> &mut Frame {
|
||||
&mut self.0[0]
|
||||
}
|
||||
|
||||
fn last_mut(&mut self) -> &mut Frame {
|
||||
let last = self.0.len() - 1;
|
||||
&mut self.0[last]
|
||||
}
|
||||
}
|
||||
|
||||
struct Frame {
|
||||
duration_frames_initial: u16,
|
||||
duration_remaining_frames: u16,
|
||||
placements: Vec<PlacementInfo>
|
||||
}
|
Loading…
Reference in New Issue