UNPKG

reactbits-mcp-server

Version:

MCP Server for React Bits - Access 99+ React components with animations, backgrounds, and UI elements

328 lines (299 loc) 9.64 kB
import { useEffect, useRef } from "react"; import { CanvasTexture, Clock, Color, LinearFilter, LinearMipmapLinearFilter, Mesh, OrthographicCamera, PlaneGeometry, Scene, ShaderMaterial, Vector2, Vector3, WebGLRenderer, WebGLRenderTarget } from "three"; const hexToRgb = (hex) => { let h = hex.replace("#", ""); if (h.length === 3) h = h .split("") .map((c) => c + c) .join(""); const n = parseInt(h, 16); return [(n >> 16) & 255, (n >> 8) & 255, n & 255]; }; const loadFont = async (fam) => { if ("fonts" in document) await (document).fonts.load(`64px "${fam}"`); }; const BASE_VERT = ` varying vec2 v_uv; void main(){gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.0);v_uv=uv;}`; const SIMPLEX = ` vec3 mod289(vec3 x){return x-floor(x*(1./289.))*289.;} vec4 mod289(vec4 x){return x-floor(x*(1./289.))*289.;} vec4 permute(vec4 x){return mod289(((x*34.)+1.)*x);} float snoise3(vec3 v){ const vec2 C=vec2(1./6.,1./3.); const vec4 D=vec4(0.,.5,1.,2.); 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.-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=permute(permute(permute(i.z+vec4(0.,i1.z,i2.z,1.))+i.y+vec4(0.,i1.y,i2.y,1.))+i.x+vec4(0.,i1.x,i2.x,1.)); float n_=1./7.; vec3 ns=n_*D.wyz-D.xzx; vec4 j=p-49.*floor(p*ns.z*ns.z); vec4 x_=floor(j*ns.z); vec4 y_=floor(j-7.*x_); vec4 x=x_*ns.x+ns.yyyy; vec4 y=y_*ns.x+ns.yyyy; vec4 h=1.-abs(x)-abs(y); vec4 b0=vec4(x.xy,y.xy); vec4 b1=vec4(x.zw,y.zw); vec4 s0=floor(b0)*2.+1.; vec4 s1=floor(b1)*2.+1.; vec4 sh=-step(h,vec4(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=inversesqrt(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(.6-vec4(dot(x0,x0),dot(x1,x1),dot(x2,x2),dot(x3,x3)),0.); m*=m; return 42.*dot(m*m,vec4(dot(p0,x0),dot(p1,x1),dot(p2,x2),dot(p3,x3))); }`; const PERSIST_FRAG = ` uniform sampler2D sampler; uniform float time; uniform vec2 mousePos; uniform float noiseFactor,noiseScale,rgbPersistFactor,alphaPersistFactor; varying vec2 v_uv; ${SIMPLEX} void main(){ float a=snoise3(vec3(v_uv*noiseFactor,time*.1))*noiseScale; float b=snoise3(vec3(v_uv*noiseFactor,time*.1+100.))*noiseScale; vec4 t=texture2D(sampler,v_uv+vec2(a,b)+mousePos*.005); gl_FragColor=vec4(t.xyz*rgbPersistFactor,alphaPersistFactor); }`; const TEXT_FRAG = ` uniform sampler2D sampler;uniform vec3 color;varying vec2 v_uv; void main(){ vec4 t=texture2D(sampler,v_uv); float alpha=smoothstep(0.1,0.9,t.a); if(alpha<0.01)discard; gl_FragColor=vec4(color,alpha); }`; const TextTrail = ({ text = "Vibe", fontFamily = "Figtree", fontWeight = "900", noiseFactor = 1, noiseScale = 0.0005, rgbPersistFactor = 0.98, alphaPersistFactor = 0.95, animateColor = false, startColor = "#ffffff", textColor = "#ffffff", backgroundColor = 0x271e37, colorCycleInterval = 3000, supersample = 2, }) => { const ref = useRef(null); const persistColor = useRef(hexToRgb(textColor || startColor).map((c) => c / 255)); const targetColor = useRef([...persistColor.current]); useEffect(() => { if (!ref.current) return; const size = () => ({ w: ref.current.clientWidth, h: ref.current.clientHeight, }); let { w, h } = size(); const renderer = new WebGLRenderer({ antialias: true }); renderer.setClearColor(new Color(backgroundColor), 1); renderer.setPixelRatio(window.devicePixelRatio || 1); renderer.setSize(w, h); ref.current.appendChild(renderer.domElement); const scene = new Scene(); const fluidScene = new Scene(); const clock = new Clock(); const cam = new OrthographicCamera(-w / 2, w / 2, h / 2, -h / 2, 0.1, 10); cam.position.z = 1; let rt0 = new WebGLRenderTarget(w, h); let rt1 = rt0.clone(); const quadMat = new ShaderMaterial({ uniforms: { sampler: { value: null }, time: { value: 0 }, mousePos: { value: new Vector2(-1, 1) }, noiseFactor: { value: noiseFactor }, noiseScale: { value: noiseScale }, rgbPersistFactor: { value: rgbPersistFactor }, alphaPersistFactor: { value: alphaPersistFactor }, }, vertexShader: BASE_VERT, fragmentShader: PERSIST_FRAG, transparent: true, }); const quad = new Mesh(new PlaneGeometry(w, h), quadMat); fluidScene.add(quad); const labelMat = new ShaderMaterial({ uniforms: { sampler: { value: null }, color: { value: new Vector3(...persistColor.current) }, }, vertexShader: BASE_VERT, fragmentShader: TEXT_FRAG, transparent: true, }); const label = new Mesh( new PlaneGeometry(Math.min(w, h), Math.min(w, h)), labelMat ); scene.add(label); const texCanvas = document.createElement("canvas"); const ctx = texCanvas.getContext("2d", { alpha: true, colorSpace: "srgb", }); const drawText = () => { const max = Math.min(renderer.capabilities.maxTextureSize, 4096); const pixelRatio = (window.devicePixelRatio || 1) * supersample; const canvasSize = max * pixelRatio; texCanvas.width = canvasSize; texCanvas.height = canvasSize; texCanvas.style.width = `${max}px`; texCanvas.style.height = `${max}px`; ctx.setTransform(1, 0, 0, 1, 0, 0); ctx.scale(pixelRatio, pixelRatio); ctx.clearRect(0, 0, max, max); ctx.imageSmoothingEnabled = true; ctx.imageSmoothingQuality = "high"; ctx.shadowColor = "rgba(255,255,255,0.3)"; ctx.shadowBlur = 2; ctx.fillStyle = "#fff"; ctx.textAlign = "center"; ctx.textBaseline = "middle"; const refSize = 250; ctx.font = `${fontWeight} ${refSize}px ${fontFamily}`; const width = ctx.measureText(text).width; ctx.font = `${fontWeight} ${(refSize * max) / width}px ${fontFamily}`; const cx = max / 2, cy = max / 2; const offs = [ [0, 0], [0.1, 0], [-0.1, 0], [0, 0.1], [0, -0.1], [0.1, 0.1], [-0.1, -0.1], [0.1, -0.1], [-0.1, 0.1], ]; ctx.globalAlpha = 1 / offs.length; offs.forEach(([dx, dy]) => ctx.fillText(text, cx + dx, cy + dy)); ctx.globalAlpha = 1; const tex = new CanvasTexture(texCanvas); tex.generateMipmaps = true; tex.minFilter = LinearMipmapLinearFilter; tex.magFilter = LinearFilter; labelMat.uniforms.sampler.value = tex; }; loadFont(fontFamily).finally(drawText); const mouse = [0, 0], target = [0, 0]; const onMove = (e) => { const r = ref.current.getBoundingClientRect(); target[0] = ((e.clientX - r.left) / r.width) * 2 - 1; target[1] = ((r.top + r.height - e.clientY) / r.height) * 2 - 1; }; ref.current.addEventListener("pointermove", onMove); const ro = new ResizeObserver(() => { ({ w, h } = size()); renderer.setSize(w, h); cam.left = -w / 2; cam.right = w / 2; cam.top = h / 2; cam.bottom = -h / 2; cam.updateProjectionMatrix(); quad.geometry.dispose(); quad.geometry = new PlaneGeometry(w, h); rt0.setSize(w, h); rt1.setSize(w, h); label.geometry.dispose(); label.geometry = new PlaneGeometry(Math.min(w, h), Math.min(w, h)); }); ro.observe(ref.current); const timer = setInterval(() => { if (!textColor) { targetColor.current = [Math.random(), Math.random(), Math.random()]; } }, colorCycleInterval); renderer.setAnimationLoop(() => { const dt = clock.getDelta(); if (animateColor && !textColor) { for (let i = 0; i < 3; i++) persistColor.current[i] += (targetColor.current[i] - persistColor.current[i]) * dt; } const speed = dt * 5; mouse[0] += (target[0] - mouse[0]) * speed; mouse[1] += (target[1] - mouse[1]) * speed; quadMat.uniforms.mousePos.value.set(mouse[0], mouse[1]); quadMat.uniforms.sampler.value = rt1.texture; quadMat.uniforms.time.value = clock.getElapsedTime(); labelMat.uniforms.color.value.set(...persistColor.current); renderer.autoClearColor = false; renderer.setRenderTarget(rt0); renderer.clearColor(); renderer.render(fluidScene, cam); renderer.render(scene, cam); renderer.setRenderTarget(null); renderer.render(fluidScene, cam); renderer.render(scene, cam); [rt0, rt1] = [rt1, rt0]; }); return () => { renderer.setAnimationLoop(null); clearInterval(timer); ref.current?.removeEventListener("pointermove", onMove); ro.disconnect(); // eslint-disable-next-line react-hooks/exhaustive-deps ref.current?.removeChild(renderer.domElement); renderer.dispose(); rt0.dispose(); rt1.dispose(); quadMat.dispose(); quad.geometry.dispose(); labelMat.dispose(); label.geometry.dispose(); }; }, [ text, fontFamily, fontWeight, noiseFactor, noiseScale, rgbPersistFactor, alphaPersistFactor, animateColor, startColor, textColor, backgroundColor, colorCycleInterval, supersample, ]); return <div ref={ref} className="w-full h-full" />; }; export default TextTrail;