Live2D

traojs项目中使用canvas封装一个签名画板

封装:index.tsx

import { useEffect, useRef, useState, forwardRef, ForwardedRef, useImperativeHandle } from 'react';
import * as raf from 'raf';
import Taro from '@tarojs/taro';
import { View, Canvas, ViewProps } from '@tarojs/components';
import classNames from 'classnames';
import styles from './index.module.less';

export interface ISignatureProps extends ViewProps {
  /**
   * @description 画布元素id
   * @default spcanvas
   */
  canvasId?: string;
  /**
   * @description 获取图片的类型
   * @default `png`
   */
  type?: 'jpg' | 'png';
  /**
   * @description 线条的宽度
   * @default `3`
   */
  lineWidth?: number;
  /**
   * @description 绘图颜色
   * @default `#000`
   */
  strokeStyle?: string;
  /**
   * @description 样式名
   */
  className?: string;

  /**
   * 用于判断是有发“画”过
   */
  handlerTouchEnd?: () => void;
}
/**
 * @title 组件实例
 */
export interface ISignatureInstance {
  /**
   * @description 获取绘制生成的图片相关数据,tempFilePath在h5为base64,小程序为临时图片,由于canvasToTempFilePath在部分小程序IDE无法调试
   */
  getImage: () => Promise<{
    base64: string;
    tempFilePath: string;
    canvas: HTMLCanvasElement;
  }>;
  /**
   * @description 清除画布方法
   */
  clear: () => void;
}

export function requestAnimationFrame(cb: any) {
  if (window.requestAnimationFrame) {
    return window.requestAnimationFrame(cb);
  }

  // @ts-ignore
  return raf.default(cb);
}

const defaultProps: ISignatureProps = {
  canvasId: 'spcanvas',
  type: 'png',
  lineWidth: 2,
  strokeStyle: '#000',
  className: '',
};

let componentIndex = 0;

