core: Add initial drawing API to MovieClip

This commit is contained in:
Nathan Adams 2020-05-19 13:05:58 +02:00
parent 7ab6703fc9
commit 61e464099c
3 changed files with 552 additions and 4 deletions

View File

@ -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<ReturnValue<'gc>, 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<ReturnValue<'gc>, 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<ReturnValue<'gc>, 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<ReturnValue<'gc>, 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<ReturnValue<'gc>, 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<ReturnValue<'gc>, 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<ReturnValue<'gc>, 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<ReturnValue<'gc>, Error> {
movie_clip.clear(context);
Ok(Value::Undefined.into())
}
fn attach_movie<'gc>(
mut movie_clip: MovieClip<'gc>,
avm: &mut Avm1<'gc>,

View File

@ -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 {

View File

@ -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<MovieClipFlags>,
avm1_constructor: Option<Object<'gc>>,
custom_shape: Option<ShapeHandle>,
custom_shape_bounds: BoundingBox,
custom_edge_bounds: BoundingBox,
dirty_shape: bool,
custom_fills: Vec<(FillStyle, Vec<DrawCommand>)>,
custom_lines: Vec<(LineStyle, Vec<DrawCommand>)>,
current_fill: Option<(FillStyle, Vec<DrawCommand>)>,
current_line: Option<(LineStyle, Vec<DrawCommand>)>,
}
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<FillStyle>,
) {
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<LineStyle>,
) {
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 {