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
Related
I'm trying to implement a virtualisation component with reactjs.
In order to fill the empty top and bottom gap I used an empty div with dynamic height which depends on the scroll position.
<div style={{ height: topPlaceholderHeight }} />
{visibleItems.map(renderItem)}
<div style={{ height: bottomPlaceholderHeight }} />
When the user scrolls > a scroll event is triggered > placeholderHeight is updated > triggers an other scroll event => infinite loop that leads to an auto scroll to the bottom
const [start, setStart] = useState(0);
const [end, setEnd] = useState(0);
const [scrollTop, setScrollTop] = useState(0);
const parentRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const listElement = parentRef.current;
if (!listElement) return;
const itemsPerPage = Math.ceil(listElement.offsetHeight / itemHeight) + 2;
const newStart = Math.max(0, Math.floor(scrollTop / itemHeight) - 1);
const newEnd = Math.min(items.length, newStart + itemsPerPage);
setStart(newStart);
setEnd(newEnd);
}, [items, itemHeight, scrollTop]);
useEffect(() => {
const listElement = parentRef.current;
if (!listElement) return;
const scrollHandler = () => {
setScrollTop(listElement.scrollTop);
};
listElement.addEventListener("scroll", scrollHandler);
return () => listElement.removeEventListener("scroll", scrollHandler);
}, [parentRef]);
Code sandbox
Any suggestions for solving this problem ?
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 am trying to play an animation that comes from a blender model while simultaneously having the model auto-rotate from OrbitControls. However I am getting the model to be duplicated, one model is animated on top of a stationary copy of the model that is unanimated:
I've tried removing the orbit controls code and update however I am still getting this result. Any advice would be greatly appreciated.
import {
AmbientLight,
Color,
DirectionalLight,
Mesh,
AnimationMixer,
MeshPhongMaterial,
PerspectiveCamera,
Scene,
SphereBufferGeometry,
UniformsUtils,
Vector2,
WebGLRenderer,
sRGBEncoding,
PointLight,
Clock,
} from 'three';
import { cleanRenderer,
cleanScene,
removeLights,
modelLoader,
textureLoader, } from 'utils/three';
import { GLTFLoader, OrbitControls, RenderPixelatedPass } from 'three-stdlib';
import { render } from 'react-dom';
export const ScifiWorkerRobot = props => {
const theme = useTheme();
const [loaded, setLoaded] = useState(false);
const { rgbBackground, themeId, colorWhite } = theme;
const start = useRef(Date.now());
const canvasRef = useRef();
const mouse = useRef();
const renderer = useRef();
const camera = useRef();
const scene = useRef();
const lights = useRef();
const uniforms = useRef();
const material = useRef();
const geometry = useRef();
const sphere = useRef();
const reduceMotion = useReducedMotion();
const isInViewport = useInViewport(canvasRef);
const windowSize = useWindowSize();
const rotationX = useSpring(0, springConfig);
const rotationY = useSpring(0, springConfig);
const { measureFps, isLowFps } = useFps(isInViewport);
const cameraXSpring = useSpring(0, cameraSpringConfig);
const cameraYSpring = useSpring(0, cameraSpringConfig);
const cameraZSpring = useSpring(0, cameraSpringConfig);
const controls = useRef();
const animations = useRef();
const mixer = useRef();
const clock = useRef();
const animationFrame = useRef();
const mounted = useRef();
const sceneModel = useRef();
//Setting up initial scene
useEffect(() => {
//mounted.current = true;
const { innerWidth, innerHeight } = window;
mouse.current = new Vector2(0.8, 0.5);
renderer.current = new WebGLRenderer({
canvas: canvasRef.current,
antialias: false,
alpha: true,
powerPreference: 'high-performance',
failIfMajorPerformanceCaveat: true,
});
renderer.current.setSize(innerWidth, innerHeight);
renderer.current.setPixelRatio(1);
renderer.current.outputEncoding = sRGBEncoding;
camera.current = new PerspectiveCamera(54, innerWidth / innerHeight, 0.1, 100);
camera.current.position.z = 5;
camera.current.position.x = 0;
camera.current.position.y = 0;
scene.current = new Scene();
clock.current = new Clock();
const ambientLight = new AmbientLight(colorWhite, 0.9);
const dirLight = new DirectionalLight(colorWhite, 0.8);
dirLight.position.set(0,0,0);
const lights = [ambientLight, dirLight];
lights.forEach(light => scene.current.add(light));
controls.current = new OrbitControls(camera.current, canvasRef.current);
controls.current.enableZoom = true;
controls.current.enablePan = true;
controls.current.enableDamping = true;
controls.current.rotateSpeed = 0.5;
controls.current.autoRotate = true;
const loader = new GLTFLoader();
loader.load(veribot, function ( gltf ) {
scene.current.add( gltf.scene );
animations.current = gltf.animations;
mixer.current = new AnimationMixer(gltf.scene);
mixer.current.timeScale = 0.8;
animations.current.forEach((clip, index) => {
const animation = mixer.current.clipAction(clip);
animation.play();
});
}, undefined, function ( error ) {
console.error( error );
} );
return () => {
//mounted.current = false;
cancelAnimationFrame(animationFrame.current);
removeLights(lights);
cleanScene(scene.current);
cleanRenderer(renderer.current);
};
}, []);
useEffect(() => {
let animation;
const animate = () => {
animation = requestAnimationFrame(animate);
const delta = clock.current.getDelta();
if (mixer.current){
mixer.current.update(delta);
}
controls.current.update();
renderer.current.render(scene.current, camera.current);
measureFps();
};
animate();
return () => {
cancelAnimationFrame(animation);
};
}, [isInViewport, measureFps, reduceMotion, isLowFps, rotationX, rotationY]);
return (
<Transition in timeout={2000}>
{visible => (
<canvas
aria-hidden
className={styles.canvas}
data-visible={visible}
ref={canvasRef}
{...props}
/>
)}
</Transition>
);
};
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])
I need to make a styled-component div with button, button generates size of the div. Then a component nested inside of the div component displays the size. It has to use forwardRef and both components have to be functional.
Base component code:
const StyledComp = () => {
const [size, setSize] = useState({ width: 100, height: 100 });
const [maxMin, setMaxMin] = useState({ max: 500, min: 100 });
const [count, setCount] = useState(0);
let ref = useRef(<Shape />);
const handleResize = () => {
const max = maxMin.max;
const min = maxMin.min;
let width = Math.floor(Math.random() * (max - min)) + min;
let height = Math.floor(Math.random() * (max - min)) + min;
setSize({ width, height });
setCount(count+1);
};
return (
<div className="styled-component">
<Shape height={size.height} width={size.width} ref={ref}>
<ShapeResult count={count} {...{ ref }} />
</Shape>
<p>Width: {size.width}</p>
<p>Height: {size.height}</p>
<button onClick={handleResize}>Generate</button>
</div>
);
};
The component is a simple styled-components div with width and height from props.
The gets the ref just fine, I can print it in console and it does show the current size of the element. But when I try to get the width/height values via innerRef.current.offsetWidth it gets previous generation results. Here's the code:
const ShapeResult = ({ count }, ref) => {
let innerRef = React.useRef(ref);
const [countState, setCountState] = useState(0);
const [width, setWidth] = useState(100);
const [height, setHeight] = useState(100);
useEffect(() => {
innerRef = ref;
console.log(innerRef.current);
setWidth(innerRef.current.offsetWidth);
setHeight(innerRef.current.offsetHeight);
setCountState(count);
}, [count, ref]);
return (
<div className="shape-result">
<p>Click count: {countState}</p>
<p>Width: {width}px</p>
<p>Height: {height}px</p>
</div>
);
};
const ForwardedResult = React.forwardRef(ShapeResult);
export default ForwardedResult;
I'll be grateful for any help, I've been searching for an answer for way too long and it really shouldn't be that hard task...
Apparently the main problem was in the useEffect. It should be:
setWidth(innerRef.current.getAttribute("width"));
setHeight(innerRef.current.getAttribute("height"));
It finally updates now
Your approach seems incorrect.
As shown below, in the StyledComp component, access DOM through shapeResultForwardRef.current.
const StyledComp = () => {
const [size, setSize] = useState({ width: 100, height: 100 });
const [maxMin, setMaxMin] = useState({ max: 500, min: 100 });
const [count, setCount] = useState(0);
let ref = useRef(<Shape />);
const shapeResultForwardRef = useRef(null); // add
const handleResize = () => {
const max = maxMin.max;
const min = maxMin.min;
let width = Math.floor(Math.random() * (max - min)) + min;
let height = Math.floor(Math.random() * (max - min)) + min;
setSize({ width, height });
setCount(count+1);
};
return (
<div className="styled-component">
<Shape height={size.height} width={size.width} ref={ref}>
<ShapeResult count={count} ref={shapeResultForwardRef} />
</Shape>
<p>Width: {size.width}</p>
<p>Height: {size.height}</p>
<button onClick={handleResize}>Generate</button>
</div>
);
};
another:
const ShapeResult = ({ count }, ref) => {
return (
<div className="shape-result" ref={ref}>
...
</div>
);
};
const ForwardedResult = React.forwardRef(ShapeResult);
export default ForwardedResult;