mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-09 00:19:31 -06:00
Add prettier config, format codebase
This commit is contained in:
@@ -1,22 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { gsap } from 'gsap'
|
||||
import { ScrollTrigger } from 'gsap/ScrollTrigger'
|
||||
import { ref, onMounted, onUnmounted, watch } from 'vue';
|
||||
import { gsap } from 'gsap';
|
||||
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
||||
|
||||
gsap.registerPlugin(ScrollTrigger)
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
|
||||
interface AnimatedContentProps {
|
||||
distance?: number
|
||||
direction?: 'vertical' | 'horizontal'
|
||||
reverse?: boolean
|
||||
duration?: number
|
||||
ease?: string | ((progress: number) => number)
|
||||
initialOpacity?: number
|
||||
animateOpacity?: boolean
|
||||
scale?: number
|
||||
threshold?: number
|
||||
delay?: number
|
||||
className?: string
|
||||
distance?: number;
|
||||
direction?: 'vertical' | 'horizontal';
|
||||
reverse?: boolean;
|
||||
duration?: number;
|
||||
ease?: string | ((progress: number) => number);
|
||||
initialOpacity?: number;
|
||||
animateOpacity?: boolean;
|
||||
scale?: number;
|
||||
threshold?: number;
|
||||
delay?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<AnimatedContentProps>(), {
|
||||
@@ -31,27 +31,27 @@ const props = withDefaults(defineProps<AnimatedContentProps>(), {
|
||||
threshold: 0.1,
|
||||
delay: 0,
|
||||
className: ''
|
||||
})
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
complete: []
|
||||
}>()
|
||||
complete: [];
|
||||
}>();
|
||||
|
||||
const containerRef = ref<HTMLDivElement>()
|
||||
const containerRef = ref<HTMLDivElement>();
|
||||
|
||||
onMounted(() => {
|
||||
const el = containerRef.value
|
||||
if (!el) return
|
||||
const el = containerRef.value;
|
||||
if (!el) return;
|
||||
|
||||
const axis = props.direction === 'horizontal' ? 'x' : 'y'
|
||||
const offset = props.reverse ? -props.distance : props.distance
|
||||
const startPct = (1 - props.threshold) * 100
|
||||
const axis = props.direction === 'horizontal' ? 'x' : 'y';
|
||||
const offset = props.reverse ? -props.distance : props.distance;
|
||||
const startPct = (1 - props.threshold) * 100;
|
||||
|
||||
gsap.set(el, {
|
||||
[axis]: offset,
|
||||
scale: props.scale,
|
||||
opacity: props.animateOpacity ? props.initialOpacity : 1,
|
||||
})
|
||||
opacity: props.animateOpacity ? props.initialOpacity : 1
|
||||
});
|
||||
|
||||
gsap.to(el, {
|
||||
[axis]: 0,
|
||||
@@ -65,10 +65,10 @@ onMounted(() => {
|
||||
trigger: el,
|
||||
start: `top ${startPct}%`,
|
||||
toggleActions: 'play none none none',
|
||||
once: true,
|
||||
},
|
||||
})
|
||||
})
|
||||
once: true
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
watch(
|
||||
() => [
|
||||
@@ -81,24 +81,24 @@ watch(
|
||||
props.animateOpacity,
|
||||
props.scale,
|
||||
props.threshold,
|
||||
props.delay,
|
||||
props.delay
|
||||
],
|
||||
() => {
|
||||
const el = containerRef.value
|
||||
if (!el) return
|
||||
const el = containerRef.value;
|
||||
if (!el) return;
|
||||
|
||||
ScrollTrigger.getAll().forEach((t) => t.kill())
|
||||
gsap.killTweensOf(el)
|
||||
ScrollTrigger.getAll().forEach(t => t.kill());
|
||||
gsap.killTweensOf(el);
|
||||
|
||||
const axis = props.direction === 'horizontal' ? 'x' : 'y'
|
||||
const offset = props.reverse ? -props.distance : props.distance
|
||||
const startPct = (1 - props.threshold) * 100
|
||||
const axis = props.direction === 'horizontal' ? 'x' : 'y';
|
||||
const offset = props.reverse ? -props.distance : props.distance;
|
||||
const startPct = (1 - props.threshold) * 100;
|
||||
|
||||
gsap.set(el, {
|
||||
[axis]: offset,
|
||||
scale: props.scale,
|
||||
opacity: props.animateOpacity ? props.initialOpacity : 1,
|
||||
})
|
||||
opacity: props.animateOpacity ? props.initialOpacity : 1
|
||||
});
|
||||
|
||||
gsap.to(el, {
|
||||
[axis]: 0,
|
||||
@@ -112,27 +112,24 @@ watch(
|
||||
trigger: el,
|
||||
start: `top ${startPct}%`,
|
||||
toggleActions: 'play none none none',
|
||||
once: true,
|
||||
},
|
||||
})
|
||||
once: true
|
||||
}
|
||||
});
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
);
|
||||
|
||||
onUnmounted(() => {
|
||||
const el = containerRef.value
|
||||
const el = containerRef.value;
|
||||
if (el) {
|
||||
ScrollTrigger.getAll().forEach((t) => t.kill())
|
||||
gsap.killTweensOf(el)
|
||||
ScrollTrigger.getAll().forEach(t => t.kill());
|
||||
gsap.killTweensOf(el);
|
||||
}
|
||||
})
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="containerRef"
|
||||
:class="`animated-content ${props.className}`"
|
||||
>
|
||||
<div ref="containerRef" :class="`animated-content ${props.className}`">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,35 +1,29 @@
|
||||
<template>
|
||||
<div
|
||||
ref="containerRef"
|
||||
class="relative w-full h-full"
|
||||
@click="handleClick"
|
||||
>
|
||||
<canvas
|
||||
ref="canvasRef"
|
||||
class="absolute inset-0 pointer-events-none"
|
||||
/>
|
||||
<div ref="containerRef" class="relative w-full h-full" @click="handleClick">
|
||||
<canvas ref="canvasRef" class="absolute inset-0 pointer-events-none" />
|
||||
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, computed, watch } from 'vue'
|
||||
import { ref, onMounted, onUnmounted, computed, watch } from 'vue';
|
||||
|
||||
interface Spark {
|
||||
x: number
|
||||
y: number
|
||||
angle: number
|
||||
startTime: number
|
||||
x: number;
|
||||
y: number;
|
||||
angle: number;
|
||||
startTime: number;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
sparkColor?: string
|
||||
sparkSize?: number
|
||||
sparkRadius?: number
|
||||
sparkCount?: number
|
||||
duration?: number
|
||||
easing?: "linear" | "ease-in" | "ease-out" | "ease-in-out"
|
||||
extraScale?: number
|
||||
sparkColor?: string;
|
||||
sparkSize?: number;
|
||||
sparkRadius?: number;
|
||||
sparkCount?: number;
|
||||
duration?: number;
|
||||
easing?: 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out';
|
||||
extraScale?: number;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
@@ -40,149 +34,152 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
duration: 400,
|
||||
easing: 'ease-out',
|
||||
extraScale: 1.0
|
||||
})
|
||||
});
|
||||
|
||||
const containerRef = ref<HTMLDivElement | null>(null)
|
||||
const canvasRef = ref<HTMLCanvasElement | null>(null)
|
||||
const sparks = ref<Spark[]>([])
|
||||
const startTimeRef = ref<number | null>(null)
|
||||
const animationId = ref<number | null>(null)
|
||||
const containerRef = ref<HTMLDivElement | null>(null);
|
||||
const canvasRef = ref<HTMLCanvasElement | null>(null);
|
||||
const sparks = ref<Spark[]>([]);
|
||||
const startTimeRef = ref<number | null>(null);
|
||||
const animationId = ref<number | null>(null);
|
||||
|
||||
const easeFunc = computed(() => {
|
||||
return (t: number) => {
|
||||
switch (props.easing) {
|
||||
case "linear":
|
||||
return t
|
||||
case "ease-in":
|
||||
return t * t
|
||||
case "ease-in-out":
|
||||
return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t
|
||||
case 'linear':
|
||||
return t;
|
||||
case 'ease-in':
|
||||
return t * t;
|
||||
case 'ease-in-out':
|
||||
return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
|
||||
default:
|
||||
return t * (2 - t)
|
||||
return t * (2 - t);
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
const handleClick = (e: MouseEvent) => {
|
||||
const canvas = canvasRef.value
|
||||
if (!canvas) return
|
||||
const rect = canvas.getBoundingClientRect()
|
||||
const x = e.clientX - rect.left
|
||||
const y = e.clientY - rect.top
|
||||
const canvas = canvasRef.value;
|
||||
if (!canvas) return;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
|
||||
const now = performance.now()
|
||||
const now = performance.now();
|
||||
const newSparks: Spark[] = Array.from({ length: props.sparkCount }, (_, i) => ({
|
||||
x,
|
||||
y,
|
||||
angle: (2 * Math.PI * i) / props.sparkCount,
|
||||
startTime: now,
|
||||
}))
|
||||
startTime: now
|
||||
}));
|
||||
|
||||
sparks.value.push(...newSparks)
|
||||
}
|
||||
sparks.value.push(...newSparks);
|
||||
};
|
||||
|
||||
const draw = (timestamp: number) => {
|
||||
if (!startTimeRef.value) {
|
||||
startTimeRef.value = timestamp
|
||||
startTimeRef.value = timestamp;
|
||||
}
|
||||
|
||||
const canvas = canvasRef.value
|
||||
const ctx = canvas?.getContext('2d')
|
||||
if (!ctx || !canvas) return
|
||||
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||
|
||||
const canvas = canvasRef.value;
|
||||
const ctx = canvas?.getContext('2d');
|
||||
if (!ctx || !canvas) return;
|
||||
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
sparks.value = sparks.value.filter((spark: Spark) => {
|
||||
const elapsed = timestamp - spark.startTime
|
||||
const elapsed = timestamp - spark.startTime;
|
||||
if (elapsed >= props.duration) {
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
|
||||
const progress = elapsed / props.duration
|
||||
const eased = easeFunc.value(progress)
|
||||
const progress = elapsed / props.duration;
|
||||
const eased = easeFunc.value(progress);
|
||||
|
||||
const distance = eased * props.sparkRadius * props.extraScale
|
||||
const lineLength = props.sparkSize * (1 - eased)
|
||||
const distance = eased * props.sparkRadius * props.extraScale;
|
||||
const lineLength = props.sparkSize * (1 - eased);
|
||||
|
||||
const x1 = spark.x + distance * Math.cos(spark.angle)
|
||||
const y1 = spark.y + distance * Math.sin(spark.angle)
|
||||
const x2 = spark.x + (distance + lineLength) * Math.cos(spark.angle)
|
||||
const y2 = spark.y + (distance + lineLength) * Math.sin(spark.angle)
|
||||
const x1 = spark.x + distance * Math.cos(spark.angle);
|
||||
const y1 = spark.y + distance * Math.sin(spark.angle);
|
||||
const x2 = spark.x + (distance + lineLength) * Math.cos(spark.angle);
|
||||
const y2 = spark.y + (distance + lineLength) * Math.sin(spark.angle);
|
||||
|
||||
ctx.strokeStyle = props.sparkColor
|
||||
ctx.lineWidth = 2
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(x1, y1)
|
||||
ctx.lineTo(x2, y2)
|
||||
ctx.stroke()
|
||||
ctx.strokeStyle = props.sparkColor;
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x1, y1);
|
||||
ctx.lineTo(x2, y2);
|
||||
ctx.stroke();
|
||||
|
||||
return true
|
||||
})
|
||||
return true;
|
||||
});
|
||||
|
||||
animationId.value = requestAnimationFrame(draw)
|
||||
}
|
||||
animationId.value = requestAnimationFrame(draw);
|
||||
};
|
||||
|
||||
const resizeCanvas = () => {
|
||||
const canvas = canvasRef.value
|
||||
if (!canvas) return
|
||||
const canvas = canvasRef.value;
|
||||
if (!canvas) return;
|
||||
|
||||
const parent = canvas.parentElement
|
||||
if (!parent) return
|
||||
const parent = canvas.parentElement;
|
||||
if (!parent) return;
|
||||
|
||||
const { width, height } = parent.getBoundingClientRect()
|
||||
const { width, height } = parent.getBoundingClientRect();
|
||||
if (canvas.width !== width || canvas.height !== height) {
|
||||
canvas.width = width
|
||||
canvas.height = height
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let resizeTimeout: number
|
||||
let resizeTimeout: number;
|
||||
|
||||
const handleResize = () => {
|
||||
clearTimeout(resizeTimeout)
|
||||
resizeTimeout = setTimeout(resizeCanvas, 100)
|
||||
}
|
||||
clearTimeout(resizeTimeout);
|
||||
resizeTimeout = setTimeout(resizeCanvas, 100);
|
||||
};
|
||||
|
||||
let resizeObserver: ResizeObserver | null = null
|
||||
let resizeObserver: ResizeObserver | null = null;
|
||||
|
||||
onMounted(() => {
|
||||
const canvas = canvasRef.value
|
||||
if (!canvas) return
|
||||
const canvas = canvasRef.value;
|
||||
if (!canvas) return;
|
||||
|
||||
const parent = canvas.parentElement
|
||||
if (!parent) return
|
||||
const parent = canvas.parentElement;
|
||||
if (!parent) return;
|
||||
|
||||
resizeObserver = new ResizeObserver(handleResize)
|
||||
resizeObserver.observe(parent)
|
||||
resizeObserver = new ResizeObserver(handleResize);
|
||||
resizeObserver.observe(parent);
|
||||
|
||||
resizeCanvas()
|
||||
resizeCanvas();
|
||||
|
||||
animationId.value = requestAnimationFrame(draw)
|
||||
})
|
||||
animationId.value = requestAnimationFrame(draw);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (resizeObserver) {
|
||||
resizeObserver.disconnect()
|
||||
resizeObserver.disconnect();
|
||||
}
|
||||
clearTimeout(resizeTimeout)
|
||||
|
||||
if (animationId.value) {
|
||||
cancelAnimationFrame(animationId.value)
|
||||
}
|
||||
})
|
||||
clearTimeout(resizeTimeout);
|
||||
|
||||
watch([
|
||||
() => props.sparkColor,
|
||||
() => props.sparkSize,
|
||||
() => props.sparkRadius,
|
||||
() => props.sparkCount,
|
||||
() => props.duration,
|
||||
easeFunc,
|
||||
() => props.extraScale
|
||||
], () => {
|
||||
if (animationId.value) {
|
||||
cancelAnimationFrame(animationId.value)
|
||||
cancelAnimationFrame(animationId.value);
|
||||
}
|
||||
animationId.value = requestAnimationFrame(draw)
|
||||
})
|
||||
});
|
||||
|
||||
watch(
|
||||
[
|
||||
() => props.sparkColor,
|
||||
() => props.sparkSize,
|
||||
() => props.sparkRadius,
|
||||
() => props.sparkCount,
|
||||
() => props.duration,
|
||||
easeFunc,
|
||||
() => props.extraScale
|
||||
],
|
||||
() => {
|
||||
if (animationId.value) {
|
||||
cancelAnimationFrame(animationId.value);
|
||||
}
|
||||
animationId.value = requestAnimationFrame(draw);
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -3,161 +3,164 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, watch, computed } from 'vue'
|
||||
import { ref, onMounted, onUnmounted, watch, computed } from 'vue';
|
||||
|
||||
interface Props {
|
||||
to: number
|
||||
from?: number
|
||||
direction?: "up" | "down"
|
||||
delay?: number
|
||||
duration?: number
|
||||
className?: string
|
||||
startWhen?: boolean
|
||||
separator?: string
|
||||
onStart?: () => void
|
||||
onEnd?: () => void
|
||||
to: number;
|
||||
from?: number;
|
||||
direction?: 'up' | 'down';
|
||||
delay?: number;
|
||||
duration?: number;
|
||||
className?: string;
|
||||
startWhen?: boolean;
|
||||
separator?: string;
|
||||
onStart?: () => void;
|
||||
onEnd?: () => void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
from: 0,
|
||||
direction: "up",
|
||||
direction: 'up',
|
||||
delay: 0,
|
||||
duration: 2,
|
||||
className: "",
|
||||
className: '',
|
||||
startWhen: true,
|
||||
separator: ""
|
||||
})
|
||||
separator: ''
|
||||
});
|
||||
|
||||
const elementRef = ref<HTMLSpanElement | null>(null)
|
||||
const currentValue = ref(props.direction === "down" ? props.to : props.from)
|
||||
const isInView = ref(false)
|
||||
const animationId = ref<number | null>(null)
|
||||
const hasStarted = ref(false)
|
||||
const elementRef = ref<HTMLSpanElement | null>(null);
|
||||
const currentValue = ref(props.direction === 'down' ? props.to : props.from);
|
||||
const isInView = ref(false);
|
||||
const animationId = ref<number | null>(null);
|
||||
const hasStarted = ref(false);
|
||||
|
||||
let intersectionObserver: IntersectionObserver | null = null
|
||||
let intersectionObserver: IntersectionObserver | null = null;
|
||||
|
||||
const damping = computed(() => 20 + 40 * (1 / props.duration))
|
||||
const stiffness = computed(() => 100 * (1 / props.duration))
|
||||
const damping = computed(() => 20 + 40 * (1 / props.duration));
|
||||
const stiffness = computed(() => 100 * (1 / props.duration));
|
||||
|
||||
let velocity = 0
|
||||
let startTime = 0
|
||||
let velocity = 0;
|
||||
let startTime = 0;
|
||||
|
||||
const formatNumber = (value: number) => {
|
||||
const options = {
|
||||
useGrouping: !!props.separator,
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0,
|
||||
}
|
||||
maximumFractionDigits: 0
|
||||
};
|
||||
|
||||
const formattedNumber = Intl.NumberFormat("en-US", options).format(
|
||||
Number(value.toFixed(0))
|
||||
)
|
||||
const formattedNumber = Intl.NumberFormat('en-US', options).format(Number(value.toFixed(0)));
|
||||
|
||||
return props.separator
|
||||
? formattedNumber.replace(/,/g, props.separator)
|
||||
: formattedNumber
|
||||
}
|
||||
return props.separator ? formattedNumber.replace(/,/g, props.separator) : formattedNumber;
|
||||
};
|
||||
|
||||
const updateDisplay = () => {
|
||||
if (elementRef.value) {
|
||||
elementRef.value.textContent = formatNumber(currentValue.value)
|
||||
elementRef.value.textContent = formatNumber(currentValue.value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const springAnimation = (timestamp: number) => {
|
||||
if (!startTime) startTime = timestamp
|
||||
if (!startTime) startTime = timestamp;
|
||||
|
||||
const target = props.direction === "down" ? props.from : props.to
|
||||
const current = currentValue.value
|
||||
const target = props.direction === 'down' ? props.from : props.to;
|
||||
const current = currentValue.value;
|
||||
|
||||
const displacement = target - current
|
||||
const springForce = displacement * stiffness.value
|
||||
const dampingForce = velocity * damping.value
|
||||
const acceleration = springForce - dampingForce
|
||||
const displacement = target - current;
|
||||
const springForce = displacement * stiffness.value;
|
||||
const dampingForce = velocity * damping.value;
|
||||
const acceleration = springForce - dampingForce;
|
||||
|
||||
velocity += acceleration * 0.016 // Assuming 60fps
|
||||
currentValue.value += velocity * 0.016
|
||||
velocity += acceleration * 0.016; // Assuming 60fps
|
||||
currentValue.value += velocity * 0.016;
|
||||
|
||||
updateDisplay()
|
||||
updateDisplay();
|
||||
|
||||
if (Math.abs(displacement) > 0.01 || Math.abs(velocity) > 0.01) {
|
||||
animationId.value = requestAnimationFrame(springAnimation)
|
||||
animationId.value = requestAnimationFrame(springAnimation);
|
||||
} else {
|
||||
currentValue.value = target
|
||||
updateDisplay()
|
||||
animationId.value = null
|
||||
currentValue.value = target;
|
||||
updateDisplay();
|
||||
animationId.value = null;
|
||||
|
||||
if (props.onEnd) {
|
||||
props.onEnd()
|
||||
props.onEnd();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const startAnimation = () => {
|
||||
if (hasStarted.value || !isInView.value || !props.startWhen) return
|
||||
if (hasStarted.value || !isInView.value || !props.startWhen) return;
|
||||
|
||||
hasStarted.value = true
|
||||
hasStarted.value = true;
|
||||
|
||||
if (props.onStart) {
|
||||
props.onStart()
|
||||
props.onStart();
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
startTime = 0
|
||||
velocity = 0
|
||||
animationId.value = requestAnimationFrame(springAnimation)
|
||||
}, props.delay * 1000)
|
||||
}
|
||||
startTime = 0;
|
||||
velocity = 0;
|
||||
animationId.value = requestAnimationFrame(springAnimation);
|
||||
}, props.delay * 1000);
|
||||
};
|
||||
|
||||
const setupIntersectionObserver = () => {
|
||||
if (!elementRef.value) return
|
||||
if (!elementRef.value) return;
|
||||
|
||||
intersectionObserver = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting && !isInView.value) {
|
||||
isInView.value = true
|
||||
startAnimation()
|
||||
isInView.value = true;
|
||||
startAnimation();
|
||||
}
|
||||
},
|
||||
{
|
||||
threshold: 0,
|
||||
rootMargin: "0px"
|
||||
rootMargin: '0px'
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
intersectionObserver.observe(elementRef.value)
|
||||
}
|
||||
intersectionObserver.observe(elementRef.value);
|
||||
};
|
||||
|
||||
const cleanup = () => {
|
||||
if (animationId.value) {
|
||||
cancelAnimationFrame(animationId.value)
|
||||
animationId.value = null
|
||||
cancelAnimationFrame(animationId.value);
|
||||
animationId.value = null;
|
||||
}
|
||||
|
||||
if (intersectionObserver) {
|
||||
intersectionObserver.disconnect()
|
||||
intersectionObserver = null
|
||||
intersectionObserver.disconnect();
|
||||
intersectionObserver = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
watch([() => props.from, () => props.to, () => props.direction], () => {
|
||||
currentValue.value = props.direction === "down" ? props.to : props.from
|
||||
updateDisplay()
|
||||
hasStarted.value = false
|
||||
}, { immediate: true })
|
||||
watch(
|
||||
[() => props.from, () => props.to, () => props.direction],
|
||||
() => {
|
||||
currentValue.value = props.direction === 'down' ? props.to : props.from;
|
||||
updateDisplay();
|
||||
hasStarted.value = false;
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
watch(() => props.startWhen, () => {
|
||||
if (props.startWhen && isInView.value && !hasStarted.value) {
|
||||
startAnimation()
|
||||
watch(
|
||||
() => props.startWhen,
|
||||
() => {
|
||||
if (props.startWhen && isInView.value && !hasStarted.value) {
|
||||
startAnimation();
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
updateDisplay()
|
||||
setupIntersectionObserver()
|
||||
})
|
||||
updateDisplay();
|
||||
setupIntersectionObserver();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
cleanup()
|
||||
})
|
||||
</script>
|
||||
cleanup();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -2,46 +2,74 @@
|
||||
<div class="relative w-1/2 max-md:w-11/12 aspect-square" :style="wrapperStyle">
|
||||
<div ref="sceneRef" class="grid w-full h-full" :style="sceneStyle">
|
||||
<template v-for="(_, r) in cells" :key="`row-${r}`">
|
||||
<div v-for="(__, c) in cells" :key="`${r}-${c}`"
|
||||
class="cube relative w-full h-full aspect-square [transform-style:preserve-3d]" :data-row="r" :data-col="c">
|
||||
<div
|
||||
v-for="(__, c) in cells"
|
||||
:key="`${r}-${c}`"
|
||||
class="cube relative w-full h-full aspect-square [transform-style:preserve-3d]"
|
||||
:data-row="r"
|
||||
:data-col="c"
|
||||
>
|
||||
<span class="absolute pointer-events-none -inset-9" />
|
||||
|
||||
<div class="cube-face absolute inset-0 flex items-center justify-center" :style="{
|
||||
background: 'var(--cube-face-bg)',
|
||||
border: 'var(--cube-face-border)',
|
||||
boxShadow: 'var(--cube-face-shadow)',
|
||||
transform: 'translateY(-50%) rotateX(90deg)',
|
||||
}" />
|
||||
<div class="cube-face absolute inset-0 flex items-center justify-center" :style="{
|
||||
background: 'var(--cube-face-bg)',
|
||||
border: 'var(--cube-face-border)',
|
||||
boxShadow: 'var(--cube-face-shadow)',
|
||||
transform: 'translateY(50%) rotateX(-90deg)',
|
||||
}" />
|
||||
<div class="cube-face absolute inset-0 flex items-center justify-center" :style="{
|
||||
background: 'var(--cube-face-bg)',
|
||||
border: 'var(--cube-face-border)',
|
||||
boxShadow: 'var(--cube-face-shadow)',
|
||||
transform: 'translateX(-50%) rotateY(-90deg)',
|
||||
}" />
|
||||
<div class="cube-face absolute inset-0 flex items-center justify-center" :style="{
|
||||
background: 'var(--cube-face-bg)',
|
||||
border: 'var(--cube-face-border)',
|
||||
boxShadow: 'var(--cube-face-shadow)',
|
||||
transform: 'translateX(50%) rotateY(90deg)',
|
||||
}" />
|
||||
<div class="cube-face absolute inset-0 flex items-center justify-center" :style="{
|
||||
background: 'var(--cube-face-bg)',
|
||||
border: 'var(--cube-face-border)',
|
||||
boxShadow: 'var(--cube-face-shadow)',
|
||||
transform: 'rotateY(-90deg) translateX(50%) rotateY(90deg)',
|
||||
}" />
|
||||
<div class="cube-face absolute inset-0 flex items-center justify-center" :style="{
|
||||
background: 'var(--cube-face-bg)',
|
||||
border: 'var(--cube-face-border)',
|
||||
boxShadow: 'var(--cube-face-shadow)',
|
||||
transform: 'rotateY(90deg) translateX(-50%) rotateY(-90deg)',
|
||||
}" />
|
||||
<div
|
||||
class="cube-face absolute inset-0 flex items-center justify-center"
|
||||
:style="{
|
||||
background: 'var(--cube-face-bg)',
|
||||
border: 'var(--cube-face-border)',
|
||||
boxShadow: 'var(--cube-face-shadow)',
|
||||
transform: 'translateY(-50%) rotateX(90deg)'
|
||||
}"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="cube-face absolute inset-0 flex items-center justify-center"
|
||||
:style="{
|
||||
background: 'var(--cube-face-bg)',
|
||||
border: 'var(--cube-face-border)',
|
||||
boxShadow: 'var(--cube-face-shadow)',
|
||||
transform: 'translateY(50%) rotateX(-90deg)'
|
||||
}"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="cube-face absolute inset-0 flex items-center justify-center"
|
||||
:style="{
|
||||
background: 'var(--cube-face-bg)',
|
||||
border: 'var(--cube-face-border)',
|
||||
boxShadow: 'var(--cube-face-shadow)',
|
||||
transform: 'translateX(-50%) rotateY(-90deg)'
|
||||
}"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="cube-face absolute inset-0 flex items-center justify-center"
|
||||
:style="{
|
||||
background: 'var(--cube-face-bg)',
|
||||
border: 'var(--cube-face-border)',
|
||||
boxShadow: 'var(--cube-face-shadow)',
|
||||
transform: 'translateX(50%) rotateY(90deg)'
|
||||
}"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="cube-face absolute inset-0 flex items-center justify-center"
|
||||
:style="{
|
||||
background: 'var(--cube-face-bg)',
|
||||
border: 'var(--cube-face-border)',
|
||||
boxShadow: 'var(--cube-face-shadow)',
|
||||
transform: 'rotateY(-90deg) translateX(50%) rotateY(90deg)'
|
||||
}"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="cube-face absolute inset-0 flex items-center justify-center"
|
||||
:style="{
|
||||
background: 'var(--cube-face-bg)',
|
||||
border: 'var(--cube-face-border)',
|
||||
boxShadow: 'var(--cube-face-shadow)',
|
||||
transform: 'rotateY(90deg) translateX(-50%) rotateY(-90deg)'
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -49,34 +77,34 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted, withDefaults } from 'vue'
|
||||
import gsap from 'gsap'
|
||||
import { ref, computed, onMounted, onUnmounted, withDefaults } from 'vue';
|
||||
import gsap from 'gsap';
|
||||
|
||||
interface Gap {
|
||||
row: number
|
||||
col: number
|
||||
row: number;
|
||||
col: number;
|
||||
}
|
||||
|
||||
interface Duration {
|
||||
enter: number
|
||||
leave: number
|
||||
enter: number;
|
||||
leave: number;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
gridSize?: number
|
||||
cubeSize?: number
|
||||
maxAngle?: number
|
||||
radius?: number
|
||||
easing?: gsap.EaseString
|
||||
duration?: Duration
|
||||
cellGap?: number | Gap
|
||||
borderStyle?: string
|
||||
faceColor?: string
|
||||
shadow?: boolean | string
|
||||
autoAnimate?: boolean
|
||||
rippleOnClick?: boolean
|
||||
rippleColor?: string
|
||||
rippleSpeed?: number
|
||||
gridSize?: number;
|
||||
cubeSize?: number;
|
||||
maxAngle?: number;
|
||||
radius?: number;
|
||||
easing?: gsap.EaseString;
|
||||
duration?: Duration;
|
||||
cellGap?: number | Gap;
|
||||
borderStyle?: string;
|
||||
faceColor?: string;
|
||||
shadow?: boolean | string;
|
||||
autoAnimate?: boolean;
|
||||
rippleOnClick?: boolean;
|
||||
rippleColor?: string;
|
||||
rippleSpeed?: number;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
@@ -91,37 +119,37 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
autoAnimate: true,
|
||||
rippleOnClick: true,
|
||||
rippleColor: '#fff',
|
||||
rippleSpeed: 2,
|
||||
})
|
||||
rippleSpeed: 2
|
||||
});
|
||||
|
||||
const sceneRef = ref<HTMLDivElement | null>(null)
|
||||
const rafRef = ref<number | null>(null)
|
||||
const idleTimerRef = ref<number | null>(null)
|
||||
const userActiveRef = ref(false)
|
||||
const simPosRef = ref<{ x: number; y: number }>({ x: 0, y: 0 })
|
||||
const simTargetRef = ref<{ x: number; y: number }>({ x: 0, y: 0 })
|
||||
const simRAFRef = ref<number | null>(null)
|
||||
const sceneRef = ref<HTMLDivElement | null>(null);
|
||||
const rafRef = ref<number | null>(null);
|
||||
const idleTimerRef = ref<number | null>(null);
|
||||
const userActiveRef = ref(false);
|
||||
const simPosRef = ref<{ x: number; y: number }>({ x: 0, y: 0 });
|
||||
const simTargetRef = ref<{ x: number; y: number }>({ x: 0, y: 0 });
|
||||
const simRAFRef = ref<number | null>(null);
|
||||
|
||||
const colGap = computed(() => {
|
||||
return typeof props.cellGap === 'number'
|
||||
? `${props.cellGap}px`
|
||||
: (props.cellGap as Gap)?.col !== undefined
|
||||
? `${(props.cellGap as Gap).col}px`
|
||||
: '5%'
|
||||
})
|
||||
: '5%';
|
||||
});
|
||||
|
||||
const rowGap = computed(() => {
|
||||
return typeof props.cellGap === 'number'
|
||||
? `${props.cellGap}px`
|
||||
: (props.cellGap as Gap)?.row !== undefined
|
||||
? `${(props.cellGap as Gap).row}px`
|
||||
: '5%'
|
||||
})
|
||||
: '5%';
|
||||
});
|
||||
|
||||
const enterDur = computed(() => props.duration.enter)
|
||||
const leaveDur = computed(() => props.duration.leave)
|
||||
const enterDur = computed(() => props.duration.enter);
|
||||
const leaveDur = computed(() => props.duration.leave);
|
||||
|
||||
const cells = computed(() => Array.from({ length: props.gridSize }))
|
||||
const cells = computed(() => Array.from({ length: props.gridSize }));
|
||||
|
||||
const sceneStyle = computed(() => ({
|
||||
gridTemplateColumns: props.cubeSize
|
||||
@@ -133,189 +161,184 @@ const sceneStyle = computed(() => ({
|
||||
columnGap: colGap.value,
|
||||
rowGap: rowGap.value,
|
||||
perspective: '99999999px',
|
||||
gridAutoRows: '1fr',
|
||||
}))
|
||||
gridAutoRows: '1fr'
|
||||
}));
|
||||
|
||||
const wrapperStyle = computed(() => ({
|
||||
'--cube-face-border': props.borderStyle,
|
||||
'--cube-face-bg': props.faceColor,
|
||||
'--cube-face-shadow':
|
||||
props.shadow === true ? '0 0 6px rgba(0,0,0,.5)' : props.shadow || 'none',
|
||||
'--cube-face-shadow': props.shadow === true ? '0 0 6px rgba(0,0,0,.5)' : props.shadow || 'none',
|
||||
...(props.cubeSize
|
||||
? {
|
||||
width: `${props.gridSize * props.cubeSize}px`,
|
||||
height: `${props.gridSize * props.cubeSize}px`,
|
||||
}
|
||||
: {}),
|
||||
}))
|
||||
width: `${props.gridSize * props.cubeSize}px`,
|
||||
height: `${props.gridSize * props.cubeSize}px`
|
||||
}
|
||||
: {})
|
||||
}));
|
||||
|
||||
const tiltAt = (rowCenter: number, colCenter: number) => {
|
||||
if (!sceneRef.value) return
|
||||
if (!sceneRef.value) return;
|
||||
|
||||
sceneRef.value.querySelectorAll<HTMLDivElement>('.cube').forEach((cube) => {
|
||||
const r = +(cube.dataset.row!)
|
||||
const c = +(cube.dataset.col!)
|
||||
const dist = Math.hypot(r - rowCenter, c - colCenter)
|
||||
sceneRef.value.querySelectorAll<HTMLDivElement>('.cube').forEach(cube => {
|
||||
const r = +cube.dataset.row!;
|
||||
const c = +cube.dataset.col!;
|
||||
const dist = Math.hypot(r - rowCenter, c - colCenter);
|
||||
|
||||
if (dist <= props.radius) {
|
||||
const pct = 1 - dist / props.radius
|
||||
const angle = pct * props.maxAngle
|
||||
const pct = 1 - dist / props.radius;
|
||||
const angle = pct * props.maxAngle;
|
||||
gsap.to(cube, {
|
||||
duration: enterDur.value,
|
||||
ease: props.easing,
|
||||
overwrite: true,
|
||||
rotateX: -angle,
|
||||
rotateY: angle,
|
||||
})
|
||||
rotateY: angle
|
||||
});
|
||||
} else {
|
||||
gsap.to(cube, {
|
||||
duration: leaveDur.value,
|
||||
ease: 'power3.out',
|
||||
overwrite: true,
|
||||
rotateX: 0,
|
||||
rotateY: 0,
|
||||
})
|
||||
rotateY: 0
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onPointerMove = (e: PointerEvent) => {
|
||||
userActiveRef.value = true
|
||||
if (idleTimerRef.value) clearTimeout(idleTimerRef.value)
|
||||
userActiveRef.value = true;
|
||||
if (idleTimerRef.value) clearTimeout(idleTimerRef.value);
|
||||
|
||||
const rect = sceneRef.value!.getBoundingClientRect()
|
||||
const cellW = rect.width / props.gridSize
|
||||
const cellH = rect.height / props.gridSize
|
||||
const colCenter = (e.clientX - rect.left) / cellW
|
||||
const rowCenter = (e.clientY - rect.top) / cellH
|
||||
const rect = sceneRef.value!.getBoundingClientRect();
|
||||
const cellW = rect.width / props.gridSize;
|
||||
const cellH = rect.height / props.gridSize;
|
||||
const colCenter = (e.clientX - rect.left) / cellW;
|
||||
const rowCenter = (e.clientY - rect.top) / cellH;
|
||||
|
||||
if (rafRef.value) cancelAnimationFrame(rafRef.value)
|
||||
rafRef.value = requestAnimationFrame(() =>
|
||||
tiltAt(rowCenter, colCenter)
|
||||
)
|
||||
if (rafRef.value) cancelAnimationFrame(rafRef.value);
|
||||
rafRef.value = requestAnimationFrame(() => tiltAt(rowCenter, colCenter));
|
||||
|
||||
idleTimerRef.value = setTimeout(() => {
|
||||
userActiveRef.value = false
|
||||
}, 3000)
|
||||
}
|
||||
userActiveRef.value = false;
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
const resetAll = () => {
|
||||
if (!sceneRef.value) return
|
||||
sceneRef.value.querySelectorAll<HTMLDivElement>('.cube').forEach((cube) =>
|
||||
if (!sceneRef.value) return;
|
||||
sceneRef.value.querySelectorAll<HTMLDivElement>('.cube').forEach(cube =>
|
||||
gsap.to(cube, {
|
||||
duration: leaveDur.value,
|
||||
rotateX: 0,
|
||||
rotateY: 0,
|
||||
ease: 'power3.out',
|
||||
ease: 'power3.out'
|
||||
})
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const onClick = (e: MouseEvent) => {
|
||||
if (!props.rippleOnClick || !sceneRef.value) return
|
||||
if (!props.rippleOnClick || !sceneRef.value) return;
|
||||
|
||||
const rect = sceneRef.value.getBoundingClientRect()
|
||||
const cellW = rect.width / props.gridSize
|
||||
const cellH = rect.height / props.gridSize
|
||||
const colHit = Math.floor((e.clientX - rect.left) / cellW)
|
||||
const rowHit = Math.floor((e.clientY - rect.top) / cellH)
|
||||
const rect = sceneRef.value.getBoundingClientRect();
|
||||
const cellW = rect.width / props.gridSize;
|
||||
const cellH = rect.height / props.gridSize;
|
||||
const colHit = Math.floor((e.clientX - rect.left) / cellW);
|
||||
const rowHit = Math.floor((e.clientY - rect.top) / cellH);
|
||||
|
||||
const baseRingDelay = 0.15
|
||||
const baseAnimDur = 0.3
|
||||
const baseHold = 0.6
|
||||
const baseRingDelay = 0.15;
|
||||
const baseAnimDur = 0.3;
|
||||
const baseHold = 0.6;
|
||||
|
||||
const spreadDelay = baseRingDelay / props.rippleSpeed
|
||||
const animDuration = baseAnimDur / props.rippleSpeed
|
||||
const holdTime = baseHold / props.rippleSpeed
|
||||
const spreadDelay = baseRingDelay / props.rippleSpeed;
|
||||
const animDuration = baseAnimDur / props.rippleSpeed;
|
||||
const holdTime = baseHold / props.rippleSpeed;
|
||||
|
||||
const rings: Record<number, HTMLDivElement[]> = {}
|
||||
sceneRef.value.querySelectorAll<HTMLDivElement>('.cube').forEach((cube) => {
|
||||
const r = +(cube.dataset.row!)
|
||||
const c = +(cube.dataset.col!)
|
||||
const dist = Math.hypot(r - rowHit, c - colHit)
|
||||
const ring = Math.round(dist)
|
||||
if (!rings[ring]) rings[ring] = []
|
||||
rings[ring].push(cube)
|
||||
})
|
||||
const rings: Record<number, HTMLDivElement[]> = {};
|
||||
sceneRef.value.querySelectorAll<HTMLDivElement>('.cube').forEach(cube => {
|
||||
const r = +cube.dataset.row!;
|
||||
const c = +cube.dataset.col!;
|
||||
const dist = Math.hypot(r - rowHit, c - colHit);
|
||||
const ring = Math.round(dist);
|
||||
if (!rings[ring]) rings[ring] = [];
|
||||
rings[ring].push(cube);
|
||||
});
|
||||
|
||||
Object.keys(rings)
|
||||
.map(Number)
|
||||
.sort((a, b) => a - b)
|
||||
.forEach((ring) => {
|
||||
const delay = ring * spreadDelay
|
||||
const faces = rings[ring].flatMap((cube) =>
|
||||
Array.from(cube.querySelectorAll<HTMLElement>('.cube-face'))
|
||||
)
|
||||
.forEach(ring => {
|
||||
const delay = ring * spreadDelay;
|
||||
const faces = rings[ring].flatMap(cube => Array.from(cube.querySelectorAll<HTMLElement>('.cube-face')));
|
||||
|
||||
gsap.to(faces, {
|
||||
backgroundColor: props.rippleColor,
|
||||
duration: animDuration,
|
||||
delay,
|
||||
ease: 'power3.out',
|
||||
})
|
||||
ease: 'power3.out'
|
||||
});
|
||||
gsap.to(faces, {
|
||||
backgroundColor: props.faceColor,
|
||||
duration: animDuration,
|
||||
delay: delay + animDuration + holdTime,
|
||||
ease: 'power3.out',
|
||||
})
|
||||
})
|
||||
}
|
||||
ease: 'power3.out'
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const startAutoAnimation = () => {
|
||||
if (!props.autoAnimate || !sceneRef.value) return
|
||||
if (!props.autoAnimate || !sceneRef.value) return;
|
||||
|
||||
simPosRef.value = {
|
||||
x: Math.random() * props.gridSize,
|
||||
y: Math.random() * props.gridSize,
|
||||
}
|
||||
y: Math.random() * props.gridSize
|
||||
};
|
||||
simTargetRef.value = {
|
||||
x: Math.random() * props.gridSize,
|
||||
y: Math.random() * props.gridSize,
|
||||
}
|
||||
y: Math.random() * props.gridSize
|
||||
};
|
||||
|
||||
const speed = 0.02
|
||||
const speed = 0.02;
|
||||
const loop = () => {
|
||||
if (!userActiveRef.value) {
|
||||
const pos = simPosRef.value
|
||||
const tgt = simTargetRef.value
|
||||
pos.x += (tgt.x - pos.x) * speed
|
||||
pos.y += (tgt.y - pos.y) * speed
|
||||
tiltAt(pos.y, pos.x)
|
||||
const pos = simPosRef.value;
|
||||
const tgt = simTargetRef.value;
|
||||
pos.x += (tgt.x - pos.x) * speed;
|
||||
pos.y += (tgt.y - pos.y) * speed;
|
||||
tiltAt(pos.y, pos.x);
|
||||
|
||||
if (Math.hypot(pos.x - tgt.x, pos.y - tgt.y) < 0.1) {
|
||||
simTargetRef.value = {
|
||||
x: Math.random() * props.gridSize,
|
||||
y: Math.random() * props.gridSize,
|
||||
}
|
||||
y: Math.random() * props.gridSize
|
||||
};
|
||||
}
|
||||
}
|
||||
simRAFRef.value = requestAnimationFrame(loop)
|
||||
}
|
||||
simRAFRef.value = requestAnimationFrame(loop)
|
||||
}
|
||||
simRAFRef.value = requestAnimationFrame(loop);
|
||||
};
|
||||
simRAFRef.value = requestAnimationFrame(loop);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
const el = sceneRef.value
|
||||
if (!el) return
|
||||
const el = sceneRef.value;
|
||||
if (!el) return;
|
||||
|
||||
el.addEventListener('pointermove', onPointerMove)
|
||||
el.addEventListener('pointerleave', resetAll)
|
||||
el.addEventListener('click', onClick)
|
||||
el.addEventListener('pointermove', onPointerMove);
|
||||
el.addEventListener('pointerleave', resetAll);
|
||||
el.addEventListener('click', onClick);
|
||||
|
||||
startAutoAnimation()
|
||||
})
|
||||
startAutoAnimation();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
const el = sceneRef.value
|
||||
const el = sceneRef.value;
|
||||
if (el) {
|
||||
el.removeEventListener('pointermove', onPointerMove)
|
||||
el.removeEventListener('pointerleave', resetAll)
|
||||
el.removeEventListener('click', onClick)
|
||||
el.removeEventListener('pointermove', onPointerMove);
|
||||
el.removeEventListener('pointerleave', resetAll);
|
||||
el.removeEventListener('click', onClick);
|
||||
}
|
||||
|
||||
if (rafRef.value !== null) cancelAnimationFrame(rafRef.value)
|
||||
if (idleTimerRef.value !== null) clearTimeout(idleTimerRef.value)
|
||||
if (simRAFRef.value !== null) cancelAnimationFrame(simRAFRef.value)
|
||||
})
|
||||
if (rafRef.value !== null) cancelAnimationFrame(rafRef.value);
|
||||
if (idleTimerRef.value !== null) clearTimeout(idleTimerRef.value);
|
||||
if (simRAFRef.value !== null) cancelAnimationFrame(simRAFRef.value);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
:style="{
|
||||
opacity: inView ? 1 : initialOpacity,
|
||||
transition: `opacity ${duration}ms ${easing}, filter ${duration}ms ${easing}`,
|
||||
filter: blur ? (inView ? 'blur(0px)' : 'blur(10px)') : 'none',
|
||||
filter: blur ? (inView ? 'blur(0px)' : 'blur(10px)') : 'none'
|
||||
}"
|
||||
>
|
||||
<slot />
|
||||
@@ -13,16 +13,16 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { ref, onMounted, onUnmounted } from 'vue';
|
||||
|
||||
interface Props {
|
||||
blur?: boolean
|
||||
duration?: number
|
||||
easing?: string
|
||||
delay?: number
|
||||
threshold?: number
|
||||
initialOpacity?: number
|
||||
className?: string
|
||||
blur?: boolean;
|
||||
duration?: number;
|
||||
easing?: string;
|
||||
delay?: number;
|
||||
threshold?: number;
|
||||
initialOpacity?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
@@ -33,34 +33,34 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
threshold: 0.1,
|
||||
initialOpacity: 0,
|
||||
className: ''
|
||||
})
|
||||
});
|
||||
|
||||
const inView = ref(false)
|
||||
const elementRef = ref<HTMLDivElement | null>(null)
|
||||
let observer: IntersectionObserver | null = null
|
||||
const inView = ref(false);
|
||||
const elementRef = ref<HTMLDivElement | null>(null);
|
||||
let observer: IntersectionObserver | null = null;
|
||||
|
||||
onMounted(() => {
|
||||
const element = elementRef.value
|
||||
if (!element) return
|
||||
const element = elementRef.value;
|
||||
if (!element) return;
|
||||
|
||||
observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
observer?.unobserve(element)
|
||||
observer?.unobserve(element);
|
||||
setTimeout(() => {
|
||||
inView.value = true
|
||||
}, props.delay)
|
||||
inView.value = true;
|
||||
}, props.delay);
|
||||
}
|
||||
},
|
||||
{ threshold: props.threshold }
|
||||
)
|
||||
);
|
||||
|
||||
observer.observe(element)
|
||||
})
|
||||
observer.observe(element);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (observer) {
|
||||
observer.disconnect()
|
||||
observer.disconnect();
|
||||
}
|
||||
})
|
||||
</script>
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, computed } from 'vue';
|
||||
|
||||
interface GlareHoverProps {
|
||||
width?: string
|
||||
height?: string
|
||||
background?: string
|
||||
borderRadius?: string
|
||||
borderColor?: string
|
||||
glareColor?: string
|
||||
glareOpacity?: number
|
||||
glareAngle?: number
|
||||
glareSize?: number
|
||||
transitionDuration?: number
|
||||
playOnce?: boolean
|
||||
className?: string
|
||||
style?: Record<string, string | number>
|
||||
width?: string;
|
||||
height?: string;
|
||||
background?: string;
|
||||
borderRadius?: string;
|
||||
borderColor?: string;
|
||||
glareColor?: string;
|
||||
glareOpacity?: number;
|
||||
glareAngle?: number;
|
||||
glareSize?: number;
|
||||
transitionDuration?: number;
|
||||
playOnce?: boolean;
|
||||
className?: string;
|
||||
style?: Record<string, string | number>;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<GlareHoverProps>(), {
|
||||
@@ -31,28 +31,28 @@ const props = withDefaults(defineProps<GlareHoverProps>(), {
|
||||
playOnce: false,
|
||||
className: '',
|
||||
style: () => ({})
|
||||
})
|
||||
});
|
||||
|
||||
const overlayRef = ref<HTMLDivElement | null>(null)
|
||||
const overlayRef = ref<HTMLDivElement | null>(null);
|
||||
|
||||
const rgba = computed(() => {
|
||||
const hex = props.glareColor.replace('#', '')
|
||||
let result = props.glareColor
|
||||
|
||||
const hex = props.glareColor.replace('#', '');
|
||||
let result = props.glareColor;
|
||||
|
||||
if (/^[\dA-Fa-f]{6}$/.test(hex)) {
|
||||
const r = parseInt(hex.slice(0, 2), 16)
|
||||
const g = parseInt(hex.slice(2, 4), 16)
|
||||
const b = parseInt(hex.slice(4, 6), 16)
|
||||
result = `rgba(${r}, ${g}, ${b}, ${props.glareOpacity})`
|
||||
const r = parseInt(hex.slice(0, 2), 16);
|
||||
const g = parseInt(hex.slice(2, 4), 16);
|
||||
const b = parseInt(hex.slice(4, 6), 16);
|
||||
result = `rgba(${r}, ${g}, ${b}, ${props.glareOpacity})`;
|
||||
} else if (/^[\dA-Fa-f]{3}$/.test(hex)) {
|
||||
const r = parseInt(hex[0] + hex[0], 16)
|
||||
const g = parseInt(hex[1] + hex[1], 16)
|
||||
const b = parseInt(hex[2] + hex[2], 16)
|
||||
result = `rgba(${r}, ${g}, ${b}, ${props.glareOpacity})`
|
||||
const r = parseInt(hex[0] + hex[0], 16);
|
||||
const g = parseInt(hex[1] + hex[1], 16);
|
||||
const b = parseInt(hex[2] + hex[2], 16);
|
||||
result = `rgba(${r}, ${g}, ${b}, ${props.glareOpacity})`;
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
const overlayStyle = computed(() => ({
|
||||
position: 'absolute' as const,
|
||||
@@ -64,32 +64,32 @@ const overlayStyle = computed(() => ({
|
||||
backgroundSize: `${props.glareSize}% ${props.glareSize}%, 100% 100%`,
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundPosition: '-100% -100%, 0 0',
|
||||
pointerEvents: 'none' as const,
|
||||
}))
|
||||
pointerEvents: 'none' as const
|
||||
}));
|
||||
|
||||
const animateIn = () => {
|
||||
const el = overlayRef.value
|
||||
if (!el) return
|
||||
const el = overlayRef.value;
|
||||
if (!el) return;
|
||||
|
||||
el.style.transition = 'none'
|
||||
el.style.backgroundPosition = '-100% -100%, 0 0'
|
||||
void el.offsetHeight
|
||||
el.style.transition = `${props.transitionDuration}ms ease`
|
||||
el.style.backgroundPosition = '100% 100%, 0 0'
|
||||
}
|
||||
el.style.transition = 'none';
|
||||
el.style.backgroundPosition = '-100% -100%, 0 0';
|
||||
void el.offsetHeight;
|
||||
el.style.transition = `${props.transitionDuration}ms ease`;
|
||||
el.style.backgroundPosition = '100% 100%, 0 0';
|
||||
};
|
||||
|
||||
const animateOut = () => {
|
||||
const el = overlayRef.value
|
||||
if (!el) return
|
||||
const el = overlayRef.value;
|
||||
if (!el) return;
|
||||
|
||||
if (props.playOnce) {
|
||||
el.style.transition = 'none'
|
||||
el.style.backgroundPosition = '-100% -100%, 0 0'
|
||||
el.style.transition = 'none';
|
||||
el.style.backgroundPosition = '-100% -100%, 0 0';
|
||||
} else {
|
||||
el.style.transition = `${props.transitionDuration}ms ease`
|
||||
el.style.backgroundPosition = '-100% -100%, 0 0'
|
||||
el.style.transition = `${props.transitionDuration}ms ease`;
|
||||
el.style.backgroundPosition = '-100% -100%, 0 0';
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -101,12 +101,13 @@ const animateOut = () => {
|
||||
background: props.background,
|
||||
borderRadius: props.borderRadius,
|
||||
borderColor: props.borderColor,
|
||||
...props.style,
|
||||
...props.style
|
||||
}"
|
||||
@mouseenter="animateIn"
|
||||
@mouseleave="animateOut"
|
||||
>
|
||||
<div ref="overlayRef" :style="overlayStyle" />
|
||||
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,27 +1,34 @@
|
||||
<template>
|
||||
<div ref="magnetRef" :class="wrapperClassName" :style="{ position: 'relative', display: 'inline-block' }"
|
||||
v-bind="$attrs">
|
||||
<div :class="innerClassName" :style="{
|
||||
transform: `translate3d(${position.x}px, ${position.y}px, 0)`,
|
||||
transition: transitionStyle,
|
||||
willChange: 'transform',
|
||||
}">
|
||||
<div
|
||||
ref="magnetRef"
|
||||
:class="wrapperClassName"
|
||||
:style="{ position: 'relative', display: 'inline-block' }"
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<div
|
||||
:class="innerClassName"
|
||||
:style="{
|
||||
transform: `translate3d(${position.x}px, ${position.y}px, 0)`,
|
||||
transition: transitionStyle,
|
||||
willChange: 'transform'
|
||||
}"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
|
||||
|
||||
interface Props {
|
||||
padding?: number
|
||||
disabled?: boolean
|
||||
magnetStrength?: number
|
||||
activeTransition?: string
|
||||
inactiveTransition?: string
|
||||
wrapperClassName?: string
|
||||
innerClassName?: string
|
||||
padding?: number;
|
||||
disabled?: boolean;
|
||||
magnetStrength?: number;
|
||||
activeTransition?: string;
|
||||
inactiveTransition?: string;
|
||||
wrapperClassName?: string;
|
||||
innerClassName?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
@@ -32,53 +39,54 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
inactiveTransition: 'transform 0.5s ease-in-out',
|
||||
wrapperClassName: '',
|
||||
innerClassName: ''
|
||||
})
|
||||
});
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false
|
||||
})
|
||||
});
|
||||
|
||||
const magnetRef = ref<HTMLDivElement | null>(null)
|
||||
const isActive = ref(false)
|
||||
const position = ref({ x: 0, y: 0 })
|
||||
const magnetRef = ref<HTMLDivElement | null>(null);
|
||||
const isActive = ref(false);
|
||||
const position = ref({ x: 0, y: 0 });
|
||||
|
||||
const transitionStyle = computed(() =>
|
||||
isActive.value ? props.activeTransition : props.inactiveTransition
|
||||
)
|
||||
const transitionStyle = computed(() => (isActive.value ? props.activeTransition : props.inactiveTransition));
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (!magnetRef.value || props.disabled) return
|
||||
if (!magnetRef.value || props.disabled) return;
|
||||
|
||||
const { left, top, width, height } = magnetRef.value.getBoundingClientRect()
|
||||
const centerX = left + width / 2
|
||||
const centerY = top + height / 2
|
||||
const { left, top, width, height } = magnetRef.value.getBoundingClientRect();
|
||||
const centerX = left + width / 2;
|
||||
const centerY = top + height / 2;
|
||||
|
||||
const distX = Math.abs(centerX - e.clientX)
|
||||
const distY = Math.abs(centerY - e.clientY)
|
||||
const distX = Math.abs(centerX - e.clientX);
|
||||
const distY = Math.abs(centerY - e.clientY);
|
||||
|
||||
if (distX < width / 2 + props.padding && distY < height / 2 + props.padding) {
|
||||
isActive.value = true
|
||||
const offsetX = (e.clientX - centerX) / props.magnetStrength
|
||||
const offsetY = (e.clientY - centerY) / props.magnetStrength
|
||||
position.value = { x: offsetX, y: offsetY }
|
||||
isActive.value = true;
|
||||
const offsetX = (e.clientX - centerX) / props.magnetStrength;
|
||||
const offsetY = (e.clientY - centerY) / props.magnetStrength;
|
||||
position.value = { x: offsetX, y: offsetY };
|
||||
} else {
|
||||
isActive.value = false
|
||||
position.value = { x: 0, y: 0 }
|
||||
isActive.value = false;
|
||||
position.value = { x: 0, y: 0 };
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('mousemove', handleMouseMove)
|
||||
})
|
||||
window.addEventListener('mousemove', handleMouseMove);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('mousemove', handleMouseMove)
|
||||
})
|
||||
window.removeEventListener('mousemove', handleMouseMove);
|
||||
});
|
||||
|
||||
watch(() => props.disabled, (newDisabled) => {
|
||||
if (newDisabled) {
|
||||
position.value = { x: 0, y: 0 }
|
||||
isActive.value = false
|
||||
watch(
|
||||
() => props.disabled,
|
||||
newDisabled => {
|
||||
if (newDisabled) {
|
||||
position.value = { x: 0, y: 0 };
|
||||
isActive.value = false;
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, computed } from 'vue'
|
||||
import { ref, onMounted, onUnmounted, computed } from 'vue';
|
||||
|
||||
interface MagnetLinesProps {
|
||||
rows?: number
|
||||
columns?: number
|
||||
containerSize?: string
|
||||
lineColor?: string
|
||||
lineWidth?: string
|
||||
lineHeight?: string
|
||||
baseAngle?: number
|
||||
className?: string
|
||||
style?: Record<string, string | number>
|
||||
rows?: number;
|
||||
columns?: number;
|
||||
containerSize?: string;
|
||||
lineColor?: string;
|
||||
lineWidth?: string;
|
||||
lineHeight?: string;
|
||||
baseAngle?: number;
|
||||
className?: string;
|
||||
style?: Record<string, string | number>;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<MagnetLinesProps>(), {
|
||||
@@ -23,53 +23,53 @@ const props = withDefaults(defineProps<MagnetLinesProps>(), {
|
||||
baseAngle: -10,
|
||||
className: '',
|
||||
style: () => ({})
|
||||
})
|
||||
});
|
||||
|
||||
const containerRef = ref<HTMLDivElement | null>(null)
|
||||
const containerRef = ref<HTMLDivElement | null>(null);
|
||||
|
||||
const total = computed(() => props.rows * props.columns)
|
||||
const total = computed(() => props.rows * props.columns);
|
||||
|
||||
const onPointerMove = (pointer: { x: number; y: number }) => {
|
||||
const container = containerRef.value
|
||||
if (!container) return
|
||||
const container = containerRef.value;
|
||||
if (!container) return;
|
||||
|
||||
const items = container.querySelectorAll<HTMLSpanElement>('span')
|
||||
const items = container.querySelectorAll<HTMLSpanElement>('span');
|
||||
|
||||
items.forEach((item) => {
|
||||
const rect = item.getBoundingClientRect()
|
||||
const centerX = rect.x + rect.width / 2
|
||||
const centerY = rect.y + rect.height / 2
|
||||
items.forEach(item => {
|
||||
const rect = item.getBoundingClientRect();
|
||||
const centerX = rect.x + rect.width / 2;
|
||||
const centerY = rect.y + rect.height / 2;
|
||||
|
||||
const b = pointer.x - centerX
|
||||
const a = pointer.y - centerY
|
||||
const c = Math.sqrt(a * a + b * b) || 1
|
||||
const r = ((Math.acos(b / c) * 180) / Math.PI) * (pointer.y > centerY ? 1 : -1)
|
||||
const b = pointer.x - centerX;
|
||||
const a = pointer.y - centerY;
|
||||
const c = Math.sqrt(a * a + b * b) || 1;
|
||||
const r = ((Math.acos(b / c) * 180) / Math.PI) * (pointer.y > centerY ? 1 : -1);
|
||||
|
||||
item.style.setProperty('--rotate', `${r}deg`)
|
||||
})
|
||||
}
|
||||
item.style.setProperty('--rotate', `${r}deg`);
|
||||
});
|
||||
};
|
||||
|
||||
const handlePointerMove = (e: PointerEvent) => {
|
||||
onPointerMove({ x: e.x, y: e.y })
|
||||
}
|
||||
onPointerMove({ x: e.x, y: e.y });
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
const container = containerRef.value
|
||||
if (!container) return
|
||||
const container = containerRef.value;
|
||||
if (!container) return;
|
||||
|
||||
window.addEventListener('pointermove', handlePointerMove)
|
||||
window.addEventListener('pointermove', handlePointerMove);
|
||||
|
||||
const items = container.querySelectorAll<HTMLSpanElement>('span')
|
||||
const items = container.querySelectorAll<HTMLSpanElement>('span');
|
||||
if (items.length) {
|
||||
const middleIndex = Math.floor(items.length / 2)
|
||||
const rect = items[middleIndex].getBoundingClientRect()
|
||||
onPointerMove({ x: rect.x, y: rect.y })
|
||||
const middleIndex = Math.floor(items.length / 2);
|
||||
const rect = items[middleIndex].getBoundingClientRect();
|
||||
onPointerMove({ x: rect.x, y: rect.y });
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('pointermove', handlePointerMove)
|
||||
})
|
||||
window.removeEventListener('pointermove', handlePointerMove);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -81,7 +81,7 @@ onUnmounted(() => {
|
||||
gridTemplateRows: `repeat(${props.rows}, 1fr)`,
|
||||
width: props.containerSize,
|
||||
height: props.containerSize,
|
||||
...props.style,
|
||||
...props.style
|
||||
}"
|
||||
>
|
||||
<span
|
||||
@@ -94,7 +94,7 @@ onUnmounted(() => {
|
||||
height: props.lineHeight,
|
||||
'--rotate': `${props.baseAngle}deg`,
|
||||
transform: 'rotate(var(--rotate))',
|
||||
willChange: 'transform',
|
||||
willChange: 'transform'
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch, onUnmounted, nextTick } from 'vue'
|
||||
import { gsap } from 'gsap'
|
||||
import { ref, onMounted, watch, onUnmounted, nextTick } from 'vue';
|
||||
import { gsap } from 'gsap';
|
||||
|
||||
interface PixelTransitionProps {
|
||||
gridSize?: number
|
||||
pixelColor?: string
|
||||
animationStepDuration?: number
|
||||
className?: string
|
||||
style?: Record<string, string | number>
|
||||
aspectRatio?: string
|
||||
gridSize?: number;
|
||||
pixelColor?: string;
|
||||
animationStepDuration?: number;
|
||||
className?: string;
|
||||
style?: Record<string, string | number>;
|
||||
aspectRatio?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<PixelTransitionProps>(), {
|
||||
@@ -18,114 +18,127 @@ const props = withDefaults(defineProps<PixelTransitionProps>(), {
|
||||
className: '',
|
||||
style: () => ({}),
|
||||
aspectRatio: '100%'
|
||||
})
|
||||
});
|
||||
|
||||
const containerRef = ref<HTMLDivElement | null>(null)
|
||||
const pixelGridRef = ref<HTMLDivElement | null>(null)
|
||||
const activeRef = ref<HTMLDivElement | null>(null)
|
||||
const isActive = ref(false)
|
||||
let delayedCall: gsap.core.Tween | null = null
|
||||
const containerRef = ref<HTMLDivElement | null>(null);
|
||||
const pixelGridRef = ref<HTMLDivElement | null>(null);
|
||||
const activeRef = ref<HTMLDivElement | null>(null);
|
||||
const isActive = ref(false);
|
||||
let delayedCall: gsap.core.Tween | null = null;
|
||||
|
||||
const isTouchDevice =
|
||||
typeof window !== 'undefined' &&
|
||||
('ontouchstart' in window ||
|
||||
(navigator && navigator.maxTouchPoints > 0) ||
|
||||
(window.matchMedia && window.matchMedia('(pointer: coarse)').matches))
|
||||
(window.matchMedia && window.matchMedia('(pointer: coarse)').matches));
|
||||
|
||||
function buildPixelGrid() {
|
||||
const pixelGridEl = pixelGridRef.value
|
||||
if (!pixelGridEl) return
|
||||
pixelGridEl.innerHTML = ''
|
||||
const pixelGridEl = pixelGridRef.value;
|
||||
if (!pixelGridEl) return;
|
||||
pixelGridEl.innerHTML = '';
|
||||
for (let row = 0; row < props.gridSize; row++) {
|
||||
for (let col = 0; col < props.gridSize; col++) {
|
||||
const pixel = document.createElement('div')
|
||||
pixel.classList.add('pixelated-image-card__pixel', 'absolute', 'hidden')
|
||||
pixel.style.backgroundColor = props.pixelColor
|
||||
const size = 100 / props.gridSize
|
||||
pixel.style.width = `${size}%`
|
||||
pixel.style.height = `${size}%`
|
||||
pixel.style.left = `${col * size}%`
|
||||
pixel.style.top = `${row * size}%`
|
||||
pixelGridEl.appendChild(pixel)
|
||||
const pixel = document.createElement('div');
|
||||
pixel.classList.add('pixelated-image-card__pixel', 'absolute', 'hidden');
|
||||
pixel.style.backgroundColor = props.pixelColor;
|
||||
const size = 100 / props.gridSize;
|
||||
pixel.style.width = `${size}%`;
|
||||
pixel.style.height = `${size}%`;
|
||||
pixel.style.left = `${col * size}%`;
|
||||
pixel.style.top = `${row * size}%`;
|
||||
pixelGridEl.appendChild(pixel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function animatePixels(activate: boolean) {
|
||||
isActive.value = activate
|
||||
await nextTick()
|
||||
const pixelGridEl = pixelGridRef.value
|
||||
const activeEl = activeRef.value
|
||||
if (!pixelGridEl || !activeEl) return
|
||||
const pixels = pixelGridEl.querySelectorAll<HTMLDivElement>('.pixelated-image-card__pixel')
|
||||
if (!pixels.length) return
|
||||
gsap.killTweensOf(pixels)
|
||||
if (delayedCall) delayedCall.kill()
|
||||
gsap.set(pixels, { display: 'none' })
|
||||
const totalPixels = pixels.length
|
||||
const staggerDuration = props.animationStepDuration / totalPixels
|
||||
isActive.value = activate;
|
||||
await nextTick();
|
||||
const pixelGridEl = pixelGridRef.value;
|
||||
const activeEl = activeRef.value;
|
||||
if (!pixelGridEl || !activeEl) return;
|
||||
const pixels = pixelGridEl.querySelectorAll<HTMLDivElement>('.pixelated-image-card__pixel');
|
||||
if (!pixels.length) return;
|
||||
gsap.killTweensOf(pixels);
|
||||
if (delayedCall) delayedCall.kill();
|
||||
gsap.set(pixels, { display: 'none' });
|
||||
const totalPixels = pixels.length;
|
||||
const staggerDuration = props.animationStepDuration / totalPixels;
|
||||
gsap.to(pixels, {
|
||||
display: 'block',
|
||||
duration: 0,
|
||||
stagger: {
|
||||
each: staggerDuration,
|
||||
from: 'random',
|
||||
},
|
||||
})
|
||||
from: 'random'
|
||||
}
|
||||
});
|
||||
delayedCall = gsap.delayedCall(props.animationStepDuration, () => {
|
||||
activeEl.style.display = activate ? 'block' : 'none'
|
||||
activeEl.style.pointerEvents = activate ? 'none' : ''
|
||||
})
|
||||
activeEl.style.display = activate ? 'block' : 'none';
|
||||
activeEl.style.pointerEvents = activate ? 'none' : '';
|
||||
});
|
||||
gsap.to(pixels, {
|
||||
display: 'none',
|
||||
duration: 0,
|
||||
delay: props.animationStepDuration,
|
||||
stagger: {
|
||||
each: staggerDuration,
|
||||
from: 'random',
|
||||
},
|
||||
})
|
||||
from: 'random'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleMouseEnter() {
|
||||
if (isTouchDevice) return
|
||||
if (!isActive.value) animatePixels(true)
|
||||
if (isTouchDevice) return;
|
||||
if (!isActive.value) animatePixels(true);
|
||||
}
|
||||
function handleMouseLeave() {
|
||||
if (isTouchDevice) return
|
||||
if (isActive.value) animatePixels(false)
|
||||
if (isTouchDevice) return;
|
||||
if (isActive.value) animatePixels(false);
|
||||
}
|
||||
function handleClick() {
|
||||
if (!isTouchDevice) return
|
||||
animatePixels(!isActive.value)
|
||||
if (!isTouchDevice) return;
|
||||
animatePixels(!isActive.value);
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await nextTick()
|
||||
buildPixelGrid()
|
||||
})
|
||||
await nextTick();
|
||||
buildPixelGrid();
|
||||
});
|
||||
|
||||
watch(() => [props.gridSize, props.pixelColor], () => {
|
||||
buildPixelGrid()
|
||||
})
|
||||
watch(
|
||||
() => [props.gridSize, props.pixelColor],
|
||||
() => {
|
||||
buildPixelGrid();
|
||||
}
|
||||
);
|
||||
|
||||
onUnmounted(() => {
|
||||
if (delayedCall) delayedCall.kill()
|
||||
})
|
||||
if (delayedCall) delayedCall.kill();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="containerRef" :class="[
|
||||
props.className,
|
||||
'bg-[#222] text-white rounded-[15px] border-2 border-white w-[300px] max-w-full relative overflow-hidden'
|
||||
]" :style="props.style" @mouseenter="handleMouseEnter" @mouseleave="handleMouseLeave" @click="handleClick">
|
||||
<div
|
||||
ref="containerRef"
|
||||
:class="[
|
||||
props.className,
|
||||
'bg-[#222] text-white rounded-[15px] border-2 border-white w-[300px] max-w-full relative overflow-hidden'
|
||||
]"
|
||||
:style="props.style"
|
||||
@mouseenter="handleMouseEnter"
|
||||
@mouseleave="handleMouseLeave"
|
||||
@click="handleClick"
|
||||
>
|
||||
<div :style="{ paddingTop: props.aspectRatio }" />
|
||||
|
||||
<div class="absolute inset-0 w-full h-full">
|
||||
<slot name="firstContent" />
|
||||
</div>
|
||||
<div ref="activeRef" class="absolute inset-0 w-full h-full z-[2]" style="display: none;">
|
||||
|
||||
<div ref="activeRef" class="absolute inset-0 w-full h-full z-[2]" style="display: none">
|
||||
<slot name="secondContent" />
|
||||
</div>
|
||||
|
||||
<div ref="pixelGridRef" class="absolute inset-0 w-full h-full pointer-events-none z-[3]" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user