Add prettier config, format codebase

This commit is contained in:
David Haz
2025-07-12 11:59:33 +03:00
parent ac8b2c04d8
commit f4d97ee94e
211 changed files with 10586 additions and 8810 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>