Clearing a specific shape in canvas with pixel accurcy - javascript

Trying to add a shape to canvas and then clearing it without using clearRect, and here is a demonstration
const canvas = document.createElement('canvas')
canvas.height = 300
canvas.width = 300
const ctx = canvas.getContext('2d')
document.body.appendChild(canvas)
const make = () => {
ctx.beginPath();
ctx.moveTo(50, 50);
ctx.quadraticCurveTo(100, 75, 100, 100);
ctx.arc(75, 100, 10, 0, 2 * Math.PI)
ctx.lineWidth = 1
ctx.stroke()
}
const draw = () => {
make()
}
const clear = () => {
ctx.save()
ctx.globalCompositeOperation = "destination-out";
make()
ctx.restore()
}
draw()
clear()
as you can notice the shape doesn't get cleared as exoected it just gets lighter and some pixels are not removed,
the problem is shapes are relatively complex and I can't use clearRect , and the only solution I've found is increasing the linewidth when clearing, but its more of a hack, any idea how to resolve this neatly, is it because of antialiasing?

Related

How do I add canvas drawings to an array and determine if an array value is an image or canvas drawing?

I need an array which can include canvas drawings and/or images.
Drawing example below:
function drawCircle() {
ctx.beginPath();
ctx.arc(Life.x, Life.y, Life.radius, 0, Math.PI * 2);
ctx.fillStyle = 'black;
ctx.fill();
ctx.strokeStyle = 'orange';
ctx.stroke();
ctx.closePath();
}
Image example below:
const LIFE_IMG=new Image();
LIFE_IMG.src="images/life.png"
Array;
let LIFE_ARRAY=[LIFE_IMG,LIFE_IMG,LIFE_IMG];
Draw:
function drawLife(imgY2,imgWidth,imgHeight){
LIFE_ARRAY.forEach(function(item, index) {
let offsetY,imgX,imgY;
offsetY=(imgHeight+5)*(index+1)
imgX=(SCOREBOARD_WIDTH/2)-(imgWidth/2)
imgY=imgY2
}
ctx.drawImage(item, imgX, imgY+offsetY, imgWidth, imgHeight);
})
}
I need to be able to
push the canvas drawing to the array
determine if the array value is an image or canvas drawing
draw the canvas drawing or the image
Part 2 was answered on an old stack overflow thread with:
function isImage(i) {return i instanceof HTMLImageElement;}
but I can't figure out how to tie it into my code.
Any help would be greatly appreciated!
Usually you just have one on-screen <canvas> element. So your sample code:
let canvas = document.getElementById('canvas');
let ctx = canvas.getContext('2d');
let Life = {
x: 100,
y: 100,
radius: 50
};
function drawCircle() {
ctx.beginPath();
ctx.arc(Life.x, Life.y, Life.radius, 0, Math.PI * 2);
ctx.fillStyle = 'black';
ctx.fill();
ctx.strokeStyle = 'orange';
ctx.stroke();
ctx.closePath();
}
drawCircle();
<canvas id="canvas"></canvas>
draws a circle on the on-screen canvas' context. This is just a drawing operation though, so there is no reference to it making it sort of an object. In the end it's just pixel data somewhere on the canvas.
Although I don't see an use-case, what you want to achieve can be done of course. The trick here is to create individual off-screen canvases for all canvas drawings you want to store in the array.
A <canvas> element can be created at runtime using:
document.createElement('canvas')
If we modify your drawCircle() function a little so it draws to an off-screen canvas and returns it, you can even call this function from within your array to push it's reference into.
function drawCircle() {
let tempCanvas=document.createElement('canvas');
let tempCtx=tempCanvas.getContext('2d');
tempCtx.beginPath();
tempCtx.arc(Life.x, Life.y, Life.radius, 0, Math.PI * 2);
tempCtx.fillStyle = 'black';
tempCtx.fill();
tempCtx.strokeStyle = 'orange';
tempCtx.stroke();
tempCtx.closePath();
return tempCanvas;
}
As you already figured yourself an image is an instance of HTMLImageElement. Likewise a canvas is of HTMLCanvasElement. This can be used in the forEach loop to distinct one from the other.
Here's an example:
let canvas = document.getElementById('canvas');
let ctx = canvas.getContext('2d');
let Life = {
x: 100,
y: 100,
radius: 50
};
function drawCircle() {
let tempCanvas = document.createElement('canvas');
let tempCtx = tempCanvas.getContext('2d');
tempCtx.beginPath();
tempCtx.arc(Life.x, Life.y, Life.radius, 0, Math.PI * 2);
tempCtx.fillStyle = 'black';
tempCtx.fill();
tempCtx.strokeStyle = 'orange';
tempCtx.stroke();
tempCtx.closePath();
return tempCanvas;
}
drawCircle();
const LIFE_IMG = new Image();
LIFE_IMG.src = "images/life.png";
let LIFE_ARRAY = [LIFE_IMG, drawCircle(), LIFE_IMG];
LIFE_ARRAY.forEach(function(item, index) {
if (item instanceof HTMLImageElement) {
console.log("an Image");
}
if (item instanceof HTMLCanvasElement) {
console.log("a Canvas");
}
});
<canvas id="canvas"></canvas>

