avm2: Implement DisplayObject.transform and most of Transform

This PR implements the 'DisplayObject.transform' getters/setters,
and most of the getters/setters in the `Transform` class

From testing in FP, it appears that each call to the
'DisplayObject.transform' property produces a new
'Transform' instance, which is permanently tied to the
owner 'DisplayObject'. All of the getters/setters in
`Transform` operate directly on owner `DisplayObject`.
However, note that the `Matrix` and `ColorTransform`
valuse *produced* the getter are plain ActionScript objects,
and have no further tie to the `DisplayObject`.

Using the `DisplayObject.transform` setter results in
values being *copied* from the input `Transform` object.
The input object retains its original owner `DisplayObject`.

Not implemented:
* Transform.concatenatedColorTransform
* Transform.pixelBounds

When a DisplayObject is not a descendant of the stage,
the `concatenatedMatrix` property produces a bizarre matrix:
a scale matrix that the depends on the global state quality.
Any DisplayObject that *is* a descendant of the stage has
a `concatenatedMatrix` that does not depend on the stage quality.
I'm not sure why the behavior occurs - for now, I just manually
mimic the values prdduced by FP. However, these values may indicate
that we need to do some internal scaling based on stage quality values,
and then 'undo' this in certain circumstances when constructing
an ActionScript matrix.

Unfortunately, some of the computed 'concatenatedMatrix' values
are off by f32::EPSILON. This is likely due to us storing some
internal values in pixels rather than twips (the rounding introduced
by round-trip twips conversions could cause this slight difference0.
For now, I've opted to mark these tests as 'approximate'.

To support this, I've extended our test framework to support providing
a regex that matches floating-point values in the output. This allows
us to print out 'Matrix.toString()' and still perform approximate
comparisons between strings of the format
'(a=0, b=0, c=0, d=0, tx=0, ty=0)'
This commit is contained in:
Aaron Hill 2022-06-19 15:37:31 -05:00 committed by Mike Welsh
parent c4488fc883
commit 6f20e8882d
16 changed files with 486 additions and 4 deletions

1
Cargo.lock generated
View File

@ -3621,6 +3621,7 @@ dependencies = [
"futures",
"image",
"pretty_assertions",
"regex",
"ruffle_core",
"ruffle_input_format",
"ruffle_render_wgpu",

View File

@ -93,6 +93,9 @@ pub struct SystemClasses<'gc> {
pub errorevent: ClassObject<'gc>,
pub ioerrorevent: ClassObject<'gc>,
pub securityerrorevent: ClassObject<'gc>,
pub transform: ClassObject<'gc>,
pub colortransform: ClassObject<'gc>,
pub matrix: ClassObject<'gc>,
}
impl<'gc> SystemClasses<'gc> {
@ -155,6 +158,9 @@ impl<'gc> SystemClasses<'gc> {
errorevent: object,
ioerrorevent: object,
securityerrorevent: object,
transform: object,
colortransform: object,
matrix: object,
}
}
}
@ -752,6 +758,9 @@ fn load_playerglobal<'gc>(
("flash.events", "IOErrorEvent", ioerrorevent),
("flash.events", "MouseEvent", mouseevent),
("flash.events", "FullScreenEvent", fullscreenevent),
("flash.geom", "Matrix", matrix),
("flash.geom", "Transform", transform),
("flash.geom", "ColorTransform", colortransform),
]
);

View File

@ -0,0 +1,5 @@
// This is a stub - the actual class is implemented in 'displayobject.rs'
package flash.display {
public class DisplayObject {
}
}

View File

