core: Store object scale as percentages to avoid floating point precision problems.

Fixes a specific pattern of preloader design where animations were handled by just making the box bigger every frame until it's 100. Of course, direct equality of f64 is a terrible idea, but it works in Flash, which apparantly must store scale in percentages. So we must, too.
This commit is contained in:
David Wendt 2020-09-24 23:17:07 -04:00 committed by Mike Welsh
parent f6f084098e
commit 28bf84bd8b
11 changed files with 95 additions and 42 deletions

View File

@ -625,7 +625,7 @@ fn x_scale<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
this: DisplayObject<'gc>,
) -> Result<Value<'gc>, Error<'gc>> {
let val = this.scale_x(activation.context.gc_context) * 100.0;
let val: f64 = this.scale_x(activation.context.gc_context).into();
Ok(val.into())
}
@ -635,7 +635,7 @@ fn set_x_scale<'gc>(
val: Value<'gc>,
) -> Result<(), Error<'gc>> {
if let Some(val) = property_coerce_to_number(activation, val)? {
this.set_scale_x(activation.context.gc_context, val / 100.0);
this.set_scale_x(activation.context.gc_context, val.into());
}
Ok(())
}
@ -644,7 +644,7 @@ fn y_scale<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
this: DisplayObject<'gc>,
) -> Result<Value<'gc>, Error<'gc>> {
let scale_y = this.scale_y(activation.context.gc_context) * 100.0;
let scale_y: f64 = this.scale_y(activation.context.gc_context).into();
Ok(scale_y.into())
}
@ -654,7 +654,7 @@ fn set_y_scale<'gc>(
val: Value<'gc>,
) -> Result<(), Error<'gc>> {
if let Some(val) = property_coerce_to_number(activation, val)? {
this.set_scale_y(activation.context.gc_context, val / 100.0);
this.set_scale_y(activation.context.gc_context, val.into());
}
Ok(())
}

View File

