@razorpay/blade
Version:
The Design System that powers Razorpay
914 lines (836 loc) • 40.5 kB
JavaScript
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