Files
vue-bits/public/r/MetaBalls.json
David Haz e621971723 jsrepo v3
2025-12-15 23:50:24 +02:00

1 line
8.7 KiB
JSON

{"name":"MetaBalls","title":"MetaBalls","description":"Liquid metaball blobs that merge and separate with smooth implicit surface animation.","type":"registry:component","add":"when-added","files":[{"type":"registry:component","role":"file","content":"<script setup lang=\"ts\">\nimport { Camera, Mesh, Program, Renderer, Transform, Triangle, Vec3 } from 'ogl';\nimport { onMounted, onUnmounted, useTemplateRef, watch } from 'vue';\n\ninterface MetaBallsProps {\n color?: string;\n speed?: number;\n enableMouseInteraction?: boolean;\n hoverSmoothness?: number;\n animationSize?: number;\n ballCount?: number;\n clumpFactor?: number;\n cursorBallSize?: number;\n cursorBallColor?: string;\n enableTransparency?: boolean;\n mixBlendMode?: string;\n}\n\ntype BallParams = {\n st: number;\n dtFactor: number;\n baseScale: number;\n toggle: number;\n radius: number;\n};\n\nconst props = withDefaults(defineProps<MetaBallsProps>(), {\n color: '#27FF64',\n speed: 0.3,\n enableMouseInteraction: true,\n hoverSmoothness: 0.05,\n animationSize: 30,\n ballCount: 15,\n clumpFactor: 1,\n cursorBallSize: 3,\n cursorBallColor: '#27FF64',\n enableTransparency: false,\n mixBlendMode: 'normal'\n});\n\nfunction parseHexColor(hex: string): [number, number, number] {\n const c = hex.replace('#', '');\n return [parseInt(c.slice(0, 2), 16) / 255, parseInt(c.slice(2, 4), 16) / 255, parseInt(c.slice(4, 6), 16) / 255];\n}\n\nfunction fract(x: number) {\n return x - Math.floor(x);\n}\n\nfunction hash31(p: number): number[] {\n const r = [p * 0.1031, p * 0.103, p * 0.0973].map(fract);\n const r_yzx = [r[1], r[2], r[0]];\n const dotVal = r[0] * (r_yzx[0] + 33.33) + r[1] * (r_yzx[1] + 33.33) + r[2] * (r_yzx[2] + 33.33);\n return r.map(val => fract(val + dotVal));\n}\n\nfunction hash33(v: number[]): number[] {\n const p = [v[0] * 0.1031, v[1] * 0.103, v[2] * 0.0973].map(fract);\n const dotVal = p[0] * (p[1] + 33.33) + p[1] * (p[2] + 33.33) + p[2] * (p[0] + 33.33);\n const r = p.map(val => fract(val + dotVal));\n return r.map((_, i) => fract((r[i % 3] + r[(i + 1) % 3]) * r[(i + 2) % 3]));\n}\n\nconst vertex = `#version 300 es\nprecision highp float;\nlayout(location = 0) in vec2 position;\nvoid main() {\n gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\nconst fragment = `#version 300 es\nprecision highp float;\nuniform vec3 iResolution;\nuniform float iTime;\nuniform vec3 iMouse;\nuniform vec3 iColor;\nuniform vec3 iCursorColor;\nuniform float iAnimationSize;\nuniform int iBallCount;\nuniform float iCursorBallSize;\nuniform vec3 iMetaBalls[50];\nuniform float iClumpFactor;\nuniform bool enableTransparency;\nout vec4 outColor;\n\nfloat getMetaBallValue(vec2 c, float r, vec2 p) {\n vec2 d = p - c;\n float dist2 = dot(d, d);\n return (r * r) / dist2;\n}\n\nvoid main() {\n vec2 fc = gl_FragCoord.xy;\n float scale = iAnimationSize / iResolution.y;\n vec2 coord = (fc - iResolution.xy * 0.5) * scale;\n vec2 mouseW = (iMouse.xy - iResolution.xy * 0.5) * scale;\n\n float m1 = 0.0;\n for (int i = 0; i < 50; i++) {\n if (i >= iBallCount) break;\n m1 += getMetaBallValue(iMetaBalls[i].xy, iMetaBalls[i].z, coord);\n }\n\n float m2 = getMetaBallValue(mouseW, iCursorBallSize, coord);\n float total = m1 + m2;\n\n float f = smoothstep(-1.0, 1.0, (total - 1.3) / min(1.0, fwidth(total)));\n\n vec3 cFinal = vec3(0.0);\n if (total > 0.0) {\n float alpha1 = m1 / total;\n float alpha2 = m2 / total;\n cFinal = iColor * alpha1 + iCursorColor * alpha2;\n }\n\n outColor = vec4(cFinal * f, enableTransparency ? f : 1.0);\n}\n`;\n\nconst containerRef = useTemplateRef<HTMLDivElement>('containerRef');\nlet cleanUpAnimation: () => void = () => {};\n\nconst setupAnimation = () => {\n const container = containerRef.value;\n if (!container) return;\n\n const dpr = 1;\n const renderer = new Renderer({ dpr, alpha: true, premultipliedAlpha: false });\n const gl = renderer.gl;\n gl.clearColor(0, 0, 0, props.enableTransparency ? 0 : 1);\n container.appendChild(gl.canvas);\n\n const camera = new Camera(gl, {\n left: -1,\n right: 1,\n top: 1,\n bottom: -1,\n near: 0.1,\n far: 10\n });\n camera.position.z = 1;\n\n const geometry = new Triangle(gl);\n const [r1, g1, b1] = parseHexColor(props.color);\n const [r2, g2, b2] = parseHexColor(props.cursorBallColor);\n\n const metaBallsUniform: Vec3[] = Array.from({ length: 50 }, () => new Vec3());\n const program = new Program(gl, {\n vertex,\n fragment,\n uniforms: {\n iTime: { value: 0 },\n iResolution: { value: new Vec3() },\n iMouse: { value: new Vec3() },\n iColor: { value: new Vec3(r1, g1, b1) },\n iCursorColor: { value: new Vec3(r2, g2, b2) },\n iAnimationSize: { value: props.animationSize },\n iBallCount: { value: props.ballCount },\n iCursorBallSize: { value: props.cursorBallSize },\n iMetaBalls: { value: metaBallsUniform },\n iClumpFactor: { value: props.clumpFactor },\n enableTransparency: { value: props.enableTransparency }\n }\n });\n\n const mesh = new Mesh(gl, { geometry, program });\n mesh.setParent(new Transform());\n\n const effectiveBallCount = Math.min(props.ballCount, 50);\n const ballParams: BallParams[] = Array.from({ length: effectiveBallCount }, (_, i) => {\n const h1 = hash31(i + 1);\n const h2 = hash33(h1);\n return {\n st: h1[0] * 2 * Math.PI,\n dtFactor: 0.1 * Math.PI + h1[1] * (0.3 * Math.PI),\n baseScale: 5.0 + h1[1] * 5.0,\n toggle: Math.floor(h2[0] * 2),\n radius: 0.5 + h2[2] * 1.5\n };\n });\n\n const mouseBallPos = { x: 0, y: 0 };\n let pointerInside = false,\n pointerX = 0,\n pointerY = 0;\n\n function resize() {\n if (!container) return;\n const { clientWidth, clientHeight } = container;\n renderer.setSize(clientWidth * dpr, clientHeight * dpr);\n gl.canvas.style.width = `${clientWidth}px`;\n gl.canvas.style.height = `${clientHeight}px`;\n program.uniforms.iResolution.value.set(gl.canvas.width, gl.canvas.height, 0);\n }\n\n window.addEventListener('resize', resize);\n resize();\n\n container.addEventListener('pointermove', e => {\n if (!props.enableMouseInteraction) return;\n const rect = container.getBoundingClientRect();\n pointerX = ((e.clientX - rect.left) / rect.width) * gl.canvas.width;\n pointerY = (1 - (e.clientY - rect.top) / rect.height) * gl.canvas.height;\n });\n\n container.addEventListener('pointerenter', () => (pointerInside = true));\n container.addEventListener('pointerleave', () => (pointerInside = false));\n\n const startTime = performance.now();\n let animationFrameId: number;\n\n function update(time: number) {\n animationFrameId = requestAnimationFrame(update);\n const elapsed = (time - startTime) * 0.001;\n program.uniforms.iTime.value = elapsed;\n\n for (let i = 0; i < effectiveBallCount; i++) {\n const { st, dtFactor, baseScale, toggle, radius } = ballParams[i];\n const dt = elapsed * props.speed * dtFactor;\n const angle = st + dt;\n const x = Math.cos(angle);\n const y = Math.sin(angle + dt * toggle);\n metaBallsUniform[i].set(x * baseScale * props.clumpFactor, y * baseScale * props.clumpFactor, radius);\n }\n\n const targetX = pointerInside\n ? pointerX\n : gl.canvas.width * 0.5 + Math.cos(elapsed * props.speed) * gl.canvas.width * 0.15;\n const targetY = pointerInside\n ? pointerY\n : gl.canvas.height * 0.5 + Math.sin(elapsed * props.speed) * gl.canvas.height * 0.15;\n\n mouseBallPos.x += (targetX - mouseBallPos.x) * props.hoverSmoothness;\n mouseBallPos.y += (targetY - mouseBallPos.y) * props.hoverSmoothness;\n program.uniforms.iMouse.value.set(mouseBallPos.x, mouseBallPos.y, 0);\n\n renderer.render({ scene: mesh.parent!, camera });\n }\n\n animationFrameId = requestAnimationFrame(update);\n\n cleanUpAnimation = () => {\n cancelAnimationFrame(animationFrameId);\n window.removeEventListener('resize', resize);\n container.removeEventListener('pointermove', () => {});\n container.removeEventListener('pointerenter', () => {});\n container.removeEventListener('pointerleave', () => {});\n if (container.contains(gl.canvas)) container.removeChild(gl.canvas);\n gl.getExtension('WEBGL_lose_context')?.loseContext();\n };\n};\n\nonMounted(setupAnimation);\nonUnmounted(() => cleanUpAnimation?.());\n\nwatch(\n () => props,\n () => {\n cleanUpAnimation();\n setupAnimation();\n },\n { deep: true }\n);\n</script>\n\n<template>\n <div ref=\"containerRef\" class=\"relative w-full h-full\" :style=\"`mix-blend-mode: ${props.mixBlendMode}`\" />\n</template>\n","path":"MetaBalls/MetaBalls.vue","_imports_":[],"registryDependencies":[],"dependencies":[],"devDependencies":[]}],"registryDependencies":[],"dependencies":[{"ecosystem":"js","name":"ogl","version":"^1.0.11"}],"devDependencies":[],"categories":["Animations"]}