diff --git a/flixel/FlxCamera.hx b/flixel/FlxCamera.hx index 6574d832e3..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; @@ -1785,7 +1785,333 @@ class FlxCamera extends FlxBasic x = X; 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 + * + * @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 + * @since 6.2.0 + */ + overload public inline extern function worldToViewPosition(worldPos:FlxPoint, scrollFactorX = 1.0, scrollFactorY = 1.0, ?result:FlxPoint) + { + result = worldToViewHelper(worldPos.x, worldPos.y, scrollFactorX, scrollFactorY, result); + worldPos.putWeak(); + return 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 + * @since 6.2.0 + */ + overload public inline extern function worldToViewPosition(worldPos:FlxPoint, ?scrollFactor:FlxPoint, ?result:FlxPoint) + { + result = worldToViewHelper(worldPos.x, worldPos.y, safeGetX(scrollFactor, 1.0), safeGetY(scrollFactor, 1.0), result); + worldPos.putWeak(); + FlxDestroyUtil.putWeak(scrollFactor); + 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 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, safeGetX(scrollFactor, 1.0), safeGetY(scrollFactor, 1.0), result); + FlxDestroyUtil.putWeak(scrollFactor); + 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 + * @since 6.2.0 + */ + 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 + * @since 6.2.0 + */ + public function worldToViewX(worldX:Float, scrollFactor = 1.0) + { + return worldX - (scroll.x * scrollFactor) - viewMarginX; + } + + /** + * 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 + * @since 6.2.0 + */ + public function worldToViewY(worldY:Float, scrollFactor = 1.0) + { + return worldY - (scroll.y * scrollFactor) - viewMarginY; + } + + /** + * 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 + * @since 6.2.0 + */ + overload public inline extern function viewToWorldPosition(viewPos:FlxPoint, scrollFactorX = 1.0, scrollFactorY = 1.0, ?result:FlxPoint) + { + result = viewToWorldHelper(viewPos.x, viewPos.y, scrollFactorX, scrollFactorY, result); + viewPos.putWeak(); + return 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 + * @since 6.2.0 + */ + overload public inline extern function viewToWorldPosition(viewPos:FlxPoint, ?scrollFactor:FlxPoint, ?result:FlxPoint) + { + result = viewToWorldHelper(viewPos.x, viewPos.y, safeGetX(scrollFactor, 1.0), safeGetY(scrollFactor, 1.0), result); + viewPos.putWeak(); + FlxDestroyUtil.putWeak(scrollFactor); + 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 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, safeGetX(scrollFactor, 1.0), safeGetY(scrollFactor, 1.0), result); + FlxDestroyUtil.putWeak(scrollFactor); + 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 + * @since 6.2.0 + */ + overload public inline extern function viewToWorldPosition(viewX:Float, viewY:Float, scrollFactorX = 1.0, scrollFactorY = 1.0, ?result) + { + return viewToWorldHelper(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 + * @since 6.2.0 + */ + public function viewToWorldX(viewX:Float, scrollFactor = 1.0) + { + return viewX + (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 + * @since 6.2.0 + */ + public function viewToWorldY(viewY:Float, scrollFactor = 1.0) + { + 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; + } + /** * 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..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; @@ -271,8 +270,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 +730,147 @@ 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 + * @since 6.2.0 + */ + 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 + * @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 = 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(); + 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 + * @since 6.2.0 + */ + 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 + * @since 6.2.0 + */ + 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 + * @since 6.2.0 + */ + 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`. @@ -1070,9 +1215,42 @@ 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`, 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 +1259,15 @@ class FlxSprite extends FlxObject */ public function getPixelAt(worldPoint:FlxPoint, ?camera:FlxCamera):Null { - transformWorldToPixels(worldPoint, camera, _point); - - // point is inside the graphic - if (_point.x >= 0 && _point.x <= frameWidth && _point.y >= 0 && _point.y <= frameHeight) + final point = worldToFramePosition(worldPoint, camera, FlxPoint.weak()); + final overlaps = point.x >= 0 && point.x <= frameWidth && point.y >= 0 && point.y <= frameHeight; + if (!overlaps) { - var frameData:BitmapData = updateFramePixels(); - return frameData.getPixel32(Std.int(_point.x), Std.int(_point.y)); + point.put(); + return null; } - return null; + return transformColor(frame.getPixelAt(point)); } /** @@ -1104,27 +1281,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 +1313,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 +1341,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 +1364,167 @@ 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 + */ + 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 + return viewToFrameHelper(camera.worldToViewX(worldX), camera.worldToViewY(worldY), 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 + */ + overload public inline extern function worldToFramePositionSimple(worldPos:FlxPoint, ?result:FlxPoint):FlxPoint + { + result = worldToFrameSimpleHelper(worldPos.x, worldPos.y, 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. 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); + 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, camera, 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 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 + */ + 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/flixel/graphics/frames/FlxFrame.hx b/flixel/graphics/frames/FlxFrame.hx index b68ffcf73e..62ec9e11bf 100644 --- a/flixel/graphics/frames/FlxFrame.hx +++ b/flixel/graphics/frames/FlxFrame.hx @@ -295,7 +295,88 @@ 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 + * @since 6.2.0 + */ + 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/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/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/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 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/FlxCameraTest.hx b/tests/unit/src/flixel/FlxCameraTest.hx index 85459f6094..e624fb8b95 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(100); + 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(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 = 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 diff --git a/tests/unit/src/flixel/FlxSpriteTest.hx b/tests/unit/src/flixel/FlxSpriteTest.hx index d58c103b87..f6b3483cdd 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 { @@ -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 @@ -359,6 +359,348 @@ 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(); + + #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.pointNearXY(expectedX, expectedY, actual, margin, getMsg('(p)'), pos); + + sprite1.worldToFramePositionSimple(worldPos, actual); + FlxAssert.pointNearXY(expectedX, expectedY, actual, margin, getMsg('simple(p)'), pos); + } + + assertFramePosition(100, 100, 0, 0); + assertFramePosition(100, 110, 0, 10); + 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 + function testViewToFramePosition() + { + sprite1.x = 100; + sprite1.y = 100; + sprite1.makeGraphic(100, 100); + + 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.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); + assertFramePosition(100, 110, 0, 10); + 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(150, 150, 0, 75); + + FlxG.camera.zoom = 2; + assertFramePosition(100, 100, -35, 130); + assertFramePosition(150, 150, -60, 155); + + #if FLX_POINT_POOL + Assert.areEqual(startingPoolLength, pointPool.length); + #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.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 + } + + @Test + function testClipToWorldBounds() + { + sprite1.x = 100; + sprite1.y = 100; + 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); + } + + 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); + + 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); + + #if FLX_POINT_POOL + Assert.areEqual(startingPoolLength, pointPool.length); + #end + } + + @Test + function testClipToViewBounds() + { + sprite1.x = 100; + sprite1.y = 100; + sprite1.makeGraphic(100, 100); + 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); + } + + 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); + + 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(-65, 170, 50, 50); + sprite1.clipToViewBounds(40, 40, 150, 150); + assertClipRect(-65, 165, 55, 55);// wtf! + sprite1.clipToViewBounds(30, 30, 150, 150); + 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}'); + } + } } abstract SimplePoint(Array) from Array 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); 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}'); } } } 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); + } }