mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 06:29:30 -07:00
1 line
39 KiB
JSON
1 line
39 KiB
JSON
{"name":"LiquidEther","title":"LiquidEther","description":"Interactive liquid shader with flowing distortion and customizable colors.","type":"registry:component","add":"when-added","files":[{"type":"registry:component","role":"file","content":"<template>\n <div ref=\"mountRef\" :class=\"`w-full h-full relative overflow-hidden ${className || ''}`\" :style=\"style\" />\n</template>\n\n<script setup lang=\"ts\">\nimport * as THREE from 'three';\nimport { onMounted, onUnmounted, ref, watch } from 'vue';\n\ninterface LiquidEtherProps {\n mouseForce?: number;\n cursorSize?: number;\n isViscous?: boolean;\n viscous?: number;\n iterationsViscous?: number;\n iterationsPoisson?: number;\n dt?: number;\n BFECC?: boolean;\n resolution?: number;\n isBounce?: boolean;\n colors?: string[];\n style?: Record<string, any>;\n className?: string;\n autoDemo?: boolean;\n autoSpeed?: number;\n autoIntensity?: number;\n takeoverDuration?: number;\n autoResumeDelay?: number;\n autoRampDuration?: number;\n}\n\ninterface SimOptions {\n iterations_poisson: number;\n iterations_viscous: number;\n mouse_force: number;\n resolution: number;\n cursor_size: number;\n viscous: number;\n isBounce: boolean;\n dt: number;\n isViscous: boolean;\n BFECC: boolean;\n}\n\ninterface LiquidEtherWebGL {\n output?: { simulation?: { options: SimOptions; resize: () => void } };\n autoDriver?: {\n enabled: boolean;\n speed: number;\n resumeDelay: number;\n rampDurationMs: number;\n mouse?: { autoIntensity: number; takeoverDuration: number };\n forceStop: () => void;\n };\n resize: () => void;\n start: () => void;\n pause: () => void;\n dispose: () => void;\n}\n\nconst props = withDefaults(defineProps<LiquidEtherProps>(), {\n mouseForce: 20,\n cursorSize: 100,\n isViscous: false,\n viscous: 30,\n iterationsViscous: 32,\n iterationsPoisson: 32,\n dt: 0.014,\n BFECC: true,\n resolution: 0.5,\n isBounce: false,\n colors: () => ['#5227FF', '#FF9FFC', '#B19EEF'],\n style: () => ({}),\n className: '',\n autoDemo: true,\n autoSpeed: 0.5,\n autoIntensity: 2.2,\n takeoverDuration: 0.25,\n autoResumeDelay: 1000,\n autoRampDuration: 0.6\n});\n\nconst mountRef = ref<HTMLDivElement | null>(null);\nconst webglRef = ref<LiquidEtherWebGL | null>(null);\nconst resizeObserverRef = ref<ResizeObserver | null>(null);\nconst rafRef = ref<number | null>(null);\nconst intersectionObserverRef = ref<IntersectionObserver | null>(null);\nconst isVisibleRef = ref<boolean>(true);\nconst resizeRafRef = ref<number | null>(null);\n\nconst initWebGL = () => {\n if (!mountRef.value) return;\n\n function makePaletteTexture(stops: string[]): THREE.DataTexture {\n let arr: string[];\n if (Array.isArray(stops) && stops.length > 0) {\n arr = stops.length === 1 ? [stops[0], stops[0]] : stops;\n } else {\n arr = ['#ffffff', '#ffffff'];\n }\n const w = arr.length;\n const data = new Uint8Array(w * 4);\n for (let i = 0; i < w; i++) {\n const c = new THREE.Color(arr[i]);\n data[i * 4 + 0] = Math.round(c.r * 255);\n data[i * 4 + 1] = Math.round(c.g * 255);\n data[i * 4 + 2] = Math.round(c.b * 255);\n data[i * 4 + 3] = 255;\n }\n const tex = new THREE.DataTexture(data, w, 1, THREE.RGBAFormat);\n tex.magFilter = THREE.LinearFilter;\n tex.minFilter = THREE.LinearFilter;\n tex.wrapS = THREE.ClampToEdgeWrapping;\n tex.wrapT = THREE.ClampToEdgeWrapping;\n tex.generateMipmaps = false;\n tex.needsUpdate = true;\n return tex;\n }\n\n const paletteTex = makePaletteTexture(props.colors);\n const bgVec4 = new THREE.Vector4(0, 0, 0, 0);\n\n class CommonClass {\n width = 0;\n height = 0;\n aspect = 1;\n pixelRatio = 1;\n isMobile = false;\n breakpoint = 768;\n fboWidth: number | null = null;\n fboHeight: number | null = null;\n time = 0;\n delta = 0;\n container: HTMLElement | null = null;\n renderer: THREE.WebGLRenderer | null = null;\n clock: THREE.Clock | null = null;\n\n init(container: HTMLElement) {\n this.container = container;\n this.pixelRatio = Math.min(window.devicePixelRatio || 1, 2);\n this.resize();\n this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });\n this.renderer.autoClear = false;\n this.renderer.setClearColor(new THREE.Color(0x000000), 0);\n this.renderer.setPixelRatio(this.pixelRatio);\n this.renderer.setSize(this.width, this.height);\n const el = this.renderer.domElement;\n el.style.width = '100%';\n el.style.height = '100%';\n el.style.display = 'block';\n this.clock = new THREE.Clock();\n this.clock.start();\n }\n\n resize() {\n if (!this.container) return;\n const rect = this.container.getBoundingClientRect();\n this.width = Math.max(1, Math.floor(rect.width));\n this.height = Math.max(1, Math.floor(rect.height));\n this.aspect = this.width / this.height;\n if (this.renderer) this.renderer.setSize(this.width, this.height, false);\n }\n\n update() {\n if (!this.clock) return;\n this.delta = this.clock.getDelta();\n this.time += this.delta;\n }\n }\n const Common = new CommonClass();\n\n class MouseClass {\n mouseMoved = false;\n coords = new THREE.Vector2();\n coords_old = new THREE.Vector2();\n diff = new THREE.Vector2();\n timer: number | null = null;\n container: HTMLElement | null = null;\n isHoverInside = false;\n hasUserControl = false;\n isAutoActive = false;\n autoIntensity = 2.0;\n takeoverActive = false;\n takeoverStartTime = 0;\n takeoverDuration = 0.25;\n takeoverFrom = new THREE.Vector2();\n takeoverTo = new THREE.Vector2();\n onInteract: (() => void) | null = null;\n private _onMouseMove = this.onDocumentMouseMove.bind(this);\n private _onTouchStart = this.onDocumentTouchStart.bind(this);\n private _onTouchMove = this.onDocumentTouchMove.bind(this);\n private _onMouseEnter = this.onMouseEnter.bind(this);\n private _onMouseLeave = this.onMouseLeave.bind(this);\n private _onTouchEnd = this.onTouchEnd.bind(this);\n\n init(container: HTMLElement) {\n this.container = container;\n container.addEventListener('mousemove', this._onMouseMove);\n container.addEventListener('touchstart', this._onTouchStart, { passive: true });\n container.addEventListener('touchmove', this._onTouchMove, { passive: true });\n container.addEventListener('mouseenter', this._onMouseEnter);\n container.addEventListener('mouseleave', this._onMouseLeave);\n container.addEventListener('touchend', this._onTouchEnd);\n }\n\n dispose() {\n const c = this.container;\n if (!c) return;\n c.removeEventListener('mousemove', this._onMouseMove);\n c.removeEventListener('touchstart', this._onTouchStart);\n c.removeEventListener('touchmove', this._onTouchMove);\n c.removeEventListener('mouseenter', this._onMouseEnter);\n c.removeEventListener('mouseleave', this._onMouseLeave);\n c.removeEventListener('touchend', this._onTouchEnd);\n }\n\n setCoords(x: number, y: number) {\n if (!this.container) return;\n if (this.timer) window.clearTimeout(this.timer);\n const rect = this.container.getBoundingClientRect();\n const nx = (x - rect.left) / rect.width;\n const ny = (y - rect.top) / rect.height;\n this.coords.set(nx * 2 - 1, -(ny * 2 - 1));\n this.mouseMoved = true;\n this.timer = window.setTimeout(() => {\n this.mouseMoved = false;\n }, 100);\n }\n\n setNormalized(nx: number, ny: number) {\n this.coords.set(nx, ny);\n this.mouseMoved = true;\n }\n\n onDocumentMouseMove(event: MouseEvent) {\n if (this.onInteract) this.onInteract();\n if (this.isAutoActive && !this.hasUserControl && !this.takeoverActive) {\n if (!this.container) return;\n const rect = this.container.getBoundingClientRect();\n const nx = (event.clientX - rect.left) / rect.width;\n const ny = (event.clientY - rect.top) / rect.height;\n this.takeoverFrom.copy(this.coords);\n this.takeoverTo.set(nx * 2 - 1, -(ny * 2 - 1));\n this.takeoverStartTime = performance.now();\n this.takeoverActive = true;\n this.hasUserControl = true;\n this.isAutoActive = false;\n return;\n }\n this.setCoords(event.clientX, event.clientY);\n this.hasUserControl = true;\n }\n\n onDocumentTouchStart(event: TouchEvent) {\n if (event.touches.length === 1) {\n const t = event.touches[0];\n if (this.onInteract) this.onInteract();\n this.setCoords(t.pageX, t.pageY);\n this.hasUserControl = true;\n }\n }\n\n onDocumentTouchMove(event: TouchEvent) {\n if (event.touches.length === 1) {\n const t = event.touches[0];\n if (this.onInteract) this.onInteract();\n this.setCoords(t.pageX, t.pageY);\n }\n }\n\n onTouchEnd() {\n this.isHoverInside = false;\n }\n\n onMouseEnter() {\n this.isHoverInside = true;\n }\n\n onMouseLeave() {\n this.isHoverInside = false;\n }\n\n update() {\n if (this.takeoverActive) {\n const t = (performance.now() - this.takeoverStartTime) / (this.takeoverDuration * 1000);\n if (t >= 1) {\n this.takeoverActive = false;\n this.coords.copy(this.takeoverTo);\n this.coords_old.copy(this.coords);\n this.diff.set(0, 0);\n } else {\n const k = t * t * (3 - 2 * t);\n this.coords.copy(this.takeoverFrom).lerp(this.takeoverTo, k);\n }\n }\n this.diff.subVectors(this.coords, this.coords_old);\n this.coords_old.copy(this.coords);\n if (this.coords_old.x === 0 && this.coords_old.y === 0) this.diff.set(0, 0);\n if (this.isAutoActive && !this.takeoverActive) this.diff.multiplyScalar(this.autoIntensity);\n }\n }\n const Mouse = new MouseClass();\n\n class AutoDriver {\n mouse: MouseClass;\n manager: WebGLManager;\n enabled: boolean;\n speed: number;\n resumeDelay: number;\n rampDurationMs: number;\n active = false;\n current = new THREE.Vector2(0, 0);\n target = new THREE.Vector2();\n lastTime = performance.now();\n activationTime = 0;\n margin = 0.2;\n private _tmpDir = new THREE.Vector2();\n\n constructor(\n mouse: MouseClass,\n manager: WebGLManager,\n opts: { enabled: boolean; speed: number; resumeDelay: number; rampDuration: number }\n ) {\n this.mouse = mouse;\n this.manager = manager;\n this.enabled = opts.enabled;\n this.speed = opts.speed;\n this.resumeDelay = opts.resumeDelay || 3000;\n this.rampDurationMs = (opts.rampDuration || 0) * 1000;\n this.pickNewTarget();\n }\n\n pickNewTarget() {\n const r = Math.random;\n this.target.set((r() * 2 - 1) * (1 - this.margin), (r() * 2 - 1) * (1 - this.margin));\n }\n\n forceStop() {\n this.active = false;\n this.mouse.isAutoActive = false;\n }\n\n update() {\n if (!this.enabled) return;\n const now = performance.now();\n const idle = now - this.manager.lastUserInteraction;\n if (idle < this.resumeDelay) {\n if (this.active) this.forceStop();\n return;\n }\n if (this.mouse.isHoverInside) {\n if (this.active) this.forceStop();\n return;\n }\n if (!this.active) {\n this.active = true;\n this.current.copy(this.mouse.coords);\n this.lastTime = now;\n this.activationTime = now;\n }\n if (!this.active) return;\n this.mouse.isAutoActive = true;\n let dtSec = (now - this.lastTime) / 1000;\n this.lastTime = now;\n if (dtSec > 0.2) dtSec = 0.016;\n const dir = this._tmpDir.subVectors(this.target, this.current);\n const dist = dir.length();\n if (dist < 0.01) {\n this.pickNewTarget();\n return;\n }\n dir.normalize();\n let ramp = 1;\n if (this.rampDurationMs > 0) {\n const t = Math.min(1, (now - this.activationTime) / this.rampDurationMs);\n ramp = t * t * (3 - 2 * t);\n }\n const step = this.speed * dtSec * ramp;\n const move = Math.min(step, dist);\n this.current.addScaledVector(dir, move);\n this.mouse.setNormalized(this.current.x, this.current.y);\n }\n }\n\n // Shader code\n const face_vert = `\n attribute vec3 position;\n uniform vec2 px;\n uniform vec2 boundarySpace;\n varying vec2 uv;\n precision highp float;\n void main(){\n vec3 pos = position;\n vec2 scale = 1.0 - boundarySpace * 2.0;\n pos.xy = pos.xy * scale;\n uv = vec2(0.5)+(pos.xy)*0.5;\n gl_Position = vec4(pos, 1.0);\n }\n `;\n const line_vert = `\n attribute vec3 position;\n uniform vec2 px;\n precision highp float;\n varying vec2 uv;\n void main(){\n vec3 pos = position;\n uv = 0.5 + pos.xy * 0.5;\n vec2 n = sign(pos.xy);\n pos.xy = abs(pos.xy) - px * 1.0;\n pos.xy *= n;\n gl_Position = vec4(pos, 1.0);\n }\n `;\n const mouse_vert = `\n precision highp float;\n attribute vec3 position;\n attribute vec2 uv;\n uniform vec2 center;\n uniform vec2 scale;\n uniform vec2 px;\n varying vec2 vUv;\n void main(){\n vec2 pos = position.xy * scale * 2.0 * px + center;\n vUv = uv;\n gl_Position = vec4(pos, 0.0, 1.0);\n }\n `;\n const advection_frag = `\n precision highp float;\n uniform sampler2D velocity;\n uniform float dt;\n uniform bool isBFECC;\n uniform vec2 fboSize;\n uniform vec2 px;\n varying vec2 uv;\n void main(){\n vec2 ratio = max(fboSize.x, fboSize.y) / fboSize;\n if(isBFECC == false){\n vec2 vel = texture2D(velocity, uv).xy;\n vec2 uv2 = uv - vel * dt * ratio;\n vec2 newVel = texture2D(velocity, uv2).xy;\n gl_FragColor = vec4(newVel, 0.0, 0.0);\n } else {\n vec2 spot_new = uv;\n vec2 vel_old = texture2D(velocity, uv).xy;\n vec2 spot_old = spot_new - vel_old * dt * ratio;\n vec2 vel_new1 = texture2D(velocity, spot_old).xy;\n vec2 spot_new2 = spot_old + vel_new1 * dt * ratio;\n vec2 error = spot_new2 - spot_new;\n vec2 spot_new3 = spot_new - error / 2.0;\n vec2 vel_2 = texture2D(velocity, spot_new3).xy;\n vec2 spot_old2 = spot_new3 - vel_2 * dt * ratio;\n vec2 newVel2 = texture2D(velocity, spot_old2).xy; \n gl_FragColor = vec4(newVel2, 0.0, 0.0);\n }\n }\n `;\n const color_frag = `\n precision highp float;\n uniform sampler2D velocity;\n uniform sampler2D palette;\n uniform vec4 bgColor;\n varying vec2 uv;\n void main(){\n vec2 vel = texture2D(velocity, uv).xy;\n float lenv = clamp(length(vel), 0.0, 1.0);\n vec3 c = texture2D(palette, vec2(lenv, 0.5)).rgb;\n vec3 outRGB = mix(bgColor.rgb, c, lenv);\n float outA = mix(bgColor.a, 1.0, lenv);\n gl_FragColor = vec4(outRGB, outA);\n }\n `;\n const divergence_frag = `\n precision highp float;\n uniform sampler2D velocity;\n uniform float dt;\n uniform vec2 px;\n varying vec2 uv;\n void main(){\n float x0 = texture2D(velocity, uv-vec2(px.x, 0.0)).x;\n float x1 = texture2D(velocity, uv+vec2(px.x, 0.0)).x;\n float y0 = texture2D(velocity, uv-vec2(0.0, px.y)).y;\n float y1 = texture2D(velocity, uv+vec2(0.0, px.y)).y;\n float divergence = (x1 - x0 + y1 - y0) / 2.0;\n gl_FragColor = vec4(divergence / dt);\n }\n `;\n const externalForce_frag = `\n precision highp float;\n uniform vec2 force;\n uniform vec2 center;\n uniform vec2 scale;\n uniform vec2 px;\n varying vec2 vUv;\n void main(){\n vec2 circle = (vUv - 0.5) * 2.0;\n float d = 1.0 - min(length(circle), 1.0);\n d *= d;\n gl_FragColor = vec4(force * d, 0.0, 1.0);\n }\n `;\n const poisson_frag = `\n precision highp float;\n uniform sampler2D pressure;\n uniform sampler2D divergence;\n uniform vec2 px;\n varying vec2 uv;\n void main(){\n float p0 = texture2D(pressure, uv + vec2(px.x * 2.0, 0.0)).r;\n float p1 = texture2D(pressure, uv - vec2(px.x * 2.0, 0.0)).r;\n float p2 = texture2D(pressure, uv + vec2(0.0, px.y * 2.0)).r;\n float p3 = texture2D(pressure, uv - vec2(0.0, px.y * 2.0)).r;\n float div = texture2D(divergence, uv).r;\n float newP = (p0 + p1 + p2 + p3) / 4.0 - div;\n gl_FragColor = vec4(newP);\n }\n `;\n const pressure_frag = `\n precision highp float;\n uniform sampler2D pressure;\n uniform sampler2D velocity;\n uniform vec2 px;\n uniform float dt;\n varying vec2 uv;\n void main(){\n float step = 1.0;\n float p0 = texture2D(pressure, uv + vec2(px.x * step, 0.0)).r;\n float p1 = texture2D(pressure, uv - vec2(px.x * step, 0.0)).r;\n float p2 = texture2D(pressure, uv + vec2(0.0, px.y * step)).r;\n float p3 = texture2D(pressure, uv - vec2(0.0, px.y * step)).r;\n vec2 v = texture2D(velocity, uv).xy;\n vec2 gradP = vec2(p0 - p1, p2 - p3) * 0.5;\n v = v - gradP * dt;\n gl_FragColor = vec4(v, 0.0, 1.0);\n }\n `;\n const viscous_frag = `\n precision highp float;\n uniform sampler2D velocity;\n uniform sampler2D velocity_new;\n uniform float v;\n uniform vec2 px;\n uniform float dt;\n varying vec2 uv;\n void main(){\n vec2 old = texture2D(velocity, uv).xy;\n vec2 new0 = texture2D(velocity_new, uv + vec2(px.x * 2.0, 0.0)).xy;\n vec2 new1 = texture2D(velocity_new, uv - vec2(px.x * 2.0, 0.0)).xy;\n vec2 new2 = texture2D(velocity_new, uv + vec2(0.0, px.y * 2.0)).xy;\n vec2 new3 = texture2D(velocity_new, uv - vec2(0.0, px.y * 2.0)).xy;\n vec2 newv = 4.0 * old + v * dt * (new0 + new1 + new2 + new3);\n newv /= 4.0 * (1.0 + v * dt);\n gl_FragColor = vec4(newv, 0.0, 0.0);\n }\n `;\n\n type Uniforms = Record<string, { value: any }>;\n\n class ShaderPass {\n props: any;\n uniforms?: Uniforms;\n scene: THREE.Scene | null = null;\n camera: THREE.Camera | null = null;\n material: THREE.RawShaderMaterial | null = null;\n geometry: THREE.BufferGeometry | null = null;\n plane: THREE.Mesh | null = null;\n\n constructor(props: any) {\n this.props = props || {};\n this.uniforms = this.props.material?.uniforms;\n }\n\n init(..._args: any[]) {\n this.scene = new THREE.Scene();\n this.camera = new THREE.Camera();\n if (this.uniforms) {\n this.material = new THREE.RawShaderMaterial(this.props.material);\n this.geometry = new THREE.PlaneGeometry(2, 2);\n this.plane = new THREE.Mesh(this.geometry, this.material);\n this.scene.add(this.plane);\n }\n }\n\n update(..._args: any[]) {\n if (!Common.renderer || !this.scene || !this.camera) return;\n Common.renderer.setRenderTarget(this.props.output || null);\n Common.renderer.render(this.scene, this.camera);\n Common.renderer.setRenderTarget(null);\n }\n }\n\n // Shader pass classes (Advection, ExternalForce, etc.) - keeping them the same as in React version\n class Advection extends ShaderPass {\n line!: THREE.LineSegments;\n\n constructor(simProps: any) {\n super({\n material: {\n vertexShader: face_vert,\n fragmentShader: advection_frag,\n uniforms: {\n boundarySpace: { value: simProps.cellScale },\n px: { value: simProps.cellScale },\n fboSize: { value: simProps.fboSize },\n velocity: { value: simProps.src.texture },\n dt: { value: simProps.dt },\n isBFECC: { value: true }\n }\n },\n output: simProps.dst\n });\n this.uniforms = this.props.material.uniforms;\n this.init();\n }\n\n init() {\n super.init();\n this.createBoundary();\n }\n\n createBoundary() {\n const boundaryG = new THREE.BufferGeometry();\n const vertices_boundary = new Float32Array([\n -1, -1, 0, -1, 1, 0, -1, 1, 0, 1, 1, 0, 1, 1, 0, 1, -1, 0, 1, -1, 0, -1, -1, 0\n ]);\n boundaryG.setAttribute('position', new THREE.BufferAttribute(vertices_boundary, 3));\n const boundaryM = new THREE.RawShaderMaterial({\n vertexShader: line_vert,\n fragmentShader: advection_frag,\n uniforms: this.uniforms!\n });\n this.line = new THREE.LineSegments(boundaryG, boundaryM);\n this.scene!.add(this.line);\n }\n\n update(...args: any[]) {\n const { dt, isBounce, BFECC } = (args[0] || {}) as { dt?: number; isBounce?: boolean; BFECC?: boolean };\n if (!this.uniforms) return;\n if (typeof dt === 'number') this.uniforms.dt.value = dt;\n if (typeof isBounce === 'boolean') this.line.visible = isBounce;\n if (typeof BFECC === 'boolean') this.uniforms.isBFECC.value = BFECC;\n super.update();\n }\n }\n\n class ExternalForce extends ShaderPass {\n mouse!: THREE.Mesh;\n\n constructor(simProps: any) {\n super({ output: simProps.dst });\n this.init(simProps);\n }\n\n init(simProps: any) {\n super.init();\n const mouseG = new THREE.PlaneGeometry(1, 1);\n const mouseM = new THREE.RawShaderMaterial({\n vertexShader: mouse_vert,\n fragmentShader: externalForce_frag,\n blending: THREE.AdditiveBlending,\n depthWrite: false,\n uniforms: {\n px: { value: simProps.cellScale },\n force: { value: new THREE.Vector2(0, 0) },\n center: { value: new THREE.Vector2(0, 0) },\n scale: { value: new THREE.Vector2(simProps.cursor_size, simProps.cursor_size) }\n }\n });\n this.mouse = new THREE.Mesh(mouseG, mouseM);\n this.scene!.add(this.mouse);\n }\n\n update(...args: any[]) {\n const props = args[0] || {};\n const forceX = (Mouse.diff.x / 2) * (props.mouse_force || 0);\n const forceY = (Mouse.diff.y / 2) * (props.mouse_force || 0);\n const cellScale = props.cellScale || { x: 1, y: 1 };\n const cursorSize = props.cursor_size || 0;\n const cursorSizeX = cursorSize * cellScale.x;\n const cursorSizeY = cursorSize * cellScale.y;\n const centerX = Math.min(\n Math.max(Mouse.coords.x, -1 + cursorSizeX + cellScale.x * 2),\n 1 - cursorSizeX - cellScale.x * 2\n );\n const centerY = Math.min(\n Math.max(Mouse.coords.y, -1 + cursorSizeY + cellScale.y * 2),\n 1 - cursorSizeY - cellScale.y * 2\n );\n const uniforms = (this.mouse.material as THREE.RawShaderMaterial).uniforms;\n uniforms.force.value.set(forceX, forceY);\n uniforms.center.value.set(centerX, centerY);\n uniforms.scale.value.set(cursorSize, cursorSize);\n super.update();\n }\n }\n\n class Viscous extends ShaderPass {\n constructor(simProps: any) {\n super({\n material: {\n vertexShader: face_vert,\n fragmentShader: viscous_frag,\n uniforms: {\n boundarySpace: { value: simProps.boundarySpace },\n velocity: { value: simProps.src.texture },\n velocity_new: { value: simProps.dst_.texture },\n v: { value: simProps.viscous },\n px: { value: simProps.cellScale },\n dt: { value: simProps.dt }\n }\n },\n output: simProps.dst,\n output0: simProps.dst_,\n output1: simProps.dst\n });\n this.init();\n }\n\n update(...args: any[]) {\n const { viscous, iterations, dt } = (args[0] || {}) as { viscous?: number; iterations?: number; dt?: number };\n if (!this.uniforms) return;\n let fbo_in: any, fbo_out: any;\n if (typeof viscous === 'number') this.uniforms.v.value = viscous;\n const iter = iterations ?? 0;\n for (let i = 0; i < iter; i++) {\n if (i % 2 === 0) {\n fbo_in = this.props.output0;\n fbo_out = this.props.output1;\n } else {\n fbo_in = this.props.output1;\n fbo_out = this.props.output0;\n }\n this.uniforms.velocity_new.value = fbo_in.texture;\n this.props.output = fbo_out;\n if (typeof dt === 'number') this.uniforms.dt.value = dt;\n super.update();\n }\n return fbo_out;\n }\n }\n\n class Divergence extends ShaderPass {\n constructor(simProps: any) {\n super({\n material: {\n vertexShader: face_vert,\n fragmentShader: divergence_frag,\n uniforms: {\n boundarySpace: { value: simProps.boundarySpace },\n velocity: { value: simProps.src.texture },\n px: { value: simProps.cellScale },\n dt: { value: simProps.dt }\n }\n },\n output: simProps.dst\n });\n this.init();\n }\n\n update(...args: any[]) {\n const { vel } = (args[0] || {}) as { vel?: any };\n if (this.uniforms && vel) {\n this.uniforms.velocity.value = vel.texture;\n }\n super.update();\n }\n }\n\n class Poisson extends ShaderPass {\n constructor(simProps: any) {\n super({\n material: {\n vertexShader: face_vert,\n fragmentShader: poisson_frag,\n uniforms: {\n boundarySpace: { value: simProps.boundarySpace },\n pressure: { value: simProps.dst_.texture },\n divergence: { value: simProps.src.texture },\n px: { value: simProps.cellScale }\n }\n },\n output: simProps.dst,\n output0: simProps.dst_,\n output1: simProps.dst\n });\n this.init();\n }\n\n update(...args: any[]) {\n const { iterations } = (args[0] || {}) as { iterations?: number };\n let p_in: any, p_out: any;\n const iter = iterations ?? 0;\n for (let i = 0; i < iter; i++) {\n if (i % 2 === 0) {\n p_in = this.props.output0;\n p_out = this.props.output1;\n } else {\n p_in = this.props.output1;\n p_out = this.props.output0;\n }\n if (this.uniforms) this.uniforms.pressure.value = p_in.texture;\n this.props.output = p_out;\n super.update();\n }\n return p_out;\n }\n }\n\n class Pressure extends ShaderPass {\n constructor(simProps: any) {\n super({\n material: {\n vertexShader: face_vert,\n fragmentShader: pressure_frag,\n uniforms: {\n boundarySpace: { value: simProps.boundarySpace },\n pressure: { value: simProps.src_p.texture },\n velocity: { value: simProps.src_v.texture },\n px: { value: simProps.cellScale },\n dt: { value: simProps.dt }\n }\n },\n output: simProps.dst\n });\n this.init();\n }\n\n update(...args: any[]) {\n const { vel, pressure } = (args[0] || {}) as { vel?: any; pressure?: any };\n if (this.uniforms && vel && pressure) {\n this.uniforms.velocity.value = vel.texture;\n this.uniforms.pressure.value = pressure.texture;\n }\n super.update();\n }\n }\n\n class Simulation {\n options: SimOptions;\n fbos: Record<string, THREE.WebGLRenderTarget | null> = {\n vel_0: null,\n vel_1: null,\n vel_viscous0: null,\n vel_viscous1: null,\n div: null,\n pressure_0: null,\n pressure_1: null\n };\n fboSize = new THREE.Vector2();\n cellScale = new THREE.Vector2();\n boundarySpace = new THREE.Vector2();\n advection!: Advection;\n externalForce!: ExternalForce;\n viscous!: Viscous;\n divergence!: Divergence;\n poisson!: Poisson;\n pressure!: Pressure;\n\n constructor(options?: Partial<SimOptions>) {\n this.options = {\n iterations_poisson: 32,\n iterations_viscous: 32,\n mouse_force: 20,\n resolution: 0.5,\n cursor_size: 100,\n viscous: 30,\n isBounce: false,\n dt: 0.014,\n isViscous: false,\n BFECC: true,\n ...options\n };\n this.init();\n }\n\n init() {\n this.calcSize();\n this.createAllFBO();\n this.createShaderPass();\n }\n\n getFloatType() {\n const isIOS = /(iPad|iPhone|iPod)/i.test(navigator.userAgent);\n return isIOS ? THREE.HalfFloatType : THREE.FloatType;\n }\n\n createAllFBO() {\n const type = this.getFloatType();\n const opts = {\n type,\n depthBuffer: false,\n stencilBuffer: false,\n minFilter: THREE.LinearFilter,\n magFilter: THREE.LinearFilter,\n wrapS: THREE.ClampToEdgeWrapping,\n wrapT: THREE.ClampToEdgeWrapping\n } as const;\n for (const key in this.fbos) {\n this.fbos[key] = new THREE.WebGLRenderTarget(this.fboSize.x, this.fboSize.y, opts);\n }\n }\n\n createShaderPass() {\n this.advection = new Advection({\n cellScale: this.cellScale,\n fboSize: this.fboSize,\n dt: this.options.dt,\n src: this.fbos.vel_0,\n dst: this.fbos.vel_1\n });\n this.externalForce = new ExternalForce({\n cellScale: this.cellScale,\n cursor_size: this.options.cursor_size,\n dst: this.fbos.vel_1\n });\n this.viscous = new Viscous({\n cellScale: this.cellScale,\n boundarySpace: this.boundarySpace,\n viscous: this.options.viscous,\n src: this.fbos.vel_1,\n dst: this.fbos.vel_viscous1,\n dst_: this.fbos.vel_viscous0,\n dt: this.options.dt\n });\n this.divergence = new Divergence({\n cellScale: this.cellScale,\n boundarySpace: this.boundarySpace,\n src: this.fbos.vel_viscous0,\n dst: this.fbos.div,\n dt: this.options.dt\n });\n this.poisson = new Poisson({\n cellScale: this.cellScale,\n boundarySpace: this.boundarySpace,\n src: this.fbos.div,\n dst: this.fbos.pressure_1,\n dst_: this.fbos.pressure_0\n });\n this.pressure = new Pressure({\n cellScale: this.cellScale,\n boundarySpace: this.boundarySpace,\n src_p: this.fbos.pressure_0,\n src_v: this.fbos.vel_viscous0,\n dst: this.fbos.vel_0,\n dt: this.options.dt\n });\n }\n\n calcSize() {\n const width = Math.max(1, Math.round(this.options.resolution * Common.width));\n const height = Math.max(1, Math.round(this.options.resolution * Common.height));\n this.cellScale.set(1 / width, 1 / height);\n this.fboSize.set(width, height);\n }\n\n resize() {\n this.calcSize();\n for (const key in this.fbos) {\n this.fbos[key]!.setSize(this.fboSize.x, this.fboSize.y);\n }\n }\n\n update() {\n if (this.options.isBounce) this.boundarySpace.set(0, 0);\n else this.boundarySpace.copy(this.cellScale);\n this.advection.update({ dt: this.options.dt, isBounce: this.options.isBounce, BFECC: this.options.BFECC });\n this.externalForce.update({\n cursor_size: this.options.cursor_size,\n mouse_force: this.options.mouse_force,\n cellScale: this.cellScale\n });\n let vel: any = this.fbos.vel_1;\n if (this.options.isViscous) {\n vel = this.viscous.update({\n viscous: this.options.viscous,\n iterations: this.options.iterations_viscous,\n dt: this.options.dt\n });\n }\n this.divergence.update({ vel });\n const pressure = this.poisson.update({ iterations: this.options.iterations_poisson });\n this.pressure.update({ vel, pressure });\n }\n }\n\n class Output {\n simulation: Simulation;\n scene: THREE.Scene;\n camera: THREE.Camera;\n output: THREE.Mesh;\n\n constructor() {\n this.simulation = new Simulation();\n this.scene = new THREE.Scene();\n this.camera = new THREE.Camera();\n this.output = new THREE.Mesh(\n new THREE.PlaneGeometry(2, 2),\n new THREE.RawShaderMaterial({\n vertexShader: face_vert,\n fragmentShader: color_frag,\n transparent: true,\n depthWrite: false,\n uniforms: {\n velocity: { value: this.simulation.fbos.vel_0!.texture },\n boundarySpace: { value: new THREE.Vector2() },\n palette: { value: paletteTex },\n bgColor: { value: bgVec4 }\n }\n })\n );\n this.scene.add(this.output);\n }\n\n resize() {\n this.simulation.resize();\n }\n\n render() {\n if (!Common.renderer) return;\n Common.renderer.setRenderTarget(null);\n Common.renderer.render(this.scene, this.camera);\n }\n\n update() {\n this.simulation.update();\n this.render();\n }\n }\n\n class WebGLManager implements LiquidEtherWebGL {\n props: any;\n output!: Output;\n autoDriver?: AutoDriver;\n lastUserInteraction = performance.now();\n running = false;\n private _loop = this.loop.bind(this);\n private _resize = this.resize.bind(this);\n private _onVisibility?: () => void;\n\n constructor(props: any) {\n this.props = props;\n Common.init(props.$wrapper);\n Mouse.init(props.$wrapper);\n Mouse.autoIntensity = props.autoIntensity;\n Mouse.takeoverDuration = props.takeoverDuration;\n Mouse.onInteract = () => {\n this.lastUserInteraction = performance.now();\n if (this.autoDriver) this.autoDriver.forceStop();\n };\n this.autoDriver = new AutoDriver(Mouse, this as any, {\n enabled: props.autoDemo,\n speed: props.autoSpeed,\n resumeDelay: props.autoResumeDelay,\n rampDuration: props.autoRampDuration\n });\n this.init();\n window.addEventListener('resize', this._resize);\n this._onVisibility = () => {\n const hidden = document.hidden;\n if (hidden) {\n this.pause();\n } else if (isVisibleRef.value) {\n this.start();\n }\n };\n document.addEventListener('visibilitychange', this._onVisibility);\n }\n\n init() {\n if (!Common.renderer) return;\n this.props.$wrapper.prepend(Common.renderer.domElement);\n this.output = new Output();\n }\n\n resize() {\n Common.resize();\n this.output.resize();\n }\n\n render() {\n if (this.autoDriver) this.autoDriver.update();\n Mouse.update();\n Common.update();\n this.output.update();\n }\n\n loop() {\n if (!this.running) return;\n this.render();\n rafRef.value = requestAnimationFrame(this._loop);\n }\n\n start() {\n if (this.running) return;\n this.running = true;\n this._loop();\n }\n\n pause() {\n this.running = false;\n if (rafRef.value) {\n cancelAnimationFrame(rafRef.value);\n rafRef.value = null;\n }\n }\n\n dispose() {\n try {\n window.removeEventListener('resize', this._resize);\n if (this._onVisibility) document.removeEventListener('visibilitychange', this._onVisibility);\n Mouse.dispose();\n if (Common.renderer) {\n const canvas = Common.renderer.domElement;\n if (canvas && canvas.parentNode) canvas.parentNode.removeChild(canvas);\n Common.renderer.dispose();\n }\n } catch {\n /* noop */\n }\n }\n }\n\n const container = mountRef.value;\n container.style.position = container.style.position || 'relative';\n container.style.overflow = container.style.overflow || 'hidden';\n\n const webgl = new WebGLManager({\n $wrapper: container,\n autoDemo: props.autoDemo,\n autoSpeed: props.autoSpeed,\n autoIntensity: props.autoIntensity,\n takeoverDuration: props.takeoverDuration,\n autoResumeDelay: props.autoResumeDelay,\n autoRampDuration: props.autoRampDuration\n });\n webglRef.value = webgl;\n\n const applyOptionsFromProps = () => {\n if (!webglRef.value) return;\n const sim = webglRef.value.output?.simulation;\n if (!sim) return;\n const prevRes = sim.options.resolution;\n Object.assign(sim.options, {\n mouse_force: props.mouseForce,\n cursor_size: props.cursorSize,\n isViscous: props.isViscous,\n viscous: props.viscous,\n iterations_viscous: props.iterationsViscous,\n iterations_poisson: props.iterationsPoisson,\n dt: props.dt,\n BFECC: props.BFECC,\n resolution: props.resolution,\n isBounce: props.isBounce\n });\n if (props.resolution !== prevRes) sim.resize();\n };\n applyOptionsFromProps();\n webgl.start();\n\n const io = new IntersectionObserver(\n entries => {\n const entry = entries[0];\n const isVisible = entry.isIntersecting && entry.intersectionRatio > 0;\n isVisibleRef.value = isVisible;\n if (!webglRef.value) return;\n if (isVisible && !document.hidden) {\n webglRef.value.start();\n } else {\n webglRef.value.pause();\n }\n },\n { threshold: [0, 0.01, 0.1] }\n );\n io.observe(container);\n intersectionObserverRef.value = io;\n\n const ro = new ResizeObserver(() => {\n if (!webglRef.value) return;\n if (resizeRafRef.value) cancelAnimationFrame(resizeRafRef.value);\n resizeRafRef.value = requestAnimationFrame(() => {\n if (!webglRef.value) return;\n webglRef.value.resize();\n });\n });\n ro.observe(container);\n resizeObserverRef.value = ro;\n};\n\n// Watchers for prop changes\nwatch(\n () => [\n props.mouseForce,\n props.cursorSize,\n props.isViscous,\n props.viscous,\n props.iterationsViscous,\n props.iterationsPoisson,\n props.dt,\n props.BFECC,\n props.resolution,\n props.isBounce,\n props.autoDemo,\n props.autoSpeed,\n props.autoIntensity,\n props.takeoverDuration,\n props.autoResumeDelay,\n props.autoRampDuration\n ],\n () => {\n const webgl = webglRef.value;\n if (!webgl) return;\n const sim = webgl.output?.simulation;\n if (!sim) return;\n const prevRes = sim.options.resolution;\n Object.assign(sim.options, {\n mouse_force: props.mouseForce,\n cursor_size: props.cursorSize,\n isViscous: props.isViscous,\n viscous: props.viscous,\n iterations_viscous: props.iterationsViscous,\n iterations_poisson: props.iterationsPoisson,\n dt: props.dt,\n BFECC: props.BFECC,\n resolution: props.resolution,\n isBounce: props.isBounce\n });\n if (webgl.autoDriver) {\n webgl.autoDriver.enabled = props.autoDemo;\n webgl.autoDriver.speed = props.autoSpeed;\n webgl.autoDriver.resumeDelay = props.autoResumeDelay;\n webgl.autoDriver.rampDurationMs = props.autoRampDuration * 1000;\n if (webgl.autoDriver.mouse) {\n webgl.autoDriver.mouse.autoIntensity = props.autoIntensity;\n webgl.autoDriver.mouse.takeoverDuration = props.takeoverDuration;\n }\n }\n if (props.resolution !== prevRes) sim.resize();\n }\n);\n\nonMounted(() => {\n initWebGL();\n});\n\nonUnmounted(() => {\n if (rafRef.value) cancelAnimationFrame(rafRef.value);\n if (resizeObserverRef.value) {\n try {\n resizeObserverRef.value.disconnect();\n } catch {\n /* noop */\n }\n }\n if (intersectionObserverRef.value) {\n try {\n intersectionObserverRef.value.disconnect();\n } catch {\n /* noop */\n }\n }\n if (webglRef.value) {\n webglRef.value.dispose();\n }\n webglRef.value = null;\n});\n</script>\n","path":"LiquidEther/LiquidEther.vue","_imports_":[],"registryDependencies":[],"dependencies":[],"devDependencies":[]}],"registryDependencies":[],"dependencies":[{"ecosystem":"js","name":"three","version":"^0.178.0"}],"devDependencies":[],"categories":["Backgrounds"]} |