
if(!window._impulseGyro){window._impulseGyro=(function(){var granted=false,listeners=[],requested=false,needsPerm=typeof DeviceOrientationEvent!=='undefined'&&typeof DeviceOrientationEvent.requestPermission==='function';function activate(){if(granted)return;granted=true;window._impulseGyro.granted=true;for(var i=0;i<listeners.length;i++)window.addEventListener('deviceorientation',listeners[i],{passive:true});}function reqGesture(){if(requested)return;requested=true;document.addEventListener('touchstart',function f(){document.removeEventListener('touchstart',f);DeviceOrientationEvent.requestPermission().then(function(s){if(s==='granted')activate();}).catch(function(){});},{once:true,passive:true});}return{granted:false,register:function(fn){listeners.push(fn);if(granted)window.addEventListener('deviceorientation',fn,{passive:true});else if(needsPerm)reqGesture();else if(typeof DeviceOrientationEvent!=='undefined')activate();},unregister:function(fn){window.removeEventListener('deviceorientation',fn);var i=listeners.indexOf(fn);if(i!==-1)listeners.splice(i,1);}};})();}

(function(){
'use strict';
if(typeof THREE==='undefined'){window.Impulse3D={init:function(){},destroy:function(){}};return;}

var instances=new Map();
var observer=null;

var _quality=1.0;
(function detectQuality(){
    var isMobile='ontouchstart' in window||navigator.maxTouchPoints>0;
    var isLowEnd=navigator.hardwareConcurrency&&navigator.hardwareConcurrency<=4;
    var isSmallScreen=window.innerWidth<=768;
    if(isMobile||isSmallScreen)_quality=0.5;
    else if(isLowEnd)_quality=0.75;
})();

var NOISE_2D=[
'vec3 mod289(vec3 x){return x-floor(x*(1.0/289.0))*289.0;}',
'vec2 mod289v2(vec2 x){return x-floor(x*(1.0/289.0))*289.0;}',
'vec3 permute(vec3 x){return mod289(((x*34.0)+1.0)*x);}',
'float snoise(vec2 v){',
'  const vec4 C=vec4(0.211324865,0.366025404,-0.577350269,0.024390244);',
'  vec2 i=floor(v+dot(v,C.yy));',
'  vec2 x0=v-i+dot(i,C.xx);',
'  vec2 i1=(x0.x>x0.y)?vec2(1.0,0.0):vec2(0.0,1.0);',
'  vec4 x12=x0.xyxy+C.xxzz;x12.xy-=i1;',
'  i=mod289v2(i);',
'  vec3 p=permute(permute(i.y+vec3(0.0,i1.y,1.0))+i.x+vec3(0.0,i1.x,1.0));',
'  vec3 m=max(0.5-vec3(dot(x0,x0),dot(x12.xy,x12.xy),dot(x12.zw,x12.zw)),0.0);',
'  m=m*m;m=m*m;',
'  vec3 x=2.0*fract(p*C.www)-1.0;',
'  vec3 h=abs(x)-0.5;',
'  vec3 ox=floor(x+0.5);',
'  vec3 a0=x-ox;',
'  m*=1.79284291400159-0.85373472095314*(a0*a0+h*h);',
'  vec3 g;',
'  g.x=a0.x*x0.x+h.x*x0.y;',
'  g.yz=a0.yz*x12.xz+h.yz*x12.yw;',
'  return 130.0*dot(m,g);',
'}'
].join('\n');

var NOISE_3D=[
'vec4 mod289v4(vec4 x){return x-floor(x*(1.0/289.0))*289.0;}',
'vec4 permute4(vec4 x){return mod289v4(((x*34.0)+1.0)*x);}',
'vec4 taylorInvSqrt(vec4 r){return 1.79284291400159-0.85373472095314*r;}',
'float snoise3(vec3 v){',
'  const vec2 C=vec2(1.0/6.0,1.0/3.0);',
'  const vec4 D=vec4(0.0,0.5,1.0,2.0);',
'  vec3 i=floor(v+dot(v,C.yyy));',
'  vec3 x0=v-i+dot(i,C.xxx);',
'  vec3 g=step(x0.yzx,x0.xyz);',
'  vec3 l=1.0-g;',
'  vec3 i1=min(g.xyz,l.zxy);',
'  vec3 i2=max(g.xyz,l.zxy);',
'  vec3 x1=x0-i1+C.xxx;',
'  vec3 x2=x0-i2+C.yyy;',
'  vec3 x3=x0-D.yyy;',
'  i=mod289(i);',
'  vec4 p=permute4(permute4(permute4(',
'    i.z+vec4(0.0,i1.z,i2.z,1.0))',
'    +i.y+vec4(0.0,i1.y,i2.y,1.0))',
'    +i.x+vec4(0.0,i1.x,i2.x,1.0));',
'  float n_=0.142857142857;',
'  vec3 ns=n_*D.wyz-D.xzx;',
'  vec4 j=p-49.0*floor(p*ns.z*ns.z);',
'  vec4 x_=floor(j*ns.z);',
'  vec4 y_=floor(j-7.0*x_);',
'  vec4 x=x_*ns.x+ns.yyyy;',
'  vec4 y=y_*ns.x+ns.yyyy;',
'  vec4 h=1.0-abs(x)-abs(y);',
'  vec4 b0=vec4(x.xy,y.xy);',
'  vec4 b1=vec4(x.zw,y.zw);',
'  vec4 s0=floor(b0)*2.0+1.0;',
'  vec4 s1=floor(b1)*2.0+1.0;',
'  vec4 sh=-step(h,vec4(0.0));',
'  vec4 a0=b0.xzyw+s0.xzyw*sh.xxyy;',
'  vec4 a1=b1.xzyw+s1.xzyw*sh.zzww;',
'  vec3 p0=vec3(a0.xy,h.x);',
'  vec3 p1=vec3(a0.zw,h.y);',
'  vec3 p2=vec3(a1.xy,h.z);',
'  vec3 p3=vec3(a1.zw,h.w);',
'  vec4 norm=taylorInvSqrt(vec4(dot(p0,p0),dot(p1,p1),dot(p2,p2),dot(p3,p3)));',
'  p0*=norm.x;p1*=norm.y;p2*=norm.z;p3*=norm.w;',
'  vec4 m=max(0.6-vec4(dot(x0,x0),dot(x1,x1),dot(x2,x2),dot(x3,x3)),0.0);',
'  m=m*m;',
'  return 42.0*dot(m*m,vec4(dot(p0,x0),dot(p1,x1),dot(p2,x2),dot(p3,x3)));',
'}'
].join('\n');

var FBM_GLSL=[
'float fbm(vec3 p,int octaves){',
'  float v=0.0,a=0.5;',
'  for(int i=0;i<6;i++){',
'    if(i>=octaves)break;',
'    v+=a*snoise3(p);',
'    p*=2.0;a*=0.5;',
'  }',
'  return v;',
'}'
].join('\n');

function createPalette(colors){
    
    var _fallback=new THREE.Color('#888888');
    while(colors.length<4)colors.push(_fallback);
    var s=512,cv=document.createElement('canvas');
    cv.width=s;cv.height=s;
    var ctx=cv.getContext('2d');
    var g1=ctx.createLinearGradient(0,0,0,s);
    g1.addColorStop(0,'#'+colors[0].getHexString());
    g1.addColorStop(0.3,'#'+colors[1].getHexString());
    g1.addColorStop(0.6,'#'+colors[2].getHexString());
    g1.addColorStop(1,'#'+colors[3].getHexString());
    ctx.fillStyle=g1;ctx.fillRect(0,0,s,s);
    ctx.globalCompositeOperation='overlay';
    var g2=ctx.createLinearGradient(0,s,s,0);
    g2.addColorStop(0,'#'+colors[3].getHexString());
    g2.addColorStop(0.35,'#'+colors[2].getHexString());
    g2.addColorStop(0.7,'#'+colors[0].getHexString());
    g2.addColorStop(1,'#'+colors[1].getHexString());
    ctx.fillStyle=g2;ctx.fillRect(0,0,s,s);
    ctx.globalCompositeOperation='soft-light';
    var g3=ctx.createRadialGradient(s*0.4,s*0.5,0,s*0.4,s*0.5,s*0.7);
    g3.addColorStop(0,'#ffffff');
    g3.addColorStop(0.5,'#'+colors[1].getHexString());
    g3.addColorStop(1,'#'+colors[2].getHexString());
    ctx.fillStyle=g3;ctx.fillRect(0,0,s,s);
    var t=new THREE.CanvasTexture(cv);
    t.minFilter=THREE.LinearFilter;t.magFilter=THREE.LinearFilter;
    t.wrapS=THREE.RepeatWrapping;t.wrapT=THREE.RepeatWrapping;
    return t;
}

var PP_VERT='varying vec2 v_uv;void main(){v_uv=uv;gl_Position=vec4(position,1.0);}';
var PP_FRAG=[
'uniform sampler2D u_scene;',
'uniform float u_blurAmount;',
'uniform float u_grainAmount;',
'uniform vec2 u_resolution;',
'varying vec2 v_uv;',
'float random(vec2 st){return fract(sin(dot(st,vec2(12.9898,78.233)))*43758.5453);}',
'vec3 grain(vec3 color,float amount){',
'  float g=random(gl_FragCoord.xy*0.01);',
'  vec3 d=vec3(4.0/255.0);',
'  return color+mix(amount*d,-amount*d,g);',
'}',
'vec4 blurAngular(sampler2D tex,vec2 uv,float angle,int samples){',
'  vec4 total=vec4(0.0);',
'  vec2 coord=uv-0.5;',
'  float d=1.0/float(samples);',
'  float ca=cos(angle*d),sa=sin(angle*d);',
'  mat2 rot=mat2(ca,sa,-sa,ca);',
'  for(int i=0;i<12;i++){',
'    if(i>=samples)break;',
'    total+=texture2D(tex,coord+0.5);',
'    coord=rot*coord;',
'  }',
'  return total*d;',
'}',
'void main(){',
'  vec4 sc=texture2D(u_scene,v_uv);',
'  vec4 bl=blurAngular(u_scene,v_uv,u_blurAmount,6);',
'  float bp=smoothstep(0.0,0.7,v_uv.y)-smoothstep(0.2,1.0,v_uv.y);',
'  vec4 fc=mix(bl,sc,bp);',
'  fc.rgb=grain(fc.rgb,u_grainAmount);',
'  gl_FragColor=vec4(min(fc.rgb,1.0),fc.a);',
'}'
].join('\n');

var PARTICLES={};

PARTICLES.vert=[
'uniform float u_time;',
'uniform float u_speed;',
'uniform float u_pointSize;',
'uniform float u_intensity;',
'uniform vec2 u_resolution;',
'uniform vec2 u_mouse;',
'uniform float u_displaceAmt;',
'attribute float a_seed;',
'attribute float a_size;',
'varying float v_seed;',
'varying float v_depth;',
'varying float v_alpha;',
'',
NOISE_2D,'',NOISE_3D,
'',
'void main(){',
'  v_seed=a_seed;',
'  float t=u_time*u_speed*0.0025;',
'  float intMult=0.4+u_intensity*0.6;',
'  vec3 p=position;',
'  float s=a_seed*6.283;',
'  p.x+=sin(t*0.5+s)*u_displaceAmt*0.5*intMult;',
'  p.y+=cos(t*0.3+s*1.3)*u_displaceAmt*0.3*intMult;',
'  p.z+=sin(t*0.4+s*0.7)*u_displaceAmt*0.4*intMult;',
'  float n=snoise3(p*0.15+vec3(t*0.2));',
'  p+=vec3(n,snoise3(p*0.15+vec3(t*0.2,100.0,0.0)),snoise3(p*0.15+vec3(0.0,0.0,t*0.2+200.0)))*u_displaceAmt*0.3*intMult;',
'  vec2 mOff=(u_mouse-0.5)*4.0;',
'  p.x+=mOff.x*0.5/(1.0+length(p.xy-mOff*3.0)*0.1);',
'  p.y+=mOff.y*0.5/(1.0+length(p.xy-mOff*3.0)*0.1);',
'  vec4 mvPos=modelViewMatrix*vec4(p,1.0);',
'  v_depth=(-mvPos.z-1.0)/30.0;',
'  float sz=a_size*u_pointSize*(0.6+u_intensity*0.4)*(u_resolution.y/800.0);',
'  gl_PointSize=sz*(300.0/max(-mvPos.z,1.0));',
'  gl_PointSize=max(gl_PointSize,1.0);',
'  v_alpha=smoothstep(1.0,0.0,v_depth);',
'  gl_Position=projectionMatrix*mvPos;',
'}'
].join('\n');

PARTICLES.frag=[
'uniform sampler2D u_palette;',
'uniform float u_glowSoftness;',
'uniform float u_intensity;',
'varying float v_seed;',
'varying float v_depth;',
'varying float v_alpha;',
'',
'void main(){',
'  vec2 cxy=2.0*gl_PointCoord-1.0;',
'  float r=dot(cxy,cxy);',
'  if(r>1.0)discard;',
'  float softness=u_glowSoftness+(1.0-u_intensity)*0.2;',
'  float alpha=smoothstep(1.0,softness,r)*v_alpha;',
'  float t=fract(v_seed*3.7+v_depth*0.5);',
'  vec3 color=texture2D(u_palette,vec2(t,t)).rgb;',
'  color+=vec3(smoothstep(0.5,0.0,r)*0.08*u_intensity);',
'  color=min(color,vec3(0.95));',
'  float alphaMult=0.3+u_intensity*0.4;',
'  gl_FragColor=vec4(color,alpha*alphaMult);',
'}'
].join('\n');

PARTICLES.presets={
    nebula:{
        count:1800, spread:24, pointSize:5.0, glowSoftness:0.15,
        displaceAmt:2.0, camDist:18,
        colors:['#8b7bb5','#c4a0c7','#7ba3c9','#b8a0d0']
    },
    galaxy:{
        count:3500, spread:26, pointSize:3.5, glowSoftness:0.3,
        displaceAmt:2.5, camDist:20,
        colors:['#1a3a5c','#3ca8a0','#c77d8a','#d4b072']
    },
    fireflies:{
        count:600, spread:18, pointSize:6.0, glowSoftness:0.12,
        displaceAmt:3.5, camDist:14,
        colors:['#c89640','#dbb468','#e8d0a0','#f0e0c0']
    },
    snow:{
        count:2500, spread:24, pointSize:3.5, glowSoftness:0.35,
        displaceAmt:1.5, camDist:18,
        colors:['#8a9aa5','#a8b8c5','#c0d0d8','#d0dce0']
    },
    embers:{
        count:1000, spread:16, pointSize:5.0, glowSoftness:0.18,
        displaceAmt:2.5, camDist:14,
        colors:['#c08060','#d4a080','#c8887a','#b89078']
    },
    cosmos:{
        count:5000, spread:35, pointSize:2.2, glowSoftness:0.5,
        displaceAmt:1.0, camDist:28,
        colors:['#506880','#7090a8','#90a8b8','#a0b0c0']
    },
    spark:{
        count:2000, spread:20, pointSize:5.5, glowSoftness:0.15,
        displaceAmt:2.5, camDist:16, darkBg:false,
        colors:['#ff0044','#0044ff','#ffcc00','#00cc88']
    },
    glow:{
        count:1800, spread:18, pointSize:5.5, glowSoftness:0.15,
        displaceAmt:2.5, camDist:16, darkBg:false,
        colors:['#00ccff','#ff4400','#cc00ff','#88ff00']
    },
    prism:{
        count:2000, spread:20, pointSize:5.5, glowSoftness:0.15,
        displaceAmt:2.5, camDist:16, darkBg:false,
        colors:['#ff0066','#00ccff','#ffcc00','#00ff88']
    }
};

PARTICLES.build=function(preset){
    var _mob='ontouchstart' in window||navigator.maxTouchPoints>0;
    var count=Math.round((preset.count||3000)*(_mob?0.5:1));
    var spread=preset.spread||20;
    var positions=new Float32Array(count*3);
    var seeds=new Float32Array(count);
    var sizes=new Float32Array(count);
    for(var i=0;i<count;i++){
        
        var theta=Math.random()*Math.PI*2;
        var phi=Math.acos(2*Math.random()-1);
        var r=Math.pow(Math.random(),0.6)*spread;
        positions[i*3]=r*Math.sin(phi)*Math.cos(theta);
        positions[i*3+1]=r*Math.sin(phi)*Math.sin(theta);
        positions[i*3+2]=r*Math.cos(phi);
        seeds[i]=Math.random();
        sizes[i]=0.4+Math.random()*0.6;
    }
    var g=new THREE.BufferGeometry();
    g.setAttribute('position',new THREE.BufferAttribute(positions,3));
    g.setAttribute('a_seed',new THREE.BufferAttribute(seeds,1));
    g.setAttribute('a_size',new THREE.BufferAttribute(sizes,1));
    return g;
};

PARTICLES.setupCamera=function(camera,preset){
    camera.position.set(0,0,preset.camDist||15);
    camera.lookAt(0,0,0);
};

PARTICLES.extraUniforms=function(preset){
    return {
        u_pointSize:{value:preset.pointSize||4},
        u_glowSoftness:{value:preset.glowSoftness||0.3},
        u_displaceAmt:{value:preset.displaceAmt||3},
        u_intensity:{value:0.5}
    };
};

PARTICLES.updateFrame=function(inst,dt){
    
    inst.mesh.rotation.y+=dt*0.05;
    inst.mesh.rotation.x+=dt*0.02;
    var tx=(inst.mouseX-0.5)*0.6;
    var ty=(inst.mouseY-0.5)*0.4;
    inst.camera.position.x=tx*5;
    inst.camera.position.y=ty*4;
    inst.camera.lookAt(0,0,0);
};

PARTICLES.materialOpts={
    transparent:true,
    depthWrite:false,
    depthTest:true,
    blending:THREE.AdditiveBlending,
    darkBg:true
};

PARTICLES.isPoints=true;

var PARTICLES_LIGHT={};

PARTICLES_LIGHT.vert=PARTICLES.vert;

PARTICLES_LIGHT.frag=[
'uniform sampler2D u_palette;',
'uniform float u_glowSoftness;',
'uniform float u_intensity;',
'varying float v_seed;',
'varying float v_depth;',
'varying float v_alpha;',
'',
'void main(){',
'  vec2 cxy=2.0*gl_PointCoord-1.0;',
'  float r=dot(cxy,cxy);',
'  if(r>1.0)discard;',
'  float softness=u_glowSoftness+(1.0-u_intensity)*0.2;',
'  float alpha=smoothstep(1.0,softness,r)*v_alpha;',
'  float t=fract(v_seed*3.7+v_depth*0.5);',
'  vec3 color=texture2D(u_palette,vec2(t,t)).rgb;',
'  color*=0.85;',
'  color=mix(color,color*0.5,smoothstep(0.3,1.0,r));',
'  float alphaMult=0.5+u_intensity*0.4;',
'  gl_FragColor=vec4(color,alpha*alphaMult);',
'}'
].join('\n');

PARTICLES_LIGHT.presets={
    confetti:{
        count:2000, spread:18, pointSize:5.0, glowSoftness:0.2,
        displaceAmt:2.5, camDist:15,
        colors:['#ff6b6b','#feca57','#48dbfb','#ff9ff3']
    },
    bubbles_light:{
        count:1200, spread:16, pointSize:6.5, glowSoftness:0.15,
        displaceAmt:2.0, camDist:14,
        colors:['#74b9ff','#a29bfe','#55efc4','#fd79a8']
    },
    pollen:{
        count:1500, spread:15, pointSize:4.0, glowSoftness:0.3,
        displaceAmt:3.0, camDist:13,
        colors:['#fdcb6e','#e17055','#00b894','#6c5ce7']
    },
    dust:{
        count:3000, spread:22, pointSize:3.0, glowSoftness:0.4,
        displaceAmt:2.0, camDist:18,
        colors:['#636e72','#b2bec3','#dfe6e9','#ffeaa7']
    },
    sakura:{
        count:1000, spread:16, pointSize:5.5, glowSoftness:0.2,
        displaceAmt:2.5, camDist:14,
        colors:['#fd79a8','#fab1a0','#ffeaa7','#dfe6e9']
    },
    ink_drops:{
        count:800, spread:14, pointSize:7.0, glowSoftness:0.15,
        displaceAmt:2.0, camDist:12,
        colors:['#2d3436','#636e72','#0984e3','#6c5ce7']
    }
};

PARTICLES_LIGHT.build=PARTICLES.build;
PARTICLES_LIGHT.setupCamera=PARTICLES.setupCamera;
PARTICLES_LIGHT.extraUniforms=PARTICLES.extraUniforms;
PARTICLES_LIGHT.updateFrame=PARTICLES.updateFrame;

PARTICLES_LIGHT.materialOpts={
    transparent:true,
    depthWrite:false,
    depthTest:true
    
};

PARTICLES_LIGHT.isPoints=true;

var CRYSTAL={};

CRYSTAL.vert=[
'uniform float u_time;',
'uniform float u_speed;',
'uniform float u_displaceAmt;',
'uniform vec2 u_mouse;',
'varying vec3 v_normal;',
'varying vec3 v_viewPos;',
'varying vec3 v_worldNormal;',
'varying vec3 v_pos;',
'',
NOISE_2D,'',NOISE_3D,
'',
'void main(){',
'  float t=u_time*u_speed*0.0015;',
'  vec3 p=position;',
'  vec3 n=normal;',
'  // Mouse-driven deformation direction',
'  vec3 mDir=normalize(vec3(u_mouse.x-0.5,(u_mouse.y-0.5)*0.8,0.3));',
'  float mDot=dot(n,mDir)*0.5+0.5;',
'  // Base breathing',
'  float pulse=sin(t*2.0+length(p)*1.5)*0.1;',
'  // Noise jaggedness — u_displaceAmt scaled up for visible crystal facets',
'  float noise=snoise3(p*1.5+vec3(t*0.3))*u_displaceAmt*8.0;',
'  float noise2=snoise3(p*3.0+vec3(-t*0.5,t*0.2,t*0.4))*u_displaceAmt*4.0;',
'  // Mouse pushes deformation in its direction',
'  float mouseBoost=mDot*u_displaceAmt*4.0;',
'  p+=n*(pulse+noise+noise2+mouseBoost);',
'  v_pos=p;',
'  v_normal=normalize(normalMatrix*n);',
'  v_worldNormal=normalize((modelMatrix*vec4(n,0.0)).xyz);',
'  vec4 mvPos=modelViewMatrix*vec4(p,1.0);',
'  v_viewPos=mvPos.xyz;',
'  gl_Position=projectionMatrix*mvPos;',
'}'
].join('\n');

CRYSTAL.frag=[
'uniform sampler2D u_palette;',
'uniform float u_time;',
'uniform float u_speed;',
'varying vec3 v_normal;',
'varying vec3 v_viewPos;',
'varying vec3 v_worldNormal;',
'varying vec3 v_pos;',
'',
'void main(){',
'  float t=u_time*u_speed*0.0003;',
'  // Flat face normal from screen-space derivatives of world position',
'  vec3 fdx=dFdx(v_pos);',
'  vec3 fdy=dFdy(v_pos);',
'  vec3 faceNormal=normalize(cross(fdx,fdy));',
'  // Prismatic color from face normal direction.',
'  // Diagonal palette sample (ct,ct) traverses all 4 colour stops.',
'  float nAngle=atan(faceNormal.y,faceNormal.x)*0.159+0.5;',
'  float ct=fract(nAngle+t*0.1);',
'  vec3 color=texture2D(u_palette,vec2(ct,ct)).rgb;',
'  // Flat-shaded lighting — crisp facets',
'  vec3 lightDir=normalize(vec3(0.5,1.0,0.3));',
'  float diff=max(dot(faceNormal,lightDir),0.0);',
'  color*=(0.25+diff*0.75);',
'  // Strong specular for gem look',
'  vec3 viewDir=normalize(-v_viewPos);',
'  vec3 refl=reflect(-lightDir,faceNormal);',
'  float spec=pow(max(dot(viewDir,refl),0.0),48.0);',
'  color+=vec3(spec*0.7);',
'  // Second specular from opposite side',
'  vec3 light2=normalize(vec3(-0.3,-0.5,0.8));',
'  vec3 refl2=reflect(-light2,faceNormal);',
'  float spec2=pow(max(dot(viewDir,refl2),0.0),32.0);',
'  color+=vec3(spec2*0.3);',
'  // Fresnel rim',
'  float fresnel=1.0-max(dot(viewDir,faceNormal),0.0);',
'  color+=vec3(pow(fresnel,3.5)*0.3);',
'  gl_FragColor=vec4(color,0.95);',
'}'
].join('\n');

CRYSTAL.presets={
    diamond:{
        radius:3.5,detail:1,displaceAmt:0.08,rotSpeed:0.08,
        colors:['#b3e5fc','#e1f5fe','#ffffff','#eceff1']
    },
    amethyst:{
        radius:3.2,detail:1,displaceAmt:0.1,rotSpeed:0.1,
        colors:['#4a148c','#7b1fa2','#ce93d8','#f3e5f5']
    },
    ruby:{
        radius:3.0,detail:1,displaceAmt:0.12,rotSpeed:0.07,
        colors:['#4a0000','#b71c1c','#ef5350','#ffcdd2']
    },
    emerald:{
        radius:3.3,detail:1,displaceAmt:0.09,rotSpeed:0.09,
        colors:['#003300','#1b5e20','#4caf50','#c8e6c9']
    },
    obsidian:{
        radius:3.5,detail:2,displaceAmt:0.06,rotSpeed:0.06,
        colors:['#0a0a0a','#1c1c1c','#424242','#757575']
    },
    opal:{
        radius:3.2,detail:1,displaceAmt:0.11,rotSpeed:0.12,
        colors:['#e040fb','#40c4ff','#69f0ae','#ffd740']
    }
};

CRYSTAL.build=function(preset){
    return new THREE.IcosahedronGeometry(preset.radius||3.5, preset.detail||1);
};

CRYSTAL.setupCamera=function(camera,preset){
    var d=10;
    if(camera.aspect<1) d=d*(1/camera.aspect)*0.8;
    camera.position.set(0,0,d);
    camera.lookAt(0,0,0);
};

CRYSTAL.extraUniforms=function(preset){
    return {
        u_displaceAmt:{value:preset.displaceAmt||0.08}
    };
};

CRYSTAL.updateFrame=function(inst,dt){
    var rs=inst._activePreset.rotSpeed||0.08;
    inst.mesh.rotation.y+=dt*rs;
    inst.mesh.rotation.x+=dt*rs*0.6;
    var tx=(inst.mouseX-0.5)*0.4;
    var ty=(inst.mouseY-0.5)*0.3;
    var _cDist=10;
    if(inst.camera.aspect<1) _cDist=_cDist*(1/inst.camera.aspect)*0.8;
    inst.camera.position.x=tx*3;
    inst.camera.position.y=ty*2;
    inst.camera.position.z=_cDist;
    inst.camera.lookAt(0,0,0);
    var baseAmt=inst._activePreset.displaceAmt||0.08;
    inst.material.uniforms.u_displaceAmt.value=baseAmt*(0.3+inst.intensity*0.7);
};

CRYSTAL.materialOpts={
    side:THREE.FrontSide,
    transparent:true,
    depthWrite:true,
    depthTest:true
};

var BLOB={};

BLOB.vert=[
'varying vec2 v_uv;',
'void main(){',
'  v_uv=uv;',
'  gl_Position=vec4(position,1.0);',
'}'
].join('\n');

BLOB.frag=[
'precision highp float;',
'uniform float u_time;',
'uniform float u_speed;',
'uniform float u_displaceAmt;',
'uniform vec2 u_resolution;',
'uniform vec2 u_mouse;',
'uniform sampler2D u_palette;',
'varying vec2 v_uv;',
'',
'float sdSphere(vec3 p,float r){return length(p)-r;}',
'float smin(float a,float b,float k){',
'  float h=clamp(0.5+0.5*(b-a)/k,0.0,1.0);',
'  return mix(b,a,h)-k*h*(1.0-h);',
'}',
'float scene(vec3 p,float t){',
'  float d=1e10;',
'  // 6 independently orbiting blobs — varied radii, speeds, 3D paths',
'  for(int i=0;i<6;i++){',
'    float fi=float(i);',
'    float phase=fi*1.047;',
'    float orbitR=0.9+fi*0.22;',
'    float speedX=0.16+fi*0.04;',
'    float speedY=0.13+fi*0.05;',
'    float speedZ=0.10+fi*0.04;',
'    vec3 c=vec3(',
'      sin(t*speedX+phase)*orbitR+cos(t*speedY*0.7+phase*2.3)*0.4,',
'      cos(t*speedY+phase*1.4)*orbitR*0.7+sin(t*speedZ*0.5+phase)*0.35,',
'      sin(t*speedZ+phase*0.8)*orbitR*0.8+cos(t*speedX*0.6+phase*1.5)*0.3',
'    );',
'    float r=0.58+sin(t*0.4+fi*1.1)*0.10;',
'    d=smin(d,sdSphere(p-c,r*u_displaceAmt),1.15);',
'  }',
'  // Mouse-following blob — smoothly attracted to cursor',
'  vec3 mc=vec3((u_mouse.x-0.5)*3.2,(u_mouse.y-0.5)*2.3,sin(t*0.3)*0.6);',
'  d=smin(d,sdSphere(p-mc,0.52*u_displaceAmt),1.25);',
'  return d;',
'}',
'vec3 calcNormal(vec3 p,float t){',
'  vec2 e=vec2(0.001,0.0);',
'  return normalize(vec3(',
'    scene(p+e.xyy,t)-scene(p-e.xyy,t),',
'    scene(p+e.yxy,t)-scene(p-e.yxy,t),',
'    scene(p+e.yyx,t)-scene(p-e.yyx,t)));',
'}',
'void main(){',
'  float t=u_time*u_speed*0.0003;',
'  vec2 uv=(gl_FragCoord.xy/u_resolution.xy)*2.0-1.0;',
'  uv.x*=u_resolution.x/u_resolution.y;',
'  vec3 ro=vec3(0.0,0.0,6.0);',
'  vec3 rd=normalize(vec3(uv,-2.0));',
'  float d=0.0;',
'  bool hit=false;',
'  for(int i=0;i<64;i++){',
'    vec3 p=ro+rd*d;',
'    float sd=scene(p,t);',
'    if(sd<0.01){hit=true;break;}',
'    if(d>20.0)break;',
'    d+=sd;',
'  }',
'  if(!hit){gl_FragColor=vec4(1.0);return;}',
'  vec3 p=ro+rd*d;',
'  vec3 n=calcNormal(p,t);',
'  // Smooth palette sampling — sin avoids hard wrap discontinuity.',
'  // Diagonal sample (ct,ct) uses all 4 palette stops, not just middle.',
'  float ct=sin(length(p.xy)*0.5+t*0.08)*0.5+0.5;',
'  vec3 color=texture2D(u_palette,vec2(ct,ct)).rgb;',
'  // Lighting',
'  vec3 lightDir=normalize(vec3(0.5,0.8,0.3));',
'  float diff=max(dot(n,lightDir),0.0);',
'  color*=(0.65+diff*0.35);',
'  // Specular',
'  vec3 viewDir=normalize(ro-p);',
'  vec3 refl=reflect(-lightDir,n);',
'  float spec=pow(max(dot(viewDir,refl),0.0),24.0);',
'  color+=vec3(spec*0.5);',
'  // Fresnel',
'  float fresnel=pow(1.0-max(dot(viewDir,n),0.0),2.0);',
'  color+=vec3(fresnel*0.35);',
'  // Edge fade to white',
'  float edgeFade=smoothstep(12.0,6.0,d);',
'  color=mix(vec3(1.0),color,edgeFade);',
'  gl_FragColor=vec4(color,1.0);',
'}'
].join('\n');

BLOB.presets={
    organic:{displaceAmt:1.0,colors:['#ff6b6b','#feca57','#48dbfb','#ff9ff3']},
    mercury:{displaceAmt:0.8,colors:['#636e72','#b2bec3','#dfe6e9','#ffffff']},
    plasma:{displaceAmt:1.2,colors:['#6c5ce7','#a29bfe','#fd79a8','#ffeaa7']},
    lava_blob:{displaceAmt:1.0,colors:['#2d0a0a','#b71c1c','#ff5722','#ffab40']},
    ocean_blob:{displaceAmt:0.9,colors:['#0a1628','#1a5276','#2e86c1','#85c1e9']},
    mint:{displaceAmt:1.0,colors:['#00b894','#00cec9','#55efc4','#dfe6e9']}
};

BLOB.build=function(preset){
    return new THREE.PlaneGeometry(2,2);
};

BLOB.setupCamera=function(camera,preset){
    camera.position.set(0,0,1);
    camera.lookAt(0,0,0);
};

BLOB.extraUniforms=function(preset){
    return {u_displaceAmt:{value:preset.displaceAmt||1.0}};
};

BLOB.updateFrame=function(inst,dt){
    var baseAmt=inst._activePreset.displaceAmt||1.0;
    inst.material.uniforms.u_displaceAmt.value=baseAmt*(0.4+inst.intensity*0.6);
};

BLOB.materialOpts={
    side:THREE.DoubleSide,
    transparent:false,
    depthWrite:false,
    depthTest:false
};

var WORMHOLE={};

WORMHOLE.vert=BLOB.vert;

WORMHOLE.frag=[
'precision highp float;',
'uniform float u_time;',
'uniform float u_speed;',
'uniform float u_displaceAmt;',
'uniform vec2 u_resolution;',
'uniform vec2 u_mouse;',
'uniform sampler2D u_palette;',
'varying vec2 v_uv;',
'',
NOISE_2D,
'',
'void main(){',
'  float t=u_time*u_speed*0.0003;',
'  vec2 uv=(gl_FragCoord.xy/u_resolution.xy)*2.0-1.0;',
'  uv.x*=u_resolution.x/u_resolution.y;',
'  // Auto-rotation + mouse rotation',
'  float mAngle=(u_mouse.x-0.5)*1.5+t*0.3;',
'  float ca=cos(mAngle),sa=sin(mAngle);',
'  uv=mat2(ca,-sa,sa,ca)*uv;',
'  // Mouse Y tilts depth perception',
'  uv.y+=(u_mouse.y-0.5)*0.4;',
'  // Polar coordinates',
'  float r=length(uv);',
'  // Seamless angular — use sin/cos of angle for noise lookup (no atan seam)',
'  float rawAngle=atan(uv.y,uv.x);',
'  vec2 angleVec=vec2(cos(rawAngle),sin(rawAngle));',
'  // Twist increases toward center — vortex',
'  float twist=u_displaceAmt*3.0/(r+0.3);',
'  float twistedAngle=rawAngle+twist+t*2.0;',
'  vec2 twistedVec=vec2(cos(twistedAngle),sin(twistedAngle));',
'  // Tunnel depth — rings moving inward',
'  float depth=1.0/(r+0.1)+t*5.0;',
'  // Noise distortion — use twisted sin/cos for seamless lookup',
'  float n1=snoise(vec2(twistedVec.x*3.0+depth*0.2,twistedVec.y*3.0+t*0.3))*0.3;',
'  float n2=snoise(vec2(twistedVec.y*5.0+depth*0.3,twistedVec.x*5.0-t*0.2))*0.15;',
'  // Color from palette — seamless via sin/cos',
'  float ct=fract(depth*0.05+n1+twistedVec.x*0.3+0.5);',
'  float ct2=fract(twistedVec.y*0.3+0.5+depth*0.02);',
'  vec3 color=texture2D(u_palette,vec2(ct,ct2)).rgb;',
'  // Brightness — brighter at edges, dark center',
'  float brightness=smoothstep(0.0,0.15,r)*smoothstep(2.0,0.3,r);',
'  brightness+=n2;',
'  color*=brightness*(0.7+u_displaceAmt*0.3);',
'  // Glow rings',
'  float ring=abs(sin(depth*1.5))*0.3*smoothstep(1.5,0.2,r);',
'  color+=vec3(ring)*color;',
'  // Center glow',
'  float core=exp(-r*r*8.0)*1.5;',
'  color+=vec3(core)*texture2D(u_palette,vec2(0.8,0.5)).rgb;',
'  // Vignette',
'  float vig=1.0-smoothstep(0.8,1.8,r);',
'  color*=vig;',
'  gl_FragColor=vec4(color,1.0);',
'}'
].join('\n');

WORMHOLE.presets={
    spacetime:{displaceAmt:1.0,colors:['#333333','#999999','#666666','#cccccc']},
    void_hole:{displaceAmt:1.3,colors:['#333333','#999999','#666666','#cccccc']},
    fire_vortex:{displaceAmt:0.8,colors:['#333333','#999999','#666666','#cccccc']},
    ice_vortex:{displaceAmt:1.0,colors:['#333333','#999999','#666666','#cccccc']},
    neon_warp:{displaceAmt:1.2,colors:['#333333','#999999','#666666','#cccccc']},
    cosmic_eye:{displaceAmt:0.9,colors:['#333333','#999999','#666666','#cccccc']}
};

WORMHOLE.build=function(preset){
    return new THREE.PlaneGeometry(2,2);
};

WORMHOLE.setupCamera=BLOB.setupCamera;

WORMHOLE.extraUniforms=function(preset){
    return {u_displaceAmt:{value:preset.displaceAmt||1.0}};
};

WORMHOLE.updateFrame=function(inst,dt){
    
    var baseAmt=inst._activePreset.displaceAmt||1.0;
    var amt=baseAmt*(0.3+inst.intensity*1.4);
    if(inst.material&&inst.material.uniforms&&inst.material.uniforms.u_displaceAmt){
        inst.material.uniforms.u_displaceAmt.value=amt;
    }
};

WORMHOLE.materialOpts={
    side:THREE.DoubleSide,
    transparent:false,
    depthWrite:false,
    depthTest:false,
    darkBg:true
};

var BUBBLES={};
BUBBLES.vert=[
'varying vec3 v_normal;',
'varying vec3 v_viewPos;',
'varying vec3 v_worldPos;',
'void main(){',
'  v_normal=normalize(normalMatrix*normal);',
'  vec4 mv=modelViewMatrix*vec4(position,1.0);',
'  v_viewPos=mv.xyz;',
'  v_worldPos=(modelMatrix*vec4(position,1.0)).xyz;',
'  gl_Position=projectionMatrix*mv;',
'}'
].join('\n');
BUBBLES.frag=[
'uniform float u_time;',
'uniform float u_speed;',
'uniform sampler2D u_palette;',
'uniform vec3 u_clearColor;',
'varying vec3 v_normal;',
'varying vec3 v_viewPos;',
'varying vec3 v_worldPos;',
'void main(){',
'  float t=u_time*u_speed*0.0003;',
'  vec3 viewDir=normalize(-v_viewPos);',
'  vec3 n=normalize(v_normal);',
'  float cosA=abs(dot(viewDir,n));',
'  // Fresnel — strong rim',
'  float fresnel=pow(1.0-cosA,2.5);',
'  // Palette color from view angle + position.',
'  // Diagonal sample (ct,ct) uses all 4 palette stops.',
'  float ct=fract(v_worldPos.x*0.08+v_worldPos.y*0.06+t*0.04);',
'  vec3 palCol=texture2D(u_palette,vec2(ct,ct)).rgb;',
'  // Thin-film iridescence',
'  float film=dot(n,vec3(0.3,0.5,0.7))*0.5+0.5;',
'  vec3 irid=vec3(',
'    0.5+0.5*cos(6.283*(film+0.0)),',
'    0.5+0.5*cos(6.283*(film+0.33)),',
'    0.5+0.5*cos(6.283*(film+0.67))',
'  );',
'  vec3 baseCol=mix(palCol,irid,0.35);',
'  // Detect light/dark bg — adapt rim rendering',
'  float bgLum=dot(u_clearColor,vec3(0.299,0.587,0.114));',
'  // Specular highlights — two lights',
'  vec3 L1=normalize(vec3(1.0,1.0,1.0));',
'  vec3 h1=normalize(L1+viewDir);',
'  float spec1=pow(max(dot(n,h1),0.0),80.0)*0.6;',
'  vec3 L2=normalize(vec3(-0.5,0.3,-0.8));',
'  vec3 h2=normalize(L2+viewDir);',
'  float spec2=pow(max(dot(n,h2),0.0),40.0)*0.2;',
'  vec3 col;',
'  float alpha;',
'  if(bgLum>0.5){',
'    // Light bg: darker rim edges (refraction), subtle color',
'    vec3 rimDark=baseCol*0.3;',
'    col=mix(vec3(1.0),rimDark,fresnel*0.7);',
'    col+=vec3(spec1*0.8)+vec3(0.8,0.85,1.0)*spec2*0.3;',
'    col+=irid*fresnel*0.15;',
'    alpha=fresnel*0.35+spec1*0.5+spec2*0.15+0.04;',
'  } else {',
'    // Dark bg: bright rim glow',
'    col=baseCol*fresnel*0.8+vec3(spec1)+vec3(0.8,0.85,1.0)*spec2;',
'    alpha=fresnel*0.6+spec1*0.4+spec2*0.2+0.08;',
'  }',
'  gl_FragColor=vec4(col,alpha);',
'}'
].join('\n');
BUBBLES.presets={
    iridescent:{maxCount:30,minR:0.3,maxR:1.1,spread:14,camDist:14,colors:['#4466aa','#aabbcc','#6688bb','#ccddee']},
    soap:      {maxCount:25,minR:0.4,maxR:1.4,spread:12,camDist:12,colors:['#334466','#99aacc','#5577aa','#bbccdd']},
    glass_b:   {maxCount:15,minR:0.7,maxR:1.8,spread:16,camDist:16,colors:['#1a2a3a','#8899bb','#334466','#aabbcc']},
    neon_b:    {maxCount:35,minR:0.2,maxR:0.8,spread:16,camDist:14,colors:['#ff00cc','#00ffcc','#cc00ff','#00ccff']},
    frost_b:   {maxCount:20,minR:0.5,maxR:1.3,spread:14,camDist:14,colors:['#4488aa','#aaccdd','#6699bb','#ccddee']},
    amber:     {maxCount:25,minR:0.4,maxR:1.2,spread:12,camDist:12,colors:['#cc7700','#ffcc44','#aa5500','#ffaa22']}
};
BUBBLES._geoHi=null;
BUBBLES._geoMid=null;
BUBBLES._geoLo=null;
BUBBLES.build=function(preset){
    if(!BUBBLES._geoHi){
        BUBBLES._geoHi=new THREE.SphereGeometry(1,64,48);
        BUBBLES._geoMid=new THREE.SphereGeometry(1,36,24);
        BUBBLES._geoLo=new THREE.SphereGeometry(1,20,14);
    }
    return BUBBLES._geoHi;
};
BUBBLES.setupCamera=function(camera,preset){
    camera.position.set(0,0,preset.camDist||14);
    camera.lookAt(0,0,0);
};
BUBBLES.materialOpts={transparent:true,depthWrite:false,side:THREE.DoubleSide};
BUBBLES.quality=1.0; 
BUBBLES.ppBlur=0.0;  
BUBBLES.ppGrain=0.3; 
BUBBLES.afterInit=function(inst){
    var p=inst._activePreset;
    var _mob='ontouchstart' in window||navigator.maxTouchPoints>0;
    var maxCount=Math.round((p.maxCount||25)*(_mob?0.7:1));
    var group=new THREE.Group();
    inst.scene.remove(inst.mesh);
    inst._bubbles=[];
    var sp=p.spread||12;
    var bGeo=_mob?BUBBLES._geoMid:BUBBLES._geoHi;
    for(var i=0;i<maxCount;i++){
        var r=p.minR+(p.maxR-p.minR)*Math.random();
        var mesh=new THREE.Mesh(bGeo,inst.material);
        mesh.scale.set(r,r,r);
        mesh.position.set(
            (Math.random()-0.5)*sp,
            (Math.random()-0.5)*sp*0.8,
            (Math.random()-0.5)*sp*0.5
        );
        
        var rax=Math.random()-0.5, ray=Math.random()-0.5, raz=Math.random()-0.5;
        var rlen=Math.sqrt(rax*rax+ray*ray+raz*raz)||1;
        
        mesh.userData={
            
            hx:mesh.position.x, hy:mesh.position.y, hz:mesh.position.z,
            
            px:Math.random()*6.28, py:Math.random()*6.28, pz:Math.random()*6.28,
            fx:0.12+Math.random()*0.10, fy:0.08+Math.random()*0.08, fz:0.09+Math.random()*0.09,
            
            ax:0.6+Math.random()*0.8, ay:0.5+Math.random()*0.7, az:0.3+Math.random()*0.5,
            r:r, lod:-1,
            rx:rax/rlen, ry:ray/rlen, rz:raz/rlen,
            rotSpeed:(0.1+Math.random()*0.25)*(Math.random()<0.5?1:-1)
        };
        group.add(mesh);
        inst._bubbles.push(mesh);
    }
    inst.scene.add(group);
    inst._bubbleGroup=group;
};
var _bTmpVec=(typeof THREE!=='undefined')&&new THREE.Vector3();
BUBBLES.updateFrame=function(inst,dt){
    var bs=inst._bubbles;if(!bs)return;
    var safeDt=Math.min(dt,0.05);
    var spd=inst.speed||1;
    var t=inst.time*0.001*spd;
    var mx=(inst.mouseX-0.5)*2,my=(inst.mouseY-0.5)*2;
    var baseDist=inst._activePreset.camDist||14;
    var zoom=0.5+inst.intensity*1.0;
    var camDist=baseDist/zoom;
    var camPos=inst.camera.position;
    for(var i=0;i<bs.length;i++){
        var b=bs[i],u=b.userData;
        
        b.position.x=u.hx+Math.sin(t*u.fx+u.px)*u.ax;
        b.position.y=u.hy+Math.sin(t*u.fy+u.py)*u.ay;
        b.position.z=u.hz+Math.sin(t*u.fz+u.pz)*u.az;
        
        var bsx=b.position.x-mx*3;
        var bsy=b.position.y-my*2;
        var mDist=Math.sqrt(bsx*bsx+bsy*bsy);
        if(mDist<2.5){
            var push=0.015*(2.5-mDist);
            b.position.x+=bsx*push;
            b.position.y+=bsy*push;
        }
        
        b.rotateOnWorldAxis(_bTmpVec.set(u.rx,u.ry,u.rz),u.rotSpeed*safeDt);
        
        var dx=b.position.x-camPos.x;
        var dy=b.position.y-camPos.y;
        var dz=b.position.z-camPos.z;
        var dist2=dx*dx+dy*dy+dz*dz;
        var newLod=dist2<64?0:dist2<196?1:2;
        if(newLod!==u.lod){
            u.lod=newLod;
            b.geometry=newLod===0?BUBBLES._geoHi:newLod===1?BUBBLES._geoMid:BUBBLES._geoLo;
        }
    }
    inst.camera.position.x=mx*3;
    inst.camera.position.y=my*1.5;
    inst.camera.position.z=camDist;
    inst.camera.lookAt(0,0,0);
};

var FIREFLIES={};
FIREFLIES.vert=[
'attribute float a_seed;',
'attribute float a_size;',
'uniform float u_time;',
'uniform float u_speed;',
'uniform float u_displaceAmt;',
'uniform vec2 u_mouse;',
'varying float v_alpha;',
'varying float v_seed;',
'',
'float hash(float n){return fract(sin(n)*43758.5453);}',
'',
'void main(){',
'  float t=u_time*u_speed*0.0003;',
'  float s=a_seed;',
'  // Organic drift — each firefly follows its own slow path',
'  vec3 pos=position;',
'  pos.x+=sin(t*0.5+s*6.28)*2.0+sin(t*0.3+s*12.0)*1.0;',
'  pos.y+=cos(t*0.4+s*5.0)*1.5+sin(t*0.6+s*8.0)*0.8;',
'  pos.z+=sin(t*0.35+s*7.0)*1.2;',
'  // Mouse attraction — gently pull toward cursor',
'  vec2 mp=(u_mouse-0.5)*8.0;',
'  float dx=mp.x-pos.x;',
'  float dy=mp.y-pos.y;',
'  float mdist=sqrt(dx*dx+dy*dy);',
'  float attract=exp(-mdist*0.15)*u_displaceAmt*0.5;',
'  pos.x+=dx*attract*0.05;',
'  pos.y+=dy*attract*0.05;',
'  // Pulsing glow',
'  float pulse=0.5+0.5*sin(t*3.0+s*6.28);',
'  v_alpha=pulse*0.6+0.3;',
'  v_seed=s;',
'  vec4 mvPos=modelViewMatrix*vec4(pos,1.0);',
'  gl_PointSize=a_size*(10.0+pulse*6.0)*u_displaceAmt*(300.0/length(mvPos.xyz));',
'  gl_Position=projectionMatrix*mvPos;',
'}'
].join('\n');
FIREFLIES.frag=[
'uniform sampler2D u_palette;',
'varying float v_alpha;',
'varying float v_seed;',
'void main(){',
'  float d=length(gl_PointCoord-0.5)*2.0;',
'  if(d>1.0)discard;',
'  // Soft glow falloff',
'  float glow=exp(-d*d*3.0);',
'  // Diagonal sample uses full palette, not just y=0.5 strip.',
'  vec3 col=texture2D(u_palette,vec2(v_seed,v_seed)).rgb;',
'  gl_FragColor=vec4(col*glow,glow*v_alpha);',
'}'
].join('\n');
FIREFLIES.presets={
    warm:  {count:200,spread:12,camDist:10,displaceAmt:1,colors:['#364a1d','#459fff','#213bff','#d40000']},
    cool:  {count:180,spread:14,camDist:10,displaceAmt:1,colors:['#0088ff','#00ccff','#4400ff','#88ffff']},
    forest:{count:220,spread:10,camDist:8, displaceAmt:1,colors:['#00cc44','#88ff44','#004422','#ccff88']},
    violet:{count:160,spread:12,camDist:10,displaceAmt:1,colors:['#aa00ff','#ff44cc','#4400aa','#ffaaff']},
    ember: {count:250,spread:10,camDist:8, displaceAmt:1,colors:['#ff2200','#ff8800','#ffee00','#ff4444']},
    snow:  {count:300,spread:16,camDist:12,displaceAmt:0.8,colors:['#aaddff','#ffffff','#88bbff','#ddeeff']}
};
FIREFLIES.isPoints=true;
FIREFLIES.build=function(preset){
    var count=preset.count||200;
    var spread=preset.spread||12;
    var positions=new Float32Array(count*3);
    var seeds=new Float32Array(count);
    var sizes=new Float32Array(count);
    for(var i=0;i<count;i++){
        var ang=Math.random()*Math.PI*2;
        var r=Math.pow(Math.random(),0.5)*spread;
        var h=(Math.random()-0.5)*spread*0.5;
        positions[i*3]=Math.cos(ang)*r;
        positions[i*3+1]=h;
        positions[i*3+2]=Math.sin(ang)*r;
        seeds[i]=Math.random();
        sizes[i]=0.3+Math.random()*0.7;
    }
    var g=new THREE.BufferGeometry();
    g.setAttribute('position',new THREE.BufferAttribute(positions,3));
    g.setAttribute('a_seed',new THREE.BufferAttribute(seeds,1));
    g.setAttribute('a_size',new THREE.BufferAttribute(sizes,1));
    return g;
};
FIREFLIES.setupCamera=function(camera,preset){
    camera.position.set(0,2,preset.camDist||10);
    camera.lookAt(0,0,0);
};
FIREFLIES.extraUniforms=function(preset){
    return {u_displaceAmt:{value:preset.displaceAmt||1}};
};
FIREFLIES.materialOpts={transparent:true,depthWrite:false,blending:THREE.AdditiveBlending,darkBg:true};
FIREFLIES.afterInit=function(inst){
    
    inst.time=2000;

    

    inst._spinPhase=0; inst._spinTarget=0;
    window._impulseFirefliesHyperJump=function(){ inst._spinTarget+=Math.PI*2; };
};
FIREFLIES.updateFrame=function(inst,dt){
    var mx=(inst.mouseX-0.5)*1.5;
    var my=(inst.mouseY-0.5)*0.6;
    var cd=inst._activePreset.camDist||10;

    
    if(inst._spinTarget!==inst._spinPhase){
        inst._spinPhase+=(inst._spinTarget-inst._spinPhase)*(1-Math.pow(0.0015,dt));
        if(Math.abs(inst._spinTarget-inst._spinPhase)<0.01){inst._spinPhase=0;inst._spinTarget=0;}
    }
    
    var baseAmt=inst._activePreset.displaceAmt||1.0;
    var amt=baseAmt*(0.4+inst.intensity*1.2);
    if(inst.material&&inst.material.uniforms&&inst.material.uniforms.u_displaceAmt){
        inst.material.uniforms.u_displaceAmt.value=amt;
    }
    var ang=mx+inst._spinPhase;
    inst.camera.position.x=Math.sin(ang)*cd;
    inst.camera.position.y=2+my*cd*0.3;
    inst.camera.position.z=Math.cos(ang)*cd;
    inst.camera.lookAt(0,0,0);
};

var RAINDROP={};
RAINDROP.vert=['varying vec2 v_uv;','void main(){v_uv=uv;gl_Position=vec4(position.xy,0.0,1.0);}'].join('\n');
RAINDROP.frag=[
'uniform float u_time;',
'uniform float u_speed;',
'uniform float u_displaceAmt;',
'uniform vec2 u_resolution;',
'uniform vec2 u_mouse;',
'uniform sampler2D u_palette;',
'varying vec2 v_uv;',
'float hash(vec2 p){return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453);}',
'vec2 hash2(vec2 p){return fract(sin(vec2(dot(p,vec2(127.1,311.7)),dot(p,vec2(269.5,183.3))))*43758.5453);}',
'',
'vec3 bg(vec2 uv,float t){',
'  float n=sin(uv.x*4.0+t*0.2)*cos(uv.y*3.0-t*0.15)*0.5+0.5;',
'  n+=sin(uv.x*7.0-t*0.3)*0.15;',
'  vec3 c1=texture2D(u_palette,vec2(0.25,0.5)).rgb;',
'  vec3 c2=texture2D(u_palette,vec2(0.75,0.5)).rgb;',
'  return mix(c1*0.6,c2*0.8,n);',
'}',
'',
'void main(){',
'  float t=u_time*u_speed*0.003;',
'  vec2 uv=gl_FragCoord.xy/u_resolution;',
'  vec2 aspect=vec2(u_resolution.x/u_resolution.y,1.0);',
'  vec3 col=bg(uv,t);',
'  float dropCount=10.0+u_displaceAmt*40.0;',
'  vec2 gridSize=vec2(dropCount*aspect.x,dropCount);',
'  vec2 cell=floor(uv*gridSize);',
'  vec2 totalRefract=vec2(0.0);',
'  float totalWeight=0.0;',
'  for(int dx=-1;dx<=1;dx++){',
'    for(int dy=-1;dy<=1;dy++){',
'      vec2 nc=cell+vec2(float(dx),float(dy));',
'      vec2 rnd=hash2(nc);',
'      vec2 dropCenter=(nc+0.3+rnd*0.4)/gridSize;',
'      vec2 mDiff=dropCenter-u_mouse;',
'      float mDist=length(mDiff);',
'      dropCenter+=normalize(mDiff+0.001)*exp(-mDist*mDist*12.0)*0.03;',
'      float shimmer=0.5+0.5*sin(t*2.0+rnd.x*6.28+rnd.y*3.14);',
'      vec2 diff=(uv-dropCenter)*aspect;',
'      float dist=length(diff);',
'      float radius=(0.006+rnd.y*0.014)*shimmer;',
'      if(dist<radius){',
'        float drop=1.0-dist/radius;',
'        drop=drop*drop;',
'        totalRefract+=diff*drop*0.15;',
'        totalWeight+=drop;',
'      }',
'    }',
'  }',
'  if(totalWeight>0.0){',
'    vec2 refUV=uv-totalRefract;',
'    vec3 refracted=bg(refUV,t)*1.2;',
'    float spec=pow(totalWeight,4.0)*0.6;',
'    col=mix(col,refracted+vec3(spec),min(totalWeight*2.0,1.0));',
'  }',
'  gl_FragColor=vec4(col,1.0);',
'}'
].join('\n');
RAINDROP.presets={
    rain:{displaceAmt:0.8,colors:['#1a2a4a','#4488aa','#88bbdd','#2244aa']},
    morning_dew:{displaceAmt:0.5,colors:['#2a4a2a','#55aa55','#88cc88','#336633']},
    frost:{displaceAmt:0.6,colors:['#2a3a5a','#8899bb','#aabbdd','#556688']},
    mercury_drop:{displaceAmt:1.0,colors:['#1a1a2e','#aaaacc','#ddddee','#555577']},
    prism:{displaceAmt:0.7,colors:['#3a1a4a','#aa44bb','#44aadd','#ddaa44']},
    ocean_spray:{displaceAmt:0.9,colors:['#0a2a3a','#22aaaa','#66ddcc','#115566']}
};
RAINDROP.build=function(preset){return new THREE.PlaneGeometry(2,2);};
RAINDROP.setupCamera=function(camera){camera.position.set(0,0,1);camera.lookAt(0,0,0);};
RAINDROP.materialOpts={darkBg:true};
RAINDROP.extraUniforms=function(preset){return {u_displaceAmt:{value:preset.displaceAmt||0.8}};};
RAINDROP.updateFrame=function(inst,dt){
    var baseAmt=inst._activePreset.displaceAmt||0.8;
    inst.material.uniforms.u_displaceAmt.value=baseAmt*(0.3+inst.intensity*0.7);
};
var PLEXUS={};
PLEXUS.presets={
    sky:     {nodeCount:80,spread:14,linkDist:2.5,camDist:12,displaceAmt:1,colors:['#3366ff','#aaccff','#99bbee','#6699cc']},
    crimson: {nodeCount:80,spread:14,linkDist:2.5,camDist:12,displaceAmt:1,colors:['#ff2200','#555555','#333333','#222222']},
    mint:    {nodeCount:85,spread:14,linkDist:2.5,camDist:12,displaceAmt:1,colors:['#00cc88','#99ffdd','#66ddbb','#33aa88']},
    gold:    {nodeCount:75,spread:14,linkDist:2.5,camDist:12,displaceAmt:1,colors:['#ffcc00','#554400','#332200','#111100']},
    violet:  {nodeCount:70,spread:14,linkDist:2.5,camDist:12,displaceAmt:1,colors:['#9966ff','#ddbbff','#bb99ee','#8866cc']}
};
PLEXUS.build=function(preset){return new THREE.SphereGeometry(0.01,4,4);};
PLEXUS.setupCamera=function(camera,preset){camera.position.set(0,0,preset.camDist||12);camera.lookAt(0,0,0);};
PLEXUS.materialOpts={transparent:true,depthWrite:false}; 
PLEXUS.vert='varying vec2 v_uv;void main(){v_uv=uv;gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.0);}';
PLEXUS.frag='uniform sampler2D u_palette;varying vec2 v_uv;void main(){vec3 c=texture2D(u_palette,vec2(0.25,0.5)).rgb;gl_FragColor=vec4(c,0.9);}';
PLEXUS.extraUniforms=function(preset){return {u_displaceAmt:{value:preset.displaceAmt||1}};};
PLEXUS.afterInit=function(inst){
    var p=inst._activePreset;
    var _mob='ontouchstart' in window||navigator.maxTouchPoints>0;
    var nc=Math.round((p.nodeCount||80)*(_mob?0.6:1)),sp=p.spread||14;
    var group=new THREE.Group();
    inst.scene.remove(inst.mesh);
    
    var palCol=p.colors||['#4488ff','#88bbff'];
    var nodeColor=new THREE.Color(palCol[0]);
    var nodeMat=new THREE.MeshBasicMaterial({color:nodeColor,transparent:true,opacity:0.9});
    
    var nodes=[];
    for(var i=0;i<nc;i++){
        var nodeSize=0.04+Math.random()*0.04;
        if(!inst._plexusNodeGeo)inst._plexusNodeGeo=new THREE.SphereGeometry(1,16,12);
        var geo=inst._plexusNodeGeo;
        var mesh=new THREE.Mesh(geo,nodeMat);
        var x=(Math.random()-0.5)*sp,y=(Math.random()-0.5)*sp*0.6,z=(Math.random()-0.5)*sp*0.4;
        mesh.scale.set(nodeSize,nodeSize,nodeSize);
        mesh.position.set(x,y,z);
        mesh.userData={ox:x,oy:y,oz:z,px:Math.random()*6.28,py:Math.random()*6.28,pz:Math.random()*6.28,amp:0.4+Math.random()*0.8};
        group.add(mesh);
        nodes.push(mesh);
    }
    
    var maxLines=300;
    
    var lineColor=new THREE.Color(palCol[1]||palCol[0]);
    var lineMat=new THREE.LineBasicMaterial({color:lineColor,transparent:true,opacity:0.2});
    var lines=[];
    for(var li=0;li<maxLines;li++){
        var geo2=new THREE.BufferGeometry();
        var pts=new Float32Array(6);
        geo2.setAttribute('position',new THREE.BufferAttribute(pts,3));
        var line=new THREE.Line(geo2,lineMat.clone());
        line.visible=false;
        group.add(line);
        lines.push(line);
    }
    inst.scene.add(group);
    inst._plexusGroup=group;
    inst._plexusNodes=nodes;
    inst._plexusLines=lines;
    inst._plexusNodeMat=nodeMat;
};
PLEXUS.updateFrame=function(inst,dt){
    var nodes=inst._plexusNodes,lines=inst._plexusLines;
    if(!nodes)return;
    var t=inst.time*0.001;
    var spd=inst.speed||0.5;
    
    var baseLinkDist=inst._activePreset.linkDist||3.5;
    var linkDist=baseLinkDist*(0.5+inst.intensity*1.0);
    
    var curColors=inst.colors;
    if(curColors&&curColors.length>=2){
        var cKey=curColors[0].getHexString()+curColors[1].getHexString();
        if(cKey!==inst._plexusColorKey){
            inst._plexusColorKey=cKey;
            if(inst._plexusNodeMat) inst._plexusNodeMat.color.copy(curColors[0]);
            for(var li=0;li<lines.length;li++)lines[li].material.color.copy(curColors[Math.min(1,curColors.length-1)]);
        }
    }
    
    for(var j=0;j<nodes.length;j++){
        var n=nodes[j],u=n.userData;
        n.position.x=u.ox+Math.sin(t*spd*0.5+u.px)*u.amp;
        n.position.y=u.oy+Math.cos(t*spd*0.4+u.py)*u.amp*0.7;
        n.position.z=u.oz+Math.sin(t*spd*0.35+u.pz)*u.amp*0.5;
    }
    
    var mx=(inst.mouseX-0.5)*inst._activePreset.spread*0.8;
    var my=(inst.mouseY-0.5)*inst._activePreset.spread*0.5;
    
    var lineIdx=0;
    var ld2=linkDist*linkDist;
    for(var a=0;a<nodes.length&&lineIdx<lines.length;a++){
        var na=nodes[a];
        
        var dmx=na.position.x-mx,dmy=na.position.y-my;
        var md2=dmx*dmx+dmy*dmy;
        if(md2<ld2*1.5&&lineIdx<lines.length){
            var l=lines[lineIdx];
            var pos=l.geometry.attributes.position.array;
            pos[0]=na.position.x;pos[1]=na.position.y;pos[2]=na.position.z;
            pos[3]=mx;pos[4]=my;pos[5]=0;
            l.geometry.attributes.position.needsUpdate=true;
            l.material.opacity=0.35*(1.0-Math.sqrt(md2/(ld2*1.5)));
            l.visible=true;
            lineIdx++;
        }
        
        for(var b=a+1;b<nodes.length&&lineIdx<lines.length;b++){
            var nb=nodes[b];
            var dx=na.position.x-nb.position.x;
            var dy=na.position.y-nb.position.y;
            var dz=na.position.z-nb.position.z;
            var d2=dx*dx+dy*dy+dz*dz;
            if(d2<ld2){
                var l2=lines[lineIdx];
                var pos2=l2.geometry.attributes.position.array;
                pos2[0]=na.position.x;pos2[1]=na.position.y;pos2[2]=na.position.z;
                pos2[3]=nb.position.x;pos2[4]=nb.position.y;pos2[5]=nb.position.z;
                l2.geometry.attributes.position.needsUpdate=true;
                l2.material.opacity=0.4*(1.0-Math.sqrt(d2/ld2));
                l2.visible=true;
                lineIdx++;
            }
        }
    }
    
    for(var h=lineIdx;h<lines.length;h++)lines[h].visible=false;
    
    inst._plexusGroup.rotation.y+=dt*0.02*spd;
    
    var cmx=(inst.mouseX-0.5)*1.2;
    var cmy=(inst.mouseY-0.5)*0.5;
    var cd=inst._activePreset.camDist||12;
    inst.camera.position.x=Math.sin(cmx)*cd;
    inst.camera.position.y=cmy*cd*0.3;
    inst.camera.position.z=Math.cos(cmx)*cd;
    inst.camera.lookAt(0,0,0);
};

var STARS_3D={};
STARS_3D.vert=[
'attribute float a_seed;',
'attribute float a_baseSize;',
'attribute float a_temp;',
'attribute float a_bright;',
'uniform float u_time;',
'uniform float u_speed;',
'uniform float u_intensity;',
'uniform float u_maxDist;',
'uniform float u_jumpOffset;',
'varying float v_alpha;',
'varying float v_bright;',
'varying vec3 v_starColor;',
'void main(){',
'  float t=(u_time+u_jumpOffset)*u_speed*0.00012;',
'  float maxD=u_maxDist;',
'  float flyMult=1.0;',
'  float zCycle=mod(position.z+t*maxD*0.35*flyMult+a_seed*maxD,maxD);',
'  vec3 flyPos=vec3(position.x,position.y,-(maxD-zCycle));',
'  float tw=1.0;',
'  if(a_seed<0.03){tw=0.85+0.15*sin(u_time*0.0006*(0.8+a_seed*30.0)+a_seed*400.0);}',
'  v_alpha=tw;',
'  float tp=a_temp;',
'  vec3 hot=vec3(0.72,0.82,1.0);vec3 wht=vec3(1.0,0.98,0.96);',
'  vec3 yel=vec3(1.0,0.93,0.72);vec3 red=vec3(1.0,0.62,0.42);',
'  if(tp<0.4)v_starColor=mix(hot,wht,tp/0.4);',
'  else if(tp<0.7)v_starColor=mix(wht,yel,(tp-0.4)/0.3);',
'  else v_starColor=mix(yel,red,(tp-0.7)/0.3);',
'  vec4 mv=modelViewMatrix*vec4(flyPos,1.0);',
'  float dist=-mv.z;',
'  float zoom=0.5+u_intensity*0.9;',
'  float sz=a_baseSize*zoom;',
'  // Per-star size ceiling: dust stays small (cheaper than the old flat',
'  // 14px cap), only hero stars grow mildly bigger — keeps fill-rate low.',
'  float maxSz=6.0+a_baseSize*2.6;',
'  gl_PointSize=clamp(sz*(600.0/max(1.0,dist)),0.3,maxSz);',
'  v_bright=clamp(30.0/max(1.0,dist),0.3,3.0)*a_bright;',
'  v_alpha*=smoothstep(0.5,4.0,dist);',
'  v_alpha*=smoothstep(maxD*0.38,maxD*0.2,dist);',
'  gl_Position=projectionMatrix*mv;',
'}'
].join('\n');

STARS_3D.frag=[
'varying float v_alpha;',
'varying float v_bright;',
'varying vec3 v_starColor;',
'void main(){',
'  vec2 c=gl_PointCoord-0.5;',
'  float r=length(c)*2.0;',
'  if(r>1.0)discard;',
'  // Tight bright core + a wide soft halo (the halo fakes a bloom).',
'  float core=exp(-r*r*42.0);',
'  float halo=exp(-r*r*3.0)*0.5;',
'  float shape=core+halo;',
'  if(shape<0.008)discard;',
'  vec3 tcol=min(v_starColor*v_bright,vec3(1.4));',
'  // Halo carries the star colour; the core blows toward white.',
'  vec3 col=tcol*halo+(tcol*0.35+vec3(0.65))*core*v_bright;',
'  gl_FragColor=vec4(col,clamp(shape,0.0,1.0)*v_alpha);',
'}'
].join('\n');

STARS_3D._cloudVert=[
'varying vec2 v_uv;',
'varying vec3 v_local;',
'void main(){',
'  v_uv=uv;',
'  v_local=position;',
'  gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.0);',
'}'
].join('\n');

STARS_3D._cloudFrag=[
'precision highp float;',
'uniform vec3 u_color1;',
'uniform vec3 u_color2;',
'uniform vec3 u_color3;',
'uniform float u_opacity;',
'uniform float u_seed;',
'uniform float u_time;',
'uniform float u_flow;',
'varying vec2 v_uv;',
'',
'float chash(vec2 p){return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453);}',
'float cnoise(vec2 p){',
'  vec2 i=floor(p);vec2 f=fract(p);',
'  f=f*f*(3.0-2.0*f);',
'  return mix(mix(chash(i),chash(i+vec2(1.0,0.0)),f.x),',
'             mix(chash(i+vec2(0.0,1.0)),chash(i+vec2(1.0,1.0)),f.x),f.y);',
'}',
'float cfbm5(vec2 p){',
'  float v=0.0;float a=0.5;',
'  for(int i=0;i<5;i++){v+=a*cnoise(p);p*=2.02;a*=0.5;}',
'  return v;',
'}',
'',
'void main(){',
'  vec2 uv=v_uv-0.5;',
'  float dist=length(uv)*2.0;',
'  // Soft asymmetric radial mask — feathered edge, no hard cutoff.',
'  // Sub-1 inner taper keeps the cloud body opaque while the outer 15-20%',
'  // fades to transparency, so neighbouring puffs blend instead of stamping.',
'  float mask=smoothstep(1.08,0.18,dist);',
'  if(mask<0.004)discard;',
'  // Per-cloud seed scatters the sample space so every puff reads as unique.',
'  // Without it, every cloud reuses the same fbm footprint and the field looks',
'  // tiled. Adding u_seed-driven phase to the time term decorrelates motion too.',
'  vec2 p=uv*2.6+vec2(u_seed*17.3,u_seed*9.7);',
'  float t=u_time*u_flow+u_seed*8.0;',
'  // Two-pass domain warp — Nebula effect uses the same recipe; it produces',
'  // long curling dust lanes that a plain fbm can never deliver.',
'  vec2 q=vec2(cfbm5(p+t*0.4),',
'              cfbm5(p+vec2(5.2,1.3)+t*0.4));',
'  vec2 r=vec2(cfbm5(p+3.0*q+vec2(1.7,9.2)+t*0.2),',
'              cfbm5(p+3.0*q+vec2(8.3,2.8)+t*0.2));',
'  float n=cfbm5(p+4.0*r);',
'  // Cloud density curve — pow brings out crests, drops floors to black voids.',
'  float cloud=pow(clamp(n*1.3,0.0,1.0),1.6);',
'  // Hot spots — bright cores where noise crests (star nurseries inside the gas).',
'  float hot=smoothstep(0.68,0.92,n);',
'  hot=pow(hot,2.5);',
'  // Layered colour build: c1 mid clouds → c2 bright crests → c3 hot cores.',
'  // (Cosmos has its own 500-particle star field rendered separately — adding',
'  // in-cloud sparkles here just stamped a grid of pixel rectangles flying',
'  // toward camera. The hot-spot blend gives enough internal brightness on',
'  // its own; no need for synthetic specks.)',
'  vec3 col=u_color1*cloud;',
'  col=mix(col,u_color2,clamp(cloud*1.7-0.4,0.0,1.0));',
'  col=mix(col,u_color3,hot*0.9);',
'  float alpha=cloud*mask*u_opacity;',
'  if(alpha<0.003)discard;',
'  gl_FragColor=vec4(col,alpha);',
'}'
].join('\n');

STARS_3D.presets={
    deep_space:{starCount:500,spread:40,maxDist:200,
        colors:['#3344aa','#aa44cc','#4488cc','#88aaff']},
    inferno:{starCount:500,spread:40,maxDist:200,
        colors:['#aa2211','#ff6622','#ffaa44','#ffeebb']},
    arctic:{starCount:520,spread:40,maxDist:200,
        colors:['#114488','#33aacc','#88ddee','#ddeeff']},
    galactic:{starCount:540,spread:42,maxDist:210,
        colors:['#aa7722','#22aaaa','#eecc44','#ffffdd']}
};

STARS_3D.build=function(preset){
    var n=preset.starCount||500;
    var sp=preset.spread||40;
    var maxD=preset.maxDist||200;
    var positions=new Float32Array(n*3);
    var seeds=new Float32Array(n);
    var sizes=new Float32Array(n);
    var temps=new Float32Array(n);
    var brights=new Float32Array(n);
    for(var i=0;i<n;i++){

        

        var ang=Math.random()*Math.PI*2;
        var holeR=sp*0.30,fieldR=sp*1.35;
        var rad=Math.sqrt(holeR*holeR+Math.random()*(fieldR*fieldR-holeR*holeR));
        positions[i*3]=Math.cos(ang)*rad;
        positions[i*3+1]=Math.sin(ang)*rad;
        positions[i*3+2]=Math.random()*maxD;
        seeds[i]=Math.random();

        

        
        var r=Math.random();
        var m=r*r;
        sizes[i]=Math.max(0.2,0.32+m*4.6+(Math.random()-0.5)*0.25);
        brights[i]=Math.max(0.18,0.30+m*1.05+(Math.random()-0.5)*0.12);
        var tr=Math.random();
        temps[i]=tr<0.35?tr*0.35:(tr<0.7?0.3+tr*0.25:(tr<0.9?0.5+tr*0.2:0.7+tr*0.3));
    }
    var geo=new THREE.BufferGeometry();
    geo.setAttribute('position',new THREE.BufferAttribute(positions,3));
    geo.setAttribute('a_seed',new THREE.BufferAttribute(seeds,1));
    geo.setAttribute('a_baseSize',new THREE.BufferAttribute(sizes,1));
    geo.setAttribute('a_bright',new THREE.BufferAttribute(brights,1));
    geo.setAttribute('a_temp',new THREE.BufferAttribute(temps,1));
    return geo;
};

STARS_3D.setupCamera=function(camera,preset){
    camera.position.set(0,0,0);
    camera.lookAt(0,0,-1);
    camera.far=500;
    camera.updateProjectionMatrix();
};
STARS_3D.extraUniforms=function(preset){
    return {u_maxDist:{value:preset.maxDist||200},u_intensity:{value:0.5},u_jumpOffset:{value:0.0}};
};
STARS_3D.isPoints=true;
STARS_3D.materialOpts={
    transparent:true,
    depthWrite:false,
    depthTest:false,
    darkBg:true
};
STARS_3D.ppGrain=0.2;
STARS_3D.ppBlur=0.0;

STARS_3D.afterInit=function(inst){
    inst.material.blending=THREE.AdditiveBlending;
    
    inst._jumpRate=0;
    inst._jumpOffset=0;
    window._impulseCosmosHyperJump=function(){
        inst._jumpRate=60000;
    };
    var colors=inst.colors||[{r:0.2,g:0.27,b:0.67},{r:0.67,g:0.27,b:0.8},{r:0.27,g:0.53,b:0.8},{r:0.4,g:0.27,b:0.67}];
    var maxD=200;var sp=40;

    

    

    

    try{
        var farN=400;var farR=160;var farRin=28;
        var farPos    = new Float32Array(farN*3);
        var farRadii  = new Float32Array(farN);
        var farThetas = new Float32Array(farN);
        var farPhis   = new Float32Array(farN);
        var farRates  = new Float32Array(farN); 
        for(var i=0;i<farN;i++){

            

            var theta=Math.random()*Math.PI*2;
            var phi=Math.acos(2*Math.random()-1);
            var r=farRin + Math.random() * (farR - farRin);
            farRadii[i]=r;
            farThetas[i]=theta;
            farPhis[i]=phi;
            farRates[i]=0.7 + Math.random()*0.6; 
            farPos[i*3]   = Math.sin(phi)*Math.cos(theta)*r;
            farPos[i*3+1] = Math.sin(phi)*Math.sin(theta)*r;
            farPos[i*3+2] = Math.cos(phi)*r;
        }
        var farGeo=new THREE.BufferGeometry();
        farGeo.setAttribute('position',new THREE.BufferAttribute(farPos,3));
        var farMat=new THREE.PointsMaterial({
            color:0xaabbcc,size:1.5,transparent:true,opacity:0.5,
            sizeAttenuation:false,depthWrite:false,depthTest:false
        });
        farMat.blending=THREE.AdditiveBlending;
        inst.scene.add(new THREE.Points(farGeo,farMat));
        
        inst._farStars = {
            geo:    farGeo,
            pos:    farPos,
            radii:  farRadii,
            thetas: farThetas,
            phis:   farPhis,
            rates:  farRates,
            R_OUT:  farR,
            R_IN:   farRin
        };
    }catch(e){}

    

    inst._nebulae=[];
    function _cV3(c, fb) {
        try {
            if (c && typeof c === 'object' && 'r' in c) return new THREE.Vector3(c.r, c.g, c.b);
            var t = new THREE.Color(c || fb || '#444466');
            return new THREE.Vector3(t.r, t.g, t.b);
        } catch (_e) {
            return new THREE.Vector3(0.27, 0.27, 0.4);
        }
    }
    var palette = colors.length ? colors : [{r:0.2,g:0.27,b:0.67},{r:0.67,g:0.27,b:0.8},{r:0.27,g:0.53,b:0.8},{r:0.4,g:0.27,b:0.67}];

    var palV3 = palette.map(function(c) { return _cV3(c); });

    
    var brightest = palV3[0];
    var brightestL = brightest.x*0.3 + brightest.y*0.59 + brightest.z*0.11;
    for (var pi = 1; pi < palV3.length; pi++) {
        var L = palV3[pi].x*0.3 + palV3[pi].y*0.59 + palV3[pi].z*0.11;
        if (L > brightestL) { brightestL = L; brightest = palV3[pi]; }
    }
    var hotCore = new THREE.Vector3(
        Math.min(1, brightest.x*0.55 + 0.45),
        Math.min(1, brightest.y*0.55 + 0.45),
        Math.min(1, brightest.z*0.55 + 0.55)
    );

    try{

        

        

        var nPuffs = 10;
        for (var i = 0; i < nPuffs; i++) {
            var sz = 38 + Math.random() * 52;

            

            

            var c1i = i % palV3.length;
            var c2i = (i + 1) % palV3.length;
            var c3v = hotCore.clone();
            var cloudMat = new THREE.ShaderMaterial({
                uniforms: {
                    u_color1:  { value: palV3[c1i].clone() },
                    u_color2:  { value: palV3[c2i].clone() },
                    u_color3:  { value: c3v },
                    u_opacity: { value: 0.18 + Math.random() * 0.18 },
                    u_seed:    { value: Math.random() * 100 },
                    u_time:    { value: Math.random() * 1000 },
                    u_flow:    { value: 0.035 + Math.random() * 0.025 }
                },
                vertexShader: STARS_3D._cloudVert,
                fragmentShader: STARS_3D._cloudFrag,
                transparent: true, depthWrite: false, depthTest: false,
                side: THREE.DoubleSide, blending: THREE.AdditiveBlending
            });
            var cloudGeo = new THREE.PlaneGeometry(sz, sz);
            var cloudMesh = new THREE.Mesh(cloudGeo, cloudMat);
            cloudMesh.position.set(
                (Math.random() - 0.5) * sp * 1.4,
                (Math.random() - 0.5) * sp * 0.9,
                -(25 + Math.random() * maxD * 0.7)
            );
            cloudMesh.lookAt(inst.camera.position);

            

            cloudMesh.userData = {
                baseOp: cloudMat.uniforms.u_opacity.value,
                isBg: false,
                rotSpeed: (Math.random() - 0.5) * 0.06,
                rotAngle: Math.random() * Math.PI * 2
            };
            inst.scene.add(cloudMesh);
            inst._nebulae.push(cloudMesh);
        }
    }catch(e){}

    
    inst._asteroids=[];
    var maxD=200;var sp=40;
    
    var asteroidGeos=[];
    for(var ai=0;ai<3;ai++){
        var ag=new THREE.IcosahedronGeometry(1,ai<2?0:1);

        

        

        var aPos=ag.attributes.position.array;
        for(var av=0;av<aPos.length;av+=3){
            var ax=aPos[av],ay=aPos[av+1],az=aPos[av+2];
            var ah=Math.sin(ax*12.9898+ay*78.233+az*37.719)*43758.5453;
            ah=ah-Math.floor(ah);                       
            var deform=0.7+ah*0.6;
            aPos[av]=ax*deform;aPos[av+1]=ay*deform;aPos[av+2]=az*deform;
        }
        ag.computeVertexNormals();
        asteroidGeos.push(ag);
    }
    
    if(!inst._asteroidLight){
        var aLight=new THREE.DirectionalLight(0xffeedd,0.8);
        aLight.position.set(1,0.5,0.3);
        inst.scene.add(aLight);
        
        var aAmb=new THREE.AmbientLight(0x222233,0.15);
        inst.scene.add(aAmb);
        inst._asteroidLight=aLight;
    }
    for(var ai=0;ai<4;ai++){
        var aGeo=asteroidGeos[Math.floor(Math.random()*asteroidGeos.length)];
        var aMat=new THREE.MeshStandardMaterial({
            color:0x443322,roughness:0.9,metalness:0.1,
            transparent:true,opacity:0,depthWrite:true
        });
        var aMesh=new THREE.Mesh(aGeo,aMat);
        aMesh.visible=false;
        var scale=0.15+Math.random()*0.35;
        aMesh.scale.set(scale,scale*0.8,scale*0.9);
        inst.scene.add(aMesh);
        inst._asteroids.push({
            mesh:aMesh,mat:aMat,
            active:false,timer:0,
            cooldown:3+ai*5+Math.random()*8,
            pos:new THREE.Vector3(),vel:new THREE.Vector3(),
            rotVel:new THREE.Vector3(
                (Math.random()-0.5)*2,
                (Math.random()-0.5)*2,
                (Math.random()-0.5)*1
            )
        });
    }
};

STARS_3D.updateFrame=function(inst,dt){
    if(inst.material&&inst.material.uniforms&&inst.material.uniforms.u_intensity)inst.material.uniforms.u_intensity.value=inst.intensity;

    if(inst._jumpRate>1){
        inst._jumpRate*=Math.pow(0.005,dt);
        if(inst._jumpRate<1)inst._jumpRate=0;
        inst._jumpOffset+=inst._jumpRate*dt;
        if(inst.material&&inst.material.uniforms&&inst.material.uniforms.u_jumpOffset){
            inst.material.uniforms.u_jumpOffset.value=inst._jumpOffset;
        }
    }
    var mx=(inst.mouseX-0.5)*2;
    var my=(inst.mouseY-0.5)*2;
    inst.camera.position.x=mx*2;
    inst.camera.position.y=my*1.5;
    inst.camera.rotation.y=mx*0.015;
    inst.camera.rotation.x=my*0.01;

    var spd=inst.speed;
    var maxD=200;var sp=40;

    
    
    var nebPresence=Math.max(0,Math.min(1,inst.intensity));
    nebPresence=nebPresence*nebPresence;

    if(inst._nebulae){

        

        var nSpd=spd*12;
        var nTimeScale=0.55+spd*0.45;
        for(var ni=0;ni<inst._nebulae.length;ni++){
            var nb=inst._nebulae[ni];
            var u=nb.material.uniforms;

            
            if(u && u.u_time) u.u_time.value+=dt*nTimeScale;
            nb.position.z+=nSpd*dt;
            nb.lookAt(inst.camera.position);

            

            nb.userData.rotAngle += dt * nb.userData.rotSpeed * nTimeScale;
            nb.rotateZ(nb.userData.rotAngle);
            if(nb.position.z>5){
                nb.position.z=-(140+Math.random()*60);
                nb.position.x=(Math.random()-0.5)*sp*1.4;
                nb.position.y=(Math.random()-0.5)*sp*0.9;

                
                if(u && u.u_seed) u.u_seed.value=Math.random()*100;
                nb.userData.rotSpeed = (Math.random() - 0.5) * 0.06;
                nb.userData.rotAngle = Math.random() * Math.PI * 2;
            }

            var zAbs=-nb.position.z;
            var fadeIn=Math.min(1,Math.max(0,(140-zAbs)/40));
            var fadeOut=Math.min(1,Math.max(0,(zAbs-3)/25));
            if(u && u.u_opacity) u.u_opacity.value=nb.userData.baseOp*fadeIn*fadeOut*nebPresence;
        }
    }

    

    
    
    if (inst._farStars) {
        var fs = inst._farStars;
        var spdK = Math.max(0.25, spd); 
        var omega   = 0.06 * spdK;       
        var inflow  = 2.6  * spdK;       
        var posArr  = fs.pos;
        var rArr    = fs.radii;
        var tArr    = fs.thetas;
        var pArr    = fs.phis;
        var rateArr = fs.rates;
        var R_OUT   = fs.R_OUT;
        var R_IN    = fs.R_IN;
        for (var fi = 0; fi < rArr.length; fi++) {
            tArr[fi] += dt * omega;
            rArr[fi] -= dt * inflow * rateArr[fi];
            if (rArr[fi] < R_IN) {

                rArr[fi] = R_OUT;
                tArr[fi] = Math.random() * Math.PI * 2;
                pArr[fi] = Math.acos(2 * Math.random() - 1);
                rateArr[fi] = 0.7 + Math.random() * 0.6;
            }
            var rNow = rArr[fi], tNow = tArr[fi], pNow = pArr[fi];
            var sinP = Math.sin(pNow);
            posArr[fi*3]   = sinP * Math.cos(tNow) * rNow;
            posArr[fi*3+1] = sinP * Math.sin(tNow) * rNow;
            posArr[fi*3+2] = Math.cos(pNow) * rNow;
        }
        fs.geo.attributes.position.needsUpdate = true;
    }

    
    if(inst._asteroids){
        for(var ai=0;ai<inst._asteroids.length;ai++){
            var a=inst._asteroids[ai];
            a.timer+=dt;
            if(!a.active){
                if(a.timer>a.cooldown){
                    a.active=true;a.timer=0;
                    a.cooldown=10+Math.random()*20;

                    var ax=(Math.random()-0.5)*sp*0.36;
                    var ay=(Math.random()-0.5)*sp*0.30;
                    a.pos.set(ax,ay,-45-Math.random()*15);
                    a.vel.set((Math.random()-0.5)*2,(Math.random()-0.5)*1.5,15+Math.random()*10);
                    a.mat.opacity=0;
                    a.mesh.visible=true;
                    
                    a.rotVel.set((Math.random()-0.5)*2,(Math.random()-0.5)*2,(Math.random()-0.5)*1);
                }
            } else {
                a.pos.add(a.vel.clone().multiplyScalar(dt));
                a.mesh.position.copy(a.pos);
                
                a.mesh.rotation.x+=a.rotVel.x*dt;
                a.mesh.rotation.y+=a.rotVel.y*dt;
                a.mesh.rotation.z+=a.rotVel.z*dt;
                
                var zAbs=-a.pos.z;
                if(zAbs>30)a.mat.opacity=Math.min(1,(45-zAbs)/15);
                else if(zAbs>5)a.mat.opacity=1;
                else a.mat.opacity=Math.max(0,zAbs/5);
                
                if(a.pos.z>5){
                    a.active=false;a.timer=0;
                    a.mat.opacity=0;
                    a.mesh.visible=false;
                }
            }
        }
    }

};

var COLUMNS={};

COLUMNS.presets={
    city:   {gridN:55,colW:0.74,gap:0.26,maxH:7.5,camDist:44,waveSpd:1.00,colors:['#0a0a1a','#0d2244','#1155aa','#22aaff']},
    ember:  {gridN:50,colW:0.76,gap:0.24,maxH:6.0,camDist:38,waveSpd:1.10,colors:['#0a0000','#660000','#ff4400','#ffcc00']},
    crystal:{gridN:60,colW:0.52,gap:0.48,maxH:9.0,camDist:46,waveSpd:0.85,colors:['#0a0022','#220066','#8833ff','#eeddff']},
    ocean:  {gridN:50,colW:0.82,gap:0.18,maxH:3.5,camDist:36,waveSpd:0.70,colors:['#000a14','#002244','#006688','#00ccaa']},
    chrome: {gridN:52,colW:0.76,gap:0.24,maxH:5.5,camDist:40,waveSpd:0.90,colors:['#0a0a10','#222230','#667788','#aabbcc']}
};

COLUMNS.vert='void main(){gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.0);}';
COLUMNS.frag='void main(){gl_FragColor=vec4(1.0);}';
COLUMNS.build=function(){return new THREE.BoxGeometry(0.01,0.01,0.01);};

COLUMNS.setupCamera=function(camera,preset){
    var _mob='ontouchstart' in window||navigator.maxTouchPoints>0;
    var gN=_mob?Math.round(preset.gridN*0.6):preset.gridN;
    var half=(gN-1)*(preset.colW+preset.gap)*0.5;
    var camR=half*0.88;
    var camH=preset.maxH*1.15*0.625;
    camera.position.set(0,camH<1.5?1.5:camH,camR);
    camera.lookAt(0,(camH<1.5?1.5:camH)*0.2,0);
};

COLUMNS.materialOpts={transparent:false,depthWrite:true,darkBg:true};

COLUMNS.afterInit=function(inst){
    inst.scene.remove(inst.mesh);
    var p=inst._activePreset;
    var _mob='ontouchstart' in window||navigator.maxTouchPoints>0;

    var gridN=_mob?Math.round(p.gridN*0.6):p.gridN;
    var colW=p.colW;
    var step=colW+p.gap;
    var count=gridN*gridN;
    var half=(gridN-1)*step*0.5;

    var geo=new THREE.BoxGeometry(1.0,1.0,1.0);
    var mat=new THREE.MeshPhongMaterial({color:0xffffff,shininess:35,specular:new THREE.Color(0.06,0.06,0.09)});
    var mesh=new THREE.InstancedMesh(geo,mat,count);
    mesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
    inst.scene.add(mesh);

    inst.scene.add(new THREE.AmbientLight(0x181824,1.0));
    var dirLight=new THREE.DirectionalLight(0xffffff,1.25);
    dirLight.position.set(1.5,2.0,1.0);
    inst.scene.add(dirLight);
    var fillLight=new THREE.DirectionalLight(0x334466,0.18);
    fillLight.position.set(-1.0,0.5,-1.0);
    inst.scene.add(fillLight);

    inst._colMesh=mesh;
    inst._colGeo=geo;
    inst._colMat=mat;
    inst._colGridN=gridN;
    inst._colStep=step;
    inst._colColW=colW;
    inst._colHalf=half;
    inst._colMat4=new THREE.Matrix4();
    inst._colColor=new THREE.Color();
    inst._colOrbit=0;

    var initMat4=new THREE.Matrix4();
    var black=new THREE.Color(0x080808);
    for(var k=0;k<count;k++){
        initMat4.set(colW,0,0,0, 0,0.05,0,0.025, 0,0,colW,0, 0,0,0,1);
        mesh.setMatrixAt(k,initMat4);
        mesh.setColorAt(k,black);
    }
    mesh.instanceMatrix.needsUpdate=true;
    if(mesh.instanceColor)mesh.instanceColor.needsUpdate=true;
};

COLUMNS.updateFrame=function(inst,dt){
    var mesh=inst._colMesh;if(!mesh)return;
    var p=inst._activePreset;
    var t=inst.time*0.001; 
    var spd=(inst.speed||0.5)*(p.waveSpd||1.0);
    var gridN=inst._colGridN;
    var step=inst._colStep;
    var colW=inst._colColW;
    var half=inst._colHalf;
    var maxH=p.maxH*(0.25+inst.intensity*0.75);
    var mat4=inst._colMat4;
    var color=inst._colColor;
    var colors=inst.colors;
    var span=gridN*step;

    var mwx=(inst.mouseX-0.5)*span*0.9;
    var mwz=(0.5-inst.mouseY)*span*0.7;
    var rdSq=span*span*0.14;

    var idx=0;
    for(var i=0;i<gridN;i++){
        var nz=gridN>1?(i/(gridN-1)):0.5;
        var z=i*step-half;
        for(var j=0;j<gridN;j++){
            var nx=gridN>1?(j/(gridN-1)):0.5;
            var x=j*step-half;

            var h=Math.sin(nx*6.283+t*spd*2.0)*0.36
                +Math.sin(nz*5.236-t*spd*1.64)*0.30
                +Math.sin((nx+nz)*4.712+t*spd*2.88)*0.20
                +Math.sin(nx*9.425-nz*7.854+t*spd*1.26)*0.14;
            h=(h+1.0)*0.5; 

            var dx=x-mwx,dz=z-mwz;
            var d2=dx*dx+dz*dz;
            h+=Math.sin(Math.sqrt(d2)*1.5-t*spd*2.2)
               *Math.exp(-d2/rdSq)*0.30;
            h=h<0.04?0.04:(h>1.0?1.0:h);

            var height=h*maxH;

            
            mat4.set(
                colW,   0,      0,    x,
                0,      height, 0,    height*0.5,
                0,      0,      colW, z,
                0,      0,      0,    1
            );
            mesh.setMatrixAt(idx,mat4);

            var s=h*(colors.length-1);
            var ci=s|0;
            if(ci>=colors.length-1)ci=colors.length-2;
            var cf=s-ci;
            var ca=colors[ci],cb=colors[ci+1];
            color.setRGB(ca.r+(cb.r-ca.r)*cf,ca.g+(cb.g-ca.g)*cf,ca.b+(cb.b-ca.b)*cf);
            mesh.setColorAt(idx,color);

            idx++;
        }
    }

    mesh.instanceMatrix.needsUpdate=true;
    if(mesh.instanceColor)mesh.instanceColor.needsUpdate=true;

    inst._colOrbit+=dt*0.015*(inst.speed||0.5);
    var az=inst._colOrbit+(inst.mouseX-0.5)*0.45;
    var camR=inst._colHalf*0.88;
    var camH=p.maxH*1.15*(0.25+inst.intensity*0.75);
    if(camH<1.5)camH=1.5;
    var camY=camH+(inst.mouseY-0.5)*camH*0.3;
    if(camY<0.8)camY=0.8;
    inst.camera.position.x=Math.sin(az)*camR;
    inst.camera.position.y=camY;
    inst.camera.position.z=Math.cos(az)*camR;
    inst.camera.lookAt(0,camH*0.18,0);
};

var ASTEROID={};

ASTEROID.presets={
    inner_belt:{count:3000,beltR:8.0,beltW:4.5,thick:0.6,orbitSpd:0.18,camDist:16,colors:['#000000','#887766','#ccaa88','#ffffff','#ff8844']},
    saturn:    {count:4500,beltR:7.5,beltW:6.0,thick:0.3,orbitSpd:0.12,camDist:14,colors:['#000000','#996633','#ffcc88','#ffffff','#ffddaa']},
    kuiper:    {count:2000,beltR:12.0,beltW:5.0,thick:1.8,orbitSpd:0.06,camDist:22,colors:['#000000','#334466','#6688aa','#aaccff','#ffffff']},
    diamonds:  {count:3500,beltR:8.0,beltW:5.0,thick:0.5,orbitSpd:0.22,camDist:15,colors:['#000000','#4488cc','#88ccff','#ffffff','#ccffff']}
};

ASTEROID.vert=[
'attribute float a_seed;',
'attribute float a_ang;',
'attribute float a_radius;',
'attribute float a_height;',
'attribute float a_size;',
'uniform float u_time;',
'uniform float u_speed;',
'uniform float u_intensity;',
'uniform float u_orbitSpd;',
'varying float v_alpha;',
'varying float v_size;',
'void main(){',
'  float ang=a_ang+u_time*u_speed*u_orbitSpd*0.001*(0.7+a_seed*0.6);',
'  vec3 pos=vec3(cos(ang)*a_radius,a_height,sin(ang)*a_radius);',
'  vec4 mv=modelViewMatrix*vec4(pos,1.0);',
'  float dist=-mv.z;',
'  float sz=a_size*(0.5+u_intensity*0.8)*(220.0/max(1.0,dist));',
'  gl_PointSize=clamp(sz,0.5,14.0);',
'  v_alpha=smoothstep(35.0,8.0,dist)*smoothstep(0.5,3.0,dist);',
'  v_size=a_size;',
'  gl_Position=projectionMatrix*mv;',
'}'
].join('\n');

ASTEROID.frag=[
'varying float v_alpha;',
'varying float v_size;',
'uniform sampler2D u_palette;',
'uniform float u_halo;',
'void main(){',
'  vec2 c=gl_PointCoord-0.5;',
'  float r=length(c)*2.0;',
'  if(r>1.0)discard;',
'  float core=exp(-r*r*12.0);',
'  float glow=exp(-r*r*2.0)*u_halo;',         
'  float a=clamp(core+glow,0.0,1.0)*v_alpha;',
'  if(a<0.004)discard;',
'  float t=clamp(v_size/3.0,0.0,1.0);',
'  vec3 col=texture2D(u_palette,vec2(0.3+t*0.4,0.5)).rgb;',
'  gl_FragColor=vec4(col,a);',                
'}'
].join('\n');

ASTEROID.isPoints=true;
ASTEROID.materialOpts={transparent:true,depthWrite:false,darkBg:true};

ASTEROID.extraUniforms=function(preset){
    return {u_intensity:{value:0.5},u_orbitSpd:{value:preset.orbitSpd||0.18},u_halo:{value:0.4}};
};
ASTEROID.afterInit=function(inst){
    if(inst.material&&inst.material.uniforms.u_halo&&inst._halo!==undefined&&inst._halo!==null){
        inst.material.uniforms.u_halo.value=Math.max(0,Math.min(1,inst._halo/10));
    }
};

ASTEROID.build=function(preset){
    var _mob='ontouchstart' in window||navigator.maxTouchPoints>0;
    var cnt=Math.round((preset.count||3000)*(_mob?0.5:1));
    var bR=preset.beltR||8.0,bW=preset.beltW||4.5,bT=preset.thick||0.6;
    var positions=new Float32Array(cnt*3);
    var seeds=new Float32Array(cnt);
    var angs=new Float32Array(cnt);
    var radii=new Float32Array(cnt);
    var heights=new Float32Array(cnt);
    var sizes=new Float32Array(cnt);
    for(var i=0;i<cnt;i++){
        var ang=Math.random()*Math.PI*2;
        var r=bR+(Math.random()-0.5)*bW;
        var h=(Math.random()-0.5)*bT*2.0;
        positions[i*3]=Math.cos(ang)*r;
        positions[i*3+1]=h;
        positions[i*3+2]=Math.sin(ang)*r;
        seeds[i]=Math.random();
        angs[i]=ang;
        radii[i]=r;
        heights[i]=h;
        sizes[i]=0.5+Math.random()*Math.random()*3.0;
    }
    var geo=new THREE.BufferGeometry();
    geo.setAttribute('position',new THREE.BufferAttribute(positions,3));
    geo.setAttribute('a_seed',new THREE.BufferAttribute(seeds,1));
    geo.setAttribute('a_ang',new THREE.BufferAttribute(angs,1));
    geo.setAttribute('a_radius',new THREE.BufferAttribute(radii,1));
    geo.setAttribute('a_height',new THREE.BufferAttribute(heights,1));
    geo.setAttribute('a_size',new THREE.BufferAttribute(sizes,1));
    return geo;
};

ASTEROID.setupCamera=function(camera,preset){
    var cd=preset.camDist||16;
    camera.position.set(cd*0.6,cd*0.5,cd*0.8);
    camera.lookAt(0,0,0);
};

ASTEROID.updateFrame=function(inst,dt){
    if(inst.material&&inst.material.uniforms){
        if(inst.material.uniforms.u_intensity)inst.material.uniforms.u_intensity.value=inst.intensity||0.5;
    }
    var t=inst.time*0.001;
    var mx=(inst.mouseX-0.5)*0.5;
    var my=(inst.mouseY-0.5)*0.3;
    var cd=inst._activePreset.camDist||16;
    inst.camera.position.x=Math.sin(t*0.04+mx)*cd*0.9;
    inst.camera.position.y=cd*0.4+my*cd*0.4;
    inst.camera.position.z=Math.cos(t*0.04+mx)*cd*0.9;
    inst.camera.lookAt(0,0,0);
};

var GRID={};
GRID.presets={
  gold:     {dotColor:'#e9c24a',shapeColor:'#eab308',dotSpacing:11,shapeCount:7,camDist:90,colors:['#e9c24a','#eab308','#cccccc','#ffffff']},
  mono:     {dotColor:'#d2d2d2',shapeColor:'#aeaeae',dotSpacing:11,shapeCount:7,camDist:90,colors:['#d2d2d2','#aeaeae','#dddddd','#ffffff']},
  blueprint:{dotColor:'#a7c0e8',shapeColor:'#6f93d6',dotSpacing:11,shapeCount:7,camDist:90,colors:['#a7c0e8','#6f93d6','#cfe0f5','#ffffff']},
  warm:     {dotColor:'#e3b79a',shapeColor:'#cf9a6f',dotSpacing:11,shapeCount:7,camDist:90,colors:['#e3b79a','#cf9a6f','#dddddd','#ffffff']}
};
GRID.ppBlur=0; GRID.ppGrain=0; 
GRID.build=function(){return new THREE.SphereGeometry(0.01,4,4);};
GRID.setupCamera=function(cam,p){cam.position.set(0,0,p.camDist||90);cam.lookAt(0,0,0);};
GRID.materialOpts={transparent:true,depthWrite:false};
GRID.vert='void main(){gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.0);}';
GRID.frag='void main(){gl_FragColor=vec4(0.0);}';
GRID.extraUniforms=function(){return {};};
function _gridRR(s,r){var sh=new THREE.Shape(),h=s*0.5,rad=Math.min(r,h);
  sh.moveTo(-h+rad,-h);sh.lineTo(h-rad,-h);sh.quadraticCurveTo(h,-h,h,-h+rad);
  sh.lineTo(h,h-rad);sh.quadraticCurveTo(h,h,h-rad,h);
  sh.lineTo(-h+rad,h);sh.quadraticCurveTo(-h,h,-h,h-rad);
  sh.lineTo(-h,-h+rad);sh.quadraticCurveTo(-h,-h,-h+rad,-h);return sh;}
function _gridFrame(size,radius,thick){var o=_gridRR(size,radius),i=_gridRR(size-thick*2,Math.max(0.001,radius-thick));
  o.holes.push(new THREE.Path(i.getPoints(40)));return new THREE.ShapeGeometry(o,40);}
function _gridShapeColor(bh,dh,ds,dl,vv){
  return new THREE.Color().setHSL(
    (bh.h+dh*0.5*vv+1)%1,
    Math.max(0,Math.min(1,bh.s+ds*0.30*vv)),
    Math.max(0.12,Math.min(0.92,bh.l+dl*0.35*vv)));
}
GRID.afterInit=function(inst){
  var p=inst._activePreset;
  inst.scene.remove(inst.mesh);
  var c=inst.colors||[];
  var dotCol=c[0]?c[0].clone():new THREE.Color(p.colors[0]);
  
  var fov=(inst.camera.fov||55)*Math.PI/180, camZ=inst.camera.position.z||90;
  var worldH=2*camZ*Math.tan(fov*0.5);
  inst._gridBorder=2*(worldH/(inst.height||inst.canvas.clientHeight||800));

  
  var MAXLENS=24,lensPos=[],lensR=new Float32Array(MAXLENS);
  for(var L=0;L<MAXLENS;L++){lensPos.push(new THREE.Vector2(99999,99999));}

  var _density2spacing=function(dv){return Math.max(4,27-(dv!==undefined?dv:7)*2.3);};
  var dotMat=new THREE.ShaderMaterial({
    transparent:true,depthWrite:false,
    uniforms:{
      u_dotColor:{value:dotCol.clone()},
      u_spacing:{value:_density2spacing(inst._density)},
      u_radius:{value:0.7},
      u_lensPos:{value:lensPos},
      u_lensR:{value:lensR},
      u_lensN:{value:0},
      u_lensGain:{value:1.7}
    },
    vertexShader:'varying vec2 vP;void main(){vP=position.xy;gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.0);}',
    fragmentShader:[
      'precision highp float;varying vec2 vP;',
      'uniform vec3 u_dotColor;uniform float u_spacing;uniform float u_radius;uniform float u_lensGain;',
      'const int MAXLENS=24;uniform vec2 u_lensPos[24];uniform float u_lensR[24];uniform int u_lensN;',
      'void main(){',
      ' float mag=0.0;',
      ' for(int i=0;i<MAXLENS;i++){ if(i>=u_lensN) break;',
      '   float dd=distance(vP,u_lensPos[i]);',
      '   mag=max(mag, smoothstep(u_lensR[i],0.0,dd)); }',
      ' float r=u_radius*(1.0+mag*u_lensGain);',
      ' vec2 g=mod(vP,u_spacing)-u_spacing*0.5; float d=length(g);',
      ' float dv=smoothstep(r,r*0.5,d);',
      ' float a=dv*(0.5+mag*0.45); if(a<0.002) discard;',
      ' gl_FragColor=vec4(u_dotColor,a);',
      '}'].join('\n')
  });
  var dotPlane=new THREE.Mesh(new THREE.PlaneGeometry(1400,1400),dotMat);
  dotPlane.position.z=-300; inst.scene.add(dotPlane);
  inst._gridDotMat=dotMat; inst._gridShapes=[]; inst._gridColorKey='';
  inst._gridLensPos=lensPos; inst._gridLensR=lensR; inst._gridPlaneZ=-300; inst._gridMaxLens=MAXLENS;
  
  inst._gridSetDensity=function(dv){inst._density=dv; if(inst._gridDotMat)inst._gridDotMat.uniforms.u_spacing.value=_density2spacing(dv);};

  
  inst._gridRebuild=function(){
    var old=inst._gridShapes||[],zi,zc;
    for(zi=0;zi<old.length;zi++){inst.scene.remove(old[zi]);
      for(zc=0;zc<old[zi].children.length;zc++){var ch=old[zi].children[zc];
        if(ch.geometry)ch.geometry.dispose(); if(ch.material)ch.material.dispose();}}
    
    var cnt=inst._count||p.shapeCount||7; cnt=Math.max(1,Math.min(MAXLENS,Math.round(cnt)));
    var sizeScale=Math.max(0.1,Math.min(2.5,(inst.intensity!==undefined?inst.intensity:1.0)));
    var maxS=3+16*sizeScale, minS=0.8, border=inst._gridBorder;
    var rnd=function(n){var x=Math.sin(n*127.1+0.5)*43758.5453;return x-Math.floor(x);};

    

    var cc=inst.colors||[];
    var baseCol=cc[1]?cc[1].clone():new THREE.Color(p.colors[1]||'#8aa0c0');
    var bHSL={h:0,s:0,l:0}; baseCol.getHSL(bHSL);
    var vv=(inst._variation!==undefined?inst._variation:3)/10;
    inst._gridVar=vv;
    inst._gridShapeBaseKey=(cc[1]?cc[1].getHexString():'')+':'+vv;
    var shapes=[],placed=[],s;
    for(s=0;s<cnt;s++){
      var grp=new THREE.Group(),circle=(rnd(s*6.3)>0.6);

      var rf=rnd(s*4.5+0.13); var S=minS+Math.pow(rf,1.7)*(maxS-minS);
      var dh=(rnd(s*11.1)-0.5), ds=(rnd(s*12.2)-0.5), dl=(rnd(s*13.3)-0.5);
      var col=_gridShapeColor(bHSL,dh,ds,dl,vv);
      if(circle){
        var ro=S*0.5,ri=Math.max(0.05,ro-border);
        grp.add(new THREE.Mesh(new THREE.RingGeometry(ri,ro,64),
          new THREE.MeshBasicMaterial({color:col.clone(),transparent:true,opacity:0.9,side:THREE.DoubleSide,depthWrite:false})));
      }else{
        var rad=S*0.28;
        grp.add(new THREE.Mesh(new THREE.ShapeGeometry(_gridRR(S,rad)),
          new THREE.MeshBasicMaterial({color:col.clone(),transparent:true,opacity:0.20,side:THREE.DoubleSide,depthWrite:false})));
        grp.add(new THREE.Mesh(_gridFrame(S,rad,border),
          new THREE.MeshBasicMaterial({color:col.clone(),transparent:true,opacity:0.9,side:THREE.DoubleSide,depthWrite:false})));
      }

      var crad=S*0.5, bz=(rnd(s*3.1)-0.5)*10, bx=(rnd(s*1.3)-0.5)*86, by=(rnd(s*2.7)-0.5)*76, at;
      for(at=0;at<28;at++){
        bx=(rnd(s*1.3+at*0.317)-0.5)*86; by=(rnd(s*2.7+at*0.531)-0.5)*76;
        var clash=false;
        for(var pi=0;pi<placed.length;pi++){var P=placed[pi],ddx=bx-P.x,ddy=by-P.y,md=crad+P.crad+2.0;
          if(ddx*ddx+ddy*ddy<md*md){clash=true;break;}}
        if(!clash)break;
      }
      placed.push({x:bx,y:by,crad:crad});
      var pang=rnd(s*15.5)*6.28, rot0=(rnd(s*5.9)-0.5)*0.7;
      grp.position.set(bx,by,bz); grp.rotation.z=rot0;
      grp.userData={bx:bx,by:by,depth:0.3+rnd(s*7.7)*0.7,
        dphase:rnd(s*8.8)*6.28,damp:4+rnd(s*9.9)*8,dspd:0.1+rnd(s*3.3)*0.22,
        pcos:Math.cos(pang),psin:Math.sin(pang),
        repR:18+rnd(s*16.6)*16,repK:6+rnd(s*17.7)*12,
        k:0.05+rnd(s*18.8)*0.06,fric:0.80+rnd(s*19.9)*0.12,
        rot0:rot0,lensW:S*0.5*1.25,crad:crad,vx:0,vy:0,dh:dh,ds:ds,dl:dl};
      inst.scene.add(grp); shapes.push(grp);
    }
    inst._gridShapes=shapes;
    var li; for(li=0;li<MAXLENS;li++){
      if(li<shapes.length){lensR[li]=shapes[li].userData.lensW;}
      else{lensPos[li].set(99999,99999); lensR[li]=0;}}
    dotMat.uniforms.u_lensN.value=Math.min(MAXLENS,shapes.length);
  };
  inst._gridRebuild();
};
GRID.updateFrame=function(inst,dt){
  var shapes=inst._gridShapes; if(!shapes||!shapes.length)return;
  var c=inst.colors;
  
  if(c&&c[0]){var k=c[0].getHexString();
    if(k!==inst._gridColorKey){inst._gridColorKey=k;
      if(inst._gridDotMat)inst._gridDotMat.uniforms.u_dotColor.value.copy(c[0]);}}

  if(c&&c[1]){var k2=c[1].getHexString()+':'+(inst._gridVar||0);
    if(k2!==inst._gridShapeBaseKey){inst._gridShapeBaseKey=k2;
      var bh={h:0,s:0,l:0}; c[1].getHSL(bh); var vv2=inst._gridVar||0;
      for(var si=0;si<shapes.length;si++){var ud=shapes[si].userData,cn=shapes[si].children;
        var nc=_gridShapeColor(bh,ud.dh||0,ud.ds||0,ud.dl||0,vv2);
        for(var ci=0;ci<cn.length;ci++)cn[ci].material.color.copy(nc);}}}
  var t=inst.time*0.001,spd=inst.speed||0.5;
  var mx=inst.mouseX-0.5,my=inst.mouseY-0.5;
  var cux=mx*120,cuy=-my*108; 
  var camZ=inst.camera.position.z||90,planeZ=inst._gridPlaneZ||-300;
  var lp=inst._gridLensPos,lr=inst._gridLensR;
  var step=Math.min((dt||0.016)*60,3); 
  for(var j=0;j<shapes.length;j++){var grp=shapes[j],u=grp.userData;
    
    var dx=Math.sin(t*u.dspd*spd+u.dphase)*u.damp;
    var dy=Math.cos(t*u.dspd*0.8*spd+u.dphase)*u.damp;
    
    var px=(mx*u.pcos-my*u.psin)*u.depth*20.0;
    var py=(-my*u.pcos-mx*u.psin)*u.depth*20.0;
    
    var rx=grp.position.x-cux,ry=grp.position.y-cuy,rd=Math.sqrt(rx*rx+ry*ry)+0.001;
    var rep=rd<u.repR?(1-rd/u.repR):0; rep=rep*rep*u.repK;
    
    var tx=u.bx+dx+px+(rx/rd)*rep, ty=u.by+dy+py+(ry/rd)*rep;
    u.vx=(u.vx+(tx-grp.position.x)*u.k*step)*u.fric;
    u.vy=(u.vy+(ty-grp.position.y)*u.k*step)*u.fric;
    grp.position.x+=u.vx*step; grp.position.y+=u.vy*step;}

  

  
  for(var a=0;a<shapes.length;a++){var A=shapes[a],ua=A.userData;
    for(var b=a+1;b<shapes.length;b++){var B=shapes[b],ub=B.userData;
      var ex=A.position.x-B.position.x,ey=A.position.y-B.position.y;
      var ed=Math.sqrt(ex*ex+ey*ey)+0.0001,minD=ua.crad+ub.crad+2.0;
      if(ed<minD){var nx=ex/ed,ny=ey/ed,corr=(minD-ed)*0.5;
        A.position.x+=nx*corr; A.position.y+=ny*corr; B.position.x-=nx*corr; B.position.y-=ny*corr;
        var rvn=(ua.vx-ub.vx)*nx+(ua.vy-ub.vy)*ny; 
        if(rvn<0){var jn=rvn*0.5; ua.vx-=nx*jn; ua.vy-=ny*jn; ub.vx+=nx*jn; ub.vy+=ny*jn;}
        ua.vx*=0.94; ua.vy*=0.94; ub.vx*=0.94; ub.vy*=0.94;}}}
  
  for(var j2=0;j2<shapes.length;j2++){var g2=shapes[j2],u2=g2.userData;
    g2.rotation.z=u2.rot0+u2.vx*0.012;
    if(lp&&j2<lp.length){var f=(camZ-planeZ)/(camZ-g2.position.z);
      lp[j2].set(g2.position.x*f,g2.position.y*f); lr[j2]=u2.lensW*f;}}
};

var EFFECTS={
    cosmos:STARS_3D,
    wormhole:WORMHOLE
    
    ,
    bubbles:BUBBLES,
    particles:PARTICLES,
    crystal:CRYSTAL,
    blob:BLOB,
    fireflies:FIREFLIES,
    plexus:PLEXUS,
    columns:COLUMNS,
    asteroid:ASTEROID,
    grid:GRID
    
};

function hexToColor(h){
    if(!h||typeof h!=='string')return new THREE.Color(0);
    var alpha=1;
    
    var rm=h.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([0-9.]+))?\s*\)/);
    if(rm){if(rm[4]!==undefined)alpha=parseFloat(rm[4]);h='rgb('+rm[1]+','+rm[2]+','+rm[3]+')';}
    
    var hm=h.match(/^#([0-9a-fA-F]{8})$/);
    if(hm){alpha=parseInt(hm[1].slice(6,8),16)/255;h='#'+hm[1].slice(0,6);}
    var c=new THREE.Color(h);
    c.a=alpha; 
    return c;
}

