UNPKG

@razorpay/blade

Version:

The Design System that powers Razorpay

914 lines (836 loc) 40.5 kB
import _slicedToArray from '@babel/runtime/helpers/slicedToArray'; import _asyncToGenerator from '@babel/runtime/helpers/asyncToGenerator'; import _classCallCheck from '@babel/runtime/helpers/classCallCheck'; import _createClass from '@babel/runtime/helpers/createClass'; import _defineProperty from '@babel/runtime/helpers/defineProperty'; import _regeneratorRuntime from '@babel/runtime/regenerator'; import { rzpGlassVertexShader, rzpGlassFragmentShader } from './rzpGlassShader.js'; import { DEFAULT_CONFIG } from './presets.js'; import { isSafari, bestGuessBrowserZoom, loadImage, loadVideo } from './utils.js'; import { createProgram, setupFullscreenQuad, Texture } from './webgl-utils.js'; import { LEVEL_RENDER_SETTINGS, WebGLPerformanceController } from './PerformanceManager.js'; function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } // Reference resolution for zoom-independent displacement var REF_RESOLUTION = { width: 3000, height: 2000 }; // Default max pixel count (1920 * 1080 * 4 = 8,294,400 pixels) var DEFAULT_MAX_PIXEL_COUNT = 1920 * 1080 * 4; // Default styles for the shader container var defaultStyle = "@layer rzp-glass {\n :where([data-rzp-glass]) {\n isolation: isolate;\n position: relative;\n overflow: hidden;\n\n & canvas {\n contain: strict;\n display: block;\n position: absolute;\n z-index: -1;\n border-radius: inherit;\n pointer-events: none;\n }\n }\n}"; /** Map of config keys to uniform names */ var CONFIG_TO_UNIFORM = { enableDisplacement: 'uEnableDisplacement', enableColorama: 'uEnableColorama', enableBloom: 'uEnableBloom', enableLightSweep: 'uEnableLightSweep', inputMin: 'uInputMin', inputMax: 'uInputMax', modifyGamma: 'uModifyGamma', posterizeLevels: 'uPosterizeLevels', cycleRepetitions: 'uCycleRepetitions', phaseShift: 'uPhaseShift', cycleSpeed: 'uCycleSpeed', wrapMode: 'uWrapMode', reverse: 'uReverse', blendWithOriginal: 'uBlendWithOriginal', lightIntensity: 'uLightIntensity', lightStartFrame: 'uLightStartFrame', numSegments: 'uNumSegments', slitAngle: 'uSlitAngle', displacementX: 'uDisplacementX', displacementY: 'uDisplacementY', enableCenterElement: 'uEnableCenterElement', centerAnimDuration: 'uCenterAnimDuration', ccBlackPoint: 'uCCBlackPoint', ccWhitePoint: 'uCCWhitePoint', ccMidtoneGamma: 'uCCMidtoneGamma', ccGamma: 'uCCGamma', ccContrast: 'uCCContrast', zoom: 'uZoom', // panX and panY are combined into uPan (vec2) in setUniformValues // backgroundColor is handled separately (needs clear color update) edgeFeather: 'uEdgeFeather' }; var RzpGlassMount = /*#__PURE__*/function () { function RzpGlassMount(parentElement, assets) { var _this = this, _visualViewport2; var config = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; var frame = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 0; var _minPixelRatio = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 1; var _maxPixelCount = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : DEFAULT_MAX_PIXEL_COUNT; _classCallCheck(this, RzpGlassMount); _defineProperty(this, "program", null); _defineProperty(this, "uniformLocations", {}); _defineProperty(this, "uniformCache", {}); // Textures _defineProperty(this, "videoTexture", null); _defineProperty(this, "gradientMapTexture", null); _defineProperty(this, "gradientMap2Texture", null); _defineProperty(this, "centerGradientMapTexture", null); // Gradient map blend animation state _defineProperty(this, "currentGradientMapBlend", 0); // Video element _defineProperty(this, "video", null); _defineProperty(this, "videoFrameCallbackId", null); // Animation state (paper-shader style) _defineProperty(this, "rafId", null); /** Last render time in seconds */ _defineProperty(this, "lastRenderTime", 0); /** Frame count (increments every frame) */ _defineProperty(this, "currentFrame", 0); // Video-specific animation state /** Time for independent light animation (accumulates deltaTime) */ _defineProperty(this, "independentLightTime", 0); /** Last video animation time (for detecting jumps) */ _defineProperty(this, "lastVideoTime", 0); // State flags _defineProperty(this, "hasBeenDisposed", false); _defineProperty(this, "isInitialized", false); _defineProperty(this, "resolutionChanged", true); // Visible UV bounds (where container clips the canvas) // vec4(minX, minY, maxX, maxY) - portion of canvas UV that's visible _defineProperty(this, "visibleUvBounds", [0, 0, 1, 1]); // Resize handling _defineProperty(this, "resizeObserver", null); _defineProperty(this, "renderScale", 1); _defineProperty(this, "parentWidth", 0); _defineProperty(this, "parentHeight", 0); _defineProperty(this, "parentDevicePixelWidth", 0); _defineProperty(this, "parentDevicePixelHeight", 0); _defineProperty(this, "devicePixelsSupported", false); _defineProperty(this, "isSafariBrowser", isSafari()); // Performance monitoring _defineProperty(this, "performanceController", null); _defineProperty(this, "handleVisualViewportChange", function () { var _this$resizeObserver; // Restart resize observer to get fresh callback on zoom change (_this$resizeObserver = _this.resizeObserver) === null || _this$resizeObserver === void 0 || _this$resizeObserver.disconnect(); _this.setupResizeObserver(); }); _defineProperty(this, "handleResize", function () { var _visualViewport$scale, _visualViewport; // Container dimensions (use stored values or fallback to clientWidth/Height) var containerWidth = _this.parentWidth || _this.parentElement.clientWidth; var containerHeight = _this.parentHeight || _this.parentElement.clientHeight; var containerAspect = containerWidth / containerHeight; // "Cover" behavior: fill container while maintaining aspect ratio (crop overflow) var canvasWidth; var canvasHeight; var targetAspectRatio = _this.config.aspectRatio; if (containerAspect > targetAspectRatio) { // Container is wider than target - fit to width, crop top/bottom canvasWidth = containerWidth; canvasHeight = containerWidth / targetAspectRatio; } else { // Container is taller than target - fit to height, crop left/right canvasHeight = containerHeight; canvasWidth = containerHeight * targetAspectRatio; } // Center the canvas (overflow will be hidden by parent) var offsetX = (containerWidth - canvasWidth) / 2; var offsetY = (containerHeight - canvasHeight) / 2; // Calculate visible UV bounds (where container clips the canvas) // When canvas overflows container, we need to know which portion is visible var visibleMinX = -offsetX / canvasWidth; var visibleMaxX = (containerWidth - offsetX) / canvasWidth; var visibleMinY = -offsetY / canvasHeight; var visibleMaxY = (containerHeight - offsetY) / canvasHeight; _this.visibleUvBounds = [visibleMinX, visibleMinY, visibleMaxX, visibleMaxY]; // Set display size (CSS pixels) _this.canvasElement.style.width = "".concat(canvasWidth, "px"); _this.canvasElement.style.height = "".concat(canvasHeight, "px"); _this.canvasElement.style.left = "".concat(offsetX, "px"); _this.canvasElement.style.top = "".concat(offsetY, "px"); // Calculate target pixel dimensions for rendering var targetPixelWidth = 0; var targetPixelHeight = 0; var dpr = Math.max(1, window.devicePixelRatio); var pinchZoom = (_visualViewport$scale = (_visualViewport = visualViewport) === null || _visualViewport === void 0 ? void 0 : _visualViewport.scale) !== null && _visualViewport$scale !== void 0 ? _visualViewport$scale : 1; if (_this.devicePixelsSupported) { // Use real pixel size if we know it, but maintain aspect ratio // Calculate the scale ratio from parent to canvas (for aspect ratio correction) var canvasToParentRatioX = canvasWidth / containerWidth; var canvasToParentRatioY = canvasHeight / containerHeight; var scaleToMeetMinPixelRatio = Math.max(1, _this.minPixelRatio / dpr); // Apply aspect ratio correction to device pixel dimensions targetPixelWidth = _this.parentDevicePixelWidth * canvasToParentRatioX * scaleToMeetMinPixelRatio * pinchZoom; targetPixelHeight = _this.parentDevicePixelHeight * canvasToParentRatioY * scaleToMeetMinPixelRatio * pinchZoom; } else { // Approximate using devicePixelRatio var targetRenderScale = Math.max(dpr, _this.minPixelRatio) * pinchZoom; if (_this.isSafariBrowser) { // Safari reports physical devicePixelRatio, need to factor in zoom manually var zoomLevel = bestGuessBrowserZoom(); targetRenderScale *= Math.max(1, zoomLevel); } targetPixelWidth = Math.round(canvasWidth) * targetRenderScale; targetPixelHeight = Math.round(canvasHeight) * targetRenderScale; } // Prevent total rendered pixels from exceeding maxPixelCount var maxPixelCountHeadroom = Math.sqrt(_this.maxPixelCount) / Math.sqrt(targetPixelWidth * targetPixelHeight); var scaleToMeetMaxPixelCount = Math.min(1, maxPixelCountHeadroom); var newWidth = Math.round(targetPixelWidth * scaleToMeetMaxPixelCount); var newHeight = Math.round(targetPixelHeight * scaleToMeetMaxPixelCount); var newRenderScale = newWidth / Math.round(canvasWidth); if (_this.canvasElement.width !== newWidth || _this.canvasElement.height !== newHeight || _this.renderScale !== newRenderScale) { _this.renderScale = newRenderScale; _this.canvasElement.width = newWidth; _this.canvasElement.height = newHeight; _this.resolutionChanged = true; _this.gl.viewport(0, 0, newWidth, newHeight); // Only render immediately when the loop isn't running — if it is, // resolutionChanged=true is enough and the next RAF picks it up. // Calling render() while the loop is active spawns a duplicate RAF chain. if (_this.rafId === null) { _this.render(performance.now()); } } }); _defineProperty(this, "handleDocumentVisibilityChange", function () { if (document.hidden) { var _this$video; // Pause render loop when tab is hidden _this.stopRenderLoop(); (_this$video = _this.video) === null || _this$video === void 0 || _this$video.pause(); } else { // Resume render loop when tab is visible _this.startRenderLoop(); // Only resume video if not paused if (!_this.config.paused) { var _this$video2; (_this$video2 = _this.video) === null || _this$video2 === void 0 || _this$video2.play()["catch"](function () {}); } } }); _defineProperty(this, "render", function (currentTime) { if (_this.hasBeenDisposed) return; // ALWAYS schedule next frame first (like the original loop) _this.rafId = requestAnimationFrame(_this.render); if (_this.program === null) { console.warn('Tried to render before program was initialized'); return; } var gl = _this.gl; var video = _this.video; // Calculate delta time in seconds (framerate independent) var currentTimeSeconds = currentTime * 0.001; // Convert ms to seconds var deltaTime = currentTimeSeconds - _this.lastRenderTime; _this.lastRenderTime = currentTimeSeconds; _this.currentFrame++; // Increment frame count every frame var usingStaticImage = !video && _this.videoTexture !== null; // Skip rendering if video isn't ready — do NOT clear the canvas here so the // last rendered frame stays visible instead of flashing transparent/black. // This prevents flickering during video native loop boundary frames. if (!usingStaticImage) { if (!video || video.readyState < video.HAVE_CURRENT_DATA) { return; } // Update video texture (fallback for browsers without requestVideoFrameCallback) if (!('requestVideoFrameCallback' in video) && _this.videoTexture) { _this.videoTexture.update(video); } // Handle video looping within start/end time range (only when not paused) if (!_this.config.paused) { if (video.currentTime < _this.config.startTime || video.currentTime >= _this.config.endTime) { video.currentTime = _this.config.startTime; } } } // Clear canvas now that we know we have data to draw. // Doing this after the readyState guard means we keep the last frame // during any brief gaps (e.g. native video loop boundary). gl.clear(gl.COLOR_BUFFER_BIT); // Animation time: driven by video position for video mode, real elapsed time for static image var videoAnimTime = usingStaticImage ? currentTimeSeconds : video.currentTime - _this.config.startTime; // Update center animation time (always accumulate real time for smooth animation) if (_this.config.animateLightIndependently || usingStaticImage) { _this.independentLightTime += deltaTime; } else { var videoTimeDelta = videoAnimTime - _this.lastVideoTime; var isVideoJump = Math.abs(videoTimeDelta) > 0.1 || videoTimeDelta < -0.01; if (isVideoJump) { _this.independentLightTime = videoAnimTime; } else { _this.independentLightTime += deltaTime; } } _this.lastVideoTime = videoAnimTime; // Use program and set per-frame uniforms gl.useProgram(_this.program); // Time uniforms gl.uniform1f(_this.uniformLocations.uTime, currentTimeSeconds); // When animateLightIndependently is true, use real elapsed time for frame count // so light effects aren't tied to video playback var frameCount = _this.config.animateLightIndependently ? _this.independentLightTime * 30 // Use independent time (in seconds * 30fps) : videoAnimTime * 30; // Use video time (in seconds * 30fps) gl.uniform1f(_this.uniformLocations.uFrameCount, frameCount); gl.uniform1f(_this.uniformLocations.uCenterAnimTime, _this.independentLightTime); // Resolution uniforms (only when changed) if (_this.resolutionChanged) { gl.uniform2f(_this.uniformLocations.iResolution, _this.canvasElement.width, _this.canvasElement.height); gl.uniform1f(_this.uniformLocations.uDpr, _this.renderScale); // Update visible UV bounds (where container clips the canvas) gl.uniform4f(_this.uniformLocations.uVisibleUvBounds, _this.visibleUvBounds[0], _this.visibleUvBounds[1], _this.visibleUvBounds[2], _this.visibleUvBounds[3]); _this.resolutionChanged = false; } // Animate cycleRepetitions if enabled if (_this.config.animateCycleReps && _this.currentFrame > _this.config.cycleRepetitionsStartFrame) { var elapsed = _this.currentFrame - _this.config.cycleRepetitionsStartFrame; var cycleProgress = elapsed % (_this.config.cycleRepetitionsDuration * 2) / _this.config.cycleRepetitionsDuration; var pingPong = cycleProgress <= 1 ? cycleProgress : 2 - cycleProgress; var eased = pingPong * pingPong * (3 - 2 * pingPong); // smoothstep var delta = _this.config.cycleRepetitionsEnd - _this.config.cycleRepetitionsStart; gl.uniform1f(_this.uniformLocations.uCycleRepetitions, _this.config.cycleRepetitionsStart + eased * delta); } else { gl.uniform1f(_this.uniformLocations.uCycleRepetitions, _this.config.cycleRepetitions); } // Animate gradientMapBlend smoothly towards target var targetBlend = _this.config.gradientMapBlend; if (_this.currentGradientMapBlend !== targetBlend) { var speed = 1.0 / _this.config.gradientMapBlendDuration; var diff = targetBlend - _this.currentGradientMapBlend; var step = Math.sign(diff) * Math.min(Math.abs(diff), speed * deltaTime); _this.currentGradientMapBlend += step; gl.uniform1f(_this.uniformLocations.uGradientMapBlend, _this.currentGradientMapBlend); } // Draw gl.drawArrays(gl.TRIANGLES, 0, 6); }); _defineProperty(this, "handlePerformanceLevelChange", function (level) { if (level === 0) { _this.stopRenderLoop(); _this.canvasElement.style.display = 'none'; return; } var _LEVEL_RENDER_SETTING = LEVEL_RENDER_SETTINGS[level], maxPixelCount = _LEVEL_RENDER_SETTING.maxPixelCount, minPixelRatio = _LEVEL_RENDER_SETTING.minPixelRatio; _this.maxPixelCount = maxPixelCount; _this.minPixelRatio = minPixelRatio; // Restore canvas + render loop if they were previously hidden/stopped if (_this.canvasElement.style.display === 'none') { _this.canvasElement.style.display = ''; } if (_this.isInitialized) { _this.startRenderLoop(); } _this.handleResize(); }); this.parentElement = parentElement; this.assets = assets; this.config = _objectSpread(_objectSpread({}, DEFAULT_CONFIG), config); this.currentFrame = frame; this.minPixelRatio = _minPixelRatio; this.maxPixelCount = _maxPixelCount; // Inject default styles if not already present if (!document.querySelector('style[data-rzp-glass-style]')) { var styleElement = document.createElement('style'); styleElement.innerHTML = defaultStyle; styleElement.setAttribute('data-rzp-glass-style', ''); document.head.prepend(styleElement); } // Create canvas element this.canvasElement = document.createElement('canvas'); this.parentElement.prepend(this.canvasElement); this.parentElement.setAttribute('data-rzp-glass', ''); // Get WebGL context with alpha for transparency during loading var _gl = this.canvasElement.getContext('webgl', { antialias: false, premultipliedAlpha: false, depth: false, alpha: true, powerPreference: 'high-performance' }); this.gl = _gl; // Flip Y axis when uploading textures (video/images have Y=0 at top, WebGL has Y=0 at bottom) _gl.pixelStorei(_gl.UNPACK_FLIP_Y_WEBGL, true); // WebGL state setup (matching OGL defaults for 2D rendering) _gl.disable(_gl.DEPTH_TEST); _gl.disable(_gl.CULL_FACE); // Set clear color to transparent (for alpha blending during loading) _gl.clearColor(0, 0, 0, 0); // Initialize program this.initProgram(); // Initialize performance controller after the initProgram so we don't get a crash when the program is not initialized yet. this.performanceController = new WebGLPerformanceController({ gl: this.gl, onLevelChange: this.handlePerformanceLevelChange }); this.stopIfPotato(); this.setupPositionAttribute(); this.setupUniformLocations(); this.setupResizeObserver(); // Visual viewport listener for zoom changes (_visualViewport2 = visualViewport) === null || _visualViewport2 === void 0 || _visualViewport2.addEventListener('resize', this.handleVisualViewportChange); // Listen for visibility changes to pause when tab is hidden document.addEventListener('visibilitychange', this.handleDocumentVisibilityChange); } return _createClass(RzpGlassMount, [{ key: "stopIfPotato", value: function stopIfPotato() { var _this$performanceCont; if (!((_this$performanceCont = this.performanceController) !== null && _this$performanceCont !== void 0 && _this$performanceCont.isPotato())) { return; } this.stopRenderLoop(); throw new Error('RzpGlass: WebGL is not supported in this browser'); } /** * Load all assets (video or static image + gradient maps) and start rendering. * When `assets.imageSrc` is provided it is used as a static base texture and * no video element is created. */ }, { key: "loadAssets", value: (function () { var _loadAssets = _asyncToGenerator(/*#__PURE__*/_regeneratorRuntime.mark(function _callee() { var _this$assets$gradient, useStaticImage, gradientMap2Src, _yield$Promise$all, _yield$Promise$all2, baseAsset, gradientMap, gradientMap2, centerGradientMap, _t; return _regeneratorRuntime.wrap(function (_context) { while (1) switch (_context.prev = _context.next) { case 0: this.stopIfPotato(); _context.prev = 1; useStaticImage = Boolean(this.assets.imageSrc); gradientMap2Src = (_this$assets$gradient = this.assets.gradientMap2Src) !== null && _this$assets$gradient !== void 0 ? _this$assets$gradient : this.assets.gradientMapSrc; _context.next = 2; return Promise.all([useStaticImage ? loadImage(this.assets.imageSrc) : loadVideo(this.assets.videoSrc), loadImage(this.assets.gradientMapSrc), loadImage(gradientMap2Src), loadImage(this.assets.centerGradientMapSrc)]); case 2: _yield$Promise$all = _context.sent; _yield$Promise$all2 = _slicedToArray(_yield$Promise$all, 4); baseAsset = _yield$Promise$all2[0]; gradientMap = _yield$Promise$all2[1]; gradientMap2 = _yield$Promise$all2[2]; centerGradientMap = _yield$Promise$all2[3]; if (!useStaticImage) { _context.next = 3; break; } // Static image path — upload once to texture unit 0, no video loop needed this.setupImageTexture('uVideoTexture', baseAsset, 0); _context.next = 4; break; case 3: this.video = baseAsset; this.setupVideoTexture(); // Set video to start time and apply playback rate before playback this.video.currentTime = this.config.startTime; this.video.playbackRate = this.config.playbackRate; if (this.config.paused) { _context.next = 4; break; } _context.next = 4; return this.video.play()["catch"](function (e) { console.warn('Video autoplay failed:', e); }); case 4: this.setupImageTexture('uGradientMap', gradientMap, 1); this.setupImageTexture('uCenterGradientMap', centerGradientMap, 2); this.setupImageTexture('uGradientMap2', gradientMap2, 3); // Set initial uniform values this.setAllUniforms(); this.isInitialized = true; // Initial resize this.handleResize(); // Start the render loop (runs continuously) this.startRenderLoop(); _context.next = 6; break; case 5: _context.prev = 5; _t = _context["catch"](1); console.error('RzpGlass: Failed to load assets', _t); throw _t; case 6: case "end": return _context.stop(); } }, _callee, this, [[1, 5]]); })); function loadAssets() { return _loadAssets.apply(this, arguments); } return loadAssets; }()) }, { key: "initProgram", value: function initProgram() { var program = createProgram(this.gl, rzpGlassVertexShader, rzpGlassFragmentShader); if (!program) { throw new Error('RzpGlass: Failed to create WebGL program'); } this.program = program; } }, { key: "setupPositionAttribute", value: function setupPositionAttribute() { var buffers = setupFullscreenQuad(this.gl, this.program); if (!buffers) { throw new Error('RzpGlass: Failed to setup fullscreen quad'); } } }, { key: "setupUniformLocations", value: function setupUniformLocations() { var gl = this.gl; var program = this.program; // All uniform names from the shader var uniformNames = ['uTime', 'iResolution', 'uDpr', 'uVideoTexture', 'uGradientMap', 'uGradientMap2', 'uGradientMapBlend', 'uCenterGradientMap', 'uEnableDisplacement', 'uEnableColorama', 'uEnableBloom', 'uEnableLightSweep', 'uInputMin', 'uInputMax', 'uModifyGamma', 'uPosterizeLevels', 'uCycleRepetitions', 'uPhaseShift', 'uCycleSpeed', 'uWrapMode', 'uReverse', 'uBlendWithOriginal', 'uLightIntensity', 'uFrameCount', 'uLightStartFrame', 'uNumSegments', 'uSlitAngle', 'uDisplacementX', 'uDisplacementY', 'uEnableCenterElement', 'uCenterAnimDuration', 'uCenterAnimTime', 'uCCBlackPoint', 'uCCWhitePoint', 'uCCMidtoneGamma', 'uCCGamma', 'uCCContrast', 'uZoom', 'uPan', // vec2(panX, panY) - set in vertex shader 'uEdgeFeather', 'uRefResolution', 'uVisibleUvBounds', // vec4(minX, minY, maxX, maxY) - visible portion of canvas in UV space 'uBackgroundColor', // vec3(r, g, b) - background color to blend with // Ripple wave 'uEnableRippleWave', 'uRippleSpeed', 'uRippleBlend', 'uRippleAngularPower', 'uRippleRadialFalloff', 'uRippleWaitTime']; for (var _i = 0, _uniformNames = uniformNames; _i < _uniformNames.length; _i++) { var name = _uniformNames[_i]; this.uniformLocations[name] = gl.getUniformLocation(program, name); } } }, { key: "setupVideoTexture", value: function setupVideoTexture() { var _this2 = this; this.videoTexture = new Texture(this.gl, { textureUnit: 0 }); // Use requestVideoFrameCallback for efficient video texture updates if (this.video && 'requestVideoFrameCallback' in this.video) { var _updateVideoFrame = function updateVideoFrame() { if (_this2.hasBeenDisposed || !_this2.video || !_this2.videoTexture) return; _this2.videoTexture.update(_this2.video); _this2.videoFrameCallbackId = _this2.video.requestVideoFrameCallback(_updateVideoFrame); }; this.videoFrameCallbackId = this.video.requestVideoFrameCallback(_updateVideoFrame); } } }, { key: "setupImageTexture", value: function setupImageTexture(uniformName, image, textureUnit) { var texture = new Texture(this.gl, { textureUnit: textureUnit }); texture.image(image); if (uniformName === 'uVideoTexture') { this.videoTexture = texture; } else if (uniformName === 'uGradientMap') { this.gradientMapTexture = texture; } else if (uniformName === 'uGradientMap2') { this.gradientMap2Texture = texture; } else if (uniformName === 'uCenterGradientMap') { this.centerGradientMapTexture = texture; } } /** * Hot-swap the gradient map texture at runtime. * Accepts an HTMLCanvasElement (generated by generateGradientCanvas) or an HTMLImageElement. * No reinitialization required — the next frame will pick up the new texture. */ }, { key: "updateGradientMapTexture", value: function updateGradientMapTexture(source) { if (!this.isInitialized || !this.gradientMapTexture) return; this.gradientMapTexture.image(source); } }, { key: "setupResizeObserver", value: function setupResizeObserver() { var _this3 = this; this.resizeObserver = new ResizeObserver(function (_ref) { var _ref2 = _slicedToArray(_ref, 1), entry = _ref2[0]; if (entry !== null && entry !== void 0 && entry.borderBoxSize[0]) { var _entry$devicePixelCon; var physicalPixelSize = (_entry$devicePixelCon = entry.devicePixelContentBoxSize) === null || _entry$devicePixelCon === void 0 ? void 0 : _entry$devicePixelCon[0]; if (physicalPixelSize !== undefined) { _this3.devicePixelsSupported = true; _this3.parentDevicePixelWidth = physicalPixelSize.inlineSize; _this3.parentDevicePixelHeight = physicalPixelSize.blockSize; } _this3.parentWidth = entry.borderBoxSize[0].inlineSize; _this3.parentHeight = entry.borderBoxSize[0].blockSize; } _this3.handleResize(); }); this.resizeObserver.observe(this.parentElement); } }, { key: "setAllUniforms", value: function setAllUniforms() { var gl = this.gl; gl.useProgram(this.program); // Texture units gl.uniform1i(this.uniformLocations.uVideoTexture, 0); gl.uniform1i(this.uniformLocations.uGradientMap, 1); gl.uniform1i(this.uniformLocations.uCenterGradientMap, 2); gl.uniform1i(this.uniformLocations.uGradientMap2, 3); gl.uniform1f(this.uniformLocations.uGradientMapBlend, this.currentGradientMapBlend); // Set all config-based uniforms this.setUniformValues(this.config); // Explicitly set pan uniform (vertex shader uniform) gl.uniform2f(this.uniformLocations.uPan, this.config.panX, this.config.panY); // Reference resolution (constant) gl.uniform2f(this.uniformLocations.uRefResolution, REF_RESOLUTION.width, REF_RESOLUTION.height); // Background color (or set to -1 if not provided) if (this.config.backgroundColor) { var _this$config$backgrou = _slicedToArray(this.config.backgroundColor, 3), r = _this$config$backgrou[0], g = _this$config$backgrou[1], b = _this$config$backgrou[2]; gl.uniform3f(this.uniformLocations.uBackgroundColor, r, g, b); // Also set clear color to match gl.clearColor(r, g, b, 1.0); } else { // Set to -1 to indicate no background color blending gl.uniform3f(this.uniformLocations.uBackgroundColor, -1.0, -1.0, -1.0); // Use transparent clear color gl.clearColor(0, 0, 0, 0); } // Visible UV bounds (where container clips the canvas) gl.uniform4f(this.uniformLocations.uVisibleUvBounds, this.visibleUvBounds[0], this.visibleUvBounds[1], this.visibleUvBounds[2], this.visibleUvBounds[3]); } /** Check if uniform values are equal (handles arrays) */ }, { key: "areUniformValuesEqual", value: function areUniformValuesEqual(a, b) { var _this4 = this; if (a === b) return true; if (Array.isArray(a) && Array.isArray(b) && a.length === b.length) { return a.every(function (val, i) { return _this4.areUniformValuesEqual(val, b[i]); }); } return false; } /** Set uniform values with caching to avoid redundant updates */ }, { key: "setUniformValues", value: function setUniformValues(config) { var _this5 = this; var gl = this.gl; gl.useProgram(this.program); Object.entries(config).forEach(function (_ref3) { var _ref4 = _slicedToArray(_ref3, 2), key = _ref4[0], value = _ref4[1]; if (value === undefined) return; // Check if value has changed if (_this5.areUniformValuesEqual(_this5.uniformCache[key], value)) return; _this5.uniformCache[key] = value; // Get uniform name from config key var uniformName = CONFIG_TO_UNIFORM[key]; if (!uniformName) return; // Skip non-uniform config values var location = _this5.uniformLocations[uniformName]; if (!location) return; if (typeof value === 'boolean') { gl.uniform1f(location, value ? 1 : 0); } else if (typeof value === 'number') { gl.uniform1f(location, value); } else if (Array.isArray(value)) { // Handle array uniforms (cast to number[] since config values are all numbers/booleans) var flatArray = value.flat(); switch (flatArray.length) { case 2: gl.uniform2fv(location, flatArray); break; case 3: gl.uniform3fv(location, flatArray); break; case 4: gl.uniform4fv(location, flatArray); break; } } }); // Handle special pan uniform (vec2 in vertex shader) if (config.panX !== undefined || config.panY !== undefined) { var panCacheKey = 'pan'; var panValue = [this.config.panX, this.config.panY]; if (!this.areUniformValuesEqual(this.uniformCache[panCacheKey], panValue)) { this.uniformCache[panCacheKey] = panValue; gl.uniform2f(this.uniformLocations.uPan, panValue[0], panValue[1]); } } // Handle special backgroundColor uniform (vec3 in fragment shader) if (config.backgroundColor !== undefined) { var bgCacheKey = 'backgroundColor'; if (!this.areUniformValuesEqual(this.uniformCache[bgCacheKey], config.backgroundColor)) { this.uniformCache[bgCacheKey] = config.backgroundColor; if (config.backgroundColor) { var _config$backgroundCol = _slicedToArray(config.backgroundColor, 3), r = _config$backgroundCol[0], g = _config$backgroundCol[1], b = _config$backgroundCol[2]; gl.uniform3f(this.uniformLocations.uBackgroundColor, r, g, b); // Also update clear color to match gl.clearColor(r, g, b, 1.0); } else { // Set to -1 to indicate no background color blending gl.uniform3f(this.uniformLocations.uBackgroundColor, -1.0, -1.0, -1.0); // Use transparent clear color gl.clearColor(0, 0, 0, 0); } } } } /** * Update uniforms from config (partial update supported) */ }, { key: "setUniforms", value: function setUniforms(newConfig) { this.config = _objectSpread(_objectSpread({}, this.config), newConfig); if (!this.isInitialized) return; // Use the new caching-based uniform setter this.setUniformValues(newConfig); // Handle paused state changes - control video play/pause if (newConfig.paused !== undefined) { if (newConfig.paused) { var _this$video3; (_this$video3 = this.video) === null || _this$video3 === void 0 || _this$video3.pause(); } else { var _this$video4; (_this$video4 = this.video) === null || _this$video4 === void 0 || _this$video4.play()["catch"](function () {}); } } if (newConfig.playbackRate !== undefined && this.video) { this.video.playbackRate = newConfig.playbackRate; } if (newConfig.aspectRatio !== undefined) { this.handleResize(); } } }, { key: "startRenderLoop", value: function startRenderLoop() { if (this.rafId !== null) return; // Already running this.lastRenderTime = performance.now() * 0.001; // Initialize in seconds this.rafId = requestAnimationFrame(this.render); } }, { key: "stopRenderLoop", value: function stopRenderLoop() { if (this.rafId !== null) { cancelAnimationFrame(this.rafId); this.rafId = null; } } }, { key: "getCurrentFrame", value: // ===== Public API (paper-shader style) ===== /** Get the current animation frame (in ms) */ function getCurrentFrame() { return this.currentFrame; } /** Set a specific frame for deterministic results */ }, { key: "setFrame", value: function setFrame(newFrame) { this.currentFrame = newFrame; } /** Set the maximum pixel count for performance tuning */ }, { key: "setMaxPixelCount", value: function setMaxPixelCount() { var newMaxPixelCount = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : DEFAULT_MAX_PIXEL_COUNT; this.maxPixelCount = newMaxPixelCount; this.handleResize(); } /** Set the minimum pixel ratio for quality tuning */ }, { key: "setMinPixelRatio", value: function setMinPixelRatio() { var newMinPixelRatio = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 2; this.minPixelRatio = newMinPixelRatio; this.handleResize(); } /** Play video */ }, { key: "play", value: function play() { var _this$video5; this.config.paused = false; (_this$video5 = this.video) === null || _this$video5 === void 0 || _this$video5.play()["catch"](function () {}); } /** Pause video */ }, { key: "pause", value: function pause() { var _this$video6; this.config.paused = true; (_this$video6 = this.video) === null || _this$video6 === void 0 || _this$video6.pause(); } /** Seek to specific time in video */ }, { key: "setTime", value: function setTime(time) { if (this.video) { this.video.currentTime = time; } } }, { key: "dispose", value: /** Clean up all WebGL resources */ function dispose() { var _this$performanceCont2, _visualViewport3; // Immediately mark as disposed to prevent future renders this.hasBeenDisposed = true; // Cancel any pending RAF if (this.rafId !== null) { cancelAnimationFrame(this.rafId); this.rafId = null; } // Cancel video frame callback if (this.videoFrameCallbackId !== null && this.video && 'cancelVideoFrameCallback' in this.video) { this.video.cancelVideoFrameCallback(this.videoFrameCallbackId); } // Pause and remove video if (this.video) { this.video.pause(); this.video.src = ''; this.video.load(); this.video = null; } // Clean up WebGL resources if (this.gl && this.program) { var _this$videoTexture, _this$gradientMapText, _this$gradientMap2Tex, _this$centerGradientM; (_this$videoTexture = this.videoTexture) === null || _this$videoTexture === void 0 || _this$videoTexture.destroy(); (_this$gradientMapText = this.gradientMapTexture) === null || _this$gradientMapText === void 0 || _this$gradientMapText.destroy(); (_this$gradientMap2Tex = this.gradientMap2Texture) === null || _this$gradientMap2Tex === void 0 || _this$gradientMap2Tex.destroy(); (_this$centerGradientM = this.centerGradientMapTexture) === null || _this$centerGradientM === void 0 || _this$centerGradientM.destroy(); this.gl.deleteProgram(this.program); this.program = null; // Reset WebGL state this.gl.bindBuffer(this.gl.ARRAY_BUFFER, null); this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, null); this.gl.bindRenderbuffer(this.gl.RENDERBUFFER, null); this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, null); // Clear any errors this.gl.getError(); } // Clean up performance controller (_this$performanceCont2 = this.performanceController) === null || _this$performanceCont2 === void 0 || _this$performanceCont2.dispose(); this.performanceController = null; // Clean up observers and listeners if (this.resizeObserver) { this.resizeObserver.disconnect(); this.resizeObserver = null; } (_visualViewport3 = visualViewport) === null || _visualViewport3 === void 0 || _visualViewport3.removeEventListener('resize', this.handleVisualViewportChange); document.removeEventListener('visibilitychange', this.handleDocumentVisibilityChange); this.uniformLocations = {}; this.uniformCache = {}; // Remove canvas this.canvasElement.remove(); this.parentElement.removeAttribute('data-rzp-glass'); } }]); }(); export { RzpGlassMount }; //# sourceMappingURL=RzpGlassMount.js.map