多角形pathを直線で分割する

多角形pathを直線で分割する | ひまあそび-ミニゲーム hi0a.com

view source

JavaScript

document.title = '多角形pathを直線で分割する';
//

let { canvas, ctx, cx, cy, w, h, min, demo } = readyCanvas();

let paths = [];
let clickPoints = []; // [p1, p2] を保持
canvas.addEventListener("click", (e) => {
    const rect = canvas.getBoundingClientRect();  // CSS上の見た目サイズ
    const scaleX = canvas.width / rect.width;
    const scaleY = canvas.height / rect.height;

    const p = {
        x: (e.clientX - rect.left) * scaleX,
        y: (e.clientY - rect.top) * scaleY
    };

    console.log("Click at (corrected):", p);
    clickPoints.push(p);

    if (clickPoints.length === 2) {
        console.log("Splitting...");
        splitAndReplacePaths(clickPoints[0], clickPoints[1]);
        drawAllPaths();
        clickPoints = [];
    }
});




class Path {
    constructor(points, color = 'black') {
        this.points = points;
        this.color = color;
    }

    draw(ctx) {
        if (this.points.length < 2) return;
        ctx.beginPath();
        ctx.moveTo(this.points[0].x, this.points[0].y);
        for (let i = 1; i < this.points.length; i++) {
            ctx.lineTo(this.points[i].x, this.points[i].y);
        }
        ctx.closePath();
        ctx.strokeStyle = this.color;
        ctx.stroke();
    }

}

// p1→p2 を通る長い直線を作る
function extendLine(p1, p2, length = 2000) {
    const dx = p2.x - p1.x;
    const dy = p2.y - p1.y;
    const mag = Math.hypot(dx, dy);

    const unitX = dx / mag;
    const unitY = dy / mag;

    return {
        a: { x: p1.x - unitX * length, y: p1.y - unitY * length },
        b: { x: p1.x + unitX * length, y: p1.y + unitY * length }
    };
}
function findBoundaryIntersections(path, extendedLine) {
    const intersections = [];

    const points = path.points;
    for (let i = 0; i < points.length; i++) {
        const a = points[i];
        const b = points[(i + 1) % points.length];

        const intersect = getLineIntersection(extendedLine.a, extendedLine.b, a, b);
        if (intersect) {
            intersections.push({ point: intersect, index: i });
        }
    }

    return intersections;
}


function splitAndReplacePaths(p1, p2) {
    const newPaths = [];
    const splitGapX = 20;
    const splitGapY = 10;

    for (let path of paths) {
        const extended = extendLine(p1, p2);
        const intersections = findBoundaryIntersections(path, extended);

        if (intersections.length !== 2) {
            newPaths.push(path);
            continue;
        }

        const [i1, i2] = intersections;

        const angle = Math.atan2(p2.y - p1.y, p2.x - p1.x);
        const isVertical = Math.abs(Math.cos(angle)) < Math.abs(Math.sin(angle));
        const offsetAmount = isVertical ? splitGapY / 2 : splitGapX / 2;

        // 法線(p1→p2 に左回り90°)
        const normal = {
            x: Math.cos(angle + Math.PI / 2),
            y: Math.sin(angle + Math.PI / 2)
        };

        // 重心が分割線の左右どちら側にあるかを判定
        const centroid = getCentroid(path.points);
        const side = whichSide(p1, p2, centroid); // >0: 左側, <0: 右側

        // オフセット交点を生成
        const p1Plus  = addOffset(i1.point, normal, +offsetAmount);
        const p1Minus = addOffset(i1.point, normal, -offsetAmount);
        const p2Plus  = addOffset(i2.point, normal, +offsetAmount);
        const p2Minus = addOffset(i2.point, normal, -offsetAmount);

        // blue = centroid 側(左側), red = 反対側(右側)
        let bluePoly, redPoly;
        if (side > 0) {
            bluePoly = buildPathBetween(path.points, i1.index, i2.index, p1Plus, p2Plus);
            redPoly  = buildPathBetween(path.points, i2.index, i1.index, p2Minus, p1Minus);
        } else {
            bluePoly = buildPathBetween(path.points, i2.index, i1.index, p2Plus, p1Plus);
            redPoly  = buildPathBetween(path.points, i1.index, i2.index, p1Minus, p2Minus);
        }

        newPaths.push(new Path(bluePoly, 'blue'));
        newPaths.push(new Path(redPoly, 'red'));
    }

    paths = newPaths;
    drawAllPaths();
}

