I'm trying to let users specify an area by painting over it with a "paint" tool that draws semi-transparent lines on a canvas. Its purpose is specifying a "mask" for an image that will be drawn below on the canvas.
This is what I tried so far:
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
var canvasPos = canvas.getBoundingClientRect();
var dragging = false;
drawImage();
$(canvas).mousedown(mouseDown);
$(canvas).mouseup(mouseUp);
$(canvas).mousemove(mouseMove);
function drawImage() {
var img = new Image();
img.src = 'http://img2.timeinc.net/health/img/web/2013/03/slides/cat-allergies-400x400.jpg';
img.onload = function () {
ctx.drawImage(img, 0, 0);
};
}
function mouseDown(e) {
var pos = getCursorPosition(e);
dragging = true;
ctx.strokeStyle = 'rgba(0, 100, 0, 0.25)';
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.lineWidth = 15;
ctx.beginPath();
ctx.moveTo(pos.x, pos.y);
}
function mouseUp(e) {
dragging = false;
}
function mouseMove(e) {
var pos, i;
if (!dragging) {
return;
}
pos = getCursorPosition(e);
ctx.lineTo(pos.x, pos.y);
ctx.stroke();
}
function getCursorPosition(e) {
return {
x: e.clientX - canvasPos.left,
y: e.clientY - canvasPos.top
};
}
Link to a jsfiddle of the above code: http://jsfiddle.net/s34PL/2/
The issue with this example code is that subsequent pixels that are drawn are making the opacity becomes less and less visible. I think it's because the line is 15 pixels wide (but I want it that wide though).
How can I solve this issue?
Thanks!
The problem is that you are drawing the whole path again and again:
function mouseMove(e) {
...
ctx.stroke(); // Draws whole path which begins where mouseDown happened.
}
You have to draw only the new segment of the path (http://jsfiddle.net/jF9a6/). And then ... you have the problem with the 15px width of the line.
So how to solve this? We have to draw the line at once as you did, but avoid painting on top of existing lines. Here is the code: http://jsfiddle.net/yfDdC/
The biggest change is the paths array. It contains yeah, paths :-) A path is an array of points stored in mouseDown and mouseMove functions. New path is created in mouseDown function:
paths.push([pos]); // Add new path, the first point is current pos.
In the mouseMove you add current mouse position to the last path in paths array and refreshs the image.
paths[paths.length-1].push(pos); // Append point tu current path.
refresh();
The refresh() function clears the whole canvas, draws the cat again and draws every path.
function refresh() {
// Clear canvas and draw the cat.
ctx.clearRect(0, 0, ctx.width, ctx.height);
if (globImg)
ctx.drawImage(globImg, 0, 0);
for (var i=0; i<paths.length; ++i) {
var path = paths[i];
if (path.length<1)
continue; // Need at least two points to draw a line.
ctx.beginPath();
ctx.moveTo(path[0].x, path[0].y);
...
for (var j=1; j<path.length; ++j)
ctx.lineTo(path[j].x, path[j].y);
ctx.stroke();
}
}
Alternative approach is to draw the paths solid and make the whole canvas transparent. Of course you have to move the image out of the canvas and stack it underneath. You can find the code here: http://jsfiddle.net/fP297/
<div style="position: relative; border: 1px solid black; width: 400px; height: 400px;">
<img src='cat.jpg' style="position: absolute; z-order: 1;">
<canvas id="canvas" width="400" height="400" style="position: absolute; z-order: 2; opacity: 0.25;"></canvas>
</div>
By drawing the lines solid you don't have to worry about drawing an area multiple times, so you don't have to worry about erasing the image and re-drawing everything.
I agree with Strix.
Here you will find an example based on his answer.
//
let mouseDownStartPosition: number | null = null;
let mouseDownLastPaintedPosition: number | null = null;
const drawRect = (canvas: HTMLCanvasElement, pixelX0 : number, pixelX1: number) => {
const context: CanvasRenderingContext2D = canvas.getContext('2d')!;
context.globalAlpha = 0.3;
context.fillStyle = "#bada55";
context.fillRect(pixelX0, 0, pixelX1 - pixelX0, context.canvas.height);
context.globalAlpha = 1.0;
}
const onCanvasMouseDown = (e: { clientX: number; }) => {
const canvas: HTMLCanvasElement = canvasRef.current!;
let rect = canvas.getBoundingClientRect();
mouseDownStartPosition = e.clientX - rect.left;
mouseDownLastPaintedPosition = mouseDownStartPosition;
}
const onCanvasMouseMove = (e: { clientX: number; }) => {
if (mouseDownLastPaintedPosition == null) return;
const canvas: HTMLCanvasElement = canvasRef.current!;
let rect = canvas.getBoundingClientRect();
const mouseCurrentPosition = e.clientX - rect.left;
drawRect(canvas, Math.min(mouseDownLastPaintedPosition, mouseCurrentPosition), Math.max(mouseDownLastPaintedPosition, mouseCurrentPosition));
mouseDownLastPaintedPosition = mouseCurrentPosition;
}
const onCanvasMouseUp = () => {
mouseDownStartPosition = null;
mouseDownLastPaintedPosition = null;
}
<MyCanvas
ref={canvasRef}
onMouseDown={onCanvasMouseDown}
onMouseMove={onCanvasMouseMove}
onMouseUp={onCanvasMouseUp}
/>
Related
I am looking for a simple way to combine shapes via CSS or JS to achieve something like below:
At first I was trying to just overlay the basic shapes using before and after. But by doing so I can not create a border around the whole merged shape. So next I tried a solution using html canvas like:
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
ctx.lineWidth = 10;
ctx.strokeRect(30,20,150,250);
ctx.strokeRect(120,160,120,50);
ctx.globalCompositeOperation='destination-out';
//Filling the rects only keeps the border
ctx.fillStyle = "#FF0000";
ctx.fillRect(30,20,150,250);
ctx.fillRect(120,160,120,50);
body{ background-color: green; }
<canvas id="canvas" width=300 height=300></canvas>
But by doing so I wasn't able to keep the custom white fill of the combined shape. So I am now asking for a way to either optimize my current approach to be able to set a custom background for my shape or a completely new approach.
As I need to get the onMouseEnter event I didn't found using a svg-file suitable for my situation.
If there is any framework that would make this process easier, I am also happy to adapt.
I'm not sure this is the most suitable solution for your situation but note that your canvas code can be modified to fill the merged area and detect mouse events just fine.
You don't have to call strokeRect() or fillRect() that do only create a temporary rectangle only for their call. You can create a way more complex path made of several rectangles and other commands. This complex path will then be filled a single entity, doing the "merging" for you. By stroking first and filling after, we can hide the junctions between each shapes.
And to detect if you're over the filled area, you can use ctx.isPointInPath():
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
// beware, we fill over the inner half of the stroke
// so only half of this value will be visible
ctx.lineWidth = 20;
let mouse = { x: -1, y: -1 };
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
ctx.rect(30,20,150,250);
ctx.rect(120,160,120,50);
ctx.rect(350,130,50,130);
// contrarily to rect(),
// arc() doesn't call automatically moveTo()
// so to avoid having a link between the previous point
// and the beginning of the arc, we hav to call it ourselves
// this would also be true with any other command, except roundRect()
ctx.moveTo(375+70, 80);
ctx.arc(375, 80, 70, 0, Math.PI*2);
// check if the mouse is over either the stroke or the fill area
const hovering = ctx.isPointInStroke(mouse.x, mouse.y) ||
ctx.isPointInPath(mouse.x, mouse.y)
// change the color based on hover state
ctx.fillStyle = hovering ? "#FF0000" : "#FFFF00";
// It's all a single path
ctx.stroke(); // stroking would show all the various sections
ctx.fill(); // but since we fill over it, we only keep the common outline
}
draw();
// update the hovering state
addEventListener("mousemove", (evt) => {
const rect = canvas.getBoundingClientRect();
mouse.x = evt.clientX - rect.left;
mouse.y = evt.clientY - rect.top;
draw();
});
body{ background-color: green; }
<canvas id="canvas" width=600 height=300></canvas>
And if you need you could separate each shape in their own Path2D object:
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
ctx.lineWidth = 20;
let mouse = { x: -1, y: -1 };
const p1 = new Path2D();
p1.rect(30,20,150,250);
p1.rect(120,160,120,50);
const p2 = new Path2D();
p2.rect(350,130,50,130);
p2.moveTo(375+70, 80);
p2.arc(375, 80, 70, 0, Math.PI*2);
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
const overP1 = ctx.isPointInStroke(p1, mouse.x, mouse.y) ||
ctx.isPointInPath(p1, mouse.x, mouse.y)
ctx.fillStyle = overP1 ? "#FF0000" : "#FFFF00";
ctx.stroke(p1);
ctx.fill(p1);
const overP2 = ctx.isPointInStroke(p2, mouse.x, mouse.y) ||
ctx.isPointInPath(p2, mouse.x, mouse.y)
ctx.fillStyle = overP2 ? "#FF0000" : "#FFFF00";
ctx.stroke(p2);
ctx.fill(p2);
}
draw();
addEventListener("mousemove", (evt) => {
const rect = canvas.getBoundingClientRect();
mouse.x = evt.clientX - rect.left;
mouse.y = evt.clientY - rect.top;
draw();
});
body{ background-color: green; }
<canvas id="canvas" width=600 height=300></canvas>
Let's reduce the problem. Build the shapes from atomic shapes such as rectangles and circles. Then you can extrapolate for many shapes. At first I thought about canvas using this function to detect mouse over:
const pointInRect = ({x1, y1, x2, y2}, {x, y}) => (
(x > x1 && x < x2) && (y > y1 && y < y2)
)
But then again, since we reduced the problem we might as well use HTML. Since we are dealing with multiple shapes, it will be easier using some 2D map.
var shape = [
" 1 1111 ".split(""),
"11 1 1 ".split(""),
" 111 111".split(""),
" 1 1 1".split(""),
" 1111 1 ".split(""),
]
const square_size = 40;
var container = document.querySelector(".container");
function draw_square(x, y) {
var div = document.createElement("div");
div.classList.add("square")
div.style.left = x + "px"
div.style.top = y + "px"
container.appendChild(div)
return div;
}
function draw_shape() {
for (var i = 0; i < shape.length; i++) {
for (var j = 0; j < shape[i].length; j++) {
var pixel = shape[i][j];
if (pixel == 1) {
var div = draw_square(j * square_size, i * square_size);
if (i > 0 && shape[i - 1][j] == 1) {
div.style.borderTopColor = "transparent";
}
if (i < shape.length - 1 && shape[i + 1][j] == 1) {
div.style.borderBottomColor = "transparent";
}
if (j > 0 && shape[i][j - 1] == 1) {
div.style.borderLeftColor = "transparent";
}
if (j < shape[i].length - 1 && shape[i][j + 1] == 1) {
div.style.borderRightColor = "transparent";
}
}
}
}
}
draw_shape(shape)
attach_mouse_events()
function attach_mouse_events() {
var squares = document.querySelectorAll(".square");
squares.forEach(function (square) {
square.addEventListener('mouseenter', function(ev) {
squares.forEach(function (square) {
square.classList.add("active");
})
})
square.addEventListener('mouseleave', function(ev) {
squares.forEach(function (square) {
square.classList.remove("active");
})
})
})
}
body {
background: lightgray;
}
.square {
width: 40px;
height: 40px;
background: green;
border: 2px solid black;
position: absolute;
}
.square.active {
background: white;
}
.container {
margin: 20px;
background-size:cover;
height: 300px;
position: relative;
}
<div class="container">
</div>
As for the circles - it's the same idea. I would recommend painting the circles first then the squares, for a perfect borders. Good luck.
I am trying to make a highlighter,
The problem is with the transparency, maybe due to the lineCap=round, and some other reason there are many dark spots in just one line currently(it should not be). I can't explain more in words,compare the images below
Current look(when I draw 1 line):
The look I want when I draw 1 line:
The look I want when I draw 2 lines:
Gif
Current code:
lineThickess = 50;
lineColor = 'rgba(255,255,0,0.1)';
// wait for the content of the window element
// to load, then performs the operations.
// This is considered best practice.
window.addEventListener('load', () => {
resize(); // Resizes the canvas once the window loads
document.addEventListener('mousedown', startPainting);
document.addEventListener('mouseup', stopPainting);
document.addEventListener('mousemove', sketch);
window.addEventListener('resize', resize);
});
const canvas = document.querySelector('#canvas');
// Context for the canvas for 2 dimensional operations
const ctx = canvas.getContext('2d');
// Resizes the canvas to the available size of the window.
function resize() {
ctx.canvas.width = canvas.offsetWidth;
ctx.canvas.height = canvas.offsetHeight;
}
// Stores the initial position of the cursor
let coord = {
x: 0,
y: 0
};
// This is the flag that we are going to use to
// trigger drawing
let paint = false;
// Updates the coordianates of the cursor when
// an event e is triggered to the coordinates where
// the said event is triggered.
function getPosition(event) {
coord.x = event.clientX - canvas.offsetLeft;
coord.y = event.clientY - canvas.offsetTop;
}
// The following functions toggle the flag to start
// and stop drawing
function startPainting(event) {
paint = true;
getPosition(event);
}
function stopPainting() {
paint = false;
}
function sketch(event) {
if (!paint) return;
ctx.beginPath();
ctx.lineWidth = lineThickess;
// Sets the end of the lines drawn
// to a round shape.
ctx.lineCap = 'round';
ctx.strokeStyle = lineColor;
// The cursor to start drawing
// moves to this coordinate
ctx.moveTo(coord.x, coord.y);
// The position of the cursor
// gets updated as we move the
// mouse around.
getPosition(event);
// A line is traced from start
// coordinate to this coordinate
ctx.lineTo(coord.x, coord.y);
// Draws the line.
ctx.stroke();
}
html,
body {
height: 100%;
}
canvas {
width: 100%;
height: 100%;
border: 1px solid;
}
<canvas id="canvas"></canvas>
The clue
Stack overflow recommended me reviewing other posts related, and I found a clue,and code
But
As shown (and said in the answer), two different paths work well, but loops on same paths don't work as needed
The issue you are seeing is because the intersection of two lines with transparency the result is not the same transparent color, just like in the real world if you hold two sunglasses on top of each other the resulting color is not the same as the individual ones.
What you should do is use Path2D to get the same transparency over the entire object, it could be lines, arches rectangles or any other items, if they are under a common Path2D they all get the same color even on intersections.
Below is a very simple example based on your code
This is how it looks like when the lines cross:
let path = new Path2D()
document.addEventListener('mousemove', sketch)
const canvas = document.querySelector('#canvas')
const ctx = canvas.getContext('2d')
ctx.lineWidth = 10
ctx.lineCap = 'round'
ctx.strokeStyle = 'rgba(255,0,255,0.3)'
function sketch(event) {
let x = event.clientX - canvas.offsetLeft
let y = event.clientY - canvas.offsetTop
path.lineTo(x, y)
}
function drawCircles() {
for (let i = 25; i<=100; i+=25)
ctx.arc(i*2, i, 5, 0, 8)
ctx.fill()
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height)
drawCircles()
ctx.stroke(path)
requestAnimationFrame(draw)
}
draw()
<canvas id="canvas" width=300 height=150></canvas>
Another example, a bit more complex this time, using an array of Path2D objects, and added a button to change colors, the creation of the color add a new item to the array:
let paths = []
paths.unshift({path: new Path2D(), color:'rgba(255,0,255,0.3)'})
document.addEventListener('mousemove', sketch)
const canvas = document.querySelector('#canvas')
const ctx = canvas.getContext('2d')
ctx.lineWidth = 10
ctx.lineCap = 'round'
function changeColor() {
let newColor = 'rgba(0,255,255,0.3)'
if (paths[0].color == newColor)
newColor = 'rgba(255,0,255,0.3)'
paths.unshift({path:new Path2D(), color: newColor})
}
function sketch(event) {
let x = event.clientX - canvas.offsetLeft
let y = event.clientY - canvas.offsetTop
paths[0].path.lineTo(x, y)
}
function drawCircles() {
for (let i = 25; i<=100; i+=25)
ctx.arc(i*2, i, 5, 0, 8)
ctx.fill()
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height)
drawCircles()
paths.forEach(e => {
ctx.strokeStyle = e.color
ctx.stroke(e.path)
})
requestAnimationFrame(draw)
}
draw()
<canvas id="canvas" width=250 height=150></canvas>
<button onclick="changeColor()">Change Color</button>
This is how it looks like after changing colors a few times:
Now you have all the tools you need...
It's up to you to decide what is the end of a path and the start of a new one.
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).
Here is a jsfiddle https://jsfiddle.net/g10qgefy/46/
if (drawingMode === 'brush') {
ctx.globalCompositeOperation = "source-atop";
} else { //erase
ctx.globalCompositeOperation = "destination-out";
ctx.strokeStyle = 'rgba(1,0,0,0)';
}
I am trying to create this simple drawing feature. I have a canvas element, I draw an image to it using drawImage(). Then, you can brush on the non-transparent pixels using canvas method globalCompositeOperation = "source-atop".
That all does what I want. But I also need to be able to erase some of the drawn lines without affecting the transparent image. So I just want to select the erase button and start erasing away the black lines I have drawn.
I have been playing with globalCompositionOperation and changing the values from Canvas Rendering | MDN but these mostly just delete everything.
I'm sure there is a solution - would love to hear your thoughts!
Keep two layers: the one the user is drawing to, and the rendering one.
Draw the user's drawings on the first layer (offscreen canvas) and then do your compositing with the png image on top of this layer on a second canvas, the rendering one.
This way you can perform your erase calls safely, without having to care about the png layer.
let renderingElement = document.getElementById("myCanvas");
// create an offscreen canvas only for the drawings
let drawingElement = renderingElement.cloneNode();
let drawingCtx = drawingElement.getContext('2d');
let renderingCtx = renderingElement.getContext('2d');
let img = new Image();
let brushSize = 25;
let brushColor = "#000000"
let drawingMode = 'brush';
let lastX;
let lastY;
let moving = false;
img.src = 'https://i.pinimg.com/originals/49/af/b1/49afb1d21ae594cb7ac3534a15383711.png';
img.onload = () => {
renderingCtx.drawImage(img, 0, 0);
}
let eraseButton = document.getElementById('erase');
let brushButton = document.getElementById('brush');
let exportButton = document.getElementById('export');
eraseButton.addEventListener('click', () => {
drawingMode = 'erase';
})
brushButton.addEventListener('click', () => {
drawingMode = 'brush';
})
renderingElement.addEventListener('mousedown', (ev) => {
moving = true;
lastX = ev.pageX - renderingElement.offsetLeft;
lastY = ev.pageY - renderingElement.offsetTop;
})
renderingElement.addEventListener('mouseup', (ev) => {
moving = false;
lastX = ev.pageX - renderingElement.offsetLeft;
lastY = ev.pageY - renderingElement.offsetTop;
})
renderingElement.addEventListener('mousemove', (ev) => {
if (moving) {
if (drawingMode === 'brush') {
drawingCtx.globalCompositeOperation = "source-over";
} else {
drawingCtx.globalCompositeOperation = "destination-out";
}
let currentX = ev.pageX - renderingElement.offsetLeft;
let currentY = ev.pageY - renderingElement.offsetTop;
drawingCtx.beginPath();
drawingCtx.lineJoin = "round";
drawingCtx.moveTo(lastX, lastY);
drawingCtx.lineTo(currentX, currentY);
drawingCtx.closePath();
drawingCtx.strokeStyle = brushColor;
drawingCtx.lineWidth = brushSize;
drawingCtx.stroke();
lastX = currentX;
lastY = currentY;
// draw to visible canvas
renderingCtx.clearRect(0, 0, renderingElement.width, renderingElement.height);
renderingCtx.drawImage(img, 0, 0);
renderingCtx.globalCompositeOperation = 'source-atop';
renderingCtx.drawImage(drawingElement, 0, 0);
// reset
renderingCtx.globalCompositeOperation = 'source-over';
}
});
canvas {
border: 5px solid red;
}
<button id="brush">brush</button>
<button id="erase">erase</button>
<canvas id="myCanvas" width="500" height="500"></canvas>
When erasing simply set your current brush as a pattern (with no repeat) using the original image, or to work around most caveats (scale, translate etc.): define the initial canvas (with the image drawn in) as the pattern and store the pattern globally (shown in the modified code below).
Update Although this is correct code-wise, it turns out some browsers currently has issues with using the canvas element as pattern source (a browser bug, in this case FF v59 on osX, ref. Kaiido's comment).
No worries though, there is a simple workaround for this though: just use the original image instead of canvas for the pattern (as shown below - code example updated). ***
This will allow you to draw back portions of the image using the same compositing mode as before.
// define and set brush
let pattern = ctx.createPattern(img, "no-repeat");
ctx.strokeStyle = pattern;
let canvasElement = document.getElementById("myCanvas");
let img = new Image();
let brushSize = 25;
let brushColor = "#000000"
let drawingMode = 'brush';
let ctx = canvasElement.getContext('2d');
let lastX;
let lastY;
let moving = false;
let pattern;
img.src = 'https://i.pinimg.com/originals/49/af/b1/49afb1d21ae594cb7ac3534a15383711.png';
img.onload = () => {
ctx.drawImage(img, 0, 0);
ctx.globalCompositeOperation = "source-atop";
pattern = ctx.createPattern(img, "no-repeat");
}
let eraseButton = document.getElementById('erase');
let brushButton = document.getElementById('brush');
eraseButton.addEventListener('click', () => {
drawingMode = 'erase';
ctx.strokeStyle = pattern; // use pattern for style here
})
brushButton.addEventListener('click', () => {
drawingMode = 'brush';
ctx.strokeStyle = "#000"; // restore current color here
})
canvasElement.addEventListener('mousedown', (ev) => {
moving = true;
lastX = ev.pageX;
lastY = ev.pageY;
})
canvasElement.addEventListener('mouseup', (ev) => {
moving = false;
lastX = ev.pageX;
lastY = ev.pageY;
})
canvasElement.addEventListener('mousemove', (ev) => {
if (moving) {
let currentX = ev.pageX;
let currentY = ev.pageY;
ctx.beginPath();
ctx.lineJoin = "round";
ctx.moveTo(lastX, lastY);
ctx.lineTo(currentX, currentY);
ctx.closePath();
//ctx.strokeStyle = brushColor; // can be set during color selecting (if mode=draw)
ctx.lineWidth = brushSize;
ctx.stroke();
lastX = currentX;
lastY = currentY;
}
})
canvas {
position: absolute;
left: 0;
border: 5px solid red;
}
button {
position: absolute;
top: 550px;
}
#erase {
left: 60px;
}
<canvas id="myCanvas" width="500" height="500"></canvas>
<button id="brush">
brush
</button>
<button id="erase">
erase
</button>