整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

使用 canvas 绘制网格背景

有没有很好奇,类似 processOn 这类作图网站中,网格背景 是怎么做的呢?

网格背景

如果你 F12 看过它的代码,你将发现原来不是通过 background-image 之类 css 属性做的,而是通过 canvas 实现的。

网格背景

这篇主要讲如何通过 canvas 来绘制出这样的 网格背景,以及中间碰到的一个 线条模糊的问题

手把手写代码

1. 创建 ctx 对象

这边将新建一个和屏幕尺寸相同的 canvas 画布:

const canvasEl = document.createElement("canvas");
const sh = screen.height;
const sw = screen.width;
canvasEl.width = sw;
canvasEl.height = sh;
const ctx = canvasEl.getContext("2d");

2. 绘制网格

先简单说下思路:

根据 canvas 画布尺寸,以 10px 为间距,分别绘制横纵坐标的线条。

比如绘制横向线条,先将“画笔”移至起点坐标 (0, y),然后通过线条方法 lineTo 绘制屏幕宽度的线条,即绘制 (0, y) 至 (screen.width, y) 的直线。

然后 y 会按照间距 10px 逐渐递增,直至绘制完整个屏幕。

下面根据横纵两个方向,封装了 draw 方法:

const draw = function (isColumn) {
  const gutter = 10;
  const limit = isColumn ? sh : sw;

  let i = 0;
  while (i * gutter + gutter <= limit) {
    i++;
    const point = i * gutter;

    // 清空子路径列表开始一个新路径
    ctx.beginPath();

    // 分割线
    ctx.strokeStyle = point % 100 !== 0 ? "#f0f0f0" : "#d6e4ff";

    // 将一个新的子路径的起始点移动到(x,y)坐标
    if (isColumn) {
      ctx.moveTo(0, point);
    } else {
      ctx.moveTo(point, 0);
    }
    // 使用直线连接子路径的终点到x,y坐标
    if (isColumn) {
      ctx.lineTo(sw, point);
    } else {
      ctx.lineTo(point, sh);
    }
    // 根据当前的画线样式,绘制当前或已经存在的路径的方法
    ctx.stroke();
  }
};

然后分别绘制横纵坐标,并把 canvas 挂载到 body 上:

draw(true);
draw(false);

document.body.append(canvasEl);

很顺利你将得到 canvas 绘制的网格图形:

网格背景

线条模糊

如果你观察比较细腻,能发现上面的网格图形并不是很清晰,可以仔细观察下面的图:

对比效果(左边模糊,右边清晰)

为什么会有这样的情况出现呢?

因为调用 lineTo 是从 A 点到 B 点,轨迹是点到点的连线。当绘制 1px 线条时,是以这个轨迹连线为中间线左右各渲染 0.5px 的线条。这会使得一个像素点只渲染了一半,而另一半会用一个比较弱的颜色填充,导致绘制出模糊的线条。

怎么改?

只要在 moveTo 时,将坐标偏移 0.5px(即将线条轨迹偏移 0.5 位置),然后调用 lineTo 时,也将坐标偏移 0.5px。

这样,最终绘制线条的时候,将在一个完整的像素区域进行渲染了。

选自:https://www.jianshu.com/p/c0970eecd843

有没有便捷的方法?

使用 translate 偏移 x 和 y 坐标值,然后绘制好后再调用 setTransform 重置回来:

ctx.translate(0.5, 0.5);
//...
ctx.setTransform(1, 0, 0, 1, 0, 0);

参考

  • canvas 画布解决 1px 线条模糊问题
  • Drawing a 1px thick line in canvas creates a 2px thick line
  • CanvasRenderingContext2D.setTransform()

#前端开发#

么是Canvas

<canvas> 是HTML中的一个元素,它可被用来通过 JavaScript(Canvas API 或 WebGL API)绘制图形及图形动画。

Canvas API 提供了一个通过 JavaScriptHTML<canvas> 元素来绘制图形的方式。它可以用于动画、游戏画面、数据可视化、图片编辑以及实时视频处理等方面。

<canvas>标签本身没有绘图能力,它仅仅是图形的容器。在HTML,一般通过Javascript语言来完成实际的操作。

