用一条线把一个canvas分成两个新的canvas

Using a line to divide a canvas into two new canvases

我希望允许用户将一个现有的 canvas 分成两个 canvas 以他们想要的任何方向。

我知道如何让用户画一条线,我也知道如何将一个 canvas 的图像数据复制到两个新的图像数据上,但是我怎样才能只复制其中一个上的相关颜色数据用户绘制的线的一侧到其各自的 canvas?

例如,在下面的演示中,我希望 canvas 为 "cut",其中白线为:

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>

TL;DR the demo.


我发现这样做的最佳方法是 1) 为 canvas' 边界边缘(或外部)的线计算 "end points",2) 创建两个*使用步骤 1 中生成的线的端点和 canvas' 四个角的多边形,以及 3) 将原始 canvas' 图像数据分成两个基于我们创建的多边形。

* 我们实际上创建了一个,但是"second"是原始canvas的剩余部分。


1) 计算终点

您可以使用一种非常便宜的算法来计算给定起始坐标、x 和 y 差值(即斜率)以及 canvas 的边界的一些端点。我使用了以下内容:

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

其中

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

2) 创建两个多边形

为了创建多边形,我使用了 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];
}

连同我自己的一些逻辑:

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) 分割原始canvas'图像数据

现在我们有了多边形,剩下的就是将这些数据放入新的 canvases 中。最简单的方法是使用 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();

所有这些加起来就是 this demo