ruffle/core/src/display_object.rs

2658 lines
100 KiB
Rust
Raw Normal View History

use crate::avm1::{Object as Avm1Object, TObject as Avm1TObject, Value as Avm1Value};
use crate::avm2::{
Activation as Avm2Activation, Avm2, Error as Avm2Error, EventObject as Avm2EventObject,
Multiname as Avm2Multiname, Object as Avm2Object, TObject as Avm2TObject, Value as Avm2Value,
};
use crate::context::{RenderContext, UpdateContext};
use crate::drawing::Drawing;
2019-04-29 05:55:44 +00:00
use crate::prelude::*;
use crate::string::{AvmString, WString};
use crate::tag_utils::SwfMovie;
use crate::types::{Degrees, Percent};
use crate::vminterface::Instantiator;
use bitflags::bitflags;
use gc_arena::{Collect, Mutation};
use ruffle_macros::enum_trait_object;
use ruffle_render::pixel_bender::PixelBenderShaderHandle;
use ruffle_render::transform::{Transform, TransformStack};
use std::cell::{Ref, RefMut};
use std::fmt::Debug;
2023-05-31 09:32:58 +00:00
use std::hash::Hash;
use std::sync::Arc;
use swf::{ColorTransform, Fixed8};
2019-04-25 17:52:22 +00:00
2021-04-23 01:10:29 +00:00
mod avm1_button;
2021-04-24 01:16:27 +00:00
mod avm2_button;
mod bitmap;
mod container;
mod edit_text;
mod graphic;
mod interactive;
mod loader_display;
mod morph_shape;
mod movie_clip;
mod stage;
mod text;
2020-10-15 04:12:31 +00:00
mod video;
use crate::avm1::Activation;
use crate::display_object::bitmap::BitmapWeak;
pub use crate::display_object::container::{
dispatch_added_event_only, dispatch_added_to_stage_event_only, DisplayObjectContainer,
TDisplayObjectContainer,
};
pub use avm1_button::{Avm1Button, ButtonState, ButtonTracking};
pub use avm2_button::Avm2Button;
pub use bitmap::Bitmap;
pub use edit_text::{AutoSizeMode, EditText, TextSelection};
pub use graphic::Graphic;
pub use interactive::{Avm2MousePick, InteractiveObject, TInteractiveObject};
pub use loader_display::LoaderDisplay;
pub use morph_shape::MorphShape;
pub use movie_clip::{MovieClip, MovieClipWeak, Scene};
use ruffle_render::backend::{BitmapCacheEntry, RenderBackend};
use ruffle_render::bitmap::{BitmapHandle, BitmapInfo, PixelSnapping};
use ruffle_render::blend::ExtendedBlendMode;
use ruffle_render::commands::{CommandHandler, CommandList, RenderBlendMode};
use ruffle_render::filters::Filter;
pub use stage::{Stage, StageAlign, StageDisplayState, StageScaleMode, WindowMode};
pub use text::Text;
pub use video::Video;
use self::loader_display::LoaderDisplayWeak;
/// If a `DisplayObject` is marked `cacheAsBitmap` (via tag or AS),
/// this struct keeps the information required to uphold that cache.
/// A cached Display Object must have its bitmap invalidated when
/// any "visual" change happens, which can include:
/// - Changing the rotation
/// - Changing the scale
/// - Changing the alpha
/// - Changing the color transform
/// - Any "visual" change to children, **including** position changes
///
/// Position changes to the cached Display Object does not regenerate the cache,
/// allowing Display Objects to move freely without being regenerated.
///
/// Flash isn't very good at always recognising when it should be invalidated,
/// and there's cases such as changing the blend mode which don't always trigger it.
///
#[derive(Clone, Debug, Default)]
pub struct BitmapCache {
/// The `Matrix.a` value that was last used with this cache
matrix_a: f32,
/// The `Matrix.b` value that was last used with this cache
matrix_b: f32,
/// The `Matrix.c` value that was last used with this cache
matrix_c: f32,
/// The `Matrix.d` value that was last used with this cache
matrix_d: f32,
/// The width of the original bitmap, pre-filters
source_width: u16,
/// The height of the original bitmap, pre-filters
source_height: u16,
/// The offset used to draw the final bitmap (i.e. if a filter increases the size)
draw_offset: Point<i32>,
/// The current contents of the cache, if any. Values are post-filters.
bitmap: Option<BitmapInfo>,
/// Whether we warned that this bitmap was too large to be cached
warned_for_oversize: bool,
}
impl BitmapCache {
/// Forcefully make this BitmapCache invalid and require regeneration.
/// This should be used for changes that aren't automatically detected, such as children.
pub fn make_dirty(&mut self) {
// Setting the old transform to something invalid is a cheap way of making it invalid,
// without reserving an extra field for.
self.matrix_a = f32::NAN;
}
fn is_dirty(&self, other: &Matrix, source_width: u16, source_height: u16) -> bool {
self.matrix_a != other.a
|| self.matrix_b != other.b
|| self.matrix_c != other.c
|| self.matrix_d != other.d
|| self.source_width != source_width
|| self.source_height != source_height
|| self.bitmap.is_none()
}
/// Clears any dirtiness and ensure there's an appropriately sized texture allocated
#[allow(clippy::too_many_arguments)]
fn update(
&mut self,
renderer: &mut dyn RenderBackend,
matrix: Matrix,
source_width: u16,
source_height: u16,
actual_width: u16,
actual_height: u16,
draw_offset: Point<i32>,
swf_version: u8,
) {
self.matrix_a = matrix.a;
self.matrix_b = matrix.b;
self.matrix_c = matrix.c;
self.matrix_d = matrix.d;
self.source_width = source_width;
self.source_height = source_height;
self.draw_offset = draw_offset;
if let Some(current) = &mut self.bitmap {
if current.width == actual_width && current.height == actual_height {
return; // No need to resize it
}
}
let acceptable_size = if swf_version > 9 {
let total = actual_width as u32 * actual_height as u32;
actual_width < 8191 && actual_height < 8191 && total < 16777215
} else {
actual_width < 2880 && actual_height < 2880
};
if renderer.is_offscreen_supported()
&& actual_width > 0
&& actual_height > 0
&& acceptable_size
{
let handle = renderer.create_empty_texture(actual_width as u32, actual_height as u32);
self.bitmap = handle.ok().map(|handle| BitmapInfo {
width: actual_width,
height: actual_height,
handle,
});
} else {
self.bitmap = None;
}
}
/// Explicitly clears the cached value and drops any resources.
/// This should only be used in situations where you can't render to the cache and it needs to be
/// temporarily disabled.
fn clear(&mut self) {
self.bitmap = None;
}
fn handle(&self) -> Option<BitmapHandle> {
self.bitmap.as_ref().map(|b| b.handle.clone())
}
}
#[derive(Clone, Collect)]
#[collect(no_drop)]
2019-08-13 02:00:12 +00:00
pub struct DisplayObjectBase<'gc> {
parent: Option<DisplayObject<'gc>>,
place_frame: u16,
2019-04-29 05:55:44 +00:00
depth: Depth,
#[collect(require_static)]
2019-04-29 05:55:44 +00:00
transform: Transform,
name: Option<AvmString<'gc>>,
#[collect(require_static)]
filters: Vec<Filter>,
2019-05-12 17:48:00 +00:00
clip_depth: Depth,
2019-10-18 04:10:13 +00:00
2019-12-04 01:52:00 +00:00
// 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.
#[collect(require_static)]
rotation: Degrees,
#[collect(require_static)]
scale_x: Percent,
#[collect(require_static)]
scale_y: Percent,
2019-12-04 01:52:00 +00:00
skew: f64,
/// The next display object in order of execution.
///
/// `None` in an AVM2 movie.
next_avm1_clip: Option<DisplayObject<'gc>>,
2019-10-18 04:10:13 +00:00
/// The sound transform of sounds playing via this display object.
#[collect(require_static)]
sound_transform: SoundTransform,
/// The display object that we are being masked by.
masker: Option<DisplayObject<'gc>>,
/// The display object we are currently masking.
maskee: Option<DisplayObject<'gc>>,
2022-05-01 23:33:04 +00:00
/// The blend mode used when rendering this display object.
/// Values other than the default `BlendMode::Normal` implicitly cause cache-as-bitmap behavior.
2022-05-01 23:33:04 +00:00
#[collect(require_static)]
blend_mode: ExtendedBlendMode,
#[collect(require_static)]
blend_shader: Option<PixelBenderShaderHandle>,
2022-05-01 23:33:04 +00:00
/// The opaque background color of this display object.
/// The bounding box of the display object will be filled with the given color. This also
/// triggers cache-as-bitmap behavior. Only solid backgrounds are supported; the alpha channel
/// is ignored.
#[collect(require_static)]
opaque_background: Option<Color>,
2021-06-21 16:29:52 +00:00
/// Bit flags for various display object properties.
#[collect(require_static)]
flags: DisplayObjectFlags,
/// The 'internal' scroll rect used for rendering and methods like 'localToGlobal'.
/// This is updated from 'pre_render'
#[collect(require_static)]
scroll_rect: Option<Rectangle<Twips>>,
/// The 'next' scroll rect, which we will copy to 'scroll_rect' from 'pre_render'.
/// This is used by the ActionScript 'DisplayObject.scrollRect' getter, which sees
/// changes immediately (without needing wait for a render)
#[collect(require_static)]
next_scroll_rect: Rectangle<Twips>,
/// Rectangle used for 9-slice scaling (`DislayObject.scale9grid`).
#[collect(require_static)]
scaling_grid: Rectangle<Twips>,
/// If this Display Object should cacheAsBitmap - and if so, the cache itself.
/// None means not cached, Some means cached.
#[collect(require_static)]
cache: Option<BitmapCache>,
2019-04-25 17:52:22 +00:00
}
impl<'gc> Default for DisplayObjectBase<'gc> {
fn default() -> Self {
2019-04-29 05:55:44 +00:00
Self {
2019-08-13 02:00:12 +00:00
parent: Default::default(),
place_frame: Default::default(),
2019-04-29 05:55:44 +00:00
depth: Default::default(),
transform: Default::default(),
name: None,
filters: Default::default(),
2019-05-12 17:48:00 +00:00
clip_depth: Default::default(),
rotation: Degrees::from_radians(0.0),
scale_x: Percent::from_unit(1.0),
scale_y: Percent::from_unit(1.0),
2019-12-04 01:52:00 +00:00
skew: 0.0,
next_avm1_clip: None,
masker: None,
maskee: None,
sound_transform: Default::default(),
2022-05-01 23:33:04 +00:00
blend_mode: Default::default(),
blend_shader: None,
opaque_background: Default::default(),
flags: DisplayObjectFlags::VISIBLE,
scroll_rect: None,
next_scroll_rect: Default::default(),
scaling_grid: Default::default(),
cache: None,
2019-04-26 03:27:44 +00:00
}
}
2019-04-29 05:55:44 +00:00
}
2019-04-26 03:27:44 +00:00
impl<'gc> DisplayObjectBase<'gc> {
/// Reset all properties that would be adjusted by a movie load.
fn reset_for_movie_load(&mut self) {
let flags_to_keep = self.flags & DisplayObjectFlags::LOCK_ROOT;
self.flags = flags_to_keep | DisplayObjectFlags::VISIBLE;
}
2019-09-08 02:33:06 +00:00
fn depth(&self) -> Depth {
self.depth
}
fn set_depth(&mut self, depth: Depth) {
self.depth = depth;
}
fn place_frame(&self) -> u16 {
self.place_frame
}
fn set_place_frame(&mut self, frame: u16) {
self.place_frame = frame;
}
2019-05-01 16:55:54 +00:00
fn transform(&self) -> &Transform {
&self.transform
}
pub fn matrix(&self) -> &Matrix {
2019-04-29 05:55:44 +00:00
&self.transform.matrix
2019-04-25 17:52:22 +00:00
}
pub fn matrix_mut(&mut self) -> &mut Matrix {
&mut self.transform.matrix
}
pub fn set_matrix(&mut self, matrix: Matrix) {
self.transform.matrix = matrix;
2022-08-28 18:23:09 +00:00
self.set_scale_rotation_cached(false);
2019-04-26 03:27:44 +00:00
}
pub fn color_transform(&self) -> &ColorTransform {
2019-04-29 05:55:44 +00:00
&self.transform.color_transform
}
pub fn color_transform_mut(&mut self) -> &mut ColorTransform {
2019-12-04 01:52:00 +00:00
&mut self.transform.color_transform
}
pub fn set_color_transform(&mut self, color_transform: ColorTransform) {
self.transform.color_transform = color_transform;
2019-04-25 17:52:22 +00:00
}
fn x(&self) -> Twips {
self.transform.matrix.tx
2019-12-04 01:52:00 +00:00
}
fn set_x(&mut self, x: Twips) -> bool {
let changed = self.transform.matrix.tx != x;
self.set_transformed_by_script(true);
self.transform.matrix.tx = x;
changed
2019-12-04 01:52:00 +00:00
}
fn y(&self) -> Twips {
self.transform.matrix.ty
2019-12-04 01:52:00 +00:00
}
fn set_y(&mut self, y: Twips) -> bool {
let changed = self.transform.matrix.ty != y;
self.set_transformed_by_script(true);
self.transform.matrix.ty = y;
changed
2019-12-04 01:52:00 +00:00
}
/// Caches the scale and rotation factors for this display object, if necessary.
/// Calculating these requires heavy trig ops, so we only do it when `_xscale`, `_yscale` or
/// `_rotation` is accessed.
fn cache_scale_rotation(&mut self) {
2022-08-28 18:23:09 +00:00
if !self.scale_rotation_cached() {
2019-12-04 01:52:00 +00:00
let (a, b, c, d) = (
f64::from(self.transform.matrix.a),
f64::from(self.transform.matrix.b),
f64::from(self.transform.matrix.c),
f64::from(self.transform.matrix.d),
2019-12-04 01:52:00 +00:00
);
// If this object's transform matrix is:
// [[a c tx]
// [b d ty]]
// After transformation, the X-axis and Y-axis will turn into the column vectors x' = <a, b> and y' = <c, d>.
// We derive the scale, rotation, and skew values from these transformed axes.
// The skew value is not exposed by ActionScript, but is remembered internally.
// xscale = len(x')
// yscale = len(y')
// rotation = atan2(b, a) (the rotation of x' from the normal x-axis).
// skew = atan2(-c, d) - atan2(b, a) (the signed difference between y' and x' rotation)
// This can produce some surprising results due to the overlap between flipping/rotation/skewing.
// For example, in Flash, using Modify->Transform->Flip Horizontal and then tracing _xscale, _yscale, and _rotation
// will output 100, 100, and 180. (a horizontal flip could also be a 180 degree skew followed by 180 degree rotation!)
let rotation_x = f64::atan2(b, a);
let rotation_y = f64::atan2(-c, d);
let scale_x = f64::sqrt(a * a + b * b);
let scale_y = f64::sqrt(c * c + d * d);
self.rotation = Degrees::from_radians(rotation_x);
self.scale_x = Percent::from_unit(scale_x);
self.scale_y = Percent::from_unit(scale_y);
self.skew = rotation_y - rotation_x;
2019-12-04 01:52:00 +00:00
}
}
fn rotation(&mut self) -> Degrees {
2019-12-04 01:52:00 +00:00
self.cache_scale_rotation();
self.rotation
}
fn set_rotation(&mut self, degrees: Degrees) -> bool {
self.set_transformed_by_script(true);
2019-12-04 01:52:00 +00:00
self.cache_scale_rotation();
let changed = self.rotation != degrees;
self.rotation = degrees;
// FIXME - this isn't quite correct. In Flash player,
// trying to set rotation to NaN does nothing if the current
// matrix 'b' and 'd' terms are both zero. However, if one
// of those terms is non-zero, then the entire matrix gets
// modified in a way that depends on its starting values.
// I haven't been able to figure out how to reproduce those
// values, so for now, we never modify the matrix if the
// rotation is NaN. Hopefully, there are no SWFs depending
// on the weird behavior when b or d is non-zero.
if degrees.into_radians().is_nan() {
return changed;
}
let cos_x = f64::cos(degrees.into_radians());
let sin_x = f64::sin(degrees.into_radians());
let cos_y = f64::cos(degrees.into_radians() + self.skew);
let sin_y = f64::sin(degrees.into_radians() + self.skew);
2023-05-26 21:24:42 +00:00
let matrix = &mut self.transform.matrix;
matrix.a = (self.scale_x.unit() * cos_x) as f32;
matrix.b = (self.scale_x.unit() * sin_x) as f32;
matrix.c = (self.scale_y.unit() * -sin_y) as f32;
matrix.d = (self.scale_y.unit() * cos_y) as f32;
changed
2019-12-04 01:52:00 +00:00
}
fn scale_x(&mut self) -> Percent {
2019-12-04 01:52:00 +00:00
self.cache_scale_rotation();
self.scale_x
}
fn set_scale_x(&mut self, mut value: Percent) -> bool {
let changed = self.scale_x != value;
self.set_transformed_by_script(true);
2019-12-04 01:52:00 +00:00
self.cache_scale_rotation();
self.scale_x = value;
// Note - in order to match Flash's behavior, the 'scale_x' field is set to NaN
// (which gets reported back to ActionScript), but we treat it as 0 for
// the purposes of updating the matrix
if value.percent().is_nan() {
value = 0.0.into();
}
let cos = f64::cos(self.rotation.into_radians());
let sin = f64::sin(self.rotation.into_radians());
2023-05-26 21:24:42 +00:00
let matrix = &mut self.transform.matrix;
matrix.a = (cos * value.unit()) as f32;
matrix.b = (sin * value.unit()) as f32;
changed
2019-12-04 01:52:00 +00:00
}
fn scale_y(&mut self) -> Percent {
2019-12-04 01:52:00 +00:00
self.cache_scale_rotation();
self.scale_y
}
fn set_scale_y(&mut self, mut value: Percent) -> bool {
let changed = self.scale_y != value;
self.set_transformed_by_script(true);
2019-12-04 01:52:00 +00:00
self.cache_scale_rotation();
self.scale_y = value;
// Note - in order to match Flash's behavior, the 'scale_y' field is set to NaN
// (which gets reported back to ActionScript), but we treat it as 0 for
// the purposes of updating the matrix
if value.percent().is_nan() {
value = 0.0.into();
}
let cos = f64::cos(self.rotation.into_radians() + self.skew);
let sin = f64::sin(self.rotation.into_radians() + self.skew);
2023-05-26 21:24:42 +00:00
let matrix = &mut self.transform.matrix;
matrix.c = (-sin * value.unit()) as f32;
matrix.d = (cos * value.unit()) as f32;
changed
2019-12-04 01:52:00 +00:00
}
fn name(&self) -> Option<AvmString<'gc>> {
self.name
}
fn set_name(&mut self, name: AvmString<'gc>) {
self.name = Some(name);
}
fn filters(&self) -> Vec<Filter> {
self.filters.clone()
}
fn set_filters(&mut self, filters: Vec<Filter>) -> bool {
if filters != self.filters {
self.filters = filters;
self.recheck_cache_as_bitmap();
true
} else {
false
}
}
2019-12-04 01:52:00 +00:00
fn alpha(&self) -> f64 {
f64::from(self.color_transform().a_multiply)
2019-12-04 01:52:00 +00:00
}
fn set_alpha(&mut self, value: f64) -> bool {
let changed = self.alpha() != value;
self.set_transformed_by_script(true);
self.color_transform_mut().a_multiply = Fixed8::from_f64(value);
changed
2019-12-04 01:52:00 +00:00
}
2019-05-12 17:48:00 +00:00
fn clip_depth(&self) -> Depth {
self.clip_depth
}
fn set_clip_depth(&mut self, depth: Depth) {
2019-05-12 17:48:00 +00:00
self.clip_depth = depth;
}
fn parent(&self) -> Option<DisplayObject<'gc>> {
2019-08-13 02:00:12 +00:00
self.parent
}
/// You should almost always use `DisplayObject.set_parent` instead, which
/// properly handles 'orphan' movie clips
fn set_parent_ignoring_orphan_list(&mut self, parent: Option<DisplayObject<'gc>>) {
2019-08-13 02:00:12 +00:00
self.parent = parent;
}
fn next_avm1_clip(&self) -> Option<DisplayObject<'gc>> {
self.next_avm1_clip
2019-10-18 04:10:13 +00:00
}
fn set_next_avm1_clip(&mut self, node: Option<DisplayObject<'gc>>) {
self.next_avm1_clip = node;
2019-10-18 04:10:13 +00:00
}
fn avm1_removed(&self) -> bool {
self.flags.contains(DisplayObjectFlags::AVM1_REMOVED)
2019-12-04 01:52:00 +00:00
}
fn avm1_pending_removal(&self) -> bool {
self.flags
.contains(DisplayObjectFlags::AVM1_PENDING_REMOVAL)
}
pub fn should_skip_next_enter_frame(&self) -> bool {
self.flags
.contains(DisplayObjectFlags::SKIP_NEXT_ENTER_FRAME)
}
pub fn set_skip_next_enter_frame(&mut self, skip: bool) {
self.flags
.set(DisplayObjectFlags::SKIP_NEXT_ENTER_FRAME, skip);
}
fn set_avm1_removed(&mut self, value: bool) {
self.flags.set(DisplayObjectFlags::AVM1_REMOVED, value);
2019-10-18 04:10:13 +00:00
}
2019-12-04 01:52:00 +00:00
fn set_avm1_pending_removal(&mut self, value: bool) {
self.flags
.set(DisplayObjectFlags::AVM1_PENDING_REMOVAL, value);
}
2022-08-28 18:23:09 +00:00
fn scale_rotation_cached(&self) -> bool {
self.flags
.contains(DisplayObjectFlags::SCALE_ROTATION_CACHED)
}
fn set_scale_rotation_cached(&mut self, set_flag: bool) {
if set_flag {
self.flags |= DisplayObjectFlags::SCALE_ROTATION_CACHED;
} else {
self.flags -= DisplayObjectFlags::SCALE_ROTATION_CACHED;
}
}
pub fn sound_transform(&self) -> &SoundTransform {
&self.sound_transform
}
pub fn set_sound_transform(&mut self, sound_transform: SoundTransform) {
self.sound_transform = sound_transform;
}
2019-12-04 01:52:00 +00:00
fn visible(&self) -> bool {
self.flags.contains(DisplayObjectFlags::VISIBLE)
2019-12-04 01:52:00 +00:00
}
fn set_visible(&mut self, value: bool) -> bool {
let changed = self.visible() != value;
2021-03-06 12:26:19 +00:00
self.flags.set(DisplayObjectFlags::VISIBLE, value);
changed
2019-10-18 04:10:13 +00:00
}
fn blend_mode(&self) -> ExtendedBlendMode {
2022-05-01 23:33:04 +00:00
self.blend_mode
}
fn set_blend_mode(&mut self, value: ExtendedBlendMode) -> bool {
let changed = self.blend_mode != value;
2022-05-01 23:33:04 +00:00
self.blend_mode = value;
changed
2022-05-01 23:33:04 +00:00
}
fn blend_shader(&self) -> Option<PixelBenderShaderHandle> {
self.blend_shader.clone()
}
fn set_blend_shader(&mut self, value: Option<PixelBenderShaderHandle>) {
self.blend_shader = value;
}
/// The opaque background color of this display object.
/// The bounding box of the display object will be filled with this color.
fn opaque_background(&self) -> Option<Color> {
2023-06-29 17:46:30 +00:00
self.opaque_background
}
/// The opaque background color of this display object.
/// The bounding box of the display object will be filled with the given color. This also
/// triggers cache-as-bitmap behavior. Only solid backgrounds are supported; the alpha channel
/// is ignored.
fn set_opaque_background(&mut self, value: Option<Color>) -> bool {
let value = value.map(|mut color| {
color.a = 255;
color
});
let changed = self.opaque_background != value;
self.opaque_background = value;
changed
}
fn is_root(&self) -> bool {
self.flags.contains(DisplayObjectFlags::IS_ROOT)
}
fn set_is_root(&mut self, value: bool) {
self.flags.set(DisplayObjectFlags::IS_ROOT, value);
}
fn lock_root(&self) -> bool {
self.flags.contains(DisplayObjectFlags::LOCK_ROOT)
}
fn set_lock_root(&mut self, value: bool) {
2021-03-06 12:26:19 +00:00
self.flags.set(DisplayObjectFlags::LOCK_ROOT, value);
}
fn transformed_by_script(&self) -> bool {
self.flags
.contains(DisplayObjectFlags::TRANSFORMED_BY_SCRIPT)
}
fn set_transformed_by_script(&mut self, value: bool) {
2021-03-06 12:26:19 +00:00
self.flags
.set(DisplayObjectFlags::TRANSFORMED_BY_SCRIPT, value);
}
fn placed_by_script(&self) -> bool {
self.flags.contains(DisplayObjectFlags::PLACED_BY_SCRIPT)
}
fn set_placed_by_script(&mut self, value: bool) {
2021-03-06 12:26:19 +00:00
self.flags.set(DisplayObjectFlags::PLACED_BY_SCRIPT, value);
}
fn is_bitmap_cached_preference(&self) -> bool {
self.flags.contains(DisplayObjectFlags::CACHE_AS_BITMAP)
}
fn set_bitmap_cached_preference(&mut self, value: bool) {
self.flags.set(DisplayObjectFlags::CACHE_AS_BITMAP, value);
self.recheck_cache_as_bitmap();
}
fn bitmap_cache_mut(&mut self) -> Option<&mut BitmapCache> {
self.cache.as_mut()
}
/// Invalidates a cached bitmap, if it exists.
/// This may only be called once per frame - the first call will return true, regardless of
/// if there was a cache.
/// Any subsequent calls will return false, indicating that you do not need to invalidate the ancestors.
/// This is reset during rendering.
fn invalidate_cached_bitmap(&mut self) -> bool {
if self.flags.contains(DisplayObjectFlags::CACHE_INVALIDATED) {
return false;
}
if let Some(cache) = &mut self.cache {
cache.make_dirty();
}
self.flags.insert(DisplayObjectFlags::CACHE_INVALIDATED);
true
}
fn clear_invalidate_flag(&mut self) {
self.flags.remove(DisplayObjectFlags::CACHE_INVALIDATED);
}
fn recheck_cache_as_bitmap(&mut self) {
let should_cache = self.is_bitmap_cached_preference() || !self.filters.is_empty();
if should_cache && self.cache.is_none() {
self.cache = Some(Default::default());
} else if !should_cache && self.cache.is_some() {
self.cache = None;
}
}
fn instantiated_by_timeline(&self) -> bool {
self.flags
.contains(DisplayObjectFlags::INSTANTIATED_BY_TIMELINE)
}
fn set_instantiated_by_timeline(&mut self, value: bool) {
2021-03-06 12:26:19 +00:00
self.flags
.set(DisplayObjectFlags::INSTANTIATED_BY_TIMELINE, value);
}
fn has_scroll_rect(&self) -> bool {
self.flags.contains(DisplayObjectFlags::HAS_SCROLL_RECT)
}
fn set_has_scroll_rect(&mut self, value: bool) {
self.flags.set(DisplayObjectFlags::HAS_SCROLL_RECT, value);
}
fn has_explicit_name(&self) -> bool {
self.flags.contains(DisplayObjectFlags::HAS_EXPLICIT_NAME)
}
fn set_has_explicit_name(&mut self, value: bool) {
self.flags.set(DisplayObjectFlags::HAS_EXPLICIT_NAME, value);
}
fn masker(&self) -> Option<DisplayObject<'gc>> {
self.masker
}
fn set_masker(&mut self, node: Option<DisplayObject<'gc>>) {
self.masker = node;
}
fn maskee(&self) -> Option<DisplayObject<'gc>> {
self.maskee
}
fn set_maskee(&mut self, node: Option<DisplayObject<'gc>>) {
self.maskee = node;
}
2019-04-29 05:55:44 +00:00
}
struct DrawCacheInfo {
handle: BitmapHandle,
dirty: bool,
base_transform: Transform,
bounds: Rectangle<Twips>,
draw_offset: Point<i32>,
filters: Vec<Filter>,
}
pub fn render_base<'gc>(this: DisplayObject<'gc>, context: &mut RenderContext<'_, 'gc>) {
if this.maskee().is_some() {
return;
}
2022-06-07 21:28:05 +00:00
context.transform_stack.push(this.base().transform());
let blend_mode = this.blend_mode();
let original_commands = if blend_mode != ExtendedBlendMode::Normal {
Some(std::mem::take(&mut context.commands))
} else {
None
};
let cache_info = if context.use_bitmap_cache && this.is_bitmap_cached() {
let mut cache_info: Option<DrawCacheInfo> = None;
let base_transform = context.transform_stack.transform();
let bounds: Rectangle<Twips> = this.render_bounds_with_transform(
&base_transform.matrix,
false, // we want to do the filter growth for this object ourselves, to know the offsets
&context.stage.view_matrix(),
);
let name = this.name();
let mut filters: Vec<Filter> = this.filters();
let swf_version = this.swf_version();
filters.retain(|f| !f.impotent());
if let Some(cache) = this.base_mut(context.gc_context).bitmap_cache_mut() {
let width = bounds.width().to_pixels().ceil().max(0.0);
let height = bounds.height().to_pixels().ceil().max(0.0);
if width <= u16::MAX as f64 && height <= u16::MAX as f64 {
let width = width as u16;
let height = height as u16;
let mut filter_rect = Rectangle {
x_min: Twips::ZERO,
x_max: Twips::from_pixels_i32(width as i32),
y_min: Twips::ZERO,
y_max: Twips::from_pixels_i32(height as i32),
};
let stage_matrix = context.stage.view_matrix();
for filter in &mut filters {
// Scaling is done by *stage view matrix* only, nothing in-between
filter.scale(stage_matrix.a, stage_matrix.d);
filter_rect = filter.calculate_dest_rect(filter_rect);
}
let filter_rect = Rectangle {
x_min: filter_rect.x_min.to_pixels().floor() as i32,
x_max: filter_rect.x_max.to_pixels().ceil() as i32,
y_min: filter_rect.y_min.to_pixels().floor() as i32,
y_max: filter_rect.y_max.to_pixels().ceil() as i32,
};
let draw_offset = Point::new(filter_rect.x_min, filter_rect.y_min);
if cache.is_dirty(&base_transform.matrix, width, height) {
cache.update(
context.renderer,
base_transform.matrix,
width,
height,
filter_rect.width() as u16,
filter_rect.height() as u16,
draw_offset,
swf_version,
);
cache_info = cache.handle().map(|handle| DrawCacheInfo {
handle,
dirty: true,
base_transform,
bounds,
draw_offset,
filters,
});
} else {
cache_info = cache.handle().map(|handle| DrawCacheInfo {
handle,
dirty: false,
base_transform,
bounds,
draw_offset,
filters,
});
}
} else {
if !cache.warned_for_oversize {
tracing::warn!(
"Skipping cacheAsBitmap for incredibly large object {:?} ({width} x {height})",
name
);
cache.warned_for_oversize = true;
}
cache.clear();
cache_info = None;
}
}
cache_info
} else {
None
};
// We can't hold `cache` (which will hold `base`), so this is split up
if let Some(cache_info) = cache_info {
// In order to render an object to a texture, we need to draw its entire bounds.
// Calculate the offset from tx/ty in order to accommodate any drawings that extend the bounds
// negatively
let offset_x = cache_info.bounds.x_min - cache_info.base_transform.matrix.tx
+ Twips::from_pixels_i32(cache_info.draw_offset.x);
let offset_y = cache_info.bounds.y_min - cache_info.base_transform.matrix.ty
+ Twips::from_pixels_i32(cache_info.draw_offset.y);
if cache_info.dirty {
let mut transform_stack = TransformStack::new();
transform_stack.push(&Transform {
color_transform: Default::default(),
matrix: Matrix {
tx: -offset_x,
ty: -offset_y,
..cache_info.base_transform.matrix
},
});
let mut offscreen_context = RenderContext {
renderer: context.renderer,
commands: CommandList::new(),
cache_draws: context.cache_draws,
gc_context: context.gc_context,
library: context.library,
transform_stack: &mut transform_stack,
is_offscreen: true,
use_bitmap_cache: true,
stage: context.stage,
};
this.render_self(&mut offscreen_context);
offscreen_context.cache_draws.push(BitmapCacheEntry {
handle: cache_info.handle.clone(),
commands: offscreen_context.commands,
clear: this.opaque_background().unwrap_or_default(),
filters: cache_info.filters,
});
}
// When rendering it back, ensure we're only keeping the translation - scale/rotation is within the image already
apply_standard_mask_and_scroll(this, context, |context| {
context.commands.render_bitmap(
cache_info.handle,
Transform {
matrix: Matrix {
tx: context.transform_stack.transform().matrix.tx + offset_x,
ty: context.transform_stack.transform().matrix.ty + offset_y,
..Default::default()
},
color_transform: cache_info.base_transform.color_transform,
},
true,
PixelSnapping::Always, // cacheAsBitmap forces pixel snapping
)
});
} else {
if let Some(background) = this.opaque_background() {
// This is intended for use with cacheAsBitmap, but can be set for non-cached objects too
// It wants the entire bounding box to be cleared before any draws happen
let bounds: Rectangle<Twips> = this.render_bounds_with_transform(
&context.transform_stack.transform().matrix,
true,
&context.stage.view_matrix(),
);
context.commands.draw_rect(
background,
Matrix::create_box(
bounds.width().to_pixels() as f32,
bounds.height().to_pixels() as f32,
0.0,
bounds.x_min,
bounds.y_min,
),
);
}
apply_standard_mask_and_scroll(this, context, |context| this.render_self(context));
}
if let Some(original_commands) = original_commands {
let sub_commands = std::mem::replace(&mut context.commands, original_commands);
let render_blend_mode = if let ExtendedBlendMode::Shader = blend_mode {
// Note - Flash appears to let you set `dobj.blendMode = BlendMode.SHADER` without
// having `dobj.blendShader` result, but the resulting rendered displayobject
// seems to be corrupted. For now, let's panic, and see if any swfs actually
// rely on this behavior.
RenderBlendMode::Shader(this.blend_shader().expect("Missing blend shader"))
} else {
RenderBlendMode::Builtin(blend_mode.try_into().unwrap())
};
context.commands.blend(sub_commands, render_blend_mode);
}
context.transform_stack.pop();
}
/// This applies the **standard** method of `mask` and `scrollRect`.
///
/// It uses the stencil buffer so that any pixel drawn in the mask will allow the inner contents to show.
/// This is what is used for most cases, except for cacheAsBitmap-on-cacheAsBitmap.
pub fn apply_standard_mask_and_scroll<'gc, F>(
this: DisplayObject<'gc>,
context: &mut RenderContext<'_, 'gc>,
draw: F,
) where
F: FnOnce(&mut RenderContext<'_, 'gc>),
{
let scroll_rect_matrix = if let Some(rect) = this.scroll_rect() {
let cur_transform = context.transform_stack.transform();
// The matrix we use for actually drawing a rectangle for cropping purposes
// Note that we do *not* apply the translation yet
Some(
cur_transform.matrix
* Matrix::scale(
rect.width().to_pixels() as f32,
rect.height().to_pixels() as f32,
),
)
} else {
None
};
if let Some(rect) = this.scroll_rect() {
// Translate everything that we render (including DisplayObject.mask)
context.transform_stack.push(&Transform {
matrix: Matrix::translate(-rect.x_min, -rect.y_min),
color_transform: Default::default(),
});
}
let mask = this.masker();
let mut mask_transform = ruffle_render::transform::Transform::default();
if let Some(m) = mask {
mask_transform.matrix = this.global_to_local_matrix().unwrap_or_default();
mask_transform.matrix *= m.local_to_global_matrix();
context.commands.push_mask();
context.transform_stack.push(&mask_transform);
m.render_self(context);
context.transform_stack.pop();
context.commands.activate_mask();
}
// There are two parts to 'DisplayObject.scrollRect':
// a scroll effect (translation), and a crop effect.
// This scroll is implementing by appling a translation matrix
// when we defined 'scroll_rect_matrix'.
// The crop is implemented as a rectangular mask using the height
// and width provided by 'scrollRect'.
// Note that this mask is applied *in additon to* a mask defined
// with 'DisplayObject.mask'. We will end up rendering content that
// lies in the intersection of the scroll rect and DisplayObject.mask,
// which is exactly the behavior that we want.
if let Some(rect_mat) = scroll_rect_matrix {
context.commands.push_mask();
// The color doesn't matter, as this is a mask.
context.commands.draw_rect(Color::WHITE, rect_mat);
context.commands.activate_mask();
}
draw(context);
if let Some(rect_mat) = scroll_rect_matrix {
// Draw the rectangle again after deactivating the mask,
// to reset the stencil buffer.
context.commands.deactivate_mask();
context.commands.draw_rect(Color::WHITE, rect_mat);
context.commands.pop_mask();
}
if let Some(m) = mask {
context.commands.deactivate_mask();
context.transform_stack.push(&mask_transform);
m.render_self(context);
context.transform_stack.pop();
context.commands.pop_mask();
}
if scroll_rect_matrix.is_some() {
// Remove the translation that we pushed
context.transform_stack.pop();
}
}
#[enum_trait_object(
#[derive(Clone, Collect, Debug, Copy)]
#[collect(no_drop)]
pub enum DisplayObject<'gc> {
Stage(Stage<'gc>),
Bitmap(Bitmap<'gc>),
2021-04-23 01:10:29 +00:00
Avm1Button(Avm1Button<'gc>),
2021-04-24 01:16:27 +00:00
Avm2Button(Avm2Button<'gc>),
EditText(EditText<'gc>),
Graphic(Graphic<'gc>),
MorphShape(MorphShape<'gc>),
MovieClip(MovieClip<'gc>),
Text(Text<'gc>),
2020-10-15 04:12:31 +00:00
Video(Video<'gc>),
LoaderDisplay(LoaderDisplay<'gc>)
}
)]
pub trait TDisplayObject<'gc>:
'gc + Clone + Copy + Collect + Debug + Into<DisplayObject<'gc>>
{
fn base<'a>(&'a self) -> Ref<'a, DisplayObjectBase<'gc>>;
fn base_mut<'a>(&'a self, mc: &Mutation<'gc>) -> RefMut<'a, DisplayObjectBase<'gc>>;
2022-08-28 18:23:09 +00:00
/// The `SCALE_ROTATION_CACHED` flag should only be set in SWFv5+.
/// So scaling/rotation values always have to get recalculated from the matrix in SWFv4.
fn set_scale_rotation_cached(&self, gc_context: &Mutation<'gc>) {
2022-08-28 18:23:09 +00:00
if self.swf_version() >= 5 {
self.base_mut(gc_context).set_scale_rotation_cached(true);
}
}
2019-09-15 18:34:30 +00:00
fn id(&self) -> CharacterId;
fn depth(&self) -> Depth {
self.base().depth()
}
fn set_depth(&self, gc_context: &Mutation<'gc>, depth: Depth) {
self.base_mut(gc_context).set_depth(depth)
}
2019-12-04 01:52:00 +00:00
/// The untransformed inherent bounding box of this object.
/// These bounds do **not** include child DisplayObjects.
/// To get the bounds including children, use `bounds`, `local_bounds`, or `world_bounds`.
///
/// Implementors must override this method.
/// Leaf DisplayObjects should return their bounds.
/// Composite DisplayObjects that only contain children should return `&Default::default()`
fn self_bounds(&self) -> Rectangle<Twips>;
/// The untransformed bounding box of this object including children.
fn bounds(&self) -> Rectangle<Twips> {
self.bounds_with_transform(&Matrix::default())
2019-12-04 01:52:00 +00:00
}
/// The local bounding box of this object including children, in its parent's coordinate system.
fn local_bounds(&self) -> Rectangle<Twips> {
self.bounds_with_transform(self.base().matrix())
}
/// The world bounding box of this object including children, relative to the stage.
fn world_bounds(&self) -> Rectangle<Twips> {
self.bounds_with_transform(&self.local_to_global_matrix())
}
/// Gets the bounds of this object and all children, transformed by a given matrix.
/// This function recurses down and transforms the AABB each child before adding
/// it to the bounding box. This gives a tighter AABB then if we simply transformed
/// the overall AABB.
fn bounds_with_transform(&self, matrix: &Matrix) -> Rectangle<Twips> {
// A scroll rect completely overrides an object's bounds,
// and can even grow the bounding box to be larger than the actual content
if let Some(scroll_rect) = self.scroll_rect() {
return *matrix
* Rectangle {
x_min: Twips::ZERO,
y_min: Twips::ZERO,
x_max: scroll_rect.width(),
y_max: scroll_rect.height(),
};
}
let mut bounds = *matrix * self.self_bounds();
if let Some(ctr) = self.as_container() {
for child in ctr.iter_render_list() {
let matrix = *matrix * *child.base().matrix();
bounds = bounds.union(&child.bounds_with_transform(&matrix));
}
2019-12-04 01:52:00 +00:00
}
2019-12-04 01:52:00 +00:00
bounds
}
/// Gets the **render bounds** of this object and all its children.
/// This differs from the bounds that are exposed to Flash, in two main ways:
/// - It may be larger if filters are applied which will increase the size of what's shown
/// - It does not respect scroll rects
fn render_bounds_with_transform(
&self,
matrix: &Matrix,
include_own_filters: bool,
view_matrix: &Matrix,
) -> Rectangle<Twips> {
let mut bounds = *matrix * self.self_bounds();
if let Some(ctr) = self.as_container() {
for child in ctr.iter_render_list() {
let matrix = *matrix * *child.base().matrix();
bounds =
bounds.union(&child.render_bounds_with_transform(&matrix, true, view_matrix));
}
}
if include_own_filters {
let filters = self.filters();
for mut filter in filters {
filter.scale(view_matrix.a, view_matrix.d);
bounds = filter.calculate_dest_rect(bounds);
}
}
bounds
}
fn place_frame(&self) -> u16 {
self.base().place_frame()
}
fn set_place_frame(&self, gc_context: &Mutation<'gc>, frame: u16) {
self.base_mut(gc_context).set_place_frame(frame)
}
/// Sets the matrix of this object.
/// This does NOT invalidate the cache, as it's often used with other operations.
/// It is the callers responsibility to do so.
fn set_matrix(&self, gc_context: &Mutation<'gc>, matrix: Matrix) {
self.base_mut(gc_context).set_matrix(matrix);
}
/// Sets the color transform of this object.
/// This does NOT invalidate the cache, as it's often used with other operations.
/// It is the callers responsibility to do so.
2023-08-15 18:35:57 +00:00
fn set_color_transform(&self, gc_context: &Mutation<'gc>, color_transform: ColorTransform) {
self.base_mut(gc_context)
.set_color_transform(color_transform)
}
2019-12-04 01:52:00 +00:00
/// Should only be used to implement 'Transform.concatenatedMatrix'
fn local_to_global_matrix_without_own_scroll_rect(&self) -> Matrix {
let mut node = self.parent();
let mut matrix = *self.base().matrix();
while let Some(display_object) = node {
2023-03-13 02:31:02 +00:00
// We want to transform to Stage-local coordinates,
// so do *not* apply the Stage's matrix
if display_object.as_stage().is_some() {
break;
}
if let Some(rect) = display_object.scroll_rect() {
matrix = Matrix::translate(-rect.x_min, -rect.y_min) * matrix;
}
matrix = *display_object.base().matrix() * matrix;
node = display_object.parent();
}
matrix
}
/// Returns the matrix for transforming from this object's local space to global stage space.
fn local_to_global_matrix(&self) -> Matrix {
let mut matrix = Matrix::IDENTITY;
if let Some(rect) = self.scroll_rect() {
matrix = Matrix::translate(-rect.x_min, -rect.y_min) * matrix;
}
self.local_to_global_matrix_without_own_scroll_rect() * matrix
}
/// Returns the matrix for transforming from global stage to this object's local space.
/// `None` is returned if the object has zero scale.
fn global_to_local_matrix(&self) -> Option<Matrix> {
self.local_to_global_matrix().inverse()
}
/// Converts a local position to a global stage position
fn local_to_global(&self, local: Point<Twips>) -> Point<Twips> {
self.local_to_global_matrix() * local
}
/// Converts a local position on the stage to a local position on this display object
/// Returns `None` if the object has zero scale.
fn global_to_local(&self, global: Point<Twips>) -> Option<Point<Twips>> {
self.global_to_local_matrix().map(|matrix| matrix * global)
}
/// Converts a mouse position on the stage to a local position on this display object.
/// If the object has zero scale, then the stage `TWIPS_TO_PIXELS` matrix will be used.
/// This matches Flash's behavior for `mouseX`/`mouseY` on an object with zero scale.
fn mouse_to_local(&self, global: Point<Twips>) -> Point<Twips> {
// MIKE: I suspect the `TWIPS_TO_PIXELS` scale should always be involved in the
// calculation somehow, not just in the non-invertible case.
self.global_to_local_matrix()
.unwrap_or(Matrix::TWIPS_TO_PIXELS)
* global
}
2019-12-04 01:52:00 +00:00
/// The `x` position in pixels of this display object in local space.
/// Returned by the `_x`/`x` ActionScript properties.
fn x(&self) -> Twips {
self.base().x()
}
2019-12-04 01:52:00 +00:00
/// Sets the `x` position in pixels of this display object in local space.
/// Set by the `_x`/`x` ActionScript properties.
/// This invalidates any ancestors cacheAsBitmap automatically.
fn set_x(&self, gc_context: &Mutation<'gc>, x: Twips) {
if self.base_mut(gc_context).set_x(x) {
if let Some(parent) = self.parent() {
// Self-transform changes are automatically handled,
// we only want to inform ancestors to avoid unnecessary invalidations for tx/ty
parent.invalidate_cached_bitmap(gc_context);
}
}
}
2019-12-04 01:52:00 +00:00
/// The `y` position in pixels of this display object in local space.
/// Returned by the `_y`/`y` ActionScript properties.
fn y(&self) -> Twips {
self.base().y()
}
2019-12-04 01:52:00 +00:00
/// Sets the `y` position in pixels of this display object in local space.
/// Set by the `_y`/`y` ActionScript properties.
/// This invalidates any ancestors cacheAsBitmap automatically.
fn set_y(&self, gc_context: &Mutation<'gc>, y: Twips) {
if self.base_mut(gc_context).set_y(y) {
if let Some(parent) = self.parent() {
// Self-transform changes are automatically handled,
// we only want to inform ancestors to avoid unnecessary invalidations for tx/ty
parent.invalidate_cached_bitmap(gc_context);
}
}
}
2019-12-04 01:52:00 +00:00
/// The rotation in degrees this display object in local space.
2019-12-04 01:52:00 +00:00
/// Returned by the `_rotation`/`rotation` ActionScript properties.
fn rotation(&self, gc_context: &Mutation<'gc>) -> Degrees {
2022-08-28 18:23:09 +00:00
let degrees = self.base_mut(gc_context).rotation();
self.set_scale_rotation_cached(gc_context);
degrees
}
2019-12-04 01:52:00 +00:00
/// Sets the rotation in degrees this display object in local space.
2019-12-04 01:52:00 +00:00
/// Set by the `_rotation`/`rotation` ActionScript properties.
/// This invalidates any ancestors cacheAsBitmap automatically.
fn set_rotation(&self, gc_context: &Mutation<'gc>, radians: Degrees) {
if self.base_mut(gc_context).set_rotation(radians) {
self.set_scale_rotation_cached(gc_context);
if let Some(parent) = self.parent() {
// Self-transform changes are automatically handled,
// we only want to inform ancestors to avoid unnecessary invalidations for tx/ty
parent.invalidate_cached_bitmap(gc_context);
}
}
}
2019-12-04 01:52:00 +00:00
/// The X axis scale for this display object in local space.
/// Returned by the `_xscale`/`scaleX` ActionScript properties.
fn scale_x(&self, gc_context: &Mutation<'gc>) -> Percent {
2022-08-28 18:23:09 +00:00
let percent = self.base_mut(gc_context).scale_x();
self.set_scale_rotation_cached(gc_context);
percent
}
2019-12-04 01:52:00 +00:00
/// Sets the X axis scale for this display object in local space.
2019-12-04 01:52:00 +00:00
/// Set by the `_xscale`/`scaleX` ActionScript properties.
/// This invalidates any ancestors cacheAsBitmap automatically.
fn set_scale_x(&self, gc_context: &Mutation<'gc>, value: Percent) {
if self.base_mut(gc_context).set_scale_x(value) {
self.set_scale_rotation_cached(gc_context);
if let Some(parent) = self.parent() {
// Self-transform changes are automatically handled,
// we only want to inform ancestors to avoid unnecessary invalidations for tx/ty
parent.invalidate_cached_bitmap(gc_context);
}
}
}
2019-12-04 01:52:00 +00:00
/// The Y axis scale for this display object in local space.
/// Returned by the `_yscale`/`scaleY` ActionScript properties.
fn scale_y(&self, gc_context: &Mutation<'gc>) -> Percent {
2022-08-28 18:23:09 +00:00
let percent = self.base_mut(gc_context).scale_y();
self.set_scale_rotation_cached(gc_context);
percent
}
2019-12-04 01:52:00 +00:00
/// Sets the Y axis scale for this display object in local space.
/// Returned by the `_yscale`/`scaleY` ActionScript properties.
/// This invalidates any ancestors cacheAsBitmap automatically.
fn set_scale_y(&self, gc_context: &Mutation<'gc>, value: Percent) {
if self.base_mut(gc_context).set_scale_y(value) {
self.set_scale_rotation_cached(gc_context);
if let Some(parent) = self.parent() {
// Self-transform changes are automatically handled,
// we only want to inform ancestors to avoid unnecessary invalidations for tx/ty
parent.invalidate_cached_bitmap(gc_context);
}
}
}
2019-12-04 01:52:00 +00:00
/// Gets the pixel width of the AABB containing this display object in local space.
2019-12-04 01:52:00 +00:00
/// Returned by the ActionScript `_width`/`width` properties.
fn width(&self) -> f64 {
self.local_bounds().width().to_pixels()
2019-12-04 01:52:00 +00:00
}
/// Sets the pixel width of this display object in local space.
/// The width is based on the AABB of the object.
/// Set by the ActionScript `_width`/`width` properties.
/// This does odd things on rotated clips to match the behavior of Flash.
fn set_width(&self, context: &mut UpdateContext<'_, 'gc>, value: f64) {
let gc_context = context.gc_context;
let object_bounds = self.bounds();
let object_width = object_bounds.width().to_pixels();
let object_height = object_bounds.height().to_pixels();
2019-12-04 01:52:00 +00:00
let aspect_ratio = object_height / object_width;
let (target_scale_x, target_scale_y) = if object_width != 0.0 {
(value / object_width, value / object_height)
} else {
(0.0, 0.0)
};
// No idea about the derivation of this -- figured it out via lots of trial and error.
// 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).unit();
let prev_scale_y = self.scale_y(gc_context).unit();
2019-12-04 01:52:00 +00:00
let rotation = self.rotation(gc_context);
let cos = f64::abs(f64::cos(rotation.into_radians()));
let sin = f64::abs(f64::sin(rotation.into_radians()));
let mut new_scale_x = aspect_ratio * (cos * target_scale_x + sin * target_scale_y)
2019-12-04 01:52:00 +00:00
/ ((cos + aspect_ratio * sin) * (aspect_ratio * cos + sin));
let mut new_scale_y =
2019-12-04 01:52:00 +00:00
(sin * prev_scale_x + aspect_ratio * cos * prev_scale_y) / (aspect_ratio * cos + sin);
if !new_scale_x.is_finite() {
new_scale_x = 0.0;
}
if !new_scale_y.is_finite() {
new_scale_y = 0.0;
}
self.set_scale_x(gc_context, Percent::from_unit(new_scale_x));
self.set_scale_y(gc_context, Percent::from_unit(new_scale_y));
2019-12-04 01:52:00 +00:00
}
2019-12-04 01:52:00 +00:00
/// Gets the pixel height of the AABB containing this display object in local space.
/// Returned by the ActionScript `_height`/`height` properties.
fn height(&self) -> f64 {
self.local_bounds().height().to_pixels()
2019-12-04 01:52:00 +00:00
}
2019-12-04 01:52:00 +00:00
/// Sets the pixel height of this display object in local space.
/// Set by the ActionScript `_height`/`height` properties.
/// This does odd things on rotated clips to match the behavior of Flash.
fn set_height(&self, context: &mut UpdateContext<'_, 'gc>, value: f64) {
let gc_context = context.gc_context;
let object_bounds = self.bounds();
let object_width = object_bounds.width().to_pixels();
let object_height = object_bounds.height().to_pixels();
2019-12-04 01:52:00 +00:00
let aspect_ratio = object_width / object_height;
let (target_scale_x, target_scale_y) = if object_height != 0.0 {
(value / object_width, value / object_height)
} else {
(0.0, 0.0)
};
// No idea about the derivation of this -- figured it out via lots of trial and error.
// 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).unit();
let prev_scale_y = self.scale_y(gc_context).unit();
2019-12-04 01:52:00 +00:00
let rotation = self.rotation(gc_context);
let cos = f64::abs(f64::cos(rotation.into_radians()));
let sin = f64::abs(f64::sin(rotation.into_radians()));
let mut new_scale_x =
2019-12-04 01:52:00 +00:00
(aspect_ratio * cos * prev_scale_x + sin * prev_scale_y) / (aspect_ratio * cos + sin);
let mut new_scale_y = aspect_ratio * (sin * target_scale_x + cos * target_scale_y)
2019-12-04 01:52:00 +00:00
/ ((cos + aspect_ratio * sin) * (aspect_ratio * cos + sin));
if !new_scale_x.is_finite() {
new_scale_x = 0.0;
}
if !new_scale_y.is_finite() {
new_scale_y = 0.0;
}
self.set_scale_x(gc_context, Percent::from_unit(new_scale_x));
self.set_scale_y(gc_context, Percent::from_unit(new_scale_y));
2019-12-04 01:52:00 +00:00
}
2019-12-04 01:52:00 +00:00
/// The opacity of this display object.
/// 1 is fully opaque.
/// Returned by the `_alpha`/`alpha` ActionScript properties.
fn alpha(&self) -> f64 {
self.base().alpha()
}
2019-12-04 01:52:00 +00:00
/// Sets the opacity of this display object.
/// 1 is fully opaque.
/// Set by the `_alpha`/`alpha` ActionScript properties.
/// This invalidates any cacheAsBitmap automatically.
fn set_alpha(&self, gc_context: &Mutation<'gc>, value: f64) {
if self.base_mut(gc_context).set_alpha(value) {
if let Some(parent) = self.parent() {
// Self-transform changes are automatically handled
parent.invalidate_cached_bitmap(gc_context);
}
}
}
fn name(&self) -> AvmString<'gc> {
self.base().name().unwrap_or_default()
}
fn set_name(&self, gc_context: &Mutation<'gc>, name: AvmString<'gc>) {
self.base_mut(gc_context).set_name(name)
}
fn filters(&self) -> Vec<Filter> {
self.base().filters()
}
fn set_filters(&self, gc_context: &Mutation<'gc>, filters: Vec<Filter>) {
if self.base_mut(gc_context).set_filters(filters) {
self.invalidate_cached_bitmap(gc_context);
}
}
2019-12-03 08:19:35 +00:00
/// Returns the dot-syntax path to this display object, e.g. `_level0.foo.clip`
fn path(&self) -> WString {
if let Some(parent) = self.avm1_parent() {
2019-12-03 08:19:35 +00:00
let mut path = parent.path();
path.push_byte(b'.');
path.push_str(&self.name());
2019-12-03 08:19:35 +00:00
path
} else {
WString::from_utf8_owned(format!("_level{}", self.depth()))
2019-12-03 08:19:35 +00:00
}
}
/// Returns the Flash 4 slash-syntax path to this display object, e.g. `/foo/clip`.
/// Returned by the `_target` property in AVM1.
fn slash_path(&self) -> WString {
fn build_slash_path(object: DisplayObject<'_>) -> WString {
if let Some(parent) = object.avm1_parent() {
let mut path = build_slash_path(parent);
path.push_byte(b'/');
path.push_str(&object.name());
path
} else {
let level = object.depth();
if level == 0 {
// _level0 does not append its name in slash syntax.
WString::new()
} else {
// Other levels do append their name.
2022-10-26 23:46:09 +00:00
WString::from_utf8_owned(format!("_level{level}"))
}
}
}
if self.avm1_parent().is_some() {
build_slash_path((*self).into())
} else {
// _target of _level0 should just be '/'.
WString::from_unit(b'/'.into())
}
}
fn clip_depth(&self) -> Depth {
self.base().clip_depth()
}
fn set_clip_depth(&self, gc_context: &Mutation<'gc>, depth: Depth) {
self.base_mut(gc_context).set_clip_depth(depth);
}
/// Retrieve the parent of this display object.
///
/// This version of the function merely exposes the display object parent,
/// without any further filtering.
fn parent(&self) -> Option<DisplayObject<'gc>> {
self.base().parent()
}
/// Set the parent of this display object.
fn set_parent(&self, context: &mut UpdateContext<'_, 'gc>, parent: Option<DisplayObject<'gc>>) {
self.base_mut(context.gc_context)
.set_parent_ignoring_orphan_list(parent);
}
/// Retrieve the parent of this display object.
///
/// This version of the function implements the concept of parenthood as
/// seen in AVM1. Notably, it disallows access to the `Stage` and to
/// non-AVM1 DisplayObjects; for an unfiltered concept of parent,
/// use the `parent` method.
fn avm1_parent(&self) -> Option<DisplayObject<'gc>> {
self.parent()
.filter(|p| p.as_stage().is_none())
.filter(|p| !p.movie().is_action_script_3())
}
/// Retrieve the parent of this display object.
///
/// This version of the function implements the concept of parenthood as
/// seen in AVM2. Notably, it disallows access to non-container parents.
fn avm2_parent(&self) -> Option<DisplayObject<'gc>> {
self.parent().filter(|p| p.as_container().is_some())
}
fn next_avm1_clip(&self) -> Option<DisplayObject<'gc>> {
self.base().next_avm1_clip()
}
2023-08-15 18:35:57 +00:00
fn set_next_avm1_clip(&self, gc_context: &Mutation<'gc>, node: Option<DisplayObject<'gc>>) {
self.base_mut(gc_context).set_next_avm1_clip(node);
}
fn masker(&self) -> Option<DisplayObject<'gc>> {
self.base().masker()
}
fn set_masker(
&self,
gc_context: &Mutation<'gc>,
node: Option<DisplayObject<'gc>>,
remove_old_link: bool,
) {
if remove_old_link {
if let Some(old_masker) = self.base().masker() {
old_masker.set_maskee(gc_context, None, false);
}
if let Some(parent) = self.parent() {
// Masks are natively handled by cacheAsBitmap - don't invalidate self, only parents
parent.invalidate_cached_bitmap(gc_context);
}
}
self.base_mut(gc_context).set_masker(node);
}
fn maskee(&self) -> Option<DisplayObject<'gc>> {
self.base().maskee()
}
fn set_maskee(
&self,
gc_context: &Mutation<'gc>,
node: Option<DisplayObject<'gc>>,
remove_old_link: bool,
) {
if remove_old_link {
if let Some(old_maskee) = self.base().maskee() {
old_maskee.set_masker(gc_context, None, false);
}
self.invalidate_cached_bitmap(gc_context);
}
self.base_mut(gc_context).set_maskee(node);
}
fn scroll_rect(&self) -> Option<Rectangle<Twips>> {
self.base().scroll_rect.clone()
}
fn next_scroll_rect(&self) -> Rectangle<Twips> {
self.base().next_scroll_rect.clone()
}
2023-08-15 18:35:57 +00:00
fn set_next_scroll_rect(&self, gc_context: &Mutation<'gc>, rectangle: Rectangle<Twips>) {
self.base_mut(gc_context).next_scroll_rect = rectangle;
// Scroll rect is natively handled by cacheAsBitmap - don't invalidate self, only parents
if let Some(parent) = self.parent() {
parent.invalidate_cached_bitmap(gc_context);
}
}
fn scaling_grid(&self) -> Rectangle<Twips> {
self.base().scaling_grid.clone()
}
fn set_scaling_grid(&self, gc_context: &Mutation<'gc>, rect: Rectangle<Twips>) {
self.base_mut(gc_context).scaling_grid = rect;
}
/// Whether this object has been removed. Only applies to AVM1.
fn avm1_removed(&self) -> bool {
self.base().avm1_removed()
}
// Sets whether this object has been removed. Only applies to AVM1
fn set_avm1_removed(&self, gc_context: &Mutation<'gc>, value: bool) {
self.base_mut(gc_context).set_avm1_removed(value)
}
/// Is this object waiting to be removed on the start of the next frame
fn avm1_pending_removal(&self) -> bool {
self.base().avm1_pending_removal()
}
fn set_avm1_pending_removal(&self, gc_context: &Mutation<'gc>, value: bool) {
self.base_mut(gc_context).set_avm1_pending_removal(value)
}
/// Whether this display object is visible.
/// Invisible objects are not rendered, but otherwise continue to exist normally.
/// Returned by the `_visible`/`visible` ActionScript properties.
fn visible(&self) -> bool {
self.base().visible()
}
/// Sets whether this display object will be visible.
/// Invisible objects are not rendered, but otherwise continue to exist normally.
/// Returned by the `_visible`/`visible` ActionScript properties.
fn set_visible(&self, gc_context: &Mutation<'gc>, value: bool) {
if self.base_mut(gc_context).set_visible(value) {
if let Some(parent) = self.parent() {
// We don't need to invalidate ourselves, we're just toggling if the bitmap is rendered.
parent.invalidate_cached_bitmap(gc_context);
}
}
}
2022-05-01 23:33:04 +00:00
/// The blend mode used when rendering this display object.
/// Values other than the default `BlendMode::Normal` implicitly cause cache-as-bitmap behavior.
fn blend_mode(&self) -> ExtendedBlendMode {
2022-05-01 23:33:04 +00:00
self.base().blend_mode()
}
/// Sets the blend mode used when rendering this display object.
/// Values other than the default `BlendMode::Normal` implicitly cause cache-as-bitmap behavior.
fn set_blend_mode(&self, gc_context: &Mutation<'gc>, value: ExtendedBlendMode) {
if self.base_mut(gc_context).set_blend_mode(value) {
if let Some(parent) = self.parent() {
// We don't need to invalidate ourselves, we're just toggling how the bitmap is rendered.
// Note that Flash does not always invalidate on changing the blend mode;
// but that's a bug we don't need to copy :)
parent.invalidate_cached_bitmap(gc_context);
}
}
2022-05-01 23:33:04 +00:00
}
fn blend_shader(&self) -> Option<PixelBenderShaderHandle> {
self.base().blend_shader()
}
2023-08-15 18:35:57 +00:00
fn set_blend_shader(&self, gc_context: &Mutation<'gc>, value: Option<PixelBenderShaderHandle>) {
self.base_mut(gc_context).set_blend_shader(value);
self.set_blend_mode(gc_context, ExtendedBlendMode::Shader);
}
/// The opaque background color of this display object.
fn opaque_background(&self) -> Option<Color> {
self.base().opaque_background()
}
/// Sets the opaque background color of this display object.
/// The bounding box of the display object will be filled with the given color. This also
/// triggers cache-as-bitmap behavior. Only solid backgrounds are supported; the alpha channel
/// is ignored.
fn set_opaque_background(&self, gc_context: &Mutation<'gc>, value: Option<Color>) {
if self.base_mut(gc_context).set_opaque_background(value) {
self.invalidate_cached_bitmap(gc_context);
}
}
/// Whether this display object represents the root of loaded content.
fn is_root(&self) -> bool {
self.base().is_root()
}
/// Sets whether this display object represents the root of loaded content.
fn set_is_root(&self, gc_context: &Mutation<'gc>, value: bool) {
self.base_mut(gc_context).set_is_root(value);
}
/// The sound transform for sounds played inside this display object.
fn set_sound_transform(
&self,
context: &mut UpdateContext<'_, 'gc>,
sound_transform: SoundTransform,
) {
self.base_mut(context.gc_context)
.set_sound_transform(sound_transform);
context.set_sound_transforms_dirty();
}
/// Whether this display object is used as the _root of itself and its children.
/// Returned by the `_lockroot` ActionScript property.
fn lock_root(&self) -> bool {
self.base().lock_root()
}
/// Sets whether this display object is used as the _root of itself and its children.
/// Returned by the `_lockroot` ActionScript property.
fn set_lock_root(&self, gc_context: &Mutation<'gc>, value: bool) {
self.base_mut(gc_context).set_lock_root(value);
}
/// Whether this display object has been transformed by ActionScript.
/// When this flag is set, changes from SWF `PlaceObject` tags are ignored.
fn transformed_by_script(&self) -> bool {
self.base().transformed_by_script()
}
/// Sets whether this display object has been transformed by ActionScript.
/// When this flag is set, changes from SWF `PlaceObject` tags are ignored.
fn set_transformed_by_script(&self, gc_context: &Mutation<'gc>, value: bool) {
self.base_mut(gc_context).set_transformed_by_script(value)
}
/// Whether this display object prefers to be cached into a bitmap rendering.
/// This is the PlaceObject `cacheAsBitmap` flag - and may be overriden if filters are applied.
/// Consider `is_bitmap_cached` for if a bitmap cache is actually in use.
fn is_bitmap_cached_preference(&self) -> bool {
self.base().is_bitmap_cached_preference()
}
/// Whether this display object is using a bitmap cache, whether by preference or necessity.
fn is_bitmap_cached(&self) -> bool {
self.base().cache.is_some()
}
/// Explicitly sets the preference of this display object to be cached into a bitmap rendering.
/// Note that the object will still be bitmap cached if a filter is active.
fn set_bitmap_cached_preference(&self, gc_context: &Mutation<'gc>, value: bool) {
self.base_mut(gc_context)
.set_bitmap_cached_preference(value)
}
/// Whether this display object has a scroll rectangle applied.
fn has_scroll_rect(&self) -> bool {
self.base().has_scroll_rect()
}
/// Sets whether this display object has a scroll rectangle applied.
fn set_has_scroll_rect(&self, gc_context: &Mutation<'gc>, value: bool) {
self.base_mut(gc_context).set_has_scroll_rect(value)
}
/// Called whenever the focus tracker has deemed this display object worthy, or no longer worthy,
/// of being the currently focused object.
/// This should only be called by the focus manager. To change a focus, go through that.
fn on_focus_changed(&self, _gc_context: &Mutation<'gc>, _focused: bool) {}
/// Whether or not this clip may be focusable for keyboard input.
2023-04-07 14:19:53 +00:00
fn is_focusable(&self, _context: &mut UpdateContext<'_, 'gc>) -> bool {
false
}
/// Whether this display object has been created by ActionScript 3.
/// When this flag is set, changes from SWF `RemoveObject` tags are
/// ignored.
fn placed_by_script(&self) -> bool {
self.base().placed_by_script()
}
/// Sets whether this display object has been created by ActionScript 3.
/// When this flag is set, changes from SWF `RemoveObject` tags are
/// ignored.
fn set_placed_by_script(&self, gc_context: &Mutation<'gc>, value: bool) {
self.base_mut(gc_context).set_placed_by_script(value)
}
/// Whether this display object has been instantiated by the timeline.
/// When this flag is set, attempts to change the object's name from AVM2
/// throw an exception.
fn instantiated_by_timeline(&self) -> bool {
self.base().instantiated_by_timeline()
}
/// Sets whether this display object has been instantiated by the timeline.
/// When this flag is set, attempts to change the object's name from AVM2
/// throw an exception.
fn set_instantiated_by_timeline(&self, gc_context: &Mutation<'gc>, value: bool) {
self.base_mut(gc_context)
.set_instantiated_by_timeline(value);
}
/// Whether this display object was placed by a SWF tag with an explicit
/// name.
///
/// When this flag is set, the object will attempt to set a dynamic property
/// on the parent with the same name as itself.
fn has_explicit_name(&self) -> bool {
self.base().has_explicit_name()
}
/// Sets whether this display object was placed by a SWF tag with an
/// explicit name.
///
/// When this flag is set, the object will attempt to set a dynamic property
/// on the parent with the same name as itself.
fn set_has_explicit_name(&self, gc_context: &Mutation<'gc>, value: bool) {
self.base_mut(gc_context).set_has_explicit_name(value);
}
fn state(&self) -> Option<ButtonState> {
None
}
2023-06-21 09:05:35 +00:00
fn set_state(self, _context: &mut UpdateContext<'_, 'gc>, _state: ButtonState) {}
/// Run any start-of-frame actions for this display object.
///
/// When fired on `Stage`, this also emits the AVM2 `enterFrame` broadcast.
fn enter_frame(&self, _context: &mut UpdateContext<'_, 'gc>) {}
/// Construct all display objects that the timeline indicates should exist
/// this frame, and their children.
///
/// This function should ensure the following, from the point of view of
/// downstream VMs:
///
/// 1. That the object itself has been allocated, if not constructed
/// 2. That newly created children have been instantiated and are present
/// as properties on the class
fn construct_frame(&self, _context: &mut UpdateContext<'_, 'gc>) {}
/// To be called when an AVM2 display object has finished being constructed.
///
/// This function must be called once and ONLY once, after the object's
/// AVM2 side has been constructed. Typically, this is in construct_frame,
/// unless your object needs to construct itself earlier or later. When
/// this function is called on the child, it will fire its add events and,
/// if possible, set a named property on the parent matching the name of
/// the object.
///
/// This still needs to be called for objects placed by AVM2, since we
/// need to stop the underlying MovieClip if the constructed class
/// does not extend MovieClip.
///
/// Since we construct AVM2 display objects after they are allocated and
/// placed on the render list, these steps have to be done by the child
/// object to signal to its parent that it was added.
#[inline(never)]
fn on_construction_complete(&self, context: &mut UpdateContext<'_, 'gc>) {
let placed_by_script = self.placed_by_script();
self.fire_added_events(context);
// Check `self.placed_by_script()` before we fire events, since those
// events might `placed_by_script`
if !placed_by_script {
self.set_on_parent_field(context);
}
if let Some(movie) = self.as_movie_clip() {
let obj = movie
.object2()
.as_object()
.expect("MovieClip object should have been constructed");
let movieclip_class = context.avm2.classes().movieclip.inner_class_definition();
// It's possible to have a DefineSprite tag with multiple frames, but have
// the corresponding `SymbolClass` *not* extend `MovieClip` (e.g. extending `Sprite` directly.)
// When this occurs, Flash Player will run the first frame, and immediately stop.
// However, Flash Player runs frames for the root movie clip, even if it doesn't extend `MovieClip`.
if !obj.is_of_type(movieclip_class, context) && !movie.is_root() {
movie.stop(context);
}
movie.set_initialized(context.gc_context);
}
}
fn fire_added_events(&self, context: &mut UpdateContext<'_, 'gc>) {
if !self.placed_by_script() {
// Since we construct AVM2 display objects after they are
// allocated and placed on the render list, we have to emit all
// events after this point.
//
// Children added to buttons by the timeline do not emit events.
if self.parent().and_then(|p| p.as_avm2_button()).is_none() {
dispatch_added_event_only((*self).into(), context);
if self.avm2_stage(context).is_some() {
dispatch_added_to_stage_event_only((*self).into(), context);
}
}
}
}
fn set_on_parent_field(&self, context: &mut UpdateContext<'_, 'gc>) {
//TODO: Don't report missing property errors.
//TODO: Don't attempt to set properties if object was placed without a name.
if self.has_explicit_name() {
if let Some(Avm2Value::Object(p)) = self.parent().map(|p| p.object2()) {
if let Avm2Value::Object(c) = self.object2() {
let domain = context
.library
.library_for_movie(self.movie())
.unwrap()
.avm2_domain();
let mut activation = Avm2Activation::from_domain(context.reborrow(), domain);
let name =
Avm2Multiname::new(activation.avm2().find_public_namespace(), self.name());
if let Err(e) = p.init_property(&name, c.into(), &mut activation) {
tracing::error!(
"Got error when setting AVM2 child named \"{}\": {}",
&self.name(),
e
avm2: Implement namespace versioning This allows us to hide things like `MovieClip.isPlaying` from older SWFs, which may define an `isPlaying` method without marking it as an override. This has the largest impact on public namespaces. There is no longer just one public namespace - we now have a public namespace per API version. Most callsites should use `Avm2.find_public_namespace()`, which determines the public namespace based on the root SWF API version. This ensures that explicit property access in Ruffle (e.g. calling "toString") are able to see things defined in the user SWF. This requires several changes other: * A new `ApiVersion` enum is introduced, storing all of the api versions defined in avmplus * NamespaceData::Namespace now holds an `ApiVersion`. When we read a namespace from a user-defined ABC file, we use the `ApiVersion` from the root movie clip. This matches Flash Player's behavior - when a child SWF is loaded through `Loader`, its API version gets overwritten with the parent's API version, which affects definition visibility and 'override' requirements in the child SWF. Note that 'behavior changes' in methods like `gotoAndPlay` are unaffected. * The `PartialEq` impl for `Namespace` has been removed - each call site must now choose to compare namespaces either by an exact version match, or by 'compatible' versions (a `<=` check) with `Namespace::matches_ns` * `PropertyMap` now uses `Namespace::matches_ns` to check for a compatible version when looking up a definition. * When compiling our playerglobal, we pass in the `-apiversioning` flag, which causes asc.jar to convert `[API]` metadata into a special Unicode 'version mark' suffix in namespace URLs. We parse this when loading namespaces to determine the API version to use for playerglobal definitions (unmarked definitions use the lowest possible version). Unfortunately, this makes ffdec unable to decompile our playerglobal.swc I've going to file an upstream issue
2023-07-31 21:39:42 +00:00
);
}
}
}
}
}
/// Execute all other timeline actions on this object.
fn run_frame_avm1(&self, _context: &mut UpdateContext<'_, 'gc>) {}
2021-06-21 16:29:52 +00:00
/// Emit a `frameConstructed` event on this DisplayObject and any children it
/// may have.
fn frame_constructed(&self, context: &mut UpdateContext<'_, 'gc>) {
let frame_constructed_evt =
Avm2EventObject::bare_default_event(context, "frameConstructed");
let dobject_constr = context.avm2.classes().display_object;
Avm2::broadcast_event(context, frame_constructed_evt, dobject_constr);
}
/// Run any frame scripts (if they exist and this object needs to run them).
fn run_frame_scripts(self, context: &mut UpdateContext<'_, 'gc>) {
if let Some(container) = self.as_container() {
for child in container.iter_render_list() {
child.run_frame_scripts(context);
}
}
}
/// Emit an `exitFrame` broadcast event.
fn exit_frame(&self, context: &mut UpdateContext<'_, 'gc>) {
let exit_frame_evt = Avm2EventObject::bare_default_event(context, "exitFrame");
let dobject_constr = context.avm2.classes().display_object;
Avm2::broadcast_event(context, exit_frame_evt, dobject_constr);
self.on_exit_frame(context);
}
fn on_exit_frame(&self, context: &mut UpdateContext<'_, 'gc>) {
if let Some(container) = self.as_container() {
for child in container.iter_render_list() {
child.on_exit_frame(context);
}
}
}
/// Called before the child is about to be rendered.
/// Note that this happens even if the child is invisible
/// (as long as the child is still on a render list)
fn pre_render(&self, context: &mut RenderContext<'_, 'gc>) {
let mut this = self.base_mut(context.gc_context);
this.clear_invalidate_flag();
this.scroll_rect = this
.has_scroll_rect()
.then(|| this.next_scroll_rect.clone());
}
fn render_self(&self, _context: &mut RenderContext<'_, 'gc>) {}
fn render(&self, context: &mut RenderContext<'_, 'gc>) {
render_base((*self).into(), context)
}
#[cfg(not(feature = "avm_debug"))]
fn display_render_tree(&self, _depth: usize) {}
#[cfg(feature = "avm_debug")]
fn display_render_tree(&self, depth: usize) {
2023-01-24 01:41:00 +00:00
let mut self_str = &*format!("{self:?}");
if let Some(end_char) = self_str.find(|c: char| !c.is_ascii_alphanumeric()) {
self_str = &self_str[..end_char];
}
let bounds = self.world_bounds();
let mut classname = "".to_string();
if let Some(o) = self.object2().as_object() {
classname = format!("{:?}", o.base().debug_class_name());
}
println!(
"{} rel({},{}) abs({},{}) {} {} {} id={} depth={}",
" ".repeat(depth),
self.x(),
self.y(),
bounds.x_min.to_pixels(),
bounds.y_min.to_pixels(),
classname,
self.name(),
self_str,
self.id(),
depth
);
if let Some(ctr) = self.as_container() {
ctr.recurse_render_tree(depth + 1);
}
}
fn avm1_unload(&self, context: &mut UpdateContext<'_, 'gc>) {
// Unload children.
if let Some(ctr) = self.as_container() {
for child in ctr.iter_render_list() {
child.avm1_unload(context);
}
}
if let Some(node) = self.maskee() {
node.set_masker(context.gc_context, None, true);
} else if let Some(node) = self.masker() {
node.set_maskee(context.gc_context, None, true);
}
// Unregister any text field variable bindings, and replace them on the unbound list.
if let Avm1Value::Object(object) = self.object() {
if let Some(stage_object) = object.as_stage_object() {
stage_object.unregister_text_field_bindings(context);
}
}
self.set_avm1_removed(context.gc_context, true);
}
fn as_stage(&self) -> Option<Stage<'gc>> {
None
}
2021-04-23 01:10:29 +00:00
fn as_avm1_button(&self) -> Option<Avm1Button<'gc>> {
2019-08-19 14:53:12 +00:00
None
}
2021-04-24 01:16:27 +00:00
fn as_avm2_button(&self) -> Option<Avm2Button<'gc>> {
None
}
fn as_movie_clip(&self) -> Option<MovieClip<'gc>> {
2019-08-19 14:53:12 +00:00
None
}
fn as_edit_text(&self) -> Option<EditText<'gc>> {
None
}
fn as_morph_shape(&self) -> Option<MorphShape<'gc>> {
None
}
fn as_container(self) -> Option<DisplayObjectContainer<'gc>> {
None
}
fn as_video(self) -> Option<Video<'gc>> {
None
}
fn as_drawing(&self, _gc_context: &Mutation<'gc>) -> Option<RefMut<'_, Drawing>> {
None
}
fn as_bitmap(self) -> Option<Bitmap<'gc>> {
None
}
fn as_interactive(self) -> Option<InteractiveObject<'gc>> {
None
}
fn apply_place_object(
&self,
context: &mut UpdateContext<'_, 'gc>,
place_object: &swf::PlaceObject,
) {
// PlaceObject tags only apply if this object has not been dynamically moved by AS code.
if !self.transformed_by_script() {
if let Some(matrix) = place_object.matrix {
self.set_matrix(context.gc_context, matrix.into());
if let Some(parent) = self.parent() {
// Self-transform changes are automatically handled,
// we only want to inform ancestors to avoid unnecessary invalidations for tx/ty
parent.invalidate_cached_bitmap(context.gc_context);
}
}
if let Some(color_transform) = &place_object.color_transform {
self.set_color_transform(context.gc_context, *color_transform);
if let Some(parent) = self.parent() {
parent.invalidate_cached_bitmap(context.gc_context);
}
}
if let Some(ratio) = place_object.ratio {
if let Some(mut morph_shape) = self.as_morph_shape() {
morph_shape.set_ratio(context.gc_context, ratio);
} else if let Some(video) = self.as_video() {
video.seek(context, ratio.into());
}
}
if let Some(is_bitmap_cached) = place_object.is_bitmap_cached {
self.set_bitmap_cached_preference(context.gc_context, is_bitmap_cached);
}
if let Some(blend_mode) = place_object.blend_mode {
self.set_blend_mode(context.gc_context, blend_mode.into());
}
if self.swf_version() >= 11 {
if let Some(visible) = place_object.is_visible {
self.set_visible(context.gc_context, visible);
}
2023-06-29 17:46:30 +00:00
if let Some(mut color) = place_object.background_color {
let color = if color.a > 0 {
// Force opaque background to have no transpranecy.
color.a = 255;
Some(color)
} else {
None
};
self.set_opaque_background(context.gc_context, color);
}
}
if let Some(filters) = &place_object.filters {
self.set_filters(
context.gc_context,
filters.iter().map(Filter::from).collect(),
);
}
2022-05-02 04:15:09 +00:00
// Purposely omitted properties:
// name, clip_depth, clip_actions
// These properties are only set on initial placement in `MovieClip::instantiate_child`
// and can not be modified by subsequent PlaceObject tags.
}
}
/// Called when this object should be replaced by a PlaceObject tag.
fn replace_with(&self, _context: &mut UpdateContext<'_, 'gc>, _id: CharacterId) {
2021-06-24 10:56:20 +00:00
// Noop for most symbols; only shapes can replace their innards with another Graphic.
}
fn object(&self) -> Avm1Value<'gc> {
2021-06-21 16:29:52 +00:00
Avm1Value::Undefined // TODO: Implement for every type and delete this fallback.
}
fn object2(&self) -> Avm2Value<'gc> {
Avm2Value::Undefined // TODO: See above. Also, unconstructed objects should return null.
}
fn set_object2(&self, _context: &mut UpdateContext<'_, 'gc>, _to: Avm2Object<'gc>) {}
/// Tests if a given stage position point intersects with the world bounds of this object.
fn hit_test_bounds(&self, point: Point<Twips>) -> bool {
self.world_bounds().contains(point)
2020-08-21 06:25:31 +00:00
}
2020-12-04 23:34:04 +00:00
/// Tests if a given object's world bounds intersects with the world bounds
/// of this object.
2021-03-06 21:41:56 +00:00
fn hit_test_object(&self, other: DisplayObject<'gc>) -> bool {
self.world_bounds().intersects(&other.world_bounds())
2020-12-04 23:34:04 +00:00
}
2020-08-21 06:25:31 +00:00
/// Tests if a given stage position point intersects within this object, considering the art.
fn hit_test_shape(
&self,
_context: &mut UpdateContext<'_, 'gc>,
point: Point<Twips>,
options: HitTestOptions,
) -> bool {
// Default to using bounding box.
(!options.contains(HitTestOptions::SKIP_INVISIBLE) || self.visible())
&& self.hit_test_bounds(point)
2019-08-14 19:39:26 +00:00
}
fn post_instantiation(
&self,
context: &mut UpdateContext<'_, 'gc>,
_init_object: Option<Avm1Object<'gc>>,
_instantiated_by: Instantiator,
run_frame: bool,
) {
if run_frame && !self.movie().is_action_script_3() {
self.run_frame_avm1(context);
}
}
/// Return the version of the SWF that created this movie clip.
fn swf_version(&self) -> u8 {
self.movie().version()
}
2019-05-09 01:10:43 +00:00
/// Return the SWF that defines this display object.
fn movie(&self) -> Arc<SwfMovie>;
fn loader_info(&self) -> Option<Avm2Object<'gc>> {
None
}
fn instantiate(&self, gc_context: &Mutation<'gc>) -> DisplayObject<'gc>;
fn as_ptr(&self) -> *const DisplayObjectPtr;
/// Whether this object can be used as a mask.
/// If this returns false and this object is used as a mask, the mask will not be applied.
/// This is used by movie clips to disable the mask when there are no children, for example.
fn allow_as_mask(&self) -> bool {
true
}
/// Obtain the top-most non-Stage parent of the display tree hierarchy.
///
/// This function implements the AVM1 concept of root clips. For the AVM2
/// version, see `avm2_root`.
fn avm1_root(&self) -> DisplayObject<'gc> {
let mut root = (*self).into();
loop {
if root.lock_root() {
break;
}
if let Some(parent) = root.avm1_parent() {
if !parent.movie().is_action_script_3() {
root = parent;
} else {
// We've traversed upwards into a loader AVM2 movie, so break.
break;
}
} else {
break;
}
}
root
}
/// Obtain the top-most non-Stage parent of the display tree hierarchy, if
/// a suitable object exists.
///
/// This function implements the AVM2 concept of root clips. For the AVM1
/// version, see `avm1_root`.
fn avm2_root(&self) -> Option<DisplayObject<'gc>> {
let mut parent = Some((*self).into());
while let Some(p) = parent {
if p.is_root() {
return parent;
}
if let Some(p_parent) = p.parent() {
if !p_parent.movie().is_action_script_3() {
// We've traversed upwards into a loader AVM1 movie, so return the current parent.
return parent;
}
}
parent = p.parent();
}
None
}
/// Obtain the root of the display tree hierarchy, if a suitable object
/// exists.
///
/// This implements the AVM2 concept of `stage`. Notably, it deliberately
/// will fail to locate the current player's stage for objects that are not
/// rooted to the DisplayObject hierarchy correctly. If you just want to
/// access the current player's stage, grab it from the context.
fn avm2_stage(&self, _context: &UpdateContext<'_, 'gc>) -> Option<DisplayObject<'gc>> {
let mut parent = Some((*self).into());
while let Some(p) = parent {
if p.as_stage().is_some() {
return parent;
}
parent = p.parent();
}
None
}
/// Determine if this display object is currently on the stage.
fn is_on_stage(self, context: &UpdateContext<'_, 'gc>) -> bool {
let mut ancestor = self.avm2_parent();
while let Some(parent) = ancestor {
if parent.avm2_parent().is_some() {
ancestor = parent.avm2_parent();
} else {
break;
}
}
let ancestor = ancestor.unwrap_or_else(|| self.into());
DisplayObject::ptr_eq(ancestor, context.stage.into())
}
/// Assigns a default instance name `instanceN` to this object.
fn set_default_instance_name(&self, context: &mut UpdateContext<'_, 'gc>) {
if self.base().name().is_none() {
let name = format!("instance{}", *context.instance_counter);
self.set_name(
context.gc_context,
AvmString::new_utf8(context.gc_context, name),
);
*context.instance_counter = context.instance_counter.wrapping_add(1);
}
}
/// Assigns a default root name to this object.
///
/// The default root names change based on the AVM configuration of the
/// clip; AVM2 clips get `rootN` while AVM1 clips get blank strings.
fn set_default_root_name(&self, context: &mut UpdateContext<'_, 'gc>) {
if self.movie().is_action_script_3() {
let name = AvmString::new_utf8(context.gc_context, format!("root{}", self.depth() + 1));
self.set_name(context.gc_context, name);
} else {
self.set_name(context.gc_context, Default::default());
}
}
fn bind_text_field_variables(&self, activation: &mut Activation<'_, 'gc>) {
// Check all unbound text fields to see if they apply to this object.
// TODO: Replace with `Vec::drain_filter` when stable.
let mut i = 0;
let mut len = activation.context.unbound_text_fields.len();
while i < len {
if activation.context.unbound_text_fields[i]
.try_bind_text_field_variable(activation, false)
{
activation.context.unbound_text_fields.swap_remove(i);
len -= 1;
} else {
i += 1;
}
}
}
/// Inform this object and its ancestors that it has visually changed and must be redrawn.
/// If this object or any ancestor is marked as cacheAsBitmap, it will invalidate that cache.
fn invalidate_cached_bitmap(&self, mc: &Mutation<'gc>) {
if self.base_mut(mc).invalidate_cached_bitmap() {
// Don't inform ancestors if we've already done so this frame
if let Some(parent) = self.parent() {
parent.invalidate_cached_bitmap(mc);
}
}
}
2019-04-29 05:55:44 +00:00
}
pub enum DisplayObjectPtr {}
impl<'gc> DisplayObject<'gc> {
pub fn ptr_eq(a: DisplayObject<'gc>, b: DisplayObject<'gc>) -> bool {
a.as_ptr() == b.as_ptr()
}
pub fn option_ptr_eq(a: Option<DisplayObject<'gc>>, b: Option<DisplayObject<'gc>>) -> bool {
a.map(|o| o.as_ptr()) == b.map(|o| o.as_ptr())
}
pub fn downgrade(self) -> DisplayObjectWeak<'gc> {
match self {
DisplayObject::MovieClip(mc) => DisplayObjectWeak::MovieClip(mc.downgrade()),
DisplayObject::LoaderDisplay(l) => DisplayObjectWeak::LoaderDisplay(l.downgrade()),
DisplayObject::Bitmap(b) => DisplayObjectWeak::Bitmap(b.downgrade()),
_ => panic!("Downgrade not yet implemented for {:?}", self),
}
}
}
2019-10-18 04:10:13 +00:00
bitflags! {
/// Bit flags used by `DisplayObject`.
#[derive(Clone, Copy)]
struct DisplayObjectFlags: u16 {
/// Whether this object has been removed from the display list.
/// Necessary in AVM1 to throw away queued actions from removed movie clips.
const AVM1_REMOVED = 1 << 0;
2019-12-04 01:52:00 +00:00
/// If this object is visible (`_visible` property).
const VISIBLE = 1 << 1;
2019-12-04 01:52:00 +00:00
/// Whether the `_xscale`, `_yscale` and `_rotation` of the object have been calculated and cached.
const SCALE_ROTATION_CACHED = 1 << 2;
/// Whether this object has been transformed by ActionScript.
/// When this flag is set, changes from SWF `PlaceObject` tags are ignored.
const TRANSFORMED_BY_SCRIPT = 1 << 3;
/// Whether this object has been placed in a container by ActionScript 3.
/// When this flag is set, changes from SWF `RemoveObject` tags are ignored.
const PLACED_BY_SCRIPT = 1 << 4;
/// Whether this object has been instantiated by a SWF tag.
/// When this flag is set, attempts to change the object's name from AVM2 throw an exception.
const INSTANTIATED_BY_TIMELINE = 1 << 5;
/// Whether this object is a "root", the top-most display object of a loaded SWF or Bitmap.
/// Used by `MovieClip.getBytesLoaded` in AVM1 and `DisplayObject.root` in AVM2.
const IS_ROOT = 1 << 6;
/// Whether this object has `_lockroot` set to true, in which case
/// it becomes the _root of itself and of any children
const LOCK_ROOT = 1 << 7;
/// Whether this object will be cached to bitmap.
const CACHE_AS_BITMAP = 1 << 8;
/// Whether this object has a scroll rectangle applied.
const HAS_SCROLL_RECT = 1 << 9;
/// Whether this object has an explicit name.
const HAS_EXPLICIT_NAME = 1 << 10;
/// Flag set when we should skip running our next 'enterFrame'
/// for ourself and our children.
/// This is set for objects constructed from ActionScript,
/// which are observed to lag behind objects placed by the timeline
/// (even if they are both placed in the same frame)
const SKIP_NEXT_ENTER_FRAME = 1 << 11;
/// If this object has already had `invalidate_cached_bitmap` called this frame
const CACHE_INVALIDATED = 1 << 12;
/// If this AVM1 object is pending removal (will be removed on the next frame).
const AVM1_PENDING_REMOVAL = 1 << 13;
}
2019-12-04 01:52:00 +00:00
}
2021-05-13 05:33:04 +00:00
bitflags! {
/// Defines how hit testing should be performed.
/// Used for mouse picking and ActionScript's hitTestClip functions.
#[derive(Clone, Copy)]
2021-05-13 05:33:04 +00:00
pub struct HitTestOptions: u8 {
/// Ignore objects used as masks (setMask / clipDepth).
const SKIP_MASK = 1 << 0;
/// Ignore objects with the ActionScript's visibility flag turned off.
const SKIP_INVISIBLE = 1 << 1;
/// The options used for `hitTest` calls in ActionScript.
const AVM_HIT_TEST = Self::SKIP_MASK.bits();
2021-05-13 05:33:04 +00:00
/// The options used for mouse picking, such as clicking on buttons.
const MOUSE_PICK = Self::SKIP_MASK.bits() | Self::SKIP_INVISIBLE.bits();
2021-05-13 05:33:04 +00:00
}
}
2021-06-21 16:29:52 +00:00
/// Represents the sound transform of sounds played inside a Flash MovieClip.
/// Every value is a percentage (0-100), but out of range values are allowed.
/// In AVM1, this is returned by `Sound.getTransform`.
/// In AVM2, this is returned by `Sprite.soundTransform`.
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct SoundTransform {
pub volume: i32,
pub left_to_left: i32,
pub left_to_right: i32,
pub right_to_left: i32,
pub right_to_right: i32,
}
impl SoundTransform {
pub const MAX_VOLUME: i32 = 100;
/// Applies another SoundTransform on top of this SoundTransform.
pub fn concat(&mut self, other: &SoundTransform) {
const MAX_VOLUME: i64 = SoundTransform::MAX_VOLUME as i64;
// It seems like Flash masks the results below to 30-bit integers:
// * Negative values are equivalent to their absolute value (their sign bit is unset).
// * Specifically, 0x40000000, -0x40000000 and -0x80000000 are equivalent to zero.
const MASK: i32 = (1 << 30) - 1;
self.volume = (i64::from(self.volume) * i64::from(other.volume) / MAX_VOLUME) as i32 & MASK;
// This is a 2x2 matrix multiply between the transforms.
// Done with integer math to match Flash behavior.
2021-06-22 10:04:27 +00:00
let ll0: i64 = self.left_to_left.into();
let lr0: i64 = self.left_to_right.into();
let rl0: i64 = self.right_to_left.into();
let rr0: i64 = self.right_to_right.into();
let ll1: i64 = other.left_to_left.into();
let lr1: i64 = other.left_to_right.into();
let rl1: i64 = other.right_to_left.into();
let rr1: i64 = other.right_to_right.into();
self.left_to_left = ((ll0 * ll1 + rl0 * lr1) / MAX_VOLUME) as i32 & MASK;
self.left_to_right = ((lr0 * ll1 + rr0 * lr1) / MAX_VOLUME) as i32 & MASK;
self.right_to_left = ((ll0 * rl1 + rl0 * rr1) / MAX_VOLUME) as i32 & MASK;
self.right_to_right = ((lr0 * rl1 + rr0 * rr1) / MAX_VOLUME) as i32 & MASK;
}
/// Returns the pan of this transform.
/// -100 is full left and 100 is full right.
/// This matches the behavior of AVM1 `Sound.getPan()`
pub fn pan(&self) -> i32 {
// It's not clear why Flash has the weird `abs` behavior, but this
// mathes the values that Flash returns (see `sound` regression test).
if self.left_to_left != Self::MAX_VOLUME {
Self::MAX_VOLUME - self.left_to_left.abs()
} else {
self.right_to_right.abs() - Self::MAX_VOLUME
}
}
/// Sets this transform of this pan.
/// -100 is full left and 100 is full right.
/// This matches the behavior of AVM1 `Sound.setPan()`.
pub fn set_pan(&mut self, pan: i32) {
if pan >= 0 {
self.left_to_left = Self::MAX_VOLUME - pan;
self.right_to_right = Self::MAX_VOLUME;
} else {
self.left_to_left = Self::MAX_VOLUME;
self.right_to_right = Self::MAX_VOLUME + pan;
}
self.left_to_right = 0;
self.right_to_left = 0;
}
pub fn from_avm2_object<'gc>(
activation: &mut Avm2Activation<'_, 'gc>,
as3_st: Avm2Object<'gc>,
) -> Result<Self, Avm2Error<'gc>> {
Ok(SoundTransform {
left_to_left: (as3_st
2023-02-09 16:54:38 +00:00
.get_public_property("leftToLeft", activation)?
.coerce_to_number(activation)?
* 100.0) as i32,
left_to_right: (as3_st
2023-02-09 16:54:38 +00:00
.get_public_property("leftToRight", activation)?
.coerce_to_number(activation)?
* 100.0) as i32,
right_to_left: (as3_st
2023-02-09 16:54:38 +00:00
.get_public_property("rightToLeft", activation)?
.coerce_to_number(activation)?
* 100.0) as i32,
right_to_right: (as3_st
2023-02-09 16:54:38 +00:00
.get_public_property("rightToRight", activation)?
.coerce_to_number(activation)?
* 100.0) as i32,
volume: (as3_st
2023-02-09 16:54:38 +00:00
.get_public_property("volume", activation)?
.coerce_to_number(activation)?
* 100.0) as i32,
})
}
pub fn into_avm2_object<'gc>(
self,
activation: &mut Avm2Activation<'_, 'gc>,
) -> Result<Avm2Object<'gc>, Avm2Error<'gc>> {
let as3_st = activation
.avm2()
.classes()
.soundtransform
.construct(activation, &[])?;
2023-02-09 16:54:38 +00:00
as3_st.set_public_property(
"leftToLeft",
(self.left_to_left as f64 / 100.0).into(),
activation,
)?;
2023-02-09 16:54:38 +00:00
as3_st.set_public_property(
"leftToRight",
(self.left_to_right as f64 / 100.0).into(),
activation,
)?;
2023-02-09 16:54:38 +00:00
as3_st.set_public_property(
"rightToLeft",
(self.right_to_left as f64 / 100.0).into(),
activation,
)?;
2023-02-09 16:54:38 +00:00
as3_st.set_public_property(
"rightToRight",
(self.right_to_right as f64 / 100.0).into(),
activation,
)?;
2023-02-09 16:54:38 +00:00
as3_st.set_public_property("volume", (self.volume as f64 / 100.0).into(), activation)?;
Ok(as3_st)
}
}
impl Default for SoundTransform {
fn default() -> Self {
Self {
volume: 100,
left_to_left: 100,
left_to_right: 0,
right_to_left: 0,
right_to_right: 100,
}
}
}
/// A version of `DisplayObject` that holds weak pointers.
/// Currently, this is only used by orphan handling, so we only
/// need two variants. If other use cases arise, feel free
/// to add more variants.
#[derive(Copy, Clone, Collect)]
#[collect(no_drop)]
pub enum DisplayObjectWeak<'gc> {
MovieClip(MovieClipWeak<'gc>),
LoaderDisplay(LoaderDisplayWeak<'gc>),
Bitmap(BitmapWeak<'gc>),
}
impl<'gc> DisplayObjectWeak<'gc> {
pub fn as_ptr(&self) -> *const DisplayObjectPtr {
match self {
DisplayObjectWeak::MovieClip(mc) => mc.as_ptr(),
DisplayObjectWeak::LoaderDisplay(ld) => ld.as_ptr(),
DisplayObjectWeak::Bitmap(b) => b.as_ptr(),
}
}
pub fn upgrade(&self, mc: &Mutation<'gc>) -> Option<DisplayObject<'gc>> {
match self {
DisplayObjectWeak::MovieClip(movie) => movie.upgrade(mc).map(|m| m.into()),
DisplayObjectWeak::LoaderDisplay(ld) => ld.upgrade(mc).map(|ld| ld.into()),
DisplayObjectWeak::Bitmap(b) => b.upgrade(mc).map(|ld| ld.into()),
}
}
}