Spaces:
Running
Running
| /* ================================================================ | |
| WebGL Orb Renderer β ported from React/OGL to vanilla JS | |
| ================================================================ | |
| This file renders the glowing, animated orb that serves as the | |
| visual centerpiece / background element of the N.Y.R.A AI assistant | |
| UI. The orb is drawn entirely on the GPU using WebGL and GLSL | |
| shaders β no images or SVGs are involved. | |
| HOW IT WORKS (high-level): | |
| 1. A full-screen <canvas> is created inside a container element. | |
| 2. A WebGL context is obtained on that canvas. | |
| 3. A vertex shader positions a single full-screen triangle, and a | |
| fragment shader runs *per pixel* to compute the orb's color | |
| using 3D simplex noise, hue-shifting math, and procedural | |
| lighting. | |
| 4. An animation loop (requestAnimationFrame) feeds the shader a | |
| steadily increasing time value each frame, which makes the orb | |
| swirl, pulse, and react to state changes (e.g. "speaking"). | |
| KEY CONCEPTS FOR LEARNERS: | |
| - **Vertex shader**: runs once per vertex. Here it just maps our | |
| triangle so it covers the whole screen. | |
| - **Fragment shader**: runs once per *pixel*. This is where all the | |
| visual magic happens β noise, lighting, color mixing. | |
| - **Uniforms**: values we send from JavaScript into the shader each | |
| frame (time, resolution, color settings, etc.). | |
| - **Simplex noise** (snoise3): a smooth random function that gives | |
| the orb its organic, cloud-like movement. | |
| The class exposes a simple API: | |
| new OrbRenderer(containerEl, options) β start rendering | |
| .setActive(true/false) β pulse the orb (e.g. TTS speaking) | |
| .destroy() β tear everything down | |
| ================================================================ */ | |
| class OrbRenderer { | |
| /** | |
| * Creates a new OrbRenderer and immediately begins animating. | |
| * | |
| * @param {HTMLElement} container β the DOM element the canvas will fill. | |
| * @param {Object} opts β optional tweaks: | |
| * @param {number} opts.hue β base hue rotation in degrees (default 0). | |
| * @param {number} opts.hoverIntensity β strength of the wavy hover/active distortion (default 0.2). | |
| * @param {number[]} opts.backgroundColor β RGB triplet [r,g,b] each 0-1 (default dark navy). | |
| */ | |
| constructor(container, opts = {}) { | |
| this.container = container; | |
| this.hue = opts.hue ?? 0; | |
| this.hoverIntensity = opts.hoverIntensity ?? 0.2; | |
| this.bgColor = opts.backgroundColor ?? [0.02, 0.02, 0.06]; | |
| // Animation state β these are smoothly interpolated each frame | |
| // to avoid jarring jumps when setActive() is called. | |
| this.targetHover = 0; // where we want hover to be (0 or 1) | |
| this.currentHover = 0; // smoothly chases targetHover | |
| this.currentRot = 0; // cumulative rotation (radians) applied while active | |
| this.lastTs = 0; // timestamp of previous frame for delta-time calculation | |
| // Create and insert the drawing surface | |
| this.canvas = document.createElement('canvas'); | |
| this.canvas.style.width = '100%'; | |
| this.canvas.style.height = '100%'; | |
| this.container.appendChild(this.canvas); | |
| // Acquire a WebGL 1 context. | |
| // alpha:true lets the orb float over whatever is behind the canvas. | |
| // premultipliedAlpha:false keeps our alpha blending straightforward. | |
| this.gl = this.canvas.getContext('webgl', { alpha: true, premultipliedAlpha: false, antialias: false }); | |
| if (!this.gl) { console.warn('WebGL not available'); return; } | |
| // Compile shaders, create buffers, look up uniform locations | |
| this._build(); | |
| // Set the canvas resolution to match its CSS size Γ devicePixelRatio | |
| this._resize(); | |
| // Re-adjust whenever the browser window changes size | |
| this._onResize = this._resize.bind(this); | |
| window.addEventListener('resize', this._onResize); | |
| // Kick off the animation loop | |
| this._raf = requestAnimationFrame(this._loop.bind(this)); | |
| } | |
| /* ============================================================= | |
| VERTEX SHADER (GLSL) | |
| ============================================================= | |
| The vertex shader runs once for each vertex we send to the GPU | |
| (in our case just 3 β a single triangle that covers the whole | |
| screen). | |
| Inputs (attributes): | |
| position β the XY clip-space coordinate of this vertex. | |
| uv β a texture coordinate we pass through to the | |
| fragment shader so it knows where on the | |
| "screen rectangle" each pixel is. | |
| Output: | |
| gl_Position β the final clip-space position (vec4). | |
| vUv β passed to the fragment shader via a "varying". | |
| ============================================================= */ | |
| static VERT = ` | |
| precision highp float; | |
| attribute vec2 position; | |
| attribute vec2 uv; | |
| varying vec2 vUv; | |
| void main(){ vUv=uv; gl_Position=vec4(position,0.0,1.0); }`; | |
| /* ============================================================= | |
| FRAGMENT SHADER (GLSL) | |
| ============================================================= | |
| The fragment shader runs once for every pixel on screen. It | |
| receives the interpolated UV coordinate from the vertex shader | |
| and computes the final RGBA color for that pixel. | |
| UNIFORMS (values supplied from JavaScript every frame): | |
| iTime β elapsed time in seconds; drives all animation. | |
| iResolution β vec3(canvasWidth, canvasHeight, aspectRatio). | |
| hue β degree offset applied to the base palette via | |
| YIQ color-space rotation (lets you recolor the | |
| whole orb without changing any other code). | |
| hover β 0.0 β 1.0 interpolation: how "active" the orb | |
| is right now. Drives the wavy UV distortion. | |
| rot β current rotation angle (radians). Accumulated | |
| on the JS side while the orb is active. | |
| hoverIntensity β multiplier for the wavy UV distortion amplitude. | |
| backgroundColor β the scene's background color (RGB 0-1). The | |
| shader blends toward this so the orb sits | |
| naturally on any background. | |
| The shader contains several helper functions (explained inline | |
| below) and a main draw() routine that assembles the orb. | |
| ============================================================= */ | |
| static FRAG = ` | |
| precision highp float; | |
| uniform float iTime; | |
| uniform vec3 iResolution; | |
| uniform float hue; | |
| uniform float hover; | |
| uniform float rot; | |
| uniform float hoverIntensity; | |
| uniform vec3 backgroundColor; | |
| varying vec2 vUv; | |
| /* ----- Color-space conversion: RGB β YIQ ----- */ | |
| // YIQ is the color model used by NTSC television. Converting to | |
| // YIQ lets us rotate the hue of any color by simply rotating the | |
| // I and Q components, then converting back to RGB. | |
| vec3 rgb2yiq(vec3 c){float y=dot(c,vec3(.299,.587,.114));float i=dot(c,vec3(.596,-.274,-.322));float q=dot(c,vec3(.211,-.523,.312));return vec3(y,i,q);} | |
| vec3 yiq2rgb(vec3 c){return vec3(c.x+.956*c.y+.621*c.z,c.x-.272*c.y-.647*c.z,c.x-1.106*c.y+1.703*c.z);} | |
| // adjustHue: rotate a color's hue by 'hueDeg' degrees. | |
| // 1. Convert RGB β YIQ. | |
| // 2. Rotate the (I, Q) pair by the hue angle (2D rotation matrix). | |
| // 3. Convert YIQ β RGB. | |
| vec3 adjustHue(vec3 color,float hueDeg){float h=hueDeg*3.14159265/180.0;vec3 yiq=rgb2yiq(color);float cosA=cos(h);float sinA=sin(h);float i2=yiq.y*cosA-yiq.z*sinA;float q2=yiq.y*sinA+yiq.z*cosA;yiq.y=i2;yiq.z=q2;return yiq2rgb(yiq);} | |
| /* ----- 3D Simplex Noise (snoise3) ----- */ | |
| // Simplex noise is a smooth, natural-looking pseudo-random function | |
| // invented by Ken Perlin. Given a 3D coordinate it returns a value | |
| // roughly in [-1, 1]. By feeding (uv, time) we get animated, | |
| // organic-looking variation that drives the orb's wobbly edge. | |
| // | |
| // hash33: a cheap hash that maps a vec3 to a pseudo-random vec3 in | |
| // [-1, 1]. Used internally by the noise to create random | |
| // gradient vectors at each lattice point. | |
| vec3 hash33(vec3 p3){p3=fract(p3*vec3(.1031,.11369,.13787));p3+=dot(p3,p3.yxz+19.19);return -1.0+2.0*fract(vec3(p3.x+p3.y,p3.x+p3.z,p3.y+p3.z)*p3.zyx);} | |
| // snoise3: the actual 3D simplex noise implementation. | |
| // K1 and K2 are the skew/unskew constants for a 3D simplex grid. | |
| // The function: | |
| // 1. Skews the input into simplex (tetrahedral) space. | |
| // 2. Determines which simplex cell the point falls in. | |
| // 3. Computes distance vectors to each of the cell's 4 corners. | |
| // 4. For each corner, evaluates a radial falloff kernel multiplied | |
| // by the dot product of a pseudo-random gradient and the | |
| // distance vector. | |
| // 5. Sums the contributions and scales to roughly [-1, 1]. | |
| float snoise3(vec3 p){const float K1=.333333333;const float K2=.166666667;vec3 i=floor(p+(p.x+p.y+p.z)*K1);vec3 d0=p-(i-(i.x+i.y+i.z)*K2);vec3 e=step(vec3(0.0),d0-d0.yzx);vec3 i1=e*(1.0-e.zxy);vec3 i2=1.0-e.zxy*(1.0-e);vec3 d1=d0-(i1-K2);vec3 d2=d0-(i2-K1);vec3 d3=d0-0.5;vec4 h=max(0.6-vec4(dot(d0,d0),dot(d1,d1),dot(d2,d2),dot(d3,d3)),0.0);vec4 n=h*h*h*h*vec4(dot(d0,hash33(i)),dot(d1,hash33(i+i1)),dot(d2,hash33(i+i2)),dot(d3,hash33(i+1.0)));return dot(vec4(31.316),n);} | |
| // extractAlpha: the orb is rendered on a transparent background. | |
| // This helper takes an RGB color and derives an alpha from the | |
| // brightest channel. That way fully-black areas become transparent | |
| // and bright areas become opaque β giving us a soft-edged glow | |
| // without needing a separate alpha mask. | |
| vec4 extractAlpha(vec3 c){float a=max(max(c.r,c.g),c.b);return vec4(c/(a+1e-5),a);} | |
| /* ----- Palette & geometry constants ----- */ | |
| // Three base colors that define the orb's purple-cyan palette. | |
| // They get hue-shifted at runtime by the 'hue' uniform. | |
| const vec3 baseColor1=vec3(.611765,.262745,.996078); // vivid purple | |
| const vec3 baseColor2=vec3(.298039,.760784,.913725); // cyan / teal | |
| const vec3 baseColor3=vec3(.062745,.078431,.600000); // deep indigo | |
| const float innerRadius=0.6; // normalized radius of the orb's inner core | |
| const float noiseScale=0.65; // how zoomed-in the noise pattern is | |
| /* ----- Procedural light falloff helpers ----- */ | |
| // light1: inverse-distance falloff β I / (1 + dΒ·a) | |
| // light2: inverse-square falloff β I / (1 + dΒ²Β·a) | |
| // 'i' = intensity, 'a' = attenuation, 'd' = distance. | |
| // These give the orb its glowing highlight spots. | |
| float light1(float i,float a,float d){return i/(1.0+d*a);} | |
| float light2(float i,float a,float d){return i/(1.0+d*d*a);} | |
| /* ----- draw(): the core orb rendering routine ----- */ | |
| // Given a UV coordinate (centered, normalized so the short axis | |
| // spans -1 to 1), this function returns an RGBA color for that | |
| // pixel. | |
| // | |
| // Step-by-step: | |
| // 1. Hue-shift the three base colors. | |
| // 2. Convert the UV to polar-ish helpers (angle and length). | |
| // 3. Sample 3D simplex noise at (uv, time) to create organic, | |
| // time-varying distortion. | |
| // 4. Compute a wobbly radius (r0) from the noise β this is what | |
| // makes the edge of the orb undulate. | |
| // 5. Calculate multiple light/glow terms: | |
| // v0 β main glow field (radial, noise-modulated) | |
| // v1 β an orbiting highlight point | |
| // v2, v3 β radial fade masks that confine color to the orb | |
| // 6. Blend the base colors using the angular position (cl) so | |
| // the orb shifts between purple and cyan as you go around it. | |
| // 7. Compose a "dark" version and a "light" version of the orb, | |
| // then blend between them based on background luminance so | |
| // the orb looks good on both dark and light UIs. | |
| // 8. Pass the result through extractAlpha to get proper | |
| // transparency for compositing. | |
| vec4 draw(vec2 uv){ | |
| vec3 c1=adjustHue(baseColor1,hue);vec3 c2=adjustHue(baseColor2,hue);vec3 c3=adjustHue(baseColor3,hue); | |
| float ang=atan(uv.y,uv.x);float len=length(uv);float invLen=len>0.0?1.0/len:0.0; | |
| float bgLum=dot(backgroundColor,vec3(.299,.587,.114)); // perceptual luminance of the bg | |
| float n0=snoise3(vec3(uv*noiseScale,iTime*0.5))*0.5+0.5; // noise remapped to [0,1] | |
| float r0=mix(mix(innerRadius,1.0,0.4),mix(innerRadius,1.0,0.6),n0); // wobbly radius | |
| float d0=distance(uv,(r0*invLen)*uv); // distance from pixel to the wobbly edge | |
| float v0=light1(1.0,10.0,d0); // main radial glow | |
| v0*=smoothstep(r0*1.05,r0,len); // hard-ish cutoff just outside the radius | |
| float innerFade=smoothstep(r0*0.8,r0*0.95,len); // fade near the center | |
| v0*=mix(innerFade,1.0,bgLum*0.7); | |
| float cl=cos(ang+iTime*2.0)*0.5+0.5; // angular color blend (rotates over time) | |
| float a2=iTime*-1.0;vec2 pos=vec2(cos(a2),sin(a2))*r0;float d=distance(uv,pos); // orbiting light | |
| float v1=light2(1.5,5.0,d);v1*=light1(1.0,50.0,d0); // highlight with quick falloff | |
| float v2=smoothstep(1.0,mix(innerRadius,1.0,n0*0.5),len); // outer fade mask | |
| float v3=smoothstep(innerRadius,mix(innerRadius,1.0,0.5),len); // innerβouter ramp | |
| vec3 colBase=mix(c1,c2,cl); // angular purpleβcyan blend | |
| float fadeAmt=mix(1.0,0.1,bgLum); | |
| // "dark" composite β used on dark backgrounds | |
| vec3 darkCol=mix(c3,colBase,v0);darkCol=(darkCol+v1)*v2*v3;darkCol=clamp(darkCol,0.0,1.0); | |
| // "light" composite β blends toward the background color | |
| vec3 lightCol=(colBase+v1)*mix(1.0,v2*v3,fadeAmt);lightCol=mix(backgroundColor,lightCol,v0);lightCol=clamp(lightCol,0.0,1.0); | |
| // final mix: lean toward lightCol when the background is bright | |
| vec3 fc=mix(darkCol,lightCol,bgLum); | |
| return extractAlpha(fc); | |
| } | |
| /* ----- mainImage(): entry point called by main() ----- */ | |
| // Transforms the raw pixel coordinate into a centered, normalized | |
| // UV, applies rotation and the wavy hover distortion, then calls | |
| // draw(). | |
| vec4 mainImage(vec2 fragCoord){ | |
| vec2 center=iResolution.xy*0.5;float sz=min(iResolution.x,iResolution.y); | |
| vec2 uv=(fragCoord-center)/sz*2.0; // center and normalize UV to [-1,1] on short axis | |
| // Apply 2D rotation (accumulated while the orb is "active") | |
| float s2=sin(rot);float c2=cos(rot);uv=vec2(c2*uv.x-s2*uv.y,s2*uv.x+c2*uv.y); | |
| // Wavy UV distortion driven by 'hover' (0β1 when active) | |
| uv.x+=hover*hoverIntensity*0.1*sin(uv.y*10.0+iTime); | |
| uv.y+=hover*hoverIntensity*0.1*sin(uv.x*10.0+iTime); | |
| return draw(uv); | |
| } | |
| /* ----- main(): GLSL entry point ----- */ | |
| // Converts the varying vUv (0-1 range) back to pixel coordinates, | |
| // calls mainImage(), and writes the final pre-multiplied alpha | |
| // color to gl_FragColor. | |
| void main(){ | |
| vec2 fc=vUv*iResolution.xy;vec4 col=mainImage(fc); | |
| gl_FragColor=vec4(col.rgb*col.a,col.a); | |
| }`; | |
| /* ============================================================= | |
| _compile(type, src) | |
| ============================================================= | |
| Compiles a single GLSL shader (vertex or fragment). | |
| WebGL shaders are written in GLSL (a C-like language) and must | |
| be compiled at runtime by the GPU driver. If compilation fails | |
| (e.g. syntax error in the GLSL), we log the error and return | |
| null so _build() can bail out gracefully. | |
| ============================================================= */ | |
| _compile(type, src) { | |
| const gl = this.gl; | |
| const s = gl.createShader(type); | |
| gl.shaderSource(s, src); | |
| gl.compileShader(s); | |
| if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) { | |
| console.error('Shader compile error:', gl.getShaderInfoLog(s)); | |
| gl.deleteShader(s); | |
| return null; | |
| } | |
| return s; | |
| } | |
| /* ============================================================= | |
| _build() | |
| ============================================================= | |
| Sets up everything the GPU needs to render the orb: | |
| 1. COMPILE both shaders (vertex + fragment). | |
| 2. LINK them into a "program" β the GPU pipeline that will run | |
| every frame. | |
| 3. CREATE VERTEX BUFFERS. We use a single oversized triangle | |
| (the "full-screen triangle" trick) instead of a quad. Its 3 | |
| vertices at (-1,-1), (3,-1), (-1,3) in clip space cover the | |
| entire [-1,1]Β² viewport and beyond, so every pixel gets a | |
| fragment shader invocation. This is faster than two triangles | |
| because the GPU only processes one primitive. | |
| 4. LOOK UP UNIFORM LOCATIONS. gl.getUniformLocation returns a | |
| handle we use each frame to send updated values to the shader. | |
| 5. ENABLE ALPHA BLENDING so the orb composites transparently | |
| over whatever is behind the canvas. | |
| ============================================================= */ | |
| _build() { | |
| const gl = this.gl; | |
| const vs = this._compile(gl.VERTEX_SHADER, OrbRenderer.VERT); | |
| const fs = this._compile(gl.FRAGMENT_SHADER, OrbRenderer.FRAG); | |
| if (!vs || !fs) return; | |
| this.pgm = gl.createProgram(); | |
| gl.attachShader(this.pgm, vs); | |
| gl.attachShader(this.pgm, fs); | |
| gl.linkProgram(this.pgm); | |
| if (!gl.getProgramParameter(this.pgm, gl.LINK_STATUS)) { | |
| console.error('Program link error:', gl.getProgramInfoLog(this.pgm)); | |
| return; | |
| } | |
| gl.useProgram(this.pgm); | |
| // Get attribute locations from the compiled program | |
| const posLoc = gl.getAttribLocation(this.pgm, 'position'); | |
| const uvLoc = gl.getAttribLocation(this.pgm, 'uv'); | |
| // Position buffer: a single full-screen triangle in clip space. | |
| // (-1,-1) is bottom-left, (3,-1) extends far right, (-1,3) extends far up. | |
| // The GPU clips to the viewport, so the visible area is exactly [-1,1]Β². | |
| const posBuf = gl.createBuffer(); | |
| gl.bindBuffer(gl.ARRAY_BUFFER, posBuf); | |
| gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW); | |
| gl.enableVertexAttribArray(posLoc); | |
| gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 0, 0); | |
| // UV buffer: matching texture coordinates for the triangle. | |
| // (0,0) maps to the bottom-left corner; values > 1 are clipped away. | |
| const uvBuf = gl.createBuffer(); | |
| gl.bindBuffer(gl.ARRAY_BUFFER, uvBuf); | |
| gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0, 0, 2, 0, 0, 2]), gl.STATIC_DRAW); | |
| gl.enableVertexAttribArray(uvLoc); | |
| gl.vertexAttribPointer(uvLoc, 2, gl.FLOAT, false, 0, 0); | |
| // Cache uniform locations so we can efficiently set them each frame | |
| this.u = {}; | |
| ['iTime', 'iResolution', 'hue', 'hover', 'rot', 'hoverIntensity', 'backgroundColor'].forEach(name => { | |
| this.u[name] = gl.getUniformLocation(this.pgm, name); | |
| }); | |
| // Enable standard alpha blending for transparent compositing | |
| gl.enable(gl.BLEND); | |
| gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); | |
| gl.clearColor(0, 0, 0, 0); | |
| } | |
| /* ============================================================= | |
| _resize() | |
| ============================================================= | |
| Keeps the canvas resolution in sync with its on-screen size. | |
| CSS sizes the canvas element (100% Γ 100%), but the actual | |
| pixel buffer must be set explicitly via canvas.width/height. | |
| We multiply by devicePixelRatio so the orb looks sharp on | |
| HiDPI / Retina displays. The gl.viewport call tells WebGL | |
| to use the full buffer. | |
| ============================================================= */ | |
| _resize() { | |
| const dpr = window.devicePixelRatio || 1; | |
| const w = this.container.clientWidth; | |
| const h = this.container.clientHeight; | |
| this.canvas.width = w * dpr; | |
| this.canvas.height = h * dpr; | |
| if (this.gl) this.gl.viewport(0, 0, this.canvas.width, this.canvas.height); | |
| } | |
| /* ============================================================= | |
| _loop(ts) | |
| ============================================================= | |
| The animation frame callback β called ~60 times per second by | |
| the browser via requestAnimationFrame. | |
| Each frame it: | |
| 1. Schedules the next frame immediately (so animation never | |
| stops, even if this frame is slow). | |
| 2. Converts the browser's millisecond timestamp to seconds and | |
| computes the delta-time (dt) since the last frame. | |
| 3. Smoothly interpolates currentHover toward targetHover using | |
| an exponential ease (lerp with dt-scaled factor). This gives | |
| a nice fade-in / fade-out when setActive() is toggled. | |
| 4. Accumulates rotation while active (currentHover > 0.5). | |
| 5. Clears the canvas (transparent), uploads all uniform values | |
| for this frame, and issues a single draw call (3 vertices = | |
| one triangle that covers the screen). | |
| ============================================================= */ | |
| _loop(ts) { | |
| this._raf = requestAnimationFrame(this._loop.bind(this)); | |
| if (!this.pgm) return; | |
| const gl = this.gl; | |
| const t = ts * 0.001; // ms β seconds | |
| const dt = this.lastTs ? t - this.lastTs : 0.016; // delta time (fallback ~60fps) | |
| this.lastTs = t; | |
| // Smooth hover interpolation: exponential ease toward target | |
| this.currentHover += (this.targetHover - this.currentHover) * Math.min(dt * 4, 1); | |
| // Slowly rotate the orb while it's in the "active" state | |
| if (this.currentHover > 0.5) this.currentRot += dt * 0.3; | |
| gl.clear(gl.COLOR_BUFFER_BIT); | |
| gl.useProgram(this.pgm); | |
| gl.uniform1f(this.u.iTime, t); // elapsed seconds | |
| gl.uniform3f(this.u.iResolution, this.canvas.width, this.canvas.height, this.canvas.width / this.canvas.height); | |
| gl.uniform1f(this.u.hue, this.hue); // palette rotation (degrees) | |
| gl.uniform1f(this.u.hover, this.currentHover); // 0β1 active interpolation | |
| gl.uniform1f(this.u.rot, this.currentRot); // accumulated rotation | |
| gl.uniform1f(this.u.hoverIntensity, this.hoverIntensity); // wave distortion strength | |
| gl.uniform3f(this.u.backgroundColor, this.bgColor[0], this.bgColor[1], this.bgColor[2]); | |
| gl.drawArrays(gl.TRIANGLES, 0, 3); // draw the single full-screen triangle | |
| } | |
| /* ============================================================= | |
| setActive(active) | |
| ============================================================= | |
| Toggles the orb between its idle and active (e.g. "speaking") | |
| states. | |
| - When active=true, targetHover is set to 1.0. Over the next | |
| few frames, _loop() will smoothly ramp currentHover up to 1, | |
| which makes the shader apply the wavy UV distortion and the | |
| rotation starts accumulating. The CSS class 'active' can be | |
| used to style the container (e.g. scale or glow via CSS). | |
| - When active=false, the reverse happens β the distortion and | |
| rotation smoothly fade out. | |
| ============================================================= */ | |
| setActive(active) { | |
| this.targetHover = active ? 1.0 : 0.0; | |
| const ctn = this.container; | |
| if (active) ctn.classList.add('active'); | |
| else ctn.classList.remove('active'); | |
| } | |
| /* ============================================================= | |
| destroy() | |
| ============================================================= | |
| Cleans up all resources so the renderer can be safely removed: | |
| 1. Cancels the pending animation frame. | |
| 2. Removes the window resize listener. | |
| 3. Detaches the <canvas> element from the DOM. | |
| 4. Asks the browser to release the WebGL context and its GPU | |
| memory via the WEBGL_lose_context extension. | |
| Always call this when the orb is no longer needed (e.g. when | |
| navigating away from the page or unmounting a component). | |
| ============================================================= */ | |
| destroy() { | |
| cancelAnimationFrame(this._raf); | |
| window.removeEventListener('resize', this._onResize); | |
| if (this.canvas.parentNode) this.canvas.parentNode.removeChild(this.canvas); | |
| const ext = this.gl.getExtension('WEBGL_lose_context'); | |
| if (ext) ext.loseContext(); | |
| } | |
| } |