mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 22:49:31 -07:00
Added <BlobCursor /> animation
This commit is contained in:
@@ -42,7 +42,8 @@ export const CATEGORIES = [
|
||||
'Metallic Paint',
|
||||
'Click Spark',
|
||||
'Magnet',
|
||||
'Cubes'
|
||||
'Cubes',
|
||||
'Blob Cursor',
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -11,6 +11,7 @@ const animations = {
|
||||
'cubes': () => import('../demo/Animations/CubesDemo.vue'),
|
||||
'count-up': () => import('../demo/Animations/CountUpDemo.vue'),
|
||||
'splash-cursor': () => import('../demo/Animations/SplashCursorDemo.vue'),
|
||||
'blob-cursor': () => import('../demo/Animations/BlobCursorDemo.vue'),
|
||||
};
|
||||
|
||||
const textAnimations = {
|
||||
|
||||
32
src/constants/code/Animations/blobCursorCode.ts
Normal file
32
src/constants/code/Animations/blobCursorCode.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import code from '@/content/Animations/BlobCursor/BlobCursor.vue?raw';
|
||||
import type { CodeObject } from '../../../types/code';
|
||||
|
||||
export const blobCursor: CodeObject = {
|
||||
cli: `npx jsrepo add https://vue-bits.dev/ui/Animations/BlobCursor`,
|
||||
installation: `npm i gsap`,
|
||||
usage: `<template>
|
||||
<BlobCursor
|
||||
blobType="circle"
|
||||
fillColor="#27FF64"
|
||||
:trailCount="3"
|
||||
:sizes="[60, 125, 75]"
|
||||
:innerSizes="[20, 35, 25]"
|
||||
innerColor="rgba(255,255,255,0.8)"
|
||||
:opacities="[0.6, 0.6, 0.6]"
|
||||
shadowColor="rgba(0,0,0,0.75)"
|
||||
:shadow-Blur="5"
|
||||
:shadowOffsetX="10"
|
||||
:shadowOffsetY="10"
|
||||
:filterDtdDeviation="30"
|
||||
:useFilter="true"
|
||||
:fastDuration="0.1"
|
||||
:slowDuration="0.5"
|
||||
:zIndex="100"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import BlobCursor from "./BlobCursor.vue";
|
||||
</script>`,
|
||||
code
|
||||
};
|
||||
141
src/content/Animations/BlobCursor/BlobCursor.vue
Normal file
141
src/content/Animations/BlobCursor/BlobCursor.vue
Normal file
@@ -0,0 +1,141 @@
|
||||
<script setup lang="ts">
|
||||
import gsap from 'gsap';
|
||||
import { onMounted, onUnmounted, ref } from 'vue';
|
||||
|
||||
interface BlobCursorProps {
|
||||
blobType?: 'circle' | 'square';
|
||||
fillColor?: string;
|
||||
trailCount?: number;
|
||||
sizes?: number[];
|
||||
innerSizes?: number[];
|
||||
innerColor?: string;
|
||||
opacities?: number[];
|
||||
shadowColor?: string;
|
||||
shadowBlur?: number;
|
||||
shadowOffsetX?: number;
|
||||
shadowOffsetY?: number;
|
||||
filterId?: string;
|
||||
filterStdDeviation?: number;
|
||||
filterColorMatrixValues?: string;
|
||||
useFilter?: boolean;
|
||||
fastDuration?: number;
|
||||
slowDuration?: number;
|
||||
fastEase?: string;
|
||||
slowEase?: string;
|
||||
zIndex?: number;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<BlobCursorProps>(), {
|
||||
blobType: 'circle',
|
||||
fillColor: '#27FF64',
|
||||
trailCount: 3,
|
||||
sizes: () => [60, 125, 75],
|
||||
innerSizes: () => [20, 35, 25],
|
||||
innerColor: 'rgba(255,255,255,0.8)',
|
||||
opacities: () => [0.6, 0.6, 0.6],
|
||||
shadowColor: 'rgba(0,0,0,0.75)',
|
||||
shadowBlur: 5,
|
||||
shadowOffsetX: 10,
|
||||
shadowOffsetY: 10,
|
||||
filterId: 'blob',
|
||||
filterStdDeviation: 30,
|
||||
filterColorMatrixValues: '1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 35 -10',
|
||||
useFilter: true,
|
||||
fastDuration: 0.1,
|
||||
slowDuration: 0.5,
|
||||
fastEase: 'power3.out',
|
||||
slowEase: 'power1.out',
|
||||
zIndex: 100
|
||||
});
|
||||
|
||||
const containerRef = ref<HTMLDivElement | null>(null);
|
||||
const blobsRef = ref<(HTMLElement | null)[]>([]);
|
||||
|
||||
const updateOffset = () => {
|
||||
if (!containerRef.value) return { left: 0, top: 0 };
|
||||
const rect = containerRef.value.getBoundingClientRect();
|
||||
return { left: rect.left, top: rect.top };
|
||||
};
|
||||
|
||||
const handleMove = (e: MouseEvent | TouchEvent) => {
|
||||
const { left, top } = updateOffset();
|
||||
const x = 'clientX' in e ? e.clientX : e.touches[0].clientX;
|
||||
const y = 'clientY' in e ? e.clientY : e.touches[0].clientY;
|
||||
|
||||
blobsRef.value.forEach((el, i) => {
|
||||
if (!el) return;
|
||||
|
||||
const isLead = i === 0;
|
||||
gsap.to(el, {
|
||||
x: x - left,
|
||||
y: y - top,
|
||||
duration: isLead ? props.fastDuration : props.slowDuration,
|
||||
ease: isLead ? props.fastEase : props.slowEase
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (!updateOffset) return;
|
||||
window.addEventListener('resize', updateOffset);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', updateOffset);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="containerRef"
|
||||
@mousemove="handleMove"
|
||||
@touchmove="handleMove"
|
||||
class="top-0 left-0 relative w-full h-full"
|
||||
:style="{ zIndex: props.zIndex }"
|
||||
>
|
||||
<svg v-if="props.useFilter" class="absolute w-0 h-0">
|
||||
<filter :id="props.filterId">
|
||||
<feGaussianBlur in="SourceGraphic" result="blur" :stdDeviation="props.filterStdDeviation" />
|
||||
<feColorMatrix in="blur" :values="props.filterColorMatrixValues" />
|
||||
</filter>
|
||||
</svg>
|
||||
|
||||
<div
|
||||
class="absolute inset-0 overflow-hidden cursor-default pointer-events-none select-none"
|
||||
:style="{
|
||||
filter: props.useFilter ? `url(#${props.filterId})` : undefined
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-for="(_, i) in props.trailCount"
|
||||
:key="i"
|
||||
:ref="
|
||||
el => {
|
||||
blobsRef[i] = el as HTMLElement | null;
|
||||
}
|
||||
"
|
||||
class="absolute -translate-x-1/2 -translate-y-1/2 will-change-transform transform"
|
||||
:style="{
|
||||
width: `${props.sizes[i]}px`,
|
||||
height: `${props.sizes[i]}px`,
|
||||
borderRadius: props.blobType === 'circle' ? '50%' : '0',
|
||||
backgroundColor: props.fillColor,
|
||||
opacity: props.opacities[i],
|
||||
boxShadow: `${props.shadowOffsetX}px ${props.shadowOffsetY}px ${props.shadowBlur}px 0 ${props.shadowColor}`
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="absolute"
|
||||
:style="{
|
||||
width: `${props.innerSizes[i]}px`,
|
||||
height: `${props.innerSizes[i]}px`,
|
||||
top: `${(props.sizes[i] - props.innerSizes[i]) / 2}px`,
|
||||
left: `${(props.sizes[i] - props.innerSizes[i]) / 2}px`,
|
||||
backgroundColor: props.innerColor,
|
||||
borderRadius: props.blobType === 'circle' ? '50%' : '0'
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
307
src/demo/Animations/BlobCursorDemo.vue
Normal file
307
src/demo/Animations/BlobCursorDemo.vue
Normal file
@@ -0,0 +1,307 @@
|
||||
<template>
|
||||
<TabbedLayout>
|
||||
<template #preview>
|
||||
<div class="relative h-[600px] overflow-hidden demo-container">
|
||||
<BlobCursor
|
||||
:blobType="blobType"
|
||||
:fillColor="fillColor"
|
||||
:trailCount="trailCount"
|
||||
:sizes="sizes"
|
||||
:innerSizes="innerSizes"
|
||||
:innerColor="innerColor"
|
||||
:opacities="opacities"
|
||||
:shadowColor="shadowColor"
|
||||
:shadowBlur="shadowBlur"
|
||||
:shadowOffsetX="shadowOffsetX"
|
||||
:shadowOffsetY="shadowOffsetY"
|
||||
:fastDuration="fastDuration"
|
||||
:slowDuration="slowDuration"
|
||||
:zIndex="zIndex"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Customize>
|
||||
<button
|
||||
@click="
|
||||
() => {
|
||||
blobType = blobType === 'circle' ? 'square' : 'circle';
|
||||
}
|
||||
"
|
||||
class="bg-[#170D27] hover:bg-[#271E37] mb-2 px-3 border border-[#271E37] rounded-[10px] h-8 text-white text-xs transition"
|
||||
>
|
||||
Blob Type:
|
||||
<span class="ml-1 text-gray-400">{{ blobType }}</span>
|
||||
</button>
|
||||
|
||||
<div class="flex flex-col gap-2 mt-2 text-xs">
|
||||
<div class="flex items-center gap-3">
|
||||
<label for="fillColor">Fill Color:</label>
|
||||
<input
|
||||
id="fillColor"
|
||||
type="color"
|
||||
v-model="fillColor"
|
||||
class="bg-transparent rounded-full w-16 h-8 appearance-none cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<label for="innerColor">Inner Color:</label>
|
||||
<input
|
||||
id="innerColor"
|
||||
type="color"
|
||||
v-model="innerColor"
|
||||
class="bg-transparent rounded-full w-16 h-8 appearance-none cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<label for="shadowColor">Shadow Color:</label>
|
||||
<input
|
||||
id="shadowColor"
|
||||
type="color"
|
||||
v-model="shadowColor"
|
||||
class="bg-transparent rounded-full w-16 h-8 appearance-none cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<PreviewSlider
|
||||
title="Trail Count"
|
||||
:min="1"
|
||||
:max="5"
|
||||
:step="1"
|
||||
v-model="trailCount"
|
||||
@onChange="
|
||||
(val: number) => {
|
||||
trailCount = val;
|
||||
const newSizes = Array(val)
|
||||
.fill(0)
|
||||
.map((_, i) => sizes[i] || sizes[sizes.length - 1] || 60);
|
||||
const newInnerSizes = Array(val)
|
||||
.fill(0)
|
||||
.map((_, i) => innerSizes[i] || innerSizes[innerSizes.length - 1] || 20);
|
||||
const newOpacities = Array(val)
|
||||
.fill(0)
|
||||
.map((_, i) => opacities[i] || opacities[opacities.length - 1] || 0.6);
|
||||
sizes = newSizes;
|
||||
innerSizes = newInnerSizes;
|
||||
opacities = newOpacities;
|
||||
}
|
||||
"
|
||||
/>
|
||||
<PreviewSlider
|
||||
title="Lead Blob Size"
|
||||
:min="10"
|
||||
:max="200"
|
||||
:step="1"
|
||||
v-model="sizes[0]"
|
||||
@onChange="
|
||||
(val: number) => {
|
||||
sizes[0] = val;
|
||||
}
|
||||
"
|
||||
:isDisabled="trailCount < 1"
|
||||
/>
|
||||
<PreviewSlider
|
||||
title="Lead Inner Dot Size"
|
||||
:min="1"
|
||||
:max="100"
|
||||
:step="1"
|
||||
v-model="innerSizes[0]"
|
||||
@onChange="
|
||||
(val: number) => {
|
||||
innerSizes[0] = val;
|
||||
}
|
||||
"
|
||||
:isDisabled="trailCount < 1"
|
||||
/>
|
||||
<PreviewSlider
|
||||
title="Lead Blob Opacity"
|
||||
:min="0.1"
|
||||
:max="1"
|
||||
:step="0.05"
|
||||
v-model="opacities[0]"
|
||||
@onChange="
|
||||
(val: number) => {
|
||||
opacities[0] = val;
|
||||
}
|
||||
"
|
||||
:isDisabled="trailCount < 1"
|
||||
/>
|
||||
<PreviewSlider
|
||||
title="Shadow Blur"
|
||||
:min="0"
|
||||
:max="50"
|
||||
:step="1"
|
||||
v-model="shadowBlur"
|
||||
@onChange="
|
||||
(val: number) => {
|
||||
shadowBlur = val;
|
||||
}
|
||||
"
|
||||
/>
|
||||
<PreviewSlider
|
||||
title="Shadow Offset X"
|
||||
:min="-50"
|
||||
:max="50"
|
||||
:step="1"
|
||||
v-model="shadowOffsetX"
|
||||
@onChange="
|
||||
(val: number) => {
|
||||
shadowOffsetX = val;
|
||||
}
|
||||
"
|
||||
/>
|
||||
<PreviewSlider
|
||||
title="Shadow Offset Y"
|
||||
:min="-50"
|
||||
:max="50"
|
||||
:step="1"
|
||||
v-model="shadowOffsetY"
|
||||
@onChange="
|
||||
(val: number) => {
|
||||
shadowOffsetY = val;
|
||||
}
|
||||
"
|
||||
/>
|
||||
<PreviewSlider
|
||||
title="Fast Duration (Lead)"
|
||||
:min="0.01"
|
||||
:max="2"
|
||||
:step="0.01"
|
||||
v-model="fastDuration"
|
||||
@onChange="
|
||||
(val: number) => {
|
||||
fastDuration = val;
|
||||
}
|
||||
"
|
||||
/>
|
||||
<PreviewSlider
|
||||
title="Slow Duration (Trail)"
|
||||
:min="0.01"
|
||||
:max="3"
|
||||
:step="0.01"
|
||||
v-model="slowDuration"
|
||||
@onChange="
|
||||
(val: number) => {
|
||||
slowDuration = val;
|
||||
}
|
||||
"
|
||||
/>
|
||||
<PreviewSlider
|
||||
title="Z-Index"
|
||||
:min="0"
|
||||
:max="1000"
|
||||
:step="10"
|
||||
v-model="zIndex"
|
||||
@onChange="
|
||||
(val: number) => {
|
||||
zIndex = val;
|
||||
}
|
||||
"
|
||||
/>
|
||||
</Customize>
|
||||
|
||||
<p className="demo-extra-info" style="margin-top: 20px">
|
||||
SVG filters are not fully supported on Safari. Performance may vary.
|
||||
</p>
|
||||
|
||||
<PropTable :data="propData" />
|
||||
<Dependencies :dependency-list="['gsap']" />
|
||||
</template>
|
||||
|
||||
<template #code>
|
||||
<CodeExample :code-object="blobCursor" />
|
||||
</template>
|
||||
|
||||
<template #cli>
|
||||
<CliInstallation :command="blobCursor.cli" />
|
||||
</template>
|
||||
</TabbedLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import CliInstallation from '../../components/code/CliInstallation.vue';
|
||||
import CodeExample from '../../components/code/CodeExample.vue';
|
||||
import Dependencies from '../../components/code/Dependencies.vue';
|
||||
import PropTable from '../../components/common/PropTable.vue';
|
||||
import TabbedLayout from '../../components/common/TabbedLayout.vue';
|
||||
import { blobCursor } from '../../constants/code/Animations/blobCursorCode';
|
||||
import Customize from '../../components/common/Customize.vue';
|
||||
import PreviewSlider from '../../components/common/PreviewSlider.vue';
|
||||
import BlobCursor from '../../content/Animations/BlobCursor/BlobCursor.vue';
|
||||
|
||||
const blobType = ref<'circle' | 'square'>('circle');
|
||||
const fillColor = ref<string>('#27FF64');
|
||||
const trailCount = ref<number>(3);
|
||||
const sizes = ref<number[]>([60, 125, 75]);
|
||||
const innerSizes = ref<number[]>([20, 35, 25]);
|
||||
const innerColor = ref<string>('rgba(255,255,255,0.8)');
|
||||
const opacities = ref<number[]>([0.6, 0.6, 0.6]);
|
||||
const shadowColor = ref<string>('rgba(0,0,0,0.75)');
|
||||
const shadowBlur = ref<number>(5);
|
||||
const shadowOffsetX = ref<number>(10);
|
||||
const shadowOffsetY = ref<number>(10);
|
||||
const fastDuration = ref<number>(0.1);
|
||||
const slowDuration = ref<number>(0.5);
|
||||
const zIndex = ref<number>(100);
|
||||
|
||||
const propData = [
|
||||
{ name: 'blobType', type: "'circle' | 'square'", default: "'circle'", description: 'Shape of the blobs.' },
|
||||
{ name: 'fillColor', type: 'string', default: "'#27FF64'", description: 'Background color of each blob.' },
|
||||
{ name: 'trailCount', type: 'number', default: '3', description: 'How many trailing blobs.' },
|
||||
{
|
||||
name: 'sizes',
|
||||
type: 'number[]',
|
||||
default: '[60, 125, 75]',
|
||||
description: 'Sizes (px) of each blob. Length must be ≥ trailCount.'
|
||||
},
|
||||
{
|
||||
name: 'innerSizes',
|
||||
type: 'number[]',
|
||||
default: '[20, 35, 25]',
|
||||
description: 'Sizes (px) of inner dots. Length must be ≥ trailCount.'
|
||||
},
|
||||
{
|
||||
name: 'innerColor',
|
||||
type: 'string',
|
||||
default: "'rgba(255,255,255,0.8)'",
|
||||
description: 'Background color of the inner dot.'
|
||||
},
|
||||
{
|
||||
name: 'opacities',
|
||||
type: 'number[]',
|
||||
default: '[0.6, 0.6, 0.6]',
|
||||
description: 'Opacity of each blob. Length ≥ trailCount.'
|
||||
},
|
||||
{ name: 'shadowColor', type: 'string', default: "'rgba(0,0,0,0.75)'", description: 'Box-shadow color.' },
|
||||
{ name: 'shadowBlur', type: 'number', default: '5', description: 'Box-shadow blur radius (px).' },
|
||||
{ name: 'shadowOffsetX', type: 'number', default: '10', description: 'Box-shadow X offset (px).' },
|
||||
{ name: 'shadowOffsetY', type: 'number', default: '10', description: 'Box-shadow Y offset (px).' },
|
||||
{
|
||||
name: 'filterId',
|
||||
type: 'string',
|
||||
default: "'blob'",
|
||||
description: 'Optional custom filter ID (for multiple instances).'
|
||||
},
|
||||
{
|
||||
name: 'filterStdDeviation',
|
||||
type: 'number',
|
||||
default: '30',
|
||||
description: 'feGaussianBlur stdDeviation for SVG filter.'
|
||||
},
|
||||
{
|
||||
name: 'filterColorMatrixValues',
|
||||
type: 'string',
|
||||
default: "'1 0 0 ...'",
|
||||
description: 'feColorMatrix values for SVG filter.'
|
||||
},
|
||||
{ name: 'useFilter', type: 'boolean', default: 'true', description: 'Enable the SVG filter.' },
|
||||
{ name: 'fastDuration', type: 'number', default: '0.1', description: 'GSAP duration for the lead blob.' },
|
||||
{ name: 'slowDuration', type: 'number', default: '0.5', description: 'GSAP duration for the following blobs.' },
|
||||
{ name: 'fastEase', type: 'string', default: "'power3.out'", description: 'GSAP ease for the lead blob.' },
|
||||
{ name: 'slowEase', type: 'string', default: "'power1.out'", description: 'GSAP ease for the following blobs.' },
|
||||
{ name: 'zIndex', type: 'number', default: '100', description: 'CSS z-index of the whole component.' }
|
||||
];
|
||||
</script>
|
||||
Reference in New Issue
Block a user