@ -594,6 +594,54 @@ pub fn loader_info<'gc>(
Ok(Value::Undefined)
}
pub fn transform<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
this: Option<Object<'gc>>,
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
if let Some(this) = this {
return Ok(activation
.avm2()
.classes()
.transform
.construct(activation, &[this.into()])?
.into());
}
Ok(Value::Undefined)
}
pub fn set_transform<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
this: Option<Object<'gc>>,
args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
if let Some(this) = this {
let transform = args[0].coerce_to_object(activation)?;
// FIXME - consider 3D matrix and pixel bounds
let matrix = transform
.get_property(&QName::dynamic_name("matrix").into(), activation)?
.coerce_to_object(activation)?;
let color_transform = transform
.get_property(&QName::dynamic_name("matrix").into(), activation)?
.coerce_to_object(activation)?;
let matrix =
crate::avm2::globals::flash::geom::transform::object_to_matrix(matrix, activation)?;
let color_transform =
crate::avm2::globals::flash::geom::transform::object_to_color_transform(
color_transform,
activation,
)?;
let dobj = this.as_display_object().unwrap();
let mut write = dobj.base_mut(activation.context.gc_context);
write.set_color_transform(&color_transform);
write.set_matrix(&matrix);
}
Ok(Value::Undefined)
}
/// Construct `DisplayObject`'s class.
pub fn create_class<'gc>(mc: MutationContext<'gc, '_>) -> GcCell<'gc, Class<'gc>> {
let class = Class::new(
@ -637,6 +685,7 @@ pub fn create_class<'gc>(mc: MutationContext<'gc, '_>) -> GcCell<'gc, Class<'gc>
("mouseY", Some(mouse_y), None),
("loaderInfo", Some(loader_info), None),
("filters", Some(filters), Some(set_filters)),
("transform", Some(transform), Some(set_transform)),
];
write.define_public_builtin_instance_properties(mc, PUBLIC_INSTANCE_PROPERTIES);

View File

@ -1 +1,3 @@
//! `flash.geom` namespace
pub mod transform;

View File

@ -29,5 +29,9 @@ package flash.geom {
this.a *= sx;
this.d *= sy;
}
public function toString():String {
return "(a=" + this.a + ", b=" + this.b + ", c=" + this.c + ", d=" + this.d + ", tx=" + this.tx + ", ty=" + this.ty + ")";
}
}
}

View File

@ -0,0 +1,19 @@
package flash.geom {
import flash.display.DisplayObject;
public class Transform {
private var _displayObject:DisplayObject;
function Transform(object: DisplayObject) {
this.init(object);
}
native function init(object:DisplayObject):void;
public native function get colorTransform():ColorTransform;
public native function set colorTransform(value: ColorTransform):void;
public native function get matrix():Matrix;
public native function set matrix(value:Matrix):void;
public native function get concatenatedColorTransform():ColorTransform;
public native function get concatenatedMatrix():Matrix;
}
}

View File

