Skip to content

Instantly share code, notes, and snippets.

@Gaubee
Last active March 1, 2018 02:57
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Gaubee/ebd320c4b92023484ac7433a9f91e26b to your computer and use it in GitHub Desktop.
Save Gaubee/ebd320c4b92023484ac7433a9f91e26b to your computer and use it in GitHub Desktop.
Pattern Lock Base on Canvas

How to use

const lock = new H5lock({
  width: number,
  height: number,
  container: Element|id,
  chooseType ?: 2|3|4|5, // default is 3: 3×3
  inputEnd(pwd){
    console.log('Pattern Lock Value Is:', pwd);
  }
});

API

.disabled : boolean

.reset()

.showError(msg: string, time = 1200, cb?: () => void)

custom Style

3×3 base circle

  • .normal_circle_style
  • .error_circle_style

the selected point

  • .normal_point_style
  • .error_point_style

the line of point to point

  • .normal_line_style
  • .error_line_style
type ChooseType = 2 | 3 | 4 | 5;
type Position = {
x: number
y: number
}
type Point = {
x: number
y: number
index: number;
}
type H5lockOptions = {
height: number
width: number
chooseType?: ChooseType
container: Element | string
inputEnd?: (res: string) => void
}
export class H5lock {
height: number
width: number
chooseType: ChooseType = 3
container: Element | string
inputEnd: (res: string) => void
devicePixelRatio: number = window.devicePixelRatio || 1
constructor(obj: H5lockOptions) {
this.height = obj.height;
this.width = obj.width;
if (obj.chooseType && isFinite(obj.chooseType)) {
const chooseType = obj.chooseType | 0;
if (chooseType >= 2 && chooseType <= 5) {
this.chooseType = chooseType as ChooseType;
}
}
this.container = obj.container;
this.inputEnd = obj.inputEnd;
this._updateFrame = this._updateFrame.bind(this);
}
private ctx: CanvasRenderingContext2D
private r: number
normal_circle_style = {
gradient: true,
colors: [
[0, "#42d6cb"],
[0.5, "#46ced8"],
[1, "#4dc0db"],
],
lineWidth: 2,
color: '#CFE6FF'
}
error_circle_style = {
gradient: false,
colors: [],
lineWidth: 2,
color: 'darkred'
}
get circle_style() {
return this.show_error ? this.error_circle_style : this.normal_circle_style;
}
private drawCle(x, y) { // 初始化解锁密码面板
const ctx = this.ctx;
const circle_style = this.circle_style;
if (circle_style.gradient) {
const gradient = ctx.createLinearGradient(x, y, x, y + this.r * 2);
circle_style.colors.forEach(color_args => {
gradient.addColorStop(color_args[0] as number, color_args[1] as string);
});
ctx.strokeStyle = gradient;
} else {
ctx.strokeStyle = circle_style.color;
}
ctx.lineWidth = 2 * this.devicePixelRatio;
ctx.beginPath();
ctx.arc(x, y, this.r, 0, Math.PI * 2, true);
ctx.closePath();
ctx.stroke();
}
/**
* 已使用的起来的轨迹点
*
* @private
* @type {Point[]}
* @memberof H5lock
*/
private lastPoints: Point[]
normal_point_style = {
gradient: true,
colors: [
[0, "#42d6cb"],
[0.5, "#46ced8"],
[1, "#4dc0db"],
],
color: '#CFE6FF'
}
error_point_style = {
gradient: false,
colors: [],
color: 'darkred'
}
get point_style() {
return this.show_error ? this.error_point_style : this.normal_point_style;
}
/**
* 初始化圆心
*
* @memberof H5lock
*/
drawPoints() { // 初始化圆心
const ctx = this.ctx;
const point_style = this.point_style;
const lastPoints = this.lastPoints;
if (point_style.gradient) {
const gradient = ctx.createLinearGradient(0, 0, this.canvas.width, this.canvas.height);
point_style.colors.forEach(color_args => {
gradient.addColorStop(color_args[0] as number, color_args[1] as string);
});
ctx.fillStyle = gradient;
} else {
ctx.fillStyle = point_style.color;
}
for (let point of lastPoints) {
const r = this.r / 2;
// if (point_style.gradient) {
// const gradient = ctx.createLinearGradient(point.x, point.y, point.x, point.y + r * 2);
// point_style.colors.forEach(color_args => {
// gradient.addColorStop(color_args[0] as number, color_args[1] as string);
// });
// ctx.fillStyle = gradient;
// } else {
// ctx.fillStyle = point_style.color;
// }
ctx.beginPath();
ctx.arc(point.x, point.y, r, 0, Math.PI * 2, true);
ctx.closePath();
ctx.fill();
}
}
normal_line_style = {
gradient: true,
colors: [
[0, "#42d6cb"],
[0.5, "#46ced8"],
[1, "#4dc0db"],
],
lineWidth: 3,
color: '#CFE6FF'
}
error_line_style = {
gradient: false,
colors: [],
lineWidth: 3,
color: 'darkred'
}
get line_style() {
return this.show_error ? this.error_line_style : this.normal_line_style;
}
/**
* 解锁轨迹
*
* @param {any} po
* @param {any} lastPoints
* @memberof H5lock
*/
drawLines(po?: Position) {
const ctx = this.ctx;
const line_style = this.line_style;
const lastPoints = this.lastPoints;
if (!lastPoints.length) {
return;
}
ctx.beginPath();
ctx.lineWidth = line_style.lineWidth * this.devicePixelRatio;
if (line_style.gradient) {
const gradient = ctx.createLinearGradient(0, 0, this.canvas.width, this.canvas.height);
line_style.colors.forEach(color_args => {
gradient.addColorStop(color_args[0] as number, color_args[1] as string);
});
ctx.strokeStyle = gradient;
} else {
ctx.strokeStyle = line_style.color;
}
ctx.moveTo(lastPoints[0].x, lastPoints[0].y);
for (var i = 1; i < lastPoints.length; i++) {
// const pre_point = lastPoints[i - 1];
const cur_point = lastPoints[i];
ctx.lineTo(cur_point.x, cur_point.y);
}
if (po) {
ctx.lineTo(po.x, po.y);
}
ctx.stroke();
ctx.closePath();
}
/**
* 所有的轨迹点
*
* @private
* @type {Point[]}
* @memberof H5lock
*/
private arr: Point[]
/**
* 剩余可用的轨迹点
*
* @private
* @type {Point[]}
* @memberof H5lock
*/
private restPoint: Point[]
/**
* 创建解锁点的坐标,根据canvas的大小来平均分配半径
*
* @memberof H5lock
*/
createCircle() {
var n = this.chooseType;
var count = 0;
this.r = this.ctx.canvas.width / (2 + 4 * n);// 公式计算
this.lastPoints = [];
this.arr = [];
this.restPoint = [];
var r = this.r;
for (var i = 0; i < n; i++) {
for (var j = 0; j < n; j++) {
count++;
var obj = {
x: j * 4 * r + 3 * r,
y: i * 4 * r + 3 * r,
index: count
};
this.arr.push(obj);
this.restPoint.push(obj);
}
}
this.drawCircles();
//return arr;
}
drawCircles() {
this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
for (var i = 0; i < this.arr.length; i++) {
this.drawCle(this.arr[i].x, this.arr[i].y);
}
}
/**
* 获取touch点相对于canvas的坐标
*
* @param {any} e
* @returns
* @memberof H5lock
*/
getPosition(e): Position | null {
if (e && e.touches && e.touches.length) {
var rect = e.currentTarget.getBoundingClientRect();
var po = {
x: (e.touches[0].clientX - rect.left) * this.devicePixelRatio,
y: (e.touches[0].clientY - rect.top) * this.devicePixelRatio
};
return po;
}
return null;
}
error_msg_style = {
font_size: 15,
color: "#ff5c6a",
stroke_color: "#ff99a2",
stroke_width: 1,
}
private _show_error = false;
set show_error(show_error) {
this._show_error = show_error;
requestAnimationFrame(this._update.bind(this))
}
get show_error() {
return this._show_error;
}
private _error_msg: string
set error_msg(error_msg) {
this._error_msg = error_msg;
if (this.show_error) {
requestAnimationFrame(this._update.bind(this))
}
}
get error_msg() {
return this._error_msg;
}
private _error_auto_close_ti: any
showError(msg: string, time = 1200, cb?: () => void) {
this.show_error = true;
this.error_msg = msg;
clearTimeout(this._error_auto_close_ti);
this._error_auto_close_ti = setTimeout(() => {
this._error_auto_close_ti = null;
this.show_error = false;
cb instanceof Function && cb();
}, time)
}
/**
* 最少选择的点
*
* @memberof H5lock
*/
min_point_number = 4;
/**
* touchend结束之后对密码和状态的处理
*
* @param {any} psw
* @memberof H5lock
*/
private storePass() {
// this.prevPoint = psw;
// var str = '';
// for (var i = 0; i < psw.length; i++) {
// str += psw[i].index;
// }
var str = '';
const lastPoints = this.lastPoints;
for (var i = 0; i < lastPoints.length; i++) {
str += lastPoints[i].index;
}
if (lastPoints.length < this.min_point_number) {
this.disabled = true;
this.showError(`请至少选择 ${this.min_point_number} 个点`, void 0, () => {
this.disabled = false;
this.reset();
});
} else {
this.inputEnd && this.inputEnd(str);
}
}
setChooseType(type) {
this.chooseType = type;
this.init();
}
updatePassword() {
window.localStorage.removeItem('passwordxx');
window.localStorage.removeItem('chooseType');
this.pswObj = {};
document.getElementById('title').innerHTML = '绘制解锁图案';
this.reset();
}
initDom() {
var wrap: Element
if (this.container instanceof Element) {
wrap = this.container
} else {
wrap = document.getElementById(this.container);
}
if (wrap) {
var canvas = this.canvas = document.createElement('canvas');
canvas.className = "h5lock";
canvas.style.cssText = 'display: inline-block;';
wrap.appendChild(canvas);
var width = this.width || 300;
var height = this.height || 300;
// document.body.appendChild(wrap);
// 高清屏锁放
canvas.style.width = width + "px";
canvas.style.height = height + "px";
canvas.height = height * this.devicePixelRatio;
canvas.width = width * this.devicePixelRatio;
}
}
private pswObj: any
private touchFlag: boolean
private canvas: HTMLCanvasElement
init() {
this.initDom();
this.pswObj = window.localStorage.getItem('passwordxx') ? {
step: 2,
spassword: JSON.parse(window.localStorage.getItem('passwordxx'))
} : {};
this.lastPoints = [];
// this.prevPoint = [];
this.touchFlag = false;
// this.canvas = document.getElementById('canvas') as HTMLCanvasElement;
this.ctx = this.canvas.getContext('2d');
this.createCircle();
this.bindEvent();
}
reset() {
this.createCircle();
}
bindEvent() {
this.canvas.addEventListener("touchstart", this.touchstart.bind(this), false);
this.canvas.addEventListener("mousedown", this.touchstart.bind(this), false);
this.canvas.addEventListener("touchmove", this.touchmove.bind(this), false);
this.canvas.addEventListener("mousemove", this.touchmove.bind(this), false);
this.canvas.addEventListener("touchend", this.touchend.bind(this), false);
this.canvas.addEventListener("mouseup", this.touchend.bind(this), false);
}
private _loop = false
startUpdateFrame() {
if (!this._loop) {
this._loop = true;
requestAnimationFrame(this._updateFrame);
}
}
private _updateFrame() {
if (this._loop) {
this._update();
requestAnimationFrame(this._updateFrame);
}
}
stopUpdateFrame() {
this._loop = false;
}
private _lastPos: Position | null = null;
/**
* 核心变换方法在touchmove时候调用
*
* @memberof H5lock
*/
private _update() {
const po = this._lastPos;
this.drawCircles();
// 绘制错误信息
if (this.show_error) {
const error_msg_style = this.error_msg_style;
const ctx = this.ctx;
ctx.fillStyle = error_msg_style.color;
ctx.strokeStyle = error_msg_style.stroke_color;
ctx.lineWidth = error_msg_style.stroke_width * this.devicePixelRatio;
ctx.textAlign = "center";
const font_size = error_msg_style.font_size * this.devicePixelRatio;
ctx.font = font_size + "px sans-serif";
ctx.strokeText(this.error_msg, this.canvas.width / 2, font_size, this.canvas.width);
ctx.fillText(this.error_msg, this.canvas.width / 2, font_size, this.canvas.width);
}
this.drawLines(po);// 每帧画轨迹
this.drawPoints();// 每帧画圆心
// for (var i = 0; i < this.restPoint.length; i++) {
// if (Math.abs(po.x - this.restPoint[i].x) < this.r && Math.abs(po.y - this.restPoint[i].y) < this.r) {
// this.drawPoints();
// this.lastPoints.push(this.restPoint[i]);
// this.restPoint.splice(i, 1);
// break;
// }
// }
}
disabled = false;
private touchstart(e) {
if (this.disabled) {
return;
}
e.preventDefault();// 某些android 的 touchmove不宜触发 所以增加此行代码
this._lastPos = this.getPosition(e);
if (this._tryStorePoint()) {
this.touchFlag = true;
this.startUpdateFrame();
}
}
private _tryStorePoint() {
const pos = this._lastPos;
for (let point of this.restPoint) {
if (Math.abs(pos.x - point.x) < this.r && Math.abs(pos.y - point.y) < this.r) {
this.lastPoints.push(point);
this.restPoint.splice(this.restPoint.indexOf(point), 1);
// console.log("触摸到指定的点咯!!", point);
return true;
}
}
}
private touchmove(e) {
if (this.disabled) {
return;
}
if (this.touchFlag) {
this._tryStorePoint();
this._lastPos = this.getPosition(e);
}
}
private touchend(e) {
if (this.disabled) {
return;
}
if (this.touchFlag) {
this.touchFlag = false;
this.storePass();
this._lastPos = null;
}
// 等绘制玩最后一帧再结束
requestAnimationFrame(() => {
this.stopUpdateFrame();
});
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment