import { Canvas } from './Canvas.js' import { Camera } from './Camera.js' import { PointerManager } from './PointerManager.js' import { Controller } from './Controller.js'; import { addSignals } from './Signals.js' /** * @typedef {Object} ViewerOptions * Configuration options for Viewer initialization * @property {string} [background] - CSS background style * @property {boolean} [autofit=true] - Auto-fit camera to scene * @property {Object} [canvas={}] - Canvas configuration options * @property {Camera} [camera] - Custom camera instance */ /** * @typedef {Object} Viewport * Viewport configuration * @property {number} x - Left coordinate * @property {number} y - Top coordinate * @property {number} dx - Width in pixels * @property {number} dy - Height in pixels * @property {number} w - Total width * @property {number} h - Total height */ /** * Fired when frame is drawn * @event Viewer#draw */ /** * Fired when viewer is resized * @event Viewer#resize * @property {Viewport} viewport - New viewport configuration */ /** * * Central class of the OpenLIME framework. * Creates and manages the main viewer interface, coordinates components, * and handles rendering pipeline. * * Core Responsibilities: * - Canvas management * - Layer coordination * - Camera control * - Event handling * - Rendering pipeline * - Resource management * * * * Component Relationships: * ``` * Viewer * ├── Canvas * │ └── Layers * ├── Camera * ├── PointerManager * └── Controllers * ``` * * Rendering Pipeline: * 1. Camera computes current transform * 2. Canvas prepares render state * 3. Layers render in order * 4. Post-processing applied * 5. Frame timing recorded * * Event System: * - draw: Emitted after each frame render * - resize: Emitted when viewport changes * * Performance Considerations: * - Uses requestAnimationFrame * - Tracks frame timing * - Handles device pixel ratio * - Optimizes redraw requests * * Resource Management: * - Automatic canvas cleanup * - Proper event listener removal * - ResizeObserver handling * * @fires Viewer#draw * @fires Viewer#resize * * @example * ```javascript * // Basic viewer setup * const viewer = new OpenLIME.Viewer('#container'); * * // Add image layer * const layer = new OpenLIME.Layer({ * layout: 'image', * type: 'image', * url: 'image.jpg' * }); * viewer.addLayer('main', layer); * * // Access components * const camera = viewer.camera; * const canvas = viewer.canvas; * ``` */ class Viewer { /** * Creates a new Viewer instance * @param {HTMLElement|string} div - Container element or selector * @param {ViewerOptions} [options] - Configuration options * @param {number} [options.idleTime=60] - Seconds of inactivity before idle event * @throws {Error} If container element not found * * Component Setup: * 1. Creates/configures canvas element * 2. Sets up overlay system * 3. Initializes camera * 4. Creates pointer manager * 5. Sets up resize observer * * @example * ```javascript * // Create with options * const viewer = new OpenLIME.Viewer('.container', { * background: '#000000', * autofit: true, * canvas: { * preserveDrawingBuffer: true * } * }); * ``` */ constructor(div, options) { Object.assign(this, { background: null, autofit: true, canvas: {}, camera: new Camera(), idleTime: 60 // in seconds }); if (typeof (div) == 'string') div = document.querySelector(div); if (!div) throw "Missing element parameter"; Object.assign(this, options); if (this.background) div.style.background = this.background; this.containerElement = div; this.canvasElement = div.querySelector('canvas'); if (!this.canvasElement) { this.canvasElement = document.createElement('canvas'); div.prepend(this.canvasElement); } this.overlayElement = document.createElement('div'); this.overlayElement.classList.add('openlime-overlay'); this.containerElement.appendChild(this.overlayElement); this.canvas = new Canvas(this.canvasElement, this.overlayElement, this.camera, this.canvas); this.canvas.addEvent('update', () => { this.redraw(); }); if (this.autofit) this.canvas.addEvent('updateSize', () => this.camera.fitCameraBox(0)); this.pointerManager = new PointerManager(this.overlayElement, { idleTime: this.idleTime }); this.canvasElement.addEventListener('contextmenu', (e) => { e.preventDefault(); return false; }); let resizeobserver = new ResizeObserver(entries => { for (let entry of entries) { this.resize(entry.contentRect.width, entry.contentRect.height); } }); resizeobserver.observe(this.canvasElement); this.resize(this.canvasElement.clientWidth, this.canvasElement.clientHeight); } /** * Adds a device event controller to the viewer. * @param {Controller} controller An OpenLIME controller. */ addController(controller) { this.pointerManager.onEvent(controller); } /** * Adds layer to viewer * @param {string} id - Unique layer identifier * @param {Layer} layer - Layer instance * @fires Canvas#update * * @example * ```javascript * const layer = new OpenLIME.Layer({ * type: 'image', * url: 'image.jpg' * }); * viewer.addLayer('background', layer); * ``` */ addLayer(id, layer) { this.canvas.addLayer(id, layer); this.redraw(); } /** * Removes layer from viewer * @param {Layer|string} layer - Layer instance or ID * @fires Canvas#update */ removeLayer(layer) { if (typeof (layer) == 'string') layer = this.canvas.layers[layer]; if (layer) { this.canvas.removeLayer(layer); this.redraw(); } } /** * Handles viewer resizing * @param {number} width - New width in CSS pixels * @param {number} height - New height in CSS pixels * @private * @fires Viewer#resize */ resize(width, height) { if (width == 0 || height == 0) return; // Test with retina display! this.canvasElement.width = width * window.devicePixelRatio; this.canvasElement.height = height * window.devicePixelRatio; let view = { x: 0, y: 0, dx: width, dy: height, w: width, h: height }; this.camera.setViewport(view); this.canvas.updateSize(); this.emit('resize', view); this.canvas.prefetch(); this.redraw(); } /** * Schedules next frame for rendering * Uses requestAnimationFrame for optimal performance */ redraw() { if (this.animaterequest) return; this.animaterequest = requestAnimationFrame((time) => { this.draw(time); }); this.requestTime = performance.now(); } /** * Performs actual rendering * @param {number} time - Current timestamp * @private * @fires Viewer#draw */ draw(time) { if (!time) time = performance.now(); this.animaterequest = null; let elapsed = performance.now() - this.requestTime; this.canvas.addRenderTiming(elapsed); let viewport = this.camera.viewport; let transform = this.camera.getCurrentTransform(time); let done = this.canvas.draw(time); if (!done) this.redraw(); this.emit('draw'); } } addSignals(Viewer, 'draw'); addSignals(Viewer, 'resize'); //args: viewport export { Viewer };