前言
相信大家在平时生活中,应该遇到过电子签名和电子刮刮卡的情况吧,其实这俩种效果都可以利用 canvas 来实现,本篇文章就带着大家一起实现一下这俩个小案例。
写字板
这里我用原生 HTML 写,这样大家可以更好根据这个进行 UI 的扩展
HTML 代码
先写这样的一段 HTML 代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <div id="setting-line"> <div> 字体大小: <select id="font-size"></select> </div> <div> 字体颜色: <input type="color" id="font-color" /> </div> <button id="undo">撤销</button> <button id="clear">清空画布</button> <button id="download">生成图片</button> </div> <canvas width="800" height="300" id="canvas"></canvas>
|
CSS 代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| #setting-line { display: flex; margin: auto; margin-top: 100px; line-height: 27px; justify-content: center; gap: 20px; }
#canvas { display: block; margin: auto; margin-top: 10px; background-color: #eee; }
|
JS 代码
初始化 canvas
先初始化 canvas 和其 2d 上下文 ctx
设置线宽为 5,线的末端和连接处均为圆角,这里我就简单处理了,如果大家有兴趣,也可以把线宽和线的末端和连接处进行配置修改
1 2 3 4 5
| const tablet = document.getElementById("canvas"); const ctx = tablet.getContext("2d"); ctx.lineWidth = 5; ctx.lineCap = "round"; ctx.lineJoin = "round";
|
字体大小
然后我们生成字体大小的选项,允许 1-16px,默认为 5px
1 2 3 4 5 6 7 8 9 10 11 12 13
| const fontSize = document.getElementById("font-size"); for (let i = 1; i <= 16; i++) { const option = document.createElement("option"); option.value = i; option.innerText = i + "px"; fontSize.appendChild(option); if (i == 5) { option.selected = true; } } fontSize.addEventListener("change", (e) => { ctx.lineWidth = e.target.value; });
|
字体颜色
读取颜色 input 的内容设置 canvas 画笔的笔触颜色
1 2 3 4
| const fontColor = document.getElementById("font-color"); fontColor.addEventListener("change", (e) => { ctx.strokeStyle = e.target.value; });
|
写字
写字分为三部分
鼠标落下:开始写字,对应 mousedown 事件
鼠标移动:正在写字,对应 mousemove 事件
鼠标抬起:结束写字,对应 mouseup 事件
写字无非就是许多点连成的线,在写字过程中,记录鼠标上一点的位置,将其与当前点连起来
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| let prex, prey; const mousedown = (e) => { if (e.button != 0) return; prex = e.offsetX; prey = e.offsetY; ctx.beginPath(); ctx.moveTo(prex, prey); tablet.addEventListener("mousemove", mousemove); document.addEventListener("mouseup", mouseup); tablet.addEventListener("mouseenter", mouseenter); }; const mousemove = (e) => { ctx.lineTo(e.offsetX, e.offsetY); ctx.stroke(); }; const mouseup = () => { ctx.closePath(); tablet.removeEventListener("mousemove", mousemove); document.removeEventListener("mouseup", mouseup); tablet.removeEventListener("mouseenter", mouseenter); }; const mouseenter = (e) => { prex = e.offsetX; prey = e.offsetY; ctx.moveTo(prex, prey); }; tablet.addEventListener("mousedown", mousedown);
|
撤销
这里撤销我只做一步,多做也可以的,就是存数组,但是因为内容比较大,这个比较吃性能,建议如果真要存多步的话存少一点。
1 2 3 4 5 6 7 8 9 10
| let imageData const mousedown = (e) => { imageData = ctx.getImageData(0, 0, tablet.width, tablet.height) …… }
const undo = document.getElementById('undo') undo.addEventListener('click', () => { ctx.putImageData(imageData, 0, 0) })
|
清空画布
清空画布很简单,clearRect
就好了
1 2 3 4
| const clear = document.getElementById("clear"); clear.addEventListener("click", () => { ctx.clearRect(0, 0, tablet.width, tablet.height); });
|
下载图片
canvas 生成 DateURL,然后利用 a 标签下载
1 2 3 4 5 6 7 8 9 10
| let a; const download = document.getElementById("download"); download.addEventListener("click", () => { if (!a) { a = document.createElement("a"); } a.href = tablet.toDataURL("image/png"); a.download = "sign.png"; a.click(); });
|
完整代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>写字板</title> </head> <body> <div id="setting-line"> <div> 字体大小: <select id="font-size"></select> </div> <div> 字体颜色: <input type="color" id="font-color" /> </div> <button id="undo">撤销</button> <button id="clear">清空画布</button> <button id="download">生成图片</button> </div> <canvas width="800" height="300" id="canvas"></canvas> </body> <script> const tablet = document.getElementById("canvas"); const ctx = tablet.getContext("2d"); ctx.lineCap = "round"; ctx.lineJoin = "round"; ctx.lineWidth = 5; ctx.strokeStyle = "#000";
const fontSize = document.getElementById("font-size"); for (let i = 1; i <= 16; i++) { const option = document.createElement("option"); option.value = i; option.innerText = i + "px"; if (i == 5) { option.selected = true; } fontSize.appendChild(option); } fontSize.addEventListener("change", (e) => { ctx.lineWidth = e.target.value; });
const fontColor = document.getElementById("font-color"); fontColor.addEventListener("change", (e) => { ctx.strokeStyle = e.target.value; }); let imageData; let prex, prey; const mousedown = (e) => { if (e.button != 0) return; imageData = ctx.getImageData(0, 0, tablet.width, tablet.height); prex = e.offsetX; prey = e.offsetY; ctx.beginPath(); ctx.moveTo(prex, prey); tablet.addEventListener("mousemove", mousemove); document.addEventListener("mouseup", mouseup); tablet.addEventListener("mouseenter", mouseenter); }; const mousemove = (e) => { ctx.lineTo(e.offsetX, e.offsetY); ctx.stroke(); }; const mouseup = () => { tablet.removeEventListener("mousemove", mousemove); document.removeEventListener("mouseup", mouseup); tablet.removeEventListener("mouseenter", mouseenter); }; const mouseenter = (e) => { prex = e.offsetX; prey = e.offsetY; ctx.moveTo(prex, prey); }; tablet.addEventListener("mousedown", mousedown);
const undo = document.getElementById("undo"); undo.addEventListener("click", () => { ctx.putImageData(imageData, 0, 0); });
const clear = document.getElementById("clear"); clear.addEventListener("click", () => { ctx.clearRect(0, 0, tablet.width, tablet.height); });
let a; const download = document.getElementById("download"); download.addEventListener("click", () => { if (!a) { a = document.createElement("a"); } a.href = tablet.toDataURL(); a.download = "sign.png"; a.click(); }); </script> <style> #setting-line { display: flex; margin: auto; margin-top: 100px; line-height: 27px; justify-content: center; gap: 20px; }
#canvas { display: block; margin: auto; margin-top: 10px; background-color: #eee; } </style> </html>
|
效果
注意
PC 端的鼠标事件在移动端是不存在的,需要做一定的转化
1 2 3 4 5
| const hastouch = Boolean("ontouchstart" in window), tapstart = hastouch ? "touchstart" : "mousedown", tapmove = hastouch ? "touchmove" : "mousemove", tapend = hastouch ? "touchend" : "mouseup";
|
刮刮卡
实现原理
我们来讲解一下实现原理,本质上就是利用 canvas 的后绘制的内容在原先内容之上的逻辑,我们可以设置一张底图或者文字,然后,铺一层灰黑色的内容。在我们鼠标划过的时候,把这层灰黑色的内容给清楚掉即可。
HTML
1 2 3 4 5
| <body onload="init()"> <div id="wrapper"> <div id="reward"></div> </div> </body>
|
CSS
1 2 3 4 5 6 7 8 9 10 11
| #wrapper { display: flex; margin: auto; margin-top: 100px; justify-content: center; } #reward { background: url("./assets/img/winning.png") no-repeat; width: 697px; height: 376px; }
|
JS
我们没有在 Dom 中直接加上 canvas,这是因为,我们需要我们的 canvas 等于底图的大小,因此需要在 dom 加载完成之后进行操作,也就是我加了 onload 方法。
初始化 canvas
没有 canvas 的 dom 我们就创建一个,根据父级的宽高设置 canvas 的宽高,将 canvas 作为子级添加进去
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| function init() { let width, height; if (!document.getElementById("canvas")) { const canvas = document.createElement("canvas"); const fatherDom = document.getElementById("reward"); width = fatherDom.offsetWidth; height = fatherDom.offsetHeight; canvas.setAttribute("width", width + "px"); canvas.setAttribute("height", height + "px"); canvas.setAttribute("style", "border:1px solid green"); canvas.id = "canvas"; fatherDom.appendChild(canvas); }
const myCanvas = document.getElementById("canvas"); const ctx = myCanvas.getContext("2d"); }
|
添加黑灰色的刮刮层
这里我们绘制一个矩形和写上刮一刮三个字
1 2 3 4 5 6 7 8 9 10 11
| ctx.beginPath(); ctx.fillStyle = "#939393"; ctx.rect(0, 0, width, height); ctx.closePath(); ctx.fill();
ctx.font = "bold 80px Arial"; ctx.fillStyle = "#000"; ctx.textAlign = "center"; ctx.fillText("刮一刮", width / 2, height / 2);
|
定义常量/变量
接下来,我们定义一些常量/变量
1 2 3 4 5 6 7 8 9 10
| let isDown = false; const pointerArr = []; let xPointer = 0; let yPointer = 0;
const hastouch = Boolean("ontouchstart" in window), tapstart = hastouch ? "touchstart" : "mousedown", tapmove = hastouch ? "touchmove" : "mousemove", tapend = hastouch ? "touchend" : "mouseup";
|
定义事件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| myCanvas.addEventListener(tapstart, function (e) { this.style.cursor = "move"; isDown = true; xPointer = hastouch ? e.targetTouches[0].pageX : e.clientX - this.offsetLeft; yPointer = hastouch ? e.targetTouches[0].pageY : e.clientY - this.offsetTop; pointerArr.push([xPointer, yPointer]); circleReset(ctx); });
myCanvas.addEventListener(tapmove, function (e) { if (isDown) { xPointer = hastouch ? e.targetTouches[0].pageX : e.clientX - this.offsetLeft; yPointer = hastouch ? e.targetTouches[0].pageY : e.clientY - this.offsetTop; pointerArr.push([xPointer, yPointer]); circleReset(ctx); } });
myCanvas.addEventListener(tapend, function (e) { isDown = false; pointerArr = []; });
function circleReset(ctx) { ctx.save(); ctx.beginPath(); ctx.moveTo(pointerArr[0][0], pointerArr[0][1]); ctx.lineCap = "round"; ctx.lineJoin = "round"; ctx.lineWidth = 60; ctx.globalCompositeOperation = "destination-out"; if (pointerArr.length == 1) { ctx.lineTo(pointerArr[0][0] + 1, pointerArr[0][1] + 1); } else { for (var i = 1; i < pointerArr.length; i++) { ctx.lineTo(pointerArr[i][0], pointerArr[i][1]); ctx.moveTo(pointerArr[i][0], pointerArr[i][1]); } } ctx.closePath(); ctx.stroke(); ctx.restore(); }
|
这里给大家说一下为什么要用这么麻烦的方式清除内容,也许你会认为只需要
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| let isDrawing = false;
canvas.addEventListener("mousedown", (e) => { isDrawing = true; draw(e); });
canvas.addEventListener("mousemove", draw);
canvas.addEventListener("mouseup", () => { isDrawing = false; });
canvas.addEventListener("mouseout", () => { isDrawing = false; });
function draw(e) { if (!isDrawing) return; const rect = canvas.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; ctx.globalCompositeOperation = "destination-out"; ctx.beginPath(); ctx.arc(x, y, 10, 0, 2 * Math.PI); ctx.fill(); }
|
这样的确也可以清除内容,但是这样处理是比较卡顿的,而且绘制出来的可以很明显看到一个一个圆点。
完整代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132
| <!DOCTYPE html> <html> <head> <title>橡皮擦</title> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no" /> </head>
<body onload="init()"> <div id="wrapper"> <div id="reward"></div> </div> </body> <script> function init() { let width, height; if (!document.getElementById("canvas")) { const canvas = document.createElement("canvas"); const fatherDom = document.getElementById("reward"); width = fatherDom.offsetWidth; height = fatherDom.offsetHeight; canvas.setAttribute("width", width + "px"); canvas.setAttribute("height", height + "px"); canvas.setAttribute("style", "border:1px solid green"); canvas.id = "canvas"; fatherDom.appendChild(canvas); }
const myCanvas = document.getElementById("canvas"); const ctx = myCanvas.getContext("2d");
ctx.beginPath(); ctx.fillStyle = "#939393"; ctx.rect(0, 0, width, height); ctx.closePath(); ctx.fill(); ctx.font = "bold 80px Arial"; ctx.fillStyle = "#000"; ctx.textAlign = "center"; ctx.fillText("刮一刮", width / 2, height / 2);
let isDown = false; const pointerArr = []; let xPointer = 0; let yPointer = 0;
const hastouch = Boolean("ontouchstart" in window), tapstart = hastouch ? "touchstart" : "mousedown", tapmove = hastouch ? "touchmove" : "mousemove", tapend = hastouch ? "touchend" : "mouseup";
myCanvas.addEventListener(tapstart, function (e) { this.style.cursor = "move"; isDown = true; xPointer = hastouch ? e.targetTouches[0].pageX : e.clientX - this.offsetLeft; yPointer = hastouch ? e.targetTouches[0].pageY : e.clientY - this.offsetTop; pointerArr.push([xPointer, yPointer]); circleReset(ctx); });
myCanvas.addEventListener(tapmove, function (e) { if (isDown) { xPointer = hastouch ? e.targetTouches[0].pageX : e.clientX - this.offsetLeft; yPointer = hastouch ? e.targetTouches[0].pageY : e.clientY - this.offsetTop; pointerArr.push([xPointer, yPointer]); circleReset(ctx); } });
myCanvas.addEventListener(tapend, function (e) { isDown = false; pointerArr = []; });
function circleReset(ctx) { ctx.save(); ctx.beginPath(); ctx.moveTo(pointerArr[0][0], pointerArr[0][1]); ctx.lineCap = "round"; ctx.lineJoin = "round"; ctx.lineWidth = 60; ctx.globalCompositeOperation = "destination-out"; if (pointerArr.length == 1) { ctx.lineTo(pointerArr[0][0] + 1, pointerArr[0][1] + 1); } else { for (var i = 1; i < pointerArr.length; i++) { ctx.lineTo(pointerArr[i][0], pointerArr[i][1]); ctx.moveTo(pointerArr[i][0], pointerArr[i][1]); } } ctx.closePath(); ctx.stroke(); ctx.restore(); } } </script> <style> #wrapper { display: flex; margin: auto; margin-top: 100px; justify-content: center; } #reward { background: url("./assets/img/winning.png") no-repeat; width: 697px; height: 376px; } </style> </html>
|
效果
结语
我们的那个刮刮卡也可以当作橡皮擦来使用,不只是局限于我说的那些个作用,我就只是抛砖引玉,本篇文章就到这里了,更多内容敬请期待,债见~