2

I would like to be pointed in the right direction in terms of algorithm in the demo below here http://sta.sh/muro/. Also the canvas tools it is using - i.e. is it drawing lines or drawing many arcs, etc

Specifically I want to emulate the brush turning that would cause the entire "brush stroke" to be thicker. See the image for the brush settings I want to emulate.

Ultimately, I would like a to create a paint brush that would vary in thickness when turned, exactly like the behaviour below.

enter image description here

eirikrl
  • 438
  • 1
  • 5
  • 17

1 Answers1

5

To do this you need to record the mouse points when the button is down. Then you need to check each line segment to find the direction, the length of the line and the normalised vector of that line segment so you can over sample the mouse samples.

So if you have a set of points taken from the mouse the following will get the required details.

    for(var i = 0; i < len-1; i++){
        var p1 = line[i];
        var p2 = line[i+1];
        var nx = p2.x - p1.x;
        var ny = p2.y - p1.y;
        p1.dir = ((Math.atan2(ny,nx)%PI2)+PI2)%PI2; // get direction
        p1.len = Math.hypot(nx,ny);  // get length
        p1.nx = nx/p1.len;  // get normalised vector
        p1.ny = ny/p1.len;
    }

Once you have the details of each line segment it is a simple matter the change drawing parameters according to the values.

I have added a demo. It is as close as I could be bothered to get to the example as the line you provided did not give the options to draw what your image showed. The image shows they use the shadow as well and do sub mouse sample sampling. The rounded box they draw could be an image which would be much quicker than the drawn box I have done. I have also smoothed the line a bit so there is a little bit of a lag between the drawing and it being finalised and set to the background image.

At the top of the demo are a set of constants that control the various settings. It would take way too much time to add input options and vetting so play with them if you find the code useful.

Sorry the code is messy but its just an example you will have to pluck it apart yourself.

/** hypot.js begin **/
// ES6 new math function hypot. Sqrt of the sum of the squares
var hypot = Math.hypot;
if(typeof hypot === 'undefined'){
    hypot = function(x,y){
        return Math.sqrt(Math.pow(x,2)+Math.pow(y,2));
    }
}

/** hypot.js end **/

// draw options
const SUB_SECTIONS = 5; // points between mouse samples
const SIZE_MULT = 3; // Max size multiplier
const SIZE_MIN = 0.1 // min size of line
const BIG_DIR = 0.6;  // direction in radians for thickest line
const SMOOTH_MAX = 7;  // number of smoothing steps performed on a line. Bigger that 20 will slow the rendering down
const SHAPE_ALPHA = 0.5;  // the stoke alpha
const SHAPE_FILL_ALPHA = 0.75; // the fill alpha
const SHADOW_ALPHA = 0.1;   // the shadow alpha
const SHADOW_BLUR = 5;  // the shadow blur
const SHADOW_OFFX = 6;  // shoadow offest x and y
const SHADOW_OFFY = 6;
const SHAPE_LINE_WIDTH = 0.6;  // stroke width of shape. This is constant and is not scaled
const SHAPE_WIDTH = 4;  // shape drawn width;
const SHAPE_LENGTH = 20;  // shape drawn length
const SHAPE_ROUNDING = 2;  // shape rounded corner radius. Warning invalid results if rounding is greater than half width or height which ever is the smallest
const SHAPE_TRAIL = 0;  // offset  draw shape. Negivive numbers trail drawing positive are infront

var div = document.createElement("div"); 
div.textContent = "Click drag mouse to draw, Right click to clear."
document.body.appendChild(div);

var mouse;
var demo = function(){
    
    /** fullScreenCanvas.js begin **/
    var canvas = (function(){
        var canvas = document.getElementById("canv");
        if(canvas !== null){
            document.body.removeChild(canvas);
        }
        // creates a blank image with 2d context
        canvas = document.createElement("canvas"); 
        canvas.id = "canv";    
        canvas.width = window.innerWidth;
        canvas.height = window.innerHeight; 
        canvas.style.position = "absolute";
        canvas.style.top = "0px";
        canvas.style.left = "0px";
        canvas.style.zIndex = 1000;
        canvas.ctx = canvas.getContext("2d"); 
        document.body.appendChild(canvas);
        return canvas;
    })();
    var ctx = canvas.ctx;
    
    /** fullScreenCanvas.js end **/
    /** MouseFull.js begin **/
    if(typeof mouse !== "undefined"){  // if the mouse exists 
        if( mouse.removeMouse !== undefined){
            mouse.removeMouse(); // remove previouse events
        }
    }
    var canvasMouseCallBack = undefined;  // if needed
    mouse = (function(){
        var mouse = {
            x : 0, y : 0, w : 0, alt : false, shift : false, ctrl : false,
            interfaceId : 0, buttonLastRaw : 0,  buttonRaw : 0,
            over : false,  // mouse is over the element
            bm : [1, 2, 4, 6, 5, 3], // masks for setting and clearing button raw bits;
            getInterfaceId : function () { return this.interfaceId++; }, // For UI functions
            startMouse:undefined,
            mouseEvents : "mousemove,mousedown,mouseup,mouseout,mouseover,mousewheel,DOMMouseScroll".split(",")
        };
        function mouseMove(e) {
            var t = e.type, m = mouse;
            m.x = e.offsetX; m.y = e.offsetY;
            if (m.x === undefined) { m.x = e.clientX; m.y = e.clientY; }
            m.alt = e.altKey;m.shift = e.shiftKey;m.ctrl = e.ctrlKey;
            if (t === "mousedown") { m.buttonRaw |= m.bm[e.which-1];
            } else if (t === "mouseup") { m.buttonRaw &= m.bm[e.which + 2];
            } else if (t === "mouseout") { m.buttonRaw = 0; m.over = false;
            } else if (t === "mouseover") { m.over = true;
            } else if (t === "mousewheel") { m.w = e.wheelDelta;
            } else if (t === "DOMMouseScroll") { m.w = -e.detail;}
            if (canvasMouseCallBack) { canvasMouseCallBack(mouse); }
            e.preventDefault();
        }
        function startMouse(element){
            if(element === undefined){
                element = document;
            }
            mouse.element = element;
            mouse.mouseEvents.forEach(
                function(n){
                    element.addEventListener(n, mouseMove);
                }
            );
            element.addEventListener("contextmenu", function (e) {e.preventDefault();}, false);
        }
        mouse.removeMouse = function(){
            if(mouse.element !== undefined){
                mouse.mouseEvents.forEach(
                    function(n){
                        mouse.element.removeEventListener(n, mouseMove);
                    }
                );
                canvasMouseCallBack = undefined;
            }
        }
        mouse.mouseStart = startMouse;
        return mouse;
    })();
    if(typeof canvas !== "undefined"){
        mouse.mouseStart(canvas);
    }else{
        mouse.mouseStart();
    }
    /** MouseFull.js end **/
    /** CreateImage.js begin **/
    // creates a blank image with 2d context
    var createImage=function(w,h){var i=document.createElement("canvas");i.width=w;i.height=h;i.ctx=i.getContext("2d");return i;}
    
    /** CreateImage.js end **/
    /** FrameUpdate.js begin **/
    var w = canvas.width;
    var h = canvas.height;
    var cw = w / 2;
    var ch = h / 2;
    var line = []; // line to hold drawing points
    var image = createImage(w,h); // Background image to dump point to when soothed
    
    var PI2 = Math.PI * 2; // 360 to save typing 
    var PIh = Math.PI / 2; // 90 
    
    // draws a rounded rectangle path
    function roundedRect(ctx,x, y, w, h, r){

        ctx.beginPath(); 
        ctx.arc(x + r, y + r, r, PIh * 2, PIh * 3);  
        ctx.arc(x + w - r, y + r, r, PIh * 3, PI2);
        ctx.arc(x + w - r, y + h - r, r, 0, PIh);  
        ctx.arc(x + r, y + h - r, r, PIh, PIh * 2);  
        ctx.closePath(); 
    }

    // this draws a section of line
    function drawStroke(ctx,line){
        var len = line.length;

        ctx.shadowBlur = SHADOW_BLUR;
        ctx.shadowOffsetX = SHADOW_OFFX;
        ctx.shadowOffsetY = SHADOW_OFFY;
        ctx.shadowColor = "rgba(0,0,0," + SHADOW_ALPHA + ")";
        ctx.strokeStyle = "rgba(0,0,0," + SHAPE_FILL_ALPHA + ")";
        ctx.fillStyle = "rgba(255,255,255," + SHAPE_ALPHA + ")";
        for (var i = 0; i < len - 1; i++) { // for each point minus 1
            var p1 = line[i];
            var p2 = line[i + 1]; // get the point and one ahead
            if (p1.dir && p2.dir) { // do both points have a direction
                // divide the distance between the points by 5 and draw each sub section
                for (var k = 0; k < p1.len; k += p1.len / SUB_SECTIONS) {
                    // get the points between mouse samples
                    var x = p1.x + p1.nx * k;
                    var y = p1.y + p1.ny * k;
                    var kk = k / p1.len; // get normalised distance
                    // tween direction but need to check cyclic
                    if (p1.dir > Math.PI * 1.5 && p2.dir < Math.PI / 2) {
                        var dir = ((p2.dir + Math.PI * 2) - p1.dir) * kk + p1.dir;
                    } else
                    if (p2.dir > Math.PI * 1.5 && p1.dir < Math.PI / 2) {
                        var dir = ((p2.dir - Math.PI * 2) - p1.dir) * kk + p1.dir;
                    } else {
                        var dir = (p2.dir - p1.dir) * kk + p1.dir;
                    }

                    // get size dependent on direction
                    var size = (Math.abs(Math.sin(dir + BIG_DIR)) + SIZE_MIN) * SIZE_MULT;
                    // caculate the transform requiered.
                    var xdx = Math.cos(dir) * size;
                    var xdy = Math.sin(dir) * size;
                    // set the line width to the invers scale so it remains constant
                    ctx.lineWidth = SHAPE_LINE_WIDTH * (1 / size); // make sure that the line width does not scale
                    // set the transform
                    ctx.setTransform(xdx, xdy, -xdy, xdx, x, y);
                    // draw the shape
                    roundedRect(ctx, -SHAPE_LENGTH / 2 - SHAPE_TRAIL, -SHAPE_WIDTH / 2, SHAPE_LENGTH, SHAPE_WIDTH, SHAPE_ROUNDING);
                    // fill and stroke
                    ctx.fill();
                    ctx.stroke();
                }
            }
        }
        // restore transform
        ctx.setTransform(1, 0, 0, 1, 0, 0);
    }

    // update function will try 60fps but setting will slow this down.    
    function update(){
        // restore transform
        ctx.setTransform(1, 0, 0, 1, 0, 0);
        // clear
        ctx.clearRect(0, 0, w, h);
        // get line length
        var len = line.length;
        
        if (mouse.buttonRaw !== 1) { // button up so draw all onto image
            drawStroke(image.ctx, line)
            line = [];
        } else {
            // remove trailing line segments that are no longer being smoothed
            if (len > SMOOTH_MAX * 2) {
                var a = line.splice(0, SMOOTH_MAX - 1)
                    a.push(line[0]);
                drawStroke(image.ctx, a)
            }
        }
        // draw background image
        ctx.drawImage(image, 0, 0);

        // is the button down
        if (mouse.buttonRaw === 1) {
            // if more than one point
            if (line.length > 0) {
                // only add a point if mouse has moved.
                if (mouse.x !== line[line.length - 1].x || mouse.y !== line[line.length - 1].y) {
                    line.push({
                        x : mouse.x,
                        y : mouse.y,
                        s : 0
                    });
                }
            } else {
                // add a point if no points exist
                line.push({
                    x : mouse.x,
                    y : mouse.y,
                    s : 0
                });
            }
        }
        // get number of points
        var len = line.length; 
        
        
        if(mouse.buttonRaw === 1){  // mouse down the do simple running average smooth
            // This smooth will continue to refine points untill the it is outside the
            // smoothing range/
            for (var i = 0; i < len - 3; i++) {
                var p1 = line[i];
                var p2 = line[i + 1];
                var p3 = line[i + 2];
                if (p1.s < SMOOTH_MAX) {
                    p1.s += 1;
                    p2.x = ((p1.x + p3.x) / 2 + p2.x * 2) / 3;
                    p2.y = ((p1.y + p3.y) / 2 + p2.y * 2) / 3;
                }
            }
            // caculate the direction, length and normalised vector for
            // each line segment and add to the point
            for(var i = 0; i < len-1; i++){
                var p1 = line[i];
                var p2 = line[i + 1];
                var nx = p2.x - p1.x;
                var ny = p2.y - p1.y;
                p1.dir = ((Math.atan2(ny, nx) % PI2) + PI2) % PI2; // get direction
                p1.len = hypot(nx, ny); // get length
                p1.nx = nx / p1.len; // get normalised vector
                p1.ny = ny / p1.len;
    
            }
            // draw the line points onto the canvas.
            drawStroke(ctx,line)
        }
        if((mouse.buttonRaw & 4)=== 4){
            line = [];
            image.ctx.clearRect(0,0,w,h);
            ctx.clearRect(0,0,w,h);
            mouse.buttonRaw = 0;
        }
        if(!STOP){
            requestAnimationFrame(update);
        }else{
            var can = document.getElementById("canv");
            if(can !== null){
                document.body.removeChild(can);
            }     
            STOP = false;   
            
        }
    }

    update();

}
var STOP = false;  // flag to tell demo app to stop 
function resizeEvent(){
    var waitForStopped = function(){
        if(!STOP){  // wait for stop to return to false
            demo();
            return;
        }
        setTimeout(waitForStopped,200);
    }
    STOP = true;
    setTimeout(waitForStopped,100);
}
window.addEventListener("resize",resizeEvent);
demo();
/** FrameUpdate.js end **/
Blindman67
  • 51,134
  • 11
  • 73
  • 136
  • This is extremely helpful and exactly what I have been looking for - can't find anythign like this anywhere. Thank you very much. – eirikrl Jan 20 '16 at 17:46