@ -0,0 +1,242 @@
#![allow(non_snake_case)]
use crate::avm2::{Activation, Error, Namespace, Object, QName, TObject, Value};
use crate::display_object::{StageQuality, TDisplayObject};
use crate::prelude::{ColorTransform, DisplayObject, Matrix, Twips};
use swf::Fixed8;
fn get_display_object<'gc>(
this: Object<'gc>,
activation: &mut Activation<'_, 'gc, '_>,
) -> Result<DisplayObject<'gc>, Error> {
Ok(this
.get_property(
&QName::new(Namespace::Private("".into()), "_displayObject").into(),
activation,
)?
.as_object()
.unwrap()
.as_display_object()
.unwrap())
}
pub fn init<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
this: Option<Object<'gc>>,
args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
this.unwrap().set_property(
&QName::new(Namespace::Private("".into()), "_displayObject").into(),
args[0],
activation,
)?;
Ok(Value::Undefined)
}
pub fn get_color_transform<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
this: Option<Object<'gc>>,
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
let this = this.unwrap();
let ct_obj = *get_display_object(this, activation)?
.base()
.color_transform();
color_transform_to_object(&ct_obj, activation)
}
pub fn set_color_transform<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
this: Option<Object<'gc>>,
args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
let this = this.unwrap();
let ct = object_to_color_transform(args[0].coerce_to_object(activation)?, activation)?;
get_display_object(this, activation)?
.base_mut(activation.context.gc_context)
.set_color_transform(&ct);
Ok(Value::Undefined)
}
pub fn get_matrix<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
this: Option<Object<'gc>>,
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
let this = this.unwrap();
let matrix = *get_display_object(this, activation)?.base().matrix();
matrix_to_object(matrix, activation)
}
pub fn set_matrix<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
this: Option<Object<'gc>>,
args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
let this = this.unwrap();
let matrix = object_to_matrix(args[0].coerce_to_object(activation)?, activation)?;
get_display_object(this, activation)?
.base_mut(activation.context.gc_context)
.set_matrix(&matrix);
Ok(Value::Undefined)
}
pub fn get_concatenated_matrix<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
this: Option<Object<'gc>>,
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
let this = this.unwrap();
let dobj = get_display_object(this, activation)?;
let mut node = Some(dobj);
while let Some(obj) = node {
if obj.as_stage().is_some() {
break;
}
node = obj.parent();
}
// We're a child of the Stage, and not the stage itself
if node.is_some() && dobj.as_stage().is_none() {
let matrix = get_display_object(this, activation)?.local_to_global_matrix();
matrix_to_object(matrix, activation)
} else {
// If this object is the Stage itself, or an object
// that's not a child of the stage, then we need to mimic
// Flash's bizarre behavior.
let scale = match activation.context.stage.quality() {
StageQuality::Low => 20.0,
StageQuality::Medium => 10.0,
StageQuality::High | StageQuality::Best => 5.0,
StageQuality::High8x8 | StageQuality::High8x8Linear => 2.5,
StageQuality::High16x16 | StageQuality::High16x16Linear => 1.25,
};
let mut mat = *dobj.base().matrix();
mat.a *= scale;
mat.d *= scale;
matrix_to_object(mat, activation)
}
}
pub fn get_concatenated_color_transform<'gc>(
_activation: &mut Activation<'_, 'gc, '_>,
_this: Option<Object<'gc>>,
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
log::warn!("Transform.concatenatedColorTransform: not yet implemented");
Ok(Value::Undefined)
}
// FIXME - handle clamping. We're throwing away precision here in converting to an integer:
// is that what we should be doing?
pub fn object_to_color_transform<'gc>(
object: Object<'gc>,
activation: &mut Activation<'_, 'gc, '_>,
) -> Result<ColorTransform, Error> {
let red_multiplier = object
.get_property(&QName::dynamic_name("redMultiplier").into(), activation)?
.coerce_to_number(activation)?;
let green_multiplier = object
.get_property(&QName::dynamic_name("greenMultiplier").into(), activation)?
.coerce_to_number(activation)?;
let blue_multiplier = object
.get_property(&QName::dynamic_name("blueMultiplier").into(), activation)?
.coerce_to_number(activation)?;
let alpha_multiplier = object
.get_property(&QName::dynamic_name("alphaMultiplier").into(), activation)?
.coerce_to_number(activation)?;
let red_offset = object
.get_property(&QName::dynamic_name("redOffset").into(), activation)?
.coerce_to_number(activation)?;
let green_offset = object
.get_property(&QName::dynamic_name("greenOffset").into(), activation)?
.coerce_to_number(activation)?;
let blue_offset = object
.get_property(&QName::dynamic_name("blueOffset").into(), activation)?
.coerce_to_number(activation)?;
let alpha_offset = object
.get_property(&QName::dynamic_name("alphaOffset").into(), activation)?
.coerce_to_number(activation)?;
Ok(ColorTransform {
r_mult: Fixed8::from_f64(red_multiplier),
g_mult: Fixed8::from_f64(green_multiplier),
b_mult: Fixed8::from_f64(blue_multiplier),
a_mult: Fixed8::from_f64(alpha_multiplier),
r_add: red_offset as i16,
g_add: green_offset as i16,
b_add: blue_offset as i16,
a_add: alpha_offset as i16,
})
}
pub fn color_transform_to_object<'gc>(
color_transform: &ColorTransform,
activation: &mut Activation<'_, 'gc, '_>,
) -> Result<Value<'gc>, Error> {
let args = [
color_transform.r_mult.to_f64().into(),
color_transform.g_mult.to_f64().into(),
color_transform.b_mult.to_f64().into(),
color_transform.a_mult.to_f64().into(),
color_transform.r_add.into(),
color_transform.g_add.into(),
color_transform.b_add.into(),
color_transform.a_add.into(),
];
let ct_class = activation.avm2().classes().colortransform;
let object = ct_class.construct(activation, &args)?;
Ok(object.into())
}
pub fn matrix_to_object<'gc>(
matrix: Matrix,
activation: &mut Activation<'_, 'gc, '_>,
) -> Result<Value<'gc>, Error> {
let args = [
matrix.a.into(),
matrix.b.into(),
matrix.c.into(),
matrix.d.into(),
matrix.tx.to_pixels().into(),
matrix.ty.to_pixels().into(),
];
let object = activation
.avm2()
.classes()
.matrix
.construct(activation, &args)?;
Ok(object.into())
}
pub fn object_to_matrix<'gc>(
object: Object<'gc>,
activation: &mut Activation<'_, 'gc, '_>,
) -> Result<Matrix, Error> {
let a = object
.get_property(&QName::dynamic_name("a").into(), activation)?
.coerce_to_number(activation)? as f32;
let b = object
.get_property(&QName::dynamic_name("b").into(), activation)?
.coerce_to_number(activation)? as f32;
let c = object
.get_property(&QName::dynamic_name("c").into(), activation)?
.coerce_to_number(activation)? as f32;
let d = object
.get_property(&QName::dynamic_name("d").into(), activation)?
.coerce_to_number(activation)? as f32;
let tx = Twips::from_pixels(
object
.get_property(&QName::dynamic_name("tx").into(), activation)?
.coerce_to_number(activation)?,
);
let ty = Twips::from_pixels(
object
.get_property(&QName::dynamic_name("ty").into(), activation)?
.coerce_to_number(activation)?,
);
Ok(Matrix { a, b, c, d, tx, ty })
}

