Copy link to clipboard
Copied
The data type for x and y properties is a double precision floating-point Number, so why is it rounding off values? Rounding is an unnecessary operation, and furthermore it's not saving any memory, because it just requires me to store a more precise value elsewhere, in addition to the lower-bit rounded value.
This rounding causing unnecessary additional work when coding against these values, because I have to round other values as well to make sure there aren't discrepancies when transforming coordinates.
So I was really surprised to discover that if I assign a value to a DisplayObject's x or y coordinates such as 10.3333333, and trace the value, it becomes 10.3.
If I assign 10.666666, it becomes 10.65. Apparently it's rounding everything to the nearest 20th of a unit. So now, I have to override the x and y properties to store the Number-type value, once again as a Number-type, which is not rounded.
Flash's arbitrary rounding of coordinates is causing erratic rounding errors when performing coordinate system transformations using localToGlobal and globalToLocal to find the composite scale of an object on the stage.
For example, suppose an object was laid out to occupy one third of the display, and it's width ends up being 200.3333333. One calculation of my docking framework involves obtaining the orthagonal bounding box of the child by transforming its corner points into stage coordinates using localToGlobal, which accounts for things like scaling and rotation. So despite everything having a scale of 1, and having zero rotation, you'd still end up with a rectangle with a width of 200.3 instead of the expected 200.3333333 in stage coordinates. So it would appear as though the composite scale is slightly smaller than 1, since 200.3 / 200.333333 is 0.99983361081528. But the composite scale is in fact 1, we just don't know that because Flash unexpectedly rounded some coordinates to an arbitrary 1/20 unit.
No game engine in existence does that with its transformation matrices, because it's retarded to round so early, and then allow those rounding errors to accumulate through a display hierarchy via functions like localToGlobal.
This rounding is causing jittering by a pixel or so when animating a drop down panel in my my docking framework, because it's constantly correcting for unexpected anomalies in the scaling factor on each frame. Despite the parent container having a constant fixed width, the child object, once its corner coordinates are passed through localToGlobal, end up reporting rounded widths, which ultimately leads to a series such as the following:
dockedChild.width: 538.3842482614723, parent.width: 558.3412118444024
dockedChild.width: 538.3754595467979, parent.width: 558.3412118444024
dockedChild.width: 538.3666709755926, parent.width: 558.3412118444024
dockedChild.width: 538.3578825478539, parent.width: 558.3412118444024
dockedChild.width: 538.3490942635798, parent.width: 558.3412118444024
dockedChild.width: 538.3903098666023, parent.width: 558.3412118444024
dockedChild.width: 538.3815210529766, parent.width: 558.3412118444024
dockedChild.width: 538.3727323828218, parent.width: 558.3412118444024
dockedChild.width: 538.3639438561353, parent.width: 558.3412118444024
dockedChild.width: 538.3551554729148, parent.width: 558.3412118444024
dockedChild.width: 538.346367233158, parent.width: 558.3412118444024
dockedChild.width: 538.3875826274011, parent.width: 558.3412118444024
dockedChild.width: 538.3787938582956, parent.width: 558.3412118444024
dockedChild.width: 538.37000523266, parent.width: 558.3412118444024
dockedChild.width: 538.3612167504922, parent.width: 558.3412118444024
dockedChild.width: 538.3524284117897, parent.width: 558.3412118444024
dockedChild.width: 538.3436402165502, parent.width: 558.3412118444024
dockedChild.width: 538.384855402015, parent.width: 558.3412118444024
dockedChild.width: 538.3760666774294, parent.width: 558.3412118444024
dockedChild.width: 538.3672780963132, parent.width: 558.3412118444024
dockedChild.width: 538.3584896586638, parent.width: 558.3412118444024
dockedChild.width: 538.349701364479, parent.width: 558.3412118444024
dockedChild.width: 538.3909170139807, parent.width: 558.3412118444024
Is there any way to turn off this rounding to 0.05 units?
To override the x and y values to have greater precision, I must do the following:
public class Control extends MovieClip
{
public function Control()
{
super(); //Flash performs timeline/graphics initialization here, which means after this call, the object may have non-zero x and y values
_x = super.x; //acquire them immediately, so if we try to set x or y to zero, the 'if (_x != value)' check does not think it's already positioned at zero and ignore the call
_y = super.y;
}
private var _x:Number;
private var _y:Number;
override public function get x():Number { return _x; } //return precise value, rather than rounded super.x value
override public function set x( value:Number ):void
{
if (_x != value) //ensure value is actually changing before performing work
{
_x = value; //store precise value in private variable
super.x = value; //DisplayObject will round value to nearest 0.05
if (stage != null)
stage.invalidate(); //ensure RENDER event is dispatched to re-render anything that may need to account for a repositioned object
}
}
override public function get y():Number { return _y; } //return precise value, rather than rounded super.y value
override public function set y( value:Number ):void
{
if (_y != value) //ensure value is actually changing before performing work
{
_y = value; //store precise value in private variable
super.y = value; //DisplayObject will round value to nearest 0.05
if (stage != null)
stage.invalidate(); //ensure RENDER event is dispatched to re-render anything that may need to account for a repositioned object }
}
}
Most importantly, you must initialize the _x and _y values to super.x and super.y in the constructor immediately after a call to super(), in order to acquire any non-zero values that the object instance may have been initialized with on the timeline.
I just cannot fathom why they didn't leave the x and y coordinates as-is, instead of rounding them, when it causes so many problems and complications, and requires overriding not only x and y, but functions like localToGlobal/globalToLocal/getRect.
This has been an issue for a while:
flash - AS3 x and y property precision - Stack Overflow specifically: flash - AS3 x and y property precision - Stack Overflow
http://www.actionscript.org/forums/showthread.php3?t=96510
Problems with Sub-pixel Coordinate Movement
In fact, that last link says: "
Running the code:
However, the one benefit is that the distance does not diverge by more than 1 pixel."
That's precisely what I saw happening in my own code, as you can see from the series of widths I posted above, which seem to fluctuate randomly between 358.34 and 358.39.
Copy link to clipboard
Copied
there's no way to prevent/override a display object being assigned to the nearest 1/20th pixel.
Copy link to clipboard
Copied
Actually, there is a way.
If you simply activate the 3D transformation by setting z to zero, the matrix3D replaces the matrix and concatatedMatrix properties of the DisplayObject's transform object, and suddenly x and y values maintain a precision higher than a twip. It's not quite the double-precision value of the Number type, however, and looks more like a single-precision 32-bit floating point value.
For example, if you run the following code:
var mc:Sprite = new Sprite();
mc.x = 200.0 + (1/3); //assign high precision value to x
trace(mc.x); //traces 200.3 (rounded)
mc.z = 0; //activate 3D matrix
mc.x = 200.0 + (1/3);
trace(mc.x); //traces 200.33333740234374 (still rounded, but accurate to 5 decimal places)
Based on the traced output, it's clear that it is possible to force the DisplayObject to get and set higher precision values for x and y properties, without any modifications to the underlying classes. However, I'm not happy with that solution for 2 reasons. First, it activates stage 3D and introduces graphical glitches and unnecessary bitmap caching. Second, it's still not "Number" precision; it's something less than that.
Instead, I was able to successfully work around the issue by altering the overrides for properties x, y, scaleX, scaleY, and methods localToGlobal, globalToLocal, and getRect to use privately maintained values. I was already using privately maintained width and height values in order to decouple the size from the scaling factors. No need to override getBounds, since it accounts for stroke widths and will be non-exact anyway.
The consistent, high precision values are vital, and they increased the performance of my layout framework, because it's actually able to prevent unnecessary assignments to x, y, width, and height when the values aren't actually changing. #beginrant: Such detection is impossible when Flash internally rounds everything, because if you try to keep something at, for example, 1/3 of the screen, it will always think you're trying to assign a high precision value of 200.33333 over a less precise value of 200.3 as I had previously described. Alternatively, you'd have to pre-round any value you try to assign, which is more work than it's worth. It's sort of terrible that Flash rounds property values as it does, because the value you assign can never be read back the same. That's generally not how numerical properties should work when assigning values of the same data type and precision. #endrant
In particular, two optimizations were made in the getRect override. If the target coordinate system is null or "this", then it simply returns new Rectangle( 0, 0, _width, _height ), and more importantly, if the target coordinate system is "parent" (which is the case 99% of the time) and rotation is zero and scale is 1 (also the case 99% of the time for GUI elements), then it simply returns new Rectangle( _x, _y, _width, _height ), which is the internal, high-precision values for x, y, width, and height (resemblance to AS2 properties is purely coincidental; this is AS3 code). That allows me to skip the following code path 99% of the time, which would otherwise return the orthagonal bounding box of the element at any rotation in any coordinate system:
//These instance variables are used to accelerate calculations, see comments.
//Upper left is always an empty point, and these variables store upper right, lower right, and lower left corner points.
protected var p_UR:Point; //DO NOT ALTER p_UR.y; LEAVE AT ZERO ALWAYS
protected var p_LR:Point; //Lower right corner: x = width, y = height
protected var p_LL:Point; //DO NOT ALTER p_LL.x; LEAVE AT ZERO ALWAYS
//Returns the orthogonal bounding rectangle of the object in the specified target coordinate system
//based on its own internal height and width (actual contentRect may be larger).
//getContentRect function was added to replace the original functionality of this method
override public function getRect( targetCoordinateSpace:DisplayObject ):Rectangle
{
switch (_scaleMode) //GUIControl allows decoupling of size and scale
{
case SCALE_NOSYNC_SIZE:
if (targetCoordinateSpace == null || targetCoordinateSpace == this)
return new Rectangle( 0, 0, _width, _height );
//UPDATE: Created this optimization to ensure rounding errors introduced by
//Flash's tendency to round x and y coordinates to twips are not introduced
//by localToGlobal/globalToLocal calls, so they are avoided if possible.
if (targetCoordinateSpace == parent && rotation == 0 && scaleX == 1 && scaleY == 1)
return new Rectangle( _x, _y, _width, _height );
p_UR.x = _width; //note the p_UR.y is always zero
p_LR.x = _width;
p_LR.y = _height;
p_LL.y = _height; //note the p_LL.x is always zero
break;
case SCALE_SYNC_SIZE:
var contentRect:Rectangle = getContentRect( true ); //must use unscaled points when performing local/global transforms, since this object is scaled
p_UR.x = contentRect.right; //note the p_UR.y is always zero
p_LR.x = contentRect.right;
p_LR.y = contentRect.bottom;
p_LL.y = contentRect.bottom; //note the p_LL.x is always zero
break;
}
return calcOrthogonalBoundingBox(
targetCoordinateSpace.globalToLocal( localToGlobal( EMPTY_POINT ) ),
targetCoordinateSpace.globalToLocal( localToGlobal( p_UR ) ),
targetCoordinateSpace.globalToLocal( localToGlobal( p_LR ) ),
targetCoordinateSpace.globalToLocal( localToGlobal( p_LL ) )
);
}
protected function calcOrthogonalBoundingBox( p0:Point, p1:Point, p2:Point, p3:Point ):Rectangle
{
//Assuming no rotation, points 0 and 3 are most likely to be min_x. This optimization minimizes the likely number of assignments.
//Similar optimizations are in place for max_x, min_y, and max_y, all patterns are ordered 0,1,2,3... starting with the two most likely candidates in the sequence.
var min_x:Number = p3.x;
if (p0.x < min_x) min_x = p0.x;
if (p1.x < min_x) min_x = p1.x;
if (p2.x < min_x) min_x = p2.x;
var max_x:Number = p1.x;
if (p2.x > max_x) max_x = p2.x;
if (p3.x > max_x) max_x = p3.x;
if (p0.x > max_x) max_x = p0.x;
var min_y:Number = p0.y;
if (p1.y < min_y) min_y = p1.y;
if (p2.y < min_y) min_y = p2.y;
if (p3.y < min_y) min_y = p3.y;
var max_y:Number = p2.y;
if (p3.y > max_y) max_y = p3.y;
if (p0.y > max_y) max_y = p0.y;
if (p1.y > max_y) max_y = p1.y;
return new Rectangle( min_x, min_y, max_x - min_x, max_y - min_y );
}
In this framework, particularly in SCALE_NOSYNC_SIZE mode (the default), the width and height are assigned and internally maintained, independently of the scaleX and scaleY values. The internally maintained values are used for performing layout and drawing operations such as backgrounds and borders. In the extremely rare occasion where the actual content needs to be measured, I just use getBounds, such as in the constructor when initializing the "original" size of the clip's content, if it has timeline content, or possibly bitmap methods for masked objects.
These changes have all increased the performance of my framework by an order of magnitude and have virtually eliminated 3rd layout passes, so I'm happy with it. I still wish the Flash runtime would be updated to simply stop rounding these values.
Get ready! An upgraded Adobe Community experience is coming in January.
Learn more