I try to build mini paint in browser with using typescript + styled-components + react, however Canvas calculates position of mouse wrongly when I try to draw something. Thing is that it happens only when I use canvas which is located inside CanvasContainer, If I just write tag <canvas ... /> inside ImageEditor then position is counted correctly. Does anyone has any idea why does it happen and how to fix it? As I don't really understand this behaviour.
export default function ImageEditorContainer() {
const CANVAS_REF = useRef<any>(null);
const isDrawing = useRef(false);
function startDrawing(event: MouseEvent) {
isDrawing.current = true;
const canvas = CANVAS_REF.current;
const context = canvas.getContext('2d');
console.log('STARt');
context.beginPath();
context.moveTo(event.clientX - canvas.offsetLeft, event.clientY - canvas.offsetTop);
event.preventDefault();
}
function draw(event: MouseEvent) {
if (isDrawing.current) {
const canvas = CANVAS_REF.current;
const context = canvas.getContext('2d');
console.log('Draw');
context.lineTo(event.clientX - canvas.offsetLeft, event.clientY - canvas.offsetTop);
context.strokeStyle = 'black';
context.lineWidth = '3px';
context.lineCap = 'round';
context.lineJoin = 'round';
context.stroke();
}
event.preventDefault();
}
function stopDrawing(event: MouseEvent) {
if (isDrawing.current) {
const canvas = CANVAS_REF.current;
const context = canvas.getContext('2d');
console.log('STOP');
context.stroke();
context.closePath();
isDrawing.current = false;
}
event.preventDefault();
}
useEffect(() => {
const canvas = CANVAS_REF.current;
if (canvas) {
canvas.addEventListener('mousedown', startDrawing);
canvas.addEventListener('mousemove', draw);
canvas.addEventListener('mouseup', stopDrawing);
canvas.addEventListener('mouseout', stopDrawing);
}
}, []);
return (
<>
....
<CanvasContainer CANVAS_REF={CANVAS_REF} ... />
</>
);
}
export default function CanvasContainer({ ...restProps }: TCanvasContent) {
return (
<Canvas.Container>
<Canvas.Content {...restProps} />
</Canvas.Container>
);
}
export default function Canvas({ children }: TCanvas) {
return { children };
}
Canvas.Content = function CanvasContent({ CANVAS_REF, ...restProps }: TCanvasContent) {
return <Content ref={CANVAS_REF} {...restProps} />;
};
https://codesandbox.io/s/dank-dust-vo4lh?file=/src/CanvasContainer.js
Got it worked.
https://codesandbox.io/s/peaceful-jepsen-wh913?file=/src/App.js
The "Style" of the canvas was wrong.
Related
I have this easy animation but I have issue. Everytime I update something on page (for example r of arc) animation become faster and faster after each update or after I saved changes in code.
Here is my code:
import React, { useEffect, useRef } from "react";
const CanvasPage = () => {
const canvasRef = useRef(null);
const x = useRef(0);
const r = 50
const v = useRef(1);
useEffect(() => {
const canvas = canvasRef.current;
const context = canvas.getContext("2d");
//animation function
const render = () => {
window.requestAnimationFrame(render);
//bouncing left and right
x.current = x.current + v.current;
if(x.current > canvas.width){
v.current = -1;
}
if(x.current <= 0){
v.current = 1;
}
context.clearRect(0, 0, canvas.width, canvas.height);
context.beginPath();
context.arc(x.current, 75, r, 0, 2 * Math.PI);
context.stroke();
};
render();
}, []);
return (
<div className="Canvas">
<h1>Canvas page</h1>
<canvas id="canvas" ref={canvasRef} height="500px" width="500px" />
</div>
)
}
export default CanvasPage;
I have tried all topics regarding with this issue sutch a cancelAnimationFrame() but it did not work.
Can some one help me please?
Thank you very much.
It is most likely because the render function is being triggered on each load. And since requestAnimationFrame call is not being cancelled in your code, previous animation function will keep being triggered.
Below fix may solve your issue:
useEffect(() => {
const canvas = canvasRef.current;
const context = canvas.getContext("2d");
const timerIdHolder = {timerId: null};
//animation function
const render = () => {
// let's store the last timerId in our ref object
timerIdHolder.timerId = window.requestAnimationFrame(render);
//bouncing left and right
x.current = x.current + v.current;
if(x.current > canvas.width){
v.current = -1;
}
if(x.current <= 0){
v.current = 1;
}
context.clearRect(0, 0, canvas.width, canvas.height);
context.beginPath();
context.arc(x.current, 75, r, 0, 2 * Math.PI);
context.stroke();
};
render();
// let's return a unmount callback
return () => cancelAnimationFrame(timerIdHolder.timerId);
}, []);
I have a canvas that is adding dynamically to the page on on load.
I want to draw user's mouse path on the canvas, but I found that if I clear the canvas before drawing, it will draw smooth lines, otherwise, it will draw ugly lines like following screenshot!
To test the problem, please uncomment first line of draw_on_canvas function in the code to see the difference.
$(document).ready(function() {
//Create DRAWING environment
var canvasWidth = 400;
var canvasHeight = 200;
var drawn_shape_list = [];
var current_shape_info = {};
var is_painting = false;
function add_path_to_drawn_shape_list() {
if (current_shape_info.path && current_shape_info.path.length > 0) {
drawn_shape_list.push(current_shape_info);
}
current_shape_info = {};
}
function add_path(x, y) {
current_shape_info.color = "#000000";
current_shape_info.size = 2;
if (!current_shape_info.path) {
current_shape_info.path = [];
}
current_shape_info.path.push({
"x": x,
"y": y
});
}
function draw_on_canvas() {
//Uncomment following line to have smooth drawing!
//context.clearRect(0, 0, context.canvas.width, context.canvas.height); //clear canvas
context.strokeStyle = current_shape_info.color;
context.lineWidth = current_shape_info.size;
context.beginPath();
context.moveTo(current_shape_info.path[0].x, current_shape_info.path[0].y);
for (var i = 1; i < current_shape_info.path.length; i++) {
context.lineTo(current_shape_info.path[i].x, current_shape_info.path[i].y);
}
context.stroke();
}
//Create canvas node
var canvas_holder = document.getElementById('canvas_holder');
canvas = document.createElement('canvas');
canvas.setAttribute('width', canvasWidth);
canvas.setAttribute('height', canvasHeight);
canvas.setAttribute('id', 'whitboard_canvas');
canvas_holder.appendChild(canvas);
if (typeof G_vmlCanvasManager != 'undefined') {
canvas = G_vmlCanvasManager.initElement(canvas);
}
context = canvas.getContext("2d");
$('#canvas_holder').mousedown(function(e) {
var mouseX = e.pageX - this.offsetLeft;
var mouseY = e.pageY - this.offsetTop;
is_painting = true;
add_path(mouseX, mouseY, false);
draw_on_canvas();
});
$('#canvas_holder').mousemove(function(e) {
if (is_painting) {
var mouseX = e.pageX - this.offsetLeft;
var mouseY = e.pageY - this.offsetTop;
var can = $('#whitboard_canvas');
add_path(mouseX, mouseY, true);
draw_on_canvas();
}
});
$('#canvas_holder').mouseup(function(e) {
is_painting = false;
add_path_to_drawn_shape_list();
});
$('#canvas_holder').mouseleave(function(e) {
is_painting = false;
add_path_to_drawn_shape_list();
});
});
#canvas_holder {
border: solid 1px #eee;
}
canvas {
border: solid 1px #ccc;
}
<HTML>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="canvas_holder"></div>
</body>
</HTML>
You can see an example of my code here with two canvas.
I have tried context.lineJoin = "round"; and context.lineCap = 'round';, but the result didn't change.
Is it normal canvas behavior or I should set something?
how to draw smooth lines on canvas without clearing it?
You don't. Clearing and redrawing is the way to go.
Is it normal canvas behavior
Yes completely. Unless you perform some action that should clear the canvas, it won't be cleared. So when you draw multiple times over the same area using semi transparent color, the pixels will become darker and darker.
Don't be afraid of performances, having to deal with the compositing of previous drawings may even be slower than drawing a single more complex path.
One thing you can do to improve performances is to use a single Path, so that at every frame only a single paint operation occurs:
const canvas = document.getElementById( 'canvas' );
const ctx = canvas.getContext( '2d' );
const path = new Path2D();
const mouse = {};
function draw() {
// clear all
ctx.clearRect( 0, 0, canvas.width, canvas.height );
// draw the single path
ctx.stroke( path );
// tell we need to redraw next frame
mouse.dirty = false;
}
canvas.onmousedown = (evt) => {
mouse.down = true;
// always use the same path
path.moveTo( evt.offsetX, evt.offsetY );
};
document.onmouseup = (evt) => {
mouse.down = false;
};
document.onmousemove = (evt) => {
if( mouse.down ) {
const rect = canvas.getBoundingClientRect();
path.lineTo( evt.clientX - rect.left, evt.clientY - rect.top );
}
if( !mouse.dirty ) {
mouse.dirty = true;
requestAnimationFrame(draw);
}
};
canvas { border: 1px solid }
<canvas id="canvas" width="500" height="500"></canvas>
If you need to have different path styles, then you can create one path per style.
const canvas = document.getElementById( 'canvas' );
const ctx = canvas.getContext( '2d' );
const makePath = (color) => ({
color,
path2d: new Path2D()
});
const pathes = [makePath('black')];
const mouse = {};
function draw() {
// clear all
ctx.clearRect( 0, 0, canvas.width, canvas.height );
pathes.forEach( (path) => {
// draw the single path
ctx.strokeStyle = path.color;
ctx.stroke( path.path2d );
} );
// tell we need to redraw next frame
mouse.dirty = false;
}
document.getElementById('inp').onchange = (evt) =>
pathes.push( makePath( evt.target.value ) );
canvas.onmousedown = (evt) => {
mouse.down = true;
const path = pathes[ pathes.length - 1 ].path2d;
// always use the same path
path.moveTo( evt.offsetX, evt.offsetY );
};
document.onmouseup = (evt) => {
mouse.down = false;
};
document.onmousemove = (evt) => {
if( mouse.down ) {
const rect = canvas.getBoundingClientRect();
const path = pathes[ pathes.length - 1 ].path2d;
path.lineTo( evt.clientX - rect.left, evt.clientY - rect.top );
}
if( !mouse.dirty ) {
mouse.dirty = true;
requestAnimationFrame(draw);
}
};
canvas { border: 1px solid }
<input type="color" id="inp"><br>
<canvas id="canvas" width="500" height="500"></canvas>
Im making a painting tool, and one of the feature is showing cropped image of the drawn path.
The path I have drawn(image)
For example in above the picture, the white colored path indicates what I have drawn, just like a painting tool.
Cropped image
And here is the cropped image of the path. If you look at the picture, you can see that it crops the image as if the path is closed and therefore it crops the image "area" not the path.
and here is the code
function crop({ image, points }) {
return Observable.create(observer => {
const { width, height } = getImageSize(image);
const canvas = document.createElement('canvas') as HTMLCanvasElement;
const context = canvas.getContext('2d');
canvas.width = width;
canvas.height = height;
context.beginPath();
points.forEach(([x, y], idx) => {
if (idx === 0) {
context.moveTo(x, y);
} else {
context.lineTo(x, y);
}
});
context.clip();
context.drawImage(image);
...etc
}
The crop function receives points which is consisted [x coordinate, y coordinate][ ] of the drawn path.
Is there an way to show image only the path that I've painted?
That's more what is generally called a mask then, but note that both for the current clip or for the mask you want to attain, the best is to use compositing.
Canvas context has various compositing options, allowing you to generate complex compositions, from pixels's alpha value.
const ctx = canvas.getContext('2d');
const pathes = [[]];
let down = false;
let dirty = false;
const bg = new Image();
bg.onload = begin;
bg.src = 'https://upload.wikimedia.org/wikipedia/commons/thumb/e/ec/Serene_Sunset_%2826908986301%29.jpg/320px-Serene_Sunset_%2826908986301%29.jpg';
function begin() {
canvas.width = this.width;
canvas.height = this.height;
ctx.lineWidth = 10;
addEventListener('mousemove', onmousemove);
addEventListener('mousedown', onmousedown);
addEventListener('mouseup', onmouseup);
anim();
ctx.fillText("Use your mouse to draw a path", 20,50)
}
function anim() {
requestAnimationFrame(anim);
if(dirty) draw();
dirty = false;
}
function draw() {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.drawImage(bg, 0, 0);
ctx.beginPath();
pathes.forEach(path => {
if(!path.length) return;
ctx.moveTo(path[0].x, path[0].y);
path.forEach(pt => {
ctx.lineTo(pt.x, pt.y);
});
});
// old drawings will remain on where new drawings will be
ctx.globalCompositeOperation = 'destination-in';
ctx.stroke();
// reset
ctx.globalCompositeOperation = 'source-over';
}
function onmousemove(evt) {
if(!down) return;
const rect = canvas.getBoundingClientRect();
pathes[pathes.length - 1].push({
x: evt.clientX - rect.left,
y: evt.clientY - rect.top
});
dirty = true;
}
function onmousedown(evt) {
down = true;
}
function onmouseup(evt) {
down = false;
pathes.push([]);
}
canvas {border: 1px solid}
<canvas id="canvas"></canvas>
Don't hesitate to look at all the compositing options, various cases will require different options, for instance if you need to draw multiple paths, you may prefer to render first your paths and then keep your image only where you did already drawn, using the source-atop option:
const ctx = canvas.getContext('2d');
const pathes = [[]];
pathes[0].lineWidth = (Math.random() * 20) + 0.2;
let down = false;
let dirty = false;
const bg = new Image();
bg.onload = begin;
bg.src = 'https://upload.wikimedia.org/wikipedia/commons/thumb/e/ec/Serene_Sunset_%2826908986301%29.jpg/320px-Serene_Sunset_%2826908986301%29.jpg';
function begin() {
canvas.width = this.width;
canvas.height = this.height;
addEventListener('mousemove', onmousemove);
addEventListener('mousedown', onmousedown);
addEventListener('mouseup', onmouseup);
anim();
ctx.fillText("Use your mouse to draw a path", 20,50)
}
function anim() {
requestAnimationFrame(anim);
if(dirty) draw();
dirty = false;
}
function draw() {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
pathes.forEach(path => {
if(!path.length) return;
ctx.beginPath();
ctx.lineWidth = path.lineWidth;
ctx.moveTo(path[0].x, path[0].y);
path.forEach(pt => {
ctx.lineTo(pt.x, pt.y);
});
ctx.stroke();
});
// new drawings will appear on where old drawings were
ctx.globalCompositeOperation = 'source-atop';
ctx.drawImage(bg, 0, 0);
// reset
ctx.globalCompositeOperation = 'source-over';
}
function onmousemove(evt) {
if(!down) return;
const rect = canvas.getBoundingClientRect();
pathes[pathes.length - 1].push({
x: evt.clientX - rect.left,
y: evt.clientY - rect.top
});
dirty = true;
}
function onmousedown(evt) {
down = true;
}
function onmouseup(evt) {
down = false;
const path = [];
path.lineWidth = (Math.random() * 18) + 2;
pathes.push(path);
}
canvas {border: 1px solid}
<canvas id="canvas"></canvas>
And also remember that you can very well have canvases that you won't append to the document that you can use as layers to generate really complex compositions. (drawImage() does accept a <canvas> as source).
I am trying to make an image editable on in the browser. I found this, and I tried to re-purpose this code for my use. This is what I ended up with:
export default class CanvasComponent extends React.Component {
constructor(props) {
super(props);
this.lastX = 0;
this.lastY = 0;
this.ctx = 0;
}
componentWillReceiveProps(nextProps) {
this.ctx = document.getElementById('canvas').getContext('2d');
const img = new Image();
img.onload = () => {
this.ctx.drawImage(img, 0, 0, nextProps.width, nextProps.height);
};
img.src = URL.createObjectURL(nextProps.image)
}
handleClick = () => {
const canvas = document.getElementById('canvas');
canvas.addEventListener('click', (e) => {
this.Draw(e.pageX - canvas.offsetLeft, e.pageY - canvas.offsetTop);
})
};
Draw = (x, y) => {
console.log('drawing');
this.ctx.beginPath();
this.ctx.strokeStyle = 'red';
this.ctx.lineWidth = 5;
this.ctx.lineJoin = "round";
this.ctx.moveTo(this.lastX, this.lastY);
this.ctx.lineTo(x, y);
this.ctx.closePath();
this.ctx.stroke();
this.lastX = x;
this.lastY = y;
};
render() {
return (
<div>
<canvas onClick={this.handleClick} width={this.props.width} height={this.props.height} id={'canvas'}/>
</div>
);
}
}
I really don't know canvas (first time using it) so I am probably doing something really stupid (plz don't yell).
Right now the code acts like this: https://youtu.be/4rvGigRvJ8E
I was unable to make it into a gif. Sorry
What I want to be happening:
When the user clicks on the image, in the place of clicking I want a dot to appear. How can I achieve this?
New Draw method
Draw = (x, y) => {
console.log('drawing');
this.ctx.beginPath();
this.ctx.strokeStyle = 'red';
this.ctx.lineWidth = 5;
// x, y are the cords, 5 is the radius of the circle and the last 2 things specify full circle.
this.ctx.arc(x, y, 5, 0, 2 * Math.PI);
this.ctx.stroke();
this.lastX = x;
this.lastY = y;
};
Your draw code seems to be trying to draw a line instead of a circle…
To draw a circle take a look at this example.
this.ctx.beginPath();
this.ctx.strokeStyle = 'red';
this.ctx.lineWidth = 5;
// x, y are the cords, 5 is the radius of the circle and the last 2 things specify full circle.
this.ctx.arc(x, y, 5, 0, 2 * Math.PI);
this.ctx.stroke();
Documented https://developer.mozilla.org/kab/docs/Web/API/Canvas_API/Tutorial/Drawing_shapes#Arcs
Also you seem to be trying to attach a lister instead of acting on the listening, the function you pass to onClick will be called every click, no need to attach a click listener in that as well.
I want to use canvas in react and typescript, there is my code:
it's render html:
public render() {
return (
<div className="App">
<input type="radio" id={'repeatRadio'}/>sdf
<canvas id={'canvas'} width={600} height={300}>
canvas not supported
</canvas>
</div>
);
}
It's componentDidMount function:
public componentDidMount() {
this.initCanvas();
}
private initCanvas = () => {
const canvas = document.getElementById('canvas') as HTMLCanvasElement;
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
const repeatRadio = document.getElementById('repeatRadio') as HTMLElement;
const image = new Image();
image.src = 'test.jpg';
repeatRadio.onclick = () => {
this.fill(image,'repeat');
};
ctx.lineJoin = 'round';
image.onload = () => {
this.fill(image, 'repeat');
}
};
private fill = (image: any, str: string) => {
const canvas = document.getElementById('canvas') as HTMLCanvasElement;
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
debugger;
const pattern = ctx.createPattern(image,str);
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.fillRect(0,0,canvas.width,canvas.height);
ctx.fillStyle = pattern;
ctx.fill();
};
It's can run before I click input radio, but It's appear err like title when i click.
I add onerror code:
image.onerror = (message) => {
console.error(message);
}
I get info as fallow:
But I can't find something useful.
Image will have to be imported in react using import clause.
import TEXTURE_IMG1 from '../textures/honey_im_subtle.png'
Once imported you can assign it 'src' property.
var img = new Image();
img.src = TEXTURE_IMG1;
var texturePatternBrush = new fabric.PatternBrush(this.canvas);