前言
古茗目前已經有近萬傢門店了,為了對門店做規范管理,會進行巡店且輸出巡店報告,此時就需要有一個老板簽名的功能,證明老板認可且了解當前結果。由於我們巡店用到的是釘釘小程序,所以下面將會為大傢展示如何在小程序中實現一個簽名板功能。
![](https://news.xinpengboligang.com/upload/keji/70cb994353ab32eea498875645a3f57f.jpeg)
簽名效果
設計實現
為了實現簽名功能,需要用到 canvas,我們翻閱釘釘 api 文檔,發現支持Canvas組件,very nice,下面開始實現。(由於我們內部使用 taro 框架,以下代碼均為 taro react,我們設計稿均為 750,所以樣式中數值均是實際的2倍)
創建canvas
我們先在頁面中創建一個canvas畫佈
// SignaturePad.jsx
import { Canvas } from '@tarojs/components';
const SignaturePad = () => {
return (
<Canvas
id="signature"
canvasId="signature"
className="canvas"
width="343"
height="180"
/>
)
}
// SignaturePad.less
.canvas {
background: #fff;
}
![](https://news.xinpengboligang.com/upload/keji/7fb429a6d433ff8a1e19f16d126eba3d.jpeg)
1705740633476.png
此時會發生一個神奇的現象,明明設置了 width=343 和 height=180,怎麼還是釘釘默認的 300x225 ?別急,我們往下走。
調整畫佈
為了得到正確的展示大小,我們可以通過設置樣式實現
// SignaturePad.less
.canvas {
background: #fff;
width: 100%;
height: 360px;
}
![](https://news.xinpengboligang.com/upload/keji/0c2a7657f3eb43a23343e78af7b2ccdf.jpeg)
1705741273750.png
確實是起效了,那麼設置width和height屬性有什麼用呢,我們看下釘釘文檔,可以發現這兩個屬性可以用來控制繪畫精細度,解決在高dpr的情況下造成的繪畫模糊問題。
![](https://news.xinpengboligang.com/upload/keji/7791c34543c0cbb7dd8cee93dc43a580.jpeg)
1705741420270.png
這裡還需要註意,寬高屬性需要和css中寬高屬性保持相同比例,否則繪畫會出現扭曲情況
初始化
畫佈創建完成了,接下來需要實現畫筆功能,這時候就需要結合CanvasContext繪圖上下文對象預設畫筆屬性以及後續繪圖需要用到的坐標軸
// SignaturePad.jsx
let ctx = null;
let startX = 0;
let startY = 0;
const SignaturePad = () => {
const initCanvas = () => {
// 創建 canvas 的繪圖上下文 CanvasContext 對象
ctx = Taro.createCanvasContext('signature');
// 設置描邊顏色
ctx.setStrokeStyle('#000000');
// 設置線條的寬度
ctx.setLineWidth(4);
// 設置線條的端點樣式
ctx.setLineCap('round');
// 設置線條的交點樣式
ctx.setLineJoin('round');
};
useEffect(() => {
initCanvas();
return () => {
ctx = null;
};
}, []);
...
}
繪畫
所有準備工作完成,然後就是如何實現繪畫功能了。想要實現繪畫,要對 canvas 有所了解,canvas 元素默認被網格所覆蓋。通常來說網格中的一個單元相當於 canvas 元素中的一像素。柵格的起點為左上角,坐標為 (0,0) 。所有元素的位置都相對於原點來定位。所以圖中藍色方形左上角的坐標為距離左邊(X 軸)x 像素,距離上邊(Y 軸)y 像素,坐標為 (x, y)
![](https://news.xinpengboligang.com/upload/keji/9c9befcd654c5feeadff050e042d6fb0.jpeg)
Canvas_default_grid.png
Canvas相關屬性
![](https://news.xinpengboligang.com/upload/keji/7fad68345bdfa7cb8734df63f9e13f13.jpeg)
image.png
了解了基礎知識,我們就基本知道如何實現了。通過onTouchStart確定畫筆開始坐標,onTouchMove獲取用戶在canvas內的繪畫路徑,將路徑上所有的點都填充上顏色。
// 是否繪畫過
const isPaint = useRef(false)
const canvasStart = (e) => {
startX = e.touches[0].x;
startY = e.touches[0].y;
// 開始創建一個路徑
ctx.beginPath();
};
const canvasMove = (e) => {
if (startX !== 0 && !isPaint.current) {
isPaint.current = true;
}
const { x, y } = e.touches[0];
// 把路徑移動到畫佈中的指定點,不創建線條
ctx.moveTo(startX, startY);
// 增加一個新點,然後創建一條從上次指定點到目標點的線
ctx.lineTo(x, y);
// 畫出當前路徑的邊框
ctx.stroke();
// 將之前在繪圖上下文中的描述(路徑、變形、樣式)畫到 canvas 中
ctx.draw(true);
startX = x;
startY = y;
};
const canvasEnd = () => {
ctx.closePath();
};
return (
<Canvas
id="signature"
canvasId="signature"
className="canvas"
onTouchStart={canvasStart}
onTouchMove={canvasMove}
onTouchEnd={canvasEnd}
onTouchCancel={canvasEnd}
width="343"
height="180"
disableScroll // 禁止屏幕滾動以及下拉刷新
/>
)
添加操作
到這裡,基礎的繪畫已經完成了,但是我們是需要將生成的簽名保存到服務端的,所以還需要有一個確定操作。
const createImg = async () => {
if (!isPaint.current) {
Taro.showToast({
title: '簽名內容不能為空!',
icon: 'none',
});
return false;
}
// 把畫佈內容導出成圖片,返回文件路徑
const { filePath } = await ctx.toTempFilePath();
// 這裡就可以做拿到路徑的後續操作了
// ...
};
有了確定操作,假如用戶簽錯名字了想要重寫,還需要一個清除操作。
let canvasw = 0;
let canvash = 0;
// 獲取 canvas 的尺寸(寬高)
const getCanvasSize = () => {
nextTick(() => {
// 小程序查詢節點信息方法
const query = Taro.createSelectorQuery();
query
.select('#signature')
.boundingClientRect()
.exec(([rect]) => {
canvasw = rect.width;
canvash = rect.height;
});
});
};
useEffect(() => {
getCanvasSize();
...
}, []);
const clearDraw = () => {
startX = 0;
startY = 0;
// 清除畫佈上在該矩形區域內的內容
ctx.clearRect(0, 0, canvasw, canvash);
ctx.draw(true);
setIsPaint(false);
};
![](https://news.xinpengboligang.com/upload/keji/0304f8f0085c263ed47b7f5a40af15cf.jpeg)
到這裡,一個基礎的簽名板已經完成了,但是還有一些可以優化的地方,下面我們將繼續對它進行一些優化。
優化
撤回
清空雖然能解決用戶寫錯的問題,但是隻撤回上一筆對用戶體驗來說是更好的。我們可以創建一個history用於記錄用戶每一次繪畫,然後通過getImageData獲取canvas區域隱含的像素數據,將其push()到history中,在觸發撤回操作時,將最新一條數據pop()同時清空畫佈,再通過putImageData將history最後一條像素數據繪制到畫佈上,這樣就能實現撤回效果。
const history = useRef([]);
const canvasEnd = async () => {
ctx.closePath();
const res = await ctx.getImageData({ x: 0, y: 0, width: canvasw, height: canvash });
history.current.push(res);
};
// 撤回
const revoke = () => {
if (!history.current.length) return;
history.current.pop();
if (!history.current.length) {
ctx.clearRect(0, 0, canvasw, canvash);
ctx.draw(true);
return;
}
ctx.putImageData(history.current[history.current.length - 1]);
};
![](https://news.xinpengboligang.com/upload/keji/5cff86d4ffa6fdd3d1fd39bc2241f4d7.jpeg)
簽名-3.gif
橫屏
豎屏時簽字區域相對較小,隻要將其切到橫屏那麼體驗將會好非常多了。查閱釘釘文檔,發現並沒有提供小程序切換橫豎屏的api,那麼隻能我們自己做一個橫屏效果了。我們可以通過rotate和translate樣式,將簽名版橫置,再對其調整寬高。
// SignaturePad.jsx
const [full, setFull] = useState(false);
const toggleSize = () => {
setFull(!full);
};
return (
<View className="signature-pad-wrap">
<View className={`signature-pad ${full ? 'full-screen' : ''}`}>
{/* canvas */}
...
</View>
</View>
)
// SignaturePad.less
.signature-pad {
box-sizing: border-box;
width: 100%;
padding: 32px 32px 30px;
transform-origin: top left;
transition: transform 0.3s;
.canvas {
width: 686px;
height: 360px;
background: #fff;
}
&.full-screen {
width: 100vh;
height: 100vw;
transform: rotate(90deg) translate(0, -756px);
.canvas {
width: 1386px;
height: 630px;
}
}
}
![](https://news.xinpengboligang.com/upload/keji/6cd759602004e2e1cf78b9f9a5cda568.jpeg)
簽名-4.gif
然後,我們就可以看到如圖效果,簽名版是橫置了,但是這個簽名功能明顯不對了。通過打印onTouchMove的event,我們發現x,y依然是(0, 0),因為屏幕的xy軸不會變,但是我們旋轉了整個簽名版,所以展示出的canvas的xy軸是跟隨著變形了,導致了上圖情況。
![](https://news.xinpengboligang.com/upload/keji/1a7085d62378b9ce4d590b8346c17142.jpeg)
![](https://news.xinpengboligang.com/upload/keji/915d99a6fac16bec7e27cdcd9341c92a.jpeg)
既然canvas旋轉會導致xy軸變化,那麼我們可以換個角度,隻改變canvas的寬高,將標題按鈕區域進行transform是不是就可以了
// SignaturePad.jsx
<View className={`signature-pad ${full ? 'full-screen' : ''}`}>
<View className="signature-top">
<View className="title">簽名板</View>
{/* 一系列按鈕 */}
</View>
</View>
<Canvas
id="signature"
canvasId="signature"
className="canvas"
...
/>
</View>
// SignaturePad.less
.signature-pad {
.signature-top {
transform-origin: top left;
transition: transform 0.3s;
}
.canvas {
width: 686px;
height: 360px;
overflow: hidden;
background: #fff;
}
&.full-screen {
.signature-top {
position: absolute;
width: calc(100vh - 64px);
transform: translate(686px, 0) rotate(90deg);
}
.canvas {
width: 630px;
height: 1386px;
}
}
}
![](https://news.xinpengboligang.com/upload/keji/acf95d1d90f57766058a4b733e7671a6.jpeg)
簽名-6.gif
ok,可以看到,簽名功能又正常了。但是,在我們點擊清空的時候發現清空也壞了,這是因為我們調用的clearRect是清除畫佈上在該矩形區域內的內容,所以原本在初始化獲取的Canvas寬高在橫屏的時候實際上已經發生了變化,隻要在橫屏時重新獲取一次組件寬高即可
const toggleSize = () => {
setFull(!full);
setTimeout(() => {
getCanvasSize();
}, 200);
};
好了,到這裡已經能得到一個相對完整的簽名版功能了
![](https://news.xinpengboligang.com/upload/keji/38cb1001b6698130dd68dde6a87c1286.jpeg)
簽名-7.gif
總結
以上就是簽名版的實現,實際上H5的實現也是類似的,隻是某些部分會和小程序有所區別。整個簽名板的實現基本上就是使用canvas,沒有特別復雜的點,但是過程中總會遇到奇奇怪怪的問題,當你一個一個解決之後,你會發現,今天的姿勢又能 1,這不就是程序員的快樂嗎。感謝閱讀。
作者:王耀
來源:微信公眾號:Goodme前端團隊
出處
:https://mp.weixin.qq.com/s/Cm5Hbn_Osil2Tt0BDJPF9A