创建HTML页面并添加绘图容器

本文通过Javascript操作Canvas制作一个简单的显示当前时间的动画时钟,了解和学习简单的canvas用法,仅以抛砖引玉。

首先创建一个HTML文件,为了方便管理,使用一个div标签包裹两个canvas标签,并加上一些简单的css样式。

<!doctype html>
<html lang="zh-cn">
<head><title>Canvas绘制动画时钟</title>
<style>
html,body{margin:0;padding:0}
#clockWrap {
	position: relative;
}
canvas {
	position: absolute;
}
#clock-ui {
	z-index: 2;
}
#clock-plate {
	z-index: 1;
}
</style>
</head>
<body>
  <div id="clockWrap">
  <canvas id="clock-plate"></canvas>
  <canvas id="clock-ui"></canvas>
</div>
<script></script>
</body></html>

本示例中使用了两个canvas标签(为什么使用两个,一个不是更简单吗?),一个用于绘制钟面,一个根据当前时间实时显示和更新时针、分针和秒针的动态指向。好了,话不多说,开干。

绘制钟面刻度

一个简单的时钟,可以分为钟面上的刻度和指针。其中刻度和12个数字是固定的,我们可以将它们绘制在当作背景的canvas上(示例中id为clock-plate的canvas)。

(1)要使用canvas,首先必须通过容器获取渲染上下文:

var $=function(id){return document.querySelector(id);}//这个函数只是为了方便获取dom元素
const canvasbg=$("#clock-plate"),
      canvas=$("#clock-ui"),
      ctx = canvasbg.getContext("2d"),//背景容器上下文
      ctxUI = canvas.getContext("2d");//指针容器上下文,后面代码要用
//定义画布宽度和高度,时钟圆直径,并设置画布大小
const oW=1000,oH=800,cW=400,r=cW/2,oX=oW/2,oY=oH/2;
canvas.width=oW;
canvas.height=oH;
canvasbg.width=oW;
canvasbg.height=oH;

(2)画钟的边框,为了好看,这里画两个圈:

 //画出时钟外圆框
  ctx.lineWidth = 12;
	ctx.beginPath();
	ctx.arc(oX, oY, r+14, 0, 2 * Math.PI);
	ctx.stroke();
	ctx.closePath();
	ctx.lineWidth = 8;
	//画出时钟内圆框(刻度圈)
	ctx.beginPath();
	ctx.arc(oX, oY, r, 0, 2 * Math.PI);
	ctx.stroke();
	ctx.closePath();
	ctx.beginPath();

边框效果图

(3)绘制刻度线和数字,可以利用三角函数计算出每个刻度的坐标:

利用三角函数计算刻度线的坐标位置

钟面上有12个大格,从正上方12开始,它们的度数分别是270,300,330,0,30,60,90,120,150,180,210,240。然后利用JS的Math.sin和Math.cos分别计算出各大格的坐标。注意:js中Math.sin()和Math.cos()的参数不是角度数弧度。可以使用Math.PI/180*角度来转化,比如将30度转换成弧度=Math.PI/180*30

//绘制钟表中心点
	ctx.beginPath();
	ctx.arc(oX, oY, 8, 0, 2 * Math.PI);//圆心
	ctx.fill();
	ctx.closePath();
	//设置刻度线粗细度
	ctx.lineWidth = 3;
	//设置钟面12个数字的字体、大小和对齐方式
	ctx.font = "30px serif";
	ctx.textAlign="center";
	ctx.textBaseline="middle";
	var kdx,kdy;
	//绘制12个大刻度和12个数字
	//为方便计算,先定义了0-11这12个刻度对应的度数,也可以直接定义对应的弧度。
	const hd=Math.PI/180,degr=[270,300,330,0,30,60,90,120,150,180,210,240];
	for(var i=0;i<12;i++){
		kdx=oX+Math.cos(hd*degr[i])*(r-3);
		kdy=oY+Math.sin(hd*degr[i])*(r-3);
		ctx.beginPath();
		ctx.arc(kdx, kdy, 6, 0, 2 * Math.PI);//画圆形大刻度
		ctx.fill();
    //绘制刻度对应的数字
		ctx.strokeText(i==0? 12 : i,oX+Math.cos(hd*degr[i])*(r-24),oY+Math.sin(hd*degr[i])*(r-24));
		ctx.closePath();
	}
	
	//绘制小刻度
	ctx.lineWidth = 2;
	for(var i=0;i<60;i++){
		if(i % 5 == 0) continue;//跳过与刻度重叠的刻度
		x0=Math.cos(hd*i*6);
		y0=Math.sin(hd*i*6);
		ctx.beginPath();
		ctx.moveTo(oX+x0*(r-10), oY+y0*(r-10)); 
		ctx.lineTo(oX+x0*r, oY+y0*r); //画短刻度线
		ctx.stroke(); 
		ctx.closePath();
	}