View File

@ -56,6 +56,7 @@ include "flash/geom/Orientation3D.as"
include "flash/geom/Matrix.as"
include "flash/geom/Point.as"
include "flash/geom/Rectangle.as"
include "flash/geom/Transform.as"
include "flash/geom/Vector3D.as"
include "flash/net/SharedObjectFlushStatus.as"
include "flash/net/URLLoader.as"

View File

@ -8,6 +8,7 @@ include "Array.as"
include "Boolean.as"
include "flash/display/DisplayObjectContainer.as"
include "flash/display/InteractiveObject.as"
include "flash/display/DisplayObject.as"
include "flash/events/EventDispatcher.as"
include "flash/system/ApplicationDomain.as"
include "Number.as"

View File

@ -167,7 +167,7 @@ impl<'gc> DisplayObjectBase<'gc> {
&mut self.transform.matrix
}
fn set_matrix(&mut self, matrix: &Matrix) {
pub fn set_matrix(&mut self, matrix: &Matrix) {
self.transform.matrix = *matrix;
self.flags -= DisplayObjectFlags::SCALE_ROTATION_CACHED;
}
@ -180,7 +180,7 @@ impl<'gc> DisplayObjectBase<'gc> {
&mut self.transform.color_transform
}
fn set_color_transform(&mut self, color_transform: &ColorTransform) {
pub fn set_color_transform(&mut self, color_transform: &ColorTransform) {
self.transform.color_transform = *color_transform;
}

View File

@ -10,6 +10,7 @@ ruffle_core = { path = "../core", features = ["deterministic"] }
ruffle_render_wgpu = { path = "../render/wgpu" }
ruffle_input_format = { path = "input-format" }
image = "0.24.2"
regex = "1.6.0"
[features]
# Enable running image comparison tests. This is off by default,

View File

@ -3,6 +3,7 @@
//! Trace output can be compared with correct output from the official Flash Player.
use approx::assert_relative_eq;
use regex::Regex;
use ruffle_core::backend::{
log::LogBackend,
navigator::{NullExecutor, NullNavigatorBackend},
@ -45,6 +46,15 @@ macro_rules! val_or_false {
};
}
macro_rules! val_or_empty_slice {
($val:expr) => {
$val
};
() => {
&[]
};
}
// This macro generates test cases for a given list of SWFs.
// If 'img' is true, then we will render an image of the final frame
// of the SWF, and compare it against a reference image on disk.
@ -69,8 +79,12 @@ macro_rules! swf_tests {
}
// This macro generates test cases for a given list of SWFs using `test_swf_approx`.
// If provided, `@num_patterns` must be a `&[Regex]`. Each regex in the slice is
// tested against the expected and actual - if it matches, then each capture
// group is treated as a floating-point value to be compared approximately.
// The rest of the string (outside of the capture groups) is compared exactly.
macro_rules! swf_tests_approx {
($($(#[$attr:meta])* ($name:ident, $path:expr, $num_frames:literal $(, $opt:ident = $val:expr)*),)*) => {
($($(#[$attr:meta])* ($name:ident, $path:expr, $num_frames:literal $(, @num_patterns = $num_patterns:expr)? $(, $opt:ident = $val:expr)*),)*) => {
$(
#[test]
$(#[$attr])*
@ -81,6 +95,7 @@ macro_rules! swf_tests_approx {
$num_frames,
concat!("tests/swfs/", $path, "/input.json"),
concat!("tests/swfs/", $path, "/output.txt"),
val_or_empty_slice!($($num_patterns)?),
|actual, expected| assert_relative_eq!(actual, expected $(, $opt = $val)*),
)
}
@ -809,6 +824,9 @@ swf_tests_approx! {
(as3_displayobject_height, "avm2/displayobject_height", 7, epsilon = 0.06), // TODO: height/width appears to be off by 1 twip sometimes
(as3_displayobject_rotation, "avm2/displayobject_rotation", 1, epsilon = 0.0000000001),
(as3_displayobject_width, "avm2/displayobject_width", 7, epsilon = 0.06),
(as3_displayobject_transform, "avm2/displayobject_transform", 1, @num_patterns = &[
Regex::new(r"\(a=(.+), b=(.+), c=(.+), d=(.+), tx=(.+), ty=(.+)\)").unwrap()
], max_relative = f32::EPSILON as f64),
(as3_divide, "avm2/divide", 1, epsilon = 0.0), // TODO: Discrepancy in float formatting.
(as3_edittext_align, "avm2/edittext_align", 1, epsilon = 3.0),
(as3_edittext_autosize, "avm2/edittext_autosize", 1, epsilon = 0.1),
@ -1138,6 +1156,7 @@ fn test_swf_approx(
num_frames: u32,
simulated_input_path: &str,
expected_output_path: &str,
num_patterns: &[Regex],
approx_assert_fn: impl Fn(f64, f64),
) -> Result<(), Error> {
let injector =
@ -1185,9 +1204,48 @@ fn test_swf_approx(
// }
approx_assert_fn(actual, expected);
} else {
let mut found = false;
// Check each of the user-provided regexes for a match
for pattern in num_patterns {
if let (Some(actual_captures), Some(expected_captures)) =
(pattern.captures(actual), pattern.captures(expected))
{
found = true;
std::assert_eq!(
actual_captures.len(),
expected_captures.len(),
"Differing numbers of regex captures"
);
// Each capture group (other than group 0, which is always the entire regex
// match) represents a floating-point value
for (actual_val, expected_val) in actual_captures
.iter()
.skip(1)
.zip(expected_captures.iter().skip(1))
{
let actual_num = actual_val
.expect("Missing capture gruop value for 'actual'")
.as_str()
.parse::<f64>()
.expect("Failed to parse 'actual' capture group as float");
let expected_num = expected_val
.expect("Missing capture gruop value for 'expected'")
.as_str()
.parse::<f64>()
.expect("Failed to parse 'expected' capture group as float");
approx_assert_fn(actual_num, expected_num);
}
let modified_actual = pattern.replace(actual, "");
let modified_expected = pattern.replace(expected, "");
assert_eq!(modified_actual, modified_expected);
break;
}
}
if !found {
assert_eq!(actual, expected);
}
}
}
Ok(())
}

View File

@ -0,0 +1,90 @@
Checking stage qualities with non-stage child
best TextField (a=5, b=0, c=0, d=5, tx=0, ty=0)
best stage(a=5, b=0, c=0, d=5, tx=0, ty=0)
high TextField (a=5, b=0, c=0, d=5, tx=0, ty=0)
high stage(a=5, b=0, c=0, d=5, tx=0, ty=0)
16x16 TextField (a=1.25, b=0, c=0, d=1.25, tx=0, ty=0)
16x16 stage(a=1.25, b=0, c=0, d=1.25, tx=0, ty=0)
16x16linear TextField (a=1.25, b=0, c=0, d=1.25, tx=0, ty=0)
16x16linear stage(a=1.25, b=0, c=0, d=1.25, tx=0, ty=0)
8x8 TextField (a=2.5, b=0, c=0, d=2.5, tx=0, ty=0)
8x8 stage(a=2.5, b=0, c=0, d=2.5, tx=0, ty=0)
8x8linear TextField (a=2.5, b=0, c=0, d=2.5, tx=0, ty=0)
8x8linear stage(a=2.5, b=0, c=0, d=2.5, tx=0, ty=0)
low TextField (a=20, b=0, c=0, d=20, tx=0, ty=0)
low stage(a=20, b=0, c=0, d=20, tx=0, ty=0)
medium TextField (a=10, b=0, c=0, d=10, tx=0, ty=0)
medium stage(a=10, b=0, c=0, d=10, tx=0, ty=0)
Checking stage qualities with child on stage
best TextField (a=1, b=0, c=0, d=1, tx=0, ty=0)
best stage(a=5, b=0, c=0, d=5, tx=0, ty=0)
high TextField (a=1, b=0, c=0, d=1, tx=0, ty=0)
high stage(a=5, b=0, c=0, d=5, tx=0, ty=0)
16x16 TextField (a=1, b=0, c=0, d=1, tx=0, ty=0)
16x16 stage(a=1.25, b=0, c=0, d=1.25, tx=0, ty=0)
16x16linear TextField (a=1, b=0, c=0, d=1, tx=0, ty=0)
16x16linear stage(a=1.25, b=0, c=0, d=1.25, tx=0, ty=0)
8x8 TextField (a=1, b=0, c=0, d=1, tx=0, ty=0)
8x8 stage(a=2.5, b=0, c=0, d=2.5, tx=0, ty=0)
8x8linear TextField (a=1, b=0, c=0, d=1, tx=0, ty=0)
8x8linear stage(a=2.5, b=0, c=0, d=2.5, tx=0, ty=0)
low TextField (a=1, b=0, c=0, d=1, tx=0, ty=0)
low stage(a=20, b=0, c=0, d=20, tx=0, ty=0)
medium TextField (a=1, b=0, c=0, d=1, tx=0, ty=0)
medium stage(a=10, b=0, c=0, d=10, tx=0, ty=0)
// child.transform == child.transform
false
// firstTransform
colorTransform=(redMultiplier=1, greenMultiplier=1, blueMultiplier=1, alphaMultiplier=1, redOffset=0, greenOffset=0, blueOffset=0, alphaOffset=0)
matrix=(a=1, b=0, c=0, d=1, tx=0, ty=0)
concatenatedMatrix=(a=1, b=0, c=0, d=1, tx=0, ty=0)
// secondTransform
colorTransform=(redMultiplier=1, greenMultiplier=1, blueMultiplier=1, alphaMultiplier=1, redOffset=0, greenOffset=0, blueOffset=0, alphaOffset=0)
matrix=(a=1, b=0, c=0, d=1, tx=0, ty=0)
concatenatedMatrix=(a=1, b=0, c=0, d=1, tx=0, ty=0)
// firstTransform after no-op modifications
colorTransform=(redMultiplier=1, greenMultiplier=1, blueMultiplier=1, alphaMultiplier=1, redOffset=0, greenOffset=0, blueOffset=0, alphaOffset=0)
matrix=(a=1, b=0, c=0, d=1, tx=0, ty=0)
concatenatedMatrix=(a=1, b=0, c=0, d=1, tx=0, ty=0)
// firstTransform after matrix modification
colorTransform=(redMultiplier=1, greenMultiplier=1, blueMultiplier=1, alphaMultiplier=1, redOffset=0, greenOffset=0, blueOffset=0, alphaOffset=0)
matrix=(a=42, b=0, c=0, d=1, tx=0, ty=0)
concatenatedMatrix=(a=42.000003814697266, b=0, c=0, d=1, tx=0, ty=0)
// secondTransform
colorTransform=(redMultiplier=1, greenMultiplier=1, blueMultiplier=1, alphaMultiplier=1, redOffset=0, greenOffset=0, blueOffset=0, alphaOffset=0)
matrix=(a=42, b=0, c=0, d=1, tx=0, ty=0)
concatenatedMatrix=(a=42.000003814697266, b=0, c=0, d=1, tx=0, ty=0)
// firstTransform after color modification
colorTransform=(redMultiplier=1, greenMultiplier=1, blueMultiplier=1, alphaMultiplier=1, redOffset=12, greenOffset=0, blueOffset=0, alphaOffset=0)
matrix=(a=42, b=0, c=0, d=1, tx=0, ty=0)
concatenatedMatrix=(a=42.000003814697266, b=0, c=0, d=1, tx=0, ty=0)
// secondTransform
colorTransform=(redMultiplier=1, greenMultiplier=1, blueMultiplier=1, alphaMultiplier=1, redOffset=12, greenOffset=0, blueOffset=0, alphaOffset=0)
matrix=(a=42, b=0, c=0, d=1, tx=0, ty=0)
concatenatedMatrix=(a=42.000003814697266, b=0, c=0, d=1, tx=0, ty=0)
// firstTransform after setting parent
colorTransform=(redMultiplier=1, greenMultiplier=1, blueMultiplier=1, alphaMultiplier=1, redOffset=12, greenOffset=0, blueOffset=0, alphaOffset=0)
matrix=(a=42, b=0, c=0, d=1, tx=0, ty=0)
concatenatedMatrix=(a=420, b=0, c=0, d=10, tx=0, ty=0)
// secondTransform after setting parent
colorTransform=(redMultiplier=1, greenMultiplier=1, blueMultiplier=1, alphaMultiplier=1, redOffset=12, greenOffset=0, blueOffset=0, alphaOffset=0)
matrix=(a=42, b=0, c=0, d=1, tx=0, ty=0)
concatenatedMatrix=(a=420, b=0, c=0, d=10, tx=0, ty=0)
// firstTransform after indirectly added to stage
colorTransform=(redMultiplier=1, greenMultiplier=1, blueMultiplier=1, alphaMultiplier=1, redOffset=12, greenOffset=0, blueOffset=0, alphaOffset=0)
matrix=(a=42, b=0, c=0, d=1, tx=0, ty=0)
concatenatedMatrix=(a=1764.0001220703125, b=1764.0001220703125, c=42.000003814697266, d=42.000003814697266, tx=0, ty=0)
// secondTransform after indirectly added to stage
colorTransform=(redMultiplier=1, greenMultiplier=1, blueMultiplier=1, alphaMultiplier=1, redOffset=12, greenOffset=0, blueOffset=0, alphaOffset=0)
matrix=(a=42, b=0, c=0, d=1, tx=0, ty=0)
concatenatedMatrix=(a=1764.0001220703125, b=1764.0001220703125, c=42.000003814697266, d=42.000003814697266, tx=0, ty=0)
// firstTransform after indirectly removed from stage
colorTransform=(redMultiplier=1, greenMultiplier=1, blueMultiplier=1, alphaMultiplier=1, redOffset=12, greenOffset=0, blueOffset=0, alphaOffset=0)
matrix=(a=42, b=0, c=0, d=1, tx=0, ty=0)
concatenatedMatrix=(a=420, b=0, c=0, d=10, tx=0, ty=0)
// secondTransform after indirectly removed forrm stage
colorTransform=(redMultiplier=1, greenMultiplier=1, blueMultiplier=1, alphaMultiplier=1, redOffset=12, greenOffset=0, blueOffset=0, alphaOffset=0)
matrix=(a=42, b=0, c=0, d=1, tx=0, ty=0)
concatenatedMatrix=(a=420, b=0, c=0, d=10, tx=0, ty=0)

Binary file not shown.

Binary file not shown.