function createInstance(canvas,opts){
    var effectDef=EFFECTS[opts.effect];
    if(!effectDef)return null;

    var presetName=opts.preset||Object.keys(effectDef.presets)[0];
    var preset=effectDef.presets[presetName]||effectDef.presets[Object.keys(effectDef.presets)[0]];
    var colors=(opts.colors||preset.colors).map(hexToColor);
    var speed=opts.speed!==undefined?opts.speed:1;
    var intensity=opts.intensity!==undefined?opts.intensity:1;

    var matOpts=effectDef.materialOpts||{};
    var isDarkBg=!!matOpts.darkBg;

    

    var effQuality = effectDef.quality || _quality;
    var renderer;
    var _glFailed = false;
    try {
        renderer = new THREE.WebGLRenderer({canvas:canvas,antialias:true,alpha:true,preserveDrawingBuffer:true});

        

        
        if (!renderer || !renderer.getContext || !renderer.getContext()) {
            _glFailed = true;
        }
    } catch (_glErr) {
        _glFailed = true;
    }
    if (_glFailed) {
        
        if (typeof window !== 'undefined') {
            if (window._impulseWebGLOK !== false) {
                window._impulseWebGLOK = false;
                if (window.console && console.info) {
                    console.info('[Impulse] WebGL unavailable — 3D engines fall back to flat rendering for the rest of this session.');
                }
            }
        }

        if (renderer && typeof renderer.dispose === 'function') {
            try { renderer.dispose(); } catch (_e) {}
        }
        return null;
    }

    renderer.setPixelRatio(Math.min((window.devicePixelRatio||1)*effQuality, window._impulseEditorPreview ? 1 : 2));
    var isTransparentBg=!!matOpts.transparentBg;
    renderer.setClearColor(isDarkBg?0x000000:0xffffff,isTransparentBg?0:1);
    renderer.autoClear=false;

    var scene=new THREE.Scene();
    var camera;
    if(opts.effect==='grid'){
        camera=new THREE.PerspectiveCamera(55,canvas.clientWidth/canvas.clientHeight,1,1000);
    } else {
        camera=new THREE.PerspectiveCamera(50,canvas.clientWidth/canvas.clientHeight,0.1,100);
    }

    var ppScene=new THREE.Scene();
    var ppCamera=new THREE.OrthographicCamera(-1,1,1,-1,0,1);

    var paletteTex=createPalette(colors);

    var dpr=Math.min((window.devicePixelRatio||1)*effQuality, window._impulseEditorPreview ? 1 : 2);
    var w=canvas.clientWidth||window.innerWidth;
    var h=canvas.clientHeight||window.innerHeight;

    var uniforms={
        u_time:{value:0},
        u_speed:{value:speed},
        u_resolution:{value:new THREE.Vector2(w*dpr,h*dpr)},
        u_palette:{value:paletteTex},
        u_clearColor:{value:isDarkBg?new THREE.Vector3(0,0,0):new THREE.Vector3(1,1,1)},
        u_mouse:{value:new THREE.Vector2(0.5,0.5)}
    };

    if(effectDef.extraUniforms){
        var extra=effectDef.extraUniforms(preset);
        for(var key in extra)uniforms[key]=extra[key];
    }

    var material=new THREE.ShaderMaterial({
        uniforms:uniforms,
        vertexShader:effectDef.vert,
        fragmentShader:effectDef.frag,
        side:matOpts.side||THREE.FrontSide,
        transparent:matOpts.transparent!==undefined?matOpts.transparent:true,
        depthWrite:matOpts.depthWrite!==undefined?matOpts.depthWrite:true,
        depthTest:matOpts.depthTest!==undefined?matOpts.depthTest:true,
        wireframe:!!matOpts.wireframe,
        extensions:{derivatives:true}
    });
    if(matOpts.blending)material.blending=matOpts.blending;

    var geometry=effectDef.build(preset);
    var mesh;
    var _rings=null;
    if(effectDef.isPoints){
        mesh=new THREE.Points(geometry,material);
    }else if(effectDef.isGroup){
        
        mesh=new THREE.Group();
        _rings=[];
        var rc=preset.ringCount||5;
        var rr=preset.radius||3.5;
        var rt=preset.tube||0.12;
        for(var ri=0;ri<rc;ri++){
            var ringScale=0.7+ri*0.3;
            var ringGeom=new THREE.TorusGeometry(rr*ringScale,rt,16,64);
            var ringMesh=new THREE.Mesh(ringGeom,material);
            ringMesh.rotation.set(ri*0.5,ri*0.3,ri*0.2);
            mesh.add(ringMesh);
            _rings.push(ringMesh);
        }
    }else{
        mesh=new THREE.Mesh(geometry,material);
    }
    scene.add(mesh);

    effectDef.setupCamera(camera,preset);

    var sceneTarget=null;
    var ppMat=new THREE.ShaderMaterial({
        uniforms:{
            u_scene:{value:null},
            u_blurAmount:{value:effectDef.ppBlur !== undefined ? effectDef.ppBlur : 0.015},
            u_grainAmount:{value:effectDef.ppGrain !== undefined ? effectDef.ppGrain : 0.8},
            u_resolution:{value:new THREE.Vector2(w*dpr,h*dpr)}
        },
        vertexShader:PP_VERT,fragmentShader:PP_FRAG,
        depthTest:false,depthWrite:false
    });
    var ppQuad=new THREE.Mesh(new THREE.PlaneGeometry(2,2),ppMat);
    ppScene.add(ppQuad);

    var inst={
        canvas:canvas,
        renderer:renderer,
        scene:scene,
        camera:camera,
        ppScene:ppScene,
        ppCamera:ppCamera,
        ppMat:ppMat,
        ppQuad:ppQuad,
        sceneTarget:sceneTarget,
        mesh:mesh,
        material:material,
        geometry:geometry,
        paletteTex:paletteTex,
        colors:colors,
        colorAlphas:colors.map(function(c){return c.a!==undefined?c.a:1;}),
        effectName:opts.effect,
        effectDef:effectDef,
        presetName:presetName,
        _activePreset:preset,
        speed:speed,
        intensity:intensity,
        _count:opts.count,
        _variation:opts.variation,
        _density:opts.density,
        _halo:opts.halo,
        time:0,
        lastTs:0,
        frameCount:0,
        playing:false,
        visible:true,
        rafId:null,
        width:w,
        height:h,
        mouseX:0.5,mouseY:0.5,
        _targetMX:0.5,_targetMY:0.5,
        _gyroBaseB:null,_gyroBaseG:null,
        _camHeight:(preset.camHeight||0),
        _darkBg:isDarkBg,
        _transparentBg:isTransparentBg,
        _clearColor:new THREE.Color(isDarkBg?0x000000:0xffffff),
        _sensitivity:opts.sensitivity!==undefined?opts.sensitivity:1.0,
        _effQuality:effQuality,
        _rings:_rings
    };

    if(effectDef.afterInit)effectDef.afterInit(inst);

    inst._onMouse=function(e){
        var r=canvas.getBoundingClientRect();
        if(e.clientX>=r.left&&e.clientX<=r.right&&e.clientY>=r.top&&e.clientY<=r.bottom){
            inst._targetMX=(e.clientX-r.left)/r.width;
            inst._targetMY=1-(e.clientY-r.top)/r.height;
        }
    };
    document.addEventListener('mousemove',inst._onMouse);
    
    if (!window._impulseScreenMode) {
    inst._onTouch=function(e){
        if(e.touches.length===1){
            var r=canvas.getBoundingClientRect();
            inst._targetMX=(e.touches[0].clientX-r.left)/r.width;
            inst._targetMY=1-(e.touches[0].clientY-r.top)/r.height;
        }
    };
    inst._onTouchEnd=function(){
        inst._targetMX=0.5;inst._targetMY=0.5;
    };
    document.addEventListener('touchmove',inst._onTouch,{passive:true});
    document.addEventListener('touchend',inst._onTouchEnd,{passive:true});
    }

    

    

    inst._onGyro=function(e){
        if(e.beta===null&&e.gamma===null)return;
        if(inst._gyroBaseB===null){inst._gyroBaseB=e.beta||0;inst._gyroBaseG=e.gamma||0;}
        inst._targetMX=0.5+Math.max(-1,Math.min(1,((e.gamma||0)-inst._gyroBaseG)/8))*0.5;
        inst._targetMY=0.5+Math.max(-1,Math.min(1,((e.beta||0)-inst._gyroBaseB)/8))*0.5;
    };
    if(window._impulseGyro)window._impulseGyro.register(inst._onGyro);

    inst._onResize=function(){resizeInstance(inst);};
    window.addEventListener('resize',inst._onResize);
    resizeInstance(inst);

    inst.updateColors=function(hx){
        inst.colors=hx.map(hexToColor);
        inst.colorAlphas=inst.colors.map(function(c){return c.a!==undefined?c.a:1;});
        if(inst.paletteTex)inst.paletteTex.dispose();
        inst.paletteTex=createPalette(inst.colors);
        inst.material.uniforms.u_palette.value=inst.paletteTex;

        

        
        if(inst._nebulae){
            var _palV3 = inst.colors.map(function(c){
                if (c && typeof c === 'object' && 'r' in c) return new THREE.Vector3(c.r, c.g, c.b);
                var t = new THREE.Color(c || '#444466');
                return new THREE.Vector3(t.r, t.g, t.b);
            });
            if (!_palV3.length) return;
            
            var _br = _palV3[0]; var _brL = _br.x*0.3 + _br.y*0.59 + _br.z*0.11;
            for (var _pi = 1; _pi < _palV3.length; _pi++) {
                var _L = _palV3[_pi].x*0.3 + _palV3[_pi].y*0.59 + _palV3[_pi].z*0.11;
                if (_L > _brL) { _brL = _L; _br = _palV3[_pi]; }
            }
            var _hot = new THREE.Vector3(
                Math.min(1, _br.x * 0.55 + 0.45),
                Math.min(1, _br.y * 0.55 + 0.45),
                Math.min(1, _br.z * 0.55 + 0.55)
            );
            for (var ni = 0; ni < inst._nebulae.length; ni++) {
                var nb = inst._nebulae[ni];
                var u = nb.material && nb.material.uniforms;
                if (!u) continue;

                
                var c1Idx = ni % _palV3.length;
                var c2Idx = (ni + 1) % _palV3.length;
                if (u.u_color1) u.u_color1.value.copy(_palV3[c1Idx]);
                if (u.u_color2) u.u_color2.value.copy(_palV3[c2Idx]);
                if (u.u_color3) u.u_color3.value.copy(_hot);
            }
        }
    };
    inst.updateSpeed=function(s){inst.speed=s;inst.material.uniforms.u_speed.value=s;};

    
    inst._blackOnlyBg = ['bubbles','cosmos','fireflies','particles'].indexOf(inst.effectName) !== -1;
    inst.updateBgColor=function(hex){
        var useHex;
        if(inst._blackOnlyBg){ useHex='#000000'; }
        else if(!hex || typeof hex!=='string' || hex.charAt(0)!=='#'){

            
            
            useHex = inst._darkBg ? '#000000' : '#ffffff';
        } else { useHex=hex; }
        inst._clearColor=new THREE.Color(useHex);
        var c=inst._clearColor;
        if(inst.material.uniforms.u_clearColor)inst.material.uniforms.u_clearColor.value.set(c.r,c.g,c.b);
    };
    inst.updateBlendMode=function(darkBg){
        if(inst._blackOnlyBg)darkBg=true;
        inst._darkBg=darkBg;
        inst._clearColor=new THREE.Color(darkBg?0x000000:0xffffff);
        if(inst.material.uniforms.u_clearColor){
            var c=inst._clearColor;
            inst.material.uniforms.u_clearColor.value.set(c.r,c.g,c.b);
        }
        inst.material.blending=darkBg?THREE.AdditiveBlending:THREE.NormalBlending;
        inst.material.needsUpdate=true;
    };
    inst.updateIntensity=function(val){
        inst.intensity=val;
        if(inst.material.uniforms.u_displaceAmt){
            inst.material.uniforms.u_displaceAmt.value=inst._activePreset.displaceAmt*val;
        }
    };
    inst.updatePreset=function(name){
        var p=inst.effectDef.presets[name];
        if(!p)return;
        inst.presetName=name;
        inst._activePreset=p;
        inst._camHeight=p.camHeight||0;
        
        if(inst.effectDef.extraUniforms){
            var extra=inst.effectDef.extraUniforms(p);
            for(var k in extra){
                if(inst.material.uniforms[k])inst.material.uniforms[k].value=extra[k].value;
            }
        }
        
        if(inst.material.uniforms.u_displaceAmt){
            inst.material.uniforms.u_displaceAmt.value*=inst.intensity;
        }
        
        inst.effectDef.setupCamera(inst.camera,p);
        
        var rebuildEffects=['crystal','bubbles','fireflies','plexus'];
        if(rebuildEffects.indexOf(opts.effect)!==-1){
            inst.scene.remove(inst.mesh);
            if(!inst.effectDef.isGroup)inst.geometry.dispose();
            inst.geometry=inst.effectDef.build(p);
            if(inst.effectDef.isGroup){
                inst.mesh=new THREE.Group();
                inst._rings=[];
                var rc2=p.ringCount||5,rr2=p.radius||3.5,rt2=p.tube||0.12;
                for(var ri2=0;ri2<rc2;ri2++){
                    var rs2=0.7+ri2*0.3;
                    var rg2=new THREE.TorusGeometry(rr2*rs2,rt2,16,64);
                    var rm2=new THREE.Mesh(rg2,inst.material);
                    rm2.rotation.set(ri2*0.5,ri2*0.3,ri2*0.2);
                    inst.mesh.add(rm2);
                    inst._rings.push(rm2);
                }
            }else if(inst.effectDef.isPoints){
                inst.mesh=new THREE.Points(inst.geometry,inst.material);
            }else{
                inst.mesh=new THREE.Mesh(inst.geometry,inst.material);
            }
            inst.scene.add(inst.mesh);
        }
    };

    inst.destroy=function(){destroyInstance(inst);};

    

    inst.pause=function(){pauseInstance(inst);};
    inst.play=function(){playInstance(inst);};

    return inst;
}

