I have an array of image urls. I want to display one image at a time on canvas. I've setup useEffect to be triggered when index of current image is changed.
I've researched online and found out that images load async and i must use onload callback to draw new image on canvas (Why won't my image show on canvas?).
I've setup effect and callback like this:
const [image, setImage] = useState<HTMLImageElement>()
useEffect(() => {
const image = new Image()
image.onload = () => update
image.src = images[imageState.imageIndex]?.url
setImage(image)
}, [imageState.imageIndex, images, update])
const update = useCallback(() => {
if (!canvasRef.current) return
const ctx = canvasRef.current.getContext('2d')
if (!ctx || !image) return
ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height)
canvasRef.current.width = image.width
canvasRef.current.height = image.height
ctx.drawImage(image, 0, 0)
}, [image])
canvas is setup like this:
export const ImageGallery = (props: ImageGalleryProps) => {
const classes = useImageGalleryStyles()
return (
<div className={classes.outerContainer}>
<canvas
style={{
left: `${imageState.position.x}px`,
top: `${imageState.position.y}px`,
transform: `rotate(${imageState.rotate}deg) scale(${imageState.scale})`,
}}
ref={canvas.ref}
className={classes.image}
/>
</div>
)
}
Images are changed like this:
Callback never called and canvas remains blank. What is proper way to update canvas?
Ended up with this:
useEffect(() => {
const image = new Image()
image.onload = function() {
if (!canvasRef.current) return
const ctx = canvasRef.current.getContext('2d')
if (!ctx || !image) return
ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height)
canvasRef.current.width = image.width
canvasRef.current.height = image.height
ctx.drawImage(image, 0, 0)
coordinates.map(c => {
switch (c.coordinates.$type) {
case DefectCoordinatesType.WideSide:
c.coordinates.WideSides.map(ws => {
if (image) {
ctx.beginPath()
ctx.imageSmoothingEnabled = false
ctx.moveTo(ws[0].x * image.width, ws[0].y * image.height)
ctx.lineTo(ws[1].x * image.width, ws[1].y * image.height)
ctx.closePath()
ctx.strokeStyle = 'red'
ctx.lineWidth = 2
ctx.stroke()
}
})
}
})
}
image.src = images[imageState.imageIndex]?.url
}, [checkBoundaries, coordinates, imageState.imageIndex, images, prevIndex, zoomOut])
Related
I have code written on react js, using canvas html 5. It is image eraser: it should properly erase front image and when only some persent of it left, it trigger another action(i haven not done another action yet). It works fine in browser, but when i choose adaptive for any smatphone, it stops working. I tried to change mouse events to touch events but it didn't help.
Here is the code:
import './DrawingCanvas.css';
import {useEffect, useRef, useState} from 'react';
const DrawingCanvas = () => {
const canvasRef = useRef(null);
const contextRef = useRef(null);
const [isDrawing, setIsDrawing] = useState(false);
const [persent, setPersent] = useState(100);
useEffect(() => {
const url = require('../../images/one.jpg');
const canvas = canvasRef.current;
const img = new Image();
img.src = url;
const context = canvas.getContext("2d");
img.onload = function () {
let width = Math.min(500, img.width);
let height = img.height * (width / img.width);
canvas.width = width;
canvas.height = height;
context.drawImage(img, 0, 0, width, height);
contextRef.current = context;
context.lineCap = "round";
context.lineWidth = 350;
contextRef.current.globalCompositeOperation = 'destination-out';
};
}, []);
useEffect(() => {
console.log(persent);
if(persent<=0.1) return alert('hello')
}, [persent]);
const startDrawing = ({nativeEvent}) => {
const {offsetX, offsetY} = nativeEvent;
contextRef.current.beginPath();
contextRef.current.moveTo(offsetX, offsetY);
contextRef.current.lineTo(offsetX, offsetY);
// contextRef.current.stroke();
setIsDrawing(true);
nativeEvent.preventDefault();
};
const draw = ({nativeEvent}) => {
if (!isDrawing) {
return;
}
const {offsetX, offsetY} = nativeEvent;
contextRef.current.lineTo(offsetX, offsetY);
contextRef.current.stroke();
nativeEvent.preventDefault();
let pixels = contextRef.current.getImageData(0, 0, canvasRef.current.width, canvasRef.current.height);
let transPixels = [];
for (let i = 3; i < pixels.data.length; i += 4) {
transPixels.push(pixels.data[i]);
}
let transPixelsCount = transPixels.filter(Boolean).length;
setPersent((transPixelsCount / transPixels.length) * 100);
};
const stopDrawing = () => {
contextRef.current.closePath();
setIsDrawing(false);
};
return (
<div>
<canvas className="canvas-container"
ref={canvasRef}
onMouseDown={startDrawing}
onMouseMove={draw}
onMouseUp={stopDrawing}
onMouseLeave={stopDrawing}
>
</canvas>
</div>
)
}
export default DrawingCanvas;
.canvas-container {
background-image: url("https://media.istockphoto.com/id/1322277517/photo/wild-grass-in-the-mountains-at-sunset.jpg?s=612x612&w=0&k=20&c=6mItwwFFGqKNKEAzv0mv6TaxhLN3zSE43bWmFN--J5w=");
}
Maybe it all can be done through touch events, but idk how to do that properly
i got a canvas in react that i want to allow users to draw rectangles, ive searched through stackoverflow and youtube videos regarding this but my rectangle cant seem to align with the crusor. everything works fine when the canvas is the only thing on the page, as in that its positioned on the top left. but when i include my other components everything does wrong in the canvas. please help, its my first time using canvas and ive only been following tutorials, trying to understand each functions but i wasnt able to find a solution.
const HomeCanvas = () => {
const canvasRef = useRef(null);
const contextRef = useRef(null);
const [isDrawing, setIsDrawing] = useState(false);
const canvasOffSetX = useRef(null);
const canvasOffSetY = useRef(null);
const startX = useRef(null);
const startY = useRef(null);
useEffect(() => {
const canvas = canvasRef.current;
canvas.width = 500;
canvas.height = 500;
const context = canvas.getContext("2d");
context.lineCap = "round";
context.strokeStyle = "black";
context.lineWidth = 2;
contextRef.current = context;
const canvasOffSet = canvas.getBoundingClientRect();
canvasOffSetX.current = canvasOffSet.top;
canvasOffSetY.current = canvasOffSet.left;
console.log(canvasOffSet.left);
console.log(canvasOffSet.top);
}, []);
const startDrawingRectangle = ({ nativeEvent }) => {
nativeEvent.preventDefault();
nativeEvent.stopPropagation();
startX.current = nativeEvent.clientX - canvasOffSetX.current;
startY.current = nativeEvent.clientY - canvasOffSetY.current;
setIsDrawing(true);
};
const drawRectangle = ({ nativeEvent }) => {
if (!isDrawing) {
return;
}
nativeEvent.preventDefault();
nativeEvent.stopPropagation();
const newMouseX = nativeEvent.clientX - canvasOffSetX.current;
const newMouseY = nativeEvent.clientY - canvasOffSetY.current;
const rectWidth = newMouseX - startX.current;
const rectHeight = newMouseY - startY.current;
contextRef.current.clearRect(
0,
0,
canvasRef.current.width,
canvasRef.current.height
);
contextRef.current.strokeRect(
startX.current,
startY.current,
rectWidth,
rectHeight
);
};
const stopDrawingRectangle = () => {
setIsDrawing(false);
};
return (
<div className="home-canvas">
<div className="canvas-sidebar">
<div>Merge Table</div>
</div>
<div style={{ width: "100%", paddingLeft: "5%" }}>
<canvas
className="canvas-area"
ref={canvasRef}
onMouseDown={startDrawingRectangle}
onMouseUp={stopDrawingRectangle}
onMouseMove={drawRectangle}
onMouseLeave={stopDrawingRectangle}
></canvas>
</div>
</div>
);
};
what did i do wrong? here is the video of it.
https://youtu.be/ioGINfUKBgc
I want to get pixel-by-pixel color data for an image so I can adapt it into a "8-bit" sort of look using a grid of colored squares.
From the research I've done so far, it looks like the standard way to get that kind of data is using an HTML5 canvas and context.getImageData (example). So far though I've had no luck getting this to work in a React app.
The closest I've gotten is this. I'm sure there are a million things wrong with it, probably to do with how I'm interacting with the DOM, but it at least returns an imageData object. The problem is that the color value for every pixel is 0.
Updated to use ref instead of getElementById
function App() {
const imgRef = useRef(null);
const img = <img ref={imgRef} src={headshot} />;
// presumably we want to wait until after render?
useEffect(() => {
if (imgRef === null) {
console.log("image ref missing");
return;
}
if (imgRef.current === null) {
console.log("image ref is null");
return;
}
// couldn't use imgRef.current.offsetHeight/Width for these because typscript
// thinks imgRef.current is `never`?
const height = 514;
const width = 514;
const canvas = document.createElement('canvas');
canvas.height = height; canvas.width = width;
const context = canvas.getContext && canvas.getContext('2d');
if (context === null) {
console.log(`context or image missing`);
return
}
context.drawImage(imgRef.current, 0, 0);
const imageData = context.getImageData(0, 0, width, height);
console.log(`Image Data`, imageData);
}, []);
return img;
}
Related: ultimately I want to get this data without actually displaying the image, so any tips on that would also be appreciated.
Thanks!
You're not waiting for the image to load. (As with regular HTML, that happens asynchronously.)
So, instead of using an effect that's run as soon as the component has mounted, hook it up to your image's onLoad. Aside from that:
You don't need to check whether imgRef is null; the ref box itself never is.
When using TypeScript and DOM refs, use useRef<HTML...Element>(null); to have ref.current have the correct type.
There is no need to make imgRef.current a dependency for the handler, since refs changing do not cause components to update.
export default function App() {
const imgRef = React.useRef<HTMLImageElement>(null);
const readImageData = React.useCallback(() => {
const img = imgRef.current;
if (!img?.width) {
return;
}
const { width, height } = img;
const canvas = document.createElement("canvas");
canvas.height = height;
canvas.width = width;
const context = canvas.getContext?.("2d");
if (context === null) {
return;
}
context.drawImage(img, 0, 0);
const imageData = context.getImageData(0, 0, width, height);
console.log(`Image Data`, imageData);
}, []);
return <img ref={imgRef} src={kitten} onLoad={readImageData} />;
}
EDIT
Do you know how I can do this now without actually displaying the image?
To read an image without actually showing it in the DOM, you'll need new Image - and then you can use an effect.
/** Promisified image loading. */
function loadImage(src: string): Promise<HTMLImageElement> {
return new Promise((resolve, reject) => {
const img = new Image();
img.addEventListener("load", () => {
resolve(img);
});
img.addEventListener("error", reject);
img.src = src;
});
}
function analyzeImage(img: HTMLImageElement) {
const { width, height } = img;
const canvas = document.createElement("canvas");
canvas.height = height;
canvas.width = width;
const context = canvas.getContext?.("2d");
if (context === null) {
return;
}
context.drawImage(img, 0, 0);
const imageData = context.getImageData(0, 0, width, height);
console.log(`Image Data`, imageData);
}
export default function App() {
React.useEffect(() => {
loadImage(kitten).then(analyzeImage);
}, []);
return <>hi</>;
}
I have a canvas component in react. I am using useEffect to get the canvas element. So i have defined all needed functions in useEffect, as you can see below
import { useEffect, useRef } from "react"
import * as blobs2Animate from "blobs/v2/animate"
export const CornerExpand = () => {
const canvasRef = useRef<HTMLCanvasElement>(null)
useEffect(() => {
const canvas = canvasRef.current!
const ctx = canvas.getContext("2d")
const animation = blobs2Animate.canvasPath()
const width = canvas.clientWidth * window.devicePixelRatio
const height = canvas.clientHeight * window.devicePixelRatio
canvas.width = width
canvas.height = height
const renderAnimation = () => {
if (!ctx) return
ctx.clearRect(0, 0, width, height)
ctx.fillStyle = "#E0F2FE"
ctx.fill(animation.renderFrame())
requestAnimationFrame(renderAnimation)
}
requestAnimationFrame(renderAnimation)
const size = Math.min(width, height) * 1
const defaultOptions = () => ({
blobOptions: {
seed: Math.random(),
extraPoints: 36,
randomness: 0.7,
size,
},
canvasOptions: {
offsetX: -size / 2.2,
offsetY: -size / 2.2,
},
})
const loopAnimation = () => {
animation.transition({
duration: 4000,
timingFunction: "ease",
callback: loopAnimation,
...defaultOptions(),
})
}
animation.transition({
duration: 0,
callback: loopAnimation,
...defaultOptions(),
})
const fullscreen = () => {
const options = defaultOptions()
options.blobOptions.size = Math.max(width, height) * 1.6
options.blobOptions.randomness = 1.4
options.canvasOptions.offsetX = -size / 2
options.canvasOptions.offsetY = -size / 2
animation.transition({
duration: 2000,
timingFunction: "elasticEnd0",
...options,
})
}
}, [canvasRef])
return (
<div>
<canvas className="absolute top-0 left-0 h-full w-full" ref={canvasRef} />
</div>
)
}
export default CornerExpand
Everything works as well, but now I have a problem. I want to execute the fullscreen() function when a button is clicked in the parent component. Since I have defined the function in useEffect, I can't call it directly, isn't it? What can I do to solve this?
You can do it like this.
export const CornerExpand = React.forwardRef((props, ref) => {
//....
{
//...
const fullscreen = () => {
const options = defaultOptions()
options.blobOptions.size = Math.max(width, height) * 1.6
options.blobOptions.randomness = 1.4
options.canvasOptions.offsetX = -size / 2
options.canvasOptions.offsetY = -size / 2
animation.transition({
duration: 2000,
timingFunction: "elasticEnd0",
...options,
})
}
ref.current = fullscreen;
}, [canvasRef]);
You can wrap this comp with React.forwardRef. And call it from parent.
<CornerExpand ref={fullscreenCallbackRef} />
And then call like this
fullscreenCallbackRef.current()
I have a react app that allows you to draw and select different line widths.
Code:
const [sliderVal, setSliderVal] = useState(1)
const canvasRef = useRef(null)
useEffect(() => {
const canvas = canvasRef.current
const ctx = canvas.getContext('2d')
ctx.canvas.width = window.innerWidth;
ctx.canvas.height = window.innerHeight;
/* Mouse Capturing Work */
var mouse = {x: 0, y: 0};
var last_mouse = {x: 0, y: 0};
canvas.addEventListener('mousemove', function(e) {
last_mouse.x = mouse.x;
last_mouse.y = mouse.y;
mouse.x = e.pageX - this.offsetLeft;
mouse.y = e.pageY - this.offsetTop;
canvas.style.cursor = "crosshair";
}, false);
/* Drawing on Paint App */
ctx.lineWidth = sliderVal;
ctx.lineJoin = 'round';
ctx.lineCap = 'round';
ctx.strokeStyle = 'blue';
canvas.addEventListener('mousedown', function() {
canvas.addEventListener('mousemove', onPaint, false);
}, false);
canvas.addEventListener('mouseup', function() {
canvas.removeEventListener('mousemove', onPaint, false);
}, false);
var onPaint = function() {
ctx.beginPath();
ctx.moveTo(last_mouse.x, last_mouse.y);
ctx.lineTo(mouse.x, mouse.y);
ctx.closePath();
ctx.stroke();
};
}, [sliderVal])
I'm trying to make it so if sliderVal changes the whole canvas wont re-render only the line width will. Right now it works but if I select a new lineWidth the whole canvas gets cleared.
I recommend you not to use the eventListener in that way, canvas in ReactJS already has some methods to which you can access and pass a function. This new way of handling the canvas solves your problem:
const [sliderVal, setSliderVal] = useState(1);
const [mouse, setMouse] = useState({
x: 0,
y: 0,
lastX: 0,
lastY: 0,
click: false
});
const canvasRef = useRef(null);
const paint = (e) => {
const canvas = canvasRef.current;
const ctx = canvas.getContext("2d");
const bounds = canvas.getBoundingClientRect();
setMouse({
...mouse,
x: e.pageX - bounds.left - window.scrollX,
y: e.pageY - bounds.top - window.scrollY,
lastX: mouse.x,
lastY: mouse.y
});
ctx.lineWidth = sliderVal;
ctx.lineJoin = "round";
ctx.lineCap = "round";
ctx.strokeStyle = "blue";
if (mouse.click) {
ctx.beginPath();
ctx.moveTo(mouse.lastX, mouse.lastY);
ctx.lineTo(mouse.x, mouse.y);
ctx.closePath();
ctx.stroke();
}
};
useEffect(() => {
const canvas = canvasRef.current;
const ctx = canvas.getContext("2d");
ctx.canvas.width = window.innerWidth;
ctx.canvas.height = window.innerHeight;
canvas.style.cursor = "crosshair";
}, []);
return (
<canvas
onMouseDown={() => setMouse({ ...mouse, click: true })}
onMouseMove={(e) => paint(e)}
onMouseUp={() => setMouse({ ...mouse, click: false })}
ref={canvasRef}
/>
I share a codesandbox with the functional solution