000h-cojeev
uiglass-sculpturesource: ok · hit
Glass sculpture
Translucent geometry bends an original patterned studio through a softly lit glass surface.
npx shadcn add "https://000h.cojeev.com/r/glass-sculpture.json"free
Visual style
glass-blurpastel
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Dependencies
Bundle weight · lightthree@0.185.1
Registry dependencies
https://cojeev.com/ui/r/cojeev.jsonhttps://cojeev.com/ui/r/glyph-sculpture.json
Files4 · source cached 10/14/2026, 6:56:23 AM
registry/cojeev/ui/glass-sculpture.tsx
"use client";
import * as React from "react";
import { SculptureStage,type MaterialSculptureCommonProps } from "@/lib/cojeev/sculpture-stage";
import { sculptureNumber } from "@/lib/cojeev/sculpture-raster";
import type { SculptureStudio } from "@/lib/cojeev/sculpture-renderer";
export { SculptureStage } from "@/lib/cojeev/sculpture-stage";
export type { MaterialSculptureCommonProps,SculptureStudio };
export type { SculptureRendererState } from "@/lib/cojeev/sculpture-stage";
export type GlassSculptureProps=MaterialSculptureCommonProps&{refraction?:number;frost?:number;thickness?:number;dispersion?:number;studio?:SculptureStudio};
/** Translucent geometry refracts an original procedural studio, not the surrounding DOM. */
export function GlassSculpture({refraction=1.45,frost=.08,thickness=1.2,dispersion=.55,studio="ribbons",pointerTracking=false,...props}:GlassSculptureProps){
return <SculptureStage {...props} pointerTracking={pointerTracking} effect={{kind:"glass",refraction:sculptureNumber(refraction,1.45,1,2.2),frost:sculptureNumber(frost,.08,0,.8),thickness:sculptureNumber(thickness,1.2,.1,3),dispersion:sculptureNumber(dispersion,.55,0,2),studio:studio==="petals"||studio==="tiles"?studio:"ribbons"}}/>;
}
registry/cojeev/lib/sculpture-stage.tsx
"use client";
import * as React from "react";
import { assignMotionRef } from "@/lib/cojeev-motion/refs";
import { cn } from "@/lib/utils";
import { createGlyphMesh } from "@/lib/cojeev/glyph-mesh";
import { sculptureGeometry } from "@/lib/cojeev/sculpture-geometry";
import { rasterizeSculpture, sculptureGrid, sculpturePrint, sculpturePrintPath, sculptureNumber } from "@/lib/cojeev/sculpture-raster";
import { useMotionVisibility } from "@/lib/cojeev-motion/use-motion-visibility";
import type { SculptureCommonProps } from "@/components/ui/glyph-sculpture";
import { createMaterialRenderer, type MaterialEffect, type MaterialConfiguration, type MaterialRenderer } from "@/lib/cojeev/sculpture-renderer";
export type SculptureRendererState="pending"|"webgl"|"fallback"|"error";
export type MaterialSculptureCommonProps=SculptureCommonProps&{ref?:React.Ref<HTMLDivElement>;onRendererChange?:(state:SculptureRendererState)=>void};
type StageControl = {configure:(configuration:MaterialConfiguration)=>void;repaint:()=>void};
/** Shared native scene boundary; materials never take ownership of page input or scroll. */
export function SculptureStage({effect,form="bloom",tone="sky",geometry,speed=1,turn=0,pitch=0,zoom=1,pointerTracking=true,paused=false,onGeometryError,onRendererChange,className,ref,...props}:MaterialSculptureCommonProps&{effect:MaterialEffect}){
const host=React.useRef<HTMLDivElement>(null),canvas=React.useRef<HTMLCanvasElement>(null),control=React.useRef<StageControl|null>(null),{enabled,inView}=useMotionVisibility(host);
const attach=React.useCallback((node:HTMLDivElement|null)=>{host.current=node;const release=assignMotionRef(ref,node);return()=>{host.current=null;release();};},[ref]);
const safeForm=form==="seed"||form==="pebble"?form:"bloom",safeTone=tone==="rose"||tone==="ink"||tone==="moss"?tone:"sky",pace=sculptureNumber(speed,1,0,3),degrees=sculptureNumber(turn,0,-180,180),tilt=sculptureNumber(pitch,0,-80,80),scale=sculptureNumber(zoom,1,.6,1.15);
const meshResult=React.useMemo(()=>{try{return {mesh:sculptureGeometry(geometry??createGlyphMesh(safeForm)),error:null};}catch(error){return {mesh:null,error:error instanceof Error?error:new Error("The geometry could not be read.")};}},[geometry,safeForm]);
const mesh=meshResult.mesh,kind=effect.kind;
React.useEffect(()=>{if(meshResult.error)onGeometryError?.(meshResult.error);},[meshResult.error,onGeometryError]);
const configuration=React.useMemo<MaterialConfiguration>(()=>({effect,visible:inView,quiet:!enabled,run:enabled&&!paused&&pace>0,respond:enabled&&!paused&&pace>0&&pointerTracking,turn:degrees,pitch:tilt,zoom:scale,speed:pace}),[effect,inView,enabled,paused,pace,pointerTracking,degrees,tilt,scale]);
const latest=React.useRef(configuration);
const [rendered,setRendered]=React.useState<{mesh:typeof mesh;kind:typeof kind;status:"pending"|"webgl"|"fallback"}>({mesh:null,kind,status:"pending"});
const renderer=rendered.mesh===mesh&&rendered.kind===kind?rendered.status:"pending";
React.useEffect(()=>{onRendererChange?.(meshResult.error?"error":renderer);},[renderer,meshResult.error,onRendererChange]);
const still=React.useMemo(()=>{if(!mesh)return null;const grid=sculptureGrid(384,288,4),frame=rasterizeSculpture(mesh,grid,.35+degrees*Math.PI/180,-.16+tilt*Math.PI/180,scale);return {grid,path:sculpturePrintPath(sculpturePrint(frame,{kind:"dither",pattern:"ordered",density:1}),grid.columns)};},[mesh,degrees,tilt,scale]);
React.useEffect(()=>{latest.current=configuration;control.current?.configure(configuration);},[configuration]);
React.useEffect(()=>{control.current?.repaint();},[safeTone]);
React.useEffect(()=>{
const element=host.current,surface=canvas.current;if(!element||!surface||!mesh)return;
let disposed=false,engine:MaterialRenderer|undefined,frame=0,last=0,elapsed=0,options=latest.current,job:AbortController|undefined,painted=false,lost=false;
let previousPointer:{x:number;y:number}|undefined;
const hover=window.matchMedia("(any-hover: hover) and (any-pointer: fine)");
const colors=()=>{const css=getComputedStyle(element);return {paper:css.backgroundColor,ink:css.getPropertyValue("--v-text").trim()||"#20252a",accent:css.getPropertyValue("--material-accent").trim()||"#a6bde8",other:css.getPropertyValue("--v-yellow").trim()||"#edcf70"};};
function cancel(){if(frame)cancelAnimationFrame(frame);frame=0;last=0;}
function request(){if(!disposed&&!lost&&engine&&options.visible&&!document.hidden&&!frame)frame=requestAnimationFrame(draw);}
function fallback(){if(disposed)return;cancel();job?.abort();engine?.dispose(false);engine=undefined;painted=false;setRendered({mesh,kind,status:"fallback"});}
function draw(time:number){frame=0;if(disposed||lost||!engine||!options.visible||document.hidden)return;if(options.run&&last&&time-last<32){request();return;}const seconds=last?Math.min((time-last)/1000,1/30):1/30;last=time;if(options.run)elapsed+=seconds*options.speed;
try{const again=engine.draw(seconds,elapsed);if(!painted){painted=true;setRendered({mesh,kind,status:"webgl"});}if(again)request();}catch{fallback();}
}
function resize(){if(!engine)return;const bounds=element!.getBoundingClientRect();engine.resize(bounds.width,bounds.height,window.devicePixelRatio||1);request();}
function repaint(){engine?.colors(colors());request();}
async function initialize(){if(disposed||lost||engine||job||!options.visible||document.hidden)return;job=new AbortController();const ownJob=job;
try{const next=await createMaterialRenderer(surface!,mesh!,options,colors(),ownJob.signal);if(disposed||ownJob.signal.aborted){next.dispose(!surface!.isConnected);return;}engine=next;engine.configure(options);resize();request();}catch{if(!disposed&&!ownJob.signal.aborted)fallback();}
finally{if(job===ownJob)job=undefined;}
}
const instance:StageControl={configure(next){options=next;engine?.configure(next);if(!next.respond){engine?.leave();previousPointer=undefined;}cancel();if(!engine)void initialize();else request();},repaint};control.current=instance;
function pointer(event:PointerEvent){if(!engine||!options.respond||!options.visible||!hover.matches||event.pointerType==="touch"||document.hidden)return;const rect=element!.getBoundingClientRect(),x=(event.clientX-rect.left)/rect.width,y=1-(event.clientY-rect.top)/rect.height;
if(x<0||x>1||y<0||y>1){engine.leave();previousPointer=undefined;return;}const dx=previousPointer?x-previousPointer.x:.025,dy=previousPointer?y-previousPointer.y:.01;previousPointer={x,y};engine.pointer(x,y,dx,dy);request();
}
const visibility=()=>{cancel();engine?.leave();previousPointer=undefined;if(!document.hidden){if(!engine)void initialize();request();}},leave=()=>{engine?.leave();previousPointer=undefined;},onLost=(event:Event)=>{event.preventDefault();lost=true;fallback();},onRestored=()=>{lost=false;painted=false;void initialize();};
const sizeObserver=new ResizeObserver(resize);sizeObserver.observe(element);const appearance=new MutationObserver(repaint);for(let ancestor:Element|null=element;ancestor;ancestor=ancestor.parentElement)appearance.observe(ancestor,{attributes:true,attributeFilter:["class","style","data-mode"]});
window.addEventListener("pointermove",pointer,{passive:true});window.addEventListener("blur",leave);window.addEventListener("resize",resize);window.addEventListener("cojeev:appearancechange",repaint);document.addEventListener("visibilitychange",visibility);surface.addEventListener("webglcontextlost",onLost);surface.addEventListener("webglcontextrestored",onRestored);void initialize();
return()=>{disposed=true;job?.abort();cancel();if(control.current===instance)control.current=null;sizeObserver.disconnect();appearance.disconnect();window.removeEventListener("pointermove",pointer);window.removeEventListener("blur",leave);window.removeEventListener("resize",resize);window.removeEventListener("cojeev:appearancechange",repaint);document.removeEventListener("visibilitychange",visibility);surface.removeEventListener("webglcontextlost",onLost);surface.removeEventListener("webglcontextrestored",onRestored);engine?.dispose(!surface.isConnected);surface.width=1;surface.height=1;};
},[mesh,kind]);
return <div {...props} ref={attach} className={cn("v-material-sculpture",className)} data-slot={`${kind}-sculpture`} data-tone={safeTone} data-renderer={meshResult.error?"error":renderer} data-moving={configuration.run&&inView&&renderer==="webgl"?"true":"false"}>
{meshResult.error?<span data-slot="material-status" role="status">{meshResult.error.message}</span>:still&&<svg data-slot="material-still" viewBox={`0 0 ${still.grid.columns} ${still.grid.rows}`} aria-hidden="true"><path d={still.path} fill="currentColor"/></svg>}
<canvas ref={canvas} data-slot="material-canvas" aria-hidden="true" inert/>
{!meshResult.error&&renderer!=="webgl"&&<span data-slot="material-status" role={renderer==="fallback"?"status":undefined}>{renderer==="fallback"?"3D material is unavailable. ":""}Static shape preview</span>}
</div>;
}
registry/cojeev/lib/sculpture-renderer.tsx
import type * as THREE from "three";
import type { SculptureGeometry } from "@/lib/cojeev/sculpture-geometry";
import { createVelocityField, stirVelocityField, advanceVelocityField, clearVelocityField } from "@/lib/cojeev/sculpture-flow";
import { createParticleSpring, sampleSculptureSurface, stepParticleSpring, pulseParticleSpring, resetParticleSpring, type ParticlePointer } from "@/lib/cojeev/sculpture-particles";
import { materialPixelSize, orientSculptureTriangles } from "@/lib/cojeev/sculpture-stage-geometry";
export type SculptureStudio = "ribbons" | "petals" | "tiles";
export type MaterialEffect =
| {kind:"glass";refraction:number;frost:number;thickness:number;dispersion:number;studio:SculptureStudio}
| {kind:"flow";distortion:number;chromatic:number;spread:number;settle:number;swirl:number;pulse:number}
| {kind:"particle";count:number;size:number;strength:number;radius:number;spring:number;damping:number;swirl:number;pulse:number};
export type MaterialConfiguration = {effect:MaterialEffect;visible:boolean;run:boolean;quiet:boolean;respond:boolean;turn:number;pitch:number;zoom:number;speed:number};
export type MaterialColors = {ink:string;accent:string;paper:string;other:string};
export type MaterialRenderer = {configure:(options:MaterialConfiguration)=>void;colors:(colors:MaterialColors)=>void;resize:(width:number,height:number,dpr:number)=>void;pointer:(x:number,y:number,dx:number,dy:number)=>void;leave:()=>void;draw:(seconds:number,elapsed:number)=>boolean;dispose:(loseContext?:boolean)=>void};
/** Native Three materials and original field/spring dynamics; no remote textures or scene assets. */
export async function createMaterialRenderer(canvas:HTMLCanvasElement,mesh:SculptureGeometry,configuration:MaterialConfiguration,colors:MaterialColors,signal?:AbortSignal):Promise<MaterialRenderer>{
const three=await import("three");
if(signal?.aborted)throw new DOMException("Material initialization cancelled.","AbortError");
const geometries=new Set<THREE.BufferGeometry>(),materials=new Set<THREE.Material>(),textures=new Set<THREE.Texture>(),targets=new Set<THREE.WebGLRenderTarget>();
let ownedRenderer:THREE.WebGLRenderer|undefined,disposed=false;
function dispose(loseContext=true){
if(disposed)return;disposed=true;
// Every allocation is owned before subsequent setup can throw. Continue cleanup if a lost context rejects one release.
for(const resources of [geometries,materials,textures,targets]){for(const resource of resources){try{resource.dispose();}catch{}}resources.clear();}
try{ownedRenderer?.dispose();}catch{}
if(loseContext){try{ownedRenderer?.forceContextLoss();}catch{}}
}
try{
const renderer=new three.WebGLRenderer({canvas,alpha:false,antialias:true,powerPreference:"low-power"});
ownedRenderer=renderer;
renderer.outputColorSpace=three.SRGBColorSpace;renderer.toneMapping=three.ACESFilmicToneMapping;renderer.toneMappingExposure=1.05;renderer.transmissionResolutionScale=.5;
renderer.debug.onShaderError=()=>{throw new Error("The material shader could not compile. Showing the static shape.");};
const scene=new three.Scene(),camera=new three.OrthographicCamera(-1.7,1.7,1.3,-1.3,.1,30),group=new three.Group();camera.position.z=6;scene.add(group);
const geometry=new three.BufferGeometry();geometries.add(geometry);geometry.setAttribute("position",new three.BufferAttribute(Float32Array.from(mesh.positions),3));geometry.setAttribute("normal",new three.BufferAttribute(Float32Array.from(mesh.normals),3));geometry.setIndex(new three.BufferAttribute(orientSculptureTriangles(mesh),1));
const surface=new three.MeshPhysicalMaterial({roughness:.28,metalness:.04,clearcoat:.8,iridescence:.22});materials.add(surface);
const object=new three.Mesh(geometry,surface);group.add(object);
scene.add(new three.HemisphereLight(0xffffff,0x9b8c85,.85));const key=new three.DirectionalLight(0xfffaf0,1.8);key.position.set(-3,4,5);scene.add(key);const fill=new three.DirectionalLight(0xe0eaff,.65);fill.position.set(4,-1,3);scene.add(fill);
// A small original studio environment. Data textures avoid image decoding or network work.
const faces=Array.from({length:6},(_,face)=>{const data=new Uint8Array(64*64*4);for(let y=0;y<64;y++)for(let x=0;x<64;x++){const u=x/63,v=y/63,patch=Math.exp(-((u-(face%2?.7:.3))**2/.013+(v-.3)**2/.16)),wash=.25+.5*(1-v),i=(y*64+x)*4;data[i]=Math.min(255,(wash+patch*.7)*255);data[i+1]=Math.min(255,(wash+patch*.72)*255);data[i+2]=Math.min(255,(wash+patch*.76)*255);data[i+3]=255;}const texture=new three.DataTexture(data,64,64);textures.add(texture);return texture;});
const environment=new three.CubeTexture(faces);environment.colorSpace=three.SRGBColorSpace;environment.needsUpdate=true;textures.add(environment);scene.environment=environment;
const backdropGeometry=new three.PlaneGeometry(12,10);geometries.add(backdropGeometry);
const backdrop=new three.ShaderMaterial({uniforms:{paper:{value:new three.Color(colors.paper)},a:{value:new three.Color(colors.accent)},b:{value:new three.Color(colors.other)},ink:{value:new three.Color(colors.ink)},studio:{value:0}},vertexShader:"varying vec2 vUv;void main(){vUv=uv;gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.);}",fragmentShader:`varying vec2 vUv;uniform vec3 paper;uniform vec3 a;uniform vec3 b;uniform vec3 ink;uniform float studio;
void main(){vec2 p=(vUv-.5)*5.;float f;
if(studio<.5){f=smoothstep(.20,.32,sin(p.x*4.+sin(p.y*3.)*1.4));}
else if(studio<1.5){vec2 q=fract(p*1.4)-.5;float r=length(q),ang=atan(q.y,q.x);f=1.-smoothstep(.26,.31,r*(1.+.16*cos(5.*ang)));}
else{vec2 q=fract(p*1.25)-.5;f=1.-smoothstep(.16,.19,min(abs(q.x+q.y),abs(q.x-q.y)));}
vec3 pigment=mix(mix(a,b,smoothstep(-2.,2.,p.y)),ink,.2);vec3 c=mix(paper,pigment,f*.9);gl_FragColor=vec4(c,1.);
#include <tonemapping_fragment>
#include <colorspace_fragment>
}`});materials.add(backdrop);const back=new three.Mesh(backdropGeometry,backdrop);back.position.z=-1.65;scene.add(back);
const flow=createVelocityField(),flowTexture=new three.DataTexture(flow.texture,flow.width,flow.height);flowTexture.minFilter=three.LinearFilter;flowTexture.magFilter=three.LinearFilter;flowTexture.needsUpdate=true;textures.add(flowTexture);
const target=new three.WebGLRenderTarget(1,1,{depthBuffer:true});targets.add(target);
const postScene=new three.Scene(),postCamera=new three.OrthographicCamera(-1,1,1,-1,0,1),postGeometry=new three.PlaneGeometry(2,2);geometries.add(postGeometry);
const post=new three.ShaderMaterial({depthTest:false,depthWrite:false,uniforms:{picture:{value:target.texture},field:{value:flowTexture},strength:{value:.1},chromatic:{value:.35}},vertexShader:"varying vec2 vUv;void main(){vUv=uv;gl_Position=vec4(position.xy,0.,1.);}",fragmentShader:`varying vec2 vUv;uniform sampler2D picture;uniform sampler2D field;uniform float strength;uniform float chromatic;
void main(){vec3 current=texture2D(field,vUv).rgb;vec2 v=(current.xy*255.-128.)/127.;vec2 delta=v*strength;vec2 uv=clamp(vUv-delta,vec2(.001),vec2(.999));vec2 fringe=delta*chromatic*.22;
vec3 c=vec3(texture2D(picture,clamp(uv+fringe,vec2(.001),vec2(.999))).r,texture2D(picture,uv).g,texture2D(picture,clamp(uv-fringe,vec2(.001),vec2(.999))).b);gl_FragColor=vec4(c,1.);
#include <tonemapping_fragment>
#include <colorspace_fragment>
}`});materials.add(post);postScene.add(new three.Mesh(postGeometry,post));
const dotData=new Uint8Array(16*16*4);for(let y=0;y<16;y++)for(let x=0;x<16;x++){const i=(y*16+x)*4,a=Math.max(0,Math.min(1,(.48-Math.hypot((x+.5)/16-.5,(y+.5)/16-.5))*25));dotData[i]=dotData[i+1]=dotData[i+2]=255;dotData[i+3]=Math.round(a*255);}
const dots=new three.DataTexture(dotData,16,16);dots.needsUpdate=true;textures.add(dots);
const pointGeometry=new three.BufferGeometry();geometries.add(pointGeometry);const pointMaterial=new three.PointsMaterial({map:dots,alphaTest:.2,size:2.3,sizeAttenuation:false,vertexColors:true,toneMapped:false});materials.add(pointMaterial);const points=new three.Points(pointGeometry,pointMaterial);points.frustumCulled=false;group.add(points);
let options=configuration,halfHeight=1.3,aspect=1,particleCount=0,particleState=createParticleSpring(new Float32Array()),pointer:ParticlePointer|undefined,pulse=configuration.effect.kind==="glass"?0:configuration.effect.pulse;
let targetTiltX=0,targetTiltY=0,tiltX=0,tiltY=0;
function recolor(next:MaterialColors){colors=next;scene.background=new three.Color(next.paper);surface.color.set(next.accent);if(options.effect.kind==="glass")surface.color.lerp(new three.Color(0xffffff),.9);surface.attenuationColor.set(next.accent);backdrop.uniforms.paper.value.set(next.paper);backdrop.uniforms.a.value.set(next.accent);backdrop.uniforms.b.value.set(next.other);backdrop.uniforms.ink.value.set(next.ink);
const values=new Float32Array(particleState.rest.length),base=new three.Color(next.accent),ink=new three.Color(next.ink),temp=new three.Color();for(let i=0;i<values.length;i+=3){temp.copy(base).lerp(ink,.58+(particleState.rest[i+1]+1)*.12);values[i]=temp.r;values[i+1]=temp.g;values[i+2]=temp.b;}pointGeometry.setAttribute("color",new three.BufferAttribute(values,3));
}
function configure(next:MaterialConfiguration){
const previous=options;options=next;const effect=next.effect;back.visible=effect.kind==="glass";object.visible=effect.kind!=="particle";points.visible=effect.kind==="particle";
if(effect.kind==="glass"){surface.transmission=1;surface.thickness=effect.thickness;surface.ior=effect.refraction;surface.roughness=effect.frost;surface.dispersion=effect.dispersion;surface.attenuationDistance=6;surface.metalness=0;surface.clearcoat=.25;surface.iridescence=.05;surface.envMapIntensity=.65;backdrop.uniforms.studio.value=effect.studio==="petals"?1:effect.studio==="tiles"?2:0;}
else if(effect.kind==="flow"){surface.transmission=0;surface.roughness=.26;post.uniforms.strength.value=effect.distortion*.12;post.uniforms.chromatic.value=effect.chromatic;}
else{if(particleCount!==effect.count){particleCount=effect.count;particleState=createParticleSpring(sampleSculptureSurface(mesh,effect.count,17));pointGeometry.setAttribute("position",new three.BufferAttribute(particleState.positions,3).setUsage(three.DynamicDrawUsage));recolor(colors);}pointMaterial.size=effect.size;}
if(next.quiet){clearVelocityField(flow);flowTexture.needsUpdate=true;resetParticleSpring(particleState);if(pointGeometry.getAttribute("position"))pointGeometry.getAttribute("position").needsUpdate=true;pointer=undefined;targetTiltX=targetTiltY=tiltX=tiltY=0;}
if(effect.kind!=="glass"&&effect.pulse!==pulse){pulse=effect.pulse;if(next.run){if(effect.kind==="flow"){stirVelocityField(flow,.37,.47,.7,.12,effect.spread,effect.swirl);stirVelocityField(flow,.62,.56,-.2,.6,effect.spread,effect.swirl);}else pulseParticleSpring(particleState,effect.strength);}}
if(!next.respond&&previous.respond){pointer=undefined;targetTiltX=targetTiltY=0;}
}
const result:MaterialRenderer={
configure,colors:recolor,
resize(width,height,dpr){const size=materialPixelSize(width,height,dpr);renderer.setPixelRatio(size.ratio);renderer.setSize(size.cssWidth,size.cssHeight,false);target.setSize(size.width,size.height);aspect=size.cssWidth/size.cssHeight;halfHeight=Math.max(1.3,1.3/aspect);camera.left=-halfHeight*aspect;camera.right=halfHeight*aspect;camera.top=halfHeight;camera.bottom=-halfHeight;camera.updateProjectionMatrix();if(options.effect.kind==="particle")pointMaterial.size=options.effect.size;},
pointer(x,y,dx,dy){if(!options.respond)return;const effect=options.effect;if(effect.kind==="glass"){targetTiltX=(y-.5)*-.24;targetTiltY=(x-.5)*.35;}else if(effect.kind==="flow"){stirVelocityField(flow,x,y,dx*5,dy*5,effect.spread,effect.swirl);}else{const p=new three.Vector3((x-.5)*halfHeight*aspect*2,(y-.5)*halfHeight*2,0);group.updateMatrixWorld();group.worldToLocal(p);pointer={x:p.x,y:p.y,z:p.z,radius:effect.radius,strength:effect.strength,swirl:effect.swirl};}},
leave(){pointer=undefined;targetTiltX=targetTiltY=0;},
draw(seconds,elapsed){if(disposed)return false;const effect=options.effect;if(options.run){const blend=1-Math.exp(-seconds*9);tiltX+=(targetTiltX-tiltX)*blend;tiltY+=(targetTiltY-tiltY)*blend;}group.rotation.set(-.16+options.pitch*Math.PI/180+Math.sin(elapsed*.37)*.05+tiltX,.35+options.turn*Math.PI/180+Math.sin(elapsed*.43)*.16+tiltY,0);group.scale.setScalar(options.zoom);
if(effect.kind==="flow"){if(options.run)advanceVelocityField(flow,seconds*options.speed,effect.settle);flowTexture.needsUpdate=true;renderer.setRenderTarget(target);renderer.render(scene,camera);renderer.setRenderTarget(null);renderer.render(postScene,postCamera);}
else{if(effect.kind==="particle"&&options.run){stepParticleSpring(particleState,seconds*options.speed,effect.spring,effect.damping,pointer);pointGeometry.getAttribute("position").needsUpdate=true;}renderer.render(scene,camera);}
return options.run;
},
dispose
};
configure(configuration);recolor(colors);return result;
}catch(error){dispose();throw error;}
}
registry/cojeev/styles/glass-sculpture.css
@layer cojeev-states {
.v-material-sculpture{--material-accent:var(--v-blue);position:relative;width:100%;min-width:0;height:clamp(260px,44vw,420px);overflow:hidden;contain:layout paint;background:var(--card);border-radius:var(--r-card,20px);color:var(--v-text);pointer-events:none;user-select:none}
.v-material-sculpture[data-tone="rose"]{--material-accent:var(--v-pink)}
.v-material-sculpture[data-tone="moss"]{--material-accent:var(--v-olive)}
.v-material-sculpture[data-tone="ink"]{--material-accent:var(--v-text)}
.v-material-sculpture>[data-slot="material-canvas"],.v-material-sculpture>[data-slot="material-still"]{position:absolute;inset:0;width:100%;height:100%;display:block;pointer-events:none}
.v-material-sculpture>[data-slot="material-canvas"]{visibility:hidden}
.v-material-sculpture[data-renderer="webgl"]>[data-slot="material-canvas"]{visibility:visible}
.v-material-sculpture[data-renderer="webgl"]>[data-slot="material-still"]{visibility:hidden}
.v-material-sculpture>[data-slot="material-status"]{position:absolute;inset:auto 16px 12px;text-align:center;font:var(--fs-meta)/1.5 var(--font-text,system-ui);color:var(--v-text);background:var(--card);border-radius:var(--r-md);padding:var(--s-1) var(--s-2)}
.v-material-sculpture[data-renderer="error"]>[data-slot="material-status"]{inset:24px;display:grid;place-items:center;font-size:var(--fs-control)}
@media(forced-colors:active){.v-material-sculpture[data-renderer]>[data-slot="material-canvas"]{visibility:hidden}.v-material-sculpture[data-renderer]>[data-slot="material-still"]{visibility:visible;color:CanvasText}}
}