function resizeInstance(inst){
    if(!inst.renderer)return;
    var w=inst.canvas.clientWidth||window.innerWidth;
    var h=inst.canvas.clientHeight||window.innerHeight;
    
    var dpr=Math.min((window.devicePixelRatio||1)*(inst._effQuality||_quality), window._impulseEditorPreview ? 1 : 2);
    inst.renderer.setSize(w,h,false);
    inst.renderer.setPixelRatio(dpr);
    inst.camera.aspect=w/h;
    inst.camera.updateProjectionMatrix();
    inst.width=w;inst.height=h;
    var rw=Math.floor(w*dpr),rh=Math.floor(h*dpr);
    inst.material.uniforms.u_resolution.value.set(rw,rh);
    inst.ppMat.uniforms.u_resolution.value.set(rw,rh);
    if(inst.sceneTarget)inst.sceneTarget.dispose();
    inst.sceneTarget=new THREE.WebGLRenderTarget(rw,rh,{
        minFilter:THREE.LinearFilter,magFilter:THREE.LinearFilter,format:THREE.RGBAFormat
    });
    inst.ppMat.uniforms.u_scene.value=inst.sceneTarget.texture;
}

function playInstance(inst){
    if(inst.playing||!inst.renderer)return;
    inst.playing=true;inst.lastTs=0;inst.frameCount=0;

    function tick(ts){
        if(!inst.playing)return;
        inst.frameCount++;
        
        if(inst.frameCount%2!==0){inst.rafId=requestAnimationFrame(tick);return;}

        var dt=0;
        if(inst.lastTs)dt=Math.min((ts-inst.lastTs)/1000,0.1);
        inst.lastTs=ts;
        inst.time+=dt*1000;

        if(!inst.visible){inst.rafId=requestAnimationFrame(tick);return;}

        var sens=inst._sensitivity||0;
        var targetX=sens>0?0.5+(inst._targetMX-0.5)*sens:0.5;
        var targetY=sens>0?0.5+(inst._targetMY-0.5)*sens:0.5;
        inst.mouseX+=(targetX-inst.mouseX)*0.05;
        inst.mouseY+=(targetY-inst.mouseY)*0.05;
        inst.material.uniforms.u_time.value=inst.time;
        inst.material.uniforms.u_mouse.value.set(inst.mouseX,inst.mouseY);

        if(inst.effectDef.updateFrame)inst.effectDef.updateFrame(inst,dt);

        var r=inst.renderer;
        var colAlpha=inst.colorAlphas&&inst.colorAlphas.length?Math.min.apply(null,inst.colorAlphas):1;
        var bgAlpha=(inst._transparentBg?0:1)*colAlpha;
        r.setRenderTarget(inst.sceneTarget);
        r.setClearColor(inst._clearColor,bgAlpha);r.clear(true,true,false);
        r.render(inst.scene,inst.camera);
        
        r.setRenderTarget(null);
        r.setClearColor(inst._clearColor,bgAlpha);r.clear(true,true,false);
        r.render(inst.ppScene,inst.ppCamera);

        inst.rafId=requestAnimationFrame(tick);
    }
    inst.rafId=requestAnimationFrame(tick);
}

