钉钉小程序实现签名板

前言

古茗目前已经有近万家门店了,为了对门店做规范管理,会进行巡店且输出巡店报告,此时就需要有一个老板签名的功能,证明老板认可且了解当前结果。由于我们巡店用到的是钉钉小程序,所以下面将会为大家展示如何在小程序中实现一个签名板功能。

签名效果

设计实现

为了实现签名功能,需要用到 canvas,我们翻阅钉钉 api 文档,发现支持Canvas组件,very nice,下面开始实现。(由于我们内部使用 taro 框架,以下代码均为 taro + react,我们设计稿均为 750,所以样式中数值均是实际的2倍)

创建canvas

我们先在页面中创建一个canvas画布

jsx 复制代码
// 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;
}

此时会发生一个神奇的现象,明明设置了 width=343 和 height=180,怎么还是钉钉默认的 300x225 ?别急,我们往下走。

调整画布

为了得到正确的展示大小,我们可以通过设置样式实现

jsx 复制代码
// SignaturePad.less
.canvas {
  background: #fff;
  width: 100%;
  height: 360px;
}

确实是起效了,那么设置widthheight属性有什么用呢,我们看下钉钉文档,可以发现这两个属性可以用来控制绘画精细度,解决在高dpr的情况下造成的绘画模糊问题。

这里还需要注意,宽高属性需要和css中宽高属性保持相同比例,否则绘画会出现扭曲情况

初始化

画布创建完成了,接下来需要实现画笔功能,这时候就需要结合CanvasContext绘图上下文对象预设画笔属性以及后续绘图需要用到的坐标轴

jsx 复制代码
// 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)

Canvas相关属性

了解了基础知识,我们就基本知道如何实现了。通过onTouchStart确定画笔开始坐标,onTouchMove获取用户在canvas内的绘画路径,将路径上所有的点都填充上颜色。

jsx 复制代码
// 是否绘画过
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 // 禁止屏幕滚动以及下拉刷新
  />
)

添加操作

到这里,基础的绘画已经完成了,但是我们是需要将生成的签名保存到服务端的,所以还需要有一个确定操作。

jsx 复制代码
const createImg = async () => {
  if (!isPaint.current) {
    Taro.showToast({
      title: '签名内容不能为空!',
      icon: 'none',
    });
    return false;
  }
  // 把画布内容导出成图片,返回文件路径
  const { filePath } = await ctx.toTempFilePath();
  // 这里就可以做拿到路径的后续操作了
  // ...
};

有了确定操作,假如用户签错名字了想要重写,还需要一个清除操作。

jsx 复制代码
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);
};

到这里,一个基础的签名板已经完成了,但是还有一些可以优化的地方,下面我们将继续对它进行一些优化。

优化

撤回

清空虽然能解决用户写错的问题,但是只撤回上一笔对用户体验来说是更好的。我们可以创建一个history用于记录用户每一次绘画,然后通过getImageData获取canvas区域隐含的像素数据,将其push()history中,在触发撤回操作时,将最新一条数据pop()同时清空画布,再通过putImageDatahistory最后一条像素数据绘制到画布上,这样就能实现撤回效果。

jsx 复制代码
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]);
};

横屏

竖屏时签字区域相对较小,只要将其切到横屏那么体验将会好非常多了。查阅钉钉文档,发现并没有提供小程序切换横竖屏的api,那么只能我们自己做一个横屏效果了。我们可以通过rotatetranslate样式,将签名版横置,再对其调整宽高。

jsx 复制代码
// 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;
    }
  }
}

然后,我们就可以看到如图效果,签名版是横置了,但是这个签名功能明显不对了。通过打印onTouchMoveevent,我们发现x,y依然是(0, 0),因为屏幕的xy轴不会变,但是我们旋转了整个签名版,所以展示出的canvas的xy轴是跟随着变形了,导致了上图情况。

既然canvas旋转会导致xy轴变化,那么我们可以换个角度,只改变canvas的宽高,将标题按钮区域进行transform是不是就可以了

jsx 复制代码
// 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;
    }
  }
}

ok,可以看到,签名功能又正常了。但是,在我们点击清空的时候发现清空也坏了,这是因为我们调用的clearRect是清除画布上在该矩形区域内的内容,所以原本在初始化获取的Canvas宽高在横屏的时候实际上已经发生了变化,只要在横屏时重新获取一次组件宽高即可

jsx 复制代码
const toggleSize = () => {
  setFull(!full);
  setTimeout(() => {
    getCanvasSize();
  }, 200);
};

好了,到这里已经能得到一个相对完整的签名版功能了

总结

以上就是签名版的实现,实际上H5的实现也是类似的,只是某些部分会和小程序有所区别。整个签名板的实现基本上就是使用canvas,没有特别复杂的点,但是过程中总会遇到奇奇怪怪的问题,当你一个一个解决之后,你会发现,今天的姿势又能+1,这不就是程序员的快乐吗。感谢阅读。

相关推荐
wayhome在哪1 天前
用 fabric.js 搞定电子签名拖拽合成图片
javascript·产品·canvas
德育处主任1 天前
p5.js 掌握圆锥体 cone
前端·数据可视化·canvas
德育处主任2 天前
p5.js 3D 形状 "预制工厂"——buildGeometry ()
前端·javascript·canvas
德育处主任4 天前
p5.js 3D盒子的基础用法
前端·数据可视化·canvas
掘金安东尼4 天前
2分钟创建一个“不依赖任何外部库”的粒子动画背景
前端·面试·canvas
百万蹄蹄向前冲5 天前
让AI写2D格斗游戏,坏了我成测试了
前端·canvas·trae
用户2519162427117 天前
Canvas之画图板
前端·javascript·canvas
FogLetter10 天前
玩转Canvas:从静态图像到动态动画的奇妙之旅
前端·canvas
用户25191624271111 天前
Canvas之贪吃蛇
前端·javascript·canvas
用户25191624271111 天前
Canvas之粒子烟花
前端·javascript·canvas