mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 06:29:30 -07:00
Create <TiltedCard /> component
This commit is contained in:
@@ -48,6 +48,7 @@ export const CATEGORIES = [
|
|||||||
'Flying Posters',
|
'Flying Posters',
|
||||||
'Card Swap',
|
'Card Swap',
|
||||||
'Infinite Scroll',
|
'Infinite Scroll',
|
||||||
|
'Tilted Card',
|
||||||
'Glass Icons',
|
'Glass Icons',
|
||||||
'Decay Card',
|
'Decay Card',
|
||||||
'Flowing Menu',
|
'Flowing Menu',
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ const components = {
|
|||||||
'decay-card': () => import("../demo/Components/DecayCardDemo.vue"),
|
'decay-card': () => import("../demo/Components/DecayCardDemo.vue"),
|
||||||
'flowing-menu': () => import("../demo/Components/FlowingMenuDemo.vue"),
|
'flowing-menu': () => import("../demo/Components/FlowingMenuDemo.vue"),
|
||||||
'elastic-slider': () => import("../demo/Components/ElasticSliderDemo.vue"),
|
'elastic-slider': () => import("../demo/Components/ElasticSliderDemo.vue"),
|
||||||
|
'tilted-card': () => import("../demo/Components/TiltedCardDemo.vue"),
|
||||||
};
|
};
|
||||||
|
|
||||||
const backgrounds = {
|
const backgrounds = {
|
||||||
|
|||||||
34
src/constants/code/Components/tiltedCardCode.ts
Normal file
34
src/constants/code/Components/tiltedCardCode.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import code from '@content/Components/TiltedCard/TiltedCard.vue?raw'
|
||||||
|
import type { CodeObject } from '../../../types/code'
|
||||||
|
|
||||||
|
export const tiltedCard: CodeObject = {
|
||||||
|
cli: `npx jsrepo add https://vue-bits.dev/ui/Components/TiltedCard`,
|
||||||
|
installation: `npm install motion-v`,
|
||||||
|
usage: `<template>
|
||||||
|
<TiltedCard
|
||||||
|
image-src="https://example.com/image.jpg"
|
||||||
|
alt-text="Sample image"
|
||||||
|
caption-text="Hover to see tooltip"
|
||||||
|
container-height="400px"
|
||||||
|
container-width="400px"
|
||||||
|
image-height="300px"
|
||||||
|
image-width="300px"
|
||||||
|
:rotate-amplitude="14"
|
||||||
|
:scale-on-hover="1.1"
|
||||||
|
:show-mobile-warning="true"
|
||||||
|
:show-tooltip="true"
|
||||||
|
:display-overlay-content="false"
|
||||||
|
>
|
||||||
|
<template #overlay>
|
||||||
|
<div class="overlay-content">
|
||||||
|
Your overlay content here
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</TiltedCard>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import TiltedCard from "./TiltedCard.vue";
|
||||||
|
</script>`,
|
||||||
|
code
|
||||||
|
}
|
||||||
137
src/content/Components/TiltedCard/TiltedCard.vue
Normal file
137
src/content/Components/TiltedCard/TiltedCard.vue
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
<template>
|
||||||
|
<figure ref="cardRef" class="relative w-full h-full [perspective:800px] flex flex-col items-center justify-center"
|
||||||
|
:style="{
|
||||||
|
height: containerHeight,
|
||||||
|
width: containerWidth,
|
||||||
|
}" @mousemove="handleMouse" @mouseenter="handleMouseEnter" @mouseleave="handleMouseLeave">
|
||||||
|
<div v-if="showMobileWarning" class="absolute top-4 text-center text-sm block sm:hidden">
|
||||||
|
This effect is not optimized for mobile. Check on desktop.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Motion tag="div" class="relative [transform-style:preserve-3d]" :style="{
|
||||||
|
width: imageWidth,
|
||||||
|
height: imageHeight,
|
||||||
|
}" :animate="{
|
||||||
|
rotateX: rotateXValue,
|
||||||
|
rotateY: rotateYValue,
|
||||||
|
scale: scaleValue,
|
||||||
|
}" :transition="springTransition">
|
||||||
|
<img :src="imageSrc" :alt="altText"
|
||||||
|
class="absolute top-0 left-0 object-cover rounded-[15px] will-change-transform [transform:translateZ(0)]"
|
||||||
|
:style="{
|
||||||
|
width: imageWidth,
|
||||||
|
height: imageHeight,
|
||||||
|
}" />
|
||||||
|
|
||||||
|
<Motion v-if="displayOverlayContent && overlayContent" tag="div"
|
||||||
|
class="absolute top-0 left-0 z-[2] will-change-transform [transform:translateZ(30px)]">
|
||||||
|
<slot name="overlay" />
|
||||||
|
</Motion>
|
||||||
|
</Motion>
|
||||||
|
|
||||||
|
<Motion v-if="showTooltip && captionText" tag="figcaption"
|
||||||
|
class="pointer-events-none absolute left-0 top-0 rounded-[4px] bg-white px-[10px] py-[4px] text-[10px] text-[#2d2d2d] opacity-0 z-[3] hidden sm:block"
|
||||||
|
:animate="{
|
||||||
|
x: xValue,
|
||||||
|
y: yValue,
|
||||||
|
opacity: opacityValue,
|
||||||
|
rotate: rotateFigcaptionValue,
|
||||||
|
}" :transition="tooltipTransition">
|
||||||
|
{{ captionText }}
|
||||||
|
</Motion>
|
||||||
|
</figure>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { Motion } from 'motion-v'
|
||||||
|
|
||||||
|
interface TiltedCardProps {
|
||||||
|
imageSrc: string
|
||||||
|
altText?: string
|
||||||
|
captionText?: string
|
||||||
|
containerHeight?: string
|
||||||
|
containerWidth?: string
|
||||||
|
imageHeight?: string
|
||||||
|
imageWidth?: string
|
||||||
|
scaleOnHover?: number
|
||||||
|
rotateAmplitude?: number
|
||||||
|
showMobileWarning?: boolean
|
||||||
|
showTooltip?: boolean
|
||||||
|
overlayContent?: boolean
|
||||||
|
displayOverlayContent?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<TiltedCardProps>(), {
|
||||||
|
altText: 'Tilted card image',
|
||||||
|
captionText: '',
|
||||||
|
containerHeight: '300px',
|
||||||
|
containerWidth: '100%',
|
||||||
|
imageHeight: '300px',
|
||||||
|
imageWidth: '300px',
|
||||||
|
scaleOnHover: 1.1,
|
||||||
|
rotateAmplitude: 14,
|
||||||
|
showMobileWarning: true,
|
||||||
|
showTooltip: true,
|
||||||
|
overlayContent: false,
|
||||||
|
displayOverlayContent: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const cardRef = ref<HTMLElement | null>(null)
|
||||||
|
const xValue = ref(0)
|
||||||
|
const yValue = ref(0)
|
||||||
|
const rotateXValue = ref(0)
|
||||||
|
const rotateYValue = ref(0)
|
||||||
|
const scaleValue = ref(1)
|
||||||
|
const opacityValue = ref(0)
|
||||||
|
const rotateFigcaptionValue = ref(0)
|
||||||
|
const lastY = ref(0)
|
||||||
|
|
||||||
|
const springTransition = computed(() => ({
|
||||||
|
type: 'spring' as const,
|
||||||
|
damping: 30,
|
||||||
|
stiffness: 100,
|
||||||
|
mass: 2,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const tooltipTransition = computed(() => ({
|
||||||
|
type: 'spring' as const,
|
||||||
|
damping: 30,
|
||||||
|
stiffness: 350,
|
||||||
|
mass: 1,
|
||||||
|
}))
|
||||||
|
|
||||||
|
function handleMouse(e: MouseEvent) {
|
||||||
|
if (!cardRef.value) return
|
||||||
|
|
||||||
|
const rect = cardRef.value.getBoundingClientRect()
|
||||||
|
const offsetX = e.clientX - rect.left - rect.width / 2
|
||||||
|
const offsetY = e.clientY - rect.top - rect.height / 2
|
||||||
|
|
||||||
|
const rotationX = (offsetY / (rect.height / 2)) * -props.rotateAmplitude
|
||||||
|
const rotationY = (offsetX / (rect.width / 2)) * props.rotateAmplitude
|
||||||
|
|
||||||
|
rotateXValue.value = rotationX
|
||||||
|
rotateYValue.value = rotationY
|
||||||
|
|
||||||
|
xValue.value = e.clientX - rect.left
|
||||||
|
yValue.value = e.clientY - rect.top
|
||||||
|
|
||||||
|
const velocityY = offsetY - lastY.value
|
||||||
|
rotateFigcaptionValue.value = -velocityY * 0.6
|
||||||
|
lastY.value = offsetY
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseEnter() {
|
||||||
|
scaleValue.value = props.scaleOnHover
|
||||||
|
opacityValue.value = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseLeave() {
|
||||||
|
opacityValue.value = 0
|
||||||
|
scaleValue.value = 1
|
||||||
|
rotateXValue.value = 0
|
||||||
|
rotateYValue.value = 0
|
||||||
|
rotateFigcaptionValue.value = 0
|
||||||
|
}
|
||||||
|
</script>
|
||||||
183
src/demo/Components/TiltedCardDemo.vue
Normal file
183
src/demo/Components/TiltedCardDemo.vue
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
<template>
|
||||||
|
<div class="tilted-card-demo">
|
||||||
|
<TabbedLayout>
|
||||||
|
<template #preview>
|
||||||
|
<div class="demo-container" style="min-height: 500px; position: relative; overflow: hidden;">
|
||||||
|
<TiltedCard
|
||||||
|
image-src="https://i.scdn.co/image/ab67616d0000b273d9985092cd88bffd97653b58"
|
||||||
|
alt-text="Kendrick Lamar - GNX Album Cover"
|
||||||
|
caption-text="Kendrick Lamar - GNX"
|
||||||
|
container-height="300px"
|
||||||
|
container-width="300px"
|
||||||
|
image-height="300px"
|
||||||
|
image-width="300px"
|
||||||
|
:rotate-amplitude="rotateAmplitude"
|
||||||
|
:scale-on-hover="scaleOnHover"
|
||||||
|
:show-mobile-warning="false"
|
||||||
|
:show-tooltip="showTooltip"
|
||||||
|
:display-overlay-content="displayOverlayContent"
|
||||||
|
:overlay-content="displayOverlayContent"
|
||||||
|
>
|
||||||
|
<template #overlay>
|
||||||
|
<p class="tilted-card-demo-text">
|
||||||
|
Kendrick Lamar - GNX
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
</TiltedCard>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Customize>
|
||||||
|
<PreviewSlider
|
||||||
|
title="Rotate Amplitude"
|
||||||
|
v-model="rotateAmplitude"
|
||||||
|
:min="0"
|
||||||
|
:max="30"
|
||||||
|
:step="1"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PreviewSlider
|
||||||
|
title="Scale on Hover"
|
||||||
|
v-model="scaleOnHover"
|
||||||
|
:min="1"
|
||||||
|
:max="1.5"
|
||||||
|
:step="0.05"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PreviewSwitch
|
||||||
|
title="Show Tooltip"
|
||||||
|
v-model="showTooltip"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PreviewSwitch
|
||||||
|
title="Show Overlay Content"
|
||||||
|
v-model="displayOverlayContent"
|
||||||
|
/>
|
||||||
|
</Customize>
|
||||||
|
|
||||||
|
<PropTable :data="propData" />
|
||||||
|
<Dependencies :dependency-list="['motion-v']" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #code>
|
||||||
|
<CodeExample :code-object="tiltedCard" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cli>
|
||||||
|
<CliInstallation :command="tiltedCard.cli" />
|
||||||
|
</template>
|
||||||
|
</TabbedLayout>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import TabbedLayout from '../../components/common/TabbedLayout.vue'
|
||||||
|
import PropTable from '../../components/common/PropTable.vue'
|
||||||
|
import Dependencies from '../../components/code/Dependencies.vue'
|
||||||
|
import CliInstallation from '../../components/code/CliInstallation.vue'
|
||||||
|
import CodeExample from '../../components/code/CodeExample.vue'
|
||||||
|
import Customize from '../../components/common/Customize.vue'
|
||||||
|
import PreviewSwitch from '../../components/common/PreviewSwitch.vue'
|
||||||
|
import PreviewSlider from '../../components/common/PreviewSlider.vue'
|
||||||
|
import TiltedCard from '../../content/Components/TiltedCard/TiltedCard.vue'
|
||||||
|
import { tiltedCard } from '@/constants/code/Components/tiltedCardCode'
|
||||||
|
|
||||||
|
const rotateAmplitude = ref(12)
|
||||||
|
const scaleOnHover = ref(1.05)
|
||||||
|
const showTooltip = ref(true)
|
||||||
|
const displayOverlayContent = ref(true)
|
||||||
|
|
||||||
|
const propData = [
|
||||||
|
{
|
||||||
|
name: 'imageSrc',
|
||||||
|
type: 'string',
|
||||||
|
default: 'N/A',
|
||||||
|
description: 'The source URL of the image.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'altText',
|
||||||
|
type: 'string',
|
||||||
|
default: 'Tilted card image',
|
||||||
|
description: 'Alternative text for the image.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'captionText',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
description: 'Text for the tooltip caption.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'containerHeight',
|
||||||
|
type: 'string',
|
||||||
|
default: '300px',
|
||||||
|
description: 'Height of the overall card container.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'containerWidth',
|
||||||
|
type: 'string',
|
||||||
|
default: '100%',
|
||||||
|
description: 'Width of the overall card container.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'imageHeight',
|
||||||
|
type: 'string',
|
||||||
|
default: '300px',
|
||||||
|
description: 'Height of the inner image.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'imageWidth',
|
||||||
|
type: 'string',
|
||||||
|
default: '300px',
|
||||||
|
description: 'Width of the inner image.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'scaleOnHover',
|
||||||
|
type: 'number',
|
||||||
|
default: '1.1',
|
||||||
|
description: 'Scaling factor applied on hover.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'rotateAmplitude',
|
||||||
|
type: 'number',
|
||||||
|
default: '14',
|
||||||
|
description: 'Controls how much the card tilts with mouse movement.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'showMobileWarning',
|
||||||
|
type: 'boolean',
|
||||||
|
default: 'true',
|
||||||
|
description: 'Whether to show a small alert about mobile usage.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'showTooltip',
|
||||||
|
type: 'boolean',
|
||||||
|
default: 'true',
|
||||||
|
description: 'Toggles the visibility of the tooltip (figcaption).'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'displayOverlayContent',
|
||||||
|
type: 'boolean',
|
||||||
|
default: 'false',
|
||||||
|
description: 'Whether to display any overlayContent on top of the image.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'overlayContent',
|
||||||
|
type: 'boolean',
|
||||||
|
default: 'false',
|
||||||
|
description: 'Whether to show overlay content (use overlay slot for content).'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.tilted-card-demo-text {
|
||||||
|
color: white;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: center;
|
||||||
|
padding: 12px;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
border-radius: 8px;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user