Im having issues moving a rectangle on a canvas

I created a script to move a rectangle but as it moves, it doesn't remove the previous ones from the canvas.
Here is my javascript.
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
const box =25;
let snake=[];
snake[0]={x:5*box,y:4*box}
function draw(){
ctx.rect(snake[0].x, snake[0].y, box, box);
ctx.fillStroke="black";
ctx.strokeRect(snake[0].x, snake[0].y, box, box);
snake.pop();
snake.unshift({x:6*box,y:4*box});
}
let game=setInterval(draw,100)
<canvas></canvas>
You can issue ctx.clearRect on canvas object on start of each draw function call and that will do it for you
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const box = 25;
let snake = [];
snake[0] = {
x: 5 * box,
y: 4 * box
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.rect(snake[0].x, snake[0].y, box, box);
ctx.fillStroke = "black";
ctx.strokeRect(snake[0].x, snake[0].y, box, box);
snake.pop();
snake.unshift({
x: 6 * box,
y: 4 * box
});
}
let game = setInterval(draw, 350)
<canvas id=canvas></canvas>
Heres the correct way to create animations with canvas:
So you must create an animation frame the canvas can use to redraw the rectangle every time its moved.
Your issue is that youre drawing the same canvas over itself, so use ctx.clearRect(0, 0, cvs.width, cvs.height) to clear the entire canvas.
Also.... Please NEVER use intervals with canvas drawing. Only use animation frames like in my function. Just place it at the end of your main draw function, and itll repeat it forever.
Scroll down and hit "Run code snippet" to see my example run in the browser. As you notice, the rectangle will move smoothly and it leaves no trail behind.
const cvs = document.querySelector(".canvas"),
ctx = cvs.getContext("2d");
let rectX = -30;
function init() {
ctx.width = 300;
ctx.height = 300;
ctx.clearRect(0, 0, cvs.width, cvs.height);
ctx.fillRect(rectX - 25, cvs.height / 2 - 25, 50, 50);
rectX++;
if (rectX > 325) {
rectX = -25;
}
window.requestAnimationFrame(init);
}
init();
<canvas class="canvas" style="border:1px solid black"></canvas>

Filling in two colors while using the rect() method

I was trying to make two different shapes that are different colors but it isn't working. Both of the shapes are the same colors. Please help!(Please note that I am not the best coder in the world)
I've looked for other examples on this website, but all of them use the lineTo() method and I would like to use the rect() method just to make things easier.
//make canvas and set it up
var canvas = document.createElement('canvas');
document.body.appendChild(canvas);
var ctx = canvas.getContext('2d');
canvas.height = window.innerHeight;
canvas.width = window.innerWidth;
canvas.style.position = 'absolute';
canvas.style.left = '0px';
canvas.style.top = '0px';
canvas.style.backgroundColor = '#D0C6C6';
var cH = canvas.height;
var cW = canvas.width;
//draw paddles
//variables
var paddleLength = 120;
var redPaddleY = window.innerHeight / 2;
var bluePaddleY = window.innerHeight / 2;
var paddleWidth = 20;
//drawing starts
function drawPaddles() {
//RED PADDLE
var redPaddle = function(color) {
ctx.fillStyle = color;
ctx.clearRect(0, 0, cW, cH);
ctx.rect(cH / 12, redPaddleY - paddleLength / 2, paddleWidth, paddleLength);
ctx.fill();
};
//BLUE PADDLE
var bluePaddle = function(color) {
ctx.fillStyle = color;
ctx.clearRect(0, 0, cW, cH);
ctx.rect(cH / 12 * 14, bluePaddleY - paddleLength / 2, paddleWidth, paddleLength);
ctx.fill();
};
redPaddle('red');
bluePaddle('blue');
};
var interval = setInterval(drawPaddles, 25);
Whenever you add a shape to the canvas it becomes part of the current path. The current path remains open until you tell the canvas to start a new one with beginPath(). This means that when you add your second rect() it is combined with the first and filled with the same colour.
The simplest fix would be to use the fillRect() function instead of rect which begins, closes and fills a path in one call.
var redPaddle = function(color) {
ctx.fillStyle = color;
ctx.fillRect(cH / 12, redPaddleY - paddleLength / 2, paddleWidth, paddleLength);
};
If you still want to use rect() you should tell the canvas to begin a new path for each paddle.
var redPaddle = function(color) {
ctx.fillStyle = color;
ctx.beginPath();
ctx.rect(cH / 12, redPaddleY - paddleLength / 2, paddleWidth, paddleLength);
ctx.fill();
};
I would also suggest moving the clearRect() outside of the drawing functions too. Clear once per frame and draw both paddles.
...
ctx.clearRect(0, 0, cW, cH);
redPaddle();
bluePaddle();
...
You should also investigate requestAnimationFrame() to do your animation loop as it provides many performance improvements over intervals.

