mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-04-21 17:44:39 -06:00
🎉 New <MagicRings /> component
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user