Merge pull request #147 from Utkarsh-Singhal-26/feat/magic-ring-animation

🎉 New <MagicRings /> component
This commit is contained in:
David
2026-03-15 12:30:12 +02:00
committed by GitHub
10 changed files with 728 additions and 7 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -19,10 +19,10 @@
/>
<div class="hero-main-content">
<router-link to="/backgrounds/grainient" class="hero-new-badge-container">
<router-link to="/backgrounds/magic-rings" class="hero-new-badge-container">
<span class="hero-new-badge">New 🎉</span>
<div class="hero-new-badge-text">
<span>Grainient</span>
<span>Magic Rings</span>
<i class="pi-arrow-right pi" style="font-size: 0.8rem"></i>
</div>
</router-link>
+3 -4
View File
@@ -1,12 +1,10 @@
// Highlighted sidebar items
export const NEW = [
'Color Bends',
'Floating Lines',
'Light Pillar',
'Antigravity',
'Reflective Card',
'Pixel Snow',
'Grainient',
'Orbit Images',
'Magic Rings',
];
export const UPDATED = ['Metallic Paint'];
@@ -62,6 +60,7 @@ export const CATEGORIES = [
'Image Trail',
'Laser Flow',
'Logo Loop',
'Magic Rings',
'Magnet',
'Magnet Lines',
'Metallic Paint',
+1
View File
@@ -27,6 +27,7 @@ const animations = {
'antigravity': () => import('../demo/Animations/AntigravityDemo.vue'),
'pixel-trail': () => import('../demo/Animations/PixelTrailDemo.vue'),
'orbit-images': () => import('../demo/Animations/OrbitImagesDemo.vue'),
'magic-rings': () => import('../demo/Animations/MagicRingsDemo.vue'),
};
const textAnimations = {
+8
View File
@@ -222,6 +222,14 @@ export const componentMetadata: ComponentMetadata = {
docsUrl: 'https://vue-bits.dev/animations/pixel-trail',
tags: []
},
'Animations/MagicRings': {
videoUrl: '/assets/videos/magicrings.webm',
description: 'Interactive magic rings effect with customizable parameters.',
category: 'Animations',
name: 'MagicRings',
docsUrl: 'https://vue-bits.dev/animations/magic-rings',
tags: []
},
//! Text Animations -------------------------------------------------------------------------------------------------------------------------------
@@ -0,0 +1,36 @@
import code from '@/content/Animations/MagicRings/MagicRings.vue?raw';
import { createCodeObject } from '@/types/code';
export const magicRings = createCodeObject(code, 'Animations/MagicRings', {
usage: `<template>
<div style="width: 600px; height: 400px; position: relative;">
<MagicRings
color="#7cff67"
colorTwo="#42fcff"
:ringCount="6"
:speed="1"
:attenuation="10"
:lineThickness="2"
:baseRadius="0.35"
:radiusStep="0.1"
:scaleRate="0.1"
:opacity="1"
:blur="0"
:noiseAmount="0.1"
:rotation="0"
:ringGap="1.5"
:fadeIn="0.7"
:fadeOut="0.5"
:followMouse="false"
:mouseInfluence="0.2"
:hoverScale="1.2"
:parallax="0.05"
:clickBurst="false"
/>
</div>
</template>
<script setup>
import MagicRings from './MagicRings.vue'
</script>`
});
@@ -0,0 +1,327 @@
<script setup lang="ts">
import * as THREE from 'three';
import { computed, onBeforeUnmount, onMounted, ref, useTemplateRef } from 'vue';
const vertexShader = `
void main() {
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;
const fragmentShader = `
precision highp float;
uniform float uTime, uAttenuation, uLineThickness;
uniform float uBaseRadius, uRadiusStep, uScaleRate;
uniform float uOpacity, uNoiseAmount, uRotation, uRingGap;
uniform float uFadeIn, uFadeOut;
uniform float uMouseInfluence, uHoverAmount, uHoverScale, uParallax, uBurst;
uniform vec2 uResolution, uMouse;
uniform vec3 uColor, uColorTwo;
uniform int uRingCount;
const float HP = 1.5707963;
const float CYCLE = 3.45;
float fade(float t) {
return t < uFadeIn ? smoothstep(0.0, uFadeIn, t) : 1.0 - smoothstep(uFadeOut, CYCLE - 0.2, t);
}
float ring(vec2 p, float ri, float cut, float t0, float px) {
float t = mod(uTime + t0, CYCLE);
float r = ri + t / CYCLE * uScaleRate;
float d = abs(length(p) - r);
float a = atan(abs(p.y), abs(p.x)) / HP;
float th = max(1.0 - a, 0.5) * px * uLineThickness;
float h = (1.0 - smoothstep(th, th * 1.5, d)) + 1.0;
d += pow(cut * a, 3.0) * r;
return h * exp(-uAttenuation * d) * fade(t);
}
void main() {
float px = 1.0 / min(uResolution.x, uResolution.y);
vec2 p = (gl_FragCoord.xy - 0.5 * uResolution.xy) * px;
float cr = cos(uRotation), sr = sin(uRotation);
p = mat2(cr, -sr, sr, cr) * p;
p -= uMouse * uMouseInfluence;
float sc = mix(1.0, uHoverScale, uHoverAmount) + uBurst * 0.3;
p /= sc;
vec3 c = vec3(0.0);
float rcf = max(float(uRingCount) - 1.0, 1.0);
for (int i = 0; i < 10; i++) {
if (i >= uRingCount) break;
float fi = float(i);
vec2 pr = p - fi * uParallax * uMouse;
vec3 rc = mix(uColor, uColorTwo, fi / rcf);
c = mix(c, rc, vec3(ring(pr, uBaseRadius + fi * uRadiusStep, pow(uRingGap, fi), i == 0 ? 0.0 : 2.95 * fi, px)));
}
c *= 1.0 + uBurst * 2.0;
float n = fract(sin(dot(gl_FragCoord.xy + uTime * 100.0, vec2(12.9898, 78.233))) * 43758.5453);
c += (n - 0.5) * uNoiseAmount;
gl_FragColor = vec4(c, max(c.r, max(c.g, c.b)) * uOpacity);
}
`;
interface MagicRingsProps {
color?: string;
colorTwo?: string;
speed?: number;
ringCount?: number;
attenuation?: number;
lineThickness?: number;
baseRadius?: number;
radiusStep?: number;
scaleRate?: number;
opacity?: number;
blur?: number;
noiseAmount?: number;
rotation?: number;
ringGap?: number;
fadeIn?: number;
fadeOut?: number;
followMouse?: boolean;
mouseInfluence?: number;
hoverScale?: number;
parallax?: number;
clickBurst?: boolean;
}
const props = withDefaults(defineProps<MagicRingsProps>(), {
color: '#7cff67',
colorTwo: '#42fcff',
speed: 1,
ringCount: 6,
attenuation: 10,
lineThickness: 2,
baseRadius: 0.35,
radiusStep: 0.1,
scaleRate: 0.1,
opacity: 1,
blur: 0,
noiseAmount: 0.1,
rotation: 0,
ringGap: 1.5,
fadeIn: 0.7,
fadeOut: 0.5,
followMouse: false,
mouseInfluence: 0.2,
hoverScale: 1.2,
parallax: 0.05,
clickBurst: false
});
const mountRef = useTemplateRef('mountRef');
const mouseRef = ref<[number, number]>([0, 0]);
const smoothMouseRef = ref<[number, number]>([0, 0]);
const hoverAmountRef = ref(0);
const isHoveredRef = ref(false);
const burstRef = ref(0);
const propsRef = computed(() => ({
color: props.color,
colorTwo: props.colorTwo,
speed: props.speed,
ringCount: props.ringCount,
attenuation: props.attenuation,
lineThickness: props.lineThickness,
baseRadius: props.baseRadius,
radiusStep: props.radiusStep,
scaleRate: props.scaleRate,
opacity: props.opacity,
blur: props.blur,
noiseAmount: props.noiseAmount,
rotation: props.rotation,
ringGap: props.ringGap,
fadeIn: props.fadeIn,
fadeOut: props.fadeOut,
followMouse: props.followMouse,
mouseInfluence: props.mouseInfluence,
hoverScale: props.hoverScale,
parallax: props.parallax,
clickBurst: props.clickBurst
}));
let renderer: THREE.WebGLRenderer | null = null;
let frameId = 0;
let ro: ResizeObserver | null = null;
const cleanupFns: (() => void)[] = [];
onMounted(() => {
const mount = mountRef.value;
if (!mount) return;
try {
renderer = new THREE.WebGLRenderer({ alpha: true });
} catch {
return;
}
if (!renderer.capabilities.isWebGL2) {
renderer.dispose();
return;
}
renderer.setClearColor(0x000000, 0);
mount.appendChild(renderer.domElement);
const scene = new THREE.Scene();
const camera = new THREE.OrthographicCamera(-0.5, 0.5, 0.5, -0.5, 0.1, 10);
camera.position.z = 1;
const uniforms = {
uTime: { value: 0 },
uAttenuation: { value: 0 },
uResolution: { value: new THREE.Vector2() },
uColor: { value: new THREE.Color() },
uColorTwo: { value: new THREE.Color() },
uLineThickness: { value: 0 },
uBaseRadius: { value: 0 },
uRadiusStep: { value: 0 },
uScaleRate: { value: 0 },
uRingCount: { value: 0 },
uOpacity: { value: 1 },
uNoiseAmount: { value: 0 },
uRotation: { value: 0 },
uRingGap: { value: 1.6 },
uFadeIn: { value: 0.5 },
uFadeOut: { value: 0.75 },
uMouse: { value: new THREE.Vector2() },
uMouseInfluence: { value: 0 },
uHoverAmount: { value: 0 },
uHoverScale: { value: 1 },
uParallax: { value: 0 },
uBurst: { value: 0 }
};
const material = new THREE.ShaderMaterial({
vertexShader,
fragmentShader,
uniforms,
transparent: true
});
const quad = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), material);
scene.add(quad);
const resize = () => {
const w = mount.clientWidth;
const h = mount.clientHeight;
const dpr = Math.min(window.devicePixelRatio, 2);
renderer!.setSize(w, h);
renderer!.setPixelRatio(dpr);
uniforms.uResolution.value.set(w * dpr, h * dpr);
};
resize();
window.addEventListener('resize', resize);
ro = new ResizeObserver(resize);
ro.observe(mount);
const onMouseMove = (e: MouseEvent) => {
const rect = mount.getBoundingClientRect();
mouseRef.value[0] = (e.clientX - rect.left) / rect.width - 0.5;
mouseRef.value[1] = -((e.clientY - rect.top) / rect.height - 0.5);
};
const onMouseEnter = () => {
isHoveredRef.value = true;
};
const onMouseLeave = () => {
isHoveredRef.value = false;
mouseRef.value = [0, 0];
};
const onClick = () => {
burstRef.value = 1;
};
mount.addEventListener('mousemove', onMouseMove);
mount.addEventListener('mouseenter', onMouseEnter);
mount.addEventListener('mouseleave', onMouseLeave);
mount.addEventListener('click', onClick);
const animate = (t: number) => {
frameId = requestAnimationFrame(animate);
const p = propsRef.value;
smoothMouseRef.value[0] += (mouseRef.value[0] - smoothMouseRef.value[0]) * 0.08;
smoothMouseRef.value[1] += (mouseRef.value[1] - smoothMouseRef.value[1]) * 0.08;
hoverAmountRef.value += ((isHoveredRef.value ? 1 : 0) - hoverAmountRef.value) * 0.08;
burstRef.value *= 0.95;
if (burstRef.value < 0.001) burstRef.value = 0;
uniforms.uTime.value = t * 0.001 * p.speed;
uniforms.uAttenuation.value = p.attenuation;
uniforms.uColor.value.set(p.color);
uniforms.uColorTwo.value.set(p.colorTwo);
uniforms.uLineThickness.value = p.lineThickness;
uniforms.uBaseRadius.value = p.baseRadius;
uniforms.uRadiusStep.value = p.radiusStep;
uniforms.uScaleRate.value = p.scaleRate;
uniforms.uRingCount.value = p.ringCount;
uniforms.uOpacity.value = p.opacity;
uniforms.uNoiseAmount.value = p.noiseAmount;
uniforms.uRotation.value = (p.rotation * Math.PI) / 180;
uniforms.uRingGap.value = p.ringGap;
uniforms.uFadeIn.value = p.fadeIn;
uniforms.uFadeOut.value = p.fadeOut;
uniforms.uMouse.value.set(smoothMouseRef.value[0], smoothMouseRef.value[1]);
uniforms.uMouseInfluence.value = p.followMouse ? p.mouseInfluence : 0;
uniforms.uHoverAmount.value = hoverAmountRef.value;
uniforms.uHoverScale.value = p.hoverScale;
uniforms.uParallax.value = p.parallax;
uniforms.uBurst.value = p.clickBurst ? burstRef.value : 0;
renderer!.render(scene, camera);
};
frameId = requestAnimationFrame(animate);
cleanupFns.push(() => {
cancelAnimationFrame(frameId);
window.removeEventListener('resize', resize);
ro?.disconnect();
mount.removeEventListener('mousemove', onMouseMove);
mount.removeEventListener('mouseenter', onMouseEnter);
mount.removeEventListener('mouseleave', onMouseLeave);
mount.removeEventListener('click', onClick);
mount.removeChild(renderer!.domElement);
renderer?.dispose();
material.dispose();
});
});
onBeforeUnmount(() => {
cleanupFns.forEach(fn => fn());
});
</script>
<template>
<div ref="mountRef" class="w-full h-full" :style="props.blur > 0 ? { filter: `blur(${props.blur}px)` } : undefined" />
</template>
+107
View File
@@ -102,3 +102,110 @@ body {
line-height: 1.25 !important;
}
}
/* MagicRings demo card */
.mr-demo-card {
width: 340px;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0.02));
border: 1px solid #271e37;
border-radius: 24px;
overflow: hidden;
font-family: inherit;
}
.mr-demo-card-visual {
background-color: #17251420;
border: 1px solid #271e37;
position: relative;
margin: 14px;
border-radius: 16px;
overflow: hidden;
height: 220px;
}
.mr-demo-card-icon {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
opacity: 0.85;
stroke: #aeffc5;
}
.mr-demo-card-body {
padding: 10px 20px 20px;
}
.mr-demo-card-title {
font-size: 26px;
font-weight: 700;
color: #e9f8ff;
margin: 0;
}
.mr-demo-card-subtitle {
font-size: 14px;
color: rgba(255, 255, 255, 0.5);
margin-top: 2px;
}
.mr-demo-card-meta {
display: flex;
align-items: center;
gap: 16px;
margin-top: 14px;
font-size: 13px;
color: rgba(255, 255, 255, 0.65);
}
.mr-demo-card-meta span {
display: flex;
align-items: center;
gap: 6px;
}
.mr-demo-card-actions {
display: flex;
align-items: center;
gap: 10px;
margin-top: 18px;
}
.mr-demo-card-cta {
flex: 1;
padding: 12px 0;
background: #aeffc5;
color: black;
border: none;
border-radius: 50px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition:
transform 120ms ease,
filter 120ms ease,
box-shadow 120ms ease;
box-shadow: 0 4px 16px rgba(82, 39, 255, 0.25);
}
.mr-demo-card-cta:hover {
transform: translateY(-1px);
box-shadow: 0 6px 24px rgba(82, 39, 255, 0.35);
}
.mr-demo-card-heart {
width: 44px;
height: 44px;
border-radius: 50%;
border: 1px solid #271e37;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
flex-shrink: 0;
transition: border-color 200ms ease;
}
.mr-demo-card-heart:hover {
border-color: #aeffc5;
}
+242
View File
@@ -0,0 +1,242 @@
<template>
<TabbedLayout>
<template #preview>
<div class="p-0 h-[600px] overflow-hidden demo-container">
<RefreshButton @click="forceRerender" />
<!-- Card example -->
<template v-if="example === 'card'">
<div class="mr-demo-card">
<div class="mr-demo-card-visual">
<MagicRings
:key="key"
:color="color"
:color-two="colorTwo"
:ring-count="ringCount"
:speed="speed"
:attenuation="attenuation"
:line-thickness="lineThickness"
:base-radius="baseRadius"
:radius-step="radiusStep"
:scale-rate="scaleRate"
:opacity="opacity"
:blur="blur"
:noise-amount="noiseAmount"
:rotation="rotation"
:ring-gap="ringGap"
:fade-in="fadeIn"
:fade-out="fadeOut"
:follow-mouse="followMouse"
:mouse-influence="mouseInfluence"
:hover-scale="hoverScale"
:parallax="parallax"
:click-burst="clickBurst"
/>
<svg
class="mr-demo-card-icon"
xmlns="http://www.w3.org/2000/svg"
width="48"
height="48"
viewBox="0 0 24 24"
fill="none"
stroke="#aeffc5"
stroke-width="1"
stroke-linecap="round"
stroke-linejoin="round"
>
<!-- Sparkles icon -->
<path
d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z"
/>
<path d="M20 3v4" />
<path d="M22 5h-4" />
<path d="M4 17v2" />
<path d="M5 18H3" />
</svg>
</div>
<div class="mr-demo-card-body">
<h3 class="mr-demo-card-title">Magic Rings</h3>
<p class="mr-demo-card-subtitle">Interactive WebGL effect</p>
<div class="mr-demo-card-meta">
<span>
<i class="pi pi-github" style="font-size: 12px"></i>
Free & open source
</span>
<span>
<i class="pi pi-google" style="font-size: 12px"></i>
Google
</span>
</div>
<div class="mr-demo-card-actions">
<button class="mr-demo-card-cta">Copy to clipboard</button>
<div class="mr-demo-card-heart">
<i class="pi pi-heart" style="font-size: 16px"></i>
</div>
</div>
</div>
</div>
</template>
<!-- Basic example -->
<template v-else>
<MagicRings
:key="key"
:color="color"
:color-two="colorTwo"
:ring-count="ringCount"
:speed="speed"
:attenuation="attenuation"
:line-thickness="lineThickness"
:base-radius="baseRadius"
:radius-step="radiusStep"
:scale-rate="scaleRate"
:opacity="opacity"
:blur="blur"
:noise-amount="noiseAmount"
:rotation="rotation"
:ring-gap="ringGap"
:fade-in="fadeIn"
:fade-out="fadeOut"
:follow-mouse="followMouse"
:mouse-influence="mouseInfluence"
:hover-scale="hoverScale"
:parallax="parallax"
:click-burst="clickBurst"
/>
</template>
</div>
<Customize>
<PreviewSelect title="Example" v-model="example" :options="exampleOptions" />
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 mt-3">
<PreviewColor title="Color" v-model="color" />
<PreviewColor title="Color Two" v-model="colorTwo" />
<PreviewSlider title="Ring Count" :min="1" :max="10" :step="1" v-model="ringCount" />
<PreviewSlider title="Speed" :min="0" :max="3" :step="0.1" v-model="speed" />
<PreviewSlider title="Attenuation" :min="1" :max="30" :step="0.5" v-model="attenuation" />
<PreviewSlider title="Line Thickness" :min="1" :max="10" :step="0.5" v-model="lineThickness" />
<PreviewSlider title="Base Radius" :min="0.1" :max="0.5" :step="0.01" v-model="baseRadius" />
<PreviewSlider title="Radius Step" :min="0.05" :max="0.3" :step="0.01" v-model="radiusStep" />
<PreviewSlider title="Scale Rate" :min="0" :max="0.2" :step="0.01" v-model="scaleRate" />
<PreviewSlider title="Opacity" :min="0" :max="1" :step="0.05" v-model="opacity" />
<PreviewSlider title="Blur" :min="0" :max="10" :step="0.5" v-model="blur" />
<PreviewSlider title="Noise Amount" :min="0" :max="0.5" :step="0.01" v-model="noiseAmount" />
<PreviewSlider title="Rotation" :min="0" :max="360" :step="1" v-model="rotation" />
<PreviewSlider title="Ring Gap" :min="1" :max="3" :step="0.1" v-model="ringGap" />
<PreviewSlider title="Fade In" :min="0.1" :max="1.5" :step="0.05" v-model="fadeIn" />
<PreviewSlider title="Fade Out" :min="0.5" :max="3" :step="0.05" v-model="fadeOut" />
<PreviewSlider title="Mouse Influence" :min="0" :max="1" :step="0.05" v-model="mouseInfluence" />
<PreviewSlider title="Hover Scale" :min="1" :max="2" :step="0.05" v-model="hoverScale" />
<PreviewSlider title="Parallax" :min="0" :max="0.1" :step="0.005" v-model="parallax" />
<PreviewSwitch title="Follow Mouse" v-model="followMouse" />
<PreviewSwitch title="Click Burst" v-model="clickBurst" />
</div>
</Customize>
<PropTable :data="propData" />
</template>
<template #code>
<CodeExample :code-object="magicRings" />
</template>
<template #cli>
<CliInstallation :command="magicRings.cli" />
</template>
</TabbedLayout>
</template>
<script setup lang="ts">
import CliInstallation from '@/components/code/CliInstallation.vue';
import CodeExample from '@/components/code/CodeExample.vue';
import Customize from '@/components/common/Customize.vue';
import PreviewColor from '@/components/common/PreviewColor.vue';
import PreviewSelect from '@/components/common/PreviewSelect.vue';
import PreviewSlider from '@/components/common/PreviewSlider.vue';
import PreviewSwitch from '@/components/common/PreviewSwitch.vue';
import PropTable from '@/components/common/PropTable.vue';
import RefreshButton from '@/components/common/RefreshButton.vue';
import TabbedLayout from '@/components/common/TabbedLayout.vue';
import { useForceRerender } from '@/composables/useForceRerender';
import { magicRings } from '@/constants/code/Animations/magicRingsCode';
import MagicRings from '@/content/Animations/MagicRings/MagicRings.vue';
import { ref } from 'vue';
const { rerenderKey: key, forceRerender } = useForceRerender();
const example = ref<'basic' | 'card'>('basic');
const color = ref('#7cff67');
const colorTwo = ref('#42fcff');
const ringCount = ref(6);
const speed = ref(1);
const attenuation = ref(10);
const lineThickness = ref(2);
const baseRadius = ref(0.35);
const radiusStep = ref(0.1);
const scaleRate = ref(0.1);
const opacity = ref(1);
const blur = ref(0);
const noiseAmount = ref(0.1);
const rotation = ref(0);
const ringGap = ref(1.5);
const fadeIn = ref(0.7);
const fadeOut = ref(0.5);
const followMouse = ref(false);
const mouseInfluence = ref(0.2);
const hoverScale = ref(1.2);
const parallax = ref(0.05);
const clickBurst = ref(false);
const exampleOptions = [
{ label: 'Basic', value: 'basic' },
{ label: 'Card', value: 'card' }
];
const propData = [
{ name: 'color', type: 'string', default: '"#7cff67"', description: 'Hex color for the rings.' },
{
name: 'colorTwo',
type: 'string',
default: '"#42fcff"',
description: 'Second color — rings interpolate from color to colorTwo.'
},
{ name: 'ringCount', type: 'number', default: '6', description: 'Number of concentric rings to draw (1-10).' },
{ name: 'speed', type: 'number', default: '1', description: 'Animation speed multiplier.' },
{
name: 'attenuation',
type: 'number',
default: '10',
description: 'Glow falloff — higher values produce tighter glow.'
},
{ name: 'lineThickness', type: 'number', default: '2', description: 'Thickness of each ring line.' },
{ name: 'baseRadius', type: 'number', default: '0.35', description: 'Radius of the innermost ring (normalized).' },
{ name: 'radiusStep', type: 'number', default: '0.1', description: 'Spacing between successive rings.' },
{ name: 'scaleRate', type: 'number', default: '0.1', description: 'How much rings expand over time.' },
{ name: 'opacity', type: 'number', default: '1', description: 'Overall opacity of the effect (0-1).' },
{ name: 'blur', type: 'number', default: '0', description: 'CSS blur in px — creates a bloom/glow effect.' },
{ name: 'noiseAmount', type: 'number', default: '0.1', description: 'Film-grain noise intensity.' },
{ name: 'rotation', type: 'number', default: '0', description: 'Static rotation of the pattern in degrees.' },
{ name: 'ringGap', type: 'number', default: '1.5', description: 'Exponential base for angular cutaway per ring.' },
{ name: 'fadeIn', type: 'number', default: '0.7', description: 'Duration of ring fade-in within cycle.' },
{ name: 'fadeOut', type: 'number', default: '0.5', description: 'Start time of ring fade-out within cycle.' },
{ name: 'followMouse', type: 'boolean', default: 'false', description: 'Rings shift toward the mouse cursor.' },
{
name: 'mouseInfluence',
type: 'number',
default: '0.2',
description: 'Strength of mouse follow (when followMouse is true).'
},
{ name: 'hoverScale', type: 'number', default: '1.2', description: 'Scale multiplier on hover.' },
{ name: 'parallax', type: 'number', default: '0.05', description: 'Per-ring depth offset based on mouse position.' },
{
name: 'clickBurst',
type: 'boolean',
default: 'false',
description: 'Click triggers a brightness flash and scale pulse.'
}
];
</script>