Stereoscopic Wrapper wrapper
Give elements a cool 3D tilt effect. ˋ( ° ▽、° )
Usage Examples
Basic Usage
You can toggle it off at will, but it won't save electricity. 乁( ◔ ௰◔)「
View example source code
<template>
<div class="w-full flex flex-col gap-4 border border-gray-200 rounded-xl p-6">
<div class="flex gap-4 border rounded p-4">
<base-checkbox
v-model="enable"
:label="t('floatToggle')"
class="w-full"
/>
</div>
<div class="flex flex-col items-start gap-4">
<wrapper-stereoscopic :enable>
<div class="h-80 w-80 flex-center rounded bg-gray-300">
<div class="h-40 w-40 flex-center rounded bg-gray-100">
<div class="text-xl text-gray-600 font-bold">
{{ t('hello') }}
</div>
</div>
</div>
</wrapper-stereoscopic>
<div class="flex flex-col items-start justify-start gap-4">
<wrapper-stereoscopic :enable>
<div class="border rounded-full p-4 text-xl text-gray-600 font-bold">
{{ t('floating') }}
</div>
</wrapper-stereoscopic>
<wrapper-stereoscopic :enable>
<div class="border rounded-full p-4 text-xl text-gray-600 font-bold">
{{ t('coolFloat') }}
</div>
</wrapper-stereoscopic>
<wrapper-stereoscopic :enable>
<div class="border rounded-full p-4 text-xl text-gray-600 font-bold">
{{ t('hello') }}
</div>
</wrapper-stereoscopic>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import BaseCheckbox from '../../base-checkbox.vue'
import WrapperStereoscopic from '../wrapper-stereoscopic.vue'
const { t } = useI18n()
const enable = ref(true)
</script>
<style lang="sass" scoped>
.flex-center
display: flex
justify-content: center
align-items: center
</style>Multi-Layer Parallax
Multiple layers can create a multi-level stereoscopic effect.
View example source code
<template>
<div class="w-full flex flex-col gap-4 border border-gray-200 rounded-xl p-6">
<div class="content flex items-start gap-4">
<wrapper-stereoscopic v-slot="wrapper">
<div
class="h-80 w-80 flex-center rounded bg-gray-300"
:style="wrapper.style"
>
<wrapper-stereoscopic-layer v-slot="layer01">
<div
class="h-40 w-40 flex-center rounded bg-gray-200"
:style="layer01.style"
>
<wrapper-stereoscopic-layer v-slot="layer02">
<div
class="rounded bg-gray-100 p-4 text-xl font-bold"
:style="layer02.style"
>
{{ t('hello') }}
</div>
</wrapper-stereoscopic-layer>
</div>
</wrapper-stereoscopic-layer>
</div>
</wrapper-stereoscopic>
</div>
</div>
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import WrapperStereoscopicLayer from '../wrapper-stereoscopic-layer.vue'
import WrapperStereoscopic from '../wrapper-stereoscopic.vue'
const { t } = useI18n()
</script>
<style lang="sass" scoped>
.content
perspective: 2000px
.flex-center
display: flex
justify-content: center
align-items: center
</style>Max Tilt Angle
You can set the maximum tilt angle.
View example source code
<template>
<div class="w-full flex flex-col gap-4 border border-gray-200 rounded-xl p-6">
<div class="flex gap-4 border rounded p-4">
<base-input
v-model.number="x"
type="range"
:label="`${t('xMaxAngle')}: ${x} ${t('degrees')}`"
class="w-full"
:min="0"
:max="90"
/>
<base-input
v-model.number="y"
type="range"
:label="`${t('yMaxAngle')}: ${y} ${t('degrees')}`"
class="w-full"
:min="0"
:max="90"
/>
</div>
<div class="content flex items-start gap-4">
<wrapper-stereoscopic
:x-max-angle="x"
:y-max-angle="y"
>
<div class="border rounded-full p-4 text-xl text-gray-600 font-bold">
( •̀ ω •́ )✧
</div>
</wrapper-stereoscopic>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import BaseInput from '../../base-input.vue'
import WrapperStereoscopic from '../wrapper-stereoscopic.vue'
const { t } = useI18n()
const x = ref(15)
const y = ref(15)
</script>
<style lang="sass" scoped>
.content
perspective: 2000px
.flex-center
display: flex
justify-content: center
align-items: center
</style>Float Distance
You can set the float distance between each layer—lay flat whenever you want. _(:3」ㄥ)_
View example source code
<template>
<div class="w-full flex flex-col gap-4 border border-gray-200 rounded-xl p-6">
<div class="flex gap-4 border rounded p-4">
<base-input
v-model.number="zOffset"
type="range"
:label="`${t('floatDistance')}: ${zOffset} px`"
class="w-full"
:min="0"
:max="200"
/>
</div>
<div class="content flex items-start gap-4">
<wrapper-stereoscopic
v-slot="wrapper"
:z-offset="zOffset"
>
<div
class="h-80 w-80 flex-center rounded bg-gray-300"
:style="wrapper.style"
>
<wrapper-stereoscopic-layer v-slot="layer01">
<div
class="h-40 w-40 flex-center rounded bg-gray-200"
:style="layer01.style"
>
<wrapper-stereoscopic-layer v-slot="layer02">
<div
class="rounded bg-gray-100 p-4 text-xl font-bold"
:style="layer02.style"
>
{{ t('hello') }}
</div>
</wrapper-stereoscopic-layer>
</div>
</wrapper-stereoscopic-layer>
</div>
</wrapper-stereoscopic>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import BaseInput from '../../base-input.vue'
import WrapperStereoscopicLayer from '../wrapper-stereoscopic-layer.vue'
import WrapperStereoscopic from '../wrapper-stereoscopic.vue'
const { t } = useI18n()
const zOffset = ref(100)
</script>
<style lang="sass" scoped>
.content
perspective: 2000px
.flex-center
display: flex
justify-content: center
align-items: center
</style>Custom Strategy
You can customize rotation and float distance for richer interactions.
View example source code
<template>
<div class="w-full flex flex-col items-center justify-center gap-8 border border-gray-200 rounded-xl p-6">
<div class="content flex items-start">
<wrapper-stereoscopic
v-slot="wrapper"
v-bind="params"
>
<div
class="cursor-zoom-in select-none border rounded-full"
:style="wrapper.style"
>
<wrapper-stereoscopic-layer v-slot="layer01">
<div
class="flex-center px-14 py-8 text-2xl tracking-widest"
:style="layer01.style"
>
(´● ω ●`)
</div>
</wrapper-stereoscopic-layer>
</div>
</wrapper-stereoscopic>
</div>
<div
ref="blockRef"
class="w-full cursor-zoom-in border rounded border-dashed p-2 text-center text-xs tracking-widest"
>
{{ t('lookHere') }}
</div>
</div>
</template>
<script setup lang="ts">
import type { ExtractComponentProps } from '../../../types'
import { useElementHover } from '@vueuse/core'
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { mapNumber } from '../../../common/utils'
import WrapperStereoscopicLayer from '../wrapper-stereoscopic-layer.vue'
import WrapperStereoscopic from '../wrapper-stereoscopic.vue'
type Props = ExtractComponentProps<typeof WrapperStereoscopic>
const { t } = useI18n()
const blockRef = ref()
const isHovered = useElementHover(blockRef)
const params: Props = {
strategy(params) {
const {
mousePosition: { x, y },
size: { width, height },
} = params
if (isHovered.value) {
return {
x: mapNumber(y, -height, height, -50, 50),
y: mapNumber(x, -width, width, -60, 60),
zOffset: -100,
}
}
if (
params.isOutside
|| !params.enable
|| !params.isVisible
|| params.isPressed) {
return {
x: 0,
y: 0,
zOffset: 0,
}
}
return {
x: mapNumber(y, -height, height, -30, 30),
y: mapNumber(x, -width, width, -40, 40),
zOffset: 100,
}
},
}
</script>
<style lang="sass" scoped>
.content
perspective: 2000px
</style>How It Works
Uses CSS perspective and transform3d to create 3D rotation and perspective distortion effects.
The perspective property is especially important—it's what makes objects appear to have perspective distortion.
Without it, objects just look like they got squished for no reason. ...('◉◞⊖◟◉` )
Once you know how to tilt, the rest is simple. ( •̀ ω •́ )✧
Calculate the vector from the object's center to the mouse position, map the vector's x and y components to the configured angle range, and apply it to the transform.
There's a small trick here though—instead of setting the "current angle" directly to the "target angle", we gradually approach the "target angle".
This way, no matter how wildly the "target angle" jumps, the tilt effect always has a smooth animation, looking more natural and comfortable. ◝(≧∀≦)◟
Source Code
API
Props
interface StrategyParams {
enable: boolean;
xMaxAngle: number;
yMaxAngle: number;
zOffset: number;
/** 以元素中心為零點,目前滑鼠的座標 */
mousePosition: Record<'x' | 'y', number>;
/** 元素尺寸 */
size: Record<'width' | 'height', number>;
/** 滑鼠是否在元素外 */
isOutside: boolean;
/** 元素是否可見 */
isVisible: boolean;
/** 是否被按下 */
isPressed: boolean;
}
interface Props {
/** 是否開啟 */
enable?: boolean;
/** x 最大偏轉角度 */
xMaxAngle?: number;
/** y 最大偏轉角度 */
yMaxAngle?: number;
/** 懸浮高度 */
zOffset?: number;
/** 旋轉、懸浮距離邏輯 */
strategy?: (params: StrategyParams) => Record<'x' | 'y' | 'zOffset', number>;
/** 更新週期,越短會越快到達目標狀態
*
* @default 15
*/
updateInterval?: number;
}