Skip to content

立體包裝器 wrapper

可以讓元素有酷酷的 3D 偏轉效果。ˋ( ° ▽、° )

使用範例

基本用法

可以隨手關閉,不過沒辦法省電。乁( ◔ ௰◔)「

安安
漂起來惹
酷酷的漂浮
安安
查看範例原始碼
vue
<template>
  <div class="w-full flex flex-col gap-4 border border-gray-300 p-6">
    <div class="flex gap-4 border rounded p-4">
      <base-checkbox
        v-model="enable"
        label="懸浮開關"
        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">
              安安
            </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">
            漂起來惹
          </div>
        </wrapper-stereoscopic>

        <wrapper-stereoscopic :enable>
          <div class="border rounded-full p-4 text-xl text-gray-600 font-bold">
            酷酷的漂浮
          </div>
        </wrapper-stereoscopic>

        <wrapper-stereoscopic :enable>
          <div class="border rounded-full p-4 text-xl text-gray-600 font-bold">
            安安
          </div>
        </wrapper-stereoscopic>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'

import BaseCheckbox from '../../base-checkbox.vue'
import WrapperStereoscopic from '../wrapper-stereoscopic.vue'

const enable = ref(true)
</script>

<style lang="sass" scoped>
.flex-center
  display: flex
  justify-content: center
  align-items: center
</style>

多層視差

多層 layer 可以產生多層立體效果

安安
查看範例原始碼
vue
<template>
  <div class="w-full flex flex-col gap-4 border border-gray-300 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"
                >
                  安安
                </div>
              </wrapper-stereoscopic-layer>
            </div>
          </wrapper-stereoscopic-layer>
        </div>
      </wrapper-stereoscopic>
    </div>
  </div>
</template>

<script setup lang="ts">
import WrapperStereoscopicLayer from '../wrapper-stereoscopic-layer.vue'
import WrapperStereoscopic from '../wrapper-stereoscopic.vue'
</script>

<style lang="sass" scoped>
.content
  perspective: 2000px
.flex-center
  display: flex
  justify-content: center
  align-items: center
</style>

最大偏轉角

可以設定最大偏轉角度

( •̀ ω •́ )✧
查看範例原始碼
vue
<template>
  <div class="w-full flex flex-col gap-4 border border-gray-300 p-6">
    <div class="flex gap-4 border rounded p-4">
      <base-input
        v-model.number="x"
        type="range"
        :label="`X 最大角度: ${x} 度`"
        class="w-full"
        :min="0"
        :max="90"
      />

      <base-input
        v-model.number="y"
        type="range"
        :label="`Y 軸最大角度: ${y} 度`"
        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 BaseInput from '../../base-input.vue'
import WrapperStereoscopic from '../wrapper-stereoscopic.vue'

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>

漂浮距離

可以設定每層之間的漂浮距離,想躺平就躺平。_(:3」ㄥ)_

安安
查看範例原始碼
vue
<template>
  <div class="w-full flex flex-col gap-4 border border-gray-300 p-6">
    <div class="flex gap-4 border rounded p-4">
      <base-input
        v-model.number="zOffset"
        type="range"
        :label="`懸浮距離: ${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"
                >
                  安安
                </div>
              </wrapper-stereoscopic-layer>
            </div>
          </wrapper-stereoscopic-layer>
        </div>
      </wrapper-stereoscopic>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'

import BaseInput from '../../base-input.vue'
import WrapperStereoscopicLayer from '../wrapper-stereoscopic-layer.vue'
import WrapperStereoscopic from '../wrapper-stereoscopic.vue'

const zOffset = ref(100)
</script>

<style lang="sass" scoped>
.content
  perspective: 2000px
.flex-center
  display: flex
  justify-content: center
  align-items: center
</style>

自訂策略

可以自訂旋轉、懸浮距離,做出更豐富的互動。

(´● ω ●`)
看這裡
查看範例原始碼
vue
<template>
  <div class="w-full flex flex-col items-center justify-center gap-8 border border-gray-300 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"
    >
      看這裡
    </div>
  </div>
</template>

<script setup lang="ts">
import type { ExtractComponentProps } from '../../../types'
import { useElementHover } from '@vueuse/core'
import { ref } from 'vue'
import { mapNumber } from '../../../common/utils'
import WrapperStereoscopicLayer from '../wrapper-stereoscopic-layer.vue'
import WrapperStereoscopic from '../wrapper-stereoscopic.vue'

type Props = ExtractComponentProps<typeof WrapperStereoscopic>

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>

原理

利用 CSS 的 perspective 與 transform3d,產生 3D 旋轉與透視變形效果。

其中 perspective 尤為重要,此屬性負責讓物體產生透視變形效果。

沒設定的話,物體看起來像莫名其妙扁掉。...('◉◞⊖◟◉` )

📚 CSS perspective

📚 CSS translate3d

知道如何偏轉後,剩下的部分就簡單惹。( •̀ ω •́ )✧

計算從物體中心到滑鼠位置的向量,分別將向量的 x、y 分量映射到設定的角度範圍,最後套用到 transform 上即可。

不過這裡有個小技巧,我們不把「目前角度」直接設為「目標角度」,而是逐漸趨近「目標角度」。

這樣無論「目標角度」怎麼亂跳,都可以保證偏轉效果都有動畫呈現,看起來更自然、舒服。◝(≧∀≦)◟

原始碼

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;
}

v0.27.10