代码实现
1、新建初始化项目
npm init -y
2、安装依赖
npm i rollup -D
为了让库文件具有更好的兼容性,需要把ES6代码在打包的时候转义成ES5。
# 安装rollup插件包 npm install @rollup/plugin-babel -D # 安装babel相关包 npm install @babel/core @babel/preset-env -D
在根目录下新建 .babelrc
文件,并撰写如下内容:
{ "presets": [ "@babel/preset-env" ] }
需要对生产环境进行压缩
# 安装代码压缩插件 npm install rollup-plugin-terser -D
3、配置 rollup.config.js 文件
根路径添加 rollup.config.js
文件,配置如下:
// 用于es6转es5 import { babel } from '@rollup/plugin-babel'; // 用于代码压缩 import { terser } from 'rollup-plugin-terser'; const config = { input: "./src/index.js", output: [ { file: './lib/kaimo-handlock-umd.js', format: 'umd', name: 'KaimoHandlock' // 当入口文件有export时,'umd'格式必须指定name // 这样,在通过<script>标签引入时,才能通过name访问到export的内容。 }, { file: './lib/kaimo-handlock-es.js', format: 'es' }, { file: './lib/kaimo-handlock-cjs.js', format: 'cjs' } ], plugins: [ babel({ babelHelpers: 'bundled' // 建议显式配置此选项(即使使用其默认值),以便对如何将这些 babel 助手插入代码做出明智的决定。 }), terser() ] } export default config;
4、添加默认的配置项
添加 config/config.default.js
export const defaultRecorderOptions = { container: null, // 创建canvas的容器,如果不填,自动在 body 上创建覆盖全屏的层 autoRender: true, // 是否自动渲染 dotNum: 4, // 圆点的数量: n x n defaultCircleColor: "#ddd", // 未选中的圆的颜色 focusColor: '#33a06f', //当前选中的圆的颜色 bgColor: '#fff', // canvas背景颜色 innerRadius: 16, // 圆点的内半径 outerRadius: 42, // 圆点的外半径,focus 的时候显示 touchRadius: 64, // 判定touch事件的圆半径 minPoints: 4, // 最小允许的点数 } export const defaultLockerOptions = { update: { beforeRepeat: function(){}, afterRepeat: function(){} }, check: { checked: function(){} } }
5、添加绘制的工具方法
添加 utils/draw-utils.js
// 获取canvas 的坐标:canvas 显示大小缩放为实际大小的 50%。为了让图形在 Retina 屏上清晰 export function getCanvasPoint(canvas, x, y) { let rect = canvas.getBoundingClientRect(); return { x: 2 * (x - rect.left), y: 2 * (y - rect.top) }; } // 计算连点之间的距离 export function distance(p1, p2) { let x = p2.x - p1.x, y = p2.y - p1.y; return Math.sqrt(x * x + y * y); } // 画实心圆 export function drawSolidCircle(ctx, color, x, y, r) { ctx.fillStyle = color; ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2, true); ctx.closePath(); ctx.fill(); } // 画空心圆 export function drawHollowCircle(ctx, color, x, y, r) { ctx.strokeStyle = color; ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2, true); ctx.closePath(); ctx.stroke(); } // 画线段 export function drawLine(ctx, color, x1, y1, x2, y2) { ctx.strokeStyle = color; ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke(); ctx.closePath(); }
6、添加入口文件
添加 index.js
import Recorder from './core/recorder.js'; import Locker from './core/locker.js'; export { Recorder, Locker }
7、添加核心文件 recorder.js
添加核心文件 core/recorder.js
// 获取默认配置 import {defaultRecorderOptions} from "../config/config.default.js"; // 绘制方法 import { distance, drawLine, drawHollowCircle, drawSolidCircle, getCanvasPoint } from "../utils/draw-utils.js"; export default class Recorder{ static get ERR_USER_CANCELED(){ return '用户已经取消'; } static get ERR_NO_TASK(){ return '暂无任务可执行'; } constructor(options) { this.options = Object.assign({}, defaultRecorderOptions, options); this.container = null; // 容器 this.circleCanvas = null; // 画圆的 canvas this.lineCanvas = null; // 画固定线条 canvas this.moveCanvas = null; // 画不固定线条的 canvas this.circles = []; // dotNum x dotNum 个实心圆坐标相关数据 this.recordingTask = null; // 记录任务 // 是否自动渲染 if(this.options.autoRender) { this.render(); } } // 渲染方法 render() { // 拿到容器 this.container = this.options.container || document.createElement('div'); // 拿到容器宽高 let {width, height} = container.getBoundingClientRect(); // 画圆的 canvas this.circleCanvas = document.createElement("canvas"); // 设置 circleCanvas 的宽高一样 this.circleCanvas.width = this.circleCanvas.height = 2 * Math.min(width, height); // 设置 circleCanvas 的样式支持 retina 屏 Object.assign(this.circleCanvas.style, { position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%) scale(0.5)', }); /** * cloneNode(true):方法可创建指定的节点的精确拷贝、拷贝所有属性和值。 * true:递归复制当前节点的所有子孙节点 * 复制画圆的 canvas 属性给到 lineCanvas、moveCanvas * */ this.lineCanvas = this.circleCanvas.cloneNode(true); this.moveCanvas = this.circleCanvas.cloneNode(true); // 将三个 canvas 添加到容器里 container.appendChild(this.lineCanvas); container.appendChild(this.moveCanvas); container.appendChild(this.circleCanvas); // touchmove 事件在 Chrome 下默认是一个 Passive Event 需要传参 {passive: false},否则就不能 preventDefault。 this.container.addEventListener('touchmove', evt => evt.preventDefault(), { passive: false }); // 开始渲染时清除上一次记录 this.clearPath(); } // 负责在画布上清除上一次记录的结果 clearPath() { // 如果没有画圆的 canvas,则重新渲染 if(!this.circleCanvas){ this.render() }; // 获取三个 canvas 的上下文,宽度,还有配置项 let {circleCanvas, lineCanvas, moveCanvas, options} = this, circleCtx = circleCanvas.getContext('2d'), lineCtx = lineCanvas.getContext('2d'), moveCtx = moveCanvas.getContext('2d'), width = circleCanvas.width, {dotNum, defaultCircleColor, innerRadius} = options; // 清除三个 canvas 画布 circleCtx.clearRect(0, 0, width, width); lineCtx.clearRect(0, 0, width, width); moveCtx.clearRect(0, 0, width, width); // 绘制 dotNum x dotNum 个实心圆 let range = Math.round(width / (dotNum + 1)); let circles = []; for(let i = 1; i <= dotNum; i++){ for(let j = 1; j <= dotNum; j++){ let y = range * i, x = range * j; drawSolidCircle(circleCtx, defaultCircleColor, x, y, innerRadius); let circlePoint = {x, y}; circlePoint.pos = [i, j]; circles.push(circlePoint); } } this.circles = circles; } // 负责记录:它是一个异步的,因为不知道什么时候用户停止移动,这里我们返回一个 promise 对象回去,让用户决定停止移动的 async record() { // 获取三个 canvas 的上下文,还有配置项 let {circleCanvas, lineCanvas, moveCanvas, options} = this, circleCtx = circleCanvas.getContext('2d'), lineCtx = lineCanvas.getContext('2d'), moveCtx = moveCanvas.getContext('2d'); // 记录激活的圆点 let records = []; // touchstart、touchmove事件执行的方法,用于绘制激活状态 let handler = evt => { // 每次touchstart时清除上一次记录的结果 if(evt.type === "touchstart") { records = []; this.clearPath(); } // 获取配置 let {bgColor, focusColor, innerRadius, outerRadius, touchRadius} = options; // 通过 changedTouches 转换得倒移动点的坐标 let {clientX, clientY} = evt.changedTouches[0], touchPoint = getCanvasPoint(moveCanvas, clientX, clientY); // 遍历之前存的圆点 for(let i = 0; i < this.circles.length; i++){ // 取出之前圆点的坐标 let point = this.circles[i], x0 = point.x, y0 = point.y; // 判断圆点跟移动点的距离是否小于判定touch事件的圆半径 if(distance(point, touchPoint) < touchRadius){ // 绘制白色的圆,半径为 outerRadius drawSolidCircle(circleCtx, bgColor, x0, y0, outerRadius); // 绘制激活色的圆,半径为 innerRadius drawSolidCircle(circleCtx, focusColor, x0, y0, innerRadius); // 绘制激活的空心圆,半径为 outerRadius drawHollowCircle(circleCtx, focusColor, x0, y0, outerRadius); // 如果 records 里面有圆点了,就跟最后一个连线 if(records.length){ let p2 = records[records.length - 1], x1 = p2.x, y1 = p2.y; drawLine(lineCtx, focusColor, x0, y0, x1, y1); } // 将 circles 里圆点截取出来,push 到 records 里面 let circle = this.circles.splice(i, 1); records.push(circle[0]); // 找到之后就break break; } } // 如果 records 里面有圆点了,就跟最后一个连线 if(records.length){ let point = records[records.length - 1], x0 = point.x, y0 = point.y, x1 = touchPoint.x, y1 = touchPoint.y; // 先清空画布 moveCtx.clearRect(0, 0, moveCanvas.width, moveCanvas.height); // 画移动的线 drawLine(moveCtx, focusColor, x0, y0, x1, y1); } }; // 监听touchstart、touchmove事件 circleCanvas.addEventListener('touchstart', handler); circleCanvas.addEventListener('touchmove', handler); let recordingTask = {}; let promise = new Promise((resolve, reject) => { // 给recordingTask添加取消的方法 recordingTask.cancel = (res = {}) => { let promise = this.recordingTask.promise; res.err = res.err || Recorder.ERR_USER_CANCELED; circleCanvas.removeEventListener('touchstart', handler); circleCanvas.removeEventListener('touchmove', handler); document.removeEventListener('touchend', done); resolve(res); this.recordingTask = null; return promise; } let done = evt => { // 清空移动canvas的画布 moveCtx.clearRect(0, 0, moveCanvas.width, moveCanvas.height); // 如果没有记录的点直接退出 if(!records.length) return; // 移除事件 circleCanvas.removeEventListener('touchstart', handler); circleCanvas.removeEventListener('touchmove', handler); document.removeEventListener('touchend', done); let err = null; // 如果记录的个数小于配置的最小允许的点数,就提示报错 if(records.length < options.minPoints){ err = `连接点数至少需要${options.minPoints}个`; } // 把坐标转成字符串 resolve({ err, records: records.map(o => o.pos.join('')).join('') }); this.recordingTask = null; }; // 监听 touchend 事件 document.addEventListener('touchend', done); }); recordingTask.promise = promise; this.recordingTask = recordingTask; return promise; } // 负责终止记录过程,同样也是返回一个promise async cancel() { // 如果有任务就执行任务里的 cancel 方法 if(this.recordingTask){ return this.recordingTask.cancel(); } return Promise.resolve({err: Recorder.ERR_NO_TASK}); } }
8、添加核心文件 locker.js
添加核心文件 core/locker.js
import Recorder from './recorder.js'; // 获取默认配置 import {defaultLockerOptions} from "../config/config.default.js"; // Locker 继承 Recorder export default class Locker extends Recorder{ static get ERR_PASSWORD_MISMATCH(){ return '密码不匹配'; } constructor(options = {}) { options.update = Object.assign({}, defaultLockerOptions.update, options.update); options.check = Object.assign({}, defaultLockerOptions.check, options.check); super(options); } // 更新密码 async update() { await this.cancel(); // 拿到钩子方法 beforeRepeat afterRepeat let beforeRepeat = this.options.update.beforeRepeat, afterRepeat = this.options.update.afterRepeat; // 第一次输入密码 let first = await this.record(); // 如果报错并且错误等于 ERR_USER_CANCELED 就return if(first.err && first.err === Locker.ERR_USER_CANCELED) { return Promise.resolve(first); } // 如果报错,return出去 if(first.err){ this.update(); beforeRepeat.call(this, first); return Promise.resolve(first); } // 执行重复前的钩子 console.log("第一次密码:", first.records); beforeRepeat.call(this, first); // 第二次输入密码 let second = await this.record(); // 如果报错并且错误等于 ERR_USER_CANCELED 就return if(second.err && second.err === Locker.ERR_USER_CANCELED) { return Promise.resolve(second); } // 如果第二次密码没有错,并且第一次的密码不等于第二次的就报错不匹配 if(!second.err && first.records !== second.records){ second.err = Locker.ERR_PASSWORD_MISMATCH; } this.update(); // 执行重复后的钩子 console.log("第二次密码:", second.records); afterRepeat.call(this, second); return Promise.resolve(second); } // 校验密码 async check(password) { await this.cancel(); // 拿到 checked 方法 let checked = this.options.check.checked; // 输入密码 let res = await this.record(); // 如果报错并且错误等于 ERR_USER_CANCELED 就return if(res.err && res.err === Locker.ERR_USER_CANCELED){ return Promise.resolve(res); } // 如果没有错误,密码不一致提示密码匹配不正确 if(!res.err && password !== res.records){ res.err = Locker.ERR_PASSWORD_MISMATCH } // 执行 checked 回调函数,返回结果出去 checked.call(this, res); console.log("输入的密码:", res.records, "需要校验的密码:", password) // 再次执行check,失败了可以再次输入 this.check(password); return Promise.resolve(res); } }
9、添加 recorder.html 示例
添加文件 example/recorder.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>kaimo handlock recorder demo</title> <link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/element-ui/2.15.9/theme-chalk/index.css"> <style> * { padding: 0; margin: 0; } html, body { width: 100%; height: 100%; overflow: hidden; } #container { position: relative; overflow: hidden; width: 100%; padding-top: 100%; height: 0px; background-color: white; } .control { text-align: center; } </style> </head> <body> <div id="container"></div> <div class="control"> <button id="cancelBtn" class="el-button el-button--mini el-button--danger">取消</button> </div> <script src="../lib/kaimo-handlock-umd.js"></script> <script> // KaimoHandlock是打包暴露出来的,创建一个Recorder实例 var recorder = new KaimoHandlock.Recorder({ container: document.querySelector('#container'), }); console.log(recorder) function recorded(res) { if(res.err){ console.error(res.err) recorder.clearPath(); if(res.err !== KaimoHandlock.Recorder.ERR_USER_CANCELED){ recorder.record().then(recorded); } }else{ console.log("密码字符串:", res.records) recorder.record().then(recorded); } } recorder.record().then(recorded); // 点击取消 cancelBtn.onclick = function(){ recorder.cancel(); recorder.clearPath(); } </script> </body> </html>
10、添加 locker.html 示例
添加文件 example/locker.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>kaimo handlock locker demo</title> <link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/element-ui/2.15.9/theme-chalk/index.css"> <style> * { padding: 0; margin: 0; } html, body { width: 100%; height: 100%; overflow: hidden; } #container { position: relative; overflow: hidden; width: 100%; padding-top: 100%; height: 0px; background-color: white; } .control { text-align: center; } </style> </head> <body> <div id="container"></div> <div class="control"> <button id="setPasswordBtn" class="el-button el-button--mini">设置密码</button> <button id="checkPasswordBtn" class="el-button el-button--mini">验证密码</button> </div> <script src="../lib/kaimo-handlock-umd.js"></script> <script> var password = localStorage.getItem('kaimo_handlock_passwd') || '1221322322'; // KaimoHandlock是打包暴露出来的,创建一个Locker实例 var locker = new KaimoHandlock.Locker({ container: document.querySelector('#container'), check: { checked: function(res){ locker.clearPath(); console.log("checked--->", res) if(res.err){ if(res.err === KaimoHandlock.Locker.ERR_PASSWORD_MISMATCH){ console.error("密码错误,请重新绘制!") }else{ console.error(res.err) } }else{ console.log("密码正确!") } }, }, update:{ beforeRepeat: function(res){ locker.clearPath(); if(res.err){ console.log(`请连接至少${locker.options.minPoints}个点`) }else{ console.log("请再次绘制相同图案") } }, afterRepeat: function(res){ locker.clearPath(); if(res.err){ if(res.err === KaimoHandlock.Locker.ERR_PASSWORD_MISMATCH){ console.log("两次绘制的图形不一致,请重新绘制!") }else{ console.log(`请连接至少${locker.options.minPoints}个点`) } }else{ password = res.records; localStorage.setItem('kaimo_handlock_passwd', password); console.log("密码更新成功:", password) } }, } }); console.log(locker) // 点击设置密码 setPasswordBtn.onclick = function(){ setPasswordBtn.classList.add("el-button--success"); checkPasswordBtn.classList.remove("el-button--success"); locker.clearPath(); locker.update(); } // 点击验证密码 checkPasswordBtn.onclick = function(){ checkPasswordBtn.classList.add("el-button--success"); setPasswordBtn.classList.remove("el-button--success"); locker.clearPath(); locker.check(password); } </script> </body> </html>
11、启动服务测试
- 第一个服务是 rollup 的
- 第二个服务是 liveserve 的用于访问 html 页面
recorder.html
测试结果,需要切换到移动端模式,刷新之后就可以测试了。主要就是绘制完看效果,
locker.html
测试结果,相对上面一个,测试复杂一点,点击下面两个按钮是用于切换当前的模式的,切到设置模式会有两次的输入,如果两次都输入正确,就会把密码保存到localStorage,验证密码就是通过设置的密码去校验输入的密码是否一样。感兴趣的可以自己测试玩玩。
localStorage 里的缓存如下:
12、打包之后的 umd 文件
我们看一下通过打包之后的 umd 文件,lib/kaimo-handlock-umd.js
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).KaimoHandlock={})}(this,(function(t){"use strict";function e(){ /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */ e=function(){return t};var t={},r=Object.prototype,n=r.hasOwnProperty,o="function"==typeof Symbol?Symbol:{},i=o.iterator||"@@iterator",a=o.asyncIterator||"@@asyncIterator",c=o.toStringTag||"@@toStringTag";function u(t,e,r){return Object.defineProperty(t,e,{value:r,enumerable:!0,configurable:!0,writable:!0}),t[e]}try{u({},"")}catch(t){u=function(t,e,r){return t[e]=r}}function s(t,e,r,n){var o=e&&e.prototype instanceof f?e:f,i=Object.create(o.prototype),a=new C(n||[]);return i._invoke=function(t,e,r){var n="suspendedStart";return function(o,i){if("executing"===n)throw new Error("Generator is already running");if("completed"===n){if("throw"===o)throw i;return _()}for(r.method=o,r.arg=i;;){var a=r.delegate;if(a){var c=E(a,r);if(c){if(c===h)continue;return c}}if("next"===r.method)r.sent=r._sent=r.arg;else if("throw"===r.method){if("suspendedStart"===n)throw n="completed",r.arg;r.dispatchException(r.arg)}else"return"===r.method&&r.abrupt("return",r.arg);n="executing";var u=l(t,e,r);if("normal"===u.type){if(n=r.done?"completed":"suspendedYield",u.arg===h)continue;return{value:u.arg,done:r.done}}"throw"===u.type&&(n="completed",r.method="throw",r.arg=u.arg)}}}(t,r,a),i}function l(t,e,r){try{return{type:"normal",arg:t.call(e,r)}}catch(t){return{type:"throw",arg:t}}}t.wrap=s;var h={};function f(){}function p(){}function d(){}var v={};u(v,i,(function(){return this}));var y=Object.getPrototypeOf,g=y&&y(y(k([])));g&&g!==r&&n.call(g,i)&&(v=g);var m=d.prototype=f.prototype=Object.create(v);function b(t){["next","throw","return"].forEach((function(e){u(t,e,(function(t){return this._invoke(e,t)}))}))}function w(t,e){function r(o,i,a,c){var u=l(t[o],t,i);if("throw"!==u.type){var s=u.arg,h=s.value;return h&&"object"==typeof h&&n.call(h,"__await")?e.resolve(h.__await).then((function(t){r("next",t,a,c)}),(function(t){r("throw",t,a,c)})):e.resolve(h).then((function(t){s.value=t,a(s)}),(function(t){return r("throw",t,a,c)}))}c(u.arg)}var o;this._invoke=function(t,n){function i(){return new e((function(e,o){r(t,n,e,o)}))}return o=o?o.then(i,i):i()}}function E(t,e){var r=t.iterator[e.method];if(void 0===r){if(e.delegate=null,"throw"===e.method){if(t.iterator.return&&(e.method="return",e.arg=void 0,E(t,e),"throw"===e.method))return h;e.method="throw",e.arg=new TypeError("The iterator does not provide a 'throw' method")}return h}var n=l(r,t.iterator,e.arg);if("throw"===n.type)return e.method="throw",e.arg=n.arg,e.delegate=null,h;var o=n.arg;return o?o.done?(e[t.resultName]=o.value,e.next=t.nextLoc,"return"!==e.method&&(e.method="next",e.arg=void 0),e.delegate=null,h):o:(e.method="throw",e.arg=new TypeError("iterator result is not an object"),e.delegate=null,h)}function x(t){var e={tryLoc:t[0]};1 in t&&(e.catchLoc=t[1]),2 in t&&(e.finallyLoc=t[2],e.afterLoc=t[3]),this.tryEntries.push(e)}function R(t){var e=t.completion||{};e.type="normal",delete e.arg,t.completion=e}function C(t){this.tryEntries=[{tryLoc:"root"}],t.forEach(x,this),this.reset(!0)}function k(t){if(t){var e=t[i];if(e)return e.call(t);if("function"==typeof t.next)return t;if(!isNaN(t.length)){var r=-1,o=function e(){for(;++r<t.length;)if(n.call(t,r))return e.value=t[r],e.done=!1,e;return e.value=void 0,e.done=!0,e};return o.next=o}}return{next:_}}function _(){return{value:void 0,done:!0}}return p.prototype=d,u(m,"constructor",d),u(d,"constructor",p),p.displayName=u(d,c,"GeneratorFunction"),t.isGeneratorFunction=function(t){var e="function"==typeof t&&t.constructor;return!!e&&(e===p||"GeneratorFunction"===(e.displayName||e.name))},t.mark=function(t){return Object.setPrototypeOf?Object.setPrototypeOf(t,d):(t.__proto__=d,u(t,c,"GeneratorFunction")),t.prototype=Object.create(m),t},t.awrap=function(t){return{__await:t}},b(w.prototype),u(w.prototype,a,(function(){return this})),t.AsyncIterator=w,t.async=function(e,r,n,o,i){void 0===i&&(i=Promise);var a=new w(s(e,r,n,o),i);return t.isGeneratorFunction(r)?a:a.next().then((function(t){return t.done?t.value:a.next()}))},b(m),u(m,c,"Generator"),u(m,i,(function(){return this})),u(m,"toString",(function(){return"[object Generator]"})),t.keys=function(t){var e=[];for(var r in t)e.push(r);return e.reverse(),function r(){for(;e.length;){var n=e.pop();if(n in t)return r.value=n,r.done=!1,r}return r.done=!0,r}},t.values=k,C.prototype={constructor:C,reset:function(t){if(this.prev=0,this.next=0,this.sent=this._sent=void 0,this.done=!1,this.delegate=null,this.method="next",this.arg=void 0,this.tryEntries.forEach(R),!t)for(var e in this)"t"===e.charAt(0)&&n.call(this,e)&&!isNaN(+e.slice(1))&&(this[e]=void 0)},stop:function(){this.done=!0;var t=this.tryEntries[0].completion;if("throw"===t.type)throw t.arg;return this.rval},dispatchException:function(t){if(this.done)throw t;var e=this;function r(r,n){return a.type="throw",a.arg=t,e.next=r,n&&(e.method="next",e.arg=void 0),!!n}for(var o=this.tryEntries.length-1;o>=0;--o){var i=this.tryEntries[o],a=i.completion;if("root"===i.tryLoc)return r("end");if(i.tryLoc<=this.prev){var c=n.call(i,"catchLoc"),u=n.call(i,"finallyLoc");if(c&&u){if(this.prev<i.catchLoc)return r(i.catchLoc,!0);if(this.prev<i.finallyLoc)return r(i.finallyLoc)}else if(c){if(this.prev<i.catchLoc)return r(i.catchLoc,!0)}else{if(!u)throw new Error("try statement without catch or finally");if(this.prev<i.finallyLoc)return r(i.finallyLoc)}}}},abrupt:function(t,e){for(var r=this.tryEntries.length-1;r>=0;--r){var o=this.tryEntries[r];if(o.tryLoc<=this.prev&&n.call(o,"finallyLoc")&&this.prev<o.finallyLoc){var i=o;break}}i&&("break"===t||"continue"===t)&&i.tryLoc<=e&&e<=i.finallyLoc&&(i=null);var a=i?i.completion:{};return a.type=t,a.arg=e,i?(this.method="next",this.next=i.finallyLoc,h):this.complete(a)},complete:function(t,e){if("throw"===t.type)throw t.arg;return"break"===t.type||"continue"===t.type?this.next=t.arg:"return"===t.type?(this.rval=this.arg=t.arg,this.method="return",this.next="end"):"normal"===t.type&&e&&(this.next=e),h},finish:function(t){for(var e=this.tryEntries.length-1;e>=0;--e){var r=this.tryEntries[e];if(r.finallyLoc===t)return this.complete(r.completion,r.afterLoc),R(r),h}},catch:function(t){for(var e=this.tryEntries.length-1;e>=0;--e){var r=this.tryEntries[e];if(r.tryLoc===t){var n=r.completion;if("throw"===n.type){var o=n.arg;R(r)}return o}}throw new Error("illegal catch attempt")},delegateYield:function(t,e,r){return this.delegate={iterator:k(t),resultName:e,nextLoc:r},"next"===this.method&&(this.arg=void 0),h}},t}function r(t,e,r,n,o,i,a){try{var c=t[i](a),u=c.value}catch(t){return void r(t)}c.done?e(u):Promise.resolve(u).then(n,o)}function n(t){return function(){var e=this,n=arguments;return new Promise((function(o,i){var a=t.apply(e,n);function c(t){r(a,o,i,c,u,"next",t)}function u(t){r(a,o,i,c,u,"throw",t)}c(void 0)}))}}function o(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function i(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}function a(t,e,r){return e&&i(t.prototype,e),r&&i(t,r),Object.defineProperty(t,"prototype",{writable:!1}),t}function c(t){return c=Object.setPrototypeOf?Object.getPrototypeOf.bind():function(t){return t.__proto__||Object.getPrototypeOf(t)},c(t)}function u(t,e){return u=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(t,e){return t.__proto__=e,t},u(t,e)}function s(t,e){if(e&&("object"==typeof e||"function"==typeof e))return e;if(void 0!==e)throw new TypeError("Derived constructors may only return object or undefined");return function(t){if(void 0===t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}(t)}function l(t){var e=function(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){}))),!0}catch(t){return!1}}();return function(){var r,n=c(t);if(e){var o=c(this).constructor;r=Reflect.construct(n,arguments,o)}else r=n.apply(this,arguments);return s(this,r)}}var h={container:null,autoRender:!0,dotNum:4,defaultCircleColor:"#ddd",focusColor:"#33a06f",bgColor:"#fff",innerRadius:16,outerRadius:42,touchRadius:64,minPoints:4},f={beforeRepeat:function(){},afterRepeat:function(){}},p={checked:function(){}};function d(t,e){var r=e.x-t.x,n=e.y-t.y;return Math.sqrt(r*r+n*n)}function v(t,e,r,n,o){t.fillStyle=e,t.beginPath(),t.arc(r,n,o,0,2*Math.PI,!0),t.closePath(),t.fill()}function y(t,e,r,n,o){t.strokeStyle=e,t.beginPath(),t.arc(r,n,o,0,2*Math.PI,!0),t.closePath(),t.stroke()}function g(t,e,r,n,o,i){t.strokeStyle=e,t.beginPath(),t.moveTo(r,n),t.lineTo(o,i),t.stroke(),t.closePath()}var m=function(){function t(e){o(this,t),this.options=Object.assign({},h,e),this.container=null,this.circleCanvas=null,this.lineCanvas=null,this.moveCanvas=null,this.circles=[],this.recordingTask=null,this.options.autoRender&&this.render()}var r,i;return a(t,[{key:"render",value:function(){this.container=this.options.container||document.createElement("div");var t=container.getBoundingClientRect(),e=t.width,r=t.height;this.circleCanvas=document.createElement("canvas"),this.circleCanvas.width=this.circleCanvas.height=2*Math.min(e,r),Object.assign(this.circleCanvas.style,{position:"absolute",top:"50%",left:"50%",transform:"translate(-50%, -50%) scale(0.5)"}),this.lineCanvas=this.circleCanvas.cloneNode(!0),this.moveCanvas=this.circleCanvas.cloneNode(!0),container.appendChild(this.lineCanvas),container.appendChild(this.moveCanvas),container.appendChild(this.circleCanvas),this.container.addEventListener("touchmove",(function(t){return t.preventDefault()}),{passive:!1}),this.clearPath()}},{key:"clearPath",value:function(){this.circleCanvas||this.render();var t=this.circleCanvas,e=this.lineCanvas,r=this.moveCanvas,n=this.options,o=t.getContext("2d"),i=e.getContext("2d"),a=r.getContext("2d"),c=t.width,u=n.dotNum,s=n.defaultCircleColor,l=n.innerRadius;o.clearRect(0,0,c,c),i.clearRect(0,0,c,c),a.clearRect(0,0,c,c);for(var h=Math.round(c/(u+1)),f=[],p=1;p<=u;p++)for(var d=1;d<=u;d++){var y=h*p,g=h*d;v(o,s,g,y,l);var m={x:g,y:y};m.pos=[p,d],f.push(m)}this.circles=f}},{key:"record",value:(i=n(e().mark((function r(){var n,o,i,a,c,u,s,l,h,f,p,m=this;return e().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return n=this.circleCanvas,o=this.lineCanvas,i=this.moveCanvas,a=this.options,c=n.getContext("2d"),u=o.getContext("2d"),s=i.getContext("2d"),l=[],h=function(t){"touchstart"===t.type&&(l=[],m.clearPath());for(var e,r,n,o=a.bgColor,h=a.focusColor,f=a.innerRadius,p=a.outerRadius,b=a.touchRadius,w=t.changedTouches[0],E=w.clientX,x=w.clientY,R=(e=E,r=x,n=i.getBoundingClientRect(),{x:2*(e-n.left),y:2*(r-n.top)}),C=0;C<m.circles.length;C++){var k=m.circles[C],_=k.x,L=k.y;if(d(k,R)<b){if(v(c,o,_,L,p),v(c,h,_,L,f),y(c,h,_,L,p),l.length){var P=l[l.length-1],O=P.x,j=P.y;g(u,h,_,L,O,j)}var S=m.circles.splice(C,1);l.push(S[0]);break}}if(l.length){var T=l[l.length-1],N=T.x,A=T.y,M=R.x,D=R.y;s.clearRect(0,0,i.width,i.height),g(s,h,N,A,M,D)}},n.addEventListener("touchstart",h),n.addEventListener("touchmove",h),f={},p=new Promise((function(e,r){f.cancel=function(){var r=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},i=m.recordingTask.promise;return r.err=r.err||t.ERR_USER_CANCELED,n.removeEventListener("touchstart",h),n.removeEventListener("touchmove",h),document.removeEventListener("touchend",o),e(r),m.recordingTask=null,i};var o=function t(r){if(s.clearRect(0,0,i.width,i.height),l.length){n.removeEventListener("touchstart",h),n.removeEventListener("touchmove",h),document.removeEventListener("touchend",t);var o=null;l.length<a.minPoints&&(o="连接点数至少需要".concat(a.minPoints,"个")),e({err:o,records:l.map((function(t){return t.pos.join("")})).join("")}),m.recordingTask=null}};document.addEventListener("touchend",o)})),f.promise=p,this.recordingTask=f,e.abrupt("return",p);case 10:case"end":return e.stop()}}),r,this)}))),function(){return i.apply(this,arguments)})},{key:"cancel",value:(r=n(e().mark((function r(){return e().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:if(!this.recordingTask){e.next=2;break}return e.abrupt("return",this.recordingTask.cancel());case 2:return e.abrupt("return",Promise.resolve({err:t.ERR_NO_TASK}));case 3:case"end":return e.stop()}}),r,this)}))),function(){return r.apply(this,arguments)})}],[{key:"ERR_USER_CANCELED",get:function(){return"用户已经取消"}},{key:"ERR_NO_TASK",get:function(){return"暂无任务可执行"}}]),t}(),b=function(t){!function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function");t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,writable:!0,configurable:!0}}),Object.defineProperty(t,"prototype",{writable:!1}),e&&u(t,e)}(s,t);var r,i,c=l(s);function s(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return o(this,s),t.update=Object.assign({},f,t.update),t.check=Object.assign({},p,t.check),c.call(this,t)}return a(s,[{key:"update",value:(i=n(e().mark((function t(){var r,n,o,i;return e().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return t.next=2,this.cancel();case 2:return r=this.options.update.beforeRepeat,n=this.options.update.afterRepeat,t.next=5,this.record();case 5:if(!(o=t.sent).err||o.err!==s.ERR_USER_CANCELED){t.next=8;break}return t.abrupt("return",Promise.resolve(o));case 8:if(!o.err){t.next=12;break}return this.update(),r.call(this,o),t.abrupt("return",Promise.resolve(o));case 12:return console.log("第一次密码:",o.records),r.call(this,o),t.next=16,this.record();case 16:if(!(i=t.sent).err||i.err!==s.ERR_USER_CANCELED){t.next=19;break}return t.abrupt("return",Promise.resolve(i));case 19:return i.err||o.records===i.records||(i.err=s.ERR_PASSWORD_MISMATCH),this.update(),console.log("第二次密码:",i.records),n.call(this,i),t.abrupt("return",Promise.resolve(i));case 24:case"end":return t.stop()}}),t,this)}))),function(){return i.apply(this,arguments)})},{key:"check",value:(r=n(e().mark((function t(r){var n,o;return e().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return t.next=2,this.cancel();case 2:return n=this.options.check.checked,t.next=5,this.record();case 5:if(!(o=t.sent).err||o.err!==s.ERR_USER_CANCELED){t.next=8;break}return t.abrupt("return",Promise.resolve(o));case 8:return o.err||r===o.records||(o.err=s.ERR_PASSWORD_MISMATCH),n.call(this,o),console.log("输入的密码:",o.records,"需要校验的密码:",r),this.check(r),t.abrupt("return",Promise.resolve(o));case 13:case"end":return t.stop()}}),t,this)}))),function(t){return r.apply(this,arguments)})}],[{key:"ERR_PASSWORD_MISMATCH",get:function(){return"密码不匹配"}}]),s}(m);t.Locker=b,t.Recorder=m,Object.defineProperty(t,"__esModule",{value:!0})}));
13、目录结构
目录结构如下: