1.刮刮乐(橡皮擦)成果的外围api
ctx.globalCompositeOperation = type;
设置要在绘制新形态时利用的合成操作的类型。
咱们这里须要用到的类型是 destination-out
此属性的详细信息:MDN文档
2.根底版刮刮乐性能
canvs 笼罩在图片上
<style> body { margin: 0; } img { width: 400px; height: 300px; left: 200px; position: absolute; z-index: -1; } canvas { margin-left: 200px; } </style> <img src="./test.jpg" alt="pic"/> <canvas id="canvas" width="400" height="300"></canvas>
<script> let canvas = document.querySelector('#canvas'); let context = canvas.getContext('2d'); // 绘制涂层 context.beginPath(); context.fillStyle = 'grey'; context.fillRect(0, 0, 400, 300); // 监听鼠标挪动事件 canvas.addEventListener('mousemove', (e) => { // 当鼠标左键按下&&挪动鼠标时,革除鼠标左近涂层 if (e.which === 1 && e.button === 0) { const x = e.clientX, y = e.clientY; context.globalCompositeOperation = 'destination-out'; context.beginPath(); // 革除以鼠标地位为圆心,半径为10px的圆的范畴 context.arc(x - 200, y, 10, 0, Math.PI * 2); context.fill(); } }) </script>
3.进阶版刮刮乐性能
进阶性能:
点击时,以以后地位为圆心刮开一部分区域;
刮开x百分比(能够自定义)后后显示全副,并且应用动画逐步变淡;
调用第一次刮的回调办法和刮完的回调办法,能够传入或不传;
涂层下面能够显示自定义文字;
首先改为class模式,不便屡次创立刮刮乐。
class Scratch { constructor(id, { maskColor = 'grey', cursorRadius = 10 } = {}) { this.canvas = document.getElementById('canvas'); this.context = this.canvas.getContext('2d'); this.width = this.canvas.clientWidth; this.height = this.canvas.clientHeight; this.maskColor = maskColor; // 涂层色彩 this.cursorRadius = cursorRadius; // 光标半径 this.init(); } init() { // 增加涂层 this.addCoat(); let bindEarse = this.erase.bind(this); this.canvas.addEventListener('mousedown', (e) => { // 按下左键 if (e.which === 1 && e.button === 0) { // 擦掉涂层 this.canvas.addEventListener('mousemove', bindEarse); } }) document.addEventListener('mouseup', () => { this.canvas.removeEventListener('mousemove', bindEarse); }) } addCoat() { this.context.beginPath(); this.context.fillStyle = this.maskColor; this.context.fillRect(0, 0, this.width, this.height); } erase(e) { const x = e.clientX, y = e.clientY; this.context.globalCompositeOperation = 'destination-out'; this.context.beginPath(); this.context.arc(x - this.width / 2, y, this.cursorRadius, 0, Math.PI * 2); this.context.fill(); } } new Scratch('canvas');
而后,记录鼠标地位,mouseup时判断是点击还是点击&挪动鼠标,如果是点击则以以后地位为圆心刮开一部分区域;
this.canvas.addEventListener('mousedown', (e) => { this.posX = e.clientX; this.posY = e.clientY; ...}) document.addEventListener('mouseup', (e) => { if (this.posX === e.clientX && this.posY === e.clientY) { this.erase(e); } ...})
而后,判断刮开面积是否超过一半,如果是清空涂层;
ImageData ctx.getImageData(sx, sy, sw, sh);
sx:将要被提取的图像数据矩形区域的左上角 x 坐标。
sy:将要被提取的图像数据矩形区域的左上角 y 坐标。
sw:将要被提取的图像数据矩形区域的宽度。
sh:将要被提取的图像数据矩形区域的高度。
ImageData 对象,蕴含canvas给定的矩形图像数据。能够用来判断是否被刮开。
每4个元素示意一个像素点的rgba值,所以能够判断第4个的值是否小于256的一半即128,如果小于128即可视为通明(被刮开)。
清空指定区域内容:
void ctx.clearRect(x, y, width, height);
document.addEventListener('mouseup', (e) => { this.getScratchedPercentage(); if (this.currPerct >= this.maxEraseArea) { this.context.clearRect(0, 0, this.width, this.height); }})getScratchedPercentage() { const pixels = this.context.getImageData(0, 0, this.width, this.height).data; let transparentPixels = 0; for (let i = 0; i < pixels.length; i += 4) { if (pixels[i + 3] < 128) { transparentPixels++; } } this.currPerct = (transparentPixels / pixels.length * 4 * 100).toFixed(2);}
而后,设置第一次刮的回调办法和刮完的回调办法;
constructor(id, { maskColor = 'grey', cursorRadius = 10, maxEraseArea = 50, firstEraseCbk = () => { }, lastEraseCbk = () => { } } = {}) { ... this.firstEraseCbk = firstEraseCbk; // 第一次刮的回调函数 this.lastEraseCbk = lastEraseCbk; // 刮开的回调函数}this.canvas.addEventListener('mousedown', (e) => { if (this.currPerct === 0) { this.firstEraseCbk(); }})document.addEventListener('mouseup', (e) => { if (this.currPerct >= this.maxEraseArea) { this.context.clearRect(0, 0, this.width, this.height); this.lastEraseCbk(); }})
而后,刮开全副时缓缓清空涂层,设置背景色突变成果;requestAnimationFrame
做进去的动画更晦涩
回调函数用闭包模式能够给回调函数传参
document.addEventListener('mouseup', (e) => { if (this.currPerct >= this.maxEraseArea) { this.done = true; requestAnimationFrame(this.fadeOut(255)); this.lastEraseCbk(); }})fadeOut(alpha) { return () => { this.context.save(); this.context.globalCompositeOperation = 'source-in'; this.context.fillStyle = this.context.fillStyle + (alpha -= 1).toString(16); this.context.fillRect(0, 0, this.width, this.height); this.context.restore(); // 到210曾经看不到涂层了 if (alpha > 210) { requestAnimationFrame(this.fadeOut(alpha)); } }}
而后,初始化涂层的时候,再涂层上显示自定义文字;
addCoat() { ... if (this.text) { this.context.font = 'bold 48px serif'; this.context.fillStyle = '#fff'; this.context.textAlign = 'center'; this.context.textBaseline = 'middle'; this.context.fillText(this.text, this.width / 2, this.height / 2); }}
残缺代码
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <style> body { margin: 0; } img { width: 400px; height: 300px; left: 200px; position: absolute; z-index: -1; } canvas { margin-left: 200px; } </style></head><body> <img src="./test.jpg" alt="pic" /> <canvas id="canvas" width="400" height="300"></canvas> <script> class Scratch { constructor(id, { maskColor = 'grey', cursorRadius = 10, maxEraseArea = 50, text = '', firstEraseCbk = () => { }, lastEraseCbk = () => { } } = {}) { this.canvasId = id; this.canvas = document.getElementById(id); this.context = this.canvas.getContext('2d'); this.width = this.canvas.clientWidth; this.height = this.canvas.clientHeight; this.maskColor = maskColor; // 涂层色彩 this.cursorRadius = cursorRadius; // 光标半径 this.maxEraseArea = maxEraseArea; // 刮开多少后主动清空涂层 this.text = text; this.firstEraseCbk = firstEraseCbk; // 第一次刮的回调函数 this.lastEraseCbk = lastEraseCbk; // 刮开的回调函数 this.currPerct = 0; // 以后刮开多少百分比 this.done = false; // 是否刮完 this.init(); } init() { // 增加涂层 this.addCoat(); let bindEarse = this.erase.bind(this); this.canvas.addEventListener('mousedown', e => { if (this.done) { return; } this.posX = e.clientX; this.posY = e.clientY; // 按下左键 if (e.which === 1 && e.button === 0) { // 擦掉涂层 this.canvas.addEventListener('mousemove', bindEarse); } if (this.currPerct === 0) { this.firstEraseCbk(); } }) document.addEventListener('mouseup', e => { if (this.done) { return; } if (e.target.id !== this.canvasId) { return; } if (this.posX === e.clientX && this.posY === e.clientY) { this.erase(e); } this.canvas.removeEventListener('mousemove', bindEarse); this.getScratchedPercentage(); if (this.currPerct >= this.maxEraseArea) { this.done = true; requestAnimationFrame(this.fadeOut(255)); this.lastEraseCbk(); } }) } // 增加涂层 addCoat() { this.context.beginPath(); this.context.fillStyle = this.maskColor; this.context.fillRect(0, 0, this.width, this.height); // 绘制涂层上的文字 if (this.text) { this.context.font = 'bold 48px serif'; this.context.fillStyle = '#fff'; this.context.textAlign = 'center'; this.context.textBaseline = 'middle'; this.context.fillText(this.text, this.width / 2, this.height / 2); } } // 擦除某地位涂层 erase(e) { const x = e.clientX, y = e.clientY; this.context.globalCompositeOperation = 'destination-out'; this.context.beginPath(); this.context.arc(x - this.width / 2, y, this.cursorRadius, 0, Math.PI * 2); this.context.fill(); } // 计算被擦除的局部占全副的百分比 getScratchedPercentage() { const pixels = this.context.getImageData(0, 0, this.width, this.height).data; let transparentPixels = 0; for (let i = 0; i < pixels.length; i += 4) { if (pixels[i + 3] < 128) { transparentPixels++; } } this.currPerct = (transparentPixels / pixels.length * 4 * 100).toFixed(2); } // 清空涂层时淡出成果 fadeOut(alpha) { return () => { this.context.save(); this.context.globalCompositeOperation = 'source-in'; this.context.fillStyle = this.context.fillStyle + (alpha -= 1).toString(16); this.context.fillRect(0, 0, this.width, this.height); this.context.restore(); // 到210曾经看不到涂层了 if (alpha > 210) { requestAnimationFrame(this.fadeOut(alpha)); } } } } new Scratch('canvas', { text: '刮一刮', maxEraseArea: 10 }); </script></body></html>