效果如图:

钟面效果图

(4)根据当前时间绘制指针

习惯上,时针粗短,分针略粗而长,秒针细长。为加大区别,示例中秒针细长并且绘制成红色。

function drawHp(i){//绘制时针
	const x0=Math.cos(hd*i*30),y0=Math.sin(hd*i*30);
	drawPointer(oX,oY,oX+x0*(r-90),oY+y0*(r-90),10,"#000000");
}
function drawMp(i){//绘制分针
	const x0=Math.cos(hd*i*6),y0=Math.sin(hd*i*6);
	drawPointer(oX,oY,oX+x0*(r-60),oY+y0*(r-60),5,"#000000");
}
function drawSp(i){//绘制秒针
	const x0=Math.cos(hd*i*6),y0=Math.sin(hd*i*6);
	drawPointer(oX,oY,oX+x0*(r-20),oY+y0*(r-20),2,"#FF0000");
}
//抽取出绘制三种指针时共同的部分,注意指针绘制在渲染上下文ctxUI中
function drawPointer(ox,oy,tx,ty,width,color){
	ctxUI.strokeStyle = color;
	ctxUI.lineCap = "round";
	ctxUI.lineWidth = width;
	ctxUI.beginPath();
	ctxUI.moveTo(ox, oy);
	ctxUI.lineTo(tx,ty);
	ctxUI.stroke();
	ctxUI.closePath();
}

现在已经有了绘制三种指针的方法,参数是当前时间的时、分和秒,将根据它们的值确定指针的坐标。不过,因为使用的是默认的convas坐标体系,0值实际指向3的位置,需要小小的修正一下。

window.requestAnimationFrame(function fn(){
		var d = new Date();
		ctxUI.clearRect(0,0,oW,oH);
		//度数从0开始,而0在3刻度(15分/秒位置),修正为全值减15,如果小于0则修正回来
    var hour=d.getHours(),minute=d.getMinutes()-15,second=d.getSeconds()-15;
		hour=hour>11? hour-15 : hour-3;
		drawHp(hour>=0? hour : 12+hour);
		drawMp(minute>=0? minute : 60+minute);
		drawSp(second>=0? second : 60+second);
		window.requestAnimationFrame(fn);
});

接下来,调用window.requestAnimationFrame,在其中绘制并更新指标的位置。看看效果如何:

指针绘制情况与实际时间相符

貌似效果有了,截图时电脑上的时间是10时17分,指针绘制上,时针指向10时,分针指向17。嗯,感觉有点别扭?对了,时针和分针怎么是端端正正地指向它们的整时整分刻度上呢?实际钟表上时针和分针是展示动态进度的,此时时针应该越过10时的位置才对。没关系,我们在绘制时针和分针时别点东西,让它的角度值加上分针和秒针的值试试。

//修改后的绘制三种指针的方法
function drawHp(i,f,m){//绘制时针,参数:时,分,秒
	const x0=Math.cos(hd*(i+(f/60)+(m/600))*30),y0=Math.sin(hd*(i+(f/60)+(m/600))*30);
	drawPointer(oX,oY,oX+x0*(r-90),oY+y0*(r-90),10,"#000000");
}
function drawMp(i,f){//绘制分针,参数,分,秒
	const x0=Math.cos(hd*(i+(f/60))*6),y0=Math.sin(hd*(i+(f/60))*6);
	drawPointer(oX,oY,oX+x0*(r-60),oY+y0*(r-60),5,"#000000");
}
function drawSp(i){//绘制秒针
	const x0=Math.cos(hd*i*6),y0=Math.sin(hd*i*6);
	drawPointer(oX,oY,oX+x0*(r-20),oY+y0*(r-20),2,"#FF0000");
}

