How to crop canvas image by path, not area - javascript

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).

Related

I'm making a drawing based web-app and I'm using HTML Canvas to handle the drawing, however the drawing is offset to the right a lot?

const map = document.getElementById('map')
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
const grid = document.getElementById('grid')
function resize() {
canvas.width = map.offsetWidth
canvas.height = map.offsetHeight
ctx.width = map.offsetWidth
ctx.height = map.offsetHeight
}
resize();
grid.appendChild(canvas)
canvas.style.gridColumn = 2
canvas.style.gridRow = 1
let pos = { x: 0, y: 0 };
window.addEventListener('resize', resize);
document.addEventListener('mousemove', draw);
document.addEventListener('mousedown', setPosition);
document.addEventListener('mouseenter', setPosition);
function setPosition(e) {
pos.x = e.clientX;
pos.y = e.clientY;
}
function draw(e) {
if (e.buttons !== 1) return;
ctx.beginPath();
ctx.lineWidth = 5;
ctx.lineCap = 'round';
ctx.strokeStyle = '#c0392b';
ctx.moveTo(pos.x, pos.y);
setPosition(e);
ctx.lineTo(pos.x, pos.y);
ctx.stroke();
}
Heres the code that generates the canvas relative to the size of a picture and allows the user to draw on the canvas. I've looked over another StackOverflow post with the same problem but no relevant answers. I know that the cause of the problem is that the canvas is stretched from it's standard proportion of 300 x 150 and is drawing at the correct position mathematically but not physically. How do I fix this?

Drawing Dynamic Gradients in HTML5 Canvas

