效果展示
刮刮卡是?種常見的網頁交互元素,通過模擬物理世界的刮涂層來揭示下方的內容。這種效果主要依賴于HTML5的 元素來實現。以下是?個基于TypeScript的刮刮卡實現示例,包括配置項、初始化方法和核心的刮開邏輯。下面是展示的效果
部分刮開效果:
具體實現
配置項
- 蒙層圖片:可以是純色或圖片。
- 刮卡畫筆半徑:控制刮除區域的大小。
- 顯示全部的比例:當刮除面積達到?定比例時,自動顯示全部內容。
- 淡出時間:刮開涂層后的淡出動畫時間。
- Canvas元素:用于繪制刮刮卡的HTML5 元素。
代碼實現
首先,我們創建?個 ScratchCard.ts 文件,并定義 ScratchCard
類及其配置項接口。
interface ScratchCardConfig {canvas: HTMLCanvasElement; //傳?的元素showAllPercent: number; //1. 到達什么?例之后展示全部coverImg?: string; //蒙層的圖?coverColor?: string; //純?蒙層doneCallback?: () => void; //完成之后的回調radius: number; //1. 刮卡畫筆的半徑fadeOut: number; //淡出時間
}
class ScratchCard {private config: ScratchCardConfig;private canvas: HTMLCanvasElement;private ctx: CanvasRenderingContext2D | null;private offsetX: number;private offsetY: number;private isDown: boolean;private done: boolean;constructor(config: Partial<ScratchCardConfig>) {const defaultConfig: ScratchCardConfig = {canvas: config.canvas!,showAllPercent: 65,coverImg: undefined,coverColor: undefined,doneCallback: undefined,radius: 30,fadeOut: 2000,};this.config = { ...defaultConfig, ...config };this.canvas = this.config.canvas;this.ctx = null;this.offsetX = 0;this.offsetY = 0;this.isDown = false;this.done = false;}
}
init實現
然后就是要寫我們的init方法,在init?我們需要初始化數據并且把蒙層先畫出來
重點解析
drawImage
用于繪制圖像。
fillRect
用于繪制矩形,并且通過fillStyle屬性設置繪制的顏色。
并且在繪制之前先充值了?下操作模式
globalCompositeOperation
用于標識要使用哪種合成或混合模式操作。
'source-over'
是默認設置,在現有畫布內容上繪制新形狀
'destination-out'
是將現有內容將保留在不與新形狀重疊的位置,具體來說,它會在源圖形和目標圖形相交的區域,將目標圖形的顏色變為透明。這里我們設置的蒙層就是目標圖形
為了確保我們的刮刮卡生效,防止操作模式干擾,所以在init時直接將 globalCompositeOperation
重置為 'source-over'
class ScratchCard {private _init(): void {this.ctx = this.canvas.getContext('2d');this.offsetX = this.canvas.offsetLeft;this.offsetY = this.canvas.offsetTop;this._addEvent();if (this.config.coverImg) {const coverImg = new Image();// 添加跨域設置coverImg.crossOrigin = 'anonymous';coverImg.src = this.config.coverImg;coverImg.onload = () => {if (this.ctx) {this.ctx.globalCompositeOperation = 'source-over'; // 重置組合操作
模式this.ctx.drawImage(coverImg, 0, 0);this.ctx.globalCompositeOperation = 'destination-out';}};// 添加錯誤處理coverImg.onerror = (e) => {console.error('Image loading error:', e);// 加載失敗時使?純?背景作為后備?案if (this.ctx) {this.ctx.fillStyle = this.config.coverColor || '#CCCCCC';this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);this.ctx.globalCompositeOperation = 'destination-out';}};} else if (this.ctx && this.config.coverColor) {this.ctx.fillStyle = this.config.coverColor;this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);this.ctx.globalCompositeOperation = 'destination-out';}}
}
scratch實現
在實現刮刮卡的刮開效果時,關鍵在于通過 scratch 方法來模擬真實的刮除體驗。在此過程中,一個至關重要的細節是正確處理觸摸事件中的坐標獲取。并且繪制鼠標劃過的地方。
重點解析
這里在獲取的時候不能直接獲取 touch.clientX
和 touch.clientY
,因為Canvas 元素的實際
像素尺寸(往往與其在頁面上的顯示尺寸(通過 CSS 設置寬度和高度)不同步。如果直接使用未
縮放的坐標繪制內容,可能不會在正確的位置渲染,尤其是在高分辨率屏幕或有縮放的頁面布局
中。通過這種方式,我們確保了繪制坐標的準確性。
繪制時我們先通過 beginPath
繪制?條路徑,然后通過 arc 和之前傳?的半徑值來創建一個圓,最后使用 fill 方法進行填充,在混合透明的前提下,這里就會展示出被擦除的效果
class ScratchCard {private _scratch(e: MouseEvent | TouchEvent): void {e.preventDefault();if (!this.done && this.isDown && this.ctx) {let eventX: number;let eventY: number;const rect = this.canvas.getBoundingClientRect();if ('changedTouches' in e) {const touch = e.changedTouches[0];eventX = (touch.clientX - rect.left) * (this.canvas.width / rect.w
idth);eventY = (touch.clientY - rect.top) * (this.canvas.height / rect.h
eight);} else {eventX = (e.clientX + document.body.scrollLeft || e.pageX) - this.
offsetX || 0;eventY = (e.clientY + document.body.scrollTop || e.pageY) - this.o
ffsetY || 0;}//開始繪制this.ctx.beginPath();this.ctx.arc(eventX, eventY, this.config.radius, 0, Math.PI * 2);this.ctx.fill();// 如果透明的元素?例?于設置的值,則全部展現if (this._getFilledPercentage() > this.config.showAllPercent) {this._scratchAll();}}}
}
getFilledPercentage實現
然后就需要計算已經被擦除的比例,這里通過計算透明的像素的占比來確定擦除的比例
class ScratchCard {private _getFilledPercentage(): number {if (!this.ctx) return 0;// 獲取畫布的像素數據const imgData = this.ctx.getImageData(0, 0, this.canvas.width, this.ca
nvas.height);const pixels = imgData.data;const transPixels: number[] = [];// 遍歷像素數據(?組像素有四個值RGBA所以需要+4)for (let i = 0; i < pixels.length; i += 4) {// 計算透明度是否?于128(128是0~255的中間值,低于128就被認為是半透明或透明
的)if (pixels[i + 3] < 128) {transPixels.push(pixels[i + 3]);}}// 返回百分?數據return Number(((transPixels.length / (pixels.length / 4)) * 100).toFixed(2));}
}
scratchAll實現
然后就是全部刮開的方法,這?需要處理?下淡出以及剩余的元素變透明的邏輯
class ScratchCard {private _scratchAll(): void {this.done = true;// 需要漸隱就添加漸隱效果,不需要就直接clearif (this.config.fadeOut > 0) {this.canvas.style.transition = `all ${this.config.fadeOut / 1000}s l
inear`;this.canvas.style.opacity = '0';setTimeout(() => {this._clear();}, this.config.fadeOut);} else {this._clear();}}private _clear(): void {if (this.ctx) {// destination-out 模式下,它會變成透明this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);}// 如果有傳?的回調就執?if (this.config.doneCallback) {this.config.doneCallback();}}
}
事件處理
最后我們只需要再加上事件監聽,就可以實現刮刮卡的效果咯
重點解析
在addEventListener的option里,默認passive是false。但是如果事件是 touchstart
或touchmove
的話,passive的默認值則會變成true(所以preventDefault就會被忽略了),所以這里單獨給他寫出來
class ScratchCard {private _addEvent(): void {this.canvas.addEventListener('touchstart',this._eventDown.bind(this),{
passive: false });this.canvas.addEventListener('touchend',this._eventUp.bind(this), { pa
ssive: false });this.canvas.addEventListener('touchmove',this._scratch.bind(this), { p
assive: false });this.canvas.addEventListener('mousedown',this._eventDown.bind(this), {
passive: false });this.canvas.addEventListener('mouseup', this._eventUp.bind(this), { pa
ssive: false });this.canvas.addEventListener('mousemove',this._scratch.bind(this), { p
assive: false });}private _eventDown(e: MouseEvent | TouchEvent): void {e.preventDefault();this.isDown = true;}private _eventUp(e: MouseEvent | TouchEvent): void {e.preventDefault();this.isDown = false;}
}