Remove context Arc from canvas

I am working on a project where user upload structural diagram(engineering diagram). When I user double click on the intended location on canvas the speech to text engine turns on and listen for user comments and then it draw a small circle with different colors and fill text (count) on that location. I am saving comments, counts, coordinates of arc and other things in react state and displaying the list in a component with edit and delete button. When user press the delete button. comment and other property gets deleted from the state.
I want to remove the drawn arc from the canvas. How can I do it?
I have tried clearRect. But it is not working in this case.
Please let me know.
componentDidMount() {
const img = this.refs.image;
const canvas = this.refs.canvas;
const ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
img.onload = () => {
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
ctx.font = "40px Courier";
ctx.fillText('Drawing 1', 200, 100);
}
}
drawCircle(x, y, getcolor) {
const canvas = this.refs.canvas;
const ctx = canvas.getContext("2d");
ctx.beginPath();
ctx.arc(x, y, 8, 0, Math.PI * 2, false);
ctx.strokeStyle = "black";
ctx.stroke();
ctx.fillStyle = getcolor;
ctx.fill();
ctx.closePath();
// Text
ctx.beginPath();
ctx.font = "20px Arial"
ctx.fillStyle = "#00538a";
ctx.textAlign = "center";
ctx.fillText(this.state.count, x , y - 20);
ctx.fill();
ctx.closePath();
}
remove(id) {
this.setState({
comments: this.state.comments.filter(c => c.id !== id)
});
const canvas = this.refs.canvas;
const ctx = canvas.getContext('2d');
const arc = this.state.comments.filter(c => c.id === id);
let x = arc[0].coordinates.x;
let y = arc[0].coordinates.y
console.log("TCL: Drawing -> remove -> arc", arc[0].coordinates);
ctx.beginPath();
ctx.arc(x, y, 8, 0, Math.PI * 2, false);
ctx.clip();
ctx.clearRect(x-8, y-8, 16,16);
}
Thanks
Meet
As I mentioned in my comments the way you're trying to remove a circle from the canvas ain't gonna work.
If you call clearRect() on the canvas, it will essentially overwrite the target area including your original background image.
Instead you need to keep track of the circles - more precisely the position at which those should be drawn - using an array.
If you click the canvas -> add a circle element to an array -> clear the canvas -> draw the diagram again -> loop over the array to draw the circles on top
If you click the remove button of a circle -> search the array for this particular circle -> remove it from the array -> clear the canvas -> draw the diagram again -> loop over the array to draw the circles on top
Here's an example to illustrate what I'm talking about:
var comments = new Array();
var canvas = document.createElement("canvas");
canvas.style="float:left;"
canvas.width = 400;
canvas.height = 200;
document.body.appendChild(canvas);
var ctx = canvas.getContext("2d");
function updateCanvas() {
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
ctx.font = "40px Courier";
ctx.fillText('Drawing 1', 200, 100);
for (var a = 0; a < comments.length; a++) {
ctx.beginPath();
ctx.arc(comments[a].x, comments[a].y, 8, 0, Math.PI * 2, false);
ctx.strokeStyle = "black";
ctx.stroke();
ctx.fillStyle = "black";
ctx.fill();
ctx.closePath();
}
}
var img = new Image();
img.onload = () => {
updateCanvas();
}
img.src = "https://picsum.photos/id/59/400/200";
function addCircle(e) {
var div = document.createElement("div");
div.innerHTML = "remove" + comments.length;
document.body.appendChild(div);
div.addEventListener("click", function(e) {
for (var a = 0; a < comments.length; a++) {
if (comments[a].div == e.target) {
comments.splice(a, 1);
break;
}
}
document.body.removeChild(e.target);
updateCanvas();
});
comments.push({
x: e.clientX,
y: e.clientY,
div: div
});
updateCanvas();
}
canvas.addEventListener("click", addCircle);
Everytime you click on the picture a 'remove' div will be created to the right of the canvas. if you click it, the associated circle will be removed.

Rotate canvas 90 degrees clockwise and update width height

