Skip to content

科幻卡片 card

簡潔、實用的科幻風格資訊容器。

終於做出我夢想中的元件了!◝(≧∀≦)◟

在學階段的專業是機電與自動控制,一直對於科幻風格的元件情有獨鍾。

過去最接近的作品為:

礙於技術力不足,可以看得出來介面非常陽春,只有勉強沾上科幻的邊。(́⊙◞౪◟⊙‵)

時隔多年,總算藉由網頁技術實現我心目中的樣子了!✧⁑。٩(ˊᗜˋ*)و✧⁕。

使用範例

基本用法

Title
The best things in life are actually really expensive.
查看範例原始碼
vue
<template>
  <div class="w-full flex flex-col gap-10 border border-gray-300 p-6 pb-16">
    <base-checkbox
      v-model="visible"
      label="顯示"
      class="border rounded p-4"
    />

    <div class="h-full flex justify-center">
      <card-futuristic
        v-on-click-outside="() => toggleSelect(false)"
        :visible
        :selected
        class="font-orbitron"
        @click="toggleSelect(true)"
      >
        <div class="flex flex-col gap-4">
          <div class="text-xl font-bold">
            Title
          </div>

          <div>
            The best things in life are actually really expensive.
          </div>
        </div>
      </card-futuristic>
    </div>
  </div>
</template>

<script setup lang="ts">
import { vOnClickOutside } from '@vueuse/components'
import { useToggle } from '@vueuse/core'
import { ref } from 'vue'
import BaseCheckbox from '../../base-checkbox.vue'
import CardFuturistic from '../card-futuristic.vue'

const visible = ref(true)

const [selected, toggleSelect] = useToggle(false)
</script>

<style lang="sass">
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+TC:[email protected]&family=Orbitron:[email protected]&family=Oxanium:[email protected]&display=swap')

.font-orbitron
  font-family: "Orbitron", sans-serif

.font-oxanium
  font-family: "Oxanium", sans-serif
</style>

組合零件

組合不同零件,產生多種的樣式。

COD-00
FUTURISTIC CARD
CARD-01
QUOTE CORNER
CARD-02
SIDE BORDER CLIP
CARD-03
SOLID BACKGROUND
ERROR
FISH OVERWEIGHT
COD
查看範例原始碼
vue
<template>
  <div class="w-full flex flex-col gap-4 border border-gray-300 p-6">
    <base-checkbox
      v-model="visible"
      label="顯示"
      class="sticky top-20 z-10 border rounded bg-white p-4 md:relative md:top-0"
    />

    <div class="flex flex-wrap items-center justify-center gap-10">
      <card-futuristic
        v-for="item, i in list"
        :key="i"
        v-on-click-outside="() => item.selected = false"
        v-bind="item"
        class="font-orbitron"
        @click="item.selected = true"
      >
        <div class="flex flex-col">
          <div
            v-if="item.title"
            :class="item.titleClass"
          >
            {{ item.title }}
          </div>

          <div
            v-if="item.text"
            :class="item.textClass"
          >
            {{ item.text }}
          </div>
        </div>
      </card-futuristic>
    </div>
  </div>
</template>

<script setup lang="ts">
import type { Writable } from 'type-fest'
import type { ExtractComponentProps } from '../../../types'
import { vOnClickOutside } from '@vueuse/components'
import { promiseTimeout } from '@vueuse/core'
import { map, pipe } from 'remeda'
import { ref, watch } from 'vue'
import BaseCheckbox from '../../base-checkbox.vue'
import CardFuturistic from '../card-futuristic.vue'

type CardProp = Writable<ExtractComponentProps<typeof CardFuturistic>> & {
  title?: string;
  titleClass?: string;
  text?: string;
  textClass?: string;
}

