/* * @fileoverview * LightSphereController module provides a spherical interface for controlling light direction. * It creates a circular canvas-based UI element that allows users to interactively adjust * lighting direction through pointer interactions. */ /** * LightSphereController creates an interactive sphere UI for light direction control. * Features: * - Circular interface with gradient background * - Pointer-based interaction for light direction * - Configurable size, position, and colors * - Minimum theta angle constraint * - Visual feedback with gradient and marker */ class LightSphereController { /** * Creates a new LightSphereController instance. * @param {HTMLElement|string} parent - Parent element or selector where the controller will be mounted * @param {Object} [options] - Configuration options * @param {number} [options.width=128] - Width of the controller in pixels * @param {number} [options.height=128] - Height of the controller in pixels * @param {number} [options.top=60] - Top position offset in pixels * @param {number} [options.right=0] - Right position offset in pixels * @param {number} [options.thetaMin=0] - Minimum theta angle in degrees (constrains interaction radius) * @param {string} [options.colorSpot='#ffffff'] - Color of the central spot in the gradient * @param {string} [options.colorBkg='#0000ff'] - Color of the outer edge of the gradient * @param {string} [options.colorMark='#ff0000'] - Color of the position marker */ constructor(parent, options) { options = Object.assign({ width: 128, height: 128, top: 60, right: 0, thetaMin: 0, colorSpot: '#ffffff', colorBkg: '#0000ff', colorMark: '#ff0000' }, options); Object.assign(this, options); this.parent = parent; this.layers = []; if (typeof (this.parent) == 'string') this.parent = document.querySelector(this.parent); this.lightDir = [0, 0]; this.containerElement = document.createElement('div'); this.containerElement.style = `padding: 0; position: absolute; width: ${this.width}px; height: ${this.height}px; top:${this.top}px; right:${this.right}px; z-index: 200; touch-action: none; visibility: visible;`; this.containerElement.classList.add('openlime-lsc'); const sd = (this.width * 0.5) * (1 - 0.8); this.dlCanvas = document.createElement('canvas'); this.dlCanvas.width = this.width; this.dlCanvas.height = this.height; // this.dlCanvas.style = '' this.dlCanvasCtx = this.dlCanvas.getContext("2d"); this.dlGradient = ''; this.containerElement.appendChild(this.dlCanvas); this.parent.appendChild(this.containerElement); this.r = this.width * 0.5; this.thetaMinRad = this.thetaMin / 180.0 * Math.PI; this.rmax = this.r * Math.cos(this.thetaMinRad); this.interactLightDir(this.width * 0.5, this.height * 0.5); this.pointerDown = false; this.dlCanvas.addEventListener("pointerdown", (e) => { this.pointerDown = true; const rect = this.dlCanvas.getBoundingClientRect(); let clickPosX = (this.dlCanvas.width * (e.clientX - rect.left)) / rect.width; let clickPosY = (this.dlCanvas.height * (e.clientY - rect.top)) / rect.height; this.interactLightDir(clickPosX, clickPosY); e.preventDefault(); }); this.dlCanvas.addEventListener("pointermove", (e) => { if (this.pointerDown) { const rect = this.dlCanvas.getBoundingClientRect(); let clickPosX = (this.dlCanvas.width * (e.clientX - rect.left)) / rect.width; let clickPosY = (this.dlCanvas.height * (e.clientY - rect.top)) / rect.height; this.interactLightDir(clickPosX, clickPosY); e.preventDefault(); } }); this.dlCanvas.addEventListener("pointerup", (e) => { this.pointerDown = false; }); this.dlCanvas.addEventListener("pointerout", (e) => { this.pointerDown = false; }); } /** * Adds a layer to be controlled by this light sphere. * The layer must support light control operations. * @param {Layer} layer - Layer to be controlled */ addLayer(l) { this.layers.push(l); } /** * Makes the controller visible. * @returns {string} The visibility style value */ show() { return this.containerElement.style.visibility = 'visible'; } /** * Hides the controller. * @returns {string} The visibility style value */ hide() { return this.containerElement.style.visibility = 'hidden'; } /** * Computes the radial gradient based on current light direction. * Creates a gradient that provides visual feedback about the light position. * @private */ computeGradient() { const x = (this.lightDir[0] + 1.0) * this.dlCanvas.width * 0.5; const y = (-this.lightDir[1] + 1.0) * this.dlCanvas.height * 0.5; this.dlGradient = this.dlCanvasCtx.createRadialGradient( x, y, this.dlCanvas.height / 8.0, x, y, this.dlCanvas.width / 1.2 ); this.dlGradient.addColorStop(0, this.colorSpot); this.dlGradient.addColorStop(1, this.colorBkg); } /** * Handles interaction to update light direction. * Converts pointer position to light direction vector while respecting constraints. * @private * @param {number} x - X coordinate in canvas space * @param {number} y - Y coordinate in canvas space */ interactLightDir(x, y) { let xc = x - this.r; let yc = this.r - y; const phy = Math.atan2(yc, xc); let l = Math.sqrt(xc * xc + yc * yc); l = l > this.rmax ? this.rmax : l; xc = l * Math.cos(this.thetaMinRad) * Math.cos(phy); yc = l * Math.cos(this.thetaMinRad) * Math.sin(phy); x = xc + this.r; y = this.r - yc; this.lightDir[0] = 2 * (x / this.dlCanvas.width - 0.5); this.lightDir[1] = 2 * (1 - y / this.dlCanvas.height - 0.5); // console.log('LD ', this.lightDir); for (const l of this.layers) { if (l.controls.light) l.setControl('light', this.lightDir, 5); } this.computeGradient(); this.drawLightSelector(x, y); } /** * Draws the light direction selector UI. * Renders: * - Circular background with gradient * - Position marker at current light direction * @private * @param {number} x - X coordinate for position marker * @param {number} y - Y coordinate for position marker */ drawLightSelector(x, y) { this.dlCanvasCtx.clearRect(0, 0, this.dlCanvas.width, this.dlCanvas.height); this.dlCanvasCtx.beginPath(); this.dlCanvasCtx.arc( this.dlCanvas.width / 2, this.dlCanvas.height / 2, this.dlCanvas.width / 2, 0, 2 * Math.PI ); this.dlCanvasCtx.fillStyle = this.dlGradient; this.dlCanvasCtx.fill(); this.dlCanvasCtx.beginPath(); this.dlCanvasCtx.arc(x, y, this.dlCanvas.width / 30, 0, 2 * Math.PI); this.dlCanvasCtx.strokeStyle = this.colorMark; this.dlCanvasCtx.lineWidth = 2; this.dlCanvasCtx.stroke(); } } /** * Example usage of LightSphereController: * ```javascript * // Create controller with custom options * const lightController = new LightSphereController('#container', { * width: 200, * height: 200, * top: 80, * right: 20, * thetaMin: 15, * colorSpot: '#ffff00', * colorBkg: '#000066', * colorMark: '#ff3333' * }); * * // Add layers to be controlled * lightController.addLayer(layer1); * lightController.addLayer(layer2); * * // Show/hide controller * lightController.show(); * lightController.hide(); * ``` * * @property {number[]} lightDir - Current light direction vector [x, y] * @property {HTMLElement} containerElement - Main container element * @property {HTMLCanvasElement} dlCanvas - Canvas element for drawing * @property {CanvasRenderingContext2D} dlCanvasCtx - Canvas 2D rendering context * @property {CanvasGradient} dlGradient - Current radial gradient * @property {number} r - Radius of the control sphere * @property {number} thetaMinRad - Minimum theta angle in radians * @property {number} rmax - Maximum interaction radius based on thetaMin * @property {boolean} pointerDown - Whether pointer is currently pressed * @property {Layer[]} layers - Array of layers being controlled */ export { LightSphereController }