I'm working on an art app and I want to be able to draw a gradient as a color. For example, if I keep drawing in a straight line or in circles, I want the gradient to repeat itself over and over. Right now the gradient is isolated to one side of the screen when I draw, but I would like to be able to draw with the gradient anywhere.
I have included the drawing function and color variable for reference.
HTML
<canvas id="canvas"></canvas>
JS
window.addEventListener('load', () => {
const canvas = document.querySelector('#canvas');
const ctx = canvas.getContext('2d');
canvas.height = window.innerHeight;
canvas.width = window.innerWidth;
let painting = false;
var gradient = ctx.createLinearGradient(0, 0, 170, 0);
gradient.addColorStop("0", "magenta");
gradient.addColorStop("0.5", "blue");
gradient.addColorStop("1.0", "red");
function windowSize() {
canvas.height = window.innerHeight;
canvas.width = window.innerWidth;
}
function startPosition(e) {
painting = true;
draw(e);
}
function finishedPosition() {
painting = false;
ctx.beginPath();
}
function draw(e) {
if(!painting) return;
ctx.lineWidth = 10;
ctx.lineCap = "round";
ctx.strokeStyle = gradient;
ctx.lineTo(e.clientX, e.clientY);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(e.clientX, e.clientY);
}
canvas.addEventListener('mousedown', startPosition);
canvas.addEventListener('mouseup', finishedPosition);
canvas.addEventListener('mousemove', draw);
canvas.addEventListener('touchstart', startPosition);
canvas.addEventListener('touchend', finishedPosition);
canvas.addEventListener('touchmove', draw);
window.addEventListener('resize', windowSize);
});
As I understand, you are trying to make a repeating-linear-gradient pattern.
It's supported in css, but not yet in the canvas gradient.
If all what you want to archive is a drawing like in your example, I will suggest you to do th following:
add a css gradient as a background and cover all unused space in white.
cover all the space in white
set the ctx.globalCompositeOperation = 'destination-out'; (so it will clean the drawing instead of draw)
Like this:
window.addEventListener('load', () => {
const canvas = document.querySelector('#canvas');
const ctx = canvas.getContext('2d');
canvas.height = window.innerHeight;
canvas.width = window.innerWidth;
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.globalCompositeOperation = 'destination-out';
let painting = false;
function windowSize() {
canvas.height = window.innerHeight;
canvas.width = window.innerWidth;
}
function startPosition(e) {
painting = true;
draw(e);
}
function finishedPosition() {
painting = false;
ctx.beginPath();
}
function draw(e) {
if(!painting) return;
ctx.lineWidth = 10;
ctx.lineCap = "round";
ctx.lineTo(e.clientX, e.clientY);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(e.clientX, e.clientY);
}
canvas.addEventListener('mousedown', startPosition);
canvas.addEventListener('mouseup', finishedPosition);
canvas.addEventListener('mousemove', draw);
canvas.addEventListener('touchstart', startPosition);
canvas.addEventListener('touchend', finishedPosition);
canvas.addEventListener('touchmove', draw);
window.addEventListener('resize', windowSize);
});
canvas {
background: repeating-linear-gradient(to right, magenta, blue, red, magenta) repeat-x;
background-size: 50px 100%;
}
<canvas id="canvas"></canvas>
--- EDIT ---
Also, you can use different (single) color at each event, instead of use gradient, and change the hue over time.
It will produce very different result. not sure what you like more..
Will be looking like that:
window.addEventListener('load', () => {
const canvas = document.querySelector('#canvas');
const ctx = canvas.getContext('2d');
let colorIdx = 0;
canvas.height = window.innerHeight;
canvas.width = window.innerWidth;
let painting = false;
function getColor() {
colorIdx += 5;
return `hsl(${colorIdx}, 100%, 50%)`;
}
function windowSize() {
canvas.height = window.innerHeight;
canvas.width = window.innerWidth;
}
function startPosition(e) {
painting = true;
draw(e);
}
function finishedPosition() {
painting = false;
ctx.beginPath();
}
function draw(e) {
if(!painting) return;
ctx.lineWidth = 10;
ctx.lineCap = "round";
ctx.strokeStyle = getColor();
ctx.lineTo(e.clientX, e.clientY);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(e.clientX, e.clientY);
}
canvas.addEventListener('mousedown', startPosition);
canvas.addEventListener('mouseup', finishedPosition);
canvas.addEventListener('mousemove', draw);
canvas.addEventListener('touchstart', startPosition);
canvas.addEventListener('touchend', finishedPosition);
canvas.addEventListener('touchmove', draw);
window.addEventListener('resize', windowSize);
});
<canvas id="canvas"></canvas>
Repeated gradient on 2D canvas
There are several ways to repeat a gradient. However they are all somewhat involved and will have some limitations and problems.
2 methods
The simplest is to manually repeat the color stops (see example) but suffers from floating point error
Use a pattern to repeat a gradient. Render a single repeat gradient to an offscreen canvas and use that canvas as a pattern, setting the pattern repeat appropriately. You will also have to align the gradient to the offscreen canvas axis and then set the pattern transform to match the orientation of the desired pattern.
Repeating color stops
The following function creates a gradient and adds a function to it that will create repeated color stops.
function RepeatingGradient(ctx, x1, y1, x2, y2, repeats) {
const dx = x2 - x1, dy = y2 - y1;
const gradient = ctx.createLinearGradient(x1, y1, x1 + dx * repeats, y1 + dy * repeats);
return Object.assign(gradient, {
addRepeatColorStop(pos, color) {
var i = 0;
const step = 1 / repeats;
const offset = pos / repeats;
while (i < repeats) {
const p = (i++) * step + offset;
// Rounding error may cause exception so check that p is not greater than 1
gradient.addColorStop(p > 1 ? 1 : p, color)
}
}
});
}
Usage
RepeatingLinearGradient(ctx, x1, y1, x2, y2, repeats) It needs a 2D context ctx, then the standard linear gradient arguments, x1, y1, x2, y2 and then the number of repeats repeats.
The repeats extend the area of the gradient. Thus if the positioning arguments are 0,0,0,10 and the repeat is 10 then the gradient will cover the area 0,0,0,100
You add repeating color stops using the new function gradient.addRepeatColorStop(pos, color)
You use the resulting gradient as normal
const grad = RepeatingGradient(ctx, 0,0,0,10, 10);
grad.addRepeatColorStop(0, "#000");
grad.addRepeatColorStop(1, "#FFF");
ctx.strokeStyle = grad;
Example use
Use mouse to draw using repeated gradient.
function RepeatingGradient(ctx, x1, y1, x2, y2, repeats) {
const dx = x2 - x1;
const dy = y2 - y1;
const gradient = ctx.createLinearGradient(x1, y1, x1 + dx * repeats, y1 + dy * repeats);
return Object.assign(gradient, {
addRepeatColorStop(pos, color) {
var i = 0;
const step = 1 / repeats, offset = pos / repeats;
while (i < repeats) {
const p = (i++) * step + offset;
gradient.addColorStop(p > 1 ? 1 : p, color);
}
}
});
}
const lineWidth = 20;
const ctx = canvas.getContext('2d');
canvas.height = innerHeight;
canvas.width = innerWidth;
ctx.lineWidth = lineWidth;
ctx.lineCap = "round";
const gradient = RepeatingGradient(ctx, 0, 0, 80, 80, 20);
gradient.addRepeatColorStop(0, "#F00");
gradient.addRepeatColorStop(0.25, "#FF0");
gradient.addRepeatColorStop(0.5, "#0F0");
gradient.addRepeatColorStop(0.75, "#FF0");
gradient.addRepeatColorStop(1, "#F00");
ctx.strokeStyle = gradient;
const mouse = {x : 0, y : 0, ox: 0, oy: 0, button : false, updateFunc: undefined}
mouse.updateFunc = function draw() {
if (mouse.button) {
ctx.beginPath();
ctx.lineTo(mouse.ox, mouse.oy);
ctx.lineTo(mouse.x, mouse.y);
ctx.stroke();
}
}
function mouseEvents(e){
const bounds = canvas.getBoundingClientRect();
mouse.ox = mouse.x;
mouse.oy = mouse.y;
mouse.x = e.pageX - bounds.left - scrollX;
mouse.y = e.pageY - bounds.top - scrollY;
mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button;
mouse.updateFunc && mouse.updateFunc();
}
["down","up","move"].forEach(name => document.addEventListener("mouse" + name, mouseEvents));
canvas { position : absolute; top : 0px; left : 0px; }
<canvas id="canvas"></canvas>
Click drag mouse to draw.
Notes
The repeat is not infinite. You need to ensure that you cover all of the area you want painted.
Repeats will reduce performance by a small amount depending on the number of repeats.
Gradients use FLOATS (32 bit floating point) not DOUBLES (64 bit floating point). Adding too many repeats and you will start to get some artifacts (not all repeats are identical, some stops may be out of order). Try to keep the number of repeats to the minimum needed to fit the render area. (FLOAT is the upper size, low end devices may only support lower than 32bit precision floating point)
If rounding errors start to effect the quality, try adding repeat stop ends slightly off 0 and 1
eg
gradient.addRepeatColorStop(0.01, "#F00"); // slightly above
gradient.addRepeatColorStop(0.99, "#F0F"); // slightly below
For the best results try to have the first and last position in the repeat match each other.
eg
gradient.addRepeatColorStop(0, "#F00"); // start
gradient.addRepeatColorStop(0.5, "#0F0"); // mid
gradient.addRepeatColorStop(1, "#F00"); // Match start
Using pattern to repeat gradients
The next method uses a pattern. Warning calling this function too often can result in a out of memory exception (gecko)
The function
function RepeatingLinearGradient(x1, y1, x2, y2) {
const dx = x2 - x1;
const dy = y2 - y1;
const length = (dx * dx + dy * dy) ** 0.5 | 0;
const can = document.createElement("canvas"); // support for OffscreenCanvas() limited
can.width = length;
can.height = 1;
const ctxP = can.getContext("2d");
const nx = dx / length;
const ny = dy / length;
const matrix = new DOMMatrixReadOnly([nx, ny, -ny, nx ,x1, y1]);
const gradient = ctxP.createLinearGradient(0, 0, length, 0);
var dirty = true;
function update() {
ctxP.fillStyle = gradient;
ctxP.fillRect(0,0,length, 1);
var pattern;
Object.assign(pattern = ctxP.createPattern(can, "repeat"), {
addColorStop(pos, color) {
gradient.addColorStop(pos, color);
return update();
}
});
pattern.setTransform(matrix);
return pattern;
}
return update();
}
The function creates an off screen canvas and renders an axis align gradient to it. Each time you add a color stop a new pattern is created and returned.
To align the pattern with the desired gradient the patterns transform is set to match the gradients orientation.
Usage
Similar to a normal gradient but returns a pattern. Arguments are two coordinate pairs x1, y1, x2, y2
IMPORTANT As patterns are not live you must reassign the gradient every time you make a change (eg add color stop)
var gradient = RepeatingLinearGradient(0, 0, 80, 80);
/* MUST!!! reassign */
gradient = gradient.addColorStop(0, "#000"); // MUST!!! reassign
gradient = gradient.addColorStop(1, "#FFF"); // MUST!!! reassign
ctx.strokeStyle = gradient;
Example
Use mouse to draw
function RepeatingGradient(x1, y1, x2, y2) {
const dx = x2 - x1;
const dy = y2 - y1;
const length = (dx * dx + dy * dy) ** 0.5 | 0;
const can = document.createElement("canvas");
can.width = length;
can.height = 1;
const ctxP = can.getContext("2d");
const nx = dx / length;
const ny = dy / length;
const matrix = new DOMMatrixReadOnly([nx, ny, -ny, nx ,x1, y1]);
const gradient = ctxP.createLinearGradient(0, 0, length, 0);
var dirty = true;
function update() {
ctxP.fillStyle = gradient;
ctxP.fillRect(0,0,length, 1);
var pattern;
Object.assign(pattern = ctxP.createPattern(can, "repeat"), {
addColorStop(pos, color) {
gradient.addColorStop(pos, color);
return update();
}
});
pattern.setTransform(matrix);
return pattern;
}
return update();
}
const lineWidth = 20;
const ctx = canvas.getContext('2d');
canvas.height = innerHeight;
canvas.width = innerWidth;
ctx.lineWidth = lineWidth;
ctx.lineCap = "round";
var gradient = RepeatingGradient(0, 0, 40, 20);
gradient = gradient.addColorStop(0, "#F00");
gradient = gradient.addColorStop(0.25, "#FF0");
gradient = gradient.addColorStop(0.5, "#0F0");
gradient = gradient.addColorStop(0.75, "#FF0");
gradient = gradient.addColorStop(1, "#F00");
ctx.strokeStyle = gradient;
const mouse = {x : 0, y : 0, ox: 0, oy: 0, button : false, updateFunc: undefined}
mouse.updateFunc = function draw() {
if (mouse.button) {
ctx.beginPath();
ctx.lineTo(mouse.ox, mouse.oy);
ctx.lineTo(mouse.x, mouse.y);
ctx.stroke();
}
}
function mouseEvents(e){
const bounds = canvas.getBoundingClientRect();
mouse.ox = mouse.x;
mouse.oy = mouse.y;
mouse.x = e.pageX - bounds.left - scrollX;
mouse.y = e.pageY - bounds.top - scrollY;
mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button;
mouse.updateFunc && mouse.updateFunc();
}
["down","up","move"].forEach(name => document.addEventListener("mouse" + name, mouseEvents));
canvas { position : absolute; top : 0px; left : 0px; }
<canvas id="canvas"></canvas>
Click drag mouse to draw.
Notes
Does not suffer the same floating point issues as the first method.
Is slower when creating patterns and uses more memory.
Can throw memory error if creating too often or adding stops too often.
Repeats are infinite, You can not limit the number of repeats.
Uses DOMMatrixReadOnly this may not be supported by all browsers.

