每次想系统学习一下canvas,常规流程都是
看到某个炫酷效果兴致勃勃打开mdn漫无目的的翻阅5分钟后关掉
于是这次想改变一下战术,从小需求出发,从农村包围城市!
签名
这应该算是很普遍的一个需求了
思路
思路很直接,直接画,会用到下面这堆api :)
beginPath创建一个新的路径lineWidth设置线段厚度的属性(即线段的宽度)strokeStyle描述画笔(绘制图形)颜色或者样式的属性,默认为黑色moveTo(x, y)将一个新的子路径的起始点移动到(x,y)坐标的方法(并不会真正地绘制)lineTo(x, y)使用直线连接子路径的终点到x,y坐标的方法(并不会真正地绘制)closePath从当前点到起始点绘制一条直线stroke实际地绘制出通过 moveTo() 和 lineTo() 方法定义的路径,默认颜色是黑色
代码
const canvas=document.getElementById("canvas")
const ctx=canvas.getContext("2d")
canvas.width = 800
canvas.height = 800
// 样式
ctx.lineWidth = 2;
ctx.strokeStyle = "#fff";
// 获取移动坐标
const getRealCoordinate = (event) => {
const canvasOffsetLeft = canvas.offsetLeft
const canvasOffsetTop = canvas.offsetTop
return {
x: event.clientX - canvasOffsetLeft,
y: event.clientY - canvasOffsetTop
}
}
const handleMouseMove = (event) =>{
const { x, y } = getRealCoordinate(event)
ctx.lineTo(x, y);
ctx.stroke();
}
const handleMouseDown = (event) => {
ctx.beginPath();
const { x, y } = getRealCoordinate(event)
ctx.moveTo(x, y);
canvas.addEventListener('mousemove', handleMouseMove)
}
canvas.addEventListener('mousedown', handleMouseDown)
canvas.addEventListener('mouseup', (e)=>{
canvas.removeEventListener('mousemove', handleMouseMove)
})
刮刮乐
思路
- 将canvas绝对定位覆盖再获奖底图上
- 监听滑动动作,使用
clearRect清除滑动部分的一小块画布即可
代码
view
<style>
.box{
position: relative;
width: 300px;
height: 300px;
}
/* 获奖底图 */
.prize{
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
background: coral;
font: 50px serif
}
/* canvas */
#prize-mask{
position: absolute;
top: 0;
left: 0;
cursor: pointer;
z-index: 10;
}
</style>
<div class="box"">
<div class="prize">1000万</div>
<canvas id="prize-mask" />
</div>
js
const canvas = document.querySelector('#prize-mask')
const ctx = canvas.getContext('2d')
// 获取外层宽高并动态设置宽高
const { clientWidth: wrapWidth, clientHeight: wrapHeight} = document.querySelector('.box')
canvas.width = wrapWidth
canvas.height = wrapHeight
const { clientWidth: canvasWidth, clientHeight: canvasHeight } = canvas
const text = '大吉大利'
// 设置一些canvas的属性
ctx.fillStyle='black'
ctx.fillRect(0,0,canvasWidth,canvasHeight)
ctx.font='50px serif'
ctx.strokeStyle='red'
ctx.strokeText(text, ( canvasWidth - ctx.measureText( text ).width ) / 2, canvasHeight / 2 ) // 文本居中
ctx.save()
const handleMouseMove = (event) =>{
ctx.clearRect(event.offsetX,event.offsetY,20,20);
}
const handleMouseDown = () => {
canvas.addEventListener('mousemove', handleMouseMove)
}
canvas.addEventListener('mousedown', handleMouseDown)
canvas.addEventListener('mouseup', (e)=>{
canvas.removeEventListener('mousemove', handleMouseMove)
})
效果
注意点
给canvas设置宽高,不要在class中设置,会造成拉伸/错乱等乱七八糟的问题
画个坐标网格
代码
const canvas=document.getElementById("canvas")
const context=canvas.getContext("2d")
const canvasWidth=800
const canvasHeight=800
const cap=50 // 间隔
const colNum=Math.ceil(canvasWidth/cap)-1
const rowNum=Math.ceil(canvasHeight/cap)-1
let Grids=[]
const lineWidth=2
canvas.width=800
canvas.height=800
//建立网格
function initGrid(cap,width,height,lineWidth){
const colNum=Math.ceil(width/cap)-1
const rowNum=Math.ceil(height/cap)-1
for(let i=1;i<=colNum;i++){
Grids.push([[cap*i-1, 0,lineWidth,colNum*cap],[i*cap,cap*i-1,colNum*cap+5,"top","center"]])
}
for(let i=1;i<=rowNum;i++){
Grids.push([[ 0,cap*i-1,rowNum*cap,lineWidth],[i*cap,rowNum*cap+5,cap*i-1,"middle","left"]])
}
}
initGrid(cap,canvasWidth,canvasHeight,lineWidth);
function createGrid(){
context.fillStyle = 'green';
context.font="24px Arial"
Grids.forEach((grid)=>{
context.textAlign=grid[1][4]
context.textBaseline=grid[1][3]
context.fillRect(grid[0][0],grid[0][1], grid[0][2], grid[0][3])
context.fillText(grid[1][0],grid[1][1], grid[1][2]);
})
}
createGrid()
模拟下雪
思路
思路比较清晰,就是生成若干个雪花,然后一帧一帧遍历、移动
代码
定义好变量
let W = window.innerWidth;
let H = window.innerHeight;
canvas.width = W;
canvas.height = H;
let flakesCount = 100; // 雪花个数
let flakes = []; // 雪花集合
let angle = 0
初始化雪花数组,定义好每个雪花的初始位置
// 定义每个雪花的位置
for (let i = 0; i < flakesCount; i++) {
flakes.push({
x: Math.random() * W, // 雪花x轴位置
y: Math.random() * H, // 雪花y轴位置
r: Math.random() * 5 , // 雪花的半径
d: Math.random() + 1 // 雪花密度,用于控制下落速度
});
}
某一帧的移动
function moveFlakes() {
angle += 0.01;
for (let i = 0; i < flakesCount; i++) {
let flake = flakes[i];
flake.y += Math.pow(flake.d, 2) ; // 速度和密度实际上不是平方的关系,这么些是为了效果更加错落有致
flake.x += Math.sin(angle) * 2;
// 超出界限了,重新初始化该雪花
if (flake.y > H) {
flakes[i] = { x: Math.random() * W, y: 0, r: flake.r, d: flake.d };
}
}
}
定义绘制某一帧的function, 用requestAnimationFrame代替setInterval
function drawFlakes () {
// 画出静态的雪花
ctx.clearRect(0, 0, W, H);
ctx.fillStyle = '#fff';
// ctx.strokeStyle = '#fff';
ctx.beginPath();
for (let i = 0; i < flakesCount; i++) {
let flake = flakes[i];
ctx.moveTo(flake.x, flake.y);
ctx.arc(flake.x, flake.y, flake.r, 0, Math.PI * 2, true);
}
ctx.fill();
// ctx.stroke();
moveFlakes()
window.requestAnimationFrame(drawFlakes);
}
最后执行绘制
drawFlakes()
一些API注意点
beginPath 和 closePath
beginPath - 开始一条新的路径,或者说重置一条新的路径
e.g
var ctx = document. getElementById ( 'canvas' ) . getContext ( '2d' ) ;
ctx. beginPath ( ) ;
ctx. moveTo ( 100 , 20 ) ;
ctx. lineTo ( 200 , 20 ) ;
ctx. strokeStyle = 'white' ;
ctx. stroke ( ) ;
ctx.beginPath() // important
ctx.moveTo(100, 40)
ctx. lineTo ( 200 , 40 )
ctx. strokeStyle = 'red' ;
ctx. stroke ( ) ;
上面这段代码会绘制出一条白线和一条红线,如果没有第二个beginPath, 最终绘制出来的就会是两条红线
closePath - 跟beginPath没有关系,这个api只是为了闭合一段路径,即从当前点绘制一条线段到路径起点