mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 14:39:30 -07:00
Migrate <Ribbons />
This commit is contained in:
@@ -32,6 +32,7 @@ export const CATEGORIES = [
|
||||
'Animated Content',
|
||||
'Fade Content',
|
||||
'Pixel Transition',
|
||||
'Ribbons',
|
||||
'Glare Hover',
|
||||
'Magnet Lines',
|
||||
'Count Up',
|
||||
|
||||
@@ -5,6 +5,7 @@ const animations = {
|
||||
'glare-hover': () => import('../demo/Animations/GlareHoverDemo.vue'),
|
||||
'magnet-lines': () => import('../demo/Animations/MagnetLinesDemo.vue'),
|
||||
'click-spark': () => import('../demo/Animations/ClickSparkDemo.vue'),
|
||||
'ribbons': () => import('../demo/Animations/RibbonsDemo.vue'),
|
||||
'metallic-paint': () => import('../demo/Animations/MetallicPaintDemo.vue'),
|
||||
'magnet': () => import('../demo/Animations/MagnetDemo.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 instanced = new THREE.InstancedBufferGeometry();
|
||||
// Copy geometry attributes
|
||||
|
||||
for (const key in geometry.attributes) {
|
||||
instanced.setAttribute(key, geometry.attributes[key]);
|
||||
}
|
||||
@@ -634,7 +634,7 @@ class LightsSticks {
|
||||
const options = this.options;
|
||||
const geometry = new THREE.PlaneGeometry(1, 1);
|
||||
const instanced = new THREE.InstancedBufferGeometry();
|
||||
// Copy geometry attributes
|
||||
|
||||
for (const key in geometry.attributes) {
|
||||
instanced.setAttribute(key, geometry.attributes[key]);
|
||||
}
|
||||
@@ -1038,17 +1038,14 @@ class App {
|
||||
this.onMouseUp = this.onMouseUp.bind(this);
|
||||
this.onWindowResize = this.onWindowResize.bind(this);
|
||||
|
||||
// Use ResizeObserver for better container tracking
|
||||
if (typeof ResizeObserver !== 'undefined') {
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
this.onWindowResize();
|
||||
});
|
||||
resizeObserver.observe(container);
|
||||
|
||||
// Store reference for cleanup
|
||||
this.resizeObserver = resizeObserver;
|
||||
} else {
|
||||
// Fallback to window resize
|
||||
window.addEventListener('resize', this.onWindowResize);
|
||||
}
|
||||
}
|
||||
@@ -1220,7 +1217,6 @@ class App {
|
||||
tick() {
|
||||
if (this.disposed || !this) return;
|
||||
|
||||
// Ensure renderer stays within container bounds
|
||||
const containerWidth = this.container.offsetWidth;
|
||||
const containerHeight = this.container.offsetHeight;
|
||||
|
||||
|
||||
@@ -40,7 +40,6 @@ const inlineStyles = computed(
|
||||
);
|
||||
|
||||
const baseClasses = [
|
||||
// Base styling
|
||||
'text-white',
|
||||
'font-black',
|
||||
'whitespace-nowrap',
|
||||
@@ -50,7 +49,6 @@ const baseClasses = [
|
||||
'cursor-pointer',
|
||||
'text-[clamp(2rem,10vw,8rem)]',
|
||||
|
||||
// Pseudo-elements base
|
||||
'before:content-[attr(data-text)]',
|
||||
'before:absolute',
|
||||
'before:top-0',
|
||||
@@ -69,19 +67,16 @@ const baseClasses = [
|
||||
];
|
||||
|
||||
const normalGlitchClasses = [
|
||||
// After pseudo-element for normal mode
|
||||
'after:left-[10px]',
|
||||
'after:[text-shadow:var(--after-shadow,-10px_0_red)]',
|
||||
'after:[animation:animate-glitch_var(--after-duration,3s)_infinite_linear_alternate-reverse]',
|
||||
|
||||
// Before pseudo-element for normal mode
|
||||
'before:left-[-10px]',
|
||||
'before:[text-shadow:var(--before-shadow,10px_0_cyan)]',
|
||||
'before:[animation:animate-glitch_var(--before-duration,2s)_infinite_linear_alternate-reverse]'
|
||||
];
|
||||
|
||||
const hoverOnlyClasses = [
|
||||
// Hide pseudo-elements by default
|
||||
'before:content-[""]',
|
||||
'before:opacity-0',
|
||||
'before:[animation:none]',
|
||||
@@ -89,7 +84,6 @@ const hoverOnlyClasses = [
|
||||
'after:opacity-0',
|
||||
'after:[animation:none]',
|
||||
|
||||
// Show and animate on hover
|
||||
'hover:before:content-[attr(data-text)]',
|
||||
'hover:before:opacity-100',
|
||||
'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