import { Transform } from './Transform.js'
import { BoundingBox } from './BoundingBox.js'
import { addSignals } from './Signals.js'
import { Layer } from './Layer.js'
import { Cache } from './Cache.js'
//// HELPERS
window.structuredClone = typeof (structuredClone) == "function" ? structuredClone : function (value) { return JSON.parse(JSON.stringify(value)); };
/**
* Canvas class that manages WebGL context, layers, and scene rendering.
* Handles layer management, WebGL context creation/restoration, and render timing.
*/
class Canvas {
/**
* Creates a new Canvas instance with WebGL context and overlay support.
* @param {HTMLCanvasElement|string} canvas - Canvas DOM element or selector
* @param {HTMLElement|string} overlay - Overlay DOM element or selector for decorations (annotations, glyphs)
* @param {Camera} camera - Scene camera instance
* @param {Object} [options] - Configuration options
* @param {Object} [options.layers] - Layer configurations mapping layer IDs to Layer instances
* @param {boolean} [options.preserveDrawingBuffer=true] -Required for snapshots, disable for performance
* @param {number} [options.targetfps=30] - Target frames per second for rendering
* @param {boolean} [options.srgb=true] - Whether to enable sRGB color space or display-P3 for the output framebuffer
* @param {boolean} [options.stencil=false] - Whether to enable stencil buffer support
* @param {boolean} [options.useOffscreenFramebuffer=true] - Whether to use offscreen framebuffer for rendering
* @fires Canvas#update
* @fires Canvas#updateSize
* @fires Canvas#ready
*/
constructor(canvas, overlay, camera, options) {
Object.assign(this, {
canvasElement: null,
preserveDrawingBuffer: true,
gl: null,
overlayElement: null,
camera: camera,
layers: {},
ready: false,
targetfps: 30,
fps: 0,
timing: [16], //records last 30 frames time from request to next draw, rolling, primed to avoid /0
timingLength: 5, //max number of timings.
overBudget: 0, //fraction of frames that took too long to render.
srgb: true, // Enable sRGB color space by default
isSrgbSimplified: true,
stencil: false, // Disable stencil buffer by default
useOffscreenFramebuffer: true, // Use offscreen framebuffer by default
// Framebuffer objects
offscreenFramebuffer: null,
offscreenTexture: null,
offscreenRenderbuffer: null,
_renderingToOffscreen: false, // Traccia se stiamo renderizzando sul framebuffer off-screen
signals: { 'update': [], 'updateSize': [], 'ready': [] },
// Split viewport properties
splitViewport: false,
leftLayers: [],
rightLayers: []
});
Object.assign(this, options);
this.init(canvas, overlay);
for (let id in this.layers)
this.addLayer(id, new Layer(this.layers[id]));
this.camera.addEvent('update', () => this.emit('update'));
}
/**
* Records render timing information and updates FPS statistics.
* @param {number} elapsed - Time elapsed since last frame in milliseconds
* @private
*/
addRenderTiming(elapsed) {
this.timing.push(elapsed);
while (this.timing.length > this.timingLength)
this.timing.shift();
this.overBudget = this.timing.filter(t => t > 1000 / this.targetfps).length / this.timingLength;
this.fps = 1000 / (this.timing.reduce((sum, a) => sum + a, 0) / this.timing.length);
}
/**
* Initializes WebGL context and sets up event listeners.
* @param {HTMLCanvasElement|string} canvas - Canvas element or selector
* @param {HTMLElement|string} overlay - Overlay element or selector
* @throws {Error} If canvas or overlay elements cannot be found or initialized
* @private
*/
init(canvas, overlay) {
if (!canvas)
throw "Missing element parameter"
if (typeof (canvas) == 'string') {
canvas = document.querySelector(canvas);
if (!canvas)
throw "Could not find dom element.";
}
if (!overlay)
throw "Missing element parameter"
if (typeof (overlay) == 'string') {
overlay = document.querySelector(overlay);
if (!overlay)
throw "Could not find dom element.";
}
if (!canvas.tagName)
throw "Element is not a DOM element"
if (canvas.tagName != "CANVAS")
throw "Element is not a canvas element";
this.canvasElement = canvas;
this.overlayElement = overlay;
/* test context loss */
/* canvas = WebGLDebugUtils.makeLostContextSimulatingCanvas(canvas);
canvas.loseContextInNCalls(1000); */
const glopt = {
antialias: false,
depth: false,
stencil: this.stencil,
preserveDrawingBuffer: this.preserveDrawingBuffer,
colorSpace: this.srgb ? 'srgb' : 'display-p3'
};
this.gl = this.gl ||
canvas.getContext("webgl2", glopt);
if (!this.gl)
throw new Error("Could not create a WebGL 2.0 context");
// Initialize offscreen framebuffer if enabled
if (this.useOffscreenFramebuffer) {
this.setupOffscreenFramebuffer();
}
canvas.addEventListener("webglcontextlost", (event) => { console.log("Context lost."); event.preventDefault(); }, false);
canvas.addEventListener("webglcontextrestored", () => { this.restoreWebGL(); }, false);
document.addEventListener("visibilitychange", (event) => { if (this.gl.isContextLost()) { this.restoreWebGL(); } });
this.hasFloatRender = !!this.gl.getExtension('EXT_color_buffer_float');
this.hasLinearFloat = !!this.gl.getExtension('OES_texture_float_linear');
console.log('Support for rendering to float textures:', this.hasFloatRender);
console.log('Support for linear filtering on float textures:', this.hasLinearFloat);
}
/**
* Sets up the offscreen framebuffer for rendering
* @private
*/
setupOffscreenFramebuffer() {
const gl = this.gl;
// Create a framebuffer
this.offscreenFramebuffer = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, this.offscreenFramebuffer);
// Create a texture to render to
this.offscreenTexture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, this.offscreenTexture);
// Define size based on canvas size
const width = this.canvasElement.width;
const height = this.canvasElement.height;
// Initialize texture with null (we'll resize it properly in resizeOffscreenFramebuffer)
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
// Set texture parameters
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
// If stencil is enabled, create a renderbuffer for it
if (this.stencil) {
this.offscreenRenderbuffer = gl.createRenderbuffer();
gl.bindRenderbuffer(gl.RENDERBUFFER, this.offscreenRenderbuffer);
gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_STENCIL, width, height);
gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, gl.RENDERBUFFER, this.offscreenRenderbuffer);
}
// Attach the texture to the framebuffer
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.offscreenTexture, 0);
// Check framebuffer status
const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
if (status !== gl.FRAMEBUFFER_COMPLETE) {
console.error("Framebuffer not complete. Status:", status);
// Fall back to direct rendering
this.useOffscreenFramebuffer = false;
}
// Unbind framebuffer to restore default
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.bindTexture(gl.TEXTURE_2D, null);
if (this.stencil) {
gl.bindRenderbuffer(gl.RENDERBUFFER, null);
}
}
/**
* Resizes the offscreen framebuffer when canvas size changes
* @private
*/
resizeOffscreenFramebuffer() {
if (!this.useOffscreenFramebuffer || !this.offscreenFramebuffer) return;
const gl = this.gl;
const width = this.canvasElement.width;
const height = this.canvasElement.height;
// Resize texture
gl.bindTexture(gl.TEXTURE_2D, this.offscreenTexture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
// Resize renderbuffer if stencil is enabled
if (this.stencil && this.offscreenRenderbuffer) {
gl.bindRenderbuffer(gl.RENDERBUFFER, this.offscreenRenderbuffer);
gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_STENCIL, width, height);
}
gl.bindTexture(gl.TEXTURE_2D, null);
if (this.stencil) {
gl.bindRenderbuffer(gl.RENDERBUFFER, null);
}
}
/**
* Gets the currently active framebuffer.
* Use this when you need to save the state before changing framebuffers.
* @returns {WebGLFramebuffer} The currently active framebuffer
*/
getActiveFramebuffer() {
if (this.useOffscreenFramebuffer && this._renderingToOffscreen) {
return this.offscreenFramebuffer;
}
return null; // Rappresenta il framebuffer di default (schermo)
}
/**
* Sets the active framebuffer.
* Use this to restore a previously saved state.
* @param {WebGLFramebuffer} framebuffer - The framebuffer to activate
*/
setActiveFramebuffer(framebuffer) {
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, framebuffer);
this._renderingToOffscreen = (framebuffer === this.offscreenFramebuffer);
}
/**
* Updates the state of the canvas and its components.
* @param {Object} state - State object containing updates
* @param {Object} [state.camera] - Camera state updates
* @param {Object} [state.layers] - Layer state updates
* @param {number} dt - Animation duration in milliseconds
* @param {string} [easing='linear'] - Easing function for animations
*/
setState(state, dt, easing = 'linear') {
if(!state || typeof state !== 'object') return;
if ('camera' in state) {
const m = state.camera;
this.camera.setPosition(dt, m.x, m.y, m.z, m.a, easing);
}
if ('layers' in state)
for (const [k, layerState] of Object.entries(state.layers))
if (k in this.layers) {
const layer = this.layers[k];
layer.setState(layerState, dt, easing);
}
}
/**
* Retrieves current state of the canvas and its components.
* @param {Object} [stateMask=null] - Optional mask to filter returned state properties
* @returns {Object} Current state object
*/
getState(stateMask = null) {
let state = {};
if (!stateMask || stateMask.camera) {
let now = performance.now();
let m = this.camera.getCurrentTransform(now);
state.camera = { 'x': m.x, 'y': m.y, 'z': m.z, 'a': m.a };
}
state.layers = {};
for (let layer of Object.values(this.layers)) {
const layerMask = window.structuredClone(stateMask);
if (stateMask && stateMask.layers) Object.assign(layerMask, stateMask.layers[layer.id]);
state.layers[layer.id] = layer.getState(layerMask);
}
return state;
}
/**
* Restores WebGL context after loss.
* Reinitializes shaders and textures for all layers.
* @private
*/
restoreWebGL() {
let glopt = {
antialias: false,
depth: false,
stencil: this.stencil,
preserveDrawingBuffer: this.preserveDrawingBuffer,
colorSpace: this.srgb ? 'srgb' : 'display-p3'
};
this.gl = this.gl || this.canvasElement.getContext("webgl2", glopt);
// Recreate offscreen framebuffer
if (this.useOffscreenFramebuffer) {
if (this.offscreenFramebuffer) {
this.gl.deleteFramebuffer(this.offscreenFramebuffer);
}
if (this.offscreenTexture) {
this.gl.deleteTexture(this.offscreenTexture);
}
if (this.offscreenRenderbuffer) {
this.gl.deleteRenderbuffer(this.offscreenRenderbuffer);
}
this.setupOffscreenFramebuffer();
}
for (let layer of Object.values(this.layers)) {
layer.gl = this.gl;
layer.clear();
if (layer.shader)
layer.shader.restoreWebGL(this.gl);
}
this.prefetch();
this.emit('update');
}
/**
* Adds a layer to the canvas.
* @param {string} id - Unique identifier for the layer
* @param {Layer} layer - Layer instance to add
* @fires Canvas#update
* @fires Canvas#ready
* @throws {Error} If layer ID already exists
*/
addLayer(id, layer) {
console.assert(!(id in this.layers), "Duplicated layer id");
layer.id = id;
layer.addEvent('ready', () => {
if (Object.values(this.layers).every(l => l.status == 'ready')) {
this.ready = true;
this.emit('ready');
}
this.prefetch();
});
layer.addEvent('update', () => { this.emit('update'); });
layer.addEvent('updateSize', () => { this.updateSize(); });
layer.gl = this.gl;
layer.canvas = this;
layer.overlayElement = this.overlayElement;
layer.isSrgbSimplified = this.isSrgbSimplified;
this.layers[id] = layer;
this.prefetch();
}
/**
* Removes a layer from the canvas.
* @param {Layer} layer - Layer instance to remove
* @example
* const layer = new Layer(options);
* canvas.addLayer('map', layer);
* // ... later ...
* canvas.removeLayer(layer);
*/
removeLayer(layer) {
layer.clear(); //order is important.
delete this.layers[layer.id];
delete Cache.layers[layer];
this.prefetch();
}
updateSize() {
const discardHidden = false;
let sceneBBox = Layer.computeLayersBBox(this.layers, discardHidden);
let minScale = Layer.computeLayersMinScale(this.layers, discardHidden);
// Only update camera bounds if we have a valid bounding box and a viewport
if (sceneBBox && this.camera.viewport && !sceneBBox.isEmpty()) {
this.camera.updateBounds(sceneBBox, minScale);
}
// Resize offscreen framebuffer when canvas size changes
if (this.useOffscreenFramebuffer) {
this.resizeOffscreenFramebuffer();
}
this.emit('updateSize');
}
/**
* Enables or disables split viewport mode and sets which layers appear on each side
* @param {boolean} enabled - Whether split viewport mode is enabled
* @param {string[]} leftLayerIds - Array of layer IDs to show on left side
* @param {string[]} rightLayerIds - Array of layer IDs to show on right side
* @fires Canvas#update
*/
setSplitViewport(enabled, leftLayerIds = [], rightLayerIds = []) {
this.splitViewport = enabled;
this.leftLayers = leftLayerIds;
this.rightLayers = rightLayerIds;
this.emit('update');
}
/**
* Renders a frame at the specified time.
* @param {number} time - Current time in milliseconds
* @returns {boolean} True if all animations are complete
* @private
*/
draw(time) {
let gl = this.gl;
let view = this.camera.glViewport();
// Bind offscreen framebuffer if enabled
if (this.useOffscreenFramebuffer) {
gl.bindFramebuffer(gl.FRAMEBUFFER, this.offscreenFramebuffer);
this._renderingToOffscreen = true;
} else {
this._renderingToOffscreen = false;
}
gl.viewport(view.x, view.y, view.dx, view.dy);
var b = [0, 0, 0, 0];
gl.clearColor(b[0], b[1], b[2], b[3], b[4]);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
gl.enable(gl.BLEND);
let pos = this.camera.getGlCurrentTransform(time);
this.prefetch(pos);
//pos layers using zindex.
let ordered = Object.values(this.layers).sort((a, b) => a.zindex - b.zindex);
let done = true;
if (this.splitViewport) {
// For split viewport mode, we need to enable scissor test to split the rendering area
gl.enable(gl.SCISSOR_TEST);
const halfWidth = Math.floor(view.dx / 2);
// Draw left side (apply scissor to left half)
gl.scissor(view.x, view.y, halfWidth, view.dy);
for (let layer of ordered) {
if (this.leftLayers.includes(layer.id)) {
// Pass the full viewport but scissor will restrict drawing
done = layer.draw(pos, view) && done;
}
}
// Draw right side (apply scissor to right half)
gl.scissor(view.x + halfWidth, view.y, view.dx - halfWidth, view.dy);
for (let layer of ordered) {
if (this.rightLayers.includes(layer.id)) {
// Pass the full viewport but scissor will restrict drawing
done = layer.draw(pos, view) && done;
}
}
// Disable scissor when done
gl.disable(gl.SCISSOR_TEST);
} else {
// Standard rendering for normal mode
for (let layer of ordered) {
if (layer.visible)
done = layer.draw(pos, view) && done;
}
}
// Copy offscreen framebuffer to the screen if enabled
if (this.useOffscreenFramebuffer) {
// Switch to default framebuffer (the screen)
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
this._renderingToOffscreen = false;
// Draw the offscreen texture to the screen
this.drawOffscreenToCanvas();
}
// Use the isComplete flag from the transform instead of direct time comparison
return done && pos.isComplete;
}
/**
* Draws the offscreen framebuffer texture to the canvas
* @private
*/
drawOffscreenToCanvas() {
const gl = this.gl;
const view = this.camera.glViewport();
// Set viewport for the final display
gl.viewport(view.x, view.y, view.dx, view.dy);
// If we don't already have a fullscreen quad program, create one
if (!this._fullscreenQuadProgram) {
// Vertex shader
const vsSource = `#version 300 es
in vec4 aPosition;
in vec2 aTexCoord;
out vec2 vTexCoord;
void main() {
gl_Position = aPosition;
vTexCoord = aTexCoord;
}
`;
// Fragment shader
let fsSource = `#version 300 es
precision highp float;
in vec2 vTexCoord;
uniform sampler2D uTexture;
out vec4 fragColor;`;
if (this.isSrgbSimplified) {
fsSource += `
vec4 linear2srgb(vec4 linear) {
return vec4(pow(linear.rgb, vec3(1.0/2.2)), linear.a);
}`;
} else {
fsSource += `
vec4 linear2srgb(vec4 linear) {
bvec3 cutoff = lessThan(linear.rgb, vec3(0.0031308));
vec3 higher = vec3(1.055) * pow(linear.rgb, vec3(1.0/2.4)) - vec3(0.055);
vec3 lower = linear.rgb * vec3(12.92);
return vec4(mix(higher, lower, cutoff), linear.a);
}`;
}
fsSource += `
void main() {
fragColor = texture(uTexture, vTexCoord);
fragColor = linear2srgb(fragColor);
fragColor = clamp(fragColor, 0.0, 1.0);
}`;
// Create shader program
const vertexShader = this._createShader(gl, gl.VERTEX_SHADER, vsSource);
const fragmentShader = this._createShader(gl, gl.FRAGMENT_SHADER, fsSource);
this._fullscreenQuadProgram = this._createProgram(gl, vertexShader, fragmentShader);
// Get attribute and uniform locations
this._positionLocation = gl.getAttribLocation(this._fullscreenQuadProgram, 'aPosition');
this._texCoordLocation = gl.getAttribLocation(this._fullscreenQuadProgram, 'aTexCoord');
this._textureLocation = gl.getUniformLocation(this._fullscreenQuadProgram, 'uTexture');
// Create buffers for fullscreen quad
this._quadPositionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, this._quadPositionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
-1.0, 1.0, 0.0,
-1.0, -1.0, 0.0,
1.0, 1.0, 0.0,
1.0, -1.0, 0.0
]), gl.STATIC_DRAW);
this._quadTexCoordBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, this._quadTexCoordBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
0.0, 1.0,
0.0, 0.0,
1.0, 1.0,
1.0, 0.0
]), gl.STATIC_DRAW);
// Create vertex array object (VAO)
this._quadVAO = gl.createVertexArray();
gl.bindVertexArray(this._quadVAO);
// Set up position attribute
gl.bindBuffer(gl.ARRAY_BUFFER, this._quadPositionBuffer);
gl.enableVertexAttribArray(this._positionLocation);
gl.vertexAttribPointer(this._positionLocation, 3, gl.FLOAT, false, 0, 0);
// Set up texcoord attribute
gl.bindBuffer(gl.ARRAY_BUFFER, this._quadTexCoordBuffer);
gl.enableVertexAttribArray(this._texCoordLocation);
gl.vertexAttribPointer(this._texCoordLocation, 2, gl.FLOAT, false, 0, 0);
// Unbind VAO
gl.bindVertexArray(null);
}
// Set clear color and clear the screen
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT);
// Use the fullscreen quad program
gl.useProgram(this._fullscreenQuadProgram);
// Bind the VAO
gl.bindVertexArray(this._quadVAO);
// Bind the offscreen texture
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, this.offscreenTexture);
gl.uniform1i(this._textureLocation, 0);
// Draw the quad
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
// Unbind VAO and texture
gl.bindVertexArray(null);
gl.bindTexture(gl.TEXTURE_2D, null);
}
/**
* Helper method to create a shader
* @param {WebGL2RenderingContext} gl - WebGL context
* @param {number} type - Shader type (gl.VERTEX_SHADER or gl.FRAGMENT_SHADER)
* @param {string} source - Shader source code
* @returns {WebGLShader} Compiled shader
* @private
*/
_createShader(gl, type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error('Shader compilation error:', gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
return shader;
}
/**
* Helper method to create a shader program
* @param {WebGL2RenderingContext} gl - WebGL context
* @param {WebGLShader} vertexShader - Vertex shader
* @param {WebGLShader} fragmentShader - Fragment shader
* @returns {WebGLProgram} Linked shader program
* @private
*/
_createProgram(gl, vertexShader, fragmentShader) {
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.error('Program linking error:', gl.getProgramInfoLog(program));
gl.deleteProgram(program);
return null;
}
return program;
}
/**
* Schedules tile downloads based on current view.
* @param {Object} [transform] - Optional transform override, defaults to current camera transform
* @private
*/
prefetch(transform) {
if (!transform)
transform = this.camera.getGlCurrentTransform(performance.now());
for (let id in this.layers) {
let layer = this.layers[id];
//console.log(layer);
//console.log(layer.layout.status);
if (layer.visible && layer.status == 'ready') {
layer.prefetch(transform, this.camera.glViewport());
}
}
}
/**
* Cleanup resources when canvas is no longer needed
*/
dispose() {
const gl = this.gl;
// Clean up offscreen framebuffer resources
if (this.useOffscreenFramebuffer) {
if (this.offscreenFramebuffer) {
gl.deleteFramebuffer(this.offscreenFramebuffer);
this.offscreenFramebuffer = null;
}
if (this.offscreenTexture) {
gl.deleteTexture(this.offscreenTexture);
this.offscreenTexture = null;
}
if (this.offscreenRenderbuffer) {
gl.deleteRenderbuffer(this.offscreenRenderbuffer);
this.offscreenRenderbuffer = null;
}
}
// Clean up fullscreen quad resources
if (this._fullscreenQuadProgram) {
gl.deleteProgram(this._fullscreenQuadProgram);
this._fullscreenQuadProgram = null;
}
if (this._quadVAO) {
gl.deleteVertexArray(this._quadVAO);
this._quadVAO = null;
}
if (this._quadPositionBuffer) {
gl.deleteBuffer(this._quadPositionBuffer);
this._quadPositionBuffer = null;
}
if (this._quadTexCoordBuffer) {
gl.deleteBuffer(this._quadTexCoordBuffer);
this._quadTexCoordBuffer = null;
}
// Clean up layers
for (const id in this.layers) {
this.removeLayer(this.layers[id]);
}
}
}
/**
* Fired when canvas content is updated (layer changes, camera moves).
* @event Canvas#update
*/
/**
* Fired when canvas or layout size changes.
* @event Canvas#updateSize
*/
/**
* Fired when all layers are initialized and ready to display.
* @event Canvas#ready
*/
addSignals(Canvas, 'update', 'updateSize', 'ready');
export { Canvas }