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>
Related
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>
I have a canvas code to draw a signature. The code works perfectly fine with chrome and Firefox but does not draw at all on IE 11.
My canvas is:
<canvas id="signitureCanvas" style="border: 3px solid #000; cursor:crosshair; background-color:white;"></canvas>
My code is as below:
var canvas = document.getElementById('signitureCanvas');
var ctx = canvas.getContext('2d');
var canvasWidth = 200;
var canvasLength = 120;
canvas.width = canvasWidth;
canvas.height = canvasLength;
var canvasx = $(canvas).offset().left;
var canvasy = $(canvas).offset().top;
var last_mousex = last_mousey = 0;
var mousex = mousey = 0;
var mousedown = false;
var tooltype = 'draw';
//Mousedown
$(canvas).on('mousedown', function (e) {
last_mousex = mousex = parseInt(e.clientX - canvasx);
last_mousey = mousey = parseInt(e.clientY - canvasy);
mousedown = true;
});
//Mouseup
$(canvas).on('mouseup', function (e) {
mousedown = false;
});
//Mousemove
$(canvas).on('mousemove', function (e) {
mousex = parseInt(e.clientX - canvasx);
mousey = parseInt(e.clientY - canvasy);
if (mousedown) {
ctx.beginPath();
if (tooltype == 'draw') {
ctx.globalCompositeOperation = 'source-over';
ctx.strokeStyle = 'black';
ctx.lineWidth = 3;
} else {
ctx.globalCompositeOperation = 'destination-out';
ctx.lineWidth = 10;
}
ctx.moveTo(last_mousex, last_mousey);
ctx.lineTo(mousex, mousey);
ctx.lineJoin = ctx.lineCap = 'round';
ctx.stroke();
}
last_mousex = mousex;
last_mousey = mousey;
});
function ClearCanvas() {
var canvas = document.getElementById('signitureCanvas');
var ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
Is IE 11 in particular having problems?
Edit:
I figured out the problem is with my Iframe:
When height and width are set to 300 everything works fine:
<embed id="fred" type="application/pdf" style="border:1px solid #666CCC" title="PDF in an i-Frame" src="#Model.FilePath" frameborder="1" scrolling="yes" height="300" width="300" />
When I set it to 1000, it won't work:
<embed id="fred" type="application/pdf" style="border:1px solid #666CCC" title="PDF in an i-Frame" src="#Model.FilePath" frameborder="1" scrolling="yes" height="1000" width="1000" />
I believe it's something with the offset but I can't figure how to fix it.
any help?
For further knowledge to whom might ask:
I have found this piece of code that works on all browsers, layerX and layerY are different for firefox browsers:
var canvas = document.getElementById('signitureCanvas');
var ctx = canvas.getContext('2d');
var canvasWidth = 200;
var canvasLength = 120;
canvas.width = canvasWidth;
canvas.height = canvasLength;
var x = 0;
var y = 0;
function tool_pencil() {
var tool = this;
this.started = false;
// This is called when you start holding down the mouse button
// This starts the pencil drawing
this.mousedown = function (ev) {
ctx.beginPath();
ctx.strokeStyle = 'black';
ctx.lineWidth = 3;
ctx.lineJoin = ctx.lineCap = 'round';
ctx.moveTo(x, y);
tool.started = true;
};
// This function is called every time you move the mouse. Obviously, it only
// draws if the tool.started state is set to true (when you are holding down
// the mouse button)
this.mousemove = function (ev) {
if (tool.started) {
ctx.lineTo(x, y);
ctx.stroke();
}
};
// This is called when you release the mouse button
this.mouseup = function (ev) {
if (tool.started) {
tool.mousemove(ev);
tool.started = false;
}
};
}
// The general-purpose event handler. This function just determines
// the mouse position relative to the <canvas> element
function ev_canvas(ev) {
// Firefox
if (ev.offsetX || ev.offsetX == 0) {
x = ev.offsetX;
y = ev.offsetY;
// Opera
} else if (ev.layerX || ev.layerX == 0) {
x = ev.layerX;
y = ev.layerX;
}
// Call the event handler of the tool
var func = tool[ev.type];
if (func) {
func(ev);
}
}
function ClearCanvas() {
var canvas = document.getElementById('signitureCanvas');
var ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
Can you please take a look at following demo and let me know how I can enable the code to draw 100% straight Line on the canvas?
$(function() {
var drawLine = false;
var theCanvas = document.getElementById('map');
var ctx = theCanvas.getContext('2d');
theCanvas.width = 420;
theCanvas.height = 300;
var canvasOffset = $('#map').offset();
$('#map').mousemove(function(e) {
if (drawLine === true) {
ctx.lineTo(e.pageX - canvasOffset.left, e.pageY - canvasOffset.top);
ctx.stroke();
}
});
$('#map').mousedown(function() {
drawLine = true;
ctx.strokeStyle = 'blue';
ctx.lineWidth = 5;
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(e.pageX - canvasOffset.left, e.pageY - canvasOffset.top);
});
$(window).mouseup(function() {
drawLine = false;
});
});
#map{border:solid; margin-top: 50px;}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.0.3/jquery.min.js"></script>
<canvas id="map"></canvas>
As you can see the code is working fine but what I want is drawing a straight line on drawing
Thanks
You would need to have 2 canvases:
temporary for animation
permanent to store drawing.
Algorithm:
ondown write start coordinates.
onmove record endpoint, clear canvas 1, draw a line on canvas 1 from start point to end point.
onup draw final line on 2nd canvas clear 1st canvas.
Too lazy create second canvas (it'll clear it every try now);
Put a comment where pass different canvas for permanent draw
$(function() {
var drawLine = false;
var theCanvas = document.getElementById('map');
var finalPos = {x:0, y:0};
var startPos = {x:0, y:0};
var ctx = theCanvas.getContext('2d');
theCanvas.width = 420;
theCanvas.height = 300;
var canvasOffset = $('#map').offset();
function line(cnvs) {
cnvs.beginPath();
cnvs.moveTo(startPos.x, startPos.y);
cnvs.lineTo(finalPos.x, finalPos.y);
cnvs.stroke();
}
function clearCanvas()
{
ctx.clearRect(0, 0, theCanvas.width, theCanvas.height);
}
$('#map').mousemove(function(e) {
if (drawLine === true) {
finalPos = {x: e.pageX - canvasOffset.left, y:e.pageY - canvasOffset.top};
clearCanvas();
line(ctx);
}
});
$('#map').mousedown(function(e) {
drawLine = true;
ctx.strokeStyle = 'blue';
ctx.lineWidth = 5;
ctx.lineCap = 'round';
ctx.beginPath();
startPos = { x: e.pageX - canvasOffset.left, y: e.pageY - canvasOffset.top};
});
$(window).mouseup(function() {
clearCanvas();
// Replace with var that is second canvas
line(ctx);
finalPos = {x:0, y:0};
startPos = {x:0, y:0};
drawLine = false;
});
});
#map{border:solid; margin-top: 50px;}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.0.3/jquery.min.js"></script>
<canvas id="map"></canvas>
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}
/>