const list = ref(pipe(
  [
    {
      title: 'COD-00',
      titleClass: 'text-xl font-bold',
      text: 'FUTURISTIC CARD',
      bg: { type: 'halftone' },
      border: {
        type: 'specific',
        selectedColor: '#FFF',
        strokeWidth: 1,
        side: {
          t: {},
          l: {},
          b: {},
          r: {},
        },
      },
      animeSequence: {
        visible: {
          border: { duration: 400 },
        },
      },
    },
    {
      title: 'CARD-01',
      titleClass: 'text-xl font-bold',
      text: 'QUOTE CORNER',
      corner: { type: 'quote' },
      content: {
        type: 'scale',
        class: 'p-4',
      },
      bg: { type: 'halftone' },
      border: null,
      animeSequence: {
        visible: {
          content: { delay: 200 },
          bg: { delay: 400 },
        },
      },
    },
    {
      title: 'CARD-02',
      titleClass: 'text-xl font-bold ',
      text: 'SIDE BORDER CLIP',
      textClass: '',
      corner: null,
      content: {
        type: 'clip',
        class: 'p-4',
      },
      bg: {
        type: 'typical',
        margin: '0',
      },
      border: { type: 'side' },
      animeSequence: {
        normal: {
          border: { delay: 0 },
        },
        visible: {
          border: { delay: 0 },
          bg: { delay: 200 },
          content: { delay: 300 },
        },
        hidden: {
          border: { delay: 300 },
          bg: { delay: 0 },
          content: { delay: 0 },
        },
      },
    },
    {
      title: 'CARD-03',
      titleClass: 'text-xl font-bold text-white',
      text: 'SOLID BACKGROUND',
      textClass: 'text-white',
      corner: null,
      bg: {
        type: 'solid',
        selectedColor: '#444',
      },
      content: {
        type: 'typical',
        class: 'p-4 pl-6',
      },
      border: { type: 'specific' },
      animeSequence: {
        visible: {
          border: { delay: 400 },
        },
      },
    },
    {
      title: 'ERROR',
      titleClass: 'text-2xl font-bold text-[#ba2507] ',
      text: 'FISH OVERWEIGHT',
      textClass: 'text-[#ba2507]',
      corner: null,
      bg: {
        type: 'typical',
        margin: '4px 0px',
        color: '#ffe8e8',
      },
      content: {
        type: 'typical',
        class: 'py-4 px-8',
      },
      border: {
        type: 'specific',
        color: '#ba2507',
        selectedColor: '#f07860',
        strokeWidth: 2,
        side: {
          t: {},
          b: {},
        },
      },
      animeSequence: {
        normal: {
          border: { delay: 0 },
        },
        visible: {
          border: { delay: 0 },
          bg: { delay: 400 },
          content: { delay: 500 },
        },
        hidden: {
          border: { delay: 300 },
          bg: { delay: 0 },
          content: { delay: 0 },
        },
        // null 表示停用動畫
        selected: { content: null },
        hover: { content: null },
      },
    },
    {
      title: 'COD',
      corner: {
        type: 'square',
        size: 2,
      },
      bg: null,
      content: {
        type: 'typical',
        class: 'p-1 px-2',
      },
      border: {
        type: 'typical',
        color: '#BBB',
      },
      animeSequence: {
        visible: {
          corner: { delay: 0 },
          border: { delay: 400 },
          content: { delay: 500 },
        },
        hidden: {
          corner: { delay: 400 },
          border: { delay: 0 },
          content: { delay: 0 },
        },
      },
    },
  ] as CardProp[],
  map((data) => ({
    ...data,
    visible: false,
    selected: false,
  })),
))

const visible = ref(false)
watch(visible, async (value) => {
  for (const data of list.value) {
    data.visible = value
    await promiseTimeout(100)
  }
})
</script>

<style lang="sass">
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+TC:[email protected]&family=Orbitron:[email protected]&family=Oxanium:[email protected]&display=swap')

.font-orbitron
  font-family: "Orbitron", sans-serif

.font-oxanium
  font-family: "Oxanium", sans-serif
</style>

文字動畫

使用文件元件就可以產生科幻文字特效。

Futuristic Card

borderbgcornercontent ornament card 調

N

使 Vue SVG Attr 使 anime.js

