const Camera = require("../render/Camera.js");
const Common = require("../core/Common.js");
const vec = require("../geometry/vec.js");
/**
* Main render object that handles the camera, pixel ratio, resizing, what is rendered, etc
*
* ## Events
* | Name | Description | Arguments |
* | ---- | ----------- | --------- |
* | beforeUpdate | Before the render scene is moved to match the camera position and scale. Triggered every frame. | None |
* | afterUpdate | After the render scene is moved to match the camera position and scale. Triggered every frame. | None |
*/
class Render {
static defaultOptions = {
background: "transparent",
pixelRatio: window.devicePixelRatio ?? 1,
ySort: false,
parentElement: window,
antialias: true,
scaleMode: PIXI.SCALE_MODES.LINEAR,
getBoundSize: function(width, height) {
return Math.sqrt(width * height) || 1;
}
}
app = null;
camera = null;
pixelRatio = 1;
parentElement = null;
_parentBoundingBox;
/**
*
* @param {object} options - Render options
* @param {string} [options.background="transparent"] - Background color, such as `"#FFFFFF00"`, `"rgb(0, 0, 0)"`, or `"transparent"`
* @param {number} [options.pixelRatio=devicePixelRatio] - Render resolution percent, use default unless you have a reason to change it
* @param {boolean} [options.ySort=false] - Whether to sort the render layer of bodies by their y coordinate
* @param {*} [options.parentElement=window] - What the canvas element will be appended to. The canvas resizes to fit this element. Only set to window if the body has `overflow: hidden`, otherwise create a wrapper element.
* @param {boolean} [options.antialias=true] - If the render should have antialiasing
* @param {boolean} [options.scaleMode=PIXI.SCALE_MODES.LINEAR] - See [PIXI.js scale modes](https://api.pixijs.io/@pixi/constants/PIXI/SCALE_MODES.html)
* @param {function} [options.getBoundSize=function(width, height)] - Function that determines the bound size, which is how big the view should be based on the canvas width and height
*/
constructor(options = {}) {
// Test if PIXI is loaded
try { PIXI.settings; }
catch(err) {
throw new Error("PIXI is not defined\nHelp: try loading pixi.js before creating a ter app");
}
// Load options
let defaults = { ...Render.defaultOptions };
let parentElement = options.parentElement ?? defaults.parentElement;
if (parentElement === window) parentElement = document.body;
this.parentElement = parentElement;
delete options.parentElement;
Common.merge(defaults, options, 1);
options = defaults;
let { background, ySort, pixelRatio, antialias, getBoundSize, scaleMode } = options;
// Create camera
this.camera = new Camera(this);
// Setup bound size
this.getBoundSize = getBoundSize;
// Set basic settings
PIXI.settings.SCALE_MODE = scaleMode;
PIXI.settings.RESOLUTION = this.pixelRatio = pixelRatio;
PIXI.Filter.defaultResolution = 0;
PIXI.Container.defaultSortableChildren = true
// Parse background color
background = Common.parseColor(background);
// Create PIXI app
let app = this.app = new PIXI.Application({
background: background[0],
backgroundAlpha: background[1],
resizeTo: parentElement == document.body ? window : parentElement,
antialias: antialias ?? true,
});
parentElement.appendChild(app.view);
app.ticker.add(this.update.bind(this)); // Start render
app.stage.filters = []; // Makes working with pixi filters easier
app.stage.sortableChildren = true; // Important so render layers work
// Make sure canvas stays correct size
this.#setSize(app.screen.width, app.screen.height);
app.renderer.on("resize", this.#setSize.bind(this));
// Set up y sorting if enabled
if (ySort) {
app.stage.on("sort", function beforeSort(sprite) {
sprite.zOrder = sprite.y;
});
}
}
#getElementSize(element) {
let boundingRect;
if (element === window || element === document.body) {
let view = this.app.view;
boundingRect = { width: window.innerWidth, height: window.innerHeight, top: view.offsetTop, left: view.offsetLeft, };
}
else {
boundingRect = {
top: element.offsetTop,
left: element.offsetLeft,
width: element.offsetWidth,
height: element.offsetHeight,
}
}
this._parentBoundingBox = boundingRect;
return boundingRect;
}
#setSize(width, height) {
this.camera.boundSize = this.getBoundSize(width, height);
let view = this.app.view;
let { width: elemWidth, height: elemHeight } = this.#getElementSize(this.parentElement);
view.style.width = elemWidth + "px";
view.style.height = elemHeight + "px";
}
setPixelRatio(pixelRatio) {
this.pixelRatio = pixelRatio;
PIXI.settings.RESOLUTION = pixelRatio;
this.#setSize(this.app.screen.width, this.app.screen.height); // update bounds with new pixel ratio
}
/**
* Updates renderer and its camera. Triggers `beforeUpdate` and `afterUpdate` events on this Render.
* @param {number} delta - Frame time, in seconds
*/
update(delta) {
delta = delta / 60; // convert to ms
this.trigger("beforeUpdate");
let { app, camera } = this;
let { stage } = app;
let { position: cameraPosition, translation, fov, boundSize } = camera;
let screenSize = new vec(app.screen.width, app.screen.height);
let fovScale = boundSize / fov;
translation.set({ x: -cameraPosition.x * fovScale + screenSize.x/2, y: -cameraPosition.y * fovScale + screenSize.y/2 });
camera.scale = boundSize / fov;
// update camera position
stage.x = translation.x;
stage.y = translation.y;
stage.scale.x = camera.scale;
stage.scale.y = camera.scale;
this.trigger("afterUpdate");
}
// - Events
#events = {
beforeUpdate: [],
afterUpdate: [],
}
on(event, callback) {
if (this.#events[event]) {
this.#events[event].push(callback);
}
else {
console.warn(event + " is not a valid event");
}
}
off(event, callback) {
event = this.#events[event];
if (event.includes(callback)) {
event.splice(event.indexOf(callback), 1);
}
}
trigger(event) {
// Trigger each event
if (this.#events[event]) {
this.#events[event].forEach(callback => {
callback();
});
}
}
}
module.exports = Render;