ScratchCard - Scratch Card
ScratchCard is an interactive scratch card component based on Canvas and Vue 3, providing a realistic scratching experience. It supports multiple winning modes including horizontal, vertical, and diagonal lines, as well as custom symbols and winning probability configuration.
📦 Import
<script setup>
import { ScratchCard } from '@randbox/vue'
// Or import types
import type { ScratchCardProps, ScratchCardResult } from '@randbox/vue'
</script>🚀 Basic Usage
<template>
<ScratchCard @scratch="handleScratch" />
</template>
<script setup>
import { ScratchCard } from '@randbox/vue'
const handleScratch = (result) => {
console.log('Scratch result:', result)
if (result.isWinner) {
alert(`🎉 Congratulations! You won ${result.winningInfo.prize}`)
}
}
</script>🎯 Advanced Usage
Custom Grid and Symbols
<template>
<ScratchCard
:rows="4"
:cols="4"
:symbols="symbols"
:win-probability="0.3"
@scratch="handleScratch"
/>
</template>
<script setup>
import { ref } from 'vue'
const symbols = ref(['🍎', '🍊', '🍋', '🍒', '🍇', '💎', '⭐', '🔔'])
const handleScratch = (result) => {
console.log('Custom scratch:', result)
if (result.isWinner) {
const { winningInfo } = result
alert(`Win type: ${winningInfo.name} - ${winningInfo.prize}`)
}
}
</script>Multiple Winning Modes Display
<template>
<div class="scratch-game">
<div class="mode-selector">
<label>Select winning mode: </label>
<select v-model="winningMode">
<option value="all">All modes</option>
<option value="row">Horizontal</option>
<option value="col">Vertical</option>
<option value="diagonal">Diagonal</option>
</select>
</div>
<ScratchCard
:key="cardKey"
:rows="3"
:cols="3"
:symbols="symbols"
:win-probability="0.4"
@scratch="handleScratch"
/>
<div v-if="lastResult" class="result-display">
<h4>Latest scratch result:</h4>
<div>Winner: {{ lastResult.isWinner ? 'Yes' : 'No' }}</div>
<div>Scratch progress: {{ (lastResult.scratchProgress * 100).toFixed(1) }}%</div>
<div v-if="lastResult.isWinner" class="win-info">
Win info: {{ lastResult.winningInfo.name }} - {{ lastResult.winningInfo.prize }}
</div>
</div>
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
const winningMode = ref('all')
const cardKey = ref(0)
const lastResult = ref(null)
const symbols = ref(['🎯', '🎮', '🎲', '🎪', '🎨', '🎭', '🎺', '🎸'])
// Watch mode changes, regenerate card
watch(winningMode, () => {
cardKey.value++
})
const handleScratch = (result) => {
lastResult.value = result
if (result.isWinner) {
const { type, name, prize } = result.winningInfo
console.log(`Win mode: ${type}, Name: ${name}, Prize: ${prize}`)
}
}
</script>
<style scoped>
.scratch-game {
padding: 20px;
}
.mode-selector {
margin-bottom: 20px;
}
.result-display {
margin-top: 20px;
padding: 10px;
background-color: #f5f5f5;
border-radius: 8px;
}
.win-info {
color: green;
font-weight: bold;
}
</style>Scratch Card with Statistics
<template>
<div class="stats-scratch-game">
<!-- Statistics Panel -->
<div class="stats-panel">
<h3>Scratch Statistics</h3>
<div class="stats-grid">
<div class="stat-item">Total Cards: {{ stats.totalCards }}</div>
<div class="stat-item">Wins: {{ stats.wins }}</div>
<div class="stat-item">Losses: {{ stats.losses }}</div>
<div class="stat-item">Win Rate: {{ winRate }}%</div>
<div class="stat-item">Total Prize: ${{ stats.totalPrize }}</div>
</div>
</div>
<div class="current-card">
<h4>Card #{{ currentCard + 1 }}</h4>
</div>
<ScratchCard
:key="currentCard"
:rows="3"
:cols="3"
:symbols="symbols"
:win-probability="0.25"
@scratch="handleScratch"
@new-card="handleNewCard"
/>
<div class="controls">
<button @click="buyNewCard" class="buy-button">
Buy New Card
</button>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const stats = ref({
totalCards: 0,
wins: 0,
losses: 0,
totalPrize: 0
})
const currentCard = ref(0)
const symbols = ref(['💰', '💎', '🏆', '🎁', '⭐', '🍀', '🎯', '💯'])
const winRate = computed(() =>
stats.value.totalCards > 0
? ((stats.value.wins / stats.value.totalCards) * 100).toFixed(1)
: '0.0'
)
const handleNewCard = () => {
currentCard.value++
}
const handleScratch = (result) => {
stats.value.totalCards++
if (result.isWinner) {
stats.value.wins++
stats.value.totalPrize += 100
alert('🎉 Winner! Prize value: $100')
} else {
stats.value.losses++
}
}
const buyNewCard = () => {
handleNewCard()
}
</script>
<style scoped>
.stats-scratch-game {
padding: 20px;
}
.stats-panel {
margin-bottom: 20px;
padding: 15px;
background-color: #e8f4fd;
border-radius: 10px;
text-align: center;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 10px;
margin-top: 10px;
}
.stat-item {
padding: 8px;
background: white;
border-radius: 4px;
font-size: 0.9em;
}
.current-card {
text-align: center;
margin-bottom: 20px;
}
.controls {
text-align: center;
margin-top: 20px;
}
.buy-button {
padding: 10px 20px;
background-color: #007bff;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
.buy-button:hover {
background-color: #0056b3;
}
</style>Themed Scratch Cards
<template>
<div class="themed-scratch">
<div class="theme-selector">
<label>Select theme: </label>
<select v-model="currentTheme">
<option v-for="(theme, key) in themes" :key="key" :value="key">
{{ theme.name }}
</option>
</select>
</div>
<div :class="['themed-container', `theme-${currentTheme}`]">
<h4>{{ themes[currentTheme].name }} Scratch Card</h4>
<ScratchCard
:key="themeKey"
:rows="3"
:cols="3"
:symbols="themes[currentTheme].symbols"
:win-probability="0.3"
@scratch="handleScratch"
/>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
const currentTheme = ref('fruit')
const themeKey = ref(0)
const themes = {
fruit: {
symbols: ['🍎', '🍊', '🍋', '🍒', '🍇', '🥝', '🍓', '🍑'],
name: 'Fruit Theme'
},
gem: {
symbols: ['💎', '💍', '👑', '🏆', '⭐', '✨', '🌟', '💫'],
name: 'Gem Theme'
},
animal: {
symbols: ['🐱', '🐶', '🐸', '🐯', '🦁', '🐻', '🐼', '🐨'],
name: 'Animal Theme'
}
}
// Watch theme changes
watch(currentTheme, () => {
themeKey.value++
})
const handleScratch = (result) => {
if (result.isWinner) {
alert(`🎉 ${themes[currentTheme.value].name} winner!`)
}
}
</script>
<style scoped>
.themed-scratch {
padding: 20px;
}
.theme-selector {
margin-bottom: 20px;
}
.themed-container {
padding: 20px;
border-radius: 15px;
border: 2px solid #ddd;
text-align: center;
}
.theme-fruit {
background-color: #fff3e0;
border-color: #ff9800;
}
.theme-gem {
background-color: #f3e5f5;
border-color: #9c27b0;
}
.theme-animal {
background-color: #e8f5e8;
border-color: #4caf50;
}
</style>Progressive Jackpot Scratch Card
<template>
<div class="progressive-scratch">
<!-- Game Info Panel -->
<div class="game-info">
<div class="info-item jackpot">
<h4>Progressive Jackpot</h4>
<div class="amount">${{ jackpot.toLocaleString() }}</div>
</div>
<div class="info-item balance">
<h4>Account Balance</h4>
<div class="amount">${{ balance }}</div>
</div>
<div class="info-item cards">
<h4>Cards Purchased</h4>
<div class="amount">{{ cardCount }}</div>
</div>
</div>
<div class="card-info">
<div>Card price: ${{ cardPrice }} | Each card adds ${{ cardPrice * 0.5 }} to jackpot</div>
<button
@click="buyNewCard"
:disabled="balance < cardPrice"
class="buy-button"
:class="{ disabled: balance < cardPrice }"
>
Buy New Card (${{ cardPrice }})
</button>
</div>
<ScratchCard
v-if="cardCount > 0"
:key="cardCount"
:rows="3"
:cols="3"
:symbols="symbols"
:win-probability="0.4"
@scratch="handleScratch"
/>
<div v-else class="no-cards">
Buy your first scratch card to start playing!
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const jackpot = ref(1000)
const cardPrice = ref(10)
const balance = ref(100)
const cardCount = ref(0)
const symbols = ref(['💰', '💵', '💴', '💶', '💷', '🏦', '💳', '📈'])
const buyNewCard = () => {
if (balance.value < cardPrice.value) {
alert('Insufficient balance!')
return
}
balance.value -= cardPrice.value
jackpot.value += cardPrice.value * 0.5 // Each card adds half its price to jackpot
cardCount.value++
}
const handleScratch = (result) => {
if (result.isWinner) {
const winAmount = Math.random() > 0.95 ? jackpot.value : Math.floor(Math.random() * 100) + 20
if (winAmount === jackpot.value) {
alert(`🎰 Super Jackpot! Won progressive jackpot $${jackpot.value}!`)
jackpot.value = 1000 // Reset jackpot
} else {
alert(`🎉 Winner! Won $${winAmount}!`)
}
balance.value += winAmount
}
}
</script>
<style scoped>
.progressive-scratch {
padding: 20px;
}
.game-info {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
margin-bottom: 20px;
text-align: center;
}
.info-item {
padding: 15px;
border-radius: 10px;
}
.jackpot {
background-color: #fff3cd;
}
.balance {
background-color: #d1ecf1;
}
.cards {
background-color: #d4edda;
}
.amount {
font-size: 24px;
font-weight: bold;
margin-top: 8px;
}
.jackpot .amount {
color: #d4a853;
}
.balance .amount {
color: #0c5460;
}
.cards .amount {
color: #155724;
}
.card-info {
text-align: center;
margin-bottom: 20px;
}
.buy-button {
margin-top: 15px;
padding: 12px 24px;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 16px;
background-color: #28a745;
}
.buy-button.disabled {
background-color: #6c757d;
cursor: not-allowed;
}
.no-cards {
text-align: center;
padding: 40px;
background-color: #f8f9fa;
border-radius: 10px;
color: #6c757d;
}
</style>📋 API Reference
Props
| Property | Type | Default | Description |
|---|---|---|---|
rows | number | 3 | Number of grid rows |
cols | number | 3 | Number of grid columns |
symbols | string[] | ['🍎', '🍊', '🍋', '🍒'] | Available symbols array |
winProbability | number | 0.2 | Winning probability (between 0-1) |
class | string | '' | CSS class name |
style | Record<string, any> | {} | Inline styles |
disabled | boolean | false | Whether disabled |
Events
| Event | Parameters | Description |
|---|---|---|
scratch | (result: ScratchCardResult) | Scratch completion callback |
new-card | () | New card generation callback |
game-start | () | Game start callback |
game-end | (result: ScratchCardResult) | Game end callback |
ScratchCardResult
interface ScratchCardResult {
grid: string[][] // Card grid data
isWinner: boolean // Whether winner
scratchProgress?: number // Scratch progress (0-1)
winningInfo?: {
pattern: string[] // Winning pattern
name: string // Winning type name
prize: string // Prize description
symbol?: string // Winning symbol
type?: 'row' | 'col' | 'diagonal' // Winning type
positions: Array<{ // Winning positions
row: number
col: number
}>
}
}🎨 Style Customization
Container Styles
<style scoped>
.scratch-card-container {
border: 3px solid #ffc107;
border-radius: 15px;
box-shadow: 0 8px 25px rgba(255, 193, 7, 0.3);
background: linear-gradient(145deg, #fff9c4, #ffecb5);
}
.scratch-card-container:hover {
transform: scale(1.02);
transition: transform 0.3s ease;
}
</style>Winning Effects
<style scoped>
.winning-card {
animation: winPulse 2s ease-in-out infinite;
border-color: #28a745;
box-shadow: 0 0 30px rgba(40, 167, 69, 0.5);
}
@keyframes winPulse {
0%, 100% {
box-shadow: 0 0 30px rgba(40, 167, 69, 0.5);
}
50% {
box-shadow: 0 0 50px rgba(40, 167, 69, 0.8);
}
}
</style>🔧 Advanced Features
Custom Winning Rules
<script setup>
import { ref } from 'vue'
const symbols = ref(['A', 'B', 'C', 'D', 'E'])
// Custom winning detection logic
const checkCustomWinning = (grid) => {
// Check special patterns
const center = grid[1][1]
const corners = [grid[0][0], grid[0][2], grid[2][0], grid[2][2]]
// Rule 1: Center + four corners same
if (corners.every(symbol => symbol === center)) {
return {
isWinner: true,
type: 'center_corners',
name: 'Center Corners',
prize: 'Special Reward'
}
}
// Rule 2: X pattern
if (grid[0][0] === grid[1][1] && grid[1][1] === grid[2][2] &&
grid[0][2] === grid[1][1] && grid[1][1] === grid[2][0]) {
return {
isWinner: true,
type: 'x_pattern',
name: 'X Pattern',
prize: 'Grand Prize'
}
}
return { isWinner: false }
}
const handleScratch = (result) => {
const customResult = checkCustomWinning(result.grid)
if (customResult.isWinner) {
alert(`Special win: ${customResult.name} - ${customResult.prize}`)
}
}
</script>Animation Enhancement
<template>
<div class="animated-scratch" :class="{ scratching: isScratching }">
<ScratchCard
@game-start="handleGameStart"
@game-end="handleGameEnd"
/>
<Transition name="win-celebration">
<div v-if="lastWin" class="win-overlay">
🎉 {{ lastWin.name }} 🎉
<br />
{{ lastWin.prize }}
</div>
</Transition>
</div>
</template>
<script setup>
import { ref } from 'vue'
const isScratching = ref(false)
const lastWin = ref(null)
const handleGameStart = () => {
isScratching.value = true
}
const handleGameEnd = (result) => {
isScratching.value = false
if (result.isWinner) {
lastWin.value = result.winningInfo
// Delay hiding win animation
setTimeout(() => {
lastWin.value = null
}, 3000)
}
}
</script>
<style scoped>
.animated-scratch {
position: relative;
}
.scratching {
animation: scratchShake 0.2s ease-in-out infinite;
}
@keyframes scratchShake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-1px); }
75% { transform: translateX(1px); }
}
.win-overlay {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: rgba(40, 167, 69, 0.9);
color: white;
padding: 20px;
border-radius: 10px;
text-align: center;
z-index: 10;
}
.win-celebration-enter-active {
animation: bounceIn 0.5s;
}
.win-celebration-leave-active {
animation: fadeOut 0.3s;
}
@keyframes bounceIn {
0% {
transform: translate(-50%, -50%) scale(0);
}
50% {
transform: translate(-50%, -50%) scale(1.1);
}
100% {
transform: translate(-50%, -50%) scale(1);
}
}
@keyframes fadeOut {
from { opacity: 1; }
to { opacity: 0; }
}
</style>🎯 Best Practices
1. Winning Probability Design
<script setup>
// Reasonable winning probability configuration
const probabilities = {
easy: 0.4, // Easy mode, 40% win rate
normal: 0.25, // Normal mode, 25% win rate
hard: 0.15, // Hard mode, 15% win rate
extreme: 0.05 // Extreme mode, 5% win rate
}
</script>2. Symbol Design Recommendations
<script setup>
// Recommended symbol combinations
const symbolSets = {
// High contrast, easily distinguishable
classic: ['🔴', '🟢', '🔵', '🟡', '🟣', '🟠', '⚫', '⚪'],
// Themed symbols
casino: ['🎰', '🃏', '🎲', '💰', '💎', '🏆', '⭐', '🍀'],
// Avoid using similar symbols
bad: ['😀', '😃', '😄', '😁'] // Too similar, hard for users to distinguish
}
</script>3. Error Handling
<template>
<div>
<div v-if="error" class="error-message">
Error: {{ error }}
</div>
<ScratchCard
:rows="3"
:cols="3"
:win-probability="0.3"
@scratch="handleScratch"
@error="handleError"
/>
</div>
</template>
<script setup>
import { ref } from 'vue'
const error = ref('')
const handleScratch = (result) => {
error.value = ''
console.log('Scratch success:', result)
}
const handleError = (err) => {
error.value = err.message
console.error('Scratch card error:', err)
}
</script>
<style scoped>
.error-message {
color: red;
margin-bottom: 10px;
padding: 10px;
background-color: #ffe6e6;
border-radius: 4px;
}
</style>🐛 Common Issues
Q: How to achieve true randomness?
A: The component uses RandBox’s Mersenne Twister algorithm, providing high-quality random number generation to ensure each scratch is truly random.
Q: Can scratch gestures be customized?
A: Currently the component supports mouse and touch scratching, scratch area and sensitivity can be adjusted through Canvas events.
Q: How to control winning probability?
A: Use the winProbability property, range is 0-1, for example 0.3 represents 30% winning probability.
Q: What winning modes are supported?
A: Supports horizontal, vertical, and diagonal three basic modes, and more complex winning rules can be implemented through custom logic.
Q: How to integrate with Vue Router?
A: Can be used normally in route components, or dynamically configure symbols and probability through route parameters.