查看範例原始碼
vue
<template>
  <div class="w-full flex flex-col gap-4 border border-gray-300 p-6">
    <base-checkbox
      v-model="visible"
      label="顯示"
      class="sticky top-20 z-10 border rounded bg-white p-4 md:relative md:top-0"
    />

    <div class="flex flex-wrap items-center justify-center gap-20">
      <card-futuristic
        v-for="item, i in list"
        :key="i"
        v-on-click-outside="() => item.selected = false"
        v-bind="item"
        class="font-orbitron"
        @click="item.selected = true"
      >
        <div class="flex flex-col gap-2">
          <card-futuristic-text
            v-if="item.title"
            :text="item.title"
            :class="item.titleClass"
            class="!m-0"
          />

          <card-futuristic-text
            v-for="line, j in item.text"
            :key="j"
            :text="line"
            :class="item.textClass"
            class="!m-0"
          />
        </div>
      </card-futuristic>
    </div>
  </div>
</template>

<script setup lang="ts">
import type { Writable } from 'type-fest'
import type { ExtractComponentProps } from '../../../types'
import { vOnClickOutside } from '@vueuse/components'
import { promiseTimeout } from '@vueuse/core'
import { map, pipe } from 'remeda'
import { ref, watch } from 'vue'
import BaseCheckbox from '../../base-checkbox.vue'
import CardFuturistic from '../card-futuristic.vue'
import CardFuturisticText from '../card-futuristic-text.vue'

type CardProp = Writable<ExtractComponentProps<typeof CardFuturistic>> & {
  title?: string;
  titleClass?: string;
  text?: string[];
  textClass?: string;
}

const list = ref(pipe(
  [
    {
      title: 'Futuristic Card',
      titleClass: 'text-xl font-bold',
      text: [
        `此元件主要由 border、bg、corner、content 與 ornament 子元件組成,由 card 父元件負責調度動畫。`,
        `子元件可以任意組合,藉此產生 N 種有趣的樣式設計。`,
        `使用 Vue 綁定 SVG 的 Attr 進行繪圖,並使用 anime.js 實現動畫`,
      ],
      bg: {
        type: 'halftone',
        color: '#0001',
        dotSize: '1px',
        size: '10px',
      },
      corner: {
        type: 'quote',
        strokeWidth: 8,
      },
      border: {
        type: 'typical',
        color: '#777',
      },
      animeSequence: {
        visible: {
          corner: { delay: 0 },
          bg: { delay: 400 },
          border: { delay: 400 },
          content: { delay: 500 },
        },
        hidden: {
          corner: { delay: 700 },
          bg: { delay: 200 },
          border: { delay: 100 },
          content: { delay: 100 },
        },
        hover: {
          corner: null,
          border: null,
          content: null,
        },
        selected: {
          corner: null,
          border: null,
          content: null,
        },
      },
    },
  ] as CardProp[],
  map((data) => ({
    ...data,
    visible: false,
    selected: false,
  })),
))

const visible = ref(false)
watch(visible, async (value) => {
  for (const data of list.value) {
    data.visible = value
    await promiseTimeout(100)
  }
})
</script>

<style lang="sass">
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+TC:[email protected]&family=Orbitron:[email protected]&family=Oxanium:[email protected]&display=swap')

.font-orbitron
  font-family: "Orbitron", sans-serif

.font-oxanium
  font-family: "Oxanium", sans-serif
</style>

原理

動態由 border、bg、corner、content 之子元件構成,由 card 父元件負責調度動畫。

子元件可以任意組合,藉此產生 N 種有趣的樣式設計。

子元件具體實作使用 Vue 綁定 SVG 的 Attr 進行繪圖,並使用 anime.js 實現動畫

原始碼

API

Props

interface Props {
  /** 動畫序列,可自定義動畫參數 */
  animeSequence?: Partial<AnimeSequence>;

  visible?: boolean;
  selected?: boolean;
  /** 為空則自動處理,有提供則以參數數值為主 */
  hover?: boolean;

  /** null 表示不使用此元件 */
  border?: BorderParam | null;
  bg?: BgParam | null;
  corner?: CornerParam | null;
  content?: ContentParam | null;
}

Methods

defineExpose({
  /** 執行特定 part 狀態動畫 */
  execute,
})

Slots

defineSlots<{
  default?: () => unknown;
}>()

v0.23.1