Merge pull request #27 from Utkarsh-Singhal-26/feat/liquid-chrome

Added <LiquidChrome /> background
This commit is contained in:
David
2025-07-17 09:48:21 +03:00
committed by GitHub
5 changed files with 332 additions and 0 deletions

View File

@@ -91,6 +91,7 @@ export const CATEGORIES = [
'Grid Motion',
'Orb',
'Ballpit',
'Liquid Chrome',
'Grid Distortion'
]
}

View File

@@ -77,6 +77,7 @@ const backgrounds = {
'balatro': () => import('../demo/Backgrounds/BalatroDemo.vue'),
'orb': () => import('../demo/Backgrounds/OrbDemo.vue'),
'ballpit': () => import('../demo/Backgrounds/BallpitDemo.vue'),
'liquid-chrome': () => import('../demo/Backgrounds/LiquidChromeDemo.vue'),
'grid-distortion': () => import('../demo/Backgrounds/GridDistortionDemo.vue'),
};

View File

@@ -0,0 +1,22 @@
import code from '@content/Backgrounds/LiquidChrome/LiquidChrome.vue?raw';
import type { CodeObject } from '../../../types/code';
export const liquidChrome: CodeObject = {
cli: `npx jsrepo add https://vue-bits.dev/ui/Backgrounds/LiquidChrome`,
installation: `npm i ogl`,
usage: `<template>
<div class="relative w-full h-[600px]">
<LiquidChrome
:baseColor="[0.1, 0.1, 0.1]"
:speed="1"
:amplitude="0.6"
:interactive="true"
/>
</div>
</template>
<script setup lang="ts">
import LiquidChrome from "./LiquidChrome.vue";
</script>`,
code
};

View File

@@ -0,0 +1,199 @@
<script setup lang="ts">
import { onMounted, onUnmounted, ref, watch } from 'vue';
import { Renderer, Program, Mesh, Triangle } from 'ogl';
interface LiquidChromeProps {
baseColor?: number[];
speed?: number;
amplitude?: number;
frequencyX?: number;
frequencyY?: number;
interactive?: boolean;
}
const props = withDefaults(defineProps<LiquidChromeProps>(), {
baseColor: () => [0.1, 0.1, 0.1],
speed: 0.2,
amplitude: 0.5,
frequencyX: 3,
frequencyY: 2,
interactive: true
});
const containerRef = ref<HTMLDivElement | null>(null);
let cleanupAnimation: (() => void) | null = null;
const setupAnimation = () => {
const container = containerRef.value;
if (!container) return;
const renderer = new Renderer({ antialias: true });
const gl = renderer.gl;
gl.clearColor(1, 1, 1, 1);
const vertexShader = `
attribute vec2 position;
attribute vec2 uv;
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = vec4(position, 0.0, 1.0);
}
`;
const fragmentShader = `
precision highp float;
uniform float uTime;
uniform vec3 uResolution;
uniform vec3 uBaseColor;
uniform float uAmplitude;
uniform float uFrequencyX;
uniform float uFrequencyY;
uniform vec2 uMouse;
varying vec2 vUv;
vec4 renderImage(vec2 uvCoord) {
vec2 fragCoord = uvCoord * uResolution.xy;
vec2 uv = (2.0 * fragCoord - uResolution.xy) / min(uResolution.x, uResolution.y);
for (float i = 1.0; i < 10.0; i++){
uv.x += uAmplitude / i * cos(i * uFrequencyX * uv.y + uTime + uMouse.x * 3.14159);
uv.y += uAmplitude / i * cos(i * uFrequencyY * uv.x + uTime + uMouse.y * 3.14159);
}
vec2 diff = (uvCoord - uMouse);
float dist = length(diff);
float falloff = exp(-dist * 20.0);
float ripple = sin(10.0 * dist - uTime * 2.0) * 0.03;
uv += (diff / (dist + 0.0001)) * ripple * falloff;
vec3 color = uBaseColor / abs(sin(uTime - uv.y - uv.x));
return vec4(color, 1.0);
}
void main() {
vec4 col = vec4(0.0);
int samples = 0;
for (int i = -1; i <= 1; i++){
for (int j = -1; j <= 1; j++){
vec2 offset = vec2(float(i), float(j)) * (1.0 / min(uResolution.x, uResolution.y));
col += renderImage(vUv + offset);
samples++;
}
}
gl_FragColor = col / float(samples);
}
`;
const geometry = new Triangle(gl);
const program = new Program(gl, {
vertex: vertexShader,
fragment: fragmentShader,
uniforms: {
uTime: { value: 0 },
uResolution: {
value: new Float32Array([gl.canvas.width, gl.canvas.height, gl.canvas.width / gl.canvas.height])
},
uBaseColor: { value: new Float32Array(props.baseColor) },
uAmplitude: { value: props.amplitude },
uFrequencyX: { value: props.frequencyX },
uFrequencyY: { value: props.frequencyY },
uMouse: { value: new Float32Array([0, 0]) }
}
});
const mesh = new Mesh(gl, { geometry, program });
function resize() {
const scale = 1;
if (!container) return;
renderer.setSize(container.offsetWidth * scale, container.offsetHeight * scale);
const resUniform = program.uniforms.uResolution.value as Float32Array;
resUniform[0] = gl.canvas.width;
resUniform[1] = gl.canvas.height;
resUniform[2] = gl.canvas.width / gl.canvas.height;
}
window.addEventListener('resize', resize);
resize();
function handleMouseMove(event: MouseEvent) {
if (!container) return;
const rect = container.getBoundingClientRect();
const x = (event.clientX - rect.left) / rect.width;
const y = 1 - (event.clientY - rect.top) / rect.height;
const mouseUniform = program.uniforms.uMouse.value as Float32Array;
mouseUniform[0] = x;
mouseUniform[1] = y;
}
function handleTouchMove(event: TouchEvent) {
if (event.touches.length > 0) {
const touch = event.touches[0];
if (!container) return;
const rect = container.getBoundingClientRect();
const x = (touch.clientX - rect.left) / rect.width;
const y = 1 - (touch.clientY - rect.top) / rect.height;
const mouseUniform = program.uniforms.uMouse.value as Float32Array;
mouseUniform[0] = x;
mouseUniform[1] = y;
}
}
if (props.interactive) {
container.addEventListener('mousemove', handleMouseMove);
container.addEventListener('touchmove', handleTouchMove);
}
let animationId: number;
function update(t: number) {
animationId = requestAnimationFrame(update);
program.uniforms.uTime.value = t * 0.001 * props.speed;
renderer.render({ scene: mesh });
}
animationId = requestAnimationFrame(update);
container.appendChild(gl.canvas);
cleanupAnimation = () => {
cancelAnimationFrame(animationId);
window.removeEventListener('resize', resize);
if (props.interactive) {
container.removeEventListener('mousemove', handleMouseMove);
container.removeEventListener('touchmove', handleTouchMove);
}
if (gl.canvas.parentElement) {
gl.canvas.parentElement.removeChild(gl.canvas);
}
gl.getExtension('WEBGL_lose_context')?.loseContext();
};
};
onMounted(() => {
setupAnimation();
});
onUnmounted(() => {
if (cleanupAnimation) {
cleanupAnimation();
cleanupAnimation = null;
}
});
watch(
() => props,
() => {
if (cleanupAnimation) {
cleanupAnimation();
setupAnimation();
}
},
{ deep: true }
);
</script>
<template>
<div ref="containerRef" class="w-full h-full" v-bind="$attrs" />
</template>

