Реализация изменения толщины кисти в холсте HTML5 (пример, который нужно эмулировать внутри)

Я хотел бы указать правильное направление с точки зрения алгоритма в демонстрации ниже здесь http://sta.sh/muro//. Также инструменты холста, которые он использует - т. Е. Рисование линий или рисование множества дуг и т. Д.

В частности, я хочу эмулировать поворот кисти, чтобы весь «мазок кисти» был толще. На изображении показаны настройки кисти, которым я хочу подражать.

В конечном счете, я хотел бы создать кисть, толщина которой менялась бы при повороте, точно так же, как показано ниже.

введите здесь описание изображения


person eirikrl    schedule 16.01.2016    source источник


Ответы (1)


Для этого вам нужно записать точки мыши, когда кнопка нажата. Затем вам нужно проверить каждый сегмент линии, чтобы найти направление, длину линии и нормализованный вектор этого сегмента линии, чтобы вы могли пересэмплировать выборки мыши.

Итак, если у вас есть набор точек, взятых с мыши, следующие детали получат необходимые детали.

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

Когда у вас есть детали каждого сегмента линии, можно просто изменить параметры рисования в соответствии со значениями.

Я добавил демо. Это настолько близко, насколько я мог побеспокоиться, чтобы добраться до примера, поскольку предоставленная вами строка не давала возможности рисовать то, что показывало ваше изображение. На изображении показано, что они также используют тень и выполняют сэмплирование с помощью мыши. Скругленный прямоугольник, который они нарисовали, мог бы быть изображением, которое было бы намного быстрее, чем нарисованный прямоугольник, который я сделал. Я также немного сгладил линию, поэтому есть небольшая задержка между рисунком и его завершением и установкой фонового изображения.

В верхней части демо находится набор констант, управляющих различными настройками. Добавление параметров ввода и проверка заняло бы слишком много времени, поэтому поэкспериментируйте с ними, если вы найдете код полезным.

Извините, код беспорядочный, но это всего лишь пример, который вам придется разобрать самостоятельно.

/** 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 **/

person Blindman67    schedule 18.01.2016
comment
Это чрезвычайно полезно и именно то, что я искал - нигде не могу найти ничего подобного. Большое спасибо. - person eirikrl; 20.01.2016