Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions examples/guides/coordinates-and-views/split-views.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ export const drawSplitWorld = (context: RenderingContext, world: RenderNode, lef
// #endregion guide:split-views

// #region guide:pointer-world
import type { PointLike } from '@codexo/exojs';
import type { Application, PointLike } from '@codexo/exojs';

export const pointerInWorld = (view: View, pointer: PointLike): PointLike => view.screenToWorld(pointer.x, pointer.y);
export const pointerInWorld = (app: Application, view: View, pointer: PointLike): PointLike =>
view.screenToWorld(pointer.x, pointer.y, app.width, app.height);
// #endregion guide:pointer-world
2 changes: 1 addition & 1 deletion site/src/content/api/application.json
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,7 @@
}
],
"returnType": "PointLike",
"description": "Convert a logical/design-space pixel coordinate - the space of Pointer.x/Pointer.y and node positions, e.g. 0..app.width - to a world position using the active camera. At the default centered camera this is the identity; with a panned/zoomed/rotated camera it undoes the transform. Equivalent to app.rendering.view.screenToWorld(x, y)."
"description": "Convert a logical/design-space pixel coordinate - the space of Pointer.x/Pointer.y and node positions, e.g. 0..app.width - to a world position using the active camera. At the default centered camera this is the identity; with a panned/zoomed/rotated camera it undoes the transform, and it honours a camera whose viewport covers only part of the canvas. Equivalent to app.rendering.view.screenToWorld(x, y, app.width, app.height)."
},
{
"name": "setCursor",
Expand Down
4 changes: 2 additions & 2 deletions site/src/content/api/pointer.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"title": "Pointer",
"description": "Unified mouse / touch / pen pointer. Wraps a single browser `PointerEvent.pointerId` and writes its state (position, buttons, pressure, tilt, etc.) into the engine's shared channels buffer so it can be polled by Input bindings or read directly by interaction-aware scene nodes. Coordinates are stored in the application's logical space - `app.width` / `app.height` units (`0..app.width` × `0..app.height`), matching node positions, the 2-argument View.screenToWorld, and the active camera. The CSS-pixel event coordinates are mapped through the canvas's display rect, so the value is independent of Application.pixelRatio, of the render resolution, and of however the canvas is scaled for display: a click at its right edge always reads `app.width`, whichever sizing policy is in play. A point outside the canvas maps outside `0..app.width` accordingly. The channel writes are normalized to 0..1 (position, size, twist, tilt) for backend-agnostic sampling. Pointers are owned by the InputSystem, which assigns them a slot index in 0..15 (see maxPointers) and exposes their per-slot channel offsets via the Pointer class namespace constants.",
"description": "Unified mouse / touch / pen pointer. Wraps a single browser `PointerEvent.pointerId` and writes its state (position, buttons, pressure, tilt, etc.) into the engine's shared channels buffer so it can be polled by Input bindings or read directly by interaction-aware scene nodes. Coordinates are stored in the application's logical space - `app.width` / `app.height` units (`0..app.width` × `0..app.height`), matching node positions and Application.screenToWorld. The CSS-pixel event coordinates are mapped through the canvas's display rect, so the value is independent of Application.pixelRatio, of the render resolution, and of however the canvas is scaled for display: a click at its right edge always reads `app.width`, whichever sizing policy is in play. A point outside the canvas maps outside `0..app.width` accordingly. The channel writes are normalized to 0..1 (position, size, twist, tilt) for backend-agnostic sampling. Pointers are owned by the InputSystem, which assigns them a slot index in 0..15 (see maxPointers) and exposes their per-slot channel offsets via the Pointer class namespace constants.",
"symbol": "Pointer",
"kind": "class",
"subsystem": "input",
Expand All @@ -20,7 +20,7 @@
"members": [],
"paragraphs": [
"Unified mouse / touch / pen pointer. Wraps a single browser `PointerEvent.pointerId` and writes its state (position, buttons, pressure, tilt, etc.) into the engine's shared channels buffer so it can be polled by Input bindings or read directly by interaction-aware scene nodes.",
"Coordinates are stored in the application's logical space - `app.width` / `app.height` units (`0..app.width` × `0..app.height`), matching node positions, the 2-argument View.screenToWorld, and the active camera. The CSS-pixel event coordinates are mapped through the canvas's display rect, so the value is independent of Application.pixelRatio, of the render resolution, and of however the canvas is scaled for display: a click at its right edge always reads `app.width`, whichever sizing policy is in play. A point outside the canvas maps outside `0..app.width` accordingly. The channel writes are normalized to 0..1 (position, size, twist, tilt) for backend-agnostic sampling.",
"Coordinates are stored in the application's logical space - `app.width` / `app.height` units (`0..app.width` × `0..app.height`), matching node positions and Application.screenToWorld. The CSS-pixel event coordinates are mapped through the canvas's display rect, so the value is independent of Application.pixelRatio, of the render resolution, and of however the canvas is scaled for display: a click at its right edge always reads `app.width`, whichever sizing policy is in play. A point outside the canvas maps outside `0..app.width` accordingly. The channel writes are normalized to 0..1 (position, size, twist, tilt) for backend-agnostic sampling.",
"Pointers are owned by the InputSystem, which assigns them a slot index in 0..15 (see maxPointers) and exposes their per-slot channel offsets via the Pointer class namespace constants."
],
"importLine": "import { Pointer } from '@codexo/exojs'",
Expand Down
10 changes: 5 additions & 5 deletions site/src/content/api/view.json
Original file line number Diff line number Diff line change
Expand Up @@ -800,7 +800,7 @@
}
],
"returnType": "PointLike",
"description": "Convert a logical/design-space pixel coordinate to a world-space position. The 2-argument form treats (x, y) as coordinates in the view's own design space (0..view.width × 0..view.height) - the same space as Pointer.x/Pointer.y and node positions - and applies only the inverse camera transform. The viewport rectangle is intentionally ignored, because design-space input already lives inside the camera's projection - the mapping from the surface to logical pixels happened upstream. At the default centered camera this is the identity."
"description": "Convert a logical/design-space pixel coordinate to a world-space position. The 2-argument form treats (x, y) as coordinates in the view's design space: the unzoomed size given to the constructor, resize or reset. Zooming, or writing size directly, changes how much world the view shows but not this space, so a full-surface view sized to the canvas keeps accepting Pointer.x/Pointer.y unchanged at every zoom level. The viewport rectangle is ignored; for a view that covers only part of the surface (split screen) or whose design size differs from the canvas, use the 4-argument form with the surface size. At the default centered camera this is the identity."
},
{
"name": "screenToWorld",
Expand Down Expand Up @@ -910,7 +910,7 @@
}
],
"returnType": "PointLike",
"description": "Convert a raw canvas backing-store pixel coordinate to a world-space position. Accounts for the view's viewport rectangle, so it works correctly in multi-view / split-screen and letterboxed setups."
"description": "Convert a coordinate on the render surface to a world-space position. Accounts for the view's viewport rectangle, so it works correctly in multi-view / split-screen and letterboxed setups. The coordinate and the surface size only need to share a unit: pass app.width/app.height for pointer coordinates, or app.canvas.width/app.canvas.height for backing-store pixels."
},
{
"name": "setBounds",
Expand Down Expand Up @@ -1499,7 +1499,7 @@
}
],
"returnType": "PointLike",
"description": "Convert a world-space position to logical/design-space pixel coordinates (0..view.width × 0..view.height). Inverse of the 2-argument screenToWorld; the viewport is ignored. At the default centered camera this is the identity."
"description": "Convert a world-space position to design-space pixel coordinates. Inverse of the 2-argument screenToWorld, with the same design space; the viewport is ignored. At the default centered camera this is the identity."
},
{
"name": "worldToScreen",
Expand Down Expand Up @@ -1609,7 +1609,7 @@
}
],
"returnType": "PointLike",
"description": "Convert a world-space position to raw canvas backing-store pixel coordinates. Accounts for the view's viewport rectangle."
"description": "Convert a world-space position to a coordinate on the render surface. Inverse of the 4-argument screenToWorld; accounts for the view's viewport rectangle, and the result is in the unit of the surface size."
},
{
"name": "zoom",
Expand Down Expand Up @@ -1656,7 +1656,7 @@
}
],
"returnType": "this",
"description": ""
"description": "Multiply the visible area by factor relative to the current zoom. A factor of 0.5 halves the visible area, like doubling zoomLevel."
},
{
"name": "zoomIn",
Expand Down
4 changes: 2 additions & 2 deletions site/src/content/guide/input/mouse-and-pointer.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,9 @@ const world = view.screenToWorld(pointer.x, pointer.y);
// world.x, world.y are now in scene world space
```

The inverse is `worldToScreen(worldX, worldY)`, and the two round-trip. If the default centered camera is active (no pan/zoom), design space already equals world space, so `screenToWorld` is the identity and no mapping is needed.
The inverse is `worldToScreen(worldX, worldY)`, and the two round-trip. The two-argument form maps over the view's design space — the size it was created or resized with — so zooming changes how much world is visible but not which pointer coordinates are accepted. If the default centered camera is active (no pan/zoom), design space already equals world space, so `screenToWorld` is the identity and no mapping is needed. `app.screenToWorld(x, y)` maps through the active camera.

For raw canvas backing-store pixels — e.g. an off-screen render target at a different pixel ratio — the four-argument form `screenToWorld(x, y, canvasWidth, canvasHeight)` additionally accounts for the viewport rectangle and physical pixel size.
For a view that covers only part of the canvas (split screen, minimap) or whose size differs from the canvas, use the four-argument form `screenToWorld(x, y, app.width, app.height)`: it accounts for the viewport rectangle. Backing-store pixels work the same way with `app.canvas.width` and `app.canvas.height`.

## Interactive sprites

Expand Down
4 changes: 2 additions & 2 deletions site/src/content/guide/runtime/coordinates-and-views.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,11 @@ For smoothing, keep a separate world-space camera target and move it toward the

## Convert input using the same view

Pointer coordinates arrive in logical canvas units. Convert them through the view that rendered the world object:
Pointer coordinates arrive in logical canvas units. Convert them through the view that rendered the world object, over the logical canvas size so the view's viewport is taken into account:

<SourceSnippet source="examples/guides/coordinates-and-views/split-views.ts" region="pointer-world" language="ts" title="Logical pointer coordinates to world coordinates" />

In split-screen, first choose the viewport under the pointer, then use that viewport's view. Converting with the other player's camera can yield a plausible point in the wrong part of the world. If hit testing needs a child's local coordinates, apply the appropriate world-to-local conversion afterward.
For a camera that covers the whole canvas, the two-argument `view.screenToWorld(x, y)` gives the same result at every zoom level; `app.screenToWorld(x, y)` does this for the active camera. In split-screen, first choose the viewport under the pointer, then use that viewport's view with the canvas size as above. Converting with the other player's camera can yield a plausible point in the wrong part of the world. If hit testing needs a child's local coordinates, apply the appropriate world-to-local conversion afterward.

Do not multiply pointer coordinates by DPR to repair a camera mismatch. The input system already supplies logical canvas coordinates; backing pixels are a rendering detail.

Expand Down
6 changes: 4 additions & 2 deletions src/core/Application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1122,10 +1122,12 @@ export class Application<Registry extends SceneRegistryShape<Registry> = {}> {
* {@link Pointer.x}/{@link Pointer.y} and node positions, e.g. `0..app.width`
* - to a world position using the active camera. At the default centered
* camera this is the identity; with a panned/zoomed/rotated camera it undoes
* the transform. Equivalent to `app.rendering.view.screenToWorld(x, y)`.
* the transform, and it honours a camera whose viewport covers only part of
* the canvas. Equivalent to
* `app.rendering.view.screenToWorld(x, y, app.width, app.height)`.
*/
public screenToWorld(x: number, y: number): PointLike {
return this._rendering.view.screenToWorld(x, y);
return this._rendering.view.screenToWorld(x, y, this.width, this.height);
}

/**
Expand Down
8 changes: 5 additions & 3 deletions src/input/InteractionSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1633,15 +1633,15 @@ export class InteractionSystem {
const uiRoot = this._app.scenes.currentScene?._peekUI() ?? null;

if (uiRoot !== null) {
const ui = this._app.rendering.screenView.screenToWorld(x, y);
const ui = this._designToLayerSpace(x, y, true);
const uiHit = this._hitTestNode(uiRoot, ui.x, ui.y);

if (uiHit !== null) {
return { node: uiHit, x: ui.x, y: ui.y };
}
}

const world = this._app.rendering.view.screenToWorld(x, y);
const world = this._designToLayerSpace(x, y, false);

return { node: this._hitTest(world.x, world.y), x: world.x, y: world.y };
}
Expand All @@ -1665,7 +1665,9 @@ export class InteractionSystem {
private _designToLayerSpace(x: number, y: number, ui: boolean): PointLike {
const view = ui ? this._app.rendering.screenView : this._app.rendering.view;

return view.screenToWorld(x, y);
// Map over the logical surface rather than the view's own design space:
// the active camera may cover only part of the canvas (split screen).
return view.screenToWorld(x, y, this._app.width, this._app.height);
}

/** Whether `node` lives inside the active scene's UI layer. */
Expand Down
2 changes: 1 addition & 1 deletion src/input/Pointer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ export enum PointerState {
*
* Coordinates are stored in the application's logical space - `app.width` /
* `app.height` units (`0..app.width` × `0..app.height`), matching node
* positions, the 2-argument {@link View.screenToWorld}, and the active camera.
* positions and {@link Application.screenToWorld}.
* The CSS-pixel event coordinates are mapped through the canvas's display rect,
* so the value is independent of {@link Application.pixelRatio}, of the render
* resolution, and of however the canvas is scaled for display: a click at its
Expand Down
9 changes: 9 additions & 0 deletions src/rendering/RenderingContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,16 @@ export class RenderingContext implements DrawContext {
* Preserves the camera's center and zoom; only the visible area size changes.
*/
public resize(width: number, height: number): void {
const zoom = this._view.zoomLevel;

// `View.resize` resets zoom along with the design space; reapply it so a
// zoomed camera keeps its apparent scale across canvas resizes.
this._view.resize(width, height);

if (zoom !== 1) {
this._view.setZoom(zoom);
}

this._screenView.resize(width, height);
this._screenView.setCenter(width / 2, height / 2);
}
Expand Down
Loading
Loading