参加数字公司的校招,先是留了一个大作业,要求用网页制作一个图案解锁。
经过一番奋战,总算做了出来。

放张图:
pattern-lock
想感受一下效果么?戳这里:pattern-lock
要制作一个图案解锁,第一步要想好设计,对外开放哪些接口,如果我是一位开发者,我希望初始化的时候可以自定义行数列数,以及颜色大小等参数,如果用户不定义要有一套默认的参数,我希望可以调用方法获取用户的输入,并且提供一些额外的 API。就像买煎饼果子告诉老板不要加辣多放香菜,如果省略某些信息老板还要有自己的默认配置,最后给你你想要的煎饼果子。
JavaScript 实现类似这样:

constructor(obj) {
this.row = +obj.row || 3;
this.column = +obj.column || 3;
this.backgroundColor = obj.backgroundColor || 'whitesmoke';
this.opacity = obj.opacity || 0.0;
this.container = obj.container;
this.lineColor = obj.lineColor || 'springgreen';
this.lineWidth = obj.lineWidth || 3;
this.pointBackColor = obj.pointBackColor || 'white';
this.pointBorderColor = obj.pointBorderColor || 'grey';
this.radius = obj.radius || 'auto';
}

想好之后就是实现的问题了,图案有斜线等元素,比较复杂,所以采用 canvas 实现,以常见的三排三列为例,大致需要画出如下的图案,采取坐标轴如下(别问我为什么是左手系):

pattern-lock

这里定义 $xunit$ 与 $yunit$ 作为单位长度,方便后续的计算,这两个值根据 canvas 的宽高以及小圆的个数计算:
$$
\begin{cases}
xunit = \frac{width}{2 \times column} \\
yunit = \frac{height}{2 \times row}
\end{cases}
$$
根据 $xunit$ 与 $yunit$ 就可以计算出第 $i$ 排第 $j$ 列的圆的圆心的坐标:
$$
point[i][j] = (2 \times (i+1) \times xunit, 2 \times (j+1) \times yunit)
$$
有了公式后进行初始化,计算 $xunit$ 与 $yunit$,使用循环把各个点的坐标存入二维数组中,同时标记节点未触摸过。JavaScript 代码:

init () {
this.input = [];
this.canvas = document.getElementById(this.container);
this.context = this.canvas.getContext('2d');
let width = parseInt(this.canvas.getAttribute('width'));
let height = parseInt(this.canvas.getAttribute('height'));
this.xunit = width / (2*this.column);
this.yunit = height / (2*this.row);
this.radius = (this.radius === 'auto' ? Math.min(this.xunit, this.yunit) / 2 : this.radius);
this.coor = [];
for (let i = 0; i < this.column; i++) {
this.coor[i] = [];
for (let j = 0; j < this.row; j++) {
this.coor[i].push({
x: this.xunit * (2*i+1),
y: this.yunit * (2*j+1),
visit: false
});
}
}
this.drawCircle();
this.bindEvent();
}

接下来是绑定三种事件, touchstarttouchmovetouchend,这三个事件处理的事情差不多,无论哪个事件首先都要获取位置,判断是否与圆相交,这里通过
$$
\begin{cases}
recentX = \left(2 \times \left \lfloor \frac{touchX}{2 \times xunit}\right \rfloor+1 \right) \times xunit \\\\
recentY = \left( 2 \times \left \lfloor \frac{touchY}{2 \times yunit}\right \rfloor + 1 \right) \times yunit
\end{cases}
$$
算出与触摸的点最近的圆心,然后计算这两个点的距离的平方, 与半径的平方进行比较即可得知是否在圆内。每个事件都要有这个操作,所以提出来做单独的函数,JavaScript 代码:

getPosition (evt) {
let touchX = evt.touches[0].clientX - evt.currentTarget.getBoundingClientRect().left;
let touchY = evt.touches[0].clientY - evt.currentTarget.getBoundingClientRect().top;
let indexX = Math.floor(touchX / (2*this.xunit));
let indexY = Math.floor(touchY / (2*this.yunit));
let recentX = (2*indexX+1) * this.xunit;
let recentY = (2*indexY+1) * this.yunit;
let hit = Math.pow(recentX-touchX, 2)+Math.pow(recentY-touchY, 2) < Math.pow(this.radius, 2);
let pos = {
indexX: indexX,
indexY: indexY,
touchX: touchX,
touchY: touchY,
recentX: recentX,
recentY: recentY,
hit: hit
};
return pos;
}

如果在圆内并且未触摸过该点,则标记该点,并将该点存入数组中。

当然,记得每次 touchmove 发生时都要清空画布重绘图案!重新绘制时需要将走过的节点两两连线,这里用一个 reduce 函数就搞定了:

self.input.reduce((prev, next) => self.drawLine(prev, next));

最后,整个流程最消耗资源的就是每次发生 touchmove 事件时的重绘画布了,这里需要做一下函数节流和防抖,不过我做的时候出了点问题,就先搁置了。

想了解详细的使用请移步我的 GitHub

文档信息

本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。
本文链接:www.snovey.com/2017/04/pattern-lock.html