Drawing semi-transparent lines on mouse movement in HTML5 canvas
Asked Answered
T

3

7

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
    };
}

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!

Tertias answered 9/12, 2013 at 15:48 Comment(2)
Kangax wrote an awesome blog post on this topic recently.Sharpie
@Pointy, I actually read it before posting this question. Indeed a great post, learned a lot - but I didn't see a solution for my particular problem?Tertias
C
12

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();

    }
}
Clever answered 9/12, 2013 at 16:17 Comment(8)
Thank you! I understand the first part about avoid drawing the same "old" paths over and over again. But in your actual solution (where you save all the paths ever recorded by mousedown > mousemove events), your draw all the paths, including the cat image, all over again, in each mousemove event. Is that strictly necessary to solve this issue? Last thing - Is it possible to avoid running over old paths with a new path (which makes the opacity a bit less transparent) ?Tertias
Look at the first jsfiddle I posted ( jsfiddle.net/jF9a6 ). Here I do exactly what you suggest and the problem with the line width appears. That's because you draw the path one segment at the time so overlapping areas between segments get darker. If you draw the whole path at once the overlapping areas are painted only once as expected.Clever
If you don't want to get the opacity less transparent when crossing older paths you can modify the refresh() function a bit jsfiddle.net/Q6R3y Now all the different paths are drawn at once.Clever
Thanks, great answer! Drawing the image + all the paths (at once) in every mouse move - is that a big performance hit?Tertias
Well that depends on what are you trying to achieve. Test it and you will see :-) It works well for the small kitteh image.Clever
Is there any other way to achieve this particular need?Tertias
let us continue this discussion in chatClever
If I want different colors and opacities for different lines than how to achieve that with this code?Neuron
C
3

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.

Clever answered 11/12, 2013 at 8:54 Comment(0)
N
0

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}
                />
Noman answered 2/1, 2021 at 20:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.