共计 8367 个字符,预计需要花费 21 分钟才能阅读完成。
(https://juejin.cn/post/697868… “https://juejin.cn/post/6978685539985653767”)」
前言
大家好,我是林三心,回想起我当年校招的时候啊,屡次被面试官问到 canvas
,然而我却不会,起初始终想找个机会学一下canvas
,然而始终没工夫。canvas
在前端的位置是越来越重要了,为此,我顺便写了 3 个小我的项目,让你们 10 分钟就能入门canvas
,是的,我的心里没有她,只有你们
1. canvas 实现时钟转动
实现以下成果,分为几步:
- 1、找到 canvas 的
核心
,画出表心
,以及表框
- 2、获取
以后工夫
,并依据工夫画出时针,分针,秒针
,还有刻度
- 3、应用定时器,每过一秒
获取新的工夫
,并从新绘图,达到时钟转动的成果
1.1 表心,表框
画表心,表框有两个知识点:
- 1、找到 canvas 的
核心地位
2、绘制
圆形
//html <canvas id="canvas" width="600" height="600"></canvas> // js // 设置中心点,此时 300,300 变成了坐标的 0,0 ctx.translate(300, 300) // 画圆线应用 arc(中心点 X, 中心点 Y, 半径, 起始角度, 完结角度) ctx.arc(0, 0, 100, 0, 2 * Math.PI) ctx.arc(0, 0, 5, 0, 2 * Math.PI) // 执行画线段的操作 stroke ctx.stroke() 让咱们来看看成果,发现了,如同不对啊,咱们是想画
两个独立的圆线
,怎么画进去的两个圆连到一起了
:
起因是:下面代码画连个圆时,是连着画的,所以画完大圆后,线还没斩断,就接着画小圆,那必定会大圆小圆连一起,解决办法就是:beginPath,closePath
ctx.translate(300, 300) // 设置中心点,此时 300,300 变成了坐标的 0,0 | |
// 画大圆 | |
+ ctx.beginPath() | |
// 画圆线应用 arc(中心点 X, 中心点 Y, 半径, 起始角度, 完结角度) | |
ctx.arc(0, 0, 100, 0, 2 * Math.PI) | |
ctx.stroke() // 执行画线段的操作 | |
+ ctx.closePath() | |
// 画小圆 | |
+ ctx.beginPath() | |
ctx.arc(0, 0, 5, 0, 2 * Math.PI) | |
ctx.stroke() | |
+ ctx.closePath() |
1.2 时针,分针,秒针
画这三个指针,有两个知识点:
- 1、依据以后
时,分,秒
去计算角度
- 2、在计算好的角度下来画出
时针,分针,秒针
如何依据算好的角度去画线呢,比方算出以后是3 点
,那么时针就应该以12 点
为起始点,顺时针
旋转2 * Math.PI / 12 * 3 = 90°
,分针和秒针也是同样的情理,只不过跟时针不同的是比例问题
而已,因为时在表上有 12 份
,而分针和秒针都是 60 份
这时候又有一个新问题,还是以下面的例子为例,我算出了 90°
,那咱们怎么画出时针呢?咱们能够应用moveTo 和 lineTo
去画线段。至于 90°,咱们只须要将 x 轴
顺时针旋转 90°
,而后再画出这条线段,那就失去了指定角度的指针了。然而下面说了,是要以12 点为起始点
,咱们的 默认 x 轴确是程度
的,所以咱们时候秒针算出角度后,每次都要 减去 90°
。可能这有点绕,咱们通过上面的图演示一下,还是以下面 3 点
的例子:
这样就得出了 3 点指针的画线角度了。
又又又有新问题了,比方当初我画完了时针,而后我想画分针,x 轴曾经在我画时针的时候偏转了,这时候必定要让 x 轴复原到原来的模样,咱们能力持续画分针,否则画进去的分针是不准的。这时候 save 和 restore
就派上用场了,save 是把 ctx 以后的状态打包压入栈中,restore 是取出栈顶的状态并赋值给 ctx
,save 可屡次,然而 restore 取状态的次数必须等于 save 次数
懂得了下面所说,剩下画 刻度
了,起始刻度的情理跟时候秒针情理一样,只不过刻度是死的,不须要计算,只须要规定画出 60 个小刻度
,和12 个大刻度
就行
const canvas = document.getElementById('canvas') | |
const ctx = canvas.getContext('2d') | |
ctx.translate(300, 300) // 设置中心点,此时 300,300 变成了坐标的 0,0 | |
// 把状态保存起来 | |
+ ctx.save() | |
// 画大圆 | |
ctx.beginPath() | |
// 画圆线应用 arc(中心点 X, 中心点 Y, 半径, 起始角度, 完结角度) | |
ctx.arc(0, 0, 100, 0, 2 * Math.PI) | |
ctx.stroke() // 执行画线段的操作 | |
ctx.closePath() | |
// 画小圆 | |
ctx.beginPath() | |
ctx.arc(0, 0, 5, 0, 2 * Math.PI) | |
ctx.stroke() | |
ctx.closePath() | |
----- 新加代码 ------ | |
// 获取以后 时,分,秒 | |
let time = new Date() | |
let hour = time.getHours() % 12 | |
let min = time.getMinutes() | |
let sec = time.getSeconds() | |
// 时针 | |
ctx.rotate(2 * Math.PI / 12 * hour + 2 * Math.PI / 12 * (min / 60) - Math.PI / 2) | |
ctx.beginPath() | |
// moveTo 设置画线终点 | |
ctx.moveTo(-10, 0) | |
// lineTo 设置画线通过点 | |
ctx.lineTo(40, 0) | |
// 设置线宽 | |
ctx.lineWidth = 10 | |
ctx.stroke() | |
ctx.closePath() | |
// 复原成上一次 save 的状态 | |
ctx.restore() | |
// 复原完再保留一次 | |
ctx.save() | |
// 分针 | |
ctx.rotate(2 * Math.PI / 60 * min + 2 * Math.PI / 60 * (sec / 60) - Math.PI / 2) | |
ctx.beginPath() | |
ctx.moveTo(-10, 0) | |
ctx.lineTo(60, 0) | |
ctx.lineWidth = 5 | |
ctx.strokeStyle = 'blue' | |
ctx.stroke() | |
ctx.closePath() | |
ctx.restore() | |
ctx.save() | |
// 秒针 | |
ctx.rotate(2 * Math.PI / 60 * sec - - Math.PI / 2) | |
ctx.beginPath() | |
ctx.moveTo(-10, 0) | |
ctx.lineTo(80, 0) | |
ctx.strokeStyle = 'red' | |
ctx.stroke() | |
ctx.closePath() | |
ctx.restore() | |
ctx.save() | |
// 绘制刻度,也是跟绘制时候秒针一样,只不过刻度是死的 | |
ctx.lineWidth = 1 | |
for (let i = 0; i < 60; i++) {ctx.rotate(2 * Math.PI / 60) | |
ctx.beginPath() | |
ctx.moveTo(90, 0) | |
ctx.lineTo(100, 0) | |
// ctx.strokeStyle = 'red' | |
ctx.stroke() | |
ctx.closePath()} | |
ctx.restore() | |
ctx.save() | |
ctx.lineWidth = 5 | |
for (let i = 0; i < 12; i++) {ctx.rotate(2 * Math.PI / 12) | |
ctx.beginPath() | |
ctx.moveTo(85, 0) | |
ctx.lineTo(100, 0) | |
ctx.stroke() | |
ctx.closePath()} | |
ctx.restore() |
最初一步就是更新视图,使时钟转动起来,第一想到的必定是定时器 setInterval
,然而留神一个问题:每次更新视图的时候都要把上一次的画布革除,再开始画新的视图,不然就会呈现 千手观音
的现象
附上最终代码:
const canvas = document.getElementById('canvas') | |
const ctx = canvas.getContext('2d') | |
setInterval(() => {ctx.save() | |
ctx.clearRect(0, 0, 600, 600) | |
ctx.translate(300, 300) // 设置中心点,此时 300,300 变成了坐标的 0,0 | |
ctx.save() | |
// 画大圆 | |
ctx.beginPath() | |
// 画圆线应用 arc(中心点 X, 中心点 Y, 半径, 起始角度, 完结角度) | |
ctx.arc(0, 0, 100, 0, 2 * Math.PI) | |
ctx.stroke() // 执行画线段的操作 | |
ctx.closePath() | |
// 画小圆 | |
ctx.beginPath() | |
ctx.arc(0, 0, 5, 0, 2 * Math.PI) | |
ctx.stroke() | |
ctx.closePath() | |
// 获取以后 时,分,秒 | |
let time = new Date() | |
let hour = time.getHours() % 12 | |
let min = time.getMinutes() | |
let sec = time.getSeconds() | |
// 时针 | |
ctx.rotate(2 * Math.PI / 12 * hour + 2 * Math.PI / 12 * (min / 60) - Math.PI / 2) | |
ctx.beginPath() | |
// moveTo 设置画线终点 | |
ctx.moveTo(-10, 0) | |
// lineTo 设置画线通过点 | |
ctx.lineTo(40, 0) | |
// 设置线宽 | |
ctx.lineWidth = 10 | |
ctx.stroke() | |
ctx.closePath() | |
ctx.restore() | |
ctx.save() | |
// 分针 | |
ctx.rotate(2 * Math.PI / 60 * min + 2 * Math.PI / 60 * (sec / 60) - Math.PI / 2) | |
ctx.beginPath() | |
ctx.moveTo(-10, 0) | |
ctx.lineTo(60, 0) | |
ctx.lineWidth = 5 | |
ctx.strokeStyle = 'blue' | |
ctx.stroke() | |
ctx.closePath() | |
ctx.restore() | |
ctx.save() | |
// 秒针 | |
ctx.rotate(2 * Math.PI / 60 * sec - Math.PI / 2) | |
ctx.beginPath() | |
ctx.moveTo(-10, 0) | |
ctx.lineTo(80, 0) | |
ctx.strokeStyle = 'red' | |
ctx.stroke() | |
ctx.closePath() | |
ctx.restore() | |
ctx.save() | |
// 绘制刻度,也是跟绘制时候秒针一样,只不过刻度是死的 | |
ctx.lineWidth = 1 | |
for (let i = 0; i < 60; i++) {ctx.rotate(2 * Math.PI / 60) | |
ctx.beginPath() | |
ctx.moveTo(90, 0) | |
ctx.lineTo(100, 0) | |
// ctx.strokeStyle = 'red' | |
ctx.stroke() | |
ctx.closePath()} | |
ctx.restore() | |
ctx.save() | |
ctx.lineWidth = 5 | |
for (let i = 0; i < 12; i++) {ctx.rotate(2 * Math.PI / 12) | |
ctx.beginPath() | |
ctx.moveTo(85, 0) | |
ctx.lineTo(100, 0) | |
// ctx.strokeStyle = 'red' | |
ctx.stroke() | |
ctx.closePath()} | |
ctx.restore() | |
ctx.restore()}, 1000) |
成果 very good
啊:
2. canvas 实现刮刮卡
小时候很多人都买过充值卡把,懂的都懂啊哈,用指甲刮开这层灰皮,就能看底下的答案了。
思路是这样的:
- 1、底下答案是一个
div
,顶部灰皮是一个canvas
,canvas
一开始盖住div
- 2、鼠标事件,点击时并挪动时,鼠标通过的门路都
画圆形
开路,并且设置globalCompositeOperation
为destination-out
,使鼠标通过的门路都变成通明
,一通明,天然就显示出下方的答案信息。
对于 fill
这个办法,其实是对标 stroke
的,fill
是把图形填充,stroke
只是画出边框线
// html | |
<canvas id="canvas" width="400" height="100"></canvas> | |
<div class="text"> 祝贺您取得 100w</div> | |
<style> | |
* { | |
margin: 0; | |
padding: 0; | |
} | |
.text { | |
position: absolute; | |
left: 130px; | |
top: 35px; | |
z-index: -1; | |
} | |
</style> | |
// js | |
const canvas = document.getElementById('canvas') | |
const ctx = canvas.getContext('2d') | |
// 填充的色彩 | |
ctx.fillStyle = 'darkgray' | |
// 填充矩形 fillRect(起始 X, 起始 Y, 起点 X, 起点 Y) | |
ctx.fillRect(0, 0, 400, 100) | |
ctx.fillStyle = '#fff' | |
// 绘制填充文字 | |
ctx.fillText('刮刮卡', 180, 50) | |
let isDraw = false | |
canvas.onmousedown = function () {isDraw = true} | |
canvas.onmousemove = function (e) {if (!isDraw) return | |
// 计算鼠标在 canvas 里的地位 | |
const x = e.pageX - canvas.offsetLeft | |
const y = e.pageY - canvas.offsetTop | |
// 设置 globalCompositeOperation | |
ctx.globalCompositeOperation = 'destination-out' | |
// 画圆 | |
ctx.arc(x, y, 10, 0, 2 * Math.PI) | |
// 填充圆形 | |
ctx.fill()} | |
canvas.onmouseup = function () {isDraw = false} |
成果如下:
3. canvas 实现画板和保留
框架:应用vue + elementUI
其实很简略,难点有以下几点:
- 1、鼠标拖拽画正方形和圆形
- 2、画完一个保留画布,下次再画的时候叠加
- 3、保留图片
第一点,只须要计算出鼠标点击的点坐标,以及鼠标的以后坐标,就能够计算了,矩形长宽计算:x - beginX, y - beginY
,圆形则要利用勾股定理:Math.sqrt((x - beginX) * (x - beginX) + (y - beginY) * (y - beginY))
第二点,则要利用 canvas 的 getImageData
和putImageData
办法
第三点,思路是将 canvas
生成图片链接,并赋值给具备下载性能的 a 标签
,并被动点击 a 标签
进行 图片下载
看看成果吧:
具体代码我就不过多解说了,说难也不难,只有后面两个我的项目了解了,这个我的项目很容易就懂了:
<template> | |
<div> | |
<div style="margin-bottom: 10px; display: flex; align-items: center"> | |
<el-button @click="changeType('huabi')" type="primary"> 画笔 </el-button> | |
<el-button @click="changeType('rect')" type="success"> 正方形 </el-button> | |
<el-button | |
@click="changeType('arc')" | |
type="warning" | |
style="margin-right: 10px" | |
> 圆形 </el-button | |
> | |
<div> 色彩:</div> | |
<el-color-picker v-model="color"></el-color-picker> | |
<el-button @click="clear"> 清空 </el-button> | |
<el-button @click="saveImg"> 保留 </el-button> | |
</div> | |
<canvas | |
id="canvas" | |
width="800" | |
height="400" | |
@mousedown="canvasDown" | |
@mousemove="canvasMove" | |
@mouseout="canvasUp" | |
@mouseup="canvasUp" | |
> | |
</canvas> | |
</div> | |
</template> | |
<script> | |
export default {data() { | |
return { | |
type: "huabi", | |
isDraw: false, | |
canvasDom: null, | |
ctx: null, | |
beginX: 0, | |
beginY: 0, | |
color: "#000", | |
imageData: null, | |
}; | |
}, | |
mounted() {this.canvasDom = document.getElementById("canvas"); | |
this.ctx = this.canvasDom.getContext("2d"); | |
}, | |
methods: {changeType(type) {this.type = type;}, | |
canvasDown(e) { | |
this.isDraw = true; | |
const canvas = this.canvasDom; | |
this.beginX = e.pageX - canvas.offsetLeft; | |
this.beginY = e.pageY - canvas.offsetTop; | |
}, | |
canvasMove(e) {if (!this.isDraw) return; | |
const canvas = this.canvasDom; | |
const ctx = this.ctx; | |
const x = e.pageX - canvas.offsetLeft; | |
const y = e.pageY - canvas.offsetTop; | |
this[`${this.type}Fn`](ctx, x, y); | |
}, | |
canvasUp() {this.imageData = this.ctx.getImageData(0, 0, 800, 400); | |
this.isDraw = false; | |
}, | |
huabiFn(ctx, x, y) {ctx.beginPath(); | |
ctx.arc(x, y, 5, 0, 2 * Math.PI); | |
ctx.fillStyle = this.color; | |
ctx.fill(); | |
ctx.closePath();}, | |
rectFn(ctx, x, y) { | |
const beginX = this.beginX; | |
const beginY = this.beginY; | |
ctx.clearRect(0, 0, 800, 400); | |
this.imageData && ctx.putImageData(this.imageData, 0, 0, 0, 0, 800, 400); | |
ctx.beginPath(); | |
ctx.strokeStyle = this.color; | |
ctx.rect(beginX, beginY, x - beginX, y - beginY); | |
ctx.stroke(); | |
ctx.closePath();}, | |
arcFn(ctx, x, y) { | |
const beginX = this.beginX; | |
const beginY = this.beginY; | |
this.isDraw && ctx.clearRect(0, 0, 800, 400); | |
this.imageData && ctx.putImageData(this.imageData, 0, 0, 0, 0, 800, 400); | |
ctx.beginPath(); | |
ctx.strokeStyle = this.color; | |
ctx.arc( | |
beginX, | |
beginY, | |
Math.round(Math.sqrt((x - beginX) * (x - beginX) + (y - beginY) * (y - beginY)) | |
), | |
0, | |
2 * Math.PI | |
); | |
ctx.stroke(); | |
ctx.closePath();}, | |
saveImg() {const url = this.canvasDom.toDataURL(); | |
const a = document.createElement("a"); | |
a.download = "sunshine"; | |
a.href = url; | |
document.body.appendChild(a); | |
a.click(); | |
document.body.removeChild(a); | |
}, | |
clear() { | |
this.imageData = null | |
this.ctx.clearRect(0, 0, 800, 400) | |
} | |
}, | |
}; | |
</script> | |
<style lang="scss" scoped> | |
#canvas {border: 1px solid black;} | |
</style> |
结语
我是林三心,一个热心的前端菜鸟程序员。如果你上进,喜爱前端,想学习前端,那咱们能够交朋友,一起摸鱼哈哈,摸鱼群,加我请备注【思否】