ソースを参照

feat: add glass pane component

Thomas Zhang 3 ヶ月 前
コミット
d4b3115c98

+ 3 - 0
src/lib/components/common/glass-pane/index.ts

@@ -0,0 +1,3 @@
+import GlassPane from './pane.svelte';
+
+export { GlassPane };

+ 87 - 0
src/lib/components/common/glass-pane/pane.svelte

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