mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-04-22 01:54:38 -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">
|
<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>
|
<span class="hero-new-badge">New 🎉</span>
|
||||||
<div class="hero-new-badge-text">
|
<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>
|
<i class="pi-arrow-right pi" style="font-size: 0.8rem"></i>
|
||||||
</div>
|
</div>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
// Highlighted sidebar items
|
// Highlighted sidebar items
|
||||||
export const NEW = [
|
export const NEW = [
|
||||||
'Color Bends',
|
'Color Bends',
|
||||||
'Floating Lines',
|
|
||||||
'Light Pillar',
|
|
||||||
'Antigravity',
|
'Antigravity',
|
||||||
'Reflective Card',
|
|
||||||
'Pixel Snow',
|
|
||||||
'Grainient',
|
'Grainient',
|
||||||
|
'Orbit Images',
|
||||||
|
'Magic Rings',
|
||||||
];
|
];
|
||||||
export const UPDATED = ['Metallic Paint'];
|
export const UPDATED = ['Metallic Paint'];
|
||||||
|
|
||||||
@@ -62,6 +60,7 @@ export const CATEGORIES = [
|
|||||||
'Image Trail',
|
'Image Trail',
|
||||||
'Laser Flow',
|
'Laser Flow',
|
||||||
'Logo Loop',
|
'Logo Loop',
|
||||||
|
'Magic Rings',
|
||||||
'Magnet',
|
'Magnet',
|
||||||
'Magnet Lines',
|
'Magnet Lines',
|
||||||
'Metallic Paint',
|
'Metallic Paint',
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ const animations = {
|
|||||||
'antigravity': () => import('../demo/Animations/AntigravityDemo.vue'),
|
'antigravity': () => import('../demo/Animations/AntigravityDemo.vue'),
|
||||||
'pixel-trail': () => import('../demo/Animations/PixelTrailDemo.vue'),
|
'pixel-trail': () => import('../demo/Animations/PixelTrailDemo.vue'),
|
||||||
'orbit-images': () => import('../demo/Animations/OrbitImagesDemo.vue'),
|
'orbit-images': () => import('../demo/Animations/OrbitImagesDemo.vue'),
|
||||||
|
'magic-rings': () => import('../demo/Animations/MagicRingsDemo.vue'),
|
||||||
};
|
};
|
||||||
|
|
||||||
const textAnimations = {
|
const textAnimations = {
|
||||||
|
|||||||
@@ -222,6 +222,14 @@ export const componentMetadata: ComponentMetadata = {
|
|||||||
docsUrl: 'https://vue-bits.dev/animations/pixel-trail',
|
docsUrl: 'https://vue-bits.dev/animations/pixel-trail',
|
||||||
tags: []
|
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 -------------------------------------------------------------------------------------------------------------------------------
|
//! 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;
|
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