Say we have a canvas:
<canvas id="one" width="100" height="200"></canvas>
And on a button click the canvas gets rotated 90 degrees clockwise (around the center) and the dimensions of the canvas get also updated, so in a sense it looks like this afterwards:
<canvas id="one" width="200" height="100"></canvas>
Note that the id of the canvas is the same.
Imagine simply rotating an image clockwise without it being cropped or being padded.
Any suggestions before I do it the long way of creating a new canvas and rotating and copying pixel by pixel?
UPDATE sample code with suggestion from comments still not working:
function imageRotatecw90(){
var canvas = document.getElementById("one");
var context = canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;
var myImageData = context.getImageData(0,0, cw,ch);
context.save();
context.translate(cw / 2, ch / 2);
context.rotate(Math.PI/2);
context.putImageData(myImageData, 0, 0);
context.restore();
canvas.width=ch;
canvas.height=cw;
}
FiddleJS
Look at this DEMO.
To achieve the results seen in demo, I made use of canvas.toDataURL to cache the canvas into an image, then reset the canvas to their new dimensions, translate and rotate the context properly and finally draw the cached image back to modified canvas.
That way you easily rotate the canvas without need to redraw everything again. But because anti-aliasing methods used by browser, each time this operation is done you'll notice some blurriness in result. If you don't like this behavior the only solution I could figure out is to draw everything again, what is much more difficult to track.
Here follows the code:
var canvas = document.getElementById("one");
var context = canvas.getContext("2d");
var cw = canvas.width;
var ch = canvas.height;
// Sample graphic
context.beginPath();
context.rect(10, 10, 20, 50);
context.fillStyle = 'yellow';
context.fill();
context.lineWidth = 7;
context.strokeStyle = 'black';
context.stroke();
// create button
var button = document.getElementById("rotate");
button.onclick = function () {
// rotate the canvas 90 degrees each time the button is pressed
rotate();
}
var myImageData, rotating = false;
var rotate = function () {
if (!rotating) {
rotating = true;
// store current data to an image
myImageData = new Image();
myImageData.src = canvas.toDataURL();
myImageData.onload = function () {
// reset the canvas with new dimensions
canvas.width = ch;
canvas.height = cw;
cw = canvas.width;
ch = canvas.height;
context.save();
// translate and rotate
context.translate(cw, ch / cw);
context.rotate(Math.PI / 2);
// draw the previows image, now rotated
context.drawImage(myImageData, 0, 0);
context.restore();
// clear the temporary image
myImageData = null;
rotating = false;
}
}
}
Rotation
Note it is not possible to rotate a single element.
ctx.save();
ctx.rotate(0.17);
// Clear the current drawings.
ctx.fillRect()
// draw your object
ctx.restore();
Width/height adjustment
The only way I ever found to properly deal with display ratios, screen sizes etc:
canvas.width = 20;// DO NOT USE PIXELS
canvas.height = 40; // AGAIN NO PIXELS
Notice I am intentionally not using canvas.style.width or canvas.style.height. Also for an adjustable canvas don't rely on CSS or media queries to do the transformations, they are a headache because of the pixel ratio differences. JavaScript automatically accounts for those.
Update
You also have to update the width and the height before you draw. Not sure what you are trying to achieve, but I guess this isn't a problem:
Demo here
var canvas = document.getElementById("one");
var context = canvas.getContext("2d");
var cw = canvas.width;
var ch = canvas.height;
canvas.width = 200;
canvas.height = 400;
// Sample graphic
context.beginPath();
context.rect(10,10,20,50);
context.fillStyle = 'yellow';
context.fill();
context.lineWidth = 7;
context.strokeStyle = 'black';
context.stroke();
var myImageData = context.getImageData(0, 0, cw, ch);
context.save();
context.translate(cw / 2, ch / 2);
context.putImageData(myImageData, 0, 0);
context.rotate(0.20);
If you want to rotate an image by 90 degrees this might be helpful:
export const rotateBase64Image = async (base64data: string) => {
const image = new Image();
image.src = base64data;
return new Promise<string>((resolve, reject) => {
image.onload = function () {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
if (!ctx) throw new Error("cannnot get context '2d'");
canvas.width = image.height;
canvas.height = image.width;
ctx.setTransform(0, 1, -1, 0, canvas.width, 0); // overwrite existing transform
ctx!.drawImage(image, 0, 0);
canvas.toBlob((blob) => {
if (!blob) {
return reject("Canvas is empty");
}
const fileUrl = window.URL.createObjectURL(blob);
resolve(fileUrl);
}, "image/jpeg");
};
});
};
If you don't have image in base64 format you can do it like this:
const handleRotate = async () => {
const res = await fetch(link);
const blob = await res.blob();
const b64: string = await blobToB64(blob);
const rotatedImage = await rotateBase64Image(b64)
setLink(rotatedImage);
}
Here is my blobTob64 function:
export const blobToB64 = async (blob) => {
return new Promise((resolve, _) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.readAsDataURL(blob);
});
};

Categories

Resources