mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 22:49:31 -07:00
Added <LiquidChrome /> background
This commit is contained in:
@@ -90,7 +90,8 @@ export const CATEGORIES = [
|
|||||||
'Threads',
|
'Threads',
|
||||||
'Grid Motion',
|
'Grid Motion',
|
||||||
'Orb',
|
'Orb',
|
||||||
'Ballpit'
|
'Ballpit',
|
||||||
|
'Liquid Chrome',
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ const backgrounds = {
|
|||||||
'balatro': () => import('../demo/Backgrounds/BalatroDemo.vue'),
|
'balatro': () => import('../demo/Backgrounds/BalatroDemo.vue'),
|
||||||
'orb': () => import('../demo/Backgrounds/OrbDemo.vue'),
|
'orb': () => import('../demo/Backgrounds/OrbDemo.vue'),
|
||||||
'ballpit': () => import('../demo/Backgrounds/BallpitDemo.vue'),
|
'ballpit': () => import('../demo/Backgrounds/BallpitDemo.vue'),
|
||||||
|
'liquid-chrome': () => import('../demo/Backgrounds/LiquidChromeDemo.vue'),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const componentMap = {
|
export const componentMap = {
|
||||||
|
|||||||
22
src/constants/code/Backgrounds/liquidChromeCode.ts
Normal file
22
src/constants/code/Backgrounds/liquidChromeCode.ts
Normal 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
|
||||||
|
};
|
||||||
199
src/content/Backgrounds/LiquidChrome/LiquidChrome.vue
Normal file
199
src/content/Backgrounds/LiquidChrome/LiquidChrome.vue
Normal 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>
|
||||||
161
src/demo/Backgrounds/LiquidChromeDemo.vue
Normal file
161
src/demo/Backgrounds/LiquidChromeDemo.vue
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
<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"
|
||||||
|
@onChange="
|
||||||
|
(val: number) => {
|
||||||
|
baseColor[0] = val;
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PreviewSlider
|
||||||
|
:min="0"
|
||||||
|
:max="1"
|
||||||
|
:width="50"
|
||||||
|
:step="0.1"
|
||||||
|
v-model="baseColor[1]"
|
||||||
|
title="Green"
|
||||||
|
@onChange="
|
||||||
|
(val: number) => {
|
||||||
|
baseColor[1] = val;
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PreviewSlider
|
||||||
|
:min="0"
|
||||||
|
:max="1"
|
||||||
|
:width="50"
|
||||||
|
:step="0.1"
|
||||||
|
v-model="baseColor[2]"
|
||||||
|
title="Blue"
|
||||||
|
@onChange="
|
||||||
|
(val: number) => {
|
||||||
|
baseColor[2] = val;
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PreviewSlider
|
||||||
|
:min="0"
|
||||||
|
title="Speed"
|
||||||
|
:max="5"
|
||||||
|
:step="0.01"
|
||||||
|
v-model="speed"
|
||||||
|
@update:model-value="
|
||||||
|
(val: number) => {
|
||||||
|
speed = val;
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PreviewSlider
|
||||||
|
:min="0.1"
|
||||||
|
title="Amplitude"
|
||||||
|
:max="1"
|
||||||
|
:step="0.01"
|
||||||
|
v-model="amplitude"
|
||||||
|
@update:model-value="
|
||||||
|
val => {
|
||||||
|
amplitude = val;
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PreviewSwitch title="Enable Interaction" v-model="interactive" @update:model-value="forceRerender" />
|
||||||
|
</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>
|
||||||
Reference in New Issue
Block a user