Files
GDevelop/GDJS/Runtime/layer.ts
2025-08-11 10:30:28 +02:00

433 lines
14 KiB
TypeScript

/*
* GDevelop JS Platform
* Copyright 2013-2016 Florian Rival (Florian.Rival@gmail.com). All rights reserved.
* This project is released under the MIT License.
*/
namespace gdjs {
/**
* Represents a layer of a scene, used to display objects.
*/
export class Layer extends gdjs.RuntimeLayer {
_cameraRotation: float = 0;
/** The camera zoom factor strictly greater than 0. */
_zoomFactor: float = 1;
_cameraX: float;
_cameraY: float;
_cameraZ: float = 0;
/**
* `_cameraZ` is dirty when the zoom factor is set last.
*/
_isCameraZDirty: boolean = true;
/**
* @param layerData The data used to initialize the layer
* @param instanceContainer The container in which the layer is used
*/
constructor(
layerData: LayerData,
instanceContainer: gdjs.RuntimeInstanceContainer
) {
super(layerData, instanceContainer);
if (
this._defaultCameraBehavior ===
gdjs.RuntimeLayerDefaultCameraBehavior.TOP_LEFT_ANCHORED_IF_NEVER_MOVED
) {
// If top-left must stay in the top-left corner, this means we center the camera on the current size.
this._cameraX = this._runtimeScene.getViewportOriginX();
this._cameraY = this._runtimeScene.getViewportOriginY();
} else {
// Otherwise, the default camera position is the center of the initial viewport.
this._cameraX =
(this._runtimeScene.getInitialUnrotatedViewportMinX() +
this._runtimeScene.getInitialUnrotatedViewportMaxX()) /
2;
this._cameraY =
(this._runtimeScene.getInitialUnrotatedViewportMinY() +
this._runtimeScene.getInitialUnrotatedViewportMaxY()) /
2;
}
if (this.getCameraType() === gdjs.RuntimeLayerCameraType.ORTHOGRAPHIC) {
this._cameraZ =
(this._initialCamera3DFarPlaneDistance +
this._initialCamera3DNearPlaneDistance) /
2;
}
// Let the renderer do its final set up:
this._renderer.onCreated();
}
/**
* Called by the RuntimeScene whenever the game resolution size is changed.
* Updates the layer width/height and position.
*/
override onGameResolutionResized(
oldGameResolutionOriginX: float,
oldGameResolutionOriginY: float
): void {
// Adapt position of the camera center only if the camera has never moved as:
// * When the camera follows a player/object, it will rarely be at the default position.
// (and if is, it will be moved again by the behavior/events).
// * Cameras not following a player/object are usually UIs which are intuitively
// expected not to "move" (top-left stays "fixed"), while gameplay is "centered" (center stays "fixed").
//
// Note that anchor behavior is usually a better choice for UIs.
if (
this._defaultCameraBehavior ===
gdjs.RuntimeLayerDefaultCameraBehavior
.TOP_LEFT_ANCHORED_IF_NEVER_MOVED &&
// Have a safety margin of 1 pixel to avoid rounding errors.
Math.abs(this._cameraX - oldGameResolutionOriginX) < 1 &&
Math.abs(this._cameraY - oldGameResolutionOriginY) < 1 &&
this._zoomFactor === 1
) {
this._cameraX +=
this._runtimeScene.getViewportOriginX() - oldGameResolutionOriginX;
this._cameraY +=
this._runtimeScene.getViewportOriginY() - oldGameResolutionOriginY;
}
this._renderer.updatePosition();
this._renderer.updateResolution();
}
/**
* Change the camera center X position.
*
* @param cameraId The camera number. Currently ignored.
* @return The x position of the camera
*/
override getCameraX(cameraId?: integer): float {
this._forceDimensionUpdate();
return this._cameraX;
}
/**
* Change the camera center Y position.
*
* @param cameraId The camera number. Currently ignored.
* @return The y position of the camera
*/
override getCameraY(cameraId?: integer): float {
this._forceDimensionUpdate();
return this._cameraY;
}
/**
* Set the camera center X position.
*
* @param x The new x position
* @param cameraId The camera number. Currently ignored.
*/
override setCameraX(x: float, cameraId?: integer): void {
this._forceDimensionUpdate();
this._cameraX = x;
this._renderer.updatePosition();
}
/**
* Set the camera center Y position.
*
* @param y The new y position
* @param cameraId The camera number. Currently ignored.
*/
override setCameraY(y: float, cameraId?: integer): void {
this._forceDimensionUpdate();
this._cameraY = y;
this._renderer.updatePosition();
}
/**
* Get the camera width (which can be different than the game resolution width
* if the camera is zoomed).
*
* @param cameraId The camera number. Currently ignored.
* @return The width of the camera
*/
override getCameraWidth(cameraId?: integer): float {
return this.getWidth() / this._zoomFactor;
}
/**
* Get the camera height (which can be different than the game resolution height
* if the camera is zoomed).
*
* @param cameraId The camera number. Currently ignored.
* @return The height of the camera
*/
override getCameraHeight(cameraId?: integer): float {
return this.getHeight() / this._zoomFactor;
}
/**
* Set the zoom of a camera.
*
* @param newZoom The new zoom. Must be superior to 0. 1 is the default zoom.
* @param cameraId The camera number. Currently ignored.
*/
override setCameraZoom(newZoom: float, cameraId?: integer): void {
if (newZoom <= 0) {
return;
}
this._zoomFactor = newZoom;
this._isCameraZDirty = true;
this._renderer.updatePosition();
}
/**
* Get the zoom of a camera.
*
* @param cameraId The camera number. Currently ignored.
* @return The zoom.
*/
override getCameraZoom(cameraId?: integer): float {
return this._zoomFactor;
}
/**
* Set the camera center Z position.
*
* @param z The new y position.
* @param fov The field of view.
* @param cameraId The camera number. Currently ignored.
*/
override setCameraZ(z: float, fov: float | null, cameraId?: integer): void {
if (fov) {
const cameraFovInRadians = gdjs.toRad(fov);
// The zoom factor is capped to a not too big value to avoid infinity.
// MAX_SAFE_INTEGER is an arbitrary choice. It's big but not too big.
const zoomFactor = Math.min(
Number.MAX_SAFE_INTEGER,
(0.5 * this.getHeight()) / (z * Math.tan(0.5 * cameraFovInRadians))
);
if (zoomFactor > 0) {
this._zoomFactor = zoomFactor;
}
}
this._cameraZ = z;
this._isCameraZDirty = false;
this._renderer.updatePosition();
}
/**
* Get the camera center Z position.
*
* @param fov The field of view.
* @param cameraId The camera number. Currently ignored.
* @return The z position of the camera
*/
override getCameraZ(fov: float | null, cameraId?: integer): float {
if (!this._isCameraZDirty || !fov) {
return this._cameraZ;
}
// Set the camera so that it displays the whole PixiJS plane, as if it was a 2D rendering.
// The Z position is computed by taking the half height of the displayed rendering,
// and using the angle of the triangle defined by the field of view to compute the length
// of the triangle defining the distance between the camera and the rendering plane.
const cameraZPosition =
(0.5 * this.getHeight()) /
this.getCameraZoom() /
Math.tan(0.5 * gdjs.toRad(fov));
return cameraZPosition;
}
/**
* Get the rotation of the camera, expressed in degrees.
*
* @param cameraId The camera number. Currently ignored.
* @return The rotation, in degrees.
*/
override getCameraRotation(cameraId?: integer): float {
return this._cameraRotation;
}
/**
* Set the rotation of the camera, expressed in degrees.
* The rotation is made around the camera center.
*
* @param rotation The new rotation, in degrees.
* @param cameraId The camera number. Currently ignored.
*/
override setCameraRotation(rotation: float, cameraId?: integer): void {
this._cameraRotation = rotation;
this._renderer.updatePosition();
}
/**
* Convert a point from the canvas coordinates (for example,
* the mouse position) to the container coordinates.
*
* This method handles 3D rotations.
*
* @param x The x position, in canvas coordinates.
* @param y The y position, in canvas coordinates.
* @param cameraId The camera number. Currently ignored.
* @param result The point instance that is used to return the result.
*/
override convertCoords(
x: float,
y: float,
cameraId: integer = 0,
result: FloatPoint
): FloatPoint {
// This code duplicates applyLayerInverseTransformation for performance reasons;
// The result parameter used to be optional.
let position = result || [0, 0];
if (this._renderer.isCameraRotatedIn3D()) {
return this._renderer.transformTo3DWorld(x, y, 0, cameraId, result);
}
x -= this.getRuntimeScene()._cachedGameResolutionWidth / 2;
y -= this.getRuntimeScene()._cachedGameResolutionHeight / 2;
x /= this._zoomFactor;
y /= this._zoomFactor;
// Only compute angle and cos/sin once (allow heavy optimization from JS engines).
const angleInRadians = (this._cameraRotation / 180) * Math.PI;
const tmp = x;
const cosValue = Math.cos(angleInRadians);
const sinValue = Math.sin(angleInRadians);
x = cosValue * x - sinValue * y;
y = sinValue * tmp + cosValue * y;
position[0] = x + this.getCameraX(cameraId);
position[1] = y + this.getCameraY(cameraId);
return position;
}
/**
* Return an array containing the coordinates of the point passed as parameter
* in layer local coordinates (as opposed to the parent coordinates).
*
* All transformations (scale, rotation) are supported.
*
* This method doesn't handle 3D rotations.
*
* @param x The X position of the point, in parent coordinates.
* @param y The Y position of the point, in parent coordinates.
* @param result Array that will be updated with the result
* @param result The point instance that is used to return the result.
* (x and y position of the point in layer coordinates).
*/
override applyLayerInverseTransformation(
x: float,
y: float,
cameraId: integer,
result: FloatPoint
): FloatPoint {
x -= this._runtimeScene.getViewportOriginX();
y -= this._runtimeScene.getViewportOriginY();
x /= this._zoomFactor;
y /= this._zoomFactor;
// Only compute angle and cos/sin once (allow heavy optimization from JS engines).
const angleInRadians = (this._cameraRotation / 180) * Math.PI;
const tmp = x;
const cosValue = Math.cos(angleInRadians);
const sinValue = Math.sin(angleInRadians);
x = cosValue * x - sinValue * y;
y = sinValue * tmp + cosValue * y;
result[0] = x + this.getCameraX(cameraId);
result[1] = y + this.getCameraY(cameraId);
return result;
}
/**
* Convert a point from the container coordinates (for example,
* an object position) to the canvas coordinates.
*
* This method doesn't handle 3D rotations.
*
* @param x The x position, in container coordinates.
* @param y The y position, in container coordinates.
* @param cameraId The camera number. Currently ignored.
* @param result The point instance that is used to return the result.
*/
override convertInverseCoords(
x: float,
y: float,
cameraId: integer = 0,
result: FloatPoint
): FloatPoint {
// This code duplicates applyLayerTransformation for performance reasons;
// The result parameter used to be optional.
let position = result || [0, 0];
x -= this.getCameraX(cameraId);
y -= this.getCameraY(cameraId);
// Only compute angle and cos/sin once (allow heavy optimization from JS engines).
const angleInRadians = (this._cameraRotation / 180) * Math.PI;
const tmp = x;
const cosValue = Math.cos(-angleInRadians);
const sinValue = Math.sin(-angleInRadians);
x = cosValue * x - sinValue * y;
y = sinValue * tmp + cosValue * y;
x *= this._zoomFactor;
y *= this._zoomFactor;
position[0] = x + this.getRuntimeScene()._cachedGameResolutionWidth / 2;
position[1] = y + this.getRuntimeScene()._cachedGameResolutionHeight / 2;
return position;
}
/**
* Return an array containing the coordinates of the point passed as parameter
* in parent coordinate coordinates (as opposed to the layer local coordinates).
*
* All transformations (scale, rotation) are supported.
*
* This method doesn't handle 3D rotations.
*
* @param x The X position of the point, in layer coordinates.
* @param y The Y position of the point, in layer coordinates.
* @param result Array that will be updated with the result
* (x and y position of the point in parent coordinates).
*/
override applyLayerTransformation(
x: float,
y: float,
cameraId: integer,
result: FloatPoint
): FloatPoint {
x -= this.getCameraX(cameraId);
y -= this.getCameraY(cameraId);
// Only compute angle and cos/sin once (allow heavy optimization from JS engines).
const angleInRadians = (this._cameraRotation / 180) * Math.PI;
const tmp = x;
const cosValue = Math.cos(-angleInRadians);
const sinValue = Math.sin(-angleInRadians);
x = cosValue * x - sinValue * y;
y = sinValue * tmp + cosValue * y;
x *= this._zoomFactor;
y *= this._zoomFactor;
x += this._runtimeScene.getViewportOriginX();
y += this._runtimeScene.getViewportOriginY();
result[0] = x;
result[1] = y;
return result;
}
/**
* This ensure that the viewport dimensions are up to date.
*
* It's needed because custom objects dimensions are only updated on
* demand for efficiency reasons.
*/
private _forceDimensionUpdate(): void {
// This will update dimensions.
this._runtimeScene.getViewportWidth();
}
}
}