javascriptcanvaslinegetimagedata

Using a line to divide a canvas into two new canvases


I'm looking to allow users to slice an existing canvas into two canvases in whatever direction they would like.

I know how to allow the user to draw a line and I also know how to copy the image data of one canvas onto two new ones, but how can I copy only the relevant color data on either side of the user-drawn line to its respective canvas?

For example, in the following demo I'd like the canvas to be "cut" where the white line is:

const canvas = document.querySelector("canvas"),
	ctx = canvas.getContext("2d");

const red = "rgb(104, 0, 0)",
	lb = "rgb(126, 139, 185)",
  db = "rgb(20, 64, 87)";

var width,
	  height,
	  centerX,
	  centerY,
	  smallerDimen;

var canvasData,
	  inCoords;
    
function sizeCanvas() {
	  width = canvas.width = window.innerWidth;
	  height = canvas.height = window.innerHeight;
    centerX = width / 2;
	  centerY = height / 2;
    smallerDimen = Math.min(width, height);
}

function drawNormalState() {
    // Color the bg
    ctx.fillStyle = db;
    ctx.fillRect(0, 0, width, height);

    // Color the circle
    ctx.arc(centerX, centerY, smallerDimen / 4, 0, Math.PI * 2, true);
    ctx.fillStyle = red;
    ctx.fill();
    ctx.lineWidth = 3;
    ctx.strokeStyle = lb;
    ctx.stroke();

    // Color the triangle
    ctx.beginPath();
    ctx.moveTo(centerX + smallerDimen / 17, centerY - smallerDimen / 10);
    ctx.lineTo(centerX + smallerDimen / 17, centerY + smallerDimen / 10);
    ctx.lineTo(centerX - smallerDimen / 9, centerY);
    ctx.fillStyle = lb;
    ctx.fill();
    ctx.closePath();
    
    screenshot();
    
    ctx.beginPath();
    ctx.strokeStyle = "rgb(255, 255, 255)";
    ctx.moveTo(width - 20, 0);
    ctx.lineTo(20, height);
    ctx.stroke();
    ctx.closePath();
}

function screenshot() {
	  canvasData = ctx.getImageData(0, 0, width, height).data;
}

function init() {
    sizeCanvas();
    drawNormalState();
}

init();
body {
    margin: 0;
}
<canvas></canvas>


