刮刮樂包裝器 wrapper
讓任意內容一秒變成刮刮樂 ヽ(́◕◞౪◟◕‵)ノ
iOS Safari 不支援
目前(2025/12/10)測試,iOS Safari 的 CSS + SVG Mask 有問題,無法正常顯示刮除效果,還找不到解法 (╥ω╥`)
技術關鍵字
| 名稱 | 描述 |
|---|---|
| Pointer 事件 | 偵測滑鼠或觸控點移動、點擊、懸停等等事件,取得座標、目標等等資訊 |
| SVG Mask | SVG 遮罩效果,用於控制元素的顯示區域,可以實現複雜的形狀切割和遮罩 |
使用範例
基本用法
如同經典刮刮樂,可以刮除表面圖案
100.00%
查看範例原始碼
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>使用須知
不只要仔細閱讀,還要刮除遮罩 ( ´థ౪థ)
請仔細閱讀以下須知,並刮除遮罩
🐟 鱈魚使用須知
📌 重要聲明
本指南適用於任何與鱈魚相關的活動,例如食用、觀賞、聊天、或試圖與其建立深厚友誼(不建議)。
🍽️ 食用須知
- 請確保鱈魚已煮熟,除非你是北極熊。
- 如果你發現鱈魚在盤子上對你微笑,請確認你沒有嗑藥。
- 鱈魚富含不可名狀物質,吃多了可能出現幻覺。
🐠 觀賞須知
- 鱈魚外觀樸素,請勿種族歧視。
- 請勿在水族館對著鱈魚說「你好肥」,牠們也有自尊心。
💬 與鱈魚溝通須知
- 鱈魚不會講話,請不要對牠進行長篇演講。
- 如果鱈魚對你點頭,請不要高興得太早,牠可能只是因為水流晃動。
- 與鱈魚進行心靈溝通時,請確保你沒有餓過頭導致出現幻覺。
🚨 禁忌事項
- 請勿將鱈魚放入洗衣機,可能也洗不乾淨。
- 請勿將鱈魚作為武器使用,除非已事先凍成冰塊。
- 請勿遛鱈魚,因為他不會走路
🎉 結語
請以尊重與幽默的態度對待鱈魚,如有任何不滿,請記得它只是一隻魚
歡迎提出 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;
}>()