function pauseInstance(inst){
    inst.playing=false;
    if(inst.rafId){cancelAnimationFrame(inst.rafId);inst.rafId=null;}
}

function destroyInstance(inst){
    pauseInstance(inst);
    var cv=inst.canvas;
    if(inst._onResize)window.removeEventListener('resize',inst._onResize);
    if(inst._onMouse)document.removeEventListener('mousemove',inst._onMouse);
    if(inst._onTouch)document.removeEventListener('touchmove',inst._onTouch);
    if(inst._onTouchEnd)document.removeEventListener('touchend',inst._onTouchEnd);
    if(inst._onGyro){if(window._impulseGyro)window._impulseGyro.unregister(inst._onGyro);else window.removeEventListener('deviceorientation',inst._onGyro);}
    if(inst.mesh)inst.geometry.dispose();
    if(inst.material)inst.material.dispose();
    if(inst.paletteTex)inst.paletteTex.dispose();
    if(inst.sceneTarget)inst.sceneTarget.dispose();
    if(inst.ppMat)inst.ppMat.dispose();
    if(inst.ppQuad)inst.ppQuad.geometry.dispose();
    if(inst.renderer){
        inst.renderer.dispose();
        if(inst.renderer.domElement){
            inst.renderer.domElement.width=1;
            inst.renderer.domElement.height=1;
        }
        inst.renderer.forceContextLoss();
        inst.renderer.domElement=null;
    }
    inst.renderer=null;inst.canvas=null;
    if(observer&&cv)observer.unobserve(cv);
    if(cv)instances.delete(cv);
}

