|
|
@@ -0,0 +1,87 @@
|
|
|
+<script lang="ts">
|
|
|
+ import type { Snippet } from 'svelte';
|
|
|
+ import type { ClassValue } from 'svelte/elements';
|
|
|
+
|
|
|
+ import { cn } from '$lib/utils';
|
|
|
+
|
|
|
+ const {
|
|
|
+ class: className = '',
|
|
|
+ reactive = false,
|
|
|
+ children,
|
|
|
+ }: {
|
|
|
+ class?: ClassValue;
|
|
|
+ reactive?: boolean;
|
|
|
+ children?: Snippet;
|
|
|
+ } = $props();
|
|
|
+
|
|
|
+ let el = $state<HTMLDivElement | undefined>(undefined);
|
|
|
+ let glowX = $state(0);
|
|
|
+ let glowY = $state(0);
|
|
|
+ let glowVisible = $state(false);
|
|
|
+ let rotateX = $state(0);
|
|
|
+ let rotateY = $state(0);
|
|
|
+ let returning = $state(false);
|
|
|
+
|
|
|
+ const onmousemove = (e: MouseEvent) => {
|
|
|
+ if (!el || !reactive) return;
|
|
|
+ const rect = el.getBoundingClientRect();
|
|
|
+ const x = e.clientX - rect.left;
|
|
|
+ const y = e.clientY - rect.top;
|
|
|
+
|
|
|
+ glowX = x;
|
|
|
+ glowY = y;
|
|
|
+ glowVisible = true;
|
|
|
+ returning = false;
|
|
|
+
|
|
|
+ // Normalised -0.5 … 0.5 from card centre
|
|
|
+ const nx = x / rect.width - 0.5;
|
|
|
+ const ny = y / rect.height - 0.5;
|
|
|
+
|
|
|
+ rotateX = -ny * 12;
|
|
|
+ rotateY = nx * 12;
|
|
|
+ };
|
|
|
+
|
|
|
+ const onmouseleave = () => {
|
|
|
+ glowVisible = false;
|
|
|
+ returning = true;
|
|
|
+ rotateX = 0;
|
|
|
+ rotateY = 0;
|
|
|
+ };
|
|
|
+</script>
|
|
|
+
|
|
|
+<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
|
+<div
|
|
|
+ bind:this={el}
|
|
|
+ style="transform: perspective(800px) rotateX({rotateX}deg) rotateY({rotateY}deg);
|
|
|
+ transition: transform {returning ? '500ms' : '0ms'} ease-out;
|
|
|
+ will-change: transform;"
|
|
|
+ class={cn(
|
|
|
+ 'relative rounded-lg border-2 border-white/10 bg-white/5 backdrop-blur-lg backdrop-brightness-90',
|
|
|
+ className,
|
|
|
+ )}
|
|
|
+ {onmousemove}
|
|
|
+ {onmouseleave}
|
|
|
+>
|
|
|
+ <!-- noise overlay -->
|
|
|
+ <div
|
|
|
+ class="noise pointer-events-none absolute inset-0 rounded-[inherit] opacity-[0.08] mix-blend-overlay"
|
|
|
+ ></div>
|
|
|
+
|
|
|
+ <!-- pointer glow -->
|
|
|
+ {#if reactive}
|
|
|
+ <div
|
|
|
+ style="background: radial-gradient(200px circle at {glowX}px {glowY}px, rgba(255,255,255,0.05), transparent 100%);"
|
|
|
+ class="pointer-events-none absolute inset-0 rounded-[inherit] opacity-0 transition-opacity duration-200 ease-in-out"
|
|
|
+ class:opacity-100={glowVisible}
|
|
|
+ ></div>
|
|
|
+ {/if}
|
|
|
+
|
|
|
+ {@render children?.()}
|
|
|
+</div>
|
|
|
+
|
|
|
+<style>
|
|
|
+ .noise {
|
|
|
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='300'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='300' height='300' filter='url(%23n)'/%3E%3C/svg%3E");
|
|
|
+ background-size: 300px 300px;
|
|
|
+ }
|
|
|
+</style>
|