react-native实现画板功能

4423次浏览

前言

H5中利用cancas实现一个画板功能很简单,甚至你可以直接用chatGpt生成,生成之后简单的效果运行demo是可以的,然后在此基础上可以自己修改一下,集成功能,就可以了。

例如如下是html5的简单的画板demo

let canvas = document.getElementById('canvas');
let ctx = canvas.getContext('2d');
//画板控制开关
let painting = false;
//第一个点坐标
let startPoint = {x: undefined, y: undefined};
//初始化画布大小
wh();

//特性检测
if (document.body.ontouchstart !== undefined) {
    //触屏设备
    canvas.ontouchstart = function (e) {
        //[0]表示touch第一个触碰点
        let x = e.touches[0].clientX;
        let y = e.touches[0].clientY;
        painting = true;
        if (EraserEnabled) {
            ctx.clearRect(x - 20, y - 20, 40, 40)
        }
        startPoint = {x: x, y: y};
    };
    canvas.ontouchmove = function (e) {
        let x = e.touches[0].clientX;
        let y = e.touches[0].clientY;
        let newPoint = {x: x, y: y};
        if (painting) {
            if (EraserEnabled) {
                ctx.clearRect(x - 15, y - 15, 30, 30)
            } else {
                drawLine(startPoint.x, startPoint.y, newPoint.x, newPoint.y);
            }
            startPoint = newPoint;
        }
    };
    canvas.ontouchend = function () {
        painting = false;
    };
}else{// 非触屏设备
    // 按下鼠标(mouse)
    //鼠标点击事件(onmousedown)
    canvas.onmousedown = function (e) {
        let x = e.offsetX;
        let y = e.offsetY;
        painting = true;
        if (EraserEnabled) {
            ctx.clearRect(x - 15, y - 15, 30, 30)
        }
        startPoint = {x: x, y: y};
    };

//    滑动鼠标
//    鼠标滑动事件(onmousemove)
    canvas.onmousemove = function (e) {
        let x = e.offsetX;
        let y = e.offsetY;
        let newPoint = {x: x, y: y};
        if (painting) {
            if (EraserEnabled) {
                ctx.clearRect(x - 15, y - 15, 30, 30)
            } else {
                drawLine(startPoint.x, startPoint.y, newPoint.x, newPoint.y);
            }
            startPoint = newPoint;
        }
    };
//    松开鼠标
//    鼠标松开事件(onmouseup)
    canvas.onmouseup = function () {
        painting = false;
    };
}


/*****工具函数*****/
//    点与点之间连接
function drawLine(xStart, yStart, xEnd, yEnd) {
    //开始绘制路径
    ctx.beginPath();
    //线宽
    ctx.lineWidth = 2;
    //起始位置
    ctx.moveTo(xStart, yStart);
    //停止位置
    ctx.lineTo(xEnd, yEnd);
    //描绘线路
    ctx.stroke();
    //结束绘制
    ctx.closePath();
}

//    canvas与屏幕宽高一致
function wh() {
    let pageWidth = document.documentElement.clientWidth;
    let pageHeight = document.documentElement.clientHeight;
    canvas.width = pageWidth;
    canvas.height = pageHeight;
}

//控制橡皮擦开启
let EraserEnabled = false;
eraser.onclick = function () {
    EraserEnabled = true;
    eraser.classList.add('active');
    brush.classList.remove('active');
    canvas.classList.add('xiangpica');
};
brush.onclick = function () {
    EraserEnabled = false;
    brush.classList.add('active');
    eraser.classList.remove('active');
    canvas.classList.remove('xiangpica');
};

//清屏
clear.onclick = function() {
    ctx.fillStyle = '#C5C5C5';
    ctx.fillRect(0,0,canvas.width,canvas.height);
};

//保存
save.onclick = function() {
    let url = canvas.toDataURL('image/jpg');
    let a = document.createElement('a');
    document.body.appendChild(a);
    a.href = url;
    a.download = '草稿纸';
    a.target = '_blank';
    a.click()
};

//画笔颜色及鼠标样式
black.onclick = function () {
    ctx.strokeStyle = 'black';
    canvas.classList.add('cursor1');
    canvas.classList.remove('cursor2');
    canvas.classList.remove('cursor3');
    canvas.classList.remove('cursor4');
    canvas.classList.remove('cursor5');
    canvas.classList.remove('xiangpica');
    EraserEnabled = false;
    eraser.classList.remove('active');
};
red.onclick = function () {
    ctx.strokeStyle = 'red';
    canvas.classList.add('cursor2');
    canvas.classList.remove('cursor1');
    canvas.classList.remove('cursor3');
    canvas.classList.remove('cursor4');
    canvas.classList.remove('cursor5');
    canvas.classList.remove('xiangpica');
    EraserEnabled = false;
    eraser.classList.remove('active');
};
orange.onclick = function () {
    ctx.strokeStyle = 'orange';
    canvas.classList.add('cursor3');
    canvas.classList.remove('cursor2');
    canvas.classList.remove('cursor1');
    canvas.classList.remove('cursor4');
    canvas.classList.remove('cursor5');
    canvas.classList.remove('xiangpica');
    EraserEnabled = false;
    eraser.classList.remove('active');
};
green.onclick = function () {
    ctx.strokeStyle = 'green';
    canvas.classList.add('cursor4');
    canvas.classList.remove('cursor2');
    canvas.classList.remove('cursor3');
    canvas.classList.remove('cursor1');
    canvas.classList.remove('cursor5');
    canvas.classList.remove('xiangpica');
    EraserEnabled = false;
    eraser.classList.remove('active');
};
blue.onclick = function () {
    ctx.strokeStyle = 'blueviolet';
    canvas.classList.add('cursor5');
    canvas.classList.remove('cursor2');
    canvas.classList.remove('cursor3');
    canvas.classList.remove('cursor4');
    canvas.classList.remove('cursor1');
    canvas.classList.remove('xiangpica');
    EraserEnabled = false;
    eraser.classList.remove('active');
};

