From 0ea099dbda3b54ad66b600c9e238d4e357af939a Mon Sep 17 00:00:00 2001 From: George FunBook Date: Sun, 13 Apr 2025 11:49:12 -0500 Subject: [PATCH 01/12] add abs --- flixel/math/FlxRect.hx | 32 +++++++++++++++++++---- tests/unit/src/flixel/math/FlxRectTest.hx | 30 ++++++++++++++++++++- 2 files changed, 56 insertions(+), 6 deletions(-) diff --git a/flixel/math/FlxRect.hx b/flixel/math/FlxRect.hx index 8c973a377f..1d22e598bf 100644 --- a/flixel/math/FlxRect.hx +++ b/flixel/math/FlxRect.hx @@ -156,6 +156,32 @@ class FlxRect implements IFlxPooled return set(x1, y1, x2 - x1, y2 - y1); } + /** + * Ensures that width and height are positive while covering the same space. For example: + * ```haxe + * rect.set(100, 100, -50, -50).abs(); + * // Is the same as + * rect.set(50, 50, 50, 50); + * ``` + * @since 6.2.0 + */ + public inline function abs() + { + if (width < 0) + { + x += width; + width = -width; + } + + if (height < 0) + { + y += height; + height = -height; + } + + return this; + } + /** * Fills the rectangle so that it has always has a positive width and height. For example: * ```haxe @@ -172,11 +198,7 @@ class FlxRect implements IFlxPooled */ public inline function setAbs(x:Float, y:Float, width:Float, height:Float) { - this.x = width > 0 ? x : x + width; - this.y = height > 0 ? y : y + height; - this.width = width > 0 ? width : -width; - this.height = height > 0 ? height : -height; - return this; + return this.set(x, y, width, height).abs(); } /** diff --git a/tests/unit/src/flixel/math/FlxRectTest.hx b/tests/unit/src/flixel/math/FlxRectTest.hx index 0a7c24d6ac..17aeeee4ce 100644 --- a/tests/unit/src/flixel/math/FlxRectTest.hx +++ b/tests/unit/src/flixel/math/FlxRectTest.hx @@ -152,7 +152,7 @@ class FlxRectTest extends FlxTest } @Test - function testContins() + function testContains() { rect1.set(0, 0, 100, 100); @@ -174,4 +174,32 @@ class FlxRectTest extends FlxTest assertContains(0, 0, 100, 100); assertNotContains(-1, -1, 101, 101); } + + public function testBounds() + { + rect1.set(100, 150, 100, 100); + rect2.setBounds(100, 150, 200, 250); + + FlxAssert.rectsNear(rect1, rect2, 0.0001); + } + + public function testBoundsAbs() + { + rect1.set(100, 150, 100, 100); + rect2.setBoundsAbs(200, 250, 100, 150); + + FlxAssert.rectsNear(rect1, rect2, 0.0001); + } + + public function testAbs() + { + rect1.set(0, 0, 100, 100); + rect2.set(100, 100, -100, -100); + + rect2.abs(); + FlxAssert.rectsNear(rect1, rect2, 0.0001); + + rect2.abs(); + FlxAssert.rectsNear(rect1, rect2, 0.0001); + } } From 963924d10d9d6a005999f7d768aa2aac24db50de Mon Sep 17 00:00:00 2001 From: George FunBook Date: Tue, 15 Apr 2025 13:11:37 -0500 Subject: [PATCH 02/12] add getPixelAt and toSourcePosition --- flixel/graphics/frames/FlxFrame.hx | 82 +++++++++++- .../flixel/graphics/frames/FlxFrameTest.hx | 117 +++++++++++++++++- 2 files changed, 197 insertions(+), 2 deletions(-) diff --git a/flixel/graphics/frames/FlxFrame.hx b/flixel/graphics/frames/FlxFrame.hx index b68ffcf73e..0ade57ef90 100644 --- a/flixel/graphics/frames/FlxFrame.hx +++ b/flixel/graphics/frames/FlxFrame.hx @@ -295,7 +295,87 @@ class FlxFrame implements IFlxDestroyable return rotateAndFlip(mat, rotation, doFlipX, doFlipY); } - + + /** + * Finds the pixel at the position of this frame + * + * @param framePos The position in this frame + * @return The color of the pixel on this frame + * @since 6.2.0 + */ + overload public inline extern function getPixelAt(framePos:FlxPoint):Null + { + final result = getPixelAt(framePos.x, framePos.y); + framePos.putWeak(); + return result; + } + + /** + * Finds the pixel color at the position of this frame + * + * @param frameX The X position in this frame + * @param frameY The Y position in this frame + * @return The color of the pixel on this frame + * @since 6.2.0 + */ + overload public inline extern function getPixelAt(frameX:Float, frameY:Float):Null + { + final sourceX = Std.int(toSourceXHelper(frameX, frameY)); + final sourceY = Std.int(toSourceYHelper(frameX, frameY)); + return parent.bitmap.getPixel32(sourceX, sourceY); + } + + /** + * Takes the given frame position and returns the equivalent position on the source image + * + * @param framePos The position in this frame + * @param result Optional arg for the returning point + */ + public function toSourcePosition(framePos:FlxPoint, ?result:FlxPoint) + { + if (result == null) + result = FlxPoint.get(); + + if (type == FlxFrameType.EMPTY) + return result.set(0, 0); + + final sourceX = Std.int(toSourceXHelper(framePos.x, framePos.y)); + final sourceY = Std.int(toSourceYHelper(framePos.x, framePos.y)); + framePos.putWeak(); + return result.set(sourceX, sourceY); + } + + + function toSourceXHelper(frameX:Float, frameY:Float):Float + { + return frame.x + switch angle + { + // frame.flipX seems to have no effect on tile or blit targets + // TODO: investigate + // case ANGLE_0: flipX ? frame.width - frameX : frameX; + // case ANGLE_90: flipX ? frameY : frame.height - frameY; + // case ANGLE_270: flipX ? frameY : frame.height - frameY; + case ANGLE_0: frameX; + case ANGLE_90: frameY; + case ANGLE_270: frame.height - frameY; + } + } + + function toSourceYHelper(frameX:Float, frameY:Float):Float + { + return frame.y + switch angle + { + // frame.flipX seems to have no effect on tile or blit targets + // TODO: investigate + // case ANGLE_0: flipY ? frame.height - frameY : frameY; + // case ANGLE_90: flipY ? frameX : frame.width - frameX; + // case ANGLE_270: flipY ? frame.width - frameX : frameX; + case ANGLE_0: frameY; + case ANGLE_90: frame.width - frameX; + case ANGLE_270: frameX; + } + } + /** * Draws frame on specified `BitmapData` object. * diff --git a/tests/unit/src/flixel/graphics/frames/FlxFrameTest.hx b/tests/unit/src/flixel/graphics/frames/FlxFrameTest.hx index 1a3bdf8d4d..6a51649cfc 100644 --- a/tests/unit/src/flixel/graphics/frames/FlxFrameTest.hx +++ b/tests/unit/src/flixel/graphics/frames/FlxFrameTest.hx @@ -1,10 +1,13 @@ package flixel.graphics.frames; import flixel.FlxSprite; +import flixel.math.FlxPoint; import flixel.math.FlxRect; +import flixel.util.FlxColor; import haxe.PosInfos; import massive.munit.Assert; import openfl.display.BitmapData; +import openfl.geom.Rectangle; @:access(flixel.graphics.frames.FlxFrame.new) class FlxFrameTest extends FlxTest @@ -96,7 +99,119 @@ class FlxFrameTest extends FlxTest assertOverlaps(99, 99); Assert.isTrue(true); } - + + @Test + function testGetPixelAt() + { + final p = 3; + final bitmap = new BitmapData(p * 2, p * 2, false); + bitmap.fillRect(new Rectangle(0, 0, p, p), FlxColor.RED); + bitmap.fillRect(new Rectangle(p, 0, p, p), FlxColor.GREEN); + bitmap.fillRect(new Rectangle(0, p, p, p), FlxColor.BLUE); + bitmap.fillRect(new Rectangle(p, p, p, p), FlxColor.WHITE); + + final frame = new FlxSprite(0, 0, bitmap).frame; + + function getColorName(color) + { + return switch color + { + case FlxColor.RED: "RED"; + case FlxColor.GREEN: "GREEN"; + case FlxColor.BLUE: "BLUE"; + case FlxColor.WHITE: "WHITE"; + default: color.toHexString(); + } + } + + function assertPixelAt(x:Int, y:Int, expected:FlxColor, ?pos:PosInfos) + { + final actual = frame.getPixelAt((x + 0.5) * p, (y + 0.5) * p); + final msg = 'Pixel($x,$y) was [${getColorName(actual)}], expected [${getColorName(expected)}]'; + Assert.areEqual(expected, actual, msg, pos); + + // test overloads + final actual = frame.getPixelAt(FlxPoint.weak((x + 0.5) * p, (y + 0.5) * p)); + final msg = 'Pixel(P($x,$y)) was [${getColorName(actual)}], expected [${getColorName(expected)}]'; + Assert.areEqual(expected, actual, msg, pos); + } + + + assertPixelAt(0, 0, FlxColor.RED); + assertPixelAt(1, 0, FlxColor.GREEN); + assertPixelAt(0, 1, FlxColor.BLUE); + assertPixelAt(1, 1, FlxColor.WHITE); + + // flipXY shouldn't affect this + frame.flipX = true; + assertPixelAt(0, 0, FlxColor.RED); + assertPixelAt(1, 0, FlxColor.GREEN); + assertPixelAt(0, 1, FlxColor.BLUE); + assertPixelAt(1, 1, FlxColor.WHITE); + + frame.angle = ANGLE_90; + assertPixelAt(0, 0, FlxColor.BLUE); + assertPixelAt(1, 0, FlxColor.RED); + assertPixelAt(0, 1, FlxColor.WHITE); + assertPixelAt(1, 1, FlxColor.GREEN); + + frame.angle = ANGLE_NEG_90; + assertPixelAt(0, 0, FlxColor.GREEN); + assertPixelAt(1, 0, FlxColor.WHITE); + assertPixelAt(0, 1, FlxColor.RED); + assertPixelAt(1, 1, FlxColor.BLUE); + + frame.angle = ANGLE_270; + assertPixelAt(0, 0, FlxColor.GREEN); + assertPixelAt(1, 0, FlxColor.WHITE); + assertPixelAt(0, 1, FlxColor.RED); + assertPixelAt(1, 1, FlxColor.BLUE); + } + + @Test + function testToSourcePos() + { + final frame = createFrames("toSourcePos", 10, 10, 4, 4, 2).pop(); + + function assertSourcePosOf(x:Float, y:Float, expectedX:Float, expectedY:Float, margin = 0.001, ?pos:PosInfos) + { + final actual = frame.toSourcePosition(FlxPoint.weak(x, y)); + FlxAssert.areNear(expectedX, actual.x, margin, 'X Value [${actual.x}] is not within [$margin] of [$expectedX]', pos); + FlxAssert.areNear(expectedY, actual.y, margin, 'Y Value [${actual.y}] is not within [$margin] of [$expectedY]', pos); + } + + assertSourcePosOf(0, 0, 32, 32); + assertSourcePosOf(3, 0, 35, 32); + assertSourcePosOf(0, 3, 32, 35); + assertSourcePosOf(3, 3, 35, 35); + + // flipXY shouldn't affect this + frame.flipX = true; + assertSourcePosOf(0, 0, 32, 32); + assertSourcePosOf(3, 0, 35, 32); + assertSourcePosOf(0, 3, 32, 35); + assertSourcePosOf(3, 3, 35, 35); + frame.flipX = false; + + frame.angle = ANGLE_90; + assertSourcePosOf(0, 0, 32, 38); + assertSourcePosOf(3, 0, 32, 35); + assertSourcePosOf(0, 3, 35, 38); + assertSourcePosOf(3, 3, 35, 35); + + frame.angle = ANGLE_NEG_90; + assertSourcePosOf(0, 0, 38, 32); + assertSourcePosOf(3, 0, 38, 35); + assertSourcePosOf(0, 3, 35, 32); + assertSourcePosOf(3, 3, 35, 35); + + frame.angle = ANGLE_270; + assertSourcePosOf(0, 0, 38, 32); + assertSourcePosOf(3, 0, 38, 35); + assertSourcePosOf(0, 3, 35, 32); + assertSourcePosOf(3, 3, 35, 35); + } + function createFrames(name:String, width = 100, height = 100, cols = 10, rows = 10, buffer = 0):Array { final sprite = new FlxSprite(0, 0); From 2e8f7f3b119e2ba851a7d6bbab2fbdbe41dee845 Mon Sep 17 00:00:00 2001 From: George FunBook Date: Thu, 17 Apr 2025 07:51:38 -0500 Subject: [PATCH 03/12] WIP cliprect to world/view --- flixel/FlxCamera.hx | 175 +++++++++++++++- flixel/FlxObject.hx | 61 ++++++ flixel/FlxSprite.hx | 276 ++++++++++++++++++++++--- tests/unit/src/flixel/FlxSpriteTest.hx | 174 ++++++++++++++++ 4 files changed, 652 insertions(+), 34 deletions(-) diff --git a/flixel/FlxCamera.hx b/flixel/FlxCamera.hx index 6574d832e3..c90a89eac2 100644 --- a/flixel/FlxCamera.hx +++ b/flixel/FlxCamera.hx @@ -1785,7 +1785,180 @@ class FlxCamera extends FlxBasic x = X; y = Y; } - + + /** + * Takes a world position and gives the position it will be displayed in the camera's view + * + * @param worldPos The position in the world + * @param scrollFactorX How much this camera's scroll affects the result, for parallax + * @param scrollFactorY How much this camera's scroll affects the result, for parallax + * @param result Optional arg for the returning point + */ + overload public inline extern function worldToViewPosition(worldPos:FlxPoint, scrollFactorX = 1.0, scrollFactorY = 1.0, ?result:FlxPoint) + { + return worldToViewHelper(worldPos.x, worldPos.y, scrollFactorX, scrollFactorY, result); + } + + /** + * Takes a world position and gives the position it will be displayed in the camera's view + * + * @param worldPos The position in the world + * @param scrollFactor How much this camera's scroll affects the result, for parallax + * @param result Optional arg for the returning point + */ + overload public inline extern function worldToViewPosition(worldPos:FlxPoint, ?scrollFactor:FlxPoint, ?result:FlxPoint) + { + return worldToViewPosition(worldPos.x, worldPos.y, scrollFactor, result); + } + + /** + * Takes a world position and gives the position it will be displayed in the camera's view + * + * @param worldX The position in the world + * @param worldY The position in the world + * @param scrollFactor How much this camera's scroll affects the result, for parallax= + * @param result Optional arg for the returning point + */ + overload public inline extern function worldToViewPosition(worldX:Float, worldY:Float, ?scrollFactor:FlxPoint, ?result:FlxPoint) + { + result = worldToViewHelper(worldX, worldY, scrollFactor != null ? scrollFactor.x : 1.0, scrollFactor != null ? scrollFactor.y : 1.0, result); + scrollFactor.putWeak(); + return result; + } + + /** + * Takes a world position and gives the position it will be displayed in the camera's view + * + * @param worldX The position in the world + * @param worldY The position in the world + * @param scrollFactorX How much this camera's scroll affects the result, for parallax + * @param scrollFactorY How much this camera's scroll affects the result, for parallax + * @param result Optional arg for the returning point + */ + overload public inline extern function worldToViewPosition(worldX:Float, worldY:Float, scrollFactorX = 1.0, scrollFactorY = 1.0, ?result) + { + return worldToViewHelper(worldX, worldY, scrollFactorX, scrollFactorY, result); + } + + function worldToViewHelper(worldX:Float, worldY:Float, scrollFactorX = 1.0, scrollFactorY = 1.0, ?result:FlxPoint):FlxPoint + { + if (result == null) + result = FlxPoint.get(); + + return result.set(worldToViewX(worldX, scrollFactorX), worldToViewY(worldY, scrollFactorY)); + } + + /** + * Takes a world position and gives the position it will be displayed in the camera's view + * + * @param worldX The position in the world + * @param scrollFactor How much this camera's scroll affects the result, for parallax + * @param result Optional arg for the returning point + */ + public function worldToViewX(worldX:Float, scrollFactor = 1.0) + { + return (worldX - (scroll.x * scrollFactor) - viewMarginX) * zoom; + } + + /** + * Takes a world position and gives the position it will be displayed in the camera's view + * + * @param worldY The position in the world + * @param scrollFactor How much this camera's scroll affects the result, for parallax + * @param result Optional arg for the returning point + */ + public function worldToViewY(worldY:Float, scrollFactor = 1.0) + { + return (worldY - (scroll.y * scrollFactor) - viewMarginY) * zoom; + } + + /** + * Takes a position in this camera's view and gives the world position being displayed + * + * @param viewPos The position in this camera's view + * @param scrollFactorX How much this camera's scroll affects the result, for parallax + * @param scrollFactorY How much this camera's scroll affects the result, for parallax + * @param result Optional arg for the returning point + */ + overload public inline extern function viewToWorldPosition(viewPos:FlxPoint, scrollFactorX = 1.0, scrollFactorY = 1.0, ?result:FlxPoint) + { + return viewToWorldHelper(viewPos.x, viewPos.y, scrollFactorX, scrollFactorY, result); + + } + + /** + * Takes a position in this camera's view and gives the world position being displayed + * + * @param viewPos The position in this camera's view + * @param scrollFactor How much this camera's scroll affects the result, for parallax + * @param result Optional arg for the returning point + */ + overload public inline extern function viewToWorldPosition(viewPos:FlxPoint, ?scrollFactor:FlxPoint, ?result:FlxPoint) + { + return viewToWorldPosition(viewPos.x, viewPos.y, scrollFactor, result); + } + + /** + * Takes a position in this camera's view and gives the world position being displayed + * + * @param viewX The position in this camera's view + * @param viewY The position in this camera's view + * @param scrollFactor How much this camera's scroll affects the result, for parallax + * @param result Optional arg for the returning point + */ + overload public inline extern function viewToWorldPosition(viewX:Float, viewY:Float, ?scrollFactor:FlxPoint, ?result:FlxPoint) + { + result = viewToWorldHelper(viewX, viewY, scrollFactor != null ? scrollFactor.x : 1.0, scrollFactor != null ? scrollFactor.y : 1.0, result); + scrollFactor.putWeak(); + return result; + } + + /** + * Takes a position in this camera's view and gives the world position being displayed + * + * @param viewX The position in this camera's view + * @param viewY The position in this camera's view + * @param scrollFactorX How much this camera's scroll affects the result, for parallax + * @param scrollFactorY How much this camera's scroll affects the result, for parallax + * @param result Optional arg for the returning point + */ + overload public inline extern function viewToWorldPosition(viewX:Float, viewY:Float, scrollFactorX = 1.0, scrollFactorY = 1.0, ?result) + { + return worldToViewHelper(viewX, viewY, scrollFactorX, scrollFactorY, result); + } + + function viewToWorldHelper(viewX:Float, viewY:Float, scrollFactorX = 1.0, scrollFactorY = 1.0, ?result:FlxPoint):FlxPoint + { + if (result == null) + result = FlxPoint.get(); + + return result.set(viewToWorldX(viewX, scrollFactorX), viewToWorldY(viewY, scrollFactorY)); + } + + /** + * Takes a position in this camera's view and gives the world position being displayed + * + * @param viewX The position in this camera's view + * @param scrollFactor How much this camera's scroll affects the result, for parallax + * @param result Optional arg for the returning point + */ + public function viewToWorldX(viewX:Float, scrollFactor = 1.0) + { + return (viewX / zoom) + (scroll.x * scrollFactor) + viewMarginX; + } + + /** + * Takes a position in this camera's view and gives the world position being displayed + * + * @param viewY The position in this camera's view + * @param scrollFactor How much this camera's scroll affects the result, for parallax + * @param result Optional arg for the returning point + */ + public function viewToWorldY(viewY:Float, scrollFactor = 1.0) + { + return (viewY / zoom) + (scroll.y * scrollFactor) + viewMarginY; + } + /** * Specify the bounding rectangle of where the camera is allowed to move. * diff --git a/flixel/FlxObject.hx b/flixel/FlxObject.hx index bff324c41a..82104d39cd 100644 --- a/flixel/FlxObject.hx +++ b/flixel/FlxObject.hx @@ -1054,6 +1054,67 @@ class FlxObject extends FlxBasic return result.subtract(camera.scroll.x * scrollFactor.x, camera.scroll.y * scrollFactor.y); } + /** + * Returns the view position of this object + * + * @param result Optional arg for the returning poin + * @param camera The desired "view" coordinate space. If `null`, `getDefaultCamera()` is used + * @return The view position of this objects + * @since 6.2.0 + */ + public function getViewPosition(?camera:FlxCamera, ?result:FlxPoint):FlxPoint + { + if (result == null) + result = FlxPoint.get(); + + if (camera == null) + camera = getDefaultCamera(); + + return result.set(getViewXHelper(camera), getViewYHelper(camera)); + } + + /** + * Returns the view position of this object + * + * @param camera The desired "view" coordinate space. If `null`, `getDefaultCamera()` is used + * @return The view position of this object + * @since 6.2.0 + */ + public function getViewX(?camera:FlxCamera) + { + if (camera == null) + camera = getDefaultCamera(); + + return getViewXHelper(camera); + } + + inline function getViewXHelper(camera:FlxCamera) + { + final x = pixelPerfectPosition ? Math.floor(this.x) : this.x; + return (x - (camera.scroll.x * scrollFactor.x) - camera.viewMarginX) * camera.zoom; + } + + /** + * Returns the view position of this object + * + * @param camera The desired "view" coordinate space. If `null`, `getDefaultCamera()` is used + * @return The view position of this object + * @since 6.2.0 + */ + public function getViewY(?camera:FlxCamera) + { + if (camera == null) + camera = getDefaultCamera(); + + return getViewYHelper(camera); + } + + inline function getViewYHelper(camera:FlxCamera) + { + final y = pixelPerfectPosition ? Math.floor(this.y) : this.y; + return (y - (camera.scroll.y * scrollFactor.y) - camera.viewMarginY) * camera.zoom; + } + /** * Returns the world position of this object. * diff --git a/flixel/FlxSprite.hx b/flixel/FlxSprite.hx index 27cbb5af0c..f9b47f5636 100644 --- a/flixel/FlxSprite.hx +++ b/flixel/FlxSprite.hx @@ -271,8 +271,13 @@ class FlxSprite extends FlxObject public var useColorTransform(default, null):Bool = false; /** - * Clipping rectangle for this sprite. - * Set to `null` to discard graphic frame clipping. + * Clipping rectangle for this sprite's frame. When `null`, the entire + * frame is shown, otherwise `x`, `y`, `width` and `height` determine which portion + * of the frame is shown. Expected values are within (`0`,`0`) and (`frameWidth`,`frameHeight`), + * extending the rect beyond the frame will not extend the graphic. + * + * Fields like position `scale`, `offset`, `angle`, `flipX` and `flipY` have no effect and are + * applied after the frame is clipped. Use `clipToWorldBounds` or `clipToViewBounds` to convert */ public var clipRect(default, set):FlxRect; var _lastClipRect = FlxRect.get(Math.NaN); @@ -726,6 +731,95 @@ class FlxSprite extends FlxObject else if (height <= 0) scale.y = newScaleX; } + + /** + * Sets this sprite's `clipRect` so that, when rendered, + * will be clipped to the given world coordinates. + * + * **NOTE:** Does not work with most angles + */ + overload public inline extern function clipToWorldRect(x:Float, y:Float, width:Float, height:Float) + { + clipToWorldBounds(x, y, x + width, y + height); + } + + /** + * Sets this sprite's `clipRect` so that, when rendered, + * will be clipped to the given screen rectangle. + * + * **NOTE:** Does not work with most angles + */ + overload public inline extern function clipToWorldRect(rect:FlxRect) + { + clipToWorldBounds(rect.x, rect.y, rect.x + rect.width, rect.y + rect.height); + } + /** + * Sets this sprite's `clipRect` so that, when rendered, + * will be clipped to the given world coordinates. + * + * **NOTE:** Does not work with most angles + */ + public function clipToWorldBounds(left:Float, top:Float, right:Float, bottom:Float) + { + if (clipRect == null) + clipRect = new FlxRect(); + + final p1 = FlxPoint.get(left, top); + worldToFramePosition(p1, camera, p1); + final p2 = FlxPoint.get(right, bottom); + worldToFramePosition(p2, camera, p2); + + clipRect.setBoundsAbs(p1.x, p1.y, p2.x, p2.y); + p1.put(); + p2.put(); + } + + /** + * Sets this sprite's `clipRect` so that, when rendered, + * will be clipped to the given screen coordinates. + * + * **NOTE:** Does not work with most angles + */ + overload public inline extern function clipToViewRect(x:Float, y:Float, width:Float, height:Float, ?camera:FlxCamera) + { + clipToViewBounds(x, y, x + width, y + height, camera); + } + + /** + * Sets this sprite's `clipRect` so that, when rendered, will be clipped to the given + * screen rectangle. If `clipRect` is `null` a new instance is created + * + * **NOTE:** `clipRect` is not set to the passed in rect instance + * + * **NOTE:** Does not work with most angles + */ + overload public inline extern function clipToViewRect(rect:FlxRect, ?camera:FlxCamera) + { + clipToViewBounds(rect.left, rect.top, rect.right, rect.bottom, camera); + rect.putWeak(); + } + + /** + * Sets this sprite's `clipRect` so that, when rendered, + * will be clipped to the given screen coordinates. + * + * **NOTE:** Does not work with most angles + */ + public function clipToViewBounds(left:Float, top:Float, right:Float, bottom:Float, ?camera:FlxCamera) + { + if (clipRect == null) + clipRect = new FlxRect(); + + if (camera != null) + camera = getDefaultCamera(); + + final p1 = viewToFramePosition(left, top, camera); + final p2 = viewToFramePosition(right, bottom, camera); + + clipRect.setBoundsAbs(p1.x, p1.y, p2.x, p2.y); + p1.put(); + p2.put(); + } /** * Updates the sprite's hitbox (`width`, `height`, `offset`) according to the current `scale`. @@ -1072,7 +1166,7 @@ class FlxSprite extends FlxObject /** * Determines which of this sprite's pixels are at the specified world coordinate, if any. - * Factors in `scale`, `angle`, `offset`, `origin`, and `scrollFactor`. + * Factors in `scale`, `angle`, `offset`, `origin`, `scrollFactor`, `flipX` and `flipY`. * * @param worldPoint The point in world space * @param camera The camera, used for `scrollFactor`. If `null`, `getDefaultCamera()` is used. @@ -1081,16 +1175,13 @@ class FlxSprite extends FlxObject */ public function getPixelAt(worldPoint:FlxPoint, ?camera:FlxCamera):Null { - transformWorldToPixels(worldPoint, camera, _point); + final point = worldToFramePosition(worldPoint, camera); - // point is inside the graphic - if (_point.x >= 0 && _point.x <= frameWidth && _point.y >= 0 && _point.y <= frameHeight) - { - var frameData:BitmapData = updateFramePixels(); - return frameData.getPixel32(Std.int(_point.x), Std.int(_point.y)); - } + final overlaps = point.x >= 0 && point.x <= frameWidth && point.y >= 0 && point.y <= frameHeight; + final result = overlaps ? frame.getPixelAt(point) : null; - return null; + point.put(); + return result; } /** @@ -1104,27 +1195,27 @@ class FlxSprite extends FlxObject */ public function getPixelAtScreen(screenPoint:FlxPoint, ?camera:FlxCamera):Null { - transformScreenToPixels(screenPoint, camera, _point); + final point = viewToFramePosition(screenPoint, camera); - // point is inside the graphic - if (_point.x >= 0 && _point.x <= frameWidth && _point.y >= 0 && _point.y <= frameHeight) - { - var frameData:BitmapData = updateFramePixels(); - return frameData.getPixel32(Std.int(_point.x), Std.int(_point.y)); - } + final overlaps = point.x >= 0 && point.x <= frameWidth && point.y >= 0 && point.y <= frameHeight; + final result = overlaps ? frame.getPixelAt(point) : null; - return null; + point.put(); + return result; } /** - * Converts the point from world coordinates to this sprite's pixel coordinates where (0,0) - * is the top left of the graphic. + * Converts the point from world coordinates to this sprite's frame coordinates where (0,0) + * is the top left of the frame. * Factors in `scale`, `angle`, `offset`, `origin`, and `scrollFactor`. * + * **Note:** the term "pixels" in this title is a misnomer, this transforms to frame coordinates. + * * @param worldPoint The world coordinates * @param camera The camera, used for `scrollFactor`. If `null`, `getDefaultCamera()` is used * @param result Optional arg for the returning point */ + @:deprecated("transformWorldToPixels is deprecated, use worldToFramePosition") public function transformWorldToPixels(worldPoint:FlxPoint, ?camera:FlxCamera, ?result:FlxPoint):FlxPoint { if (camera == null) @@ -1136,13 +1227,16 @@ class FlxSprite extends FlxObject } /** - * Converts the point from world coordinates to this sprite's pixel coordinates where (0,0) - * is the top left of the graphic. Same as `worldToPixels` but never uses a camera, - * therefore `scrollFactor` is ignored + * Converts the point from world coordinates to this sprite's frame coordinates where (0,0) + * is the top left of the frame. Same as `worldToFramePosition` but never uses a camera, + * therefore `scrollFactor` is ignored. + * + * **Note:** the term "pixels" in this title is a misnomer, this transforms to frame coordinates. * * @param worldPoint The world coordinates. * @param result Optional arg for the returning point */ + @:deprecated("transformWorldToPixelsSimple is deprecated, use worldToFramePositionSimple") public function transformWorldToPixelsSimple(worldPoint:FlxPoint, ?result:FlxPoint):FlxPoint { result = getPosition(result); @@ -1161,19 +1255,22 @@ class FlxSprite extends FlxObject } /** - * Converts the point from screen coordinates to this sprite's pixel coordinates where (0,0) - * is the top left of the graphic. - * Factors in `scale`, `angle`, `offset`, `origin`, and `scrollFactor`. + * Converts the point from screen coordinates to this sprite's frame coordinates where (0,0) + * is the top left of the frame. + * Factors in `scale`, `angle`, `offset`, `origin`, and `scrollFactor` + * + * **Note:** the term "pixels" in this title is a misnomer, this transforms to frame coordinates. * - * @param screenPoint The screen coordinates - * @param camera The desired "screen" space. If `null`, `getDefaultCamera()` is used - * @param result Optional arg for the returning point + * @param screenPos The screen coordinates + * @param camera The desired "screen" space. If `null`, `getDefaultCamera()` is used + * @param result Optional arg for the returning point */ - public function transformScreenToPixels(screenPoint:FlxPoint, ?camera:FlxCamera, ?result:FlxPoint):FlxPoint + @:deprecated("transformScreenToPixels is deprecated, use screenToFramePosition") + public function transformScreenToPixels(screenPos:FlxPoint, ?camera:FlxCamera, ?result:FlxPoint):FlxPoint { result = getScreenPosition(result, camera); - result.subtract(screenPoint.x, screenPoint.y); + result.subtract(screenPos.x, screenPos.y); result.negate(); result.add(offset); result.subtract(origin); @@ -1181,11 +1278,124 @@ class FlxSprite extends FlxObject result.degrees -= angle; result.add(origin); - screenPoint.putWeak(); + screenPos.putWeak(); return result; } + /** + * Converts the point from world coordinates to this sprite's frame coordinates where (0,0) + * is the top left of the frame. Factors in `scale`, `angle`, `offset`, `origin`, + * `scrollFactor`, `flipX` and `flipY`. + * + * @param worldPos The world coordinates + * @param camera The camera, used for `scrollFactor`. If `null`, `getDefaultCamera()` is used + * @param result Optional arg for the returning point + * @since 6.2.0 + */ + public function worldToFramePosition(worldPos:FlxPoint, ?camera:FlxCamera, ?result:FlxPoint):FlxPoint + { + if (camera == null) + camera = getDefaultCamera(); + + // get the screen pos without scrollFactor, then get the world, WITH scrollFactor + final screenPoint = camera.worldToViewPosition(worldPos, 1.0, 1.0, FlxPoint.weak()); + return viewToFramePosition(screenPoint, camera, result); + } + + /** + * Converts the point from world coordinates to this sprite's frame coordinates where (0,0) + * is the top left of the frame. Same as `worldToFrameCoord` but never uses a camera, + * therefore `scrollFactor` is ignored + * + * @param worldPos The world coordinates. + * @param result Optional arg for the returning point + * @since 6.2.0 + */ + public function worldToFramePositionSimple(worldPos:FlxPoint, ?result:FlxPoint):FlxPoint + { + result = getPosition(result); + + result.subtract(worldPos.x, worldPos.y); + worldPos.putWeak(); + result.negate(); + result.add(offset); + result.subtract(origin); + result.scale(1 / scale.x, 1 / scale.y); + result.degrees -= angle; + result.add(origin); + + final animFlipX = animation.curAnim != null && animation.curAnim.flipX; + if (flipX != animFlipX) + result.x = frameWidth - result.x; + + final animFlipY = animation.curAnim != null && animation.curAnim.flipY; + if (flipY != animFlipY) + result.y = frameHeight - result.y; + + + return result; + } + + /** + * Converts the point from camera coordinates to this sprite's frame coordinates where (0,0) + * is the top left of the camera's frame. Factors in `scale`, `angle`, `offset`, `origin`, + * `scrollFactor`, `flipX` and `flipY`. + * + * @param viewPoint The coordinates in the camera's view + * @param camera The desired "screen" space. If `null`, `getDefaultCamera()` is used + * @param result Optional arg for the returning point + * @since 6.2.0 + */ + overload public inline extern function viewToFramePosition(viewPoint:FlxPoint, ?camera:FlxCamera, ?result:FlxPoint):FlxPoint + { + result = viewToFrameHelper(viewPoint.x, viewPoint.y, result); + viewPoint.putWeak(); + return result; + } + + /** + * Converts the point from camera coordinates to this sprite's frame coordinates where (0,0) + * is the top left of the camera's frame. Factors in `scale`, `angle`, `offset`, `origin`, + * `scrollFactor`, `flipX` and `flipY`. + * + * @param viewX The coordinates in the camera's view + * @param camera The desired "screen" space. If `null`, `getDefaultCamera()` is used + * @param result Optional arg for the returning point + * @since 6.2.0 + */ + overload public inline extern function viewToFramePosition(viewX:Float, viewY:Float, ?camera:FlxCamera, ?result:FlxPoint):FlxPoint + { + return viewToFrameHelper(viewX, viewY, camera, result); + } + + function viewToFrameHelper(viewX:Float, viewY:Float, ?camera:FlxCamera, ?result:FlxPoint):FlxPoint + { + if (camera == null) + camera = this.getDefaultCamera(); + + result = camera.viewToWorldPosition(viewX, viewY, scrollFactor, result); + result.subtract(x, y); + // return result; + + // result = getViewPosition(camera, result); + // result.set(viewX - result.x, viewY - result.y); + result.add(offset); + result.subtract(origin); + result.scale(1 / scale.x, 1 / scale.y); + result.degrees -= angle; + result.add(origin); + + final animFlipX = animation.curAnim != null && animation.curAnim.flipX; + if (flipX != animFlipX) + result.x = frameWidth - result.x; + + final animFlipY = animation.curAnim != null && animation.curAnim.flipY; + if (flipY != animFlipY) + result.y = frameHeight - result.y; + + return result; + } /** * Internal function to update the current animation frame. * diff --git a/tests/unit/src/flixel/FlxSpriteTest.hx b/tests/unit/src/flixel/FlxSpriteTest.hx index d58c103b87..02871894c1 100644 --- a/tests/unit/src/flixel/FlxSpriteTest.hx +++ b/tests/unit/src/flixel/FlxSpriteTest.hx @@ -359,6 +359,180 @@ class FlxSpriteTest extends FlxTest FlxAssert.areNear(rect.x + 0.5 * rect.width, actual.x, 0.001, pos); FlxAssert.areNear(rect.y + 0.5 * rect.height, actual.y, 0.001, pos); } + + @Test + function testWorldToFramePosition() + { + sprite1.x = 100; + sprite1.y = 100; + sprite1.makeGraphic(100, 100); + + final worldPos = FlxPoint.get(); + final actual = FlxPoint.get(); + function assertFramePosition(worldX:Float, worldY:Float, expectedX:Float, expectedY:Float, margin = 0.001, ?pos:PosInfos) + { + worldPos.set(worldX, worldY); + sprite1.worldToFramePosition(worldPos, actual); + FlxAssert.areNear(expectedX, actual.x, margin, 'X Value [${actual.x}] is not within [$margin] of [$expectedX]', pos); + FlxAssert.areNear(expectedY, actual.y, margin, 'Y Value [${actual.y}] is not within [$margin] of [$expectedY]', pos); + sprite1.worldToFramePositionSimple(worldPos, actual); + FlxAssert.areNear(expectedX, actual.x, margin, 'Simple X Value [${actual.x}] is not within [$margin] of [$expectedX]', pos); + FlxAssert.areNear(expectedY, actual.y, margin, 'Simple Y Value [${actual.y}] is not within [$margin] of [$expectedY]', pos); + } + + assertFramePosition(100, 100, 0, 0); + assertFramePosition(150, 150, 50, 50); + FlxG.camera.scroll.set(50, 100); + assertFramePosition(100, 100, 0, 0); + assertFramePosition(150, 150, 50, 50); + sprite1.scale.set(2, 2); + assertFramePosition(100, 100, 25, 25); + assertFramePosition(150, 150, 50, 50); + sprite1.angle = 90; + assertFramePosition(100, 100, 25, 75); + assertFramePosition(150, 150, 50, 50); + sprite1.flipX = true; + assertFramePosition(100, 100, 75, 75); + assertFramePosition(150, 150, 50, 50); + sprite1.flipY = true; + assertFramePosition(100, 100, 75, 25); + assertFramePosition(150, 150, 50, 50); + } + + @Test + function testViewToFramePosition() + { + sprite1.x = 100; + sprite1.y = 100; + sprite1.makeGraphic(100, 100); + + final worldPos = FlxPoint.get(); + final actual = FlxPoint.get(); + function assertFramePosition(worldX:Float, worldY:Float, expectedX:Float, expectedY:Float, margin = 0.001, ?pos:PosInfos) + { + worldPos.set(worldX, worldY); + sprite1.viewToFramePosition(worldPos, actual); + FlxAssert.areNear(expectedX, actual.x, margin, 'X Value [${actual.x}] is not within [$margin] of [$expectedX]', pos); + FlxAssert.areNear(expectedY, actual.y, margin, 'Y Value [${actual.y}] is not within [$margin] of [$expectedY]', pos); + } + + assertFramePosition(100, 100, 0, 0); + assertFramePosition(150, 150, 50, 50); + + sprite1.scale.set(2, 2); + assertFramePosition(100, 100, 25, 25); + assertFramePosition(150, 150, 50, 50); + + sprite1.angle = 90; + assertFramePosition(100, 100, 25, 75); + assertFramePosition(150, 150, 50, 50); + + sprite1.flipX = true; + assertFramePosition(100, 100, 75, 75); + assertFramePosition(150, 150, 50, 50); + + sprite1.flipY = true; + assertFramePosition(100, 100, 75, 25); + assertFramePosition(150, 150, 50, 50); + + FlxG.camera.scroll.set(50, 100); + assertFramePosition(100, 100, 25, 50); + assertFramePosition(100, 50, 50, 50); + + FlxG.camera.zoom = 2; + assertFramePosition(100, 100, 25, 50); + } + + @Test + function testClipToWorldBounds() + { + sprite1.x = 100; + sprite1.y = 100; + sprite1.makeGraphic(100, 100); + + final expected = FlxRect.get(); + function assertClipRect(expectedX:Float, expectedY:Float, expectedWidth:Float, expectedHeight:Float, margin = 0.001, ?pos:PosInfos) + { + FlxAssert.rectsNear(expected.set(expectedX, expectedY, expectedWidth, expectedHeight), sprite1.clipRect, margin, pos); + } + + sprite1.clipToWorldBounds(100, 100, 200, 200); + assertClipRect(0, 0, 100, 100); + + sprite1.scale.set(2, 2); + sprite1.clipToWorldBounds(50, 50, 150, 150); + assertClipRect(0, 0, 50, 50); + + sprite1.angle = 90; + sprite1.clipToWorldBounds(50, 50, 150, 150); + assertClipRect(0, 50, 50, 50); + + sprite1.flipX = true; + sprite1.clipToWorldBounds(50, 50, 150, 150); + assertClipRect(50, 50, 50, 50); + + sprite1.flipY = true; + sprite1.clipToWorldBounds(50, 50, 150, 150); + assertClipRect(50, 0, 50, 50); + + FlxG.camera.scroll.set(50, 100); + sprite1.clipToWorldBounds(50, 50, 150, 150); + assertClipRect(50, 0, 50, 50); + + FlxG.camera.zoom = 2; + sprite1.clipToWorldBounds(50, 50, 150, 150); + assertClipRect(50, 0, 50, 50); + } + + @Test + function testClipToViewBounds() + { + sprite1.x = 100; + sprite1.y = 100; + sprite1.makeGraphic(100, 100); + sprite1.camera = FlxG.camera; + + final expected = FlxRect.get(); + function assertClipRect(expectedX:Float, expectedY:Float, expectedWidth:Float, expectedHeight:Float, margin = 0.001, ?pos:PosInfos) + { + FlxAssert.rectsNear(expected.set(expectedX, expectedY, expectedWidth, expectedHeight), sprite1.clipRect, margin, pos); + } + + sprite1.clipToViewBounds(100, 100, 200, 200); + assertClipRect(0, 0, 100, 100); + + sprite1.scale.set(2, 2); + sprite1.clipToViewBounds(50, 50, 150, 150); + assertClipRect(0, 0, 50, 50); + + sprite1.angle = 90; + sprite1.clipToViewBounds(50, 50, 150, 150); + assertClipRect(0, 50, 50, 50); + + sprite1.flipX = true; + sprite1.clipToViewBounds(50, 50, 150, 150); + assertClipRect(50, 50, 50, 50); + + sprite1.flipY = true; + sprite1.clipToViewBounds(50, 50, 150, 150); + assertClipRect(50, 0, 50, 50); + + FlxG.camera.scroll.set(100, 50); + sprite1.clipToViewBounds(50, 50, 150, 150); + assertClipRect(25, 50, 50, 50); + sprite1.clipToViewBounds(40, 40, 150, 150); + assertClipRect(25, 45, 55, 55); + sprite1.clipToViewBounds(30, 30, 150, 150); + assertClipRect(25, 40, 60, 60); + + FlxG.camera.zoom = 4; + sprite1.clipToViewBounds(50, 50, 150, 150); + assertClipRect(25, 50, 50, 50);//TODO: why does zoom have no effect + sprite1.clipToViewBounds(40, 40, 150, 150); + assertClipRect(25, 45, 55, 55); + sprite1.clipToViewBounds(30, 30, 150, 150); + assertClipRect(25, 40, 60, 60); + } } abstract SimplePoint(Array) from Array From 7c9399d13786059a62b4aed2452ac3b40603049c Mon Sep 17 00:00:00 2001 From: GeoKureli-BlackbookPro Date: Thu, 17 Apr 2025 11:57:45 -0500 Subject: [PATCH 04/12] add FlxDestroyUtil.putWeak --- flixel/util/FlxDestroyUtil.hx | 26 ++++++++++++++++++++------ flixel/util/FlxPool.hx | 1 + 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/flixel/util/FlxDestroyUtil.hx b/flixel/util/FlxDestroyUtil.hx index 039fd15117..99391ba6a1 100644 --- a/flixel/util/FlxDestroyUtil.hx +++ b/flixel/util/FlxDestroyUtil.hx @@ -1,9 +1,9 @@ package flixel.util; +import flixel.util.FlxPool.IFlxPooled; import openfl.display.BitmapData; import openfl.display.DisplayObject; import openfl.display.DisplayObjectContainer; -import flixel.util.FlxPool.IFlxPooled; class FlxDestroyUtil { @@ -40,17 +40,31 @@ class FlxDestroyUtil } /** - * Checks if an object is not null before putting it back into the pool, always returns null. + * Checks if an object is not `null` before putting it back into the pool, always returns `null` * - * @param object An IFlxPooled object that will be put back into the pool if it's not null - * @return null + * @param object An `IFlxPooled` object that will be put back into the pool if it's not `null` + * @return `null` */ public static function put(object:IFlxPooled):T { if (object != null) - { object.put(); - } + + return null; + } + + /** + * Checks if an object is not null before calling `putWeak`, always returns `null` + * + * @param object An `IFlxPooled` object that will be put back into the pool if it's not `null` + * @return `null` + * @since 6.2.0 + */ + public static function putWeak(object:IFlxPooled):T + { + if (object != null) + object.putWeak(); + return null; } diff --git a/flixel/util/FlxPool.hx b/flixel/util/FlxPool.hx index c25a8ca880..9fee454690 100644 --- a/flixel/util/FlxPool.hx +++ b/flixel/util/FlxPool.hx @@ -221,6 +221,7 @@ class FlxPool implements IFlxPool interface IFlxPooled extends IFlxDestroyable { function put():Void; + function putWeak():Void; } interface IFlxPool From f434f85e226f65e8d8f70610196420c74a8f7c2f Mon Sep 17 00:00:00 2001 From: GeoKureli-BlackbookPro Date: Thu, 17 Apr 2025 13:40:53 -0500 Subject: [PATCH 05/12] add world, view and game coordinate converters to cameras --- flixel/FlxCamera.hx | 197 ++++++++++++++++++++++--- tests/unit/src/flixel/FlxCameraTest.hx | 149 +++++++++++++++++++ 2 files changed, 324 insertions(+), 22 deletions(-) diff --git a/flixel/FlxCamera.hx b/flixel/FlxCamera.hx index c90a89eac2..24709fe3d3 100644 --- a/flixel/FlxCamera.hx +++ b/flixel/FlxCamera.hx @@ -1,13 +1,5 @@ package flixel; -import openfl.display.Bitmap; -import openfl.display.BitmapData; -import openfl.display.DisplayObject; -import openfl.display.Graphics; -import openfl.display.Sprite; -import openfl.geom.ColorTransform; -import openfl.geom.Point; -import openfl.geom.Rectangle; import flixel.graphics.FlxGraphic; import flixel.graphics.frames.FlxFrame; import flixel.graphics.tile.FlxDrawBaseItem; @@ -22,8 +14,16 @@ import flixel.util.FlxColor; import flixel.util.FlxDestroyUtil; import flixel.util.FlxSpriteUtil; import openfl.Vector; +import openfl.display.Bitmap; +import openfl.display.BitmapData; import openfl.display.BlendMode; +import openfl.display.DisplayObject; +import openfl.display.Graphics; +import openfl.display.Sprite; import openfl.filters.BitmapFilter; +import openfl.geom.ColorTransform; +import openfl.geom.Point; +import openfl.geom.Rectangle; using flixel.util.FlxColorTransformUtil; @@ -1786,6 +1786,22 @@ class FlxCamera extends FlxBasic y = Y; } + /** + * Helper for coordinate converters + */ + static inline function safeGetX(p:FlxPoint, backup:Float) + { + return p == null ? backup : p.x; + } + + /** + * Helper for coordinate converters + */ + static inline function safeGetY(p:FlxPoint, backup:Float) + { + return p == null ? backup : p.y; + } + /** * Takes a world position and gives the position it will be displayed in the camera's view * @@ -1793,10 +1809,13 @@ class FlxCamera extends FlxBasic * @param scrollFactorX How much this camera's scroll affects the result, for parallax * @param scrollFactorY How much this camera's scroll affects the result, for parallax * @param result Optional arg for the returning point + * @since 6.2.0 */ overload public inline extern function worldToViewPosition(worldPos:FlxPoint, scrollFactorX = 1.0, scrollFactorY = 1.0, ?result:FlxPoint) { - return worldToViewHelper(worldPos.x, worldPos.y, scrollFactorX, scrollFactorY, result); + result = worldToViewHelper(worldPos.x, worldPos.y, scrollFactorX, scrollFactorY, result); + worldPos.putWeak(); + return result; } /** @@ -1805,10 +1824,14 @@ class FlxCamera extends FlxBasic * @param worldPos The position in the world * @param scrollFactor How much this camera's scroll affects the result, for parallax * @param result Optional arg for the returning point + * @since 6.2.0 */ overload public inline extern function worldToViewPosition(worldPos:FlxPoint, ?scrollFactor:FlxPoint, ?result:FlxPoint) { - return worldToViewPosition(worldPos.x, worldPos.y, scrollFactor, result); + result = worldToViewHelper(worldPos.x, worldPos.y, safeGetX(scrollFactor, 1.0), safeGetY(scrollFactor, 1.0), result); + worldPos.putWeak(); + FlxDestroyUtil.putWeak(scrollFactor); + return result; } /** @@ -1818,11 +1841,12 @@ class FlxCamera extends FlxBasic * @param worldY The position in the world * @param scrollFactor How much this camera's scroll affects the result, for parallax= * @param result Optional arg for the returning point + * @since 6.2.0 */ overload public inline extern function worldToViewPosition(worldX:Float, worldY:Float, ?scrollFactor:FlxPoint, ?result:FlxPoint) { - result = worldToViewHelper(worldX, worldY, scrollFactor != null ? scrollFactor.x : 1.0, scrollFactor != null ? scrollFactor.y : 1.0, result); - scrollFactor.putWeak(); + result = worldToViewHelper(worldX, worldY, safeGetX(scrollFactor, 1.0), safeGetY(scrollFactor, 1.0), result); + FlxDestroyUtil.putWeak(scrollFactor); return result; } @@ -1834,6 +1858,7 @@ class FlxCamera extends FlxBasic * @param scrollFactorX How much this camera's scroll affects the result, for parallax * @param scrollFactorY How much this camera's scroll affects the result, for parallax * @param result Optional arg for the returning point + * @since 6.2.0 */ overload public inline extern function worldToViewPosition(worldX:Float, worldY:Float, scrollFactorX = 1.0, scrollFactorY = 1.0, ?result) { @@ -1854,10 +1879,11 @@ class FlxCamera extends FlxBasic * @param worldX The position in the world * @param scrollFactor How much this camera's scroll affects the result, for parallax * @param result Optional arg for the returning point + * @since 6.2.0 */ public function worldToViewX(worldX:Float, scrollFactor = 1.0) { - return (worldX - (scroll.x * scrollFactor) - viewMarginX) * zoom; + return worldX - (scroll.x * scrollFactor) - viewMarginX; } /** @@ -1866,10 +1892,11 @@ class FlxCamera extends FlxBasic * @param worldY The position in the world * @param scrollFactor How much this camera's scroll affects the result, for parallax * @param result Optional arg for the returning point + * @since 6.2.0 */ public function worldToViewY(worldY:Float, scrollFactor = 1.0) { - return (worldY - (scroll.y * scrollFactor) - viewMarginY) * zoom; + return worldY - (scroll.y * scrollFactor) - viewMarginY; } /** @@ -1879,11 +1906,13 @@ class FlxCamera extends FlxBasic * @param scrollFactorX How much this camera's scroll affects the result, for parallax * @param scrollFactorY How much this camera's scroll affects the result, for parallax * @param result Optional arg for the returning point + * @since 6.2.0 */ overload public inline extern function viewToWorldPosition(viewPos:FlxPoint, scrollFactorX = 1.0, scrollFactorY = 1.0, ?result:FlxPoint) { - return viewToWorldHelper(viewPos.x, viewPos.y, scrollFactorX, scrollFactorY, result); - + result = viewToWorldHelper(viewPos.x, viewPos.y, scrollFactorX, scrollFactorY, result); + viewPos.putWeak(); + return result; } /** @@ -1892,10 +1921,14 @@ class FlxCamera extends FlxBasic * @param viewPos The position in this camera's view * @param scrollFactor How much this camera's scroll affects the result, for parallax * @param result Optional arg for the returning point + * @since 6.2.0 */ overload public inline extern function viewToWorldPosition(viewPos:FlxPoint, ?scrollFactor:FlxPoint, ?result:FlxPoint) { - return viewToWorldPosition(viewPos.x, viewPos.y, scrollFactor, result); + result = viewToWorldHelper(viewPos.x, viewPos.y, safeGetX(scrollFactor, 1.0), safeGetY(scrollFactor, 1.0), result); + viewPos.putWeak(); + FlxDestroyUtil.putWeak(scrollFactor); + return result; } /** @@ -1905,11 +1938,12 @@ class FlxCamera extends FlxBasic * @param viewY The position in this camera's view * @param scrollFactor How much this camera's scroll affects the result, for parallax * @param result Optional arg for the returning point + * @since 6.2.0 */ overload public inline extern function viewToWorldPosition(viewX:Float, viewY:Float, ?scrollFactor:FlxPoint, ?result:FlxPoint) { - result = viewToWorldHelper(viewX, viewY, scrollFactor != null ? scrollFactor.x : 1.0, scrollFactor != null ? scrollFactor.y : 1.0, result); - scrollFactor.putWeak(); + result = viewToWorldHelper(viewX, viewY, safeGetX(scrollFactor, 1.0), safeGetY(scrollFactor, 1.0), result); + FlxDestroyUtil.putWeak(scrollFactor); return result; } @@ -1921,10 +1955,11 @@ class FlxCamera extends FlxBasic * @param scrollFactorX How much this camera's scroll affects the result, for parallax * @param scrollFactorY How much this camera's scroll affects the result, for parallax * @param result Optional arg for the returning point + * @since 6.2.0 */ overload public inline extern function viewToWorldPosition(viewX:Float, viewY:Float, scrollFactorX = 1.0, scrollFactorY = 1.0, ?result) { - return worldToViewHelper(viewX, viewY, scrollFactorX, scrollFactorY, result); + return viewToWorldHelper(viewX, viewY, scrollFactorX, scrollFactorY, result); } function viewToWorldHelper(viewX:Float, viewY:Float, scrollFactorX = 1.0, scrollFactorY = 1.0, ?result:FlxPoint):FlxPoint @@ -1941,10 +1976,11 @@ class FlxCamera extends FlxBasic * @param viewX The position in this camera's view * @param scrollFactor How much this camera's scroll affects the result, for parallax * @param result Optional arg for the returning point + * @since 6.2.0 */ public function viewToWorldX(viewX:Float, scrollFactor = 1.0) { - return (viewX / zoom) + (scroll.x * scrollFactor) + viewMarginX; + return viewX + (scroll.x * scrollFactor) + viewMarginX; } /** @@ -1953,10 +1989,127 @@ class FlxCamera extends FlxBasic * @param viewY The position in this camera's view * @param scrollFactor How much this camera's scroll affects the result, for parallax * @param result Optional arg for the returning point + * @since 6.2.0 */ public function viewToWorldY(viewY:Float, scrollFactor = 1.0) { - return (viewY / zoom) + (scroll.y * scrollFactor) + viewMarginY; + return viewY + (scroll.y * scrollFactor) + viewMarginY; + } + + /** + * Takes a position in the `FlxGame` and gives the corresponding position in this camera's view + * + * @param gamePos The position in the `FlxGame` + * @param result Optional arg for the returning point + * @since 6.2.0 + */ + overload public inline extern function gameToViewPosition(gamePos:FlxPoint, ?result) + { + return gameToViewHelper(gamePos.x, gamePos.y, result); + } + + /** + * Takes a position in the `FlxGame` and gives the corresponding position in this camera's view + * + * @param gameX The position in the `FlxGame` + * @param gameY The position in the `FlxGame` + * @param result Optional arg for the returning point + * @since 6.2.0 + */ + overload public inline extern function gameToViewPosition(gameX:Float, gameY:Float, ?result:FlxPoint) + { + return gameToViewHelper(gameX, gameY, result); + } + + function gameToViewHelper(gameX:Float, gameY:Float, ?result:FlxPoint):FlxPoint + { + if (result == null) + result = FlxPoint.get(); + + return result.set(gameToViewX(gameX), gameToViewY(gameY)); + } + + /** + * Takes a position in the `FlxGame` and gives the corresponding position in this camera's view + * + * @param gameX The position in the `FlxGame` + * @param result Optional arg for the returning point + * @since 6.2.0 + */ + public function gameToViewX(gameX:Float) + { + return (gameX - x) / zoom; + } + + /** + * Takes a position in the `FlxGame` and gives the corresponding position in this camera's view + * + * @param gameY The position in the `FlxGame` + * @param result Optional arg for the returning point + * @since 6.2.0 + */ + public function gameToViewY(gameY:Float) + { + return (gameY - y) / zoom; + } + + /** + * Takes a position in this camera's view and gives the corresponding position in the `FlxGame` + * + * @param viewPos The position in this camera's view + * @param result Optional arg for the returning point + * @since 6.2.0 + */ + overload public inline extern function viewToGamePosition(viewPos:FlxPoint, ?result) + { + return viewToGameHelper(viewPos.x, viewPos.y, result); + } + + /** + * Takes a position in this camera's view and gives the corresponding position in the `FlxGame` + * + * @param viewX The position in this camera's view + * @param viewY The position in this camera's view + * @param result Optional arg for the returning point + * @since 6.2.0 + */ + overload public inline extern function viewToGamePosition(viewX:Float, viewY:Float, ?result:FlxPoint) + { + return viewToGameHelper(viewX, viewY, result); + } + + function viewToGameHelper(viewX:Float, viewY:Float, ?result:FlxPoint):FlxPoint + { + if (result == null) + result = FlxPoint.get(); + + return result.set(viewToGameX(viewX), viewToGameY(viewY)); + } + + /** + * Takes a position in this camera's view and gives the corresponding position in the `FlxGame` + * + * @param viewX The position in this camera's view + * @param result Optional arg for the returning point + * @since 6.2.0 + */ + public function viewToGameX(viewX:Float) + { + // return (viewX - x) / zoom; + return viewX * zoom + this.x; + } + + /** + * Takes a position in this camera's view and gives the corresponding position in the `FlxGame` + * + * @param viewY The position in this camera's view + * @param result Optional arg for the returning point + * @since 6.2.0 + */ + public function viewToGameY(viewY:Float) + { + // return (viewY - y) / zoom; + return viewY * zoom + this.y; } /** diff --git a/tests/unit/src/flixel/FlxCameraTest.hx b/tests/unit/src/flixel/FlxCameraTest.hx index 85459f6094..36060c21b9 100644 --- a/tests/unit/src/flixel/FlxCameraTest.hx +++ b/tests/unit/src/flixel/FlxCameraTest.hx @@ -1,6 +1,9 @@ package flixel; +import flixel.FlxSprite; +import flixel.math.FlxPoint; import flixel.util.FlxColor; +import haxe.PosInfos; import massive.munit.Assert; @:access(flixel.system.frontEnds.CameraFrontEnd) @@ -145,4 +148,150 @@ class FlxCameraTest extends FlxTest { FlxG.camera.fade(FlxColor.BLACK, 0.05, fadeIn, onComplete, force); } + + @Test + function testCoordinateConverters() + { + Assert.areEqual(camera.width, 640); + Assert.areEqual(camera.height, 480); + + final world = FlxPoint.get(); + final view = FlxPoint.get(); + final game = FlxPoint.get(); + + #if FLX_POINT_POOL + // track leaked points + @:privateAccess + final pointPool = FlxBasePoint.pool; + pointPool.preAllocate(1); + final startingPoolLength = pointPool.length; + #end + + function assertWorldToView(worldX:Float, worldY:Float, expectedX:Float, expectedY:Float, margin = 0.001, ?posInfo:PosInfos) + { + function getViewMsg(prefix:String) + { + return '[$prefix] - ViewPos [$view] is not within [$margin] of [( x:$expectedX | y:$expectedY )]'; + } + + function getWorldMsg(prefix:String) + { + return '[$prefix] - WorldPos [$world] is not within [$margin] of [( x:$worldX | y:$worldY )]'; + } + + world.set(worldX, worldY); + // test overload (point, point) + camera.worldToViewPosition(world, null, view); + FlxAssert.pointNearXY(expectedX, expectedY, view, margin, getViewMsg('p,p'), posInfo); + + camera.viewToWorldPosition(view, null, world); + FlxAssert.pointNearXY(worldX, worldY, world, margin, getWorldMsg('p,p'), posInfo); + + // test overload (point, x,y) + camera.worldToViewPosition(world, 1.0, 1.0, view); + FlxAssert.pointNearXY(expectedX, expectedY, view, margin, getViewMsg('p,xy'), posInfo); + + camera.viewToWorldPosition(view, 1.0, 1.0, world); + FlxAssert.pointNearXY(worldX, worldY, world, margin, getWorldMsg('p,xy'), posInfo); + + // test overload (x,y, x,y) + camera.worldToViewPosition(worldX, worldY, 1.0, 1.0, view); + FlxAssert.pointNearXY(expectedX, expectedY, view, margin, getViewMsg('xy,xy'), posInfo); + + camera.viewToWorldPosition(view.x, view.y, 1.0, 1.0, world); + FlxAssert.pointNearXY(worldX, worldY, world, margin, getWorldMsg('xy,xy'), posInfo); + + // test overload (x,y, point) + camera.worldToViewPosition(worldX, worldY, null, view); + FlxAssert.pointNearXY(expectedX, expectedY, view, margin, getViewMsg('xy,p'), posInfo); + + camera.viewToWorldPosition(view.x, view.y, null, world); + FlxAssert.pointNearXY(worldX, worldY, world, margin, getWorldMsg('xy,p'), posInfo); + + // test view to game and back + function getGameMsg(prefix:String) + { + return '[$prefix] - Game<->View [$view] is not within [$margin] of [( x:$expectedX | y:$expectedY )]'; + } + + camera.viewToGamePosition(view.x, view.y, game); + camera.gameToViewPosition(game.x, game.y, view); + FlxAssert.pointNearXY(expectedX, expectedY, view, margin, getGameMsg('xy'), posInfo); + + camera.viewToGamePosition(view, game); + camera.gameToViewPosition(game, view); + FlxAssert.pointNearXY(expectedX, expectedY, view, margin, getGameMsg('p'), posInfo); + } + + assertWorldToView(320, 240, 320, 240); + + camera.zoom = 2.0; + assertWorldToView(320, 240, 160, 120); + + camera.scroll.set(5, 10); + assertWorldToView(320, 240, 155, 110); + + camera.zoom = 1.0; + assertWorldToView(320, 240, 315, 230); + + // test view to game + FlxAssert.pointNearXY(320, 240, camera.viewToGamePosition(320, 240, game)); + + camera.x += 10; + camera.y += 20; + + FlxAssert.pointNearXY(330, 260, camera.viewToGamePosition(320, 240, game)); + + camera.zoom = 2.0; + FlxAssert.pointNearXY(650, 500, camera.viewToGamePosition(320, 240, game)); + + #if FLX_POINT_POOL + Assert.areEqual(pointPool.length, startingPoolLength); + #end + } + + @Test + function testCoordinateConvertersNullResult() + { + // Test that a new point is returned when a result is not supplied (A common dev error) + try + { + final result = camera.viewToWorldPosition(0, 0); + Assert.areEqual(0, result.x); + } + catch(e) + { + Assert.fail('Exception thrown from "viewToWorldPosition", message: "${e.message}", stack:\n${e.stack}'); + } + + try + { + final result = camera.worldToViewPosition(0, 0); + Assert.areEqual(0, result.x); + } + catch(e) + { + Assert.fail('Exception thrown from "worldToViewPosition", message: "${e.message}", stack:\n${e.stack}'); + } + + try + { + final result = camera.gameToViewPosition(0, 0); + Assert.areEqual(0, result.x); + } + catch(e) + { + Assert.fail('Exception thrown from "gameToViewPosition", message: "${e.message}", stack:\n${e.stack}'); + } + + try + { + final result = camera.viewToGamePosition(0, 0); + Assert.areEqual(0, result.x); + } + catch(e) + { + Assert.fail('Exception thrown from "viewToGamePosition", message: "${e.message}", stack:\n${e.stack}'); + } + } } \ No newline at end of file From e424640bc86fdb47bf202778ca0a1302df19ee9a Mon Sep 17 00:00:00 2001 From: GeoKureli-BlackbookPro Date: Thu, 17 Apr 2025 13:56:52 -0500 Subject: [PATCH 06/12] use new converters in pointers --- flixel/input/FlxPointer.hx | 13 +++--------- tests/unit/src/flixel/input/FlxPointerTest.hx | 21 ++++++++++--------- 2 files changed, 14 insertions(+), 20 deletions(-) diff --git a/flixel/input/FlxPointer.hx b/flixel/input/FlxPointer.hx index efe7578a6a..4fc8030cc2 100644 --- a/flixel/input/FlxPointer.hx +++ b/flixel/input/FlxPointer.hx @@ -74,9 +74,8 @@ class FlxPointer if (camera == null) camera = FlxG.camera; - result = getViewPosition(camera, result); - result.addPoint(camera.scroll); - return result; + final p = getViewPosition(camera, FlxPoint.weak()); + return camera.viewToWorldPosition(p, 1, 1, result); } /** @@ -111,13 +110,7 @@ class FlxPointer if (camera == null) camera = FlxG.camera; - if (result == null) - result = FlxPoint.get(); - - result.x = Std.int((gameX - camera.x) / camera.zoom + camera.viewMarginX); - result.y = Std.int((gameY - camera.y) / camera.zoom + camera.viewMarginY); - - return result; + return camera.gameToViewPosition(gameX, gameY, result); } /** diff --git a/tests/unit/src/flixel/input/FlxPointerTest.hx b/tests/unit/src/flixel/input/FlxPointerTest.hx index 81aed0c418..da74c7ed9a 100644 --- a/tests/unit/src/flixel/input/FlxPointerTest.hx +++ b/tests/unit/src/flixel/input/FlxPointerTest.hx @@ -1,7 +1,7 @@ package flixel.input; -import flixel.math.FlxPoint; import flixel.input.FlxPointer; +import flixel.math.FlxPoint; import massive.munit.Assert; class FlxPointerTest @@ -59,13 +59,13 @@ class FlxPointerTest Assert.areEqual(120, FlxG.camera.viewMarginY);// 480/4 pointer.setRawPositionUnsafe(50 * FlxG.scaleMode.scale.x, 50 * FlxG.scaleMode.scale.y); - FlxAssert.pointsEqual(p( 50, 50), pointer.getGamePosition (result)); // (50, 50) - FlxAssert.pointsEqual(p(155, 139), pointer.getViewPosition (result)); // ((50, 50) - (20, 12)) / 2 + (140, 120) - FlxAssert.pointsEqual(p(150, 124), pointer.getWorldPosition (result)); // ((50, 50) - (20, 12)) / 2 + (140, 120) - (-5, -15) + FlxAssert.pointsEqual(p( 50, 50), pointer.getGamePosition (result)); + FlxAssert.pointsEqual(p( 15, 19), pointer.getViewPosition (result)); + FlxAssert.pointsEqual(p(150, 124), pointer.getWorldPosition (result)); Assert.areEqual( 50, pointer.gameX); Assert.areEqual( 50, pointer.gameY); - Assert.areEqual(155, pointer.viewX); - Assert.areEqual(139, pointer.viewY); + Assert.areEqual( 15, pointer.viewX); + Assert.areEqual( 19, pointer.viewY); Assert.areEqual(150, pointer.x); Assert.areEqual(124, pointer.y); } @@ -73,6 +73,7 @@ class FlxPointerTest @Test function testNullResult() { + // Test that a new point is returned when a result is not supplied (A common dev error) try { final result = pointer.getPosition(); @@ -80,7 +81,7 @@ class FlxPointerTest } catch(e) { - Assert.fail('Exception thrown from "getPosition", message: "${e.message}"'); + Assert.fail('Exception thrown from "getPosition", message: "${e.message}", stack:\n${e.stack}'); } try @@ -90,7 +91,7 @@ class FlxPointerTest } catch(e) { - Assert.fail('Exception thrown from "getWorldPosition", message: "${e.message}"'); + Assert.fail('Exception thrown from "getWorldPosition", message: "${e.message}", stack:\n${e.stack}'); } try @@ -100,7 +101,7 @@ class FlxPointerTest } catch(e) { - Assert.fail('Exception thrown from "getViewPosition", message: "${e.message}"'); + Assert.fail('Exception thrown from "getViewPosition", message: "${e.message}", stack:\n${e.stack}'); } try @@ -110,7 +111,7 @@ class FlxPointerTest } catch(e) { - Assert.fail('Exception thrown from "getGamePosition", message: "${e.message}"'); + Assert.fail('Exception thrown from "getGamePosition", message: "${e.message}", stack:\n${e.stack}'); } } } From 91a7753f32acdeeecb8973ac88bb418f7685db91 Mon Sep 17 00:00:00 2001 From: GeoKureli-BlackbookPro Date: Thu, 17 Apr 2025 15:56:33 -0500 Subject: [PATCH 07/12] fix errors in clipToViewRect andthe like --- flixel/FlxSprite.hx | 123 ++++++++++++++++++--- tests/unit/src/flixel/FlxCameraTest.hx | 4 +- tests/unit/src/flixel/FlxSpriteTest.hx | 145 ++++++++++++++++++++++--- 3 files changed, 240 insertions(+), 32 deletions(-) diff --git a/flixel/FlxSprite.hx b/flixel/FlxSprite.hx index f9b47f5636..8a4f112676 100644 --- a/flixel/FlxSprite.hx +++ b/flixel/FlxSprite.hx @@ -737,6 +737,7 @@ class FlxSprite extends FlxObject * will be clipped to the given world coordinates. * * **NOTE:** Does not work with most angles + * @since 6.2.0 */ overload public inline extern function clipToWorldRect(x:Float, y:Float, width:Float, height:Float) { @@ -748,26 +749,74 @@ class FlxSprite extends FlxObject * will be clipped to the given screen rectangle. * * **NOTE:** Does not work with most angles + * @since 6.2.0 */ overload public inline extern function clipToWorldRect(rect:FlxRect) { clipToWorldBounds(rect.x, rect.y, rect.x + rect.width, rect.y + rect.height); } + /** * Sets this sprite's `clipRect` so that, when rendered, * will be clipped to the given world coordinates. * * **NOTE:** Does not work with most angles + * @since 6.2.0 */ public function clipToWorldBounds(left:Float, top:Float, right:Float, bottom:Float) { if (clipRect == null) clipRect = new FlxRect(); - final p1 = FlxPoint.get(left, top); - worldToFramePosition(p1, camera, p1); - final p2 = FlxPoint.get(right, bottom); - worldToFramePosition(p2, camera, p2); + final p1 = worldToFramePosition(left, top); + final p2 = worldToFramePosition(right, bottom); + + clipRect.setBoundsAbs(p1.x, p1.y, p2.x, p2.y); + p1.put(); + p2.put(); + } + + /** + * Sets this sprite's `clipRect` so that, when rendered, will be clipped to the given + * world coordinates. Same as `clipToWorldBounds` but never uses a camera, therefore + * `scrollFactor` is ignored + * + * **NOTE:** Does not work with most angles + * @since 6.2.0 + */ + overload public inline extern function clipToWorldRectSimple(x:Float, y:Float, width:Float, height:Float) + { + clipToWorldBoundsSimple(x, y, x + width, y + height); + } + + /** + * Sets this sprite's `clipRect` so that, when rendered, will be clipped to the given + * world coordinates. Same as `clipToWorldBounds` but never uses a camera, therefore + * `scrollFactor` is ignored + * + * **NOTE:** Does not work with most angles + * @since 6.2.0 + */ + overload public inline extern function clipToWorldRectSimple(rect:FlxRect) + { + clipToWorldBoundsSimple(rect.x, rect.y, rect.x + rect.width, rect.y + rect.height); + } + + /** + * Sets this sprite's `clipRect` so that, when rendered, will be clipped to the given + * world coordinates. Same as `clipToWorldBounds` but never uses a camera, therefore + * `scrollFactor` is ignored + * + * **NOTE:** Does not work with most angles + * @since 6.2.0 + */ + public function clipToWorldBoundsSimple(left:Float, top:Float, right:Float, bottom:Float) + { + if (clipRect == null) + clipRect = new FlxRect(); + + final p1 = worldToFrameSimpleHelper(left, top); + final p2 = worldToFrameSimpleHelper(right, bottom); clipRect.setBoundsAbs(p1.x, p1.y, p2.x, p2.y); p1.put(); @@ -779,6 +828,7 @@ class FlxSprite extends FlxObject * will be clipped to the given screen coordinates. * * **NOTE:** Does not work with most angles + * @since 6.2.0 */ overload public inline extern function clipToViewRect(x:Float, y:Float, width:Float, height:Float, ?camera:FlxCamera) { @@ -792,6 +842,7 @@ class FlxSprite extends FlxObject * **NOTE:** `clipRect` is not set to the passed in rect instance * * **NOTE:** Does not work with most angles + * @since 6.2.0 */ overload public inline extern function clipToViewRect(rect:FlxRect, ?camera:FlxCamera) { @@ -804,6 +855,7 @@ class FlxSprite extends FlxObject * will be clipped to the given screen coordinates. * * **NOTE:** Does not work with most angles + * @since 6.2.0 */ public function clipToViewBounds(left:Float, top:Float, right:Float, bottom:Float, ?camera:FlxCamera) { @@ -1293,14 +1345,36 @@ class FlxSprite extends FlxObject * @param result Optional arg for the returning point * @since 6.2.0 */ - public function worldToFramePosition(worldPos:FlxPoint, ?camera:FlxCamera, ?result:FlxPoint):FlxPoint + overload public inline extern function worldToFramePosition(worldPos:FlxPoint, ?camera:FlxCamera, ?result:FlxPoint):FlxPoint + { + result = worldToFrameHelper(worldPos.x, worldPos.y, camera, result); + worldPos.putWeak(); + return result; + } + + /** + * Converts the point from world coordinates to this sprite's frame coordinates where (0,0) + * is the top left of the frame. Factors in `scale`, `angle`, `offset`, `origin`, + * `scrollFactor`, `flipX` and `flipY`. + * + * @param worldX The world coordinates + * @param worldY The world coordinates + * @param camera The camera, used for `scrollFactor`. If `null`, `getDefaultCamera()` is used + * @param result Optional arg for the returning point + * @since 6.2.0 + */ + overload public inline extern function worldToFramePosition(worldX:Float, worldY:Float, ?camera:FlxCamera, ?result:FlxPoint):FlxPoint + { + return worldToFrameHelper(worldX, worldY, camera, result); + } + + function worldToFrameHelper(worldX:Float, worldY:Float, ?camera:FlxCamera, ?result:FlxPoint):FlxPoint { if (camera == null) camera = getDefaultCamera(); // get the screen pos without scrollFactor, then get the world, WITH scrollFactor - final screenPoint = camera.worldToViewPosition(worldPos, 1.0, 1.0, FlxPoint.weak()); - return viewToFramePosition(screenPoint, camera, result); + return viewToFrameHelper(camera.worldToViewX(worldX), camera.worldToViewY(worldX), camera, result); } /** @@ -1312,13 +1386,34 @@ class FlxSprite extends FlxObject * @param result Optional arg for the returning point * @since 6.2.0 */ - public function worldToFramePositionSimple(worldPos:FlxPoint, ?result:FlxPoint):FlxPoint + overload public inline extern function worldToFramePositionSimple(worldPos:FlxPoint, ?result:FlxPoint):FlxPoint { - result = getPosition(result); - - result.subtract(worldPos.x, worldPos.y); + result = worldToFrameSimpleHelper(worldPos.x, worldPos.y, result); worldPos.putWeak(); - result.negate(); + return result; + } + + /** + * Converts the point from world coordinates to this sprite's frame coordinates where (0,0) + * is the top left of the frame. Same as `worldToFrameCoord` but never uses a camera, + * therefore `scrollFactor` is ignored + * + * @param worldX The world coordinates. + * @param worldY The world coordinates. + * @param result Optional arg for the returning point + * @since 6.2.0 + */ + overload public inline extern function worldToFramePositionSimple(worldX:Float, worldY:Float, ?result:FlxPoint):FlxPoint + { + return worldToFrameSimpleHelper(worldX, worldY, result); + } + + function worldToFrameSimpleHelper(worldX:Float, worldY:Float, ?result:FlxPoint):FlxPoint + { + if (result == null) + result = FlxPoint.get(); + + result.set(worldX - x, worldY - y); result.add(offset); result.subtract(origin); result.scale(1 / scale.x, 1 / scale.y); @@ -1333,7 +1428,6 @@ class FlxSprite extends FlxObject if (flipY != animFlipY) result.y = frameHeight - result.y; - return result; } @@ -1349,7 +1443,7 @@ class FlxSprite extends FlxObject */ overload public inline extern function viewToFramePosition(viewPoint:FlxPoint, ?camera:FlxCamera, ?result:FlxPoint):FlxPoint { - result = viewToFrameHelper(viewPoint.x, viewPoint.y, result); + result = viewToFrameHelper(viewPoint.x, viewPoint.y, camera, result); viewPoint.putWeak(); return result; } @@ -1360,6 +1454,7 @@ class FlxSprite extends FlxObject * `scrollFactor`, `flipX` and `flipY`. * * @param viewX The coordinates in the camera's view + * @param viewY The coordinates in the camera's view * @param camera The desired "screen" space. If `null`, `getDefaultCamera()` is used * @param result Optional arg for the returning point * @since 6.2.0 diff --git a/tests/unit/src/flixel/FlxCameraTest.hx b/tests/unit/src/flixel/FlxCameraTest.hx index 36060c21b9..e624fb8b95 100644 --- a/tests/unit/src/flixel/FlxCameraTest.hx +++ b/tests/unit/src/flixel/FlxCameraTest.hx @@ -163,7 +163,7 @@ class FlxCameraTest extends FlxTest // track leaked points @:privateAccess final pointPool = FlxBasePoint.pool; - pointPool.preAllocate(1); + pointPool.preAllocate(100); final startingPoolLength = pointPool.length; #end @@ -246,7 +246,7 @@ class FlxCameraTest extends FlxTest FlxAssert.pointNearXY(650, 500, camera.viewToGamePosition(320, 240, game)); #if FLX_POINT_POOL - Assert.areEqual(pointPool.length, startingPoolLength); + Assert.areEqual(startingPoolLength, pointPool.length); #end } diff --git a/tests/unit/src/flixel/FlxSpriteTest.hx b/tests/unit/src/flixel/FlxSpriteTest.hx index 02871894c1..77a7981ddb 100644 --- a/tests/unit/src/flixel/FlxSpriteTest.hx +++ b/tests/unit/src/flixel/FlxSpriteTest.hx @@ -1,14 +1,14 @@ package flixel; -import openfl.display.BitmapData; import flixel.animation.FlxAnimation; import flixel.graphics.atlas.FlxAtlas; -import flixel.math.FlxRect; import flixel.math.FlxPoint; +import flixel.math.FlxRect; import flixel.text.FlxText; import flixel.util.FlxColor; -import massive.munit.Assert; import haxe.PosInfos; +import massive.munit.Assert; +import openfl.display.BitmapData; class FlxSpriteTest extends FlxTest { @@ -369,34 +369,66 @@ class FlxSpriteTest extends FlxTest final worldPos = FlxPoint.get(); final actual = FlxPoint.get(); + + #if FLX_POINT_POOL + // track leaked points + @:privateAccess + final pointPool = FlxBasePoint.pool; + pointPool.preAllocate(100); + final startingPoolLength = pointPool.length; + #end + function assertFramePosition(worldX:Float, worldY:Float, expectedX:Float, expectedY:Float, margin = 0.001, ?pos:PosInfos) { + function getMsg(prefix:String) + { + return '$prefix - Value [$actual] is not within [$margin] of [( x:$expectedX | y:$expectedY )]'; + } + + sprite1.worldToFramePosition(worldX, worldY, actual); + FlxAssert.pointNearXY(expectedX, expectedY, actual, margin, getMsg('(xy)'), pos); + + sprite1.worldToFramePositionSimple(worldX, worldY, actual); + FlxAssert.pointNearXY(expectedX, expectedY, actual, margin, getMsg('simple(xy)'), pos); + worldPos.set(worldX, worldY); sprite1.worldToFramePosition(worldPos, actual); - FlxAssert.areNear(expectedX, actual.x, margin, 'X Value [${actual.x}] is not within [$margin] of [$expectedX]', pos); - FlxAssert.areNear(expectedY, actual.y, margin, 'Y Value [${actual.y}] is not within [$margin] of [$expectedY]', pos); + FlxAssert.pointNearXY(expectedX, expectedY, actual, margin, getMsg('(p)'), pos); + sprite1.worldToFramePositionSimple(worldPos, actual); - FlxAssert.areNear(expectedX, actual.x, margin, 'Simple X Value [${actual.x}] is not within [$margin] of [$expectedX]', pos); - FlxAssert.areNear(expectedY, actual.y, margin, 'Simple Y Value [${actual.y}] is not within [$margin] of [$expectedY]', pos); + FlxAssert.pointNearXY(expectedX, expectedY, actual, margin, getMsg('simple(p)'), pos); } assertFramePosition(100, 100, 0, 0); assertFramePosition(150, 150, 50, 50); + + #if FLX_POINT_POOL + Assert.areEqual(startingPoolLength, pointPool.length); + #end + FlxG.camera.scroll.set(50, 100); assertFramePosition(100, 100, 0, 0); assertFramePosition(150, 150, 50, 50); + sprite1.scale.set(2, 2); assertFramePosition(100, 100, 25, 25); assertFramePosition(150, 150, 50, 50); + sprite1.angle = 90; assertFramePosition(100, 100, 25, 75); assertFramePosition(150, 150, 50, 50); + sprite1.flipX = true; assertFramePosition(100, 100, 75, 75); assertFramePosition(150, 150, 50, 50); + sprite1.flipY = true; assertFramePosition(100, 100, 75, 25); assertFramePosition(150, 150, 50, 50); + + #if FLX_POINT_POOL + Assert.areEqual(startingPoolLength, pointPool.length); + #end } @Test @@ -408,12 +440,27 @@ class FlxSpriteTest extends FlxTest final worldPos = FlxPoint.get(); final actual = FlxPoint.get(); + + #if FLX_POINT_POOL + // track leaked points + @:privateAccess + final pointPool = FlxBasePoint.pool; + pointPool.preAllocate(100); + final startingPoolLength = pointPool.length; + #end + function assertFramePosition(worldX:Float, worldY:Float, expectedX:Float, expectedY:Float, margin = 0.001, ?pos:PosInfos) { - worldPos.set(worldX, worldY); - sprite1.viewToFramePosition(worldPos, actual); - FlxAssert.areNear(expectedX, actual.x, margin, 'X Value [${actual.x}] is not within [$margin] of [$expectedX]', pos); - FlxAssert.areNear(expectedY, actual.y, margin, 'Y Value [${actual.y}] is not within [$margin] of [$expectedY]', pos); + function getMsg(prefix:String) + { + return '$prefix - Value [$actual] is not within [$margin] of [( x:$expectedX | y:$expectedY )]'; + } + + sprite1.viewToFramePosition(worldX, worldY, actual); + FlxAssert.pointNearXY(expectedX, expectedY, actual, margin, getMsg('(xy)'), pos); + + sprite1.viewToFramePosition(worldPos.set(worldX, worldY), actual); + FlxAssert.pointNearXY(expectedX, expectedY, actual, margin, getMsg('(p)'), pos); } assertFramePosition(100, 100, 0, 0); @@ -437,10 +484,15 @@ class FlxSpriteTest extends FlxTest FlxG.camera.scroll.set(50, 100); assertFramePosition(100, 100, 25, 50); - assertFramePosition(100, 50, 50, 50); + assertFramePosition(150, 150, 0, 75); FlxG.camera.zoom = 2; - assertFramePosition(100, 100, 25, 50); + assertFramePosition(100, 100, -35, 130); + assertFramePosition(150, 150, -60, 155); + + #if FLX_POINT_POOL + Assert.areEqual(startingPoolLength, pointPool.length); + #end } @Test @@ -451,6 +503,15 @@ class FlxSpriteTest extends FlxTest sprite1.makeGraphic(100, 100); final expected = FlxRect.get(); + + #if FLX_POINT_POOL + // track leaked points + @:privateAccess + final pointPool = FlxBasePoint.pool; + pointPool.preAllocate(100); + final startingPoolLength = pointPool.length; + #end + function assertClipRect(expectedX:Float, expectedY:Float, expectedWidth:Float, expectedHeight:Float, margin = 0.001, ?pos:PosInfos) { FlxAssert.rectsNear(expected.set(expectedX, expectedY, expectedWidth, expectedHeight), sprite1.clipRect, margin, pos); @@ -482,6 +543,10 @@ class FlxSpriteTest extends FlxTest FlxG.camera.zoom = 2; sprite1.clipToWorldBounds(50, 50, 150, 150); assertClipRect(50, 0, 50, 50); + + #if FLX_POINT_POOL + Assert.areEqual(startingPoolLength, pointPool.length); + #end } @Test @@ -493,6 +558,15 @@ class FlxSpriteTest extends FlxTest sprite1.camera = FlxG.camera; final expected = FlxRect.get(); + + #if FLX_POINT_POOL + // track leaked points + @:privateAccess + final pointPool = FlxBasePoint.pool; + pointPool.preAllocate(100); + final startingPoolLength = pointPool.length; + #end + function assertClipRect(expectedX:Float, expectedY:Float, expectedWidth:Float, expectedHeight:Float, margin = 0.001, ?pos:PosInfos) { FlxAssert.rectsNear(expected.set(expectedX, expectedY, expectedWidth, expectedHeight), sprite1.clipRect, margin, pos); @@ -527,11 +601,50 @@ class FlxSpriteTest extends FlxTest FlxG.camera.zoom = 4; sprite1.clipToViewBounds(50, 50, 150, 150); - assertClipRect(25, 50, 50, 50);//TODO: why does zoom have no effect + assertClipRect(-65, 170, 50, 50); sprite1.clipToViewBounds(40, 40, 150, 150); - assertClipRect(25, 45, 55, 55); + assertClipRect(-65, 165, 55, 55);// wtf! sprite1.clipToViewBounds(30, 30, 150, 150); - assertClipRect(25, 40, 60, 60); + assertClipRect(-65, 160, 60, 60); + + #if FLX_POINT_POOL + Assert.areEqual(startingPoolLength, pointPool.length); + #end + } + + @Test + function testCoordinateConvertersNullResult() + { + // Test that a new point is returned when a result is not supplied (A common dev error) + try + { + final result = sprite1.viewToFramePosition(sprite1.x, sprite1.y); + Assert.areEqual(0, result.x); + } + catch(e) + { + Assert.fail('Exception thrown from "viewToFramePosition", message: "${e.message}", stack:\n${e.stack}'); + } + + try + { + final result = sprite1.worldToFramePosition(sprite1.x, sprite1.y); + Assert.areEqual(0, result.x); + } + catch(e) + { + Assert.fail('Exception thrown from "worldToFramePosition", message: "${e.message}", stack:\n${e.stack}'); + } + + try + { + final result = sprite1.worldToFramePositionSimple(sprite1.x, sprite1.y); + Assert.areEqual(0, result.x); + } + catch(e) + { + Assert.fail('Exception thrown from "worldToFramePositionSimple", message: "${e.message}", stack:\n${e.stack}'); + } } } From c57406d36515840ae5f0a99dfae77b6de55fbaa9 Mon Sep 17 00:00:00 2001 From: GeoKureli-BlackbookPro Date: Thu, 17 Apr 2025 16:01:08 -0500 Subject: [PATCH 08/12] add since --- flixel/graphics/frames/FlxFrame.hx | 1 + 1 file changed, 1 insertion(+) diff --git a/flixel/graphics/frames/FlxFrame.hx b/flixel/graphics/frames/FlxFrame.hx index 0ade57ef90..62ec9e11bf 100644 --- a/flixel/graphics/frames/FlxFrame.hx +++ b/flixel/graphics/frames/FlxFrame.hx @@ -330,6 +330,7 @@ class FlxFrame implements IFlxDestroyable * * @param framePos The position in this frame * @param result Optional arg for the returning point + * @since 6.2.0 */ public function toSourcePosition(framePos:FlxPoint, ?result:FlxPoint) { From b532d84a48401bb5a0f585b8d712faee4c802075 Mon Sep 17 00:00:00 2001 From: GeoKureli-BlackbookPro Date: Sun, 20 Apr 2025 19:11:23 -0500 Subject: [PATCH 09/12] fix typo --- flixel/FlxSprite.hx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flixel/FlxSprite.hx b/flixel/FlxSprite.hx index 8a4f112676..03872cf8c2 100644 --- a/flixel/FlxSprite.hx +++ b/flixel/FlxSprite.hx @@ -1374,7 +1374,7 @@ class FlxSprite extends FlxObject camera = getDefaultCamera(); // get the screen pos without scrollFactor, then get the world, WITH scrollFactor - return viewToFrameHelper(camera.worldToViewX(worldX), camera.worldToViewY(worldX), camera, result); + return viewToFrameHelper(camera.worldToViewX(worldX), camera.worldToViewY(worldY), camera, result); } /** From cb276c77be8a0a48f0db4ca3ec007dc73a3cc3fa Mon Sep 17 00:00:00 2001 From: GeoKureli-BlackbookPro Date: Sun, 20 Apr 2025 21:16:45 -0500 Subject: [PATCH 10/12] more unit tests --- tests/unit/src/flixel/FlxSpriteTest.hx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/unit/src/flixel/FlxSpriteTest.hx b/tests/unit/src/flixel/FlxSpriteTest.hx index 77a7981ddb..5aa4061e8a 100644 --- a/tests/unit/src/flixel/FlxSpriteTest.hx +++ b/tests/unit/src/flixel/FlxSpriteTest.hx @@ -400,6 +400,7 @@ class FlxSpriteTest extends FlxTest } assertFramePosition(100, 100, 0, 0); + assertFramePosition(100, 110, 0, 10); assertFramePosition(150, 150, 50, 50); #if FLX_POINT_POOL @@ -464,6 +465,7 @@ class FlxSpriteTest extends FlxTest } assertFramePosition(100, 100, 0, 0); + assertFramePosition(100, 110, 0, 10); assertFramePosition(150, 150, 50, 50); sprite1.scale.set(2, 2); @@ -520,6 +522,9 @@ class FlxSpriteTest extends FlxTest sprite1.clipToWorldBounds(100, 100, 200, 200); assertClipRect(0, 0, 100, 100); + sprite1.clipToWorldBounds(100, 110, 200, 190); + assertClipRect(0, 10, 100, 80); + sprite1.scale.set(2, 2); sprite1.clipToWorldBounds(50, 50, 150, 150); assertClipRect(0, 0, 50, 50); @@ -575,6 +580,9 @@ class FlxSpriteTest extends FlxTest sprite1.clipToViewBounds(100, 100, 200, 200); assertClipRect(0, 0, 100, 100); + sprite1.clipToViewBounds(100, 110, 200, 190); + assertClipRect(0, 10, 100, 80); + sprite1.scale.set(2, 2); sprite1.clipToViewBounds(50, 50, 150, 150); assertClipRect(0, 0, 50, 50); From ee950b1b4ff665ce7efb2034bde7509d2005fcf8 Mon Sep 17 00:00:00 2001 From: GeoKureli-BlackbookPro Date: Tue, 22 Apr 2025 15:36:24 -0500 Subject: [PATCH 11/12] honor color is getPixelAt --- flixel/FlxSprite.hx | 34 +++++++++++++++--- tests/unit/src/FlxAssert.hx | 12 +++++++ tests/unit/src/flixel/FlxSpriteTest.hx | 50 ++++++++++++++++++++++++-- 3 files changed, 88 insertions(+), 8 deletions(-) diff --git a/flixel/FlxSprite.hx b/flixel/FlxSprite.hx index 03872cf8c2..8e9614e5bf 100644 --- a/flixel/FlxSprite.hx +++ b/flixel/FlxSprite.hx @@ -1227,13 +1227,37 @@ class FlxSprite extends FlxObject */ public function getPixelAt(worldPoint:FlxPoint, ?camera:FlxCamera):Null { - final point = worldToFramePosition(worldPoint, camera); - + final point = worldToFramePosition(worldPoint, camera, FlxPoint.weak()); final overlaps = point.x >= 0 && point.x <= frameWidth && point.y >= 0 && point.y <= frameHeight; - final result = overlaps ? frame.getPixelAt(point) : null; + if (!overlaps) + { + point.put(); + return null; + } - point.put(); - return result; + final frameColor = frame.getPixelAt(point); + + if (useColorTransform) + { + return FlxColor.fromRGBFloat + ( + frameColor.redFloat * colorTransform.redMultiplier + (colorTransform.redOffset / 0xFF), + frameColor.greenFloat * colorTransform.greenMultiplier + (colorTransform.greenOffset / 0xFF), + frameColor.blueFloat * colorTransform.blueMultiplier + (colorTransform.blueOffset / 0xFF) + ); + } + + if (color.rgb != 0xffffff) + { + return FlxColor.fromRGBFloat + ( + frameColor.redFloat * color.redFloat, + frameColor.greenFloat * color.greenFloat, + frameColor.blueFloat * color.blueFloat + ); + } + + return frameColor; } /** diff --git a/tests/unit/src/FlxAssert.hx b/tests/unit/src/FlxAssert.hx index 1c501493ac..5e1ca5c136 100644 --- a/tests/unit/src/FlxAssert.hx +++ b/tests/unit/src/FlxAssert.hx @@ -2,6 +2,7 @@ package; import flixel.math.FlxPoint; import flixel.math.FlxRect; +import flixel.util.FlxColor; import haxe.PosInfos; import massive.munit.Assert; @@ -124,4 +125,15 @@ class FlxAssert else Assert.fail('Value [$actual] is not within [$margin] of [( x:$expectedX | y:$expectedY )]', info); } + + + public static function colorsEqual(expected:FlxColor, actual:FlxColor, ?msg:String, ?info:PosInfos):Void + { + if (expected == actual) + Assert.assertionCount++; + else if (msg != null) + Assert.fail(msg, info); + else + Assert.fail('Value [${actual.toHexString()}] is not equal to [${expected.toHexString()}]', info); + } } diff --git a/tests/unit/src/flixel/FlxSpriteTest.hx b/tests/unit/src/flixel/FlxSpriteTest.hx index 5aa4061e8a..8b6cdfaffe 100644 --- a/tests/unit/src/flixel/FlxSpriteTest.hx +++ b/tests/unit/src/flixel/FlxSpriteTest.hx @@ -57,13 +57,13 @@ class FlxSpriteTest extends FlxTest var color = FlxColor.RED; var colorSprite = new FlxSprite(); colorSprite.makeGraphic(100, 100, color); - Assert.areEqual(color.to24Bit(), colorSprite.pixels.getPixel(0, 0)); - Assert.areEqual(color.to24Bit(), colorSprite.pixels.getPixel(90, 90)); + FlxAssert.colorsEqual(color.rgb, colorSprite.pixels.getPixel(0, 0)); + FlxAssert.colorsEqual(color.rgb, colorSprite.pixels.getPixel(90, 90)); color = FlxColor.GREEN; colorSprite = new FlxSprite(); colorSprite.makeGraphic(120, 120, color); - Assert.areEqual(color.to24Bit(), colorSprite.pixels.getPixel(119, 119)); + FlxAssert.colorsEqual(color.rgb, colorSprite.pixels.getPixel(119, 119)); } @Test @@ -497,6 +497,50 @@ class FlxSpriteTest extends FlxTest #end } + @Test + function testGetPixelAt() + { + final WHITE = FlxColor.WHITE; + final BLACK = FlxColor.BLACK; + final RED = FlxColor.RED; + + sprite1.x = 100; + sprite1.y = 0; + sprite1.makeGraphic(100, 100, WHITE); + sprite1.graphic.bitmap.fillRect(new openfl.geom.Rectangle(50, 50, 50, 50), BLACK); + + final worldPos = FlxPoint.get(); + + #if FLX_POINT_POOL + // track leaked points + @:privateAccess + final pointPool = FlxBasePoint.pool; + pointPool.preAllocate(100); + final startingPoolLength = pointPool.length; + #end + + function assertPixelAt(expected:FlxColor, x:Float, y:Float, ?pos:PosInfos) + { + FlxAssert.colorsEqual(expected, sprite1.getPixelAt(worldPos.set(x, y)), pos); + } + + assertPixelAt(WHITE, 125, 25); + assertPixelAt(BLACK, 175, 75); + + sprite1.color = RED; + assertPixelAt(RED, 125, 25); + assertPixelAt(BLACK, 175, 75); + + sprite1.color = WHITE; + sprite1.setColorTransform(1.0, 0.5, 0.0, 1.0, 0x0, 0x0, 0x80); + assertPixelAt(0xFFff8080, 125, 25); + assertPixelAt(0xFF000080, 175, 75); + + #if FLX_POINT_POOL + Assert.areEqual(startingPoolLength, pointPool.length); + #end + } + @Test function testClipToWorldBounds() { From a2b9e6165beccfd822dd79028175eaac8feab1e6 Mon Sep 17 00:00:00 2001 From: GeoKureli-BlackbookPro Date: Tue, 22 Apr 2025 16:32:23 -0500 Subject: [PATCH 12/12] fix color in getPixelAt --- flixel/FlxSprite.hx | 60 +++++++++++++++----------- tests/unit/src/flixel/FlxSpriteTest.hx | 5 ++- 2 files changed, 39 insertions(+), 26 deletions(-) diff --git a/flixel/FlxSprite.hx b/flixel/FlxSprite.hx index 8e9614e5bf..8586a55c43 100644 --- a/flixel/FlxSprite.hx +++ b/flixel/FlxSprite.hx @@ -11,8 +11,7 @@ import flixel.math.FlxMath; import flixel.math.FlxMatrix; import flixel.math.FlxPoint; import flixel.math.FlxRect; -import flixel.system.FlxAssets.FlxGraphicAsset; -import flixel.system.FlxAssets.FlxShader; +import flixel.system.FlxAssets; import flixel.util.FlxBitmapDataUtil; import flixel.util.FlxColor; import flixel.util.FlxDestroyUtil; @@ -1216,6 +1215,39 @@ class FlxSprite extends FlxObject return false; } + /** + * Helper to apply the sprite's color or colorTransform to the specified color + */ + function transformColor(colorIn:FlxColor):FlxColor + { + final colorStr = color.toHexString(); + if (useColorTransform) + { + final ct = colorTransform; + return FlxColor.fromRGB + ( + Math.round(colorIn.red * ct.redMultiplier + ct.redOffset), + Math.round(colorIn.green * ct.greenMultiplier + ct.greenOffset), + Math.round(colorIn.blue * ct.blueMultiplier + ct.blueOffset), + Math.round(colorIn.alpha * alpha) + ); + } + + if (color.rgb != 0xffffff) + { + final result = FlxColor.fromRGBFloat + ( + colorIn.redFloat * color.redFloat, + colorIn.greenFloat * color.greenFloat, + colorIn.blueFloat * color.blueFloat, + colorIn.alphaFloat * alpha + ); + return result; + } + + return colorIn; + } + /** * Determines which of this sprite's pixels are at the specified world coordinate, if any. * Factors in `scale`, `angle`, `offset`, `origin`, `scrollFactor`, `flipX` and `flipY`. @@ -1235,29 +1267,7 @@ class FlxSprite extends FlxObject return null; } - final frameColor = frame.getPixelAt(point); - - if (useColorTransform) - { - return FlxColor.fromRGBFloat - ( - frameColor.redFloat * colorTransform.redMultiplier + (colorTransform.redOffset / 0xFF), - frameColor.greenFloat * colorTransform.greenMultiplier + (colorTransform.greenOffset / 0xFF), - frameColor.blueFloat * colorTransform.blueMultiplier + (colorTransform.blueOffset / 0xFF) - ); - } - - if (color.rgb != 0xffffff) - { - return FlxColor.fromRGBFloat - ( - frameColor.redFloat * color.redFloat, - frameColor.greenFloat * color.greenFloat, - frameColor.blueFloat * color.blueFloat - ); - } - - return frameColor; + return transformColor(frame.getPixelAt(point)); } /** diff --git a/tests/unit/src/flixel/FlxSpriteTest.hx b/tests/unit/src/flixel/FlxSpriteTest.hx index 8b6cdfaffe..f6b3483cdd 100644 --- a/tests/unit/src/flixel/FlxSpriteTest.hx +++ b/tests/unit/src/flixel/FlxSpriteTest.hx @@ -531,11 +531,14 @@ class FlxSpriteTest extends FlxTest assertPixelAt(RED, 125, 25); assertPixelAt(BLACK, 175, 75); - sprite1.color = WHITE; sprite1.setColorTransform(1.0, 0.5, 0.0, 1.0, 0x0, 0x0, 0x80); assertPixelAt(0xFFff8080, 125, 25); assertPixelAt(0xFF000080, 175, 75); + sprite1.alpha = 0.5; + assertPixelAt(0x80ff8080, 125, 25); + assertPixelAt(0x80000080, 175, 75); + #if FLX_POINT_POOL Assert.areEqual(startingPoolLength, pointPool.length); #end