共计 9298 个字符,预计需要花费 24 分钟才能阅读完成。
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>
正文完