@ -1,5 +1,6 @@
use crate::avm1::{Object, TObject, Value};
use crate::context::{RenderContext, UpdateContext};
use crate::percentage::Percent;
use crate::player::NEWEST_PLAYER_VERSION;
use crate::prelude::*;
use crate::tag_utils::SwfMovie;
@ -40,11 +41,15 @@ pub struct DisplayObjectBase<'gc> {
clip_depth: Depth,
// Cached transform properties `_xscale`, `_yscale`, `_rotation`.
// These are expensive to calculate, so they will be calculated and cached when AS requests
// one of these properties.
// These are expensive to calculate, so they will be calculated and cached
// when AS requests one of these properties.
//
// `_xscale` and `_yscale` are stored in units of percentages to avoid
// floating-point precision errors with movies that accumulate onto the
// scale parameters.
rotation: f64,
scale_x: f64,
scale_y: f64,
scale_x: Percent,
scale_y: Percent,
skew: f64,
/// The first child of this display object in order of execution.
@ -71,8 +76,8 @@ impl<'gc> Default for DisplayObjectBase<'gc> {
name: Default::default(),
clip_depth: Default::default(),
rotation: 0.0,
scale_x: 1.0,
scale_y: 1.0,
scale_x: Percent::from_unit(1.0),
scale_y: Percent::from_unit(1.0),
skew: 0.0,
first_child: None,
prev_sibling: None,
@ -187,8 +192,8 @@ impl<'gc> DisplayObjectBase<'gc> {
let scale_x = f32::sqrt(a * a + b * b);
let scale_y = f32::sqrt(c * c + d * d);
self.rotation = rotation_x.into();
self.scale_x = scale_x.into();
self.scale_y = scale_y.into();
self.scale_x = Percent::from_unit(scale_x.into());
self.scale_y = Percent::from_unit(scale_y.into());
self.skew = (rotation_y - rotation_x).into();
self.flags.insert(DisplayObjectFlags::ScaleRotationCached);
}
@ -200,8 +205,8 @@ impl<'gc> DisplayObjectBase<'gc> {
let rotation = rotation.to_radians();
let cos_x = f32::cos(rotation);
let sin_x = f32::sin(rotation);
self.scale_x = scale_x.into();
self.scale_y = scale_y.into();
self.scale_x = Percent::from_unit(scale_x.into());
self.scale_y = Percent::from_unit(scale_y.into());
self.rotation = rotation.into();
matrix.a = (scale_x * cos_x) as f32;
matrix.b = (scale_x * sin_x) as f32;
@ -222,38 +227,38 @@ impl<'gc> DisplayObjectBase<'gc> {
let cos_y = f64::cos(radians + self.skew);
let sin_y = f64::sin(radians + self.skew);
let mut matrix = &mut self.transform.matrix;
matrix.a = (self.scale_x * cos_x) as f32;
matrix.b = (self.scale_x * sin_x) as f32;
matrix.c = (self.scale_y * -sin_y) as f32;
matrix.d = (self.scale_y * cos_y) as f32;
matrix.a = (self.scale_x.into_unit() * cos_x) as f32;
matrix.b = (self.scale_x.into_unit() * sin_x) as f32;
matrix.c = (self.scale_y.into_unit() * -sin_y) as f32;
matrix.d = (self.scale_y.into_unit() * cos_y) as f32;
}
fn scale_x(&mut self) -> f64 {
fn scale_x(&mut self) -> Percent {
self.cache_scale_rotation();
self.scale_x
}
fn set_scale_x(&mut self, value: f64) {
fn set_scale_x(&mut self, value: Percent) {
self.set_transformed_by_script(true);
self.cache_scale_rotation();
self.scale_x = value;
let cos = f64::cos(self.rotation);
let sin = f64::sin(self.rotation);
let mut matrix = &mut self.transform.matrix;
matrix.a = (cos * value) as f32;
matrix.b = (sin * value) as f32;
matrix.a = (cos * value.into_unit()) as f32;
matrix.b = (sin * value.into_unit()) as f32;
}
fn scale_y(&mut self) -> f64 {
fn scale_y(&mut self) -> Percent {
self.cache_scale_rotation();
self.scale_y
}
fn set_scale_y(&mut self, value: f64) {
fn set_scale_y(&mut self, value: Percent) {
self.set_transformed_by_script(true);
self.cache_scale_rotation();
self.scale_y = value;
let cos = f64::cos(self.rotation + self.skew);
let sin = f64::sin(self.rotation + self.skew);
let mut matrix = &mut self.transform.matrix;
matrix.c = (-sin * value) as f32;
matrix.d = (cos * value) as f32;
matrix.c = (-sin * value.into_unit()) as f32;
matrix.d = (cos * value.into_unit()) as f32;
}
fn name(&self) -> &str {
@ -499,22 +504,22 @@ pub trait TDisplayObject<'gc>:
/// The X axis scale for this display object in local space.
/// The normal scale is 1.
/// Returned by the `_xscale`/`scaleX` ActionScript properties.
fn scale_x(&self, gc_context: MutationContext<'gc, '_>) -> f64;
fn scale_x(&self, gc_context: MutationContext<'gc, '_>) -> Percent;
/// Sets the scale of the X axis for this display object in local space.
/// The normal scale is 1.
/// Set by the `_xscale`/`scaleX` ActionScript properties.
fn set_scale_x(&self, gc_context: MutationContext<'gc, '_>, value: f64);
fn set_scale_x(&self, gc_context: MutationContext<'gc, '_>, value: Percent);
/// The Y axis scale for this display object in local space.
/// The normal scale is 1.
/// Returned by the `_yscale`/`scaleY` ActionScript properties.
fn scale_y(&self, gc_context: MutationContext<'gc, '_>) -> f64;
fn scale_y(&self, gc_context: MutationContext<'gc, '_>) -> Percent;
/// Sets the Y axis scale for this display object in local space.
/// The normal scale is 1.
/// Returned by the `_yscale`/`scaleY` ActionScript properties.
fn set_scale_y(&self, gc_context: MutationContext<'gc, '_>, value: f64);
fn set_scale_y(&self, gc_context: MutationContext<'gc, '_>, value: Percent);
/// Sets the pixel width of this display object in local space.
/// The width is based on the AABB of the object.
@ -544,8 +549,8 @@ pub trait TDisplayObject<'gc>:
// It has to do with the length of the sides A, B of an AABB enclosing the object's OBB with sides a, b:
// A = sin(t) * a + cos(t) * b
// B = cos(t) * a + sin(t) * b
let prev_scale_x = self.scale_x(gc_context);
let prev_scale_y = self.scale_y(gc_context);
let prev_scale_x = self.scale_x(gc_context).into_unit();
let prev_scale_y = self.scale_y(gc_context).into_unit();
let rotation = self.rotation(gc_context);
let cos = f64::abs(f64::cos(rotation));
let sin = f64::abs(f64::sin(rotation));
@ -553,8 +558,8 @@ pub trait TDisplayObject<'gc>:
/ ((cos + aspect_ratio * sin) * (aspect_ratio * cos + sin));
let new_scale_y =
(sin * prev_scale_x + aspect_ratio * cos * prev_scale_y) / (aspect_ratio * cos + sin);
self.set_scale_x(gc_context, new_scale_x);
self.set_scale_y(gc_context, new_scale_y);
self.set_scale_x(gc_context, Percent::from_unit(new_scale_x));
self.set_scale_y(gc_context, Percent::from_unit(new_scale_y));
}
/// Gets the pixel height of the AABB containing this display object in local space.
/// Returned by the ActionScript `_height`/`height` properties.
@ -581,8 +586,8 @@ pub trait TDisplayObject<'gc>:
// It has to do with the length of the sides A, B of an AABB enclosing the object's OBB with sides a, b:
// A = sin(t) * a + cos(t) * b
// B = cos(t) * a + sin(t) * b
let prev_scale_x = self.scale_x(gc_context);
let prev_scale_y = self.scale_y(gc_context);
let prev_scale_x = self.scale_x(gc_context).into_unit();
let prev_scale_y = self.scale_y(gc_context).into_unit();
let rotation = self.rotation(gc_context);
let cos = f64::abs(f64::cos(rotation));
let sin = f64::abs(f64::sin(rotation));
@ -590,8 +595,8 @@ pub trait TDisplayObject<'gc>:
(aspect_ratio * cos * prev_scale_x + sin * prev_scale_y) / (aspect_ratio * cos + sin);
let new_scale_y = aspect_ratio * (sin * target_scale_x + cos * target_scale_y)
/ ((cos + aspect_ratio * sin) * (aspect_ratio * cos + sin));
self.set_scale_x(gc_context, new_scale_x);
self.set_scale_y(gc_context, new_scale_y);
self.set_scale_x(gc_context, Percent::from_unit(new_scale_x));
self.set_scale_y(gc_context, Percent::from_unit(new_scale_y));
}
/// The opacity of this display object.
/// 1 is fully opaque.
@ -996,16 +1001,16 @@ macro_rules! impl_display_object_sansbounds {
fn set_rotation(&self, gc_context: gc_arena::MutationContext<'gc, '_>, radians: f64) {
self.0.write(gc_context).$field.set_rotation(radians)
}
fn scale_x(&self, gc_context: gc_arena::MutationContext<'gc, '_>) -> f64 {
fn scale_x(&self, gc_context: gc_arena::MutationContext<'gc, '_>) -> Percent {
self.0.write(gc_context).$field.scale_x()
}
fn set_scale_x(&self, gc_context: gc_arena::MutationContext<'gc, '_>, value: f64) {
fn set_scale_x(&self, gc_context: gc_arena::MutationContext<'gc, '_>, value: Percent) {
self.0.write(gc_context).$field.set_scale_x(value)
}
fn scale_y(&self, gc_context: gc_arena::MutationContext<'gc, '_>) -> f64 {
fn scale_y(&self, gc_context: gc_arena::MutationContext<'gc, '_>) -> Percent {
self.0.write(gc_context).$field.scale_y()
}
fn set_scale_y(&self, gc_context: gc_arena::MutationContext<'gc, '_>, value: f64) {
fn set_scale_y(&self, gc_context: gc_arena::MutationContext<'gc, '_>, value: Percent) {
self.0.write(gc_context).$field.set_scale_y(value)
}
fn alpha(&self) -> f64 {

View File

@ -3,6 +3,7 @@
use crate::backend::render::BitmapHandle;
use crate::context::{RenderContext, UpdateContext};
use crate::display_object::{DisplayObjectBase, TDisplayObject};
use crate::percentage::Percent;
use crate::prelude::*;
use gc_arena::{Collect, Gc, GcCell};

View File

@ -2,6 +2,7 @@ use crate::avm1::{Object, StageObject, Value};
use crate::context::{ActionType, RenderContext, UpdateContext};
use crate::display_object::{DisplayObjectBase, TDisplayObject};
use crate::events::{ButtonKeyCode, ClipEvent, ClipEventResult};
use crate::percentage::Percent;
use crate::prelude::*;
use crate::tag_utils::{SwfMovie, SwfSlice};
use gc_arena::{Collect, GcCell, MutationContext};

View File

@ -7,6 +7,7 @@ use crate::display_object::{DisplayObjectBase, TDisplayObject};
use crate::drawing::Drawing;
use crate::font::{round_down_to_pixel, Glyph};
use crate::html::{BoxBounds, FormatSpans, LayoutBox, TextFormat};
use crate::percentage::Percent;
use crate::prelude::*;
use crate::shape_utils::DrawCommand;
use crate::tag_utils::SwfMovie;

View File

@ -1,6 +1,7 @@
use crate::backend::render::ShapeHandle;
use crate::context::{RenderContext, UpdateContext};
use crate::display_object::{DisplayObjectBase, TDisplayObject};
use crate::percentage::Percent;
use crate::prelude::*;
use gc_arena::{Collect, GcCell};

View File

@ -1,6 +1,7 @@
use crate::backend::render::{RenderBackend, ShapeHandle};
use crate::context::{RenderContext, UpdateContext};
use crate::display_object::{DisplayObjectBase, TDisplayObject};
use crate::percentage::Percent;
use crate::prelude::*;
use gc_arena::{Collect, Gc, GcCell, MutationContext};
use swf::Twips;

View File

@ -11,6 +11,7 @@ use crate::display_object::{
use crate::drawing::Drawing;
use crate::events::{ButtonKeyCode, ClipEvent, ClipEventResult};
use crate::font::Font;
use crate::percentage::Percent;
use crate::prelude::*;
use crate::shape_utils::DrawCommand;
use crate::tag_utils::{self, DecodeResult, SwfMovie, SwfSlice, SwfStream};

View File

@ -1,5 +1,6 @@
use crate::context::{RenderContext, UpdateContext};
use crate::display_object::{DisplayObjectBase, TDisplayObject};
use crate::percentage::Percent;
use crate::prelude::*;
use crate::tag_utils::SwfMovie;
use crate::transform::Transform;

View File

@ -28,6 +28,7 @@ mod font;
mod html;
mod library;
pub mod loader;
mod percentage;
mod player;
mod prelude;
mod property_map;

40
core/src/percentage.rs Normal file
View File

@ -0,0 +1,40 @@
//! Percentage type
use gc_arena::Collect;
/// Percent units for things that need to be stored as percentages.
///
/// Actual percentages (0-100) can be stored in here by `From` and `Into`
/// coercions. Thus, this wrapper serves as a unit marker. To convert into unit
/// ranges (0-1), use the `from_unit` and `into_unit` methods.
///
/// No arithmetic operators are provided on percentages as most of the math
/// they are involved in should be done in unit proportions rather than
/// percentages.
#[derive(Copy, Clone, Debug, Collect, PartialEq, PartialOrd)]
#[collect(require_static)]
pub struct Percent(f64);
impl Percent {
/// Convert a unit proportion into a percentage.
pub fn from_unit(unit: f64) -> Self {
Self(unit * 100.0)
}
/// Convert a percentage into a unit proportion.
pub fn into_unit(self) -> f64 {
self.0 / 100.0
}
}
impl From<f64> for Percent {
fn from(percent: f64) -> Self {
Self(percent)
}
}
impl Into<f64> for Percent {
fn into(self) -> f64 {
self.0
}
}