Source: Canvas.js

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=false] - Whether to preserve WebGL buffers until manually cleared
	 * @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: false,
			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 ('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 }