From 61e464099c88a2c15e40a05d53c76f39d072a132 Mon Sep 17 00:00:00 2001 From: Nathan Adams Date: Tue, 19 May 2020 13:05:58 +0200 Subject: [PATCH] core: Add initial drawing API to MovieClip --- core/src/avm1/globals/movie_clip.rs | 328 +++++++++++++++++++++++++- core/src/bounding_box.rs | 16 ++ core/src/display_object/movie_clip.rs | 212 ++++++++++++++++- 3 files changed, 552 insertions(+), 4 deletions(-) diff --git a/core/src/avm1/globals/movie_clip.rs b/core/src/avm1/globals/movie_clip.rs index 391903b58..a59e8f9ed 100644 --- a/core/src/avm1/globals/movie_clip.rs +++ b/core/src/avm1/globals/movie_clip.rs @@ -7,9 +7,13 @@ use crate::avm1::{Avm1, Error, Object, ScriptObject, TObject, UpdateContext, Val use crate::backend::navigator::NavigationMethod; use crate::display_object::{DisplayObject, EditText, MovieClip, TDisplayObject}; use crate::prelude::*; +use crate::shape_utils::DrawCommand; use crate::tag_utils::SwfSlice; use gc_arena::MutationContext; -use swf::Twips; +use swf::{ + FillStyle, Gradient, GradientInterpolation, GradientRecord, GradientSpread, LineCapStyle, + LineJoinStyle, LineStyle, Matrix, Twips, +}; /// Implements `MovieClip` pub fn constructor<'gc>( @@ -123,12 +127,332 @@ pub fn create_proto<'gc>( "stopDrag" => stop_drag, "swapDepths" => swap_depths, "toString" => to_string, - "unloadMovie" => unload_movie + "unloadMovie" => unload_movie, + "beginFill" => begin_fill, + "beginGradientFill" => begin_gradient_fill, + "moveTo" => move_to, + "lineTo" => line_to, + "curveTo" => curve_to, + "endFill" => end_fill, + "lineStyle" => line_style, + "clear" => clear ); object.into() } +fn line_style<'gc>( + movie_clip: MovieClip<'gc>, + avm: &mut Avm1<'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + args: &[Value<'gc>], +) -> Result, Error> { + if let Some(width) = args.get(0) { + let width = Twips::from_pixels(width.as_number(avm, context)?.min(255.0).max(0.0)); + let color = if let Some(rgb) = args.get(1) { + let rgb = rgb.coerce_to_u32(avm, context)?; + let alpha = if let Some(alpha) = args.get(2) { + alpha.as_number(avm, context)?.min(100.0).max(0.0) + } else { + 100.0 + } as f32 + / 100.0 + * 255.0; + Color::from_rgb(rgb, alpha as u8) + } else { + Color::from_rgb(0, 255) + }; + let is_pixel_hinted = args + .get(3) + .map_or(false, |v| v.as_bool(avm.current_swf_version())); + let (allow_scale_x, allow_scale_y) = match args + .get(4) + .and_then(|v| v.clone().coerce_to_string(avm, context).ok()) + .as_deref() + { + Some("normal") => (true, true), + Some("vertical") => (true, false), + Some("horizontal") => (false, true), + _ => (false, false), + }; + let cap_style = match args + .get(5) + .and_then(|v| v.clone().coerce_to_string(avm, context).ok()) + .as_deref() + { + Some("square") => LineCapStyle::Square, + Some("none") => LineCapStyle::None, + _ => LineCapStyle::Round, + }; + let join_style = match args + .get(6) + .and_then(|v| v.clone().coerce_to_string(avm, context).ok()) + .as_deref() + { + Some("miter") => { + if let Some(limit) = args.get(7) { + LineJoinStyle::Miter(limit.as_number(avm, context)?.max(0.0).min(255.0) as f32) + } else { + LineJoinStyle::Miter(3.0) + } + } + Some("bevel") => LineJoinStyle::Bevel, + _ => LineJoinStyle::Round, + }; + movie_clip.set_line_style( + context, + Some(LineStyle { + width, + color, + start_cap: cap_style, + end_cap: cap_style, + join_style, + fill_style: None, + allow_scale_x, + allow_scale_y, + is_pixel_hinted, + allow_close: false, + }), + ); + } else { + movie_clip.set_line_style(context, None); + } + Ok(Value::Undefined.into()) +} + +fn begin_fill<'gc>( + movie_clip: MovieClip<'gc>, + avm: &mut Avm1<'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + args: &[Value<'gc>], +) -> Result, Error> { + if let Some(rgb) = args.get(0) { + let rgb = rgb.coerce_to_u32(avm, context)?; + let alpha = if let Some(alpha) = args.get(1) { + alpha.as_number(avm, context)?.min(100.0).max(0.0) + } else { + 100.0 + } as f32 + / 100.0 + * 255.0; + movie_clip.set_fill_style( + context, + Some(FillStyle::Color(Color::from_rgb(rgb, alpha as u8))), + ); + } else { + movie_clip.set_fill_style(context, None); + } + Ok(Value::Undefined.into()) +} + +fn begin_gradient_fill<'gc>( + movie_clip: MovieClip<'gc>, + avm: &mut Avm1<'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + args: &[Value<'gc>], +) -> Result, Error> { + if let (Some(method), Some(colors), Some(alphas), Some(ratios), Some(matrix)) = ( + args.get(0), + args.get(1), + args.get(2), + args.get(3), + args.get(4), + ) { + let method = method.clone().coerce_to_string(avm, context)?; + let colors = colors.as_object()?.array(); + let alphas = alphas.as_object()?.array(); + let ratios = ratios.as_object()?.array(); + let matrix_object = matrix.as_object()?; + if colors.len() != alphas.len() || colors.len() != ratios.len() { + log::warn!( + "beginGradientFill() received different sized arrays for colors, alphas and ratios" + ); + return Ok(Value::Undefined.into()); + } + let mut records = Vec::with_capacity(colors.len()); + for i in 0..colors.len() { + let ratio = ratios[i].as_number(avm, context)?.min(255.0).max(0.0); + let rgb = colors[i].coerce_to_u32(avm, context)?; + let alpha = alphas[i].as_number(avm, context)?.min(100.0).max(0.0); + records.push(GradientRecord { + ratio: ratio as u8, + color: Color::from_rgb(rgb, (alpha / 100.0 * 255.0) as u8), + }); + } + let matrix = if matrix_object + .get("matrixType", avm, context)? + .resolve(avm, context)? + .coerce_to_string(avm, context)? + == "box" + { + let width = matrix_object + .get("w", avm, context)? + .resolve(avm, context)? + .as_number(avm, context)?; + let height = matrix_object + .get("h", avm, context)? + .resolve(avm, context)? + .as_number(avm, context)?; + let tx = matrix_object + .get("x", avm, context)? + .resolve(avm, context)? + .as_number(avm, context)? + + width / 2.0; + let ty = matrix_object + .get("y", avm, context)? + .resolve(avm, context)? + .as_number(avm, context)? + + height / 2.0; + // TODO: This is wrong, doesn't account for rotations. + Matrix { + translate_x: Twips::from_pixels(tx), + translate_y: Twips::from_pixels(ty), + scale_x: width as f32 / 1638.4, + scale_y: height as f32 / 1638.4, + rotate_skew_0: 0.0, + rotate_skew_1: 0.0, + } + } else { + log::warn!( + "beginGradientFill() received unsupported matrix object {:?}", + matrix_object + ); + return Ok(Value::Undefined.into()); + }; + let spread = match args + .get(5) + .and_then(|v| v.clone().coerce_to_string(avm, context).ok()) + .as_deref() + { + Some("reflect") => GradientSpread::Reflect, + Some("repeat") => GradientSpread::Repeat, + _ => GradientSpread::Pad, + }; + let interpolation = match args + .get(6) + .and_then(|v| v.clone().coerce_to_string(avm, context).ok()) + .as_deref() + { + Some("linearRGB") => GradientInterpolation::LinearRGB, + _ => GradientInterpolation::RGB, + }; + + let gradient = Gradient { + matrix, + spread, + interpolation, + records, + }; + let style = match method.as_str() { + "linear" => FillStyle::LinearGradient(gradient), + "radial" => { + if let Some(focal_point) = args.get(7) { + FillStyle::FocalGradient { + gradient, + focal_point: focal_point.as_number(avm, context)? as f32, + } + } else { + FillStyle::RadialGradient(gradient) + } + } + other => { + log::warn!("beginGradientFill() received invalid fill type {:?}", other); + return Ok(Value::Undefined.into()); + } + }; + movie_clip.set_fill_style(context, Some(style)); + } else { + movie_clip.set_fill_style(context, None); + } + Ok(Value::Undefined.into()) +} + +fn move_to<'gc>( + movie_clip: MovieClip<'gc>, + avm: &mut Avm1<'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + args: &[Value<'gc>], +) -> Result, Error> { + if let (Some(x), Some(y)) = (args.get(0), args.get(1)) { + let x = x.as_number(avm, context)?; + let y = y.as_number(avm, context)?; + movie_clip.draw_command( + context, + DrawCommand::MoveTo { + x: Twips::from_pixels(x), + y: Twips::from_pixels(y), + }, + ); + } + Ok(Value::Undefined.into()) +} + +fn line_to<'gc>( + movie_clip: MovieClip<'gc>, + avm: &mut Avm1<'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + args: &[Value<'gc>], +) -> Result, Error> { + if let (Some(x), Some(y)) = (args.get(0), args.get(1)) { + let x = x.as_number(avm, context)?; + let y = y.as_number(avm, context)?; + movie_clip.draw_command( + context, + DrawCommand::LineTo { + x: Twips::from_pixels(x), + y: Twips::from_pixels(y), + }, + ); + } + Ok(Value::Undefined.into()) +} + +fn curve_to<'gc>( + movie_clip: MovieClip<'gc>, + avm: &mut Avm1<'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + args: &[Value<'gc>], +) -> Result, Error> { + if let (Some(x1), Some(y1), Some(x2), Some(y2)) = + (args.get(0), args.get(1), args.get(2), args.get(3)) + { + let x1 = x1.as_number(avm, context)?; + let y1 = y1.as_number(avm, context)?; + let x2 = x2.as_number(avm, context)?; + let y2 = y2.as_number(avm, context)?; + movie_clip.draw_command( + context, + DrawCommand::CurveTo { + x1: Twips::from_pixels(x1), + y1: Twips::from_pixels(y1), + x2: Twips::from_pixels(x2), + y2: Twips::from_pixels(y2), + }, + ); + } + Ok(Value::Undefined.into()) +} + +fn end_fill<'gc>( + movie_clip: MovieClip<'gc>, + _avm: &mut Avm1<'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + _args: &[Value<'gc>], +) -> Result, Error> { + movie_clip.set_fill_style(context, None); + Ok(Value::Undefined.into()) +} + +fn clear<'gc>( + movie_clip: MovieClip<'gc>, + _avm: &mut Avm1<'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + _args: &[Value<'gc>], +) -> Result, Error> { + movie_clip.clear(context); + Ok(Value::Undefined.into()) +} + fn attach_movie<'gc>( mut movie_clip: MovieClip<'gc>, avm: &mut Avm1<'gc>, diff --git a/core/src/bounding_box.rs b/core/src/bounding_box.rs index 2f5cb544e..fac8bbb12 100644 --- a/core/src/bounding_box.rs +++ b/core/src/bounding_box.rs @@ -43,6 +43,22 @@ impl BoundingBox { } } + pub fn encompass(&mut self, x: Twips, y: Twips) { + if x < self.x_min { + self.x_min = x; + } + if x > self.x_max { + self.x_max = x; + } + if y < self.y_min { + self.y_min = y; + } + if y > self.y_max { + self.y_max = y; + } + self.valid = true; + } + pub fn union(&mut self, other: &BoundingBox) { use std::cmp::{max, min}; if self.valid && other.valid { diff --git a/core/src/display_object/movie_clip.rs b/core/src/display_object/movie_clip.rs index 002c9ce6e..70e79f363 100644 --- a/core/src/display_object/movie_clip.rs +++ b/core/src/display_object/movie_clip.rs @@ -1,6 +1,8 @@ //! `MovieClip` display object and support code. use crate::avm1::{Avm1, Object, StageObject, TObject, Value}; use crate::backend::audio::AudioStreamHandle; + +use crate::backend::render::ShapeHandle; use crate::character::Character; use crate::context::{ActionType, RenderContext, UpdateContext}; use crate::display_object::{ @@ -9,6 +11,7 @@ use crate::display_object::{ use crate::events::{ButtonKeyCode, ClipEvent}; use crate::font::Font; use crate::prelude::*; +use crate::shape_utils::{DistilledShape, DrawCommand, DrawPath}; use crate::tag_utils::{self, DecodeResult, SwfMovie, SwfSlice, SwfStream}; use enumset::{EnumSet, EnumSetType}; use gc_arena::{Collect, Gc, GcCell, MutationContext}; @@ -18,6 +21,7 @@ use std::collections::{BTreeMap, HashMap}; use std::convert::TryFrom; use std::sync::Arc; use swf::read::SwfRead; +use swf::{FillStyle, LineStyle}; type FrameNumber = u16; @@ -42,6 +46,14 @@ pub struct MovieClipData<'gc> { clip_actions: SmallVec<[ClipAction; 2]>, flags: EnumSet, avm1_constructor: Option>, + custom_shape: Option, + custom_shape_bounds: BoundingBox, + custom_edge_bounds: BoundingBox, + dirty_shape: bool, + custom_fills: Vec<(FillStyle, Vec)>, + custom_lines: Vec<(LineStyle, Vec)>, + current_fill: Option<(FillStyle, Vec)>, + current_line: Option<(LineStyle, Vec)>, } impl<'gc> MovieClip<'gc> { @@ -60,6 +72,14 @@ impl<'gc> MovieClip<'gc> { clip_actions: SmallVec::new(), flags: EnumSet::empty(), avm1_constructor: None, + custom_shape: None, + custom_shape_bounds: BoundingBox::default(), + custom_edge_bounds: BoundingBox::default(), + dirty_shape: false, + custom_fills: Vec::new(), + custom_lines: Vec::new(), + current_fill: None, + current_line: None, }, )) } @@ -92,6 +112,14 @@ impl<'gc> MovieClip<'gc> { clip_actions: SmallVec::new(), flags: MovieClipFlags::Playing.into(), avm1_constructor: None, + custom_shape: None, + custom_shape_bounds: BoundingBox::default(), + custom_edge_bounds: BoundingBox::default(), + dirty_shape: false, + custom_fills: Vec::new(), + custom_lines: Vec::new(), + current_fill: None, + current_line: None, }, )) } @@ -550,6 +578,132 @@ impl<'gc> MovieClip<'gc> { actions.into_iter() } + + pub fn set_fill_style( + self, + context: &mut UpdateContext<'_, 'gc, '_>, + style: Option, + ) { + let mut mc = self.0.write(context.gc_context); + + // TODO: If current_fill is not closed, we should close it and also close current_line + + if let Some(existing) = mc.current_fill.take() { + mc.custom_fills.push(existing); + } + if let Some(style) = style { + mc.current_fill = Some((style, Vec::new())); + } + + mc.dirty_shape = true; + } + + pub fn clear(self, context: &mut UpdateContext<'_, 'gc, '_>) { + let mut mc = self.0.write(context.gc_context); + mc.current_fill = None; + mc.current_line = None; + mc.custom_fills.clear(); + mc.custom_lines.clear(); + mc.custom_edge_bounds = BoundingBox::default(); + mc.custom_shape_bounds = BoundingBox::default(); + mc.dirty_shape = true; + } + + pub fn set_line_style( + self, + context: &mut UpdateContext<'_, 'gc, '_>, + style: Option, + ) { + let mut mc = self.0.write(context.gc_context); + + if let Some(existing) = mc.current_line.take() { + mc.custom_lines.push(existing); + } + if let Some(style) = style { + mc.current_line = Some((style, Vec::new())); + } + + mc.dirty_shape = true; + } + + pub fn draw_command(self, context: &mut UpdateContext<'_, 'gc, '_>, command: DrawCommand) { + let mut mc = self.0.write(context.gc_context); + + let mut include_last = false; + + match command { + DrawCommand::MoveTo { .. } => {} + DrawCommand::LineTo { x, y } => { + mc.custom_shape_bounds.encompass(x, y); + mc.custom_edge_bounds.encompass(x, y); + include_last = true; + } + DrawCommand::CurveTo { x1, y1, x2, y2 } => { + mc.custom_shape_bounds.encompass(x1, y1); + mc.custom_shape_bounds.encompass(x2, y2); + mc.custom_edge_bounds.encompass(x1, y1); + mc.custom_edge_bounds.encompass(x2, y2); + include_last = true; + } + } + + if let Some((_, commands)) = &mut mc.current_line { + commands.push(command.clone()); + } + if let Some((_, commands)) = &mut mc.current_fill { + commands.push(command); + } + + if include_last { + if let Some(command) = mc + .current_fill + .as_ref() + .and_then(|(_, commands)| commands.last().cloned()) + { + match command { + DrawCommand::MoveTo { x, y } => { + mc.custom_shape_bounds.encompass(x, y); + mc.custom_edge_bounds.encompass(x, y); + } + DrawCommand::LineTo { x, y } => { + mc.custom_shape_bounds.encompass(x, y); + mc.custom_edge_bounds.encompass(x, y); + } + DrawCommand::CurveTo { x1, y1, x2, y2 } => { + mc.custom_shape_bounds.encompass(x1, y1); + mc.custom_shape_bounds.encompass(x2, y2); + mc.custom_edge_bounds.encompass(x1, y1); + mc.custom_edge_bounds.encompass(x2, y2); + } + } + } + + if let Some(command) = mc + .current_line + .as_ref() + .and_then(|(_, commands)| commands.last().cloned()) + { + match command { + DrawCommand::MoveTo { x, y } => { + mc.custom_shape_bounds.encompass(x, y); + mc.custom_edge_bounds.encompass(x, y); + } + DrawCommand::LineTo { x, y } => { + mc.custom_shape_bounds.encompass(x, y); + mc.custom_edge_bounds.encompass(x, y); + } + DrawCommand::CurveTo { x1, y1, x2, y2 } => { + mc.custom_shape_bounds.encompass(x1, y1); + mc.custom_shape_bounds.encompass(x2, y2); + mc.custom_edge_bounds.encompass(x1, y1); + mc.custom_edge_bounds.encompass(x2, y2); + } + } + } + } + + mc.dirty_shape = true; + } } impl<'gc> TDisplayObject<'gc> for MovieClip<'gc> { @@ -587,17 +741,71 @@ impl<'gc> TDisplayObject<'gc> for MovieClip<'gc> { if is_load_frame { mc.run_clip_postaction((*self).into(), context, ClipEvent::Load); } + + if mc.dirty_shape { + mc.dirty_shape = false; + let mut paths = Vec::new(); + + for (style, commands) in &mc.custom_fills { + paths.push(DrawPath::Fill { + style, + commands: commands.to_owned(), + }) + } + + // TODO: If the current_fill is not closed, we should automatically close current_line + + if let Some((style, commands)) = &mc.current_fill { + paths.push(DrawPath::Fill { + style, + commands: commands.to_owned(), + }) + } + + for (style, commands) in &mc.custom_lines { + paths.push(DrawPath::Stroke { + style, + commands: commands.to_owned(), + is_closed: false, // TODO: Determine this + }) + } + + if let Some((style, commands)) = &mc.current_line { + paths.push(DrawPath::Stroke { + style, + commands: commands.to_owned(), + is_closed: false, // TODO: Determine this + }) + } + + let shape = DistilledShape { + paths, + shape_bounds: mc.custom_shape_bounds.clone(), + edge_bounds: mc.custom_shape_bounds.clone(), + id: mc.id(), + }; + + if let Some(handle) = mc.custom_shape { + context.renderer.replace_shape(shape, handle); + } else { + mc.custom_shape = Some(context.renderer.register_shape(shape)); + } + } } fn render(&self, context: &mut RenderContext<'_, 'gc>) { context.transform_stack.push(&*self.transform()); crate::display_object::render_children(context, &self.0.read().children); + if let Some(handle) = self.0.read().custom_shape { + context + .renderer + .render_shape(handle, context.transform_stack.transform()); + } context.transform_stack.pop(); } fn self_bounds(&self) -> BoundingBox { - // No inherent bounds; contains child DisplayObjects. - BoundingBox::default() + self.0.read().custom_shape_bounds.clone() } fn hit_test(&self, point: (Twips, Twips)) -> bool {