function Signature(props: ISignatureProps, ref: ForwardedRef<ISignatureInstance>) {
  const { canvasId, lineWidth, strokeStyle, className, ...rest } = {
    ...defaultProps,
    ...props,
  };
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const wrapRef = useRef<HTMLDivElement>(null);
  const [canvasHeight, setCanvasHeight] = useState(0);
  const [canvasWidth, setCanvasWidth] = useState(0);
  const ctx = useRef<any>(null);
  const [compIndex] = useState(componentIndex++);

  const startEventHandler = () => {
    if (ctx.current) {
      ctx.current.beginPath();
      ctx.current.lineWidth = lineWidth as number;
      ctx.current.strokeStyle = strokeStyle as string;
    }
  };

  const moveEventHandler = (event: any) => {
    if (ctx.current) {
      requestAnimationFrame(() => {
        const evt = event.changedTouches[0];
        let mouseX = evt.x || evt.clientX;
        let mouseY = evt.y || evt.clientY;

        if (Taro.getEnv() === 'WEB' && canvasRef.current) {
          const coverPos = canvasRef.current.getBoundingClientRect();
          mouseX = evt.clientX - coverPos.left;
          mouseY = evt.clientY - coverPos.top;
        }
        ctx.current?.lineTo(mouseX, mouseY);
        ctx.current?.stroke();
      });
    }
  };

  const endEventHandler = () => {
    props.handlerTouchEnd?.();
  };

  const handleClear = () => {
    if (ctx.current) {
      ctx.current.clearRect(0, 0, canvasWidth, canvasHeight);
      ctx.current.closePath();
    }
  };

  const getImage = (): Promise<{
    base64: string;
    tempFilePath: string;
    canvas: HTMLCanvasElement;
  }> => {
    return new Promise((resolve, reject) => {
      const base64 = ctx.current?.canvas?.toDataURL(`image/${props.type}`, 0.8);

      Taro.createSelectorQuery()
        .select(`#${canvasId}${compIndex}`)
        .fields({
          node: true,
          size: true,
        })
        .exec(res => {
          if (
            process.env.NODE_ENV === 'development' &&
            ['alipay', 'tt', 'swan', 'kwai', 'dd'].includes(process.env.TARO_ENV)
          ) {
            console.warn(
              `signature组件调用了canvasToTempFilePath, 当前IDE不支持调试,须在真机上调试`,
            );
          }
          Taro.canvasToTempFilePath({
            canvas: res[0].node,
            fileType: props.type,
            canvasId: `${canvasId}${compIndex}`,
            success: res => {
              resolve({
                tempFilePath: res.tempFilePath,
                base64,
                canvas: ctx.current?.canvas,
              });
            },
            fail: err => {
              console.error(`[signature 转换图片失败]`, err);
              reject(err);
            },
          });
        });
    });
  };

  const canvasSetting = (canvasDom: any, width: number, height: number) => {
    if (canvasDom) {
      const canvas = canvasDom;
      canvas.current = canvas;

      ctx.current = canvas.getContext('2d');
      setCanvasWidth(width);
      setCanvasHeight(height);
      canvas.width = width;
      canvas.height = height;
      if (ctx.current) {
        ctx.current.clearRect(0, 0, width, height);
        ctx.current.beginPath();
        ctx.current.lineWidth = lineWidth as number;
        ctx.current.strokeStyle = strokeStyle as string;
      }
    }
  };

  const initCanvas = () => {
    Taro.nextTick(() => {
      setTimeout(() => {
        if (process.env.TARO_ENV !== 'h5') {
          Taro.createSelectorQuery()
            .select(`#${canvasId}${compIndex}`)
            .fields(
              {
                node: true,
                size: true,
              },
              res => {
                const { node, width, height } = res;
                canvasSetting(node, width, height);
              },
            )
            .exec();
        } else {
          const canvasDom: HTMLElement | null = document.getElementById(`${canvasId}${compIndex}`);
          let canvas: HTMLCanvasElement = canvasDom as HTMLCanvasElement;
          if (canvasDom?.tagName !== 'CANVAS') {
            canvas = canvasDom?.getElementsByTagName('canvas')[0] as HTMLCanvasElement;
          }
          canvasSetting(
            canvas,
            canvasDom?.offsetWidth as number,
            canvasDom?.offsetHeight as number,
          );
        }
      }, 1000);
    });
  };

  useEffect(() => {
    initCanvas();
  }, []);

  useImperativeHandle(ref, () => {
    return {
      getImage,
      clear: handleClear,
    };
  });

  return (
    <View className={classNames(styles.signature, className)} {...(rest as any)}>
      <View className={classNames(styles.signatureInner, styles.spcanvas_WEAPP)} ref={wrapRef}>
        <Canvas
          className={styles.signatureCanvas}
          id={`${canvasId}${compIndex}`}
          ref={canvasRef}
          canvasId={`${canvasId}${compIndex}`}
          disableScroll
          type="2d"
          onTouchStart={startEventHandler}
          onTouchMove={moveEventHandler}
          onTouchEnd={endEventHandler}
        />
      </View>
    </View>
  );
}

export default forwardRef(Signature);

  index.modules.less

/* stylelint-disable-next-line no-descending-specificity */
.signature {
  transform: translateZ(0);
  z-index: 5;
}

.spcanvas_WEAPP {
  width: 100%;
  height: 100%;
  transform: translateZ(0);
}

.signatureCanvas {
  width: 100%;
  height: 100%;
  transform: translateZ(0);
  z-index: 5;

  canvas {
    background-color: #ffffff;
    width: 100%;
    border: 1px solid #dadada;
    height: 300px;
    z-index: 5;
    transform: translateZ(0);
  }
}

.signatureInner {
  background-color: #ffffff;
  width: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  transform: translateZ(0);
}

  

使用:

页面样式
.Sign {
  height: 100%;
  width: 100%;
  border: 1px solid #f5f5f5;
  border-radius: 20px;
  z-index: 5;
}



 const instance = useRef<ISignatureInstance>();

 res= await instance.current?.getImage() //获取签名图片信息

res结构:
    base64: string;
    tempFilePath: string;
    canvas: HTMLCanvasElement;

 instance.current?.clear() //清除画板内容


    <Signature
          type="png"
          className={styles.Sign}
          // @ts-ignore
          ref={instance}
       
        />    

  

posted @ 2026-07-07 15:18  喻佳文  阅读(9)  评论(0)    收藏  举报