how to draw smooth lines on canvas without clearing it?

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>

HTML canvas drawing and erasing with transparent png base

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>

How can I capture multiple images out of one in the <canvas>?

I wrote some code that draws a semi-transparent rectangle over an image that you can draw with touch:
function drawRect() {
var canvas = document.getElementById('receipt');
var ctx = canvas.getContext('2d');
var drag = false;
var imageObj;
var rect = { };
var touch;
canvas.width = WIDTH;
canvas.height = HEIGHT;
function init() {
imageObj = new Image();
imageObj.src = 'img.jpg';
imageObj.onload = function() {
ctx.drawImage(imageObj, 0, 0);
};
canvas.addEventListener('touchstart', handleTouch, false);
canvas.addEventListener('touchmove', handleTouch, false);
canvas.addEventListener('touchleave', handleEnd, false);
canvas.addEventListener('touchend', handleEnd, false);
}
function handleTouch(event) {
if (event.targetTouches.length === 1) {
touch = event.targetTouches[0];
if (event.type == 'touchmove') {
if (drag) {
rect.w = touch.pageX - rect.startX;
rect.h = touch.pageY - rect.startY ;
draw();
}
} else {
rect.startX = touch.pageX;
rect.startY = touch.pageY;
drag = true;
}
}
}
function handleEnd(event) {
drag = false;
}
function draw() {
drawImageOnCanvas();
ctx.fillStyle = 'rgba(0, 100, 255, 0.2)';
ctx.fillRect(rect.startX, rect.startY, rect.w, rect.h);
}
function drawImageOnCanvas() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(imageObj, 0, 0);
}
init();
}
This works. But, now I'd like to have it so that I can capture each of the parts of the image that are in rectangles as separate images.
How can I pull this off? Because I have that redraw stuff, it deletes the previous rectangle, which makes this difficult.
A canvas can only save out the whole canvas as an image, so the trick is to create a temporary canvas the size of the region you want to save out.
One way could be to create a function which takes the image and a rectangle object as argument and returns a data-uri (or Blob) of the region:
function saveRegion(img, rect) {
var canvas = document.createElement("canvas"),
ctx = canvas.getContext("2d");
canvas.width = rect.w;
canvas.height = rect.h;
ctx.drawImage(img, rect.startX, rect.startY, rect.w, rect.h, 0, 0, rect.w, rect.h);
return canvas.toDataURL():
}
You can pass in the original image if don't want any graphics on top, or if you draw elements on top of it just pass in the original canvas element as image source. And of course, CORS-restrictions apply.

Categories