Introduction:
Hello friends, Unlock the secrets to creating an interactive scratch card using HTML, CSS, and JavaScript! In this step-by-step tutorial, we'll guide you through the process of designing a fully functional scratch card from scratch (pun intended). Learn how to implement the scratch-off effect, add animations, and make your web projects more engaging and fun. Whether you're building a promotional feature or just want to add some interactive flair to your website, this tutorial has everything you need. Perfect for beginners and experienced developers alike, this guide will elevate your web development skills and inspire creativity. Start scratching the surface of web interactivity today!
This is the index.html file code.
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="style.css" />
<title>coding30.com | Simple Scratch Card</title>
</head>
<body >
<div class="scratch-card">
<div class="scratch-card-cover-container">
<canvas class="scratch-card-canvas" width="320" height="320"></canvas>
<img class="scratch-card-canvas-render hidden" alt="">
<div class="scratch-card-cover shine">
<img src="trophy.gif" width="220"/>
</div>
</div>
<img class="scratch-card-image" src="gift_cards.png" alt="Apple 500$ gift card">
</div>
<p class="scratch-card-text">🎁 Scratch for a surprise!</p>
<svg width="0" height="0">
<filter id="remove-black" color-interpolation-filters="sRGB">
<feColorMatrix
type="matrix"
values="1 0 0 0 0
0 1 0 0 0
0 0 1 0 0
-1 -1 -1 0 1"
result="black-pixels"
/>
<feComposite in="SourceGraphic" in2="black-pixels" operator="out" />
</filter>
<filter id="noise">
<feTurbulence baseFrequency="0.5"></feTurbulence>
</filter>
</svg>
<script src="script.js"></script>
<script src="confetti.js"></script>
</body>
</html>
This is the style.css file code.
body {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
min-height: 100vh;
row-gap: 1em;
background-image: linear-gradient(60deg, #96deda 0%, #50c9c3 100%);
font-family: sans-serif;
}
.scratch-card {
position: relative;
border: 4px solid #764ba2;
border-radius: 8px;
padding: 12px;
/* width: 320px; */
height: 320px;
background-color: #fff;
}
.scratch-card-cover-container {
position: absolute;
z-index: 1;
top: 0;
left: 0;
border-radius: 4px;
width: 100%;
height: 100%;
filter: url("#remove-black");
transition: opacity 0.4s;
}
.scratch-card-cover-container.clear {
opacity: 0;
}
.scratch-card-cover-container.hidden {
display: none;
}
.scratch-card-canvas {
position: absolute;
z-index: 2;
top: 0;
left: 0;
width: 100%;
height: 100%;
cursor: grab;
touch-action: none;
}
.scratch-card-canvas.hidden {
opacity: 0;
}
.scratch-card-canvas:active {
cursor: grabbing;
}
.scratch-card-canvas-render {
position: absolute;
z-index: 1;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: transparent;
transition: background-color 0.2s;
}
.scratch-card-canvas-render.hidden {
display: none;
}
.scratch-card-cover {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: #E90074;
background-image: linear-gradient(-225deg, #AC32E4 0%, #7918F2 48%, #4801FF 100%);
overflow: hidden;
}
.scratch-card-cover::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: linear-gradient(135deg, transparent 40%, rgba(195, 24, 141, 0.825) 50%, transparent 60%);
background-position: bottom right;
background-size: 300% 300%;
background-repeat: no-repeat;
}
.scratch-card-cover.shine::before {
animation: shine 8s infinite;
}
@keyframes shine {
50% {
background-position: 0% 0%;
}
100% {
background-position: -50% -50%;
}
}
.scratch-card-cover::after {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0.1;
filter: url("#noise");
}
.scratch-card-cover-background {
width: 100%;
height: 100%;
fill: #555;
opacity: 0.1;
}
.scratch-card-image {
border-radius: 4px;
width: 100%;
height: 100%;
object-fit: contain;
filter: drop-shadow(0 4px 4px rgba(0, 0, 0, 0.16));
user-select: none;
will-change: transform;
}
.scratch-card-image.animate {
animation: pop-out-in cubic-bezier(0.65, 1.35, 0.5, 1) 1s;
}
@keyframes pop-out-in {
36% {
transform: scale(1.125);
}
100% {
transform: scale(1);
}
}
This is the script.js file code.
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
const scratchCardCover = document.querySelector('.scratch-card-cover');
const scratchCardCanvasRender = document.querySelector('.scratch-card-canvas-render');
const scratchCardCoverContainer = document.querySelector('.scratch-card-cover-container');
const scratchCardText = document.querySelector('.scratch-card-text');
const scratchCardImage = document.querySelector('.scratch-card-image');
const canvas = document.querySelector('canvas');
const context = canvas.getContext('2d');
let isPointerDown = false;
let positionX;
let positionY;
let clearDetectionTimeout = null;
const devicePixelRatio = window.devicePixelRatio || 1;
const canvasWidth = canvas.offsetWidth * devicePixelRatio;
const canvasHeight = canvas.offsetHeight * devicePixelRatio;
canvas.width = canvasWidth;
canvas.height = canvasHeight;
context.scale(devicePixelRatio, devicePixelRatio);
if (isSafari) {
canvas.classList.add('hidden');
}
canvas.addEventListener('pointerdown', (e) => {
scratchCardCover.classList.remove('shine');
({ x: positionX, y: positionY } = getPosition(e));
clearTimeout(clearDetectionTimeout);
canvas.addEventListener('pointermove', plot);
window.addEventListener('pointerup', (e) => {
canvas.removeEventListener('pointermove', plot);
clearDetectionTimeout = setTimeout(() => {
checkBlackFillPercentage();
}, 500);
}, { once: true });
});
const checkBlackFillPercentage = () => {
const imageData = context.getImageData(0, 0, canvasWidth, canvasHeight);
const pixelData = imageData.data;
let blackPixelCount = 0;
for (let i = 0; i < pixelData.length; i += 4) {
const red = pixelData[i];
const green = pixelData[i + 1];
const blue = pixelData[i + 2];
const alpha = pixelData[i + 3];
if (red === 0 && green === 0 && blue === 0 && alpha === 255) {
blackPixelCount++;
}
}
const blackFillPercentage = blackPixelCount * 100 / (canvasWidth * canvasHeight);
if (blackFillPercentage >= 45) {
scratchCardCoverContainer.classList.add('clear');
confetti({
particleCount: 100,
spread: 90,
origin: {
y: (scratchCardText.getBoundingClientRect().bottom + 60) / window.innerHeight,
},
});
scratchCardText.textContent = '🎉 You got a $500 gift card!';
scratchCardImage.classList.add('animate');
scratchCardCoverContainer.addEventListener('transitionend', () => {
scratchCardCoverContainer.classList.add('hidden');
}, { once: true });
}
}
const getPosition = ({ clientX, clientY }) => {
const { left, top } = canvas.getBoundingClientRect();
return {
x: clientX - left,
y: clientY - top,
};
}
const plotLine = (context, x1, y1, x2, y2) => {
var diffX = Math.abs(x2 - x1);
var diffY = Math.abs(y2 - y1);
var dist = Math.sqrt(diffX * diffX + diffY * diffY);
var step = dist / 50;
var i = 0;
var t;
var x;
var y;
while (i < dist) {
t = Math.min(1, i / dist);
x = x1 + (x2 - x1) * t;
y = y1 + (y2 - y1) * t;
context.beginPath();
context.arc(x, y, 16, 0, Math.PI * 2);
context.fill();
i += step;
}
}
const setImageFromCanvas = () => {
canvas.toBlob((blob) => {
const url = URL.createObjectURL(blob);
previousUrl = scratchCardCanvasRender.src;
scratchCardCanvasRender.src = url;
if (!previousUrl) {
scratchCardCanvasRender.classList.remove('hidden');
} else {
URL.revokeObjectURL(previousUrl);
}
previousUrl = url;
});
}
let setImageTimeout = null;
const plot = (e) => {
const { x, y } = getPosition(e);
plotLine(context, positionX, positionY, x, y);
positionX = x;
positionY = y;
if (isSafari) {
clearTimeout(setImageTimeout);
setImageTimeout = setTimeout(() => {
setImageFromCanvas();
}, 5);
}
};
Video tutorial here...
Preview...