Im trying to create a drawing application in react, its working for the most part. But when i try add an undo button it doesnt work.
I try make the undo button with this finishDrawing()
function, where i use getImageData and store it in an array called restore_array
const finishDrawing = () => { //console.log(contextRef.current) contextRef.current.closePath() setIsDrawing(false) //restore_array.push(contextRef.current.getImageData(0, 0,400,256+80)) setIndex_array(index_array + 1) setRestore_Array((prevState) => { return [...prevState, contextRef.current.getImageData(0, 0,400,256+80)]}) console.log(restore_array, restore_array.length) }
then when a Undo button is clicked i try to reload this image in the restore_array
with a useEffect
which should re-render the entree canvas with the previous image
function undoLast() { console.log(restore_array[index_array], index_array, restore_array) if (index_array > -1) { setIndex_array(index_array-1) restore_array.pop() setLines(!lines) } //contextRef.current.putImageData(restore_array[index_array-1], 0, 0) }
and
useEffect(() => { console.log('use effect 3c') console.log(restore_array) const canvas = canvasRef.current; const context = canvas.getContext("2d") context.lineWidth = thick context.lineCap = "round" context.strokeStyle = colour if (restore_array.length === 0) { console.log('empty') return } //context.clearRect(0,0,400,256+80) //context.fillRect(0,0,400,256+80) context.putImageData(restore_array[index_array], 0, 0, 0, 0, 400, 256+80) contextRef.current = context }, [lines])
Does anyone know why my putImageData
isnt working in this case?
Heres my entire code for reference
import React, { useEffect, useRef, useState } from "react"; function DrawCanvas() { const canvasRef = useRef(null) const contextRef = useRef(null) const [isDrawing, setIsDrawing] = useState(false) const [colour, setColour] = useState('black') const [prcolour, setPrColour] = useState(null) const [thick, setThick] = useState(3) const [image, setImage] = useState(null) const [restore_array, setRestore_Array] = useState([]) const [index_array, setIndex_array] = useState(-1) const [clear_, setClear_] = useState(true) const [lines, setLines] = useState(true) //let restore_array = [] useEffect(() => { const canvas = canvasRef.current; canvas.width = window.innerWidth/1.5 ; canvas.height = window.innerHeight/1.5 ; canvas.style.width = `${window.innerWidth/3}px`; canvas.style.height = `${window.innerHeight/3}px`; const context = canvas.getContext("2d") context.scale(2,2) context.lineCap = "round" context.lineWidth = 3 context.strokeStyle = "black" //const img = DogImage context.fillStyle = 'gray' context.fillRect(0,0,400,256+80) contextRef.current = context }, []) useEffect(() => { console.log('use effect 1a') const canvas = canvasRef.current; const context = canvas.getContext("2d") context.lineWidth = thick context.lineCap = "round" context.strokeStyle = colour contextRef.current = context document.getElementById(colour).style.borderColor = 'gray' if(prcolour === null) { return } document.getElementById(prcolour).style.borderColor = 'black' document.getElementById(prcolour).style.borderWidth = '3px' }, [colour]) useEffect(() => { console.log('use effect 2b') const canvas = canvasRef.current; const context = canvas.getContext("2d") context.lineWidth = thick context.lineCap = "round" context.strokeStyle = colour const Dogimage = new Image(); Dogimage.src = '../../images/dog-without-labels.png' Dogimage.onLoad = () => { console.log('load image') context.current.drawImage(Dogimage, 0, 0,400,256+80); } contextRef.current = context }, [thick]) useEffect(() => { console.log('use effect 3c') console.log(restore_array) const canvas = canvasRef.current; const context = canvas.getContext("2d") context.lineWidth = thick context.lineCap = "round" context.strokeStyle = colour if (restore_array.length === 0) { console.log('empty') return } //context.clearRect(0,0,400,256+80) //context.fillRect(0,0,400,256+80) context.putImageData(restore_array[index_array], 0, 0, 0, 0, 400, 256+80) contextRef.current = context }, [lines]) useEffect(() => { console.log('use effect 4d') const canvas = canvasRef.current; const context = canvas.getContext("2d") //contextRef.fillStyle = 'gray' context.lineWidth = thick context.lineCap = "round" context.strokeStyle = colour context.clearRect(0,0,400,256+80) context.fillRect(0,0,400,256+80) contextRef.current = context }, [clear_]) function undoLast() { console.log(restore_array[index_array], index_array, restore_array) if (index_array > -1) { setIndex_array(index_array-1) restore_array.pop() setLines(!lines) } //contextRef.current.putImageData(restore_array[index_array-1], 0, 0) } const clearCanvas = () => { setClear_(!clear_) } const startDrawing = ({nativeEvent}) => { const {offsetX, offsetY} = nativeEvent contextRef.current.beginPath() contextRef.current.moveTo(offsetX, offsetY) setIsDrawing(true) } const finishDrawing = () => { //console.log(contextRef.current) contextRef.current.closePath() setIsDrawing(false) //restore_array.push(contextRef.current.getImageData(0, 0,400,256+80)) setIndex_array(index_array + 1) setRestore_Array((prevState) => { return [...prevState, contextRef.current.getImageData(0, 0,400,256+80)]}) console.log(restore_array, restore_array.length) } const draw = ({nativeEvent}) => { if (!isDrawing) { return } const {offsetX, offsetY} = nativeEvent; contextRef.current.lineTo(offsetX, offsetY) contextRef.current.stroke() } const colourChange = (value) => { setPrColour(colour) setColour(value) } const thickChange = (event) => { setThick(event) } return ( <div> <div className="left"> <canvas className="canvas" onMouseDown={startDrawing} onMouseUp={finishDrawing} onMouseMove={draw} ref={canvasRef}/> </div> <div className="right"> <h5><dt>Chart Draw</dt></h5> <p>Use your finger or your mouse to draw directly on the chart opposite.</p> <button className="draw">Draw</button> <button className="select">Select</button> <p><dt>Line thickness: {thick} </dt> </p> <input className="slider" type="range" min="1" max="19" value={thick} class="slider" id="myRange" step="1" onChange={(event)=>thickChange(event.target.value)}></input> <button className='red' id='red'onClick={() => colourChange('red')}> </button> <button className='black' id='black' onClick={() => colourChange('black')}> </button> <button className='yellow' id='rgba(204,153,0,255)' onClick={() => colourChange('rgba(204,153,0,255)')}> </button> <button className='blue' id='rgba(1,102,255,255)' onClick={() => colourChange('rgba(1,102,255,255)')}> </button> <button className="undoButton" onClick={()=>undoLast()}> <dt> Undo/Back a step</dt></button> <button className="undoButton" onClick={()=>clearCanvas()}> <dt> Clear canvas</dt></button> <button className="savecloseButton"> <dt>Save and Close</dt></button> </div> </div> ) } export default DrawCanvas;
Advertisement
Answer
Your code works “OK” for me…
I think it’s obvious that your putImageData
in function undoLast is commented.
I’m assuming that was intentional, not the actual problem.
Yes only part is getting the undo.
That is because you have hardcoded values in the getImageData:
setRestore_Array((prevState) => { return [...prevState, contextRef.current.getImageData(0, 0,400,256+80)] })
That only gets a portion of the canvas, so only that portion is getting restored…
if you needed the entire canvas, change that to use the canvas dimensions.
Something like:
setRestore_Array((prevState) => { return [...prevState, contextRef.current.getImageData(0, 0, canvasRef.current.width, canvasRef.current.height)] })
Also you should look at what @Kaiido mentioned, he has a good point:
storing an Array of drawing commands will cause your memory to explode
For your approach using image data my recommendation is to limit the undo to a fix amount, like 10 and discard older, that will guarantee a cap on memory consumption and prevent any out of memory errors.