feat(ui): Cursor Floating (#132)

Co-authored-by: Neko <neko@ayaka.moe>
This commit is contained in:
RainbowBird
2025-04-14 14:57:12 +08:00
committed by GitHub
co-authored by Neko
parent b3dd6d5b39
commit d33d0dba2c
7 changed files with 233 additions and 18 deletions
@@ -1,4 +1,6 @@
<script setup lang="ts">
import { CursorFloating } from '@proj-airi/stage-ui/components'
interface Props {
id: string
name: string
@@ -19,7 +21,7 @@ const emit = defineEmits<{
</script>
<template>
<div
<CursorFloating
relative min-h-120px flex="~ col" cursor-pointer overflow-hidden rounded-xl
:class="[
isSelected
@@ -38,11 +40,11 @@ const emit = defineEmits<{
relative flex="~ col 1" justify-between gap-3 overflow-hidden rounded-lg bg="white dark:neutral-900" p-5
transition="all ease-in-out duration-400"
after="content-empty absolute inset-0 z--2 w-full h-full bg-dotted-[neutral-200/80] bg-size-10px mask-image-[linear-gradient(165deg,white_30%,transparent_50%)] transition-all duration-400 ease-in-out"
hover="after:bg-dotted-[primary-300/50] dark:after:bg-dotted-[primary-200/20]"
hover="after:bg-dotted-[primary-300/50] dark:after:bg-dotted-[primary-200/20] text-primary-600/80 dark:text-primary-300/80"
>
<!-- Card header (name and badge) -->
<div z-1 flex items-start justify-between gap-2>
<h3 flex-1 truncate text-lg font-bold hover="text-primary-600 dark:text-primary-300">
<h3 flex-1 truncate text-lg font-bold>
{{ name }}
</h3>
<div v-if="isActive" shrink-0 rounded-md p-1 bg="primary-100 dark:primary-900/40" text="primary-600 dark:primary-400">
@@ -51,7 +53,7 @@ const emit = defineEmits<{
</div>
<!-- Card description -->
<p v-if="description" line-clamp-3 min-h-40px flex-1 text-sm text="neutral-500 dark:neutral-400" hover="text-primary-600/80 dark:text-primary-300/80">
<p v-if="description" line-clamp-3 min-h-40px flex-1 text-sm text="neutral-500 dark:neutral-400">
{{ description }}
</p>
@@ -95,5 +97,5 @@ const emit = defineEmits<{
<div i-solar:trash-bin-trash-linear text="neutral-500 dark:neutral-400" />
</button>
</div>
</div>
</CursorFloating>
</template>
@@ -1,4 +1,6 @@
<script setup lang="ts">
import { CursorFloating } from '@proj-airi/stage-ui/components'
interface Props {
id: string
name: string
@@ -19,7 +21,7 @@ const emit = defineEmits<{
</script>
<template>
<div
<CursorFloating
relative min-h-120px flex="~ col" cursor-pointer overflow-hidden rounded-xl
:class="[
isSelected
@@ -38,11 +40,11 @@ const emit = defineEmits<{
relative flex="~ col 1" justify-between gap-3 overflow-hidden rounded-lg bg="white dark:neutral-900" p-5
transition="all ease-in-out duration-400"
after="content-empty absolute inset-0 z--2 w-full h-full bg-dotted-[neutral-200/80] bg-size-10px mask-image-[linear-gradient(165deg,white_30%,transparent_50%)] transition-all duration-400 ease-in-out"
hover="after:bg-dotted-[primary-300/50] dark:after:bg-dotted-[primary-200/20]"
hover="after:bg-dotted-[primary-300/50] dark:after:bg-dotted-[primary-200/20] text-primary-600/80 dark:text-primary-300/80"
>
<!-- Card header (name and badge) -->
<div z-1 flex items-start justify-between gap-2>
<h3 flex-1 truncate text-lg font-bold hover="text-primary-600 dark:text-primary-300">
<h3 flex-1 truncate text-lg font-bold>
{{ name }}
</h3>
<div v-if="isActive" shrink-0 rounded-md p-1 bg="primary-100 dark:primary-900/40" text="primary-600 dark:primary-400">
@@ -51,7 +53,7 @@ const emit = defineEmits<{
</div>
<!-- Card description -->
<p v-if="description" line-clamp-3 min-h-40px flex-1 text-sm text="neutral-500 dark:neutral-400" hover="text-primary-600/80 dark:text-primary-300/80">
<p v-if="description" line-clamp-3 min-h-40px flex-1 text-sm text="neutral-500 dark:neutral-400">
{{ description }}
</p>
@@ -95,5 +97,5 @@ const emit = defineEmits<{
<div i-solar:trash-bin-trash-linear text="neutral-500 dark:neutral-400" />
</button>
</div>
</div>
</CursorFloating>
</template>
+1
View File
@@ -69,6 +69,7 @@ words:
- higress
- histoire
- hiyori
- Holo
- huggingface
- huggingspace
- hunyuan
@@ -2,15 +2,15 @@
</script>
<template>
<div flex="1 ~ items-center justify-center" bg-gray-500 h-full w-full>
<div rounded h-180 w-100>
<div flex h-full>
<div flex="1 ~ items-center justify-center" h-full w-full bg-gray-500>
<div h-180 w-100 rounded>
<div h-full flex>
<div flex="1 ~ col" h-full bg="[#1f1f1f]">
<div flex="1 ~ col" relative>
<div bg-blue-300 flex-1 z-1>
<div z-1 flex-1 bg-blue-300>
<span text-5xl>BG</span>
</div>
<div ml--10 py-2 z-3>
<div z-3 ml--10 py-2>
<div>
<span text-lg>subtitle</span>
</div>
@@ -18,11 +18,11 @@
<span text-5xl>Title</span>
</div>
</div>
<div bg-blue-500:50 bottom-0 left--10 right-0 top--10 absolute z-2>
<div absolute bottom-0 left--10 right-0 top--10 z-2 bg-blue-500:50>
<span text-5xl>Cover</span>
</div>
</div>
<div mx-10 bg-gray-500 h-1 />
<div mx-10 h-1 bg-gray-500 />
<div px-10 py2>
<div>
<span text-2xl>Info 1111</span>
@@ -32,7 +32,7 @@
</div>
</div>
</div>
<div bg-pink-500 h-full w-15>
<div h-full w-15 bg-pink-500>
<span text-5xl>R</span>
</div>
</div>
@@ -0,0 +1,56 @@
<script setup lang="ts">
import CursorFloating from './CursorFloating.vue'
</script>
<template>
<Story
title="Cursor Floating"
group="physics"
:layout="{ type: 'grid', width: 600 }"
>
<Variant
id="basic"
title="Basic"
>
<div class="h-40 flex items-center justify-center">
<CursorFloating>
<div class="bg-primary-500/20 h-20 w-20" />
</CursorFloating>
</div>
</Variant>
<Variant
id="with-content"
title="With Content"
>
<div class="h-40 flex items-center justify-center">
<CursorFloating>
<div class="rounded-lg bg-white p-4 dark:bg-neutral-900">
<h3 class="text-lg font-bold">
Card Title
</h3>
<p class="text-sm text-neutral-500">
Card description goes here
</p>
</div>
</CursorFloating>
</div>
</Variant>
<Variant
id="interactive"
title="Interactive"
>
<div class="h-40 flex items-center justify-center">
<CursorFloating>
<div class="cursor-pointer rounded-lg bg-white p-4 dark:bg-neutral-900 hover:bg-neutral-50 dark:hover:bg-neutral-800">
<div class="flex items-center gap-2">
<div class="i-solar:check-circle-bold-duotone text-primary-500" />
<span>Click me!</span>
</div>
</div>
</CursorFloating>
</div>
</Variant>
</Story>
</template>
@@ -0,0 +1,153 @@
<script setup lang="ts">
// https://codepen.io/simeydotme/pen/PrQKgo
import { onMounted, ref } from 'vue'
interface Props {
intensity?: number
}
const props = withDefaults(defineProps<Props>(), {
intensity: 3,
})
const cardRef = ref<HTMLElement | null>(null)
const transformStyle = ref('')
const gradientPosition = ref('50% 50%')
const sparklePosition = ref('50% 50%')
const sparkleOpacity = ref(0.5)
function handleMouseMove(event: MouseEvent) {
if (!cardRef.value)
return
const card = cardRef.value
const rect = card.getBoundingClientRect()
// Calculate mouse position relative to card
const x = event.clientX - rect.left
const y = event.clientY - rect.top
const cardWidth = card.offsetWidth
const cardHeight = card.offsetHeight
// Calculate percentages and effect values
const xPercent = Math.abs(Math.floor(100 / cardWidth * x) - 100)
const yPercent = Math.abs(Math.floor(100 / cardHeight * y) - 100)
// Calculate positions for gradient and sparkle effects
const leftPos = (50 + (xPercent - 50) / 1.5)
const topPos = (50 + (yPercent - 50) / 1.5)
const sparkleX = (50 + (xPercent - 50) / 7)
const sparkleY = (50 + (yPercent - 50) / 7)
// Calculate 3D rotation angles - intensity controlled by prop
const rotateY = ((leftPos - 50) / 1.5) * 0.2 * props.intensity
const rotateX = ((topPos - 50) / 2) * -1 * 0.2 * props.intensity
// Calculate sparkle opacity with intensity control
const pAngle = (50 - xPercent) + (50 - yPercent)
const opacity = 0.5 + (Math.abs(pAngle) * 0.008 * props.intensity)
// Update style values
transformStyle.value = `perspective(1200px) rotateX(${rotateX}deg) rotateY(${rotateY}deg) scale3d(${1 + 0.015 * props.intensity}, ${1 + 0.015 * props.intensity}, ${1 + 0.015 * props.intensity})`
gradientPosition.value = `${leftPos}% ${topPos}%`
sparklePosition.value = `${sparkleX}% ${sparkleY}%`
sparkleOpacity.value = opacity
}
function resetCard() {
transformStyle.value = 'perspective(1200px) rotateX(0deg) rotateY(0deg) scale3d(1, 1, 1)'
gradientPosition.value = '50% 50%'
sparklePosition.value = '50% 50%'
sparkleOpacity.value = 0.5
}
onMounted(() => {
// Initialize with default transform to ensure smooth transitions
transformStyle.value = 'perspective(1200px) rotateX(0deg) rotateY(0deg) scale3d(1, 1, 1)'
})
</script>
<template>
<div
ref="cardRef"
class="card-hover-effect"
:style="{
'transform': transformStyle,
'--effect-intensity': intensity,
}"
@mousemove="handleMouseMove"
@mouseleave="resetCard"
>
<slot />
</div>
</template>
<style scoped>
.card-hover-effect {
transform-style: preserve-3d;
transform-origin: center;
will-change: transform;
transition: transform 0.3s cubic-bezier(0.23, 1, 0.32, 1);
}
.card-hover-effect::before,
.card-hover-effect::after {
content: '';
position: absolute;
left: 0;
right: 0;
bottom: 0;
top: 0;
background-repeat: no-repeat;
transition: all 0.33s ease;
pointer-events: none;
}
.card-hover-effect::before {
background-position: var(--card-position-x) var(--card-position-y);
background-size: 300% 300%;
background-image: linear-gradient(
115deg,
transparent 0%,
rgba(var(--color-primary-500), calc(0.3 * var(--effect-intensity))) 25%,
transparent 47%,
transparent 53%,
rgba(var(--color-primary-600), calc(0.3 * var(--effect-intensity))) 75%,
transparent 100%
);
opacity: calc(0.5 * var(--effect-intensity));
filter: brightness(0.5) contrast(1);
z-index: 1;
mix-blend-mode: color-dodge;
}
.card-hover-effect::after {
background-position: var(--sparkle-position-x) var(--sparkle-position-y);
background-size: 160%;
background-image: linear-gradient(
125deg,
rgba(var(--color-primary-500), calc(0.3 * var(--effect-intensity))) 15%,
rgba(var(--color-primary-400), calc(0.25 * var(--effect-intensity))) 30%,
rgba(var(--color-primary-300), calc(0.2 * var(--effect-intensity))) 40%,
rgba(var(--color-primary-200), calc(0.1 * var(--effect-intensity))) 60%,
rgba(var(--color-primary-400), calc(0.25 * var(--effect-intensity))) 70%,
rgba(var(--color-primary-500), calc(0.3 * var(--effect-intensity))) 85%
);
background-blend-mode: overlay;
opacity: calc(var(--sparkle-opacity) * var(--effect-intensity));
filter: brightness(1) contrast(1);
z-index: 2;
mix-blend-mode: color-dodge;
}
.card-hover-effect:hover::before {
opacity: calc(0.88 * var(--effect-intensity));
filter: brightness(0.66) contrast(1.33);
}
.card-hover-effect:hover::after {
opacity: var(--effect-intensity);
filter: brightness(1.1) contrast(1.2);
}
</style>
@@ -1 +1,2 @@
export { default as CursorFloating } from './CursorFloating.vue'
export { default as CursorMomentum } from './CursorMomentum.vue'