再来看看效果,嗯,立竿见影呀:

指针指向更合理了

到此为止,canvas绘制一个简易时钟就完成了。下面继续优化一下。刚才使用requestAnimationFrame方法即时更新绘制情况。这个方法与刷新率有关,看看mdn上面怎么说的:

window.requestAnimationFrame() 方法会告诉浏览器你希望执行一个动画。它要求浏览器在下一次重绘之前,调用用户提供的回调函数。

对回调函数的调用频率通常与显示器的刷新率相匹配。虽然 75hz、120hz 和 144hz 也被广泛使用,但是最常见的刷新率还是 60hz(每秒 60 个周期/帧)。为了提高性能和电池寿命,大多数浏览器都会暂停在后台选项卡或者隐藏的 <iframe> 中运行的 requestAnimationFrame()。

本示例中,更新指针的位置并不需要很高的刷新频率,可以通过节流进行一下优化:

var fps = 5, fpsInterval = 1000 / fps,lastTime = new Date().getTime(); //记录上次执行的时间
function runStep() {
    requestAnimationFrame(runStep);
    var d=new Date(),now = d.getTime()
    var elapsed = now - lastTime;
    if (elapsed > fpsInterval) {
				ctxUI.clearRect(0,0,oW,oH);
        lastTime = now - (elapsed % fpsInterval); 
			//度数从0开始,而0在3刻度(15分/秒位置),修正为全值-15,如果小于0则用60减回
        var hour=d.getHours(),minute=d.getMinutes()-15,second=d.getSeconds()-15;//console.log(d.getSeconds(),second);
        hour=hour>11? hour-15 : hour-3;
        drawHp(hour>=0? hour : 12+hour,minute+15,second+15);
        drawMp(minute>=0? minute : 60+minute,second+15);
        drawSp(second>=0? second : 60+second);
    }
}
runStep();

当然,实现时钟的方法是很多,比如可以使用画布的旋转(rotate方法)来实现指针的动态转动等等。

完整HTML+JS源码:

随着HTML5的火热,越来越多的人投入到HTML5开发中了,canvas作为HTML5中比较重要的一个元素,在很多官网的主页面中被使用到。今天我们一起来看看如何使用canvas画出一个梦幻的星空背景,还会有流星运动。

本文的代码已经放到Github上了,感兴趣的可以自取,Github地址如下。

https://github.com/zhouxiongking/article-pages/blob/master/articles/starry/starry.html

HTML5

实现效果

首先我们来看看通过canvas实现的星空效果图,如下所示。

效果图

代码实现

接下来我们看看这个效果是如何通过代码一步步实现的。

首先来看看页面上的HTML代码,只有一个Div元素。

HTML代码

Javascript代码

首先我们需要定义一些常量,比如画布的宽和高,星星数量,流星个数。在这个星空中流星其实是星星的一个,只是添加了动态效果。

页面初始化

然后是设定一个定时器,通过一段随机时间生成一个流星的索引号。

流星索引号

紧接着来看看生成一个星星的方法,该方法返回一个星星的各项参数,包括x,y轴坐标,透明度,x,y轴偏移量。

生成星星的参数

然后是最重要的render方法,通过该方法可以将星星渲染至画布上,我们将这个方法拆开看,首先是对流星的绘制,流星索引号通过上面metor方法获得。

画流星

然后是对于星星各项参数的处理,比如有的星星生成的点坐标超出了屏幕宽高,有的透明度是负数,都要将其处理成正常参数。

各项参数判断

最后是在画布中进行绘制。

画布绘制

至此,这个画面效果的讲解完毕,如果代码正确的话,就可以看到文中出现的效果图。

结束语

今天这篇文章主要是借助HTML5中的canvas画出了一个梦幻星空的效果,你学会了吗?