2D coordinates: local, world, viewport and HUD
Convert points through scale, camera, input and HUD spaces without double offsets.
Use a coordinate conversion when a point comes from a different space: a click in the viewport, a muzzle offset inside a weapon, or a world marker shown in a HUD. Adding or subtracting positions alone fails once rotation, scale or a camera is involved.
Identify the input and output spaces
| Space | Meaning | Use |
|---|---|---|
| Local | Relative to the item's own origin and axes | Sprite attachment points, local drawing and geometry. |
| Parent | The parent's local coordinates | Entity2D.Position places the child's origin here. |
| Canvas / world | Coordinates in the item's canvas, after its CanvasItem ancestors | Entity2D.GlobalPosition, ToGlobal and ToLocal. Separate CanvasLayers can have separate canvases. |
| Logical viewport, canvas stage | After the default canvas transform or the applicable CanvasLayer transform | GetGlobalTransformWithCanvas and MakeCanvasPositionLocal. This is the conversion used below. |
| Window / screen | After additional presentation, stretch or embedding transforms | Do not mix OS cursor/window positions with the logical viewport points below. |
In ordinary 2D coordinates, +X points right and +Y points down. A positive quarter-turn rotates Right toward Down. One logical unit is not guaranteed to be one physical display pixel after scaling.
Convert a point in a hierarchy
item.ToGlobal(localPoint) applies the item's global transform. item.ToLocal(canvasPoint) applies its affine inverse. Global here means the item's canvas; it does not mean OS screen coordinates and does not include Camera2D's canvas transform.
For example, a parent has basis columns (0, 2) and (-3, 0) and origin (100, 50). Its child Position is (10, 4). The child's GlobalPosition is (88, 70), not (110, 54): the parent's rotation and scale affect the offset. The child's local point (2, 1) becomes canvas point (85, 74). ToLocal converts it back.
To position the child origin at a known canvas point, assign GlobalPosition. ToGlobal expects a point already local to that item; passing its Position usually applies the child offset a second time.
Within one canvas, convert a point from A to B with b.ToLocal(a.ToGlobal(pointInA)). For different CanvasLayers in one viewport, use the canvas-aware helper below.
Convert viewport input or a world point to HUD space
Save this helper as Coordinates2D.cs:
using System;
using Reckfell;
public static class Coordinates2D
{
public static Vector2 LocalToViewport(CanvasItem item, Vector2 point)
{
RequireTree(item);
return item.GetGlobalTransformWithCanvas() * point;
}
public static Vector2 ViewportToLocal(CanvasItem item, Vector2 point)
{
RequireTree(item);
return item.GetGlobalTransformWithCanvas().AffineInverse() * point;
}
public static Vector2 BetweenItems(CanvasItem source, CanvasItem target, Vector2 point)
{
RequireTree(source);
RequireTree(target);
if (source.GetViewport() != target.GetViewport())
throw new ArgumentException("Items must belong to the same Viewport.");
return ViewportToLocal(target, LocalToViewport(source, point));
}
private static void RequireTree(CanvasItem item)
{
if (!item.IsInsideTree())
throw new InvalidOperationException("Add the item to the tree first.");
}
}Call Coordinates2D.ViewportToLocal(item, viewportPoint) for a point in the item's logical viewport. The built-in item.MakeCanvasPositionLocal(viewportPoint) performs the corresponding conversion. To convert an input event, MakeInputLocal returns a transformed event; the original event is unchanged. Do not transform a position again if its callback already provides item-local coordinates, such as Control GUI input.
For a HUD marker, call Coordinates2D.BetweenItems(worldItem, hudParent, pointInWorldItem). Assign the result to the marker's Position when that position is relative to hudParent. The helper accounts for both items' ancestor transforms and their canvas/layer transforms. It requires both items in the tree and in the same viewport. A Control managed by a Container may have its position overwritten by layout; use an appropriate free-positioned parent for markers.
With viewport CanvasTransform scaling by 2 and translating by (7, 11), the earlier canvas point (85, 74) becomes (177, 159). A HUD CanvasLayer offset (20, 30) containing a parent at (5, 7) maps that point to HUD-parent local (152, 122). Subtracting only the camera position would miss scaling and the HUD transform.
The helper operates at the canvas stage. It does not map an OS screen point or a point from another SubViewport through window/stretch/embedding transforms. Keep the viewport's final presentation transforms separate; see window scaling.
Distinguish points, directions and inverses
Transform2D stores basis columns X and Y plus translation Origin. transform * point includes Origin. transform.BasisXform(direction) excludes translation, but still applies scale and skew; normalize afterwards only if a unit direction is required.
parentTransform * childTransform applies the child first, then the parent. Reversing the order changes the result.
Use AffineInverse for a basis with scale or skew. Inverse and the reversed point * transform operator assume an orthonormal basis; they are not general shortcuts for undoing a scaled transform. BasisXformInv has the same orthonormal restriction. For a scaled direction use transform.AffineInverse().BasisXform(direction).
Use Transform2D.Identity for an identity matrix in C#. The parameterless new Transform2D() and default(Transform2D) contain zeros, not an identity basis.
The basis must be invertible. A zero scale on an axis collapses a dimension; the C# AffineInverse method throws InvalidOperationException when the determinant is exactly zero. Near-zero scale can amplify numerical error even when the determinant is nonzero. Avoid singular and nonfinite transforms instead of hiding the failure with a guessed point.
Diagnose an offset
| Symptom | Likely mismatch |
|---|---|
| Correct before camera zoom, wrong afterwards | Canvas/world coordinates were mixed with logical viewport input. |
| Wrong only under a rotated/scaled parent | Position subtraction was used instead of a transform. |
| HUD marker follows the camera incorrectly | World and HUD belong to different canvases; convert through the same viewport. |
| Offset applied twice | Position was passed to the item's own ToGlobal, or already-local input was converted again. |
| TopLevel child still reacts to the camera | TopLevel stops parent CanvasItem transform inheritance; it does not remove the canvas transform. |
| Helper rejects the items | Add them to the tree and verify they belong to the same viewport. |
The runtime example verifies numeric scale/skew inverses, hierarchy, canvas/layer conversion, copied input-event positions and TopLevel behavior. OS/DPI coordinates, nested windows, actual pointer delivery and presentation-transform combinations are outside that example's tested scope.