function init(canvas,opts){
    if(!canvas||!opts||!opts.effect)return null;
    
    if (window._impulseWebGLOK === false) return null;
    
    if(instances.has(canvas))destroyInstance(instances.get(canvas));

    var inst=createInstance(canvas,opts);
    if(!inst)return null;

    instances.set(canvas,inst);

    

    
    if(!observer){
        observer=new IntersectionObserver(function(entries){
            entries.forEach(function(entry){
                var i=instances.get(entry.target);
                if(!i)return;
                i.visible=entry.isIntersecting;
                if(entry.isIntersecting){
                    if(!i.playing)playInstance(i);
                } else {
                    if(i.playing)pauseInstance(i);
                }
            });
        },{threshold:0.01});
    }
    observer.observe(canvas);

    playInstance(inst);
    return inst;
}

function destroy(canvas){
    var inst=instances.get(canvas);
    if(inst)destroyInstance(inst);
}

function destroyAll(){
    instances.forEach(function(inst){destroyInstance(inst);});
    instances.clear();
}

function getEffectList(){return Object.keys(EFFECTS);}

function getPresets(effectName){
    var e=EFFECTS[effectName];
    return e?Object.keys(e.presets):[];
}

window.Impulse3D={
    init:init,
    destroy:destroy,
    destroyAll:destroyAll,
    getEffectList:getEffectList,
    getPresets:getPresets,
    effects:Object.keys(EFFECTS),
    EFFECTS:EFFECTS
};

})();
