mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 22:49:31 -07:00
Migrate <Ribbons />
This commit is contained in:
@@ -32,6 +32,7 @@ export const CATEGORIES = [
|
|||||||
'Animated Content',
|
'Animated Content',
|
||||||
'Fade Content',
|
'Fade Content',
|
||||||
'Pixel Transition',
|
'Pixel Transition',
|
||||||
|
'Ribbons',
|
||||||
'Glare Hover',
|
'Glare Hover',
|
||||||
'Magnet Lines',
|
'Magnet Lines',
|
||||||
'Count Up',
|
'Count Up',
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ const animations = {
|
|||||||
'glare-hover': () => import('../demo/Animations/GlareHoverDemo.vue'),
|
'glare-hover': () => import('../demo/Animations/GlareHoverDemo.vue'),
|
||||||
'magnet-lines': () => import('../demo/Animations/MagnetLinesDemo.vue'),
|
'magnet-lines': () => import('../demo/Animations/MagnetLinesDemo.vue'),
|
||||||
'click-spark': () => import('../demo/Animations/ClickSparkDemo.vue'),
|
'click-spark': () => import('../demo/Animations/ClickSparkDemo.vue'),
|
||||||
|
'ribbons': () => import('../demo/Animations/RibbonsDemo.vue'),
|
||||||
'metallic-paint': () => import('../demo/Animations/MetallicPaintDemo.vue'),
|
'metallic-paint': () => import('../demo/Animations/MetallicPaintDemo.vue'),
|
||||||
'magnet': () => import('../demo/Animations/MagnetDemo.vue'),
|
'magnet': () => import('../demo/Animations/MagnetDemo.vue'),
|
||||||
'cubes': () => import('../demo/Animations/CubesDemo.vue'),
|
'cubes': () => import('../demo/Animations/CubesDemo.vue'),
|
||||||
|
|||||||
28
src/constants/code/Animations/ribbonsCode.ts
Normal file
28
src/constants/code/Animations/ribbonsCode.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import code from '@content/Animations/Ribbons/Ribbons.vue?raw';
|
||||||
|
import type { CodeObject } from '../../../types/code';
|
||||||
|
|
||||||
|
export const ribbons: CodeObject = {
|
||||||
|
cli: `npx jsrepo add https://vue-bits.dev/ui/Animations/Ribbons`,
|
||||||
|
installation: `npm install ogl`,
|
||||||
|
usage: `<template>
|
||||||
|
<Ribbons
|
||||||
|
:colors="['#ff9346', '#7cff67', '#ffee51', '#5227FF']"
|
||||||
|
:base-spring="0.03"
|
||||||
|
:base-friction="0.9"
|
||||||
|
:base-thickness="30"
|
||||||
|
:offset-factor="0.05"
|
||||||
|
:max-age="500"
|
||||||
|
:point-count="50"
|
||||||
|
:speed-multiplier="0.6"
|
||||||
|
:enable-fade="false"
|
||||||
|
:enable-shader-effect="false"
|
||||||
|
:effect-amplitude="2"
|
||||||
|
:background-color="[0, 0, 0, 0]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import Ribbons from "./Ribbons.vue";
|
||||||
|
</script>`,
|
||||||
|
code
|
||||||
|
};
|
||||||
355
src/content/Animations/Ribbons/Ribbons.vue
Normal file
355
src/content/Animations/Ribbons/Ribbons.vue
Normal file
@@ -0,0 +1,355 @@
|
|||||||
|
<template>
|
||||||
|
<div ref="ribbonsContainer" class="relative w-full h-full overflow-hidden" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, onUnmounted, watch } from 'vue';
|
||||||
|
import { Renderer, Transform, Vec3, Color, Polyline } from 'ogl';
|
||||||
|
|
||||||
|
interface RibbonsProps {
|
||||||
|
colors?: string[];
|
||||||
|
baseSpring?: number;
|
||||||
|
baseFriction?: number;
|
||||||
|
baseThickness?: number;
|
||||||
|
offsetFactor?: number;
|
||||||
|
maxAge?: number;
|
||||||
|
pointCount?: number;
|
||||||
|
speedMultiplier?: number;
|
||||||
|
enableFade?: boolean;
|
||||||
|
enableShaderEffect?: boolean;
|
||||||
|
effectAmplitude?: number;
|
||||||
|
backgroundColor?: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<RibbonsProps>(), {
|
||||||
|
colors: () => ['#ff9346', '#7cff67', '#ffee51', '#5227FF'],
|
||||||
|
baseSpring: 0.03,
|
||||||
|
baseFriction: 0.9,
|
||||||
|
baseThickness: 30,
|
||||||
|
offsetFactor: 0.05,
|
||||||
|
maxAge: 500,
|
||||||
|
pointCount: 50,
|
||||||
|
speedMultiplier: 0.6,
|
||||||
|
enableFade: false,
|
||||||
|
enableShaderEffect: false,
|
||||||
|
effectAmplitude: 2,
|
||||||
|
backgroundColor: () => [0, 0, 0, 0]
|
||||||
|
});
|
||||||
|
|
||||||
|
const ribbonsContainer = ref<HTMLDivElement>();
|
||||||
|
|
||||||
|
let renderer: Renderer;
|
||||||
|
let scene: Transform;
|
||||||
|
let lines: {
|
||||||
|
spring: number;
|
||||||
|
friction: number;
|
||||||
|
mouseVelocity: Vec3;
|
||||||
|
mouseOffset: Vec3;
|
||||||
|
points: Vec3[];
|
||||||
|
polyline: Polyline;
|
||||||
|
}[] = [];
|
||||||
|
let frameId: number;
|
||||||
|
let lastTime = performance.now();
|
||||||
|
const mouse = new Vec3();
|
||||||
|
let resizeObserver: ResizeObserver | null = null;
|
||||||
|
|
||||||
|
const vertex = `
|
||||||
|
precision highp float;
|
||||||
|
|
||||||
|
attribute vec3 position;
|
||||||
|
attribute vec3 next;
|
||||||
|
attribute vec3 prev;
|
||||||
|
attribute vec2 uv;
|
||||||
|
attribute float side;
|
||||||
|
|
||||||
|
uniform vec2 uResolution;
|
||||||
|
uniform float uDPR;
|
||||||
|
uniform float uThickness;
|
||||||
|
uniform float uTime;
|
||||||
|
uniform float uEnableShaderEffect;
|
||||||
|
uniform float uEffectAmplitude;
|
||||||
|
|
||||||
|
varying vec2 vUV;
|
||||||
|
|
||||||
|
vec4 getPosition() {
|
||||||
|
vec4 current = vec4(position, 1.0);
|
||||||
|
vec2 aspect = vec2(uResolution.x / uResolution.y, 1.0);
|
||||||
|
vec2 nextScreen = next.xy * aspect;
|
||||||
|
vec2 prevScreen = prev.xy * aspect;
|
||||||
|
vec2 tangent = normalize(nextScreen - prevScreen);
|
||||||
|
vec2 normal = vec2(-tangent.y, tangent.x);
|
||||||
|
normal /= aspect;
|
||||||
|
normal *= mix(1.0, 0.1, pow(abs(uv.y - 0.5) * 2.0, 2.0));
|
||||||
|
float dist = length(nextScreen - prevScreen);
|
||||||
|
normal *= smoothstep(0.0, 0.02, dist);
|
||||||
|
float pixelWidthRatio = 1.0 / (uResolution.y / uDPR);
|
||||||
|
float pixelWidth = current.w * pixelWidthRatio;
|
||||||
|
normal *= pixelWidth * uThickness;
|
||||||
|
current.xy -= normal * side;
|
||||||
|
if(uEnableShaderEffect > 0.5) {
|
||||||
|
current.xy += normal * sin(uTime + current.x * 10.0) * uEffectAmplitude;
|
||||||
|
}
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vUV = uv;
|
||||||
|
gl_Position = getPosition();
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const fragment = `
|
||||||
|
precision highp float;
|
||||||
|
uniform vec3 uColor;
|
||||||
|
uniform float uOpacity;
|
||||||
|
uniform float uEnableFade;
|
||||||
|
varying vec2 vUV;
|
||||||
|
void main() {
|
||||||
|
float fadeFactor = 1.0;
|
||||||
|
if(uEnableFade > 0.5) {
|
||||||
|
fadeFactor = 1.0 - smoothstep(0.0, 1.0, vUV.y);
|
||||||
|
}
|
||||||
|
gl_FragColor = vec4(uColor, uOpacity * fadeFactor);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const updateMouse = (e: MouseEvent | TouchEvent) => {
|
||||||
|
const container = ribbonsContainer.value;
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
let x: number, y: number;
|
||||||
|
const rect = container.getBoundingClientRect();
|
||||||
|
|
||||||
|
if ('changedTouches' in e && e.changedTouches.length) {
|
||||||
|
x = e.changedTouches[0].clientX - rect.left;
|
||||||
|
y = e.changedTouches[0].clientY - rect.top;
|
||||||
|
} else if (e instanceof MouseEvent) {
|
||||||
|
x = e.clientX - rect.left;
|
||||||
|
y = e.clientY - rect.top;
|
||||||
|
} else {
|
||||||
|
x = 0;
|
||||||
|
y = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const width = container.clientWidth;
|
||||||
|
const height = container.clientHeight;
|
||||||
|
mouse.set((x / width) * 2 - 1, (y / height) * -2 + 1, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const resize = () => {
|
||||||
|
const container = ribbonsContainer.value;
|
||||||
|
if (!container || !renderer) return;
|
||||||
|
|
||||||
|
const width = container.clientWidth;
|
||||||
|
const height = container.clientHeight;
|
||||||
|
renderer.setSize(width, height);
|
||||||
|
lines.forEach(line => line.polyline.resize());
|
||||||
|
};
|
||||||
|
|
||||||
|
const createLines = () => {
|
||||||
|
const center = (props.colors.length - 1) / 2;
|
||||||
|
lines = [];
|
||||||
|
|
||||||
|
props.colors.forEach((color, index) => {
|
||||||
|
const spring = props.baseSpring + (Math.random() - 0.5) * 0.05;
|
||||||
|
const friction = props.baseFriction + (Math.random() - 0.5) * 0.05;
|
||||||
|
const thickness = props.baseThickness + (Math.random() - 0.5) * 3;
|
||||||
|
const mouseOffset = new Vec3(
|
||||||
|
(index - center) * props.offsetFactor + (Math.random() - 0.5) * 0.01,
|
||||||
|
(Math.random() - 0.5) * 0.1,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
const line = {
|
||||||
|
spring,
|
||||||
|
friction,
|
||||||
|
mouseVelocity: new Vec3(),
|
||||||
|
mouseOffset,
|
||||||
|
points: [] as Vec3[],
|
||||||
|
polyline: {} as Polyline
|
||||||
|
};
|
||||||
|
|
||||||
|
const count = props.pointCount;
|
||||||
|
const points: Vec3[] = [];
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
points.push(new Vec3());
|
||||||
|
}
|
||||||
|
line.points = points;
|
||||||
|
|
||||||
|
line.polyline = new Polyline(renderer.gl, {
|
||||||
|
points,
|
||||||
|
vertex,
|
||||||
|
fragment,
|
||||||
|
uniforms: {
|
||||||
|
uColor: { value: new Color(color) },
|
||||||
|
uThickness: { value: thickness },
|
||||||
|
uOpacity: { value: 1.0 },
|
||||||
|
uTime: { value: 0.0 },
|
||||||
|
uEnableShaderEffect: { value: props.enableShaderEffect ? 1.0 : 0.0 },
|
||||||
|
uEffectAmplitude: { value: props.effectAmplitude },
|
||||||
|
uEnableFade: { value: props.enableFade ? 1.0 : 0.0 }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
line.polyline.mesh.setParent(scene);
|
||||||
|
lines.push(line);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const update = () => {
|
||||||
|
frameId = requestAnimationFrame(update);
|
||||||
|
const currentTime = performance.now();
|
||||||
|
const dt = currentTime - lastTime;
|
||||||
|
lastTime = currentTime;
|
||||||
|
|
||||||
|
const tmp = new Vec3();
|
||||||
|
lines.forEach(line => {
|
||||||
|
tmp.copy(mouse).add(line.mouseOffset).sub(line.points[0]).multiply(line.spring);
|
||||||
|
line.mouseVelocity.add(tmp).multiply(line.friction);
|
||||||
|
line.points[0].add(line.mouseVelocity);
|
||||||
|
|
||||||
|
for (let i = 1; i < line.points.length; i++) {
|
||||||
|
if (isFinite(props.maxAge) && props.maxAge > 0) {
|
||||||
|
const segmentDelay = props.maxAge / (line.points.length - 1);
|
||||||
|
const alpha = Math.min(1, (dt * props.speedMultiplier) / segmentDelay);
|
||||||
|
line.points[i].lerp(line.points[i - 1], alpha);
|
||||||
|
} else {
|
||||||
|
line.points[i].lerp(line.points[i - 1], 0.9);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (line.polyline.mesh.program.uniforms.uTime) {
|
||||||
|
line.polyline.mesh.program.uniforms.uTime.value = currentTime * 0.001;
|
||||||
|
}
|
||||||
|
line.polyline.updateGeometry();
|
||||||
|
});
|
||||||
|
|
||||||
|
renderer.render({ scene });
|
||||||
|
};
|
||||||
|
|
||||||
|
const initRibbons = () => {
|
||||||
|
const container = ribbonsContainer.value;
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
renderer = new Renderer({ dpr: window.devicePixelRatio || 2, alpha: true });
|
||||||
|
const gl = renderer.gl;
|
||||||
|
|
||||||
|
if (Array.isArray(props.backgroundColor) && props.backgroundColor.length === 4) {
|
||||||
|
gl.clearColor(
|
||||||
|
props.backgroundColor[0],
|
||||||
|
props.backgroundColor[1],
|
||||||
|
props.backgroundColor[2],
|
||||||
|
props.backgroundColor[3]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
gl.clearColor(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
gl.canvas.style.position = 'absolute';
|
||||||
|
gl.canvas.style.top = '0';
|
||||||
|
gl.canvas.style.left = '0';
|
||||||
|
gl.canvas.style.width = '100%';
|
||||||
|
gl.canvas.style.height = '100%';
|
||||||
|
container.appendChild(gl.canvas);
|
||||||
|
|
||||||
|
scene = new Transform();
|
||||||
|
|
||||||
|
createLines();
|
||||||
|
|
||||||
|
container.addEventListener('mousemove', updateMouse);
|
||||||
|
container.addEventListener('touchstart', updateMouse);
|
||||||
|
container.addEventListener('touchmove', updateMouse);
|
||||||
|
|
||||||
|
resize();
|
||||||
|
|
||||||
|
if (typeof ResizeObserver !== 'undefined') {
|
||||||
|
resizeObserver = new ResizeObserver(resize);
|
||||||
|
resizeObserver.observe(container);
|
||||||
|
} else {
|
||||||
|
window.addEventListener('resize', resize);
|
||||||
|
}
|
||||||
|
|
||||||
|
update();
|
||||||
|
};
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
if (frameId) {
|
||||||
|
cancelAnimationFrame(frameId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resizeObserver) {
|
||||||
|
resizeObserver.disconnect();
|
||||||
|
} else {
|
||||||
|
window.removeEventListener('resize', resize);
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = ribbonsContainer.value;
|
||||||
|
if (container) {
|
||||||
|
container.removeEventListener('mousemove', updateMouse);
|
||||||
|
container.removeEventListener('touchstart', updateMouse);
|
||||||
|
container.removeEventListener('touchmove', updateMouse);
|
||||||
|
|
||||||
|
if (renderer && renderer.gl.canvas && renderer.gl.canvas.parentNode === container) {
|
||||||
|
container.removeChild(renderer.gl.canvas);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const recreateLines = () => {
|
||||||
|
lines.forEach(line => {
|
||||||
|
if (line.polyline.mesh && line.polyline.mesh.parent) {
|
||||||
|
line.polyline.mesh.setParent(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
createLines();
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [props.colors, props.pointCount],
|
||||||
|
() => {
|
||||||
|
if (renderer && scene) {
|
||||||
|
recreateLines();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [props.baseThickness, props.enableFade, props.enableShaderEffect, props.effectAmplitude, props.backgroundColor],
|
||||||
|
() => {
|
||||||
|
if (renderer && lines.length > 0) {
|
||||||
|
lines.forEach(line => {
|
||||||
|
if (line.polyline.mesh.program.uniforms.uEnableFade) {
|
||||||
|
line.polyline.mesh.program.uniforms.uEnableFade.value = props.enableFade ? 1.0 : 0.0;
|
||||||
|
}
|
||||||
|
if (line.polyline.mesh.program.uniforms.uEnableShaderEffect) {
|
||||||
|
line.polyline.mesh.program.uniforms.uEnableShaderEffect.value = props.enableShaderEffect ? 1.0 : 0.0;
|
||||||
|
}
|
||||||
|
if (line.polyline.mesh.program.uniforms.uEffectAmplitude) {
|
||||||
|
line.polyline.mesh.program.uniforms.uEffectAmplitude.value = props.effectAmplitude;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const gl = renderer.gl;
|
||||||
|
if (Array.isArray(props.backgroundColor) && props.backgroundColor.length === 4) {
|
||||||
|
gl.clearColor(
|
||||||
|
props.backgroundColor[0],
|
||||||
|
props.backgroundColor[1],
|
||||||
|
props.backgroundColor[2],
|
||||||
|
props.backgroundColor[3]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
gl.clearColor(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
initRibbons();
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
cleanup();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -469,7 +469,7 @@ class CarLights {
|
|||||||
const geometry = new THREE.TubeGeometry(curve, 40, 1, 8, false);
|
const geometry = new THREE.TubeGeometry(curve, 40, 1, 8, false);
|
||||||
|
|
||||||
const instanced = new THREE.InstancedBufferGeometry();
|
const instanced = new THREE.InstancedBufferGeometry();
|
||||||
// Copy geometry attributes
|
|
||||||
for (const key in geometry.attributes) {
|
for (const key in geometry.attributes) {
|
||||||
instanced.setAttribute(key, geometry.attributes[key]);
|
instanced.setAttribute(key, geometry.attributes[key]);
|
||||||
}
|
}
|
||||||
@@ -634,7 +634,7 @@ class LightsSticks {
|
|||||||
const options = this.options;
|
const options = this.options;
|
||||||
const geometry = new THREE.PlaneGeometry(1, 1);
|
const geometry = new THREE.PlaneGeometry(1, 1);
|
||||||
const instanced = new THREE.InstancedBufferGeometry();
|
const instanced = new THREE.InstancedBufferGeometry();
|
||||||
// Copy geometry attributes
|
|
||||||
for (const key in geometry.attributes) {
|
for (const key in geometry.attributes) {
|
||||||
instanced.setAttribute(key, geometry.attributes[key]);
|
instanced.setAttribute(key, geometry.attributes[key]);
|
||||||
}
|
}
|
||||||
@@ -1038,17 +1038,14 @@ class App {
|
|||||||
this.onMouseUp = this.onMouseUp.bind(this);
|
this.onMouseUp = this.onMouseUp.bind(this);
|
||||||
this.onWindowResize = this.onWindowResize.bind(this);
|
this.onWindowResize = this.onWindowResize.bind(this);
|
||||||
|
|
||||||
// Use ResizeObserver for better container tracking
|
|
||||||
if (typeof ResizeObserver !== 'undefined') {
|
if (typeof ResizeObserver !== 'undefined') {
|
||||||
const resizeObserver = new ResizeObserver(() => {
|
const resizeObserver = new ResizeObserver(() => {
|
||||||
this.onWindowResize();
|
this.onWindowResize();
|
||||||
});
|
});
|
||||||
resizeObserver.observe(container);
|
resizeObserver.observe(container);
|
||||||
|
|
||||||
// Store reference for cleanup
|
|
||||||
this.resizeObserver = resizeObserver;
|
this.resizeObserver = resizeObserver;
|
||||||
} else {
|
} else {
|
||||||
// Fallback to window resize
|
|
||||||
window.addEventListener('resize', this.onWindowResize);
|
window.addEventListener('resize', this.onWindowResize);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1220,7 +1217,6 @@ class App {
|
|||||||
tick() {
|
tick() {
|
||||||
if (this.disposed || !this) return;
|
if (this.disposed || !this) return;
|
||||||
|
|
||||||
// Ensure renderer stays within container bounds
|
|
||||||
const containerWidth = this.container.offsetWidth;
|
const containerWidth = this.container.offsetWidth;
|
||||||
const containerHeight = this.container.offsetHeight;
|
const containerHeight = this.container.offsetHeight;
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ const inlineStyles = computed(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const baseClasses = [
|
const baseClasses = [
|
||||||
// Base styling
|
|
||||||
'text-white',
|
'text-white',
|
||||||
'font-black',
|
'font-black',
|
||||||
'whitespace-nowrap',
|
'whitespace-nowrap',
|
||||||
@@ -50,7 +49,6 @@ const baseClasses = [
|
|||||||
'cursor-pointer',
|
'cursor-pointer',
|
||||||
'text-[clamp(2rem,10vw,8rem)]',
|
'text-[clamp(2rem,10vw,8rem)]',
|
||||||
|
|
||||||
// Pseudo-elements base
|
|
||||||
'before:content-[attr(data-text)]',
|
'before:content-[attr(data-text)]',
|
||||||
'before:absolute',
|
'before:absolute',
|
||||||
'before:top-0',
|
'before:top-0',
|
||||||
@@ -69,19 +67,16 @@ const baseClasses = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const normalGlitchClasses = [
|
const normalGlitchClasses = [
|
||||||
// After pseudo-element for normal mode
|
|
||||||
'after:left-[10px]',
|
'after:left-[10px]',
|
||||||
'after:[text-shadow:var(--after-shadow,-10px_0_red)]',
|
'after:[text-shadow:var(--after-shadow,-10px_0_red)]',
|
||||||
'after:[animation:animate-glitch_var(--after-duration,3s)_infinite_linear_alternate-reverse]',
|
'after:[animation:animate-glitch_var(--after-duration,3s)_infinite_linear_alternate-reverse]',
|
||||||
|
|
||||||
// Before pseudo-element for normal mode
|
|
||||||
'before:left-[-10px]',
|
'before:left-[-10px]',
|
||||||
'before:[text-shadow:var(--before-shadow,10px_0_cyan)]',
|
'before:[text-shadow:var(--before-shadow,10px_0_cyan)]',
|
||||||
'before:[animation:animate-glitch_var(--before-duration,2s)_infinite_linear_alternate-reverse]'
|
'before:[animation:animate-glitch_var(--before-duration,2s)_infinite_linear_alternate-reverse]'
|
||||||
];
|
];
|
||||||
|
|
||||||
const hoverOnlyClasses = [
|
const hoverOnlyClasses = [
|
||||||
// Hide pseudo-elements by default
|
|
||||||
'before:content-[""]',
|
'before:content-[""]',
|
||||||
'before:opacity-0',
|
'before:opacity-0',
|
||||||
'before:[animation:none]',
|
'before:[animation:none]',
|
||||||
@@ -89,7 +84,6 @@ const hoverOnlyClasses = [
|
|||||||
'after:opacity-0',
|
'after:opacity-0',
|
||||||
'after:[animation:none]',
|
'after:[animation:none]',
|
||||||
|
|
||||||
// Show and animate on hover
|
|
||||||
'hover:before:content-[attr(data-text)]',
|
'hover:before:content-[attr(data-text)]',
|
||||||
'hover:before:opacity-100',
|
'hover:before:opacity-100',
|
||||||
'hover:before:left-[-10px]',
|
'hover:before:left-[-10px]',
|
||||||
|
|||||||
226
src/demo/Animations/RibbonsDemo.vue
Normal file
226
src/demo/Animations/RibbonsDemo.vue
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
<template>
|
||||||
|
<div class="ribbons-demo">
|
||||||
|
<TabbedLayout>
|
||||||
|
<template #preview>
|
||||||
|
<div class="demo-container" style="height: 500px; position: relative">
|
||||||
|
<div class="hover-text">Hover Me.</div>
|
||||||
|
<Ribbons
|
||||||
|
:base-thickness="baseThickness"
|
||||||
|
:colors="colors"
|
||||||
|
:speed-multiplier="speedMultiplier"
|
||||||
|
:max-age="maxAge"
|
||||||
|
:enable-fade="enableFade"
|
||||||
|
:enable-shader-effect="enableWaves"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Customize>
|
||||||
|
<div class="count-controls">
|
||||||
|
<span class="count-label">Count</span>
|
||||||
|
<button @click="removeColor" :disabled="colors.length <= 1" class="count-button">
|
||||||
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<span class="count-value">{{ colors.length }}</span>
|
||||||
|
<button @click="addColor" :disabled="colors.length >= 10" class="count-button">
|
||||||
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<line x1="12" y1="5" x2="12" y2="19"></line>
|
||||||
|
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<PreviewSlider title="Thickness" v-model="baseThickness" :min="1" :max="60" :step="1" />
|
||||||
|
|
||||||
|
<PreviewSlider title="Speed" v-model="speedMultiplier" :min="0.3" :max="0.7" :step="0.01" />
|
||||||
|
|
||||||
|
<PreviewSlider title="Max Age" v-model="maxAge" :min="300" :max="1000" :step="100" />
|
||||||
|
|
||||||
|
<PreviewSwitch title="Enable Fade" v-model="enableFade" />
|
||||||
|
|
||||||
|
<PreviewSwitch title="Enable Waves" v-model="enableWaves" />
|
||||||
|
</Customize>
|
||||||
|
|
||||||
|
<PropTable :data="propData" />
|
||||||
|
|
||||||
|
<Dependencies :dependency-list="['ogl']" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #code>
|
||||||
|
<CodeExample :code-object="ribbons" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cli>
|
||||||
|
<CliInstallation :command="ribbons.cli" />
|
||||||
|
</template>
|
||||||
|
</TabbedLayout>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import TabbedLayout from '../../components/common/TabbedLayout.vue';
|
||||||
|
import PropTable from '../../components/common/PropTable.vue';
|
||||||
|
import Dependencies from '../../components/code/Dependencies.vue';
|
||||||
|
import CliInstallation from '../../components/code/CliInstallation.vue';
|
||||||
|
import CodeExample from '../../components/code/CodeExample.vue';
|
||||||
|
import Customize from '../../components/common/Customize.vue';
|
||||||
|
import PreviewSlider from '../../components/common/PreviewSlider.vue';
|
||||||
|
import PreviewSwitch from '../../components/common/PreviewSwitch.vue';
|
||||||
|
import Ribbons from '../../content/Animations/Ribbons/Ribbons.vue';
|
||||||
|
import { ribbons } from '@/constants/code/Animations/ribbonsCode';
|
||||||
|
|
||||||
|
const baseThickness = ref(30);
|
||||||
|
const colors = ref(['#5227FF']);
|
||||||
|
const speedMultiplier = ref(0.5);
|
||||||
|
const maxAge = ref(500);
|
||||||
|
const enableFade = ref(false);
|
||||||
|
const enableWaves = ref(false);
|
||||||
|
|
||||||
|
const addColor = () => {
|
||||||
|
if (colors.value.length < 10) {
|
||||||
|
const newColor = `#${Math.floor(Math.random() * 16777215)
|
||||||
|
.toString(16)
|
||||||
|
.padStart(6, '0')}`;
|
||||||
|
colors.value.push(newColor);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeColor = () => {
|
||||||
|
if (colors.value.length > 1) {
|
||||||
|
colors.value.pop();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const propData = [
|
||||||
|
{
|
||||||
|
name: 'colors',
|
||||||
|
type: 'string[]',
|
||||||
|
default: "['#5227FF']",
|
||||||
|
description: 'An array of color strings to be used for the ribbons.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'baseSpring',
|
||||||
|
type: 'number',
|
||||||
|
default: '0.03',
|
||||||
|
description: 'Base spring factor for the physics controlling ribbon motion.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'baseFriction',
|
||||||
|
type: 'number',
|
||||||
|
default: '0.9',
|
||||||
|
description: 'Base friction factor that dampens the ribbon motion.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'baseThickness',
|
||||||
|
type: 'number',
|
||||||
|
default: '30',
|
||||||
|
description: 'The base thickness of the ribbons.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'offsetFactor',
|
||||||
|
type: 'number',
|
||||||
|
default: '0.02',
|
||||||
|
description: 'A factor to horizontally offset the starting positions of the ribbons.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'maxAge',
|
||||||
|
type: 'number',
|
||||||
|
default: '500',
|
||||||
|
description: 'Delay in milliseconds controlling how long the ribbon trails extend.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'pointCount',
|
||||||
|
type: 'number',
|
||||||
|
default: '50',
|
||||||
|
description: 'The number of points that make up each ribbon.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'speedMultiplier',
|
||||||
|
type: 'number',
|
||||||
|
default: '0.5',
|
||||||
|
description: 'Multiplier that adjusts how fast trailing points interpolate towards the head.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'enableFade',
|
||||||
|
type: 'boolean',
|
||||||
|
default: 'true',
|
||||||
|
description: 'If true, a fade effect is applied along the length of the ribbon.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'enableShaderEffect',
|
||||||
|
type: 'boolean',
|
||||||
|
default: 'true',
|
||||||
|
description: 'If true, an additional sine-wave shader effect is applied to the ribbons.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'effectAmplitude',
|
||||||
|
type: 'number',
|
||||||
|
default: '2',
|
||||||
|
description: 'The amplitude of the shader displacement effect.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'backgroundColor',
|
||||||
|
type: 'number[]',
|
||||||
|
default: '[0, 0, 0, 0]',
|
||||||
|
description: 'An RGBA array specifying the clear color for the renderer.'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.hover-text {
|
||||||
|
position: absolute;
|
||||||
|
font-size: clamp(2rem, 6vw, 6rem);
|
||||||
|
font-weight: 900;
|
||||||
|
color: #222;
|
||||||
|
z-index: 1;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-container {
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.count-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.count-label {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.count-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 2.5rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
background: #1b1b1b;
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-radius: 10px;
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.count-button:hover:not(:disabled) {
|
||||||
|
background: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
.count-button:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.count-value {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
min-width: 1.5rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user