Solution

  • TL;DR the demo.


    The best way I've found to do this is to 1) calculate "end points" for the line at the edge of (or outside) the canvas' bounds, 2) create two* polygons using the end points of the line generated in step 1 and the canvas' four corners, and 3) divide up the original canvas' image data into two new canvases based on the polygons we create.

    * We actually create one, but the "second" is the remaining part of the original canvas.


    1) Calculate the end points

    You can use a very cheap algorithm to calculate some end points given a start coordinate, x and y difference (i.e. slope), and the bounds for the canvas. I used the following:

    function getEndPoints(startX, startY, xDiff, yDiff, maxX, maxY) {
        let currX = startX, 
            currY = startY;
        while(currX > 0 && currY > 0 && currX < maxX && currY < maxY) {
            currX += xDiff;
            currY += yDiff;
        }
        let points = {
            firstPoint: [currX, currY]
        };
    
        currX = startX;
        currY = startY;
        while(currX > 0 && currY > 0 && currX < maxX && currY < maxY) {
            currX -= xDiff;
            currY -= yDiff;
        }
        points.secondPoint = [currX, currY];
    
        return points;
    }
    

    where

    let xDiff = firstPoint.x - secondPoint.x,
        yDiff = firstPoint.y - secondPoint.y;
    

    2) Create two polygons

    To create the polygons, I make use of Paul Bourke's Javascript line intersection:

    function intersect(point1, point2, point3, point4) {
        let x1 = point1[0], 
            y1 = point1[1], 
            x2 = point2[0], 
            y2 = point2[1], 
            x3 = point3[0], 
            y3 = point3[1], 
            x4 = point4[0], 
            y4 = point4[1];
    
        // Check if none of the lines are of length 0
        if((x1 === x2 && y1 === y2) || (x3 === x4 && y3 === y4)) {
            return false;
        }
    
        let denominator = ((y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1));
    
        // Lines are parallel
        if(denominator === 0) {
            return false;;
        }
    
        let ua = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / denominator;
        let ub = ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / denominator;
    
        // is the intersection along the segments
        if(ua < 0 || ua > 1 || ub < 0 || ub > 1) {
            return false;
        }
    
        // Return a object with the x and y coordinates of the intersection
        let x = x1 + ua * (x2 - x1);
        let y = y1 + ua * (y2 - y1);
    
        return [x, y];
    }
    

    Along with some of my own logic:

    let origin = [0, 0],
        xBound = [width, 0],
        xyBound = [width, height],
        yBound = [0, height];
    
    let polygon = [origin];
    
    // Work clockwise from 0,0, adding points to our polygon as appropriate
    
    // Check intersect with top bound
    let topIntersect = intersect(origin, xBound, points.firstPoint, points.secondPoint);
    if(topIntersect) {
        polygon.push(topIntersect);
    }
    if(!topIntersect) {
        polygon.push(xBound);
    }
    
    // Check intersect with right
    let rightIntersect = intersect(xBound, xyBound, points.firstPoint, points.secondPoint);
    if(rightIntersect) {
        polygon.push(rightIntersect);
    }
    if((!topIntersect && !rightIntersect)
    || (topIntersect && rightIntersect)) {
        polygon.push(xyBound);
    }
    
    
    // Check intersect with bottom
    let bottomIntersect = intersect(xyBound, yBound, points.firstPoint, points.secondPoint);
    if(bottomIntersect) {
        polygon.push(bottomIntersect);
    }
    if((topIntersect && bottomIntersect)
    || (topIntersect && rightIntersect)) {
        polygon.push(yBound);
    }
    
    // Check intersect with left
    let leftIntersect = intersect(yBound, origin, points.firstPoint, points.secondPoint);
    if(leftIntersect) {
        polygon.push(leftIntersect);
    }
    

    3) Divide up the original canvas' image data

    Now that we have our polygon, all that's left is putting this data into new canvases. The easiest way to do this is to use canvas' ctx.drawImage and ctx.globalCompositeOperation.

    // Use or create 2 new canvases with the split original canvas
    let newCanvas1 = document.querySelector("#newCanvas1");
    if(newCanvas1 == null) {
        newCanvas1 = document.createElement("canvas");
        newCanvas1.id = "newCanvas1";
        newCanvas1.width = width;
        newCanvas1.height = height;
        document.body.appendChild(newCanvas1);
    }
    let newCtx1 = newCanvas1.getContext("2d");
    newCtx1.globalCompositeOperation = 'source-over';
    newCtx1.drawImage(canvas, 0, 0);
    newCtx1.globalCompositeOperation = 'destination-in';
    newCtx1.beginPath();
    newCtx1.moveTo(polygon[0][0], polygon[0][1]);
    for(let item = 1; item < polygon.length; item++) {
        newCtx1.lineTo(polygon[item][0], polygon[item][1]);
    }
    newCtx1.closePath();
    newCtx1.fill();
    
    let newCanvas2 = document.querySelector("#newCanvas2");
    if(newCanvas2 == null) {
        newCanvas2 = document.createElement("canvas");
        newCanvas2.id = "newCanvas2";
        newCanvas2.width = width;
        newCanvas2.height = height;
        document.body.appendChild(newCanvas2);
    }
    let newCtx2 = newCanvas2.getContext("2d");
    newCtx2.globalCompositeOperation = 'source-over';
    newCtx2.drawImage(canvas, 0, 0);
    newCtx2.globalCompositeOperation = 'destination-out';
    newCtx2.beginPath();
    newCtx2.moveTo(polygon[0][0], polygon[0][1]);
    for(let item = 1; item < polygon.length; item++) {
        newCtx2.lineTo(polygon[item][0], polygon[item][1]);
    }
    newCtx2.closePath();
    newCtx2.fill();
    

    All of that put together gives us this demo!