mempercanti style animasi home
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
<script setup lang="ts">
|
||||
import { Motion, useAnimationFrame, useMotionValue, useTransform } from 'motion-v';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
interface ShinyTextProps {
|
||||
text: string;
|
||||
disabled?: boolean;
|
||||
speed?: number;
|
||||
className?: string;
|
||||
color?: string;
|
||||
shineColor?: string;
|
||||
spread?: number;
|
||||
yoyo?: boolean;
|
||||
pauseOnHover?: boolean;
|
||||
direction?: 'left' | 'right';
|
||||
delay?: number;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<ShinyTextProps>(), {
|
||||
disabled: false,
|
||||
speed: 2,
|
||||
className: '',
|
||||
color: '#b5b5b5',
|
||||
shineColor: '#ffffff',
|
||||
spread: 120,
|
||||
yoyo: false,
|
||||
pauseOnHover: false,
|
||||
direction: 'left',
|
||||
delay: 0
|
||||
});
|
||||
|
||||
const isPaused = ref(false);
|
||||
const progress = useMotionValue(0);
|
||||
const elapsedRef = ref(0);
|
||||
const lastTimeRef = ref<number | null>(null);
|
||||
const directionRef = ref(props.direction === 'left' ? 1 : -1);
|
||||
|
||||
const animationDuration = computed(() => props.speed * 1000);
|
||||
const delayDuration = computed(() => props.delay * 1000);
|
||||
|
||||
useAnimationFrame(time => {
|
||||
if (props.disabled || isPaused.value) {
|
||||
lastTimeRef.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (lastTimeRef.value === null) {
|
||||
lastTimeRef.value = time;
|
||||
return;
|
||||
}
|
||||
|
||||
const deltaTime = time - lastTimeRef.value;
|
||||
lastTimeRef.value = time;
|
||||
|
||||
elapsedRef.value += deltaTime;
|
||||
|
||||
// Animation goes from 0 to 100
|
||||
if (props.yoyo) {
|
||||
const cycleDuration = animationDuration.value + delayDuration.value;
|
||||
const fullCycle = cycleDuration * 2;
|
||||
const cycleTime = elapsedRef.value % fullCycle;
|
||||
|
||||
if (cycleTime < animationDuration.value) {
|
||||
// Forward animation: 0 -> 100
|
||||
const p = (cycleTime / animationDuration.value) * 100;
|
||||
progress.set(directionRef.value === 1 ? p : 100 - p);
|
||||
} else if (cycleTime < cycleDuration) {
|
||||
// Delay at end
|
||||
progress.set(directionRef.value === 1 ? 100 : 0);
|
||||
} else if (cycleTime < cycleDuration + animationDuration.value) {
|
||||
// Reverse animation: 100 -> 0
|
||||
const reverseTime = cycleTime - cycleDuration;
|
||||
const p = 100 - (reverseTime / animationDuration.value) * 100;
|
||||
progress.set(directionRef.value === 1 ? p : 100 - p);
|
||||
} else {
|
||||
// Delay at start
|
||||
progress.set(directionRef.value === 1 ? 0 : 100);
|
||||
}
|
||||
} else {
|
||||
const cycleDuration = animationDuration.value + delayDuration.value;
|
||||
const cycleTime = elapsedRef.value % cycleDuration;
|
||||
|
||||
if (cycleTime < animationDuration.value) {
|
||||
// Animation phase: 0 -> 100
|
||||
const p = (cycleTime / animationDuration.value) * 100;
|
||||
progress.set(directionRef.value === 1 ? p : 100 - p);
|
||||
} else {
|
||||
// Delay phase - hold at end (shine off-screen)
|
||||
progress.set(directionRef.value === 1 ? 100 : 0);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.direction,
|
||||
() => {
|
||||
directionRef.value = props.direction === 'left' ? 1 : -1;
|
||||
elapsedRef.value = 0;
|
||||
progress.set(0);
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
);
|
||||
|
||||
const backgroundPosition = useTransform(progress, p => `${150 - p * 2}% center`);
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
if (props.pauseOnHover) isPaused.value = true;
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
if (props.pauseOnHover) isPaused.value = false;
|
||||
};
|
||||
|
||||
const gradientStyle = computed(() => ({
|
||||
backgroundImage: `linear-gradient(${props.spread}deg, ${props.color} 0%, ${props.color} 35%, ${props.shineColor} 50%, ${props.color} 65%, ${props.color} 100%)`,
|
||||
backgroundSize: '200% auto',
|
||||
WebkitBackgroundClip: 'text',
|
||||
backgroundClip: 'text',
|
||||
WebkitTextFillColor: 'transparent'
|
||||
}));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Motion
|
||||
tag="span"
|
||||
:class="['inline-block', className]"
|
||||
:style="{ ...gradientStyle, backgroundPosition }"
|
||||
@mouseenter="handleMouseEnter"
|
||||
@mouseleave="handleMouseLeave"
|
||||
>
|
||||
{{ text }}
|
||||
</Motion>
|
||||
</template>
|
||||
@@ -0,0 +1,176 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onBeforeUnmount, watch, computed, useTemplateRef } from 'vue';
|
||||
import { gsap } from 'gsap';
|
||||
|
||||
interface TextTypeProps {
|
||||
className?: string;
|
||||
showCursor?: boolean;
|
||||
hideCursorWhileTyping?: boolean;
|
||||
cursorCharacter?: string;
|
||||
cursorBlinkDuration?: number;
|
||||
cursorClassName?: string;
|
||||
text: string | string[];
|
||||
as?: string;
|
||||
typingSpeed?: number;
|
||||
initialDelay?: number;
|
||||
pauseDuration?: number;
|
||||
deletingSpeed?: number;
|
||||
loop?: boolean;
|
||||
textColors?: string[];
|
||||
variableSpeed?: { min: number; max: number };
|
||||
onSentenceComplete?: (sentence: string, index: number) => void;
|
||||
startOnVisible?: boolean;
|
||||
reverseMode?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<TextTypeProps>(), {
|
||||
as: 'div',
|
||||
typingSpeed: 50,
|
||||
initialDelay: 0,
|
||||
pauseDuration: 2000,
|
||||
deletingSpeed: 30,
|
||||
loop: true,
|
||||
className: '',
|
||||
showCursor: true,
|
||||
hideCursorWhileTyping: false,
|
||||
cursorCharacter: '|',
|
||||
cursorBlinkDuration: 0.5,
|
||||
textColors: () => [],
|
||||
startOnVisible: false,
|
||||
reverseMode: false
|
||||
});
|
||||
|
||||
const displayedText = ref('');
|
||||
const currentCharIndex = ref(0);
|
||||
const isDeleting = ref(false);
|
||||
const currentTextIndex = ref(0);
|
||||
const isVisible = ref(!props.startOnVisible);
|
||||
const cursorRef = useTemplateRef('cursorRef');
|
||||
const containerRef = useTemplateRef('containerRef');
|
||||
|
||||
const textArray = computed(() => (Array.isArray(props.text) ? props.text : [props.text]));
|
||||
|
||||
const getRandomSpeed = () => {
|
||||
if (!props.variableSpeed) return props.typingSpeed;
|
||||
const { min, max } = props.variableSpeed;
|
||||
return Math.random() * (max - min) + min;
|
||||
};
|
||||
|
||||
const getCurrentTextColor = () => {
|
||||
if (!props.textColors.length) return '#ffffff';
|
||||
return props.textColors[currentTextIndex.value % props.textColors.length];
|
||||
};
|
||||
|
||||
let timeout: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
const clearTimeoutIfNeeded = () => {
|
||||
if (timeout) clearTimeout(timeout);
|
||||
};
|
||||
|
||||
const executeTypingAnimation = () => {
|
||||
const currentText = textArray.value[currentTextIndex.value];
|
||||
const processedText = props.reverseMode ? currentText.split('').reverse().join('') : currentText;
|
||||
|
||||
if (isDeleting.value) {
|
||||
if (displayedText.value === '') {
|
||||
isDeleting.value = false;
|
||||
if (currentTextIndex.value === textArray.value.length - 1 && !props.loop) return;
|
||||
|
||||
props.onSentenceComplete?.(textArray.value[currentTextIndex.value], currentTextIndex.value);
|
||||
|
||||
currentTextIndex.value = (currentTextIndex.value + 1) % textArray.value.length;
|
||||
currentCharIndex.value = 0;
|
||||
timeout = setTimeout(() => {}, props.pauseDuration);
|
||||
} else {
|
||||
timeout = setTimeout(() => {
|
||||
displayedText.value = displayedText.value.slice(0, -1);
|
||||
}, props.deletingSpeed);
|
||||
}
|
||||
} else {
|
||||
if (currentCharIndex.value < processedText.length) {
|
||||
timeout = setTimeout(
|
||||
() => {
|
||||
displayedText.value += processedText[currentCharIndex.value];
|
||||
currentCharIndex.value += 1;
|
||||
},
|
||||
props.variableSpeed ? getRandomSpeed() : props.typingSpeed
|
||||
);
|
||||
} else if (textArray.value.length > 1) {
|
||||
timeout = setTimeout(() => {
|
||||
isDeleting.value = true;
|
||||
}, props.pauseDuration);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
[displayedText, currentCharIndex, isDeleting, isVisible],
|
||||
() => {
|
||||
if (!isVisible.value) return;
|
||||
clearTimeoutIfNeeded();
|
||||
|
||||
if (currentCharIndex.value === 0 && !isDeleting.value && displayedText.value === '') {
|
||||
timeout = setTimeout(() => {
|
||||
executeTypingAnimation();
|
||||
}, props.initialDelay);
|
||||
} else {
|
||||
executeTypingAnimation();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
if (props.showCursor && cursorRef.value) {
|
||||
gsap.set(cursorRef.value, { opacity: 1 });
|
||||
gsap.to(cursorRef.value, {
|
||||
opacity: 0,
|
||||
duration: props.cursorBlinkDuration,
|
||||
repeat: -1,
|
||||
yoyo: true,
|
||||
ease: 'power2.inOut'
|
||||
});
|
||||
}
|
||||
|
||||
if (props.startOnVisible && containerRef.value) {
|
||||
const observer = new IntersectionObserver(
|
||||
entries => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) isVisible.value = true;
|
||||
});
|
||||
},
|
||||
{ threshold: 0.1 }
|
||||
);
|
||||
if (containerRef.value instanceof Element) {
|
||||
observer.observe(containerRef.value);
|
||||
}
|
||||
onBeforeUnmount(() => observer.disconnect());
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearTimeoutIfNeeded();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component
|
||||
:is="as"
|
||||
ref="containerRef"
|
||||
:class="`inline-block whitespace-pre-wrap tracking-tight ${className}`"
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<span class="inline" :style="{ color: getCurrentTextColor() }">
|
||||
{{ displayedText }}
|
||||
</span>
|
||||
<span
|
||||
v-if="showCursor"
|
||||
ref="cursorRef"
|
||||
:class="`ml-1 inline-block opacity-100 ${
|
||||
hideCursorWhileTyping && (currentCharIndex < textArray[currentTextIndex].length || isDeleting) ? 'hidden' : ''
|
||||
} ${cursorClassName}`"
|
||||
>
|
||||
{{ cursorCharacter }}
|
||||
</span>
|
||||
</component>
|
||||
</template>
|
||||
Reference in New Issue
Block a user