// 補助関数(必要に応じて定義)
function whichSide(p1, p2, point) {
    const dx1 = p2.x - p1.x;
    const dy1 = p2.y - p1.y;
    const dx2 = point.x - p1.x;
    const dy2 = point.y - p1.y;
    return dx1 * dy2 - dy1 * dx2;
}

function addOffset(point, normal, amount) {
    return {
        x: point.x + normal.x * amount,
        y: point.y + normal.y * amount
    };
}


// コマの重心を計算
function getCentroid(points) {
    let x = 0, y = 0;
    for (let p of points) {
        x += p.x;
        y += p.y;
    }
    return { x: x / points.length, y: y / points.length };
}


function getLineIntersection(p1, p2, p3, p4) {
    const x1 = p1.x, y1 = p1.y;
    const x2 = p2.x, y2 = p2.y;
    const x3 = p3.x, y3 = p3.y;
    const x4 = p4.x, y4 = p4.y;

    const denom = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);

    if (Math.abs(denom) < 0.00001) {
        return null; // 平行
    }

    const px = ((x1 * y2 - y1 * x2) * (x3 - x4) - 
                (x1 - x2) * (x3 * y4 - y3 * x4)) / denom;
    const py = ((x1 * y2 - y1 * x2) * (y3 - y4) - 
                (y1 - y2) * (x3 * y4 - y3 * x4)) / denom;

    // 線分上にあるか確認(誤差に寛容)
    function isOnSegment(px, py, xStart, yStart, xEnd, yEnd) {
        const buffer = 0.5;
        return (
            Math.min(xStart, xEnd) - buffer <= px &&
            px <= Math.max(xStart, xEnd) + buffer &&
            Math.min(yStart, yEnd) - buffer <= py &&
            py <= Math.max(yStart, yEnd) + buffer
        );
    }

    if (
        isOnSegment(px, py, x1, y1, x2, y2) &&
        isOnSegment(px, py, x3, y3, x4, y4)
    ) {
        return { x: px, y: py };
    }

    return null;
}


function buildPathBetween(points, startIdx, endIdx, iPoint1, iPoint2) {
    const result = [];
    result.push(iPoint1);
    let idx = (startIdx + 1) % points.length;
    while (idx !== (endIdx + 1) % points.length) {
        result.push(points[idx]);
        idx = (idx + 1) % points.length;
    }
    result.push(iPoint2);
    return result;
}


const frame = new Path([
    { x: 40, y: 40 },
    { x: 40, y: 600 },
    { x: 800, y: 600 },
    { x: 800, y: 40 },
]);

paths.push(frame);

function drawAllPaths() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    for (const path of paths) {
        path.draw(ctx);
    }

}

drawAllPaths();

CSS

HTML

ページのソースを表示 : Ctrl+U , DevTools : F12

view-source:https://hi0a.com/demo/-js/js-path-split/

ABOUT

hi0a.com 「ひまあそび」は無料で遊べるミニゲームや便利ツールを公開しています。

プログラミング言語の動作デモやWEBデザイン、ソースコード、フロントエンド等の開発者のための技術を公開しています。

必要な機能の関数をコピペ利用したり勉強に活用できます。

プログラムの動作サンプル結果は画面上部に出力表示されています。

環境:最新のブラウザ GoogleChrome / Windows / Android / iPhone 等の端末で動作確認しています。

画像素材や音素材は半分自作でフリー素材配布サイトも利用しています。LINK参照。

仕様変更、API廃止等、実験途中で動かないページもあります。