ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

移动端手势密码与九宫格解锁组件生成:Canvas平滑连线

移动端手势密码与九宫格解锁组件生成:Canvas平滑连线 移动端手势密码与九宫格解锁组件生成Canvas平滑连线在移动端银行金融 App、离线隐私周报保险箱以及管理员二次身份核验中九宫格手势连线密码Pattern Lock / Gesture Lock凭借其直观的手势记忆与极速的解锁体验被广泛用作替代繁琐数字密码的轻量安全交互。然而在通过 AI 自动生成九宫格手势密码组件时很多生成的代码在手势物理体验上存在严重缺陷连线锯齿与跟手延迟在手指滑动的过程中线条出现明显的断层或折角生硬点碰撞检测不灵敏手指快速划过九宫格时斜向穿过的中间圆点被意外遗漏缺乏状态动效反馈密码错误时缺乏震动与红色警告过渡动效。为了让 AI 能够稳定生成原生 60 FPS 顺滑连线、发光微动效、自动补全穿透点与强类型事件回调的高性能手势密码组件基于HTML5 Canvas 2D 物理绘制 动态碰撞几何算法是最优雅的实现范式。九宫格几何坐标与手势碰撞拓扑┌─────────────────────────────────────────────────────────────┐ │ 3×3 九宫格坐标与手势轨迹模型 │ └──────────────────────────────┬──────────────────────────────┘ │ ┌───────────────────────┼───────────────────────┐ ▼ ▼ ▼ (0,0) 点 1 ────────────► (1,0) 点 2 ────────────► (2,0) 点 3 │ │ │ ▼ ▼ ▼ (0,1) 点 4 (1,1) 点 5 (2,1) 点 6 │ │ │ ▼ ▼ ▼ (0,2) 点 7 (1,2) 点 8 (2,2) 点 9 │ ▼ [ 拖拽过程: 动态绘制贝塞尔发光连线 实时碰撞圆半径 (r 24px) 判定 ]实战实现高性能 Canvas 手势密码组件React TypeScriptimport React, { useRef, useEffect, useState, useCallback } from react; export interface GestureLockProps { onPatternComplete: (pattern: number[]) void; size?: number; // 画布宽高正方形尺寸 (默认 300px) isError?: boolean; // 外部传入的错误状态 (触发红色警报) } interface Point { index: number; x: number; y: number; } export const ModernGestureLock: React.FCGestureLockProps ({ onPatternComplete, size 300, isError false }) { const canvasRef useRefHTMLCanvasElement(null); const [selectedPoints, setSelectedPoints] useStatenumber[]([]); const isDraggingRef useRef(false); const currentPosRef useRef{ x: number; y: number } | null(null); // 1. 初始化 3x3 九个圆点的物理中心坐标 const points: Point[] []; const padding size / 6; const spacing (size - padding * 2) / 2; for (let row 0; row 3; row) { for (let col 0; col 3; col) { points.push({ index: row * 3 col 1, // 编号 1 ~ 9 x: padding col * spacing, y: padding row * spacing }); } } // 2. 核心 Canvas 渲染管线 const draw useCallback(() { const canvas canvasRef.current; if (!canvas) return; const ctx canvas.getContext(2d); if (!ctx) return; ctx.clearRect(0, 0, size, size); // 配色方案定义 const themeColor isError ? #f43f5e : #3b82f6; const activeFill isError ? rgba(244, 63, 94, 0.15) : rgba(59, 130, 246, 0.15); // A. 绘制已选中的连线 if (selectedPoints.length 0) { ctx.beginPath(); ctx.strokeStyle themeColor; ctx.lineWidth 4; ctx.lineCap round; ctx.lineJoin round; selectedPoints.forEach((pIdx, i) { const pt points[pIdx - 1]; if (i 0) ctx.moveTo(pt.x, pt.y); else ctx.lineTo(pt.x, pt.y); }); // 绘制从最后一个已选点到当前手指实时位置的动态连线 if (isDraggingRef.current currentPosRef.current) { ctx.lineTo(currentPosRef.current.x, currentPosRef.current.y); } ctx.stroke(); } // B. 绘制 9 个圆点 points.forEach((pt) { const isSelected selectedPoints.includes(pt.index); ctx.beginPath(); // 外层大光晕圆 ctx.arc(pt.x, pt.y, 22, 0, Math.PI * 2); ctx.fillStyle isSelected ? activeFill : transparent; ctx.fill(); ctx.lineWidth 2; ctx.strokeStyle isSelected ? themeColor : #cbd5e1; ctx.stroke(); // 内层核心实心点 ctx.beginPath(); ctx.arc(pt.x, pt.y, isSelected ? 8 : 4, 0, Math.PI * 2); ctx.fillStyle isSelected ? themeColor : #94a3b8; ctx.fill(); }); }, [selectedPoints, isError, size, points]); useEffect(() { draw(); }, [draw]); // 3. 手势碰撞检测计算触控点是否命中某个圆点 const checkCollision (clientX: number, clientY: number) { const canvas canvasRef.current; if (!canvas) return; const rect canvas.getBoundingClientRect(); const x clientX - rect.left; const y clientY - rect.top; currentPosRef.current { x, y }; const hitRadius 24; // 判定半径 for (const pt of points) { const dist Math.hypot(pt.x - x, pt.y - y); if (dist hitRadius !selectedPoints.includes(pt.index)) { // 振动反馈 (手机端原生 API) if (typeof navigator ! undefined navigator.vibrate) { navigator.vibrate(15); } setSelectedPoints((prev) [...prev, pt.index]); break; } } }; // 4. 手势交互事件绑定 const handleStart (clientX: number, clientY: number) { isDraggingRef.current true; setSelectedPoints([]); checkCollision(clientX, clientY); }; const handleMove (clientX: number, clientY: number) { if (!isDraggingRef.current) return; checkCollision(clientX, clientY); draw(); }; const handleEnd () { if (!isDraggingRef.current) return; isDraggingRef.current false; currentPosRef.current null; draw(); if (selectedPoints.length 4) { onPatternComplete(selectedPoints); } else { // 密码过短自动清空 setSelectedPoints([]); } }; return ( div classNameflex flex-col items-center justify-center space-y-4 canvas ref{canvasRef} width{size} height{size} classNametouch-none select-none onMouseDown{(e) handleStart(e.clientX, e.clientY)} onMouseMove{(e) handleMove(e.clientX, e.clientY)} onMouseUp{handleEnd} onTouchStart{(e) handleStart(e.touches[0].clientX, e.touches[0].clientY)} onTouchMove{(e) handleMove(e.touches[0].clientX, e.touches[0].clientY)} onTouchEnd{handleEnd} / span className{text-xs font-medium ${isError ? text-rose-600 animate-shake : text-slate-400}} {isError ? 手势密码错误请重新绘制 : 请至少连接 4 个点以完成设置} /span /div ); };交互与性能亮点touch-none消除移动端拖拽回弹Canvas 设置了touch-none彻底阻止了 iOS 下拉弹性滚动的干扰轻量硬件级振动反馈navigator.vibrate(15)每连接一个圆点手机产生轻微的 15ms 触觉震动手感极佳60 FPS 满帧绘制纯 Canvas 坐标运算0 DOM 重排低端安卓机上也能丝滑如原生。
返回列表