Skip to content

刮刮樂包裝器 wrapper

讓任意內容一秒變成刮刮樂 ヽ(́◕◞౪◟◕‵)ノ

iOS Safari 不支援

目前(2025/12/10)測試,iOS Safari 的 CSS + SVG Mask 有問題,無法正常顯示刮除效果,還找不到解法 (╥ω╥`)

技術關鍵字

名稱 描述
Pointer 事件偵測滑鼠或觸控點移動、點擊、懸停等等事件,取得座標、目標等等資訊
SVG MaskSVG 遮罩效果,用於控制元素的顯示區域,可以實現複雜的形狀切割和遮罩

使用範例

基本用法

如同經典刮刮樂,可以刮除表面圖案

查看範例原始碼
vue
<template>
  <div class="w-full flex flex-col gap-4 border border-gray-200 rounded-xl p-6">
    <wrapper-scratch-off
      ref="scratchOffRef"
      class="h-[40vh] w-full"
      @update:remain-ratio="(value) => handleUpdateRemainRatio(value)"
    >
      <div
        class="relative h-full w-full flex flex-col select-none items-center justify-center gap-4 border p-4"
        :class="{ ' pointer-events-none': remainRatioNumber > 85 }"
      >
        <a
          href="https://codlin.me/article-overview.html"
          target="_blank"
          class="text-xl font-bold duration-300"
          :class="{ ' opacity-20': remainRatioNumber > 85 }"
        >
          恭喜中獎!點我領獎!ლ(´ڡ`ლ)
        </a>
        <div
          class="text-xs opacity-70 duration-300"
          :class="{ ' !opacity-0': remainRatioNumber <= 85 }"
        >
          刮太少了,請繼續刮
        </div>

        <a
          href="https://165.npa.gov.tw/"
          target="_blank"
          class="absolute bottom-0 right-0 p-2 text-xs opacity-0 duration-500"
          :class="{ '!opacity-100': remainRatioNumber <= 70 }"
        >
          騙你的,點我才可以領獎ヽ(́◕◞౪◟◕‵)ノ
        </a>
      </div>
    </wrapper-scratch-off>

    <div class="text-center text-2xl font-bold">
      {{ remainRatio }}%
    </div>

    <base-btn
      label="重置"
      @click="handleReset"
    />
  </div>
</template>

<script setup lang="ts">
import { animate } from 'animejs-v4'
import { computed, ref, useTemplateRef } from 'vue'
import BaseBtn from '../../base-btn.vue'
import WrapperScratchOff from '../wrapper-scratch-off.vue'

const remainRatio = ref('100.00')
const remainRatioNumber = computed(() => Number.parseFloat(remainRatio.value))

function handleUpdateRemainRatio(value: number) {
  animate(remainRatio, {
    value: value * 100,
    duration: 300,
    modifier: (value) => value.toFixed(2),
  })
}

const scratchOffRef = useTemplateRef('scratchOffRef')
function handleReset() {
  scratchOffRef.value?.reset()
}
</script>

使用須知

不只要仔細閱讀,還要刮除遮罩 ( ´థ౪థ)

請仔細閱讀以下須知,並刮除遮罩

🐟 鱈魚使用須知

📌 重要聲明

本指南適用於任何與鱈魚相關的活動,例如食用、觀賞、聊天、或試圖與其建立深厚友誼(不建議)。

🍽️ 食用須知

  • 請確保鱈魚已煮熟,除非你是北極熊。
  • 如果你發現鱈魚在盤子上對你微笑,請確認你沒有嗑藥。
  • 鱈魚富含不可名狀物質,吃多了可能出現幻覺。

🐠 觀賞須知

  • 鱈魚外觀樸素,請勿種族歧視。
  • 請勿在水族館對著鱈魚說「你好肥」,牠們也有自尊心。

💬 與鱈魚溝通須知

  • 鱈魚不會講話,請不要對牠進行長篇演講。
  • 如果鱈魚對你點頭,請不要高興得太早,牠可能只是因為水流晃動。
  • 與鱈魚進行心靈溝通時,請確保你沒有餓過頭導致出現幻覺。

🚨 禁忌事項

  1. 請勿將鱈魚放入洗衣機,可能也洗不乾淨。
  2. 請勿將鱈魚作為武器使用,除非已事先凍成冰塊。
  3. 請勿遛鱈魚,因為他不會走路

🎉 結語

請以尊重與幽默的態度對待鱈魚,如有任何不滿,請記得它只是一隻魚

歡迎提出 MR 補充以上須知

閱讀率:100.00%
閱讀率 100% 才可以勾選 (・∀・)9
查看範例原始碼
vue
<template>
  <div class="relative w-full flex flex-center flex-col gap-4 border border-gray-200 rounded-xl p-6">
    <div class="w-full text-center text-lg font-bold">
      請仔細閱讀以下須知,並刮除遮罩
    </div>

    <div class="h-[55vh] overflow-y-auto">
      <wrapper-scratch-off
        ref="wrapperScratchOffRef"
        class="select-none"
        :stroke-width="100"
        trigger-scratch-off-mode="hover"
        @update:remain-ratio="(value) => handleUpdateRemainRatio(value)"
      >
        <h1>🐟 鱈魚使用須知</h1>

        <h2>
          📌 重要聲明
        </h2>
        <p>本指南適用於任何與鱈魚相關的活動,例如食用、觀賞、聊天、或試圖與其建立深厚友誼(不建議)。</p>

        <h2>
          🍽️ 食用須知
        </h2>
        <ul>
          <li>請確保鱈魚已煮熟,除非你是北極熊。</li>
          <li>如果你發現鱈魚在盤子上對你微笑,請確認你沒有嗑藥。</li>
          <li>鱈魚富含不可名狀物質,吃多了可能出現幻覺。</li>
        </ul>

        <h2>
          🐠 觀賞須知
        </h2>
        <ul>
          <li>鱈魚外觀樸素,請勿種族歧視。</li>
          <li>請勿在水族館對著鱈魚說「你好肥」,牠們也有自尊心。</li>
        </ul>

        <h2>
          💬 與鱈魚溝通須知
        </h2>
        <ul>
          <li>鱈魚不會講話,請不要對牠進行長篇演講。</li>
          <li>如果鱈魚對你點頭,請不要高興得太早,牠可能只是因為水流晃動。</li>
          <li>與鱈魚進行心靈溝通時,請確保你沒有餓過頭導致出現幻覺。</li>
        </ul>

        <h2>
          🚨 禁忌事項
        </h2>
        <ol>
          <li>請勿將鱈魚放入洗衣機,可能也洗不乾淨。</li>
          <li>請勿將鱈魚作為武器使用,除非已事先凍成冰塊。</li>
          <li>請勿遛鱈魚,因為他不會走路</li>
        </ol>

        <h2>
          🎉 結語
        </h2>
        <p>請以尊重與幽默的態度對待鱈魚,如有任何不滿,請記得它只是一隻魚</p>
        <p>歡迎提出 MR 補充以上須知</p>

        <template #mask>
          <div class="mask h-full w-full" />
        </template>
      </wrapper-scratch-off>
    </div>

    <div class="text-center text-lg font-bold">
      閱讀率:{{ remainRatio }}%
    </div>

    <div class="text-center text-xs">
      閱讀率 100% 才可以勾選 (・∀・)9
    </div>

    <div
      class="w-full duration-500"
      :class="{ 'cursor-not-allowed opacity-80': !isOk }"
    >
      <base-checkbox
        v-model="value"
        label="我已詳閱以上須知"
        class="w-full border rounded-xl p-4"
        :class="{ ' pointer-events-none': !isOk }"
      />
    </div>

    <transition name="opacity">
      <div
        v-if="value"
        class="absolute inset-0 z-[40] flex flex-col items-center justify-center gap-6 rounded-xl bg-[#383c3d] bg-opacity-90"
      >
        <span class="text-xl text-white tracking-wide">
          感謝您的閱讀!(*´∀`)~♥
        </span>
      </div>
    </transition>
  </div>
</template>

<script setup lang="ts">
import { refAutoReset, whenever } from '@vueuse/core'
import { animate } from 'animejs-v4'
import { computed, ref, useTemplateRef } from 'vue'
import BaseCheckbox from '../../base-checkbox.vue'
import WrapperScratchOff from '../wrapper-scratch-off.vue'

const wrapperScratchOffRef = useTemplateRef('wrapperScratchOffRef')

const remainRatio = ref('100.00')
const isOk = computed(() => Number.parseFloat(remainRatio.value) >= 100)

function handleUpdateRemainRatio(value: number) {
  animate(remainRatio, {
    value: (1 - value) * 100,
    duration: 300,
    modifier: (value) => value.toFixed(2),
  })
}
const value = refAutoReset(false, 2000)
whenever(() => !value.value, () => {
  wrapperScratchOffRef.value?.reset()
})
</script>

<style lang="sass" scoped>
.opacity-enter-active, .opacity-leave-active
  transition-duration: 0.4s
.opacity-enter-from, .opacity-leave-to
  opacity: 0 !important

.mask
  background: url('/low/profile-2.webp') center center
  background-size: 10%
</style>

原理

為了實現自定義遮罩,所以使用 SVG Mask 實現刮除效果,搭配 Canvas 計算刮除比例。

原始碼

API

Props

interface Props {
  disabled?: boolean;
  /** 刮除比例。用於 v-model 綁定 */
  remainRatio?: number;
  /** 刮痕寬度 */
  strokeWidth?: number;
  /** 觸發刮除方式,hover 或按著不放 */
  triggerScratchOffMode?: 'hover' | 'hold';

}

Emits

const emit = defineEmits<{
  'update:remainRatio': [value: number];
}>()

Methods

defineExpose({
  reset,
  /** 刮除比例 */
  remainRatio,
})

Slots

defineSlots<{
  mask?: (params: { remainRatio: number }) => unknown;
  default?: (params: { remainRatio: number }) => unknown;
}>()

v0.55.0