View File

@@ -0,0 +1,109 @@
<template>
<TabbedLayout>
<template #preview>
<div class="relative p-0 h-[500px] overflow-hidden demo-container">
<LiquidChrome :baseColor="baseColor" :speed="speed" :amplitude="amplitude" :interactive="interactive" />
</div>
<Customize>
<PreviewSlider :min="0" :max="1" :width="50" :step="0.1" v-model="baseColor[0]" title="Red" />
<PreviewSlider :min="0" :max="1" :width="50" :step="0.1" v-model="baseColor[1]" title="Green" />
<PreviewSlider :min="0" :max="1" :width="50" :step="0.1" v-model="baseColor[2]" title="Blue" />
<PreviewSlider :min="0" title="Speed" :max="5" :step="0.01" v-model="speed" />
<PreviewSlider :min="0.1" title="Amplitude" :max="1" :step="0.01" v-model="amplitude" />
<PreviewSwitch title="Enable Interaction" v-model="interactive" />
</Customize>
<PropTable :data="propData" />
<Dependencies :dependency-list="['ogl']" />
</template>
<template #code>
<CodeExample :code-object="liquidChrome" />
</template>
<template #cli>
<CliInstallation :command="liquidChrome.cli" />
</template>
</TabbedLayout>
</template>
<script setup lang="ts">
import { useForceRerender } from '@/composables/useForceRerender';
import { ref, watch } from 'vue';
import CliInstallation from '../../components/code/CliInstallation.vue';
import CodeExample from '../../components/code/CodeExample.vue';
import Dependencies from '../../components/code/Dependencies.vue';
import Customize from '../../components/common/Customize.vue';
import PreviewSlider from '../../components/common/PreviewSlider.vue';
import PreviewSwitch from '../../components/common/PreviewSwitch.vue';
import PropTable from '../../components/common/PropTable.vue';
import TabbedLayout from '../../components/common/TabbedLayout.vue';
import { liquidChrome } from '../../constants/code/Backgrounds/liquidChromeCode';
import LiquidChrome from '../../content/Backgrounds/LiquidChrome/LiquidChrome.vue';
const { forceRerender } = useForceRerender();
const speed = ref(0.3);
const baseColor = ref([0.1, 0.1, 0.1]);
const interactive = ref(true);
const amplitude = ref(0.3);
watch(
[baseColor, speed, amplitude, interactive],
() => {
forceRerender();
},
{ deep: true }
);
const propData = [
{
name: 'baseColor',
type: 'RGB array (number[3])',
default: '[0.1, 0.1, 0.1]',
description: 'Base color of the component. Specify as an RGB array.'
},
{
name: 'speed',
type: 'number',
default: '1.0',
description: 'Animation speed multiplier.'
},
{
name: 'amplitude',
type: 'number',
default: '0.6',
description: 'Amplitude of the distortion.'
},
{
name: 'frequencyX',
type: 'number',
default: '2.5',
description: 'Frequency modifier for the x distortion.'
},
{
name: 'frequencyY',
type: 'number',
default: '1.5',
description: 'Frequency modifier for the y distortion.'
},
{
name: 'interactive',
type: 'boolean',
default: 'true',
description: 'Enable mouse/touch interaction.'
}
];
</script>
<style scoped>
.demo-container {
padding: 0;
}
</style>