First time working with canvas in react. I am wondering what the best way to provide methods with the canvas context is (like paint below). Should I use Refs? State? Or is there another way. Thanks
Here is what I have for using refs so far:
class Canvas extends React.Component{
canvasRef = React.createRef();
contextRef = React.createRef();
componentDidMount() {
const canvas = this.canvasRef.current;
const context = canvas.getContext("2d");
this.contextRef.current = context;
}
paint = (e) => {
this.contextRef.current.lineTo(e.clientX, e.clientY);
this.contextRef.current.stroke();
};
render() {
return ( <
canvas id = "canvas"
ref = {
this.canvasRef
}
onMouseMove = {
this.paint
}
/>
);
}
}
Refs are correct for getting the canvas, but I'd suggest getting the 2D context each time you need it (getContext just returns the same context as the previous time).
You have a few other minor issues with that code, here's an example putting the context in an instance property, and fixing various issues called out inline.
class Canvas extends React.Component { // *** Note: You need to extend `React.Component`
canvasRef = React.createRef(); // *** Note: You shouldn't have `this.` here
paint = (e) => {
// ** Get the canvas
const canvas = this.canvasRef.current;
if (!canvas) {
return; // This probably won't happen
}
// Get the context
const context = canvas.getContext("2d");
// Get the point to draw at, allowing for the possibility
// that the canvas isn't at the top of the doc
const { left, top } = canvas.getBoundingClientRect();
const x = e.clientX - left;
const y = e.clientY - top;
context.lineTo(x, y);
context.stroke();
}; // *** Added missing ; here
render() {
return <canvas ref={this.canvasRef} onMouseMove={this.paint} />;
}
}
(I removed the id on the canvas, since your component might get used in more than one place.)
In a comment you mentioned issues with state changes causing trouble (I saw that as well when trying to get and keep the context rather than getting it each time), so I've added state both to a parent container element and also to the Canvas element in the following example to check that it continues to work, which it does:
const { useState } = React;
class Canvas extends React.Component { // *** Note: You need to extend `React.Component`
canvasRef = React.createRef(); // *** Note: You shouldn't have `this.` here
state = {
ticker: 0,
};
paint = (e) => {
// ** Get the canvas
const canvas = this.canvasRef.current;
if (!canvas) {
return; // This probably won't happen
}
// Get the context
const context = canvas.getContext("2d");
// Get the point to draw at, allowing for the possibility
// that the canvas isn't at the top of the doc
const { left, top } = canvas.getBoundingClientRect();
const x = e.clientX - left;
const y = e.clientY - top;
context.lineTo(x, y);
context.stroke();
}; // *** Added missing ; here
increment = () => {
this.setState(( { ticker }) => ({ ticker: ticker + 1 }));
};
render() {
const { ticker } = this.state;
return <div>
<canvas ref={this.canvasRef} onMouseMove={this.paint} />
<div onClick={this.increment}>Canvas ticker: {ticker}</div>
</div>;
}
}
class Example extends React.Component {
state = {
ticker: 0,
};
increment = () => {
this.setState(({ ticker }) => ({ ticker: ticker + 1 }));
};
render() {
const { ticker } = this.state;
return <div>
<div onClick={this.increment}>Parent ticker: {ticker}</div>
<Canvas />
</div>;
}
}
ReactDOM.render(<Example />, document.getElementById("root"));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>
Related
I have a canvas element that should be resizing as the window resizes, but for some reason it doesn't change. The element should remain a square, taking up 80% of the view height.
The relevant css looks like this, and the square class works for another element that isn't being updated in css and javascript.
static get styles() {
return [
tachyons,
css`
.square {
width: min(50vw, 75vh);
height: min(50vw, 75vh);
}`
]
}
The canvas doesn't update despite the class being recognized by the browser. I'm using Safari for testing (don't ask why).
const karelView = ({ displayAltCanvas, index, indexes, className }) => {
return html`<div class=${className || '' + ' mt5'}>
<canvas
id="canvasAlt"
class="square ${displayAltCanvas ? 'bg-light-yellow' : 'absolute o-0'}"
></canvas>
<canvas id="canvas" class="square${displayAltCanvas ? ' dn' : ''}"></canvas>
<div class="db w-100">
<input
class="w-100"
type="range"
min="0"
value=${index ? index() : 0}
max=${indexes || 0}
step="1"
#input=${e => index?.(parseInt(e.target.value))}
/>
</div>
</div>`;
};
This is where I think the error is coming from. The callback is being fired correctly, but for some reason the resolutions still match even though the window has resized and the css should have updated the clientWidth.
async handleResize(canvas) {
canvas ??= this.canvas;
if (canvas === null) {
console.warn('null canvas');
return;
}
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const resolutionMatch = canvas.width === width && canvas.height === height;
if (!resolutionMatch) {
canvas.width = width;
canvas.height = height;
}
const world = this.state || (await this.worlds).currentWorld;
// this.index !== undefined && this.index(this.index());
draw(this.canvas, world);
}
connectedCallback() {
super.connectedCallback();
this._resizers = (() => {
this.handleResize(this.canvas);
this.handleResize(this.canvasAlt);
}).bind(this);
window.addEventListener('resize', this._resizers);
}
render() {
return html` ${karelView({
displayAltCanvas: this.displayAltCanvas,
index: this.index,
indexes: this.indexes,
})}`;
}
<canvas>.clientWidth properly resizes in native code.
So if Lit is flawless, it must be a bug in your code.
Notes:
The connectedCallback will fire multiple times when you move the element in the DOM
no need for oldskool bind() (unless you pass a funtion reference)
No need to declare async handleResize (and it is not the cause of your error)
<my-element></my-element>
<script>
customElements.define('my-element', class extends HTMLElement {
constructor() {
super()
.attachShadow({mode:'open'})
.innerHTML = `<b/><style>canvas{width:50vw}</style><canvas></canvas>`;
}
query(selector){
return this.shadowRoot.querySelector(selector);
}
connectedCallback() {
window.addEventListener("resize", evt => this.handleResize(evt) );
this.handleResize();
}
handleResize(evt) {
let canvas = this.query("canvas");
this.query("b").innerHTML = canvas.clientWidth + "px";
}
})
</script>
Or:
<my-element></my-element>
<script>
customElements.define('my-element', class extends HTMLElement {
constructor() {
super()
.attachShadow({mode:'open'})
.innerHTML = `<b/><style>canvas{width:50vw}</style><canvas></canvas>`;
window.addEventListener("resize", evt => {
this.shadowRoot.querySelector("b").innerHTML =
this.shadowRoot.querySelector("canvas").clientWidth + "px";
});
window.dispatchEvent(new Event("resize"));
}
})
</script>
I am working on a react application at the minute that has some functionality that allows the user to drag and image around the canvas and reposition it, I have move functionality working but it's not particularly nice, it just seems to redraw a new image over the top of the old giving an image within an image within an image feedback. Secondly the it seems to drag to the top left corner of the of image not the click point I am not sure what I am doing wrong.
Here is an example, https://j3mzp8yxwy.codesandbox.io/, and my react code below,
import React, { Component } from 'react';
import { connect } from 'react-redux';
import styled from "styled-components";
import { toggleCommentModal, setCommentPos } from '../../actions/index';
import CommentHandle from "../../CommentHandle";
class ConnectedCanvasStage extends Component {
constructor(props) {
super(props);
this.handleMouseDown = this.handleMouseDown.bind(this);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.handleMouseUp = this.handleMouseUp.bind(this);
this.state = {
dragging : false,
dragoffx : 0,
dragoffy : 0
}
}
render() {
const CanvasStage = styled.div`
width: 100%;
height: ${document.body.clientHeight}px;
position:relative;
`;
return(
<CanvasStage>
<canvas id="canvas"
ref="canvas"
width={document.body.clientWidth - (document.body.clientWidth * 0.10)}
height={document.body.clientHeight}
onMouseDown={this.handleMouseDown}
onMouseMove={this.handleMouseMove}
onMouseUp={this.handleMouseUp}
/>
</CanvasStage>
);
}
componentDidMount() {
this.updateCanvas();
}
updateCanvas(x=0, y=0) {
this.ctx = this.refs.canvas.getContext("2d");
let base_image = new Image();
base_image.src = this.props.image;
base_image.onload = () => {
this.ctx.drawImage(base_image, x, y, (document.body.clientWidth - (document.body.clientWidth * 0.10)) * 2, document.body.clientHeight * 2);
}
}
handleMouseDown(event) {
switch(this.props.activeTool) {
case "move":
let mx = event.clientX;
let my = event.clientY;
let pos = this.getRelativeClickPosition(document.getElementById('canvas'), event);
this.setState({
dragging: !this.state.dragging,
dragoffx : parseInt(mx - pos.x),
dragoffy : parseInt(my - pos.y)
});
break;
case "comment":
let comment_position = this.getRelativeClickPosition(document.getElementById('canvas'), event);
this.addComment(comment_position.x, comment_position.y);
break;
}
}
handleMouseMove(event) {
if(this.props.activeTool == 'move' && this.state.dragging) {
var dx = event.clientX - this.state.dragoffx;
var dy = event.clientY - this.state.dragoffy;
this.updateCanvas(dx, dy);
}
}
handleMouseUp() {
this.setState({
dragging :false
});
}
shouldComponentUpdate() {
return false;
}
addComment(x, y) {
x = x - 24;
y = y - 20;
this.props.toggleCommentModal(true);
this.props.setCommentPos({x, y});
}
getRelativeClickPosition(canvas, event) {
var rect = canvas.getBoundingClientRect();
return {
x: event.clientX - rect.left,
y: event.clientY - rect.top
};
}
}
const mapDispatchToProps = dispatch => {
return {
toggleCommentModal : visible => dispatch(toggleCommentModal(visible)),
setCommentPos : position => dispatch(setCommentPos(position))
};
};
const mapStateToProps = state => {
return {
activeTool : state.activeTool,
comments : state.comments
}
};
const CanvasStage = connect(mapStateToProps, mapDispatchToProps)(ConnectedCanvasStage);
export default CanvasStage;
How would I go about smoothing this drag out so it doesn't just draw a new image on top and it also drags from the click point and not the top left of the image?
I'm using react and HTML5 canvas to draw rectangles over faces on an image, but react life-cycle is limiting me from doing this.
As I should use refs to reference to the canvas and this have to be done in componentDidMount() and I could not get props or states inside this function.
Here is the code:
import React, { Component } from 'react'
import { render } from 'react-dom'
import {Row} from 'react-bootstrap';
import '../App.css';
class TagImg extends Component {
constructor(props){
super(props);
this.state={
rects:[[110, 30, 70, 70], [40, 30, 70, 70]],
ctx : ''
};
this.tagPerson = this.tagPerson.bind(this);
}
tagPerson(e){
var x = e.offsetX,
y = e.offsetY;
for(var i=0;i<this.props.rects.length;i++) { // check whether:
if(x > this.props.rects[i][0] // mouse x between x and x + width
&& x < this.props.rects[i][0] + this.props.rects[i][2]
&& y > this.props.rects[i][1] // mouse y between y and y + height
&& y < this.props.rects[i][1] + this.props.rects[i][3]) {
alert('Rectangle ' + i + ' clicked');
}
}
}
componentDidMount() {
console.log("componentDidMount");
const ctx = this.refs.canvas.getContext('2d');
var myImage = new Image();
myImage.onload = function() {
var width = myImage.naturalWidth; // this will be 300
var height = myImage.naturalHeight; // this will be 400
ctx.drawImage(myImage, 0, 0, 300, 200);
ctx.beginPath();
ctx.strokeStyle="white";
// for(var i=0;i<this.state.rects.length;i++) {
// // ctx.rect(this.state.rects[i][0], // fill at (x, y) with (width, height)
// // this.state.rects[i][1],
// // this.state.rects[i][2],
// // this.state.rects[i][3]);
// console.log("helloo");
// }
console.log(this.state.rects);
ctx.stroke();
}
myImage.src = this.props.path;
}
render() {
return (
<div>
<form id="afterUpload" action="" method="post" encType="multipart/form-data">
<Row id="image_preview" className="row">
<canvas ref="canvas" onClick={this.tagPerson}/>
</Row>
</form>
</div>
);
}
}
export default TagImg;
After searching I figured out that I could use componentWillRecieveProps() but did not understand how to use it in my case.
componentDidMount will be called only once from React, after the component is mounted.
Since you want to draw something every time a user clicks on the canvas, you have to do that in a place that it's called at every rendering, like inside the render function itself.
Something like this:
import React, { Component } from 'react'
import { render } from 'react-dom'
import {Row} from 'react-bootstrap';
class TagImg extends Component {
constructor(props){
super(props);
this.state={
rects:[[110, 30, 70, 70], [40, 30, 70, 70]]
};
this.tagPerson = this.tagPerson.bind(this);
}
tagPerson(e){
var x = e.offsetX,
y = e.offsetY;
for(var i=0;i<this.props.rects.length;i++) { // check whether:
if(x > this.props.rects[i][0] // mouse x between x and x + width
&& x < this.props.rects[i][0] + this.props.rects[i][2]
&& y > this.props.rects[i][1] // mouse y between y and y + height
&& y < this.props.rects[i][1] + this.props.rects[i][3]) {
alert('Rectangle ' + i + ' clicked');
}
}
}
drawRects() {
const ctx = this.refs.canvas.getContext('2d');
ctx.drawImage(myImage, 0, 0, 300, 200);
ctx.beginPath();
ctx.strokeStyle="white";
// for(var i=0;i<this.state.rects.length;i++) {
// // ctx.rect(this.state.rects[i][0], // fill at (x, y) with (width, height)
// // this.state.rects[i][1],
// // this.state.rects[i][2],
// // this.state.rects[i][3]);
// console.log("helloo");
// }
console.log(this.state.rects);
ctx.stroke();
}
componentDidMount() {
console.log("componentDidMount");
var myImage = new Image();
myImage.onload = this.drawRects.bind(this);
myImage.src = this.props.path;
}
render() {
drawRects();
return (
<div>
<form id="afterUpload" action="" method="post" encType="multipart/form-data">
<Row id="image_preview" className="row">
<canvas ref="canvas" onClick={this.tagPerson}/>
</Row>
</form>
</div>
);
}
}
export default TagImg;
In the code I'm downloading the image once, in the componentDidMount method. And I'm running the drawRects() method at every rendering, so every time you call a setState you'll have your new rects
I have a relatively simply component which looks as follows (simplified)
class MouseListener extends Component {
static propTypes = {
children: PropTypes.element,
onMouseMove: PropTypes.func.isRequired
};
container = null;
onMouseMove = debounce(e => {
e.persist();
const { clientX, clientY } = e;
const { top, left, height, width } = this.container.getBoundingClientRect();
const x = (clientX - left) / width;
const y = (clientY - top) / height;
this.props.onMouseMove(x, y);
}, 50);
render() {
return (<div
ref={e => this.container = e}
onMouseMove={this.onMouseMove}
>
{this.props.children}
</div>);
}
}
However, whenever the onMouseMove is triggered, it says that the event has already passed. I expected to be able to use e.persist() to keep it in the pool for this event handler to run asynchronously.
What is the appropriate way to bind a debounced event handler in React?
Struggling a bit here. If I'm just filling or doing anything else to the canvas - no issue. I get the div without the external image. Tried local image file as well as URL... Thanks!
import React, { Component, PropTypes } from 'react';
export default class CanvasCreator extends Component {
componentDidMount() {
this.updateCanvas();
}
updateCanvas() {
const ctx = this.refs.canvas.getContext('2d');
var imageObj1 = new Image();
imageObj1.src = 'https://s-media-cache-ak0.pinimg.com/236x/d7/b3/cf/d7b3cfe04c2dc44400547ea6ef94ba35.jpg'
ctx.drawImage(imageObj1,0,0);
}
render() {
return (
<canvas ref="canvas" width={300} height={300}> </canvas>
);
}
};
You are missing the onload method. This will work for you:
updateCanvas() {
const ctx = this.refs.canvas.getContext('2d');
var imageObj1 = new Image();
imageObj1.src = 'https://s-media-cache-ak0.pinimg.com/236x/d7/b3/cf/d7b3cfe04c2dc44400547ea6ef94ba35.jpg'
imageObj1.onload = function() {
ctx.drawImage(imageObj1,0,0);
}
}
For me in order to work was also necessary to add the width and height!
Example:
imageObj1.onload = function() {
ctx.drawImage(imageObj1, 0, 0, 500, 500)
}