html代码:

<canvas id="canvas" class="cursor1" width="500" height="500"></canvas>
<div id="actions" class="actions">
    <svg id="brush" class="icon active">
        <use xlink:href="#icon-pencil"></use>
    </svg>
    <svg id="eraser" class="icon">
        <use xlink:href="#icon-xiangpica2"></use>
    </svg>
    <svg id="save" class="icon">
        <use xlink:href="#icon-xiazai"></use>
    </svg>
    <svg id="clear" class="icon">
        <use xlink:href="#icon-delete"></use>
    </svg>
</div>
<ol class="colors">
    <li id="black" class="black"></li>
    <li id="red" class="red"></li>
    <li id="orange" class="orange"></li>
    <li id="green" class="green"></li>
    <li id="blue" class="blue"></li>
</ol>

<script src="canvas-demo.js"></script>

假如要新增历史回退,两种思路

1、记录path,回退就是回退path
2、记录imageData,回退是回退imageData

方案一:记录imageData的方式是:

ctx.getImageData(0, 0, canvas.width, canvas.height)

把这些push到历史里面,每次回退拿到相应的画面。回退就可以

 ctx.putImageData(putImage, 0, 0)

问题

橡皮假如把背景擦除,那么橡皮的颜色改成背景颜色可以解决这个问题。

画布放到缩小之后,画板里面的内容不变,如何操作?

 function scaleImageData(imageData, scale, outCtx) {
    var scaled = outCtx.createImageData(imageData.width * scale, imageData.height * scale)
    outCtx.imageSmoothingEnabled = true
    for (var row = 0; row < imageData.height; row++) {
      for (var col = 0; col < imageData.width; col++) {
        var sourcePixel = [imageData.data[(row * imageData.width + col) * 4 + 0], imageData.data[(row * imageData.width + col) * 4 + 1], imageData.data[(row * imageData.width + col) * 4 + 2], imageData.data[(row * imageData.width + col) * 4 + 3]]
        for (var y = 0; y < scale; y++) {
          var destRow = Math.floor(row * scale) + y
          for (var x = 0; x < scale; x++) {
            var destCol = Math.floor(col * scale) + x
            for (var i = 0; i < 4; i++) {
              scaled.data[(destRow * scaled.width + destCol) * 4 + i] = sourcePixel[i]
            }
          }
        }
      }
    }
    return scaled
  }

通过上面方法进行图片数据的缩放。

关于具体的,我之前有篇文章,大家可以参考一下:https://www.haorooms.com/post/canvas_getimagedata

getImageData 可以改变canvas一些数据。

react-native-canvas 画板

react-native-canvas 画板其实本质也是用了react-native-webview,canvas的api和h5基本一致,但是使用下来有几个注意点

1、canvas不支持监听手势,需要外层包一层View,然后在View上面添加手势,进行监听位置移动,再来绘制画板

2、历史回退假如存放image数组的话,会很卡,需要利用path形式存放历史记录。

例如如下代码

  <View {...panResponder.panHandlers} style={{ width: wrapWidth, height: wrapWidth }}>
    {paths.length === 0 ? <Text className={Style.toptips}>让大家猜猜你画的啥</Text> : null}
    <Canvas style={{ width: wrapWidth, height: wrapWidth }} ref={canvasRef}></Canvas>
  </View>

如下方法里面进行手势监听

  const panResponder = useRef(
    PanResponder.create({
      onStartShouldSetPanResponder: () => true,
      onPanResponderGrant: moveStart,
      onPanResponderMove: listenToUser,
      onPanResponderRelease: () => {
        painting = false
      }
    })
  ).current

通过如下方式记录path

const { locationX, locationY } = event.nativeEvent

每一步记录一下。

moveStart函数里记录

   let _path = [...getPaths(), { x, y, isStart: true, color: getInitColor(), width: getWidth(), clear: getClear() }]
    setPaths(_path)

移动的时候记录

  let _path = [...getPaths(), { x, y, isStart: false, ...data }]
  setPaths(_path)

历史回退可以通过如下方案

  const deleteLastIndexBack = (data) => {
    if (!data || data.length === 0) {
      return []
    }
    const lastIndex = data.findLastIndex((item) => item.isStart === true)
    const result = lastIndex !== -1 ? data.slice(0, lastIndex) : data
    return result
  }

过滤掉isStart==false及最后一个true

react-native-canvas 注意点

另外的注意点就是canvas的写法必须完全符合规定,开始一定是ctx.beginPath(),假如漏掉或者不规范,就不行,这点在html5里面不太一样。

相关文章:

  1. 哥梵蒂冈人
    1
    蛋仔派对无限蛋币https://www.wzryzs.com/