前言
這是工作中的一個需求,要求使用PixiJS來進行繪製,所以這裏就不使用原生Canvas或是其他繪製工具了。但歸根結底,原理都是一樣的。
正文
多邊形的布爾運算( boolean operation on polygons)包括: intersection交集, union並集, difference差異, xor 異或。
具體表現如,Photoshop中的選區操作:
Sketch中的圖形疊加效果:
實現思路
根據兩個多邊形的所有頂點的座標組成GeoJSON,然後通過martinez這個庫計算出布爾運算之後的GeoJSON,再解析生成最終效果的多邊形定點數組,最後繪製數組生成圖像。(下面我使用了PIXI來繪製圖形)
API:
.intersection(<Geometry>, <Geometry>) => <Geometry>
.union(<Geometry>, <Geometry>) => <Geometry>
.diff(<Geometry>, <Geometry>) => <Geometry>
.xor(<Geometry>, <Geometry>) => <Geometry>
關於由頂點數組繪製多邊形,可參閱博文【繪製】HTML5 Canvas 實現任意圓角多邊形
效果展示
缺點
只能應用於直線邊,對於圓角/弧度邊無法計算
代碼實現
本身我是不打算貼出代碼的,因爲代碼中複用了之前的文章的“圓角多邊形”的radius效果,產生了一定的代碼冗餘,但是考慮到讀者可能會在某些地方有所顧慮,還是貼上了,請大家有選擇的翻閱。
index.js
import React, {useEffect, useState} from "react";
import * as PIXI from 'pixi.js'
import {intersection, union, xor, diff, drawAll} from './utils.js'
export default function PloygonsBooleanDemo() {
const [json1, setJson1] = useState([
// 正方形
{
x: 20,
y: 20,
radius: 0
},
{
x: 110,
y: 10,
radius: 0
},
{
x: 90,
y: 90,
radius: 0
},
{
x: 20,
y: 80,
radius: 0
}
]);
const [json2, setJson2] = useState([
{
x: 50,
y: 50,
radius: 0
},
{
x: 110,
y: 50,
radius: 0
},
{
x: 110,
y: 110,
radius: 0
},
{
x: 50,
y: 110,
radius: 0
}
]);
const app = new PIXI.Application({
antialias: true, // default: false
});
let graphics;
useEffect(() => {
document.body.appendChild(app.view);
graphics = new PIXI.Graphics();
app.stage.addChild(graphics);
}, []);
// 初始化圖形
let initGraphics = () => {
app.stage.removeChild(graphics);
graphics = new PIXI.Graphics();
app.stage.addChild(graphics);
};
return (<div>
<button onClick={() => {
initGraphics();
drawAll(intersection(json1, json2), graphics);
}
}>交集
</button>
<button onClick={() => {
initGraphics();
drawAll(union(json1, json2), graphics);
}}>並集
</button>
<button onClick={() => {
initGraphics();
drawAll(diff(json1, json2), graphics);
}}>除去一層1
</button>
<button onClick={() => {
initGraphics();
drawAll(diff(json2, json1), graphics);
}}>除去一層2
</button>
<button onClick={() => {
initGraphics();
drawAll(xor(json1, json2), graphics);
}}>差集
</button>
</div>)
}
utils.js
import * as martinez from "martinez-polygon-clipping";
export function geoJSON2RadiusShapeJSON(geoJSON) {
let radiusShapeJSON = [];
geoJSON.geometry.coordinates.forEach((element)=>{
let childRadiusShapeJSON = [];
element[0].forEach((element, index, array) => {
if (index !== array.length - 1) {
childRadiusShapeJSON.push({x: element[0], y: element[1], radius: 0});
}
});
radiusShapeJSON.push(childRadiusShapeJSON)
})
return radiusShapeJSON;
}
// 將繪製所用的json轉化爲GoeJSON
export function radiusShapeJSON2GeoJSON(radiusShapeJSON) {
let coordinatesArr = [];
radiusShapeJSON.forEach((element) => {
coordinatesArr.push([element.x, element.y]);
});
coordinatesArr.push([radiusShapeJSON[0].x, radiusShapeJSON[0].y]);
return {
"type": "Feature",
"geometry": {
"type": "Polygon", "coordinates": [
coordinatesArr
]
}
}
}
//交集
export function intersection(radiusShapeJSON1, radiusShapeJSON2) {
return geoJSON2RadiusShapeJSON(
{
"type": "Feature",
"geometry": {
"type": "Polygon", "coordinates":
martinez.intersection(
radiusShapeJSON2GeoJSON(radiusShapeJSON1).geometry.coordinates,
radiusShapeJSON2GeoJSON(radiusShapeJSON2).geometry.coordinates
)
}
}
)
}
//差
export function diff(radiusShapeJSON1, radiusShapeJSON2) {
return geoJSON2RadiusShapeJSON(
{
"type": "Feature",
"geometry": {
"type": "Polygon", "coordinates":
martinez.diff(
radiusShapeJSON2GeoJSON(radiusShapeJSON1).geometry.coordinates,
radiusShapeJSON2GeoJSON(radiusShapeJSON2).geometry.coordinates
)
}
}
)
}
//並集
export function union(radiusShapeJSON1, radiusShapeJSON2) {
return geoJSON2RadiusShapeJSON(
{
"type": "Feature",
"geometry": {
"type": "Polygon", "coordinates":
martinez.union(
radiusShapeJSON2GeoJSON(radiusShapeJSON1).geometry.coordinates,
radiusShapeJSON2GeoJSON(radiusShapeJSON2).geometry.coordinates
)
}
}
)
}
//異或
export function xor(radiusShapeJSON1, radiusShapeJSON2) {
return geoJSON2RadiusShapeJSON(
{
"type": "Feature",
"geometry": {
"type": "Polygon", "coordinates":
martinez.xor(
radiusShapeJSON2GeoJSON(radiusShapeJSON1).geometry.coordinates,
radiusShapeJSON2GeoJSON(radiusShapeJSON2).geometry.coordinates
)
}
}
)
}
/**
* 已知A、B、C三點座標,求半徑爲r的內切圓與AC的切點座標
* @param A
* @param B
* @param C
* @param r
* @returns {{x: *, y: *}}
*/
export function _toOrigin(A, B, C, r) {
let {pow, acos, tan} = Math;
//ab的長度
const ab = pow(pow(A.x - B.x, 2) + pow(A.y - B.y, 2), 0.5);
//bc的長度
const bc = pow(pow(C.x - B.x, 2) + pow(C.y - B.y, 2), 0.5);
//B的角度
const angleB = acos(((A.x - B.x) * (C.x - B.x) + (A.y - B.y) * (C.y - B.y)) / (ab * bc));
//BM的長度,即B到切線的長度
const M = r / tan(angleB / 2);
const k = (C.y - B.y) / (C.x - B.x);
if (k === '-Infinity') {
return {
x: B.x,
y: B.y - M
};
} else if (k === 'Infinity') {
return {
x: B.x,
y: B.y + M
};
} else {
let i = 1;
if (1 / k === '-Infinity') {
i = -1;
}
if (k < 0) {
if ((C.y - B.y) < 0) {
i = 1;
} else {
i = -1;
}
}
if (k > 0) {
if (C.y - B.y < 0) {
i = -1;
}
}
return {
x: B.x + (1 / pow(1 + k * k, 0.5)) * M * i,
y: B.y + (k / pow(1 + k * k, 0.5)) * M * i
};
}
}
/**
* 將json點集轉化爲可操作點handlePoints
*/
export function json2HandlePoint(json) {
//矯正radius
fixRadius(json);
//將所有可操作點保存handlePoint變量中
let handlePoints = [];
//第一個點
handlePoints.push({
start: {x: json[json.length - 1].x, y: json[json.length - 1].y},
middle: {x: json[0].x, y: json[0].y},
end: {x: json[1].x, y: json[1].y},
radius: json[0].radius
});
//中間所有點
for (let i = 1; i < json.length - 1; i++) {
handlePoints.push({
start: {x: json[i - 1].x, y: json[i - 1].y},
middle: {x: json[i].x, y: json[i].y},
end: {x: json[i + 1].x, y: json[i + 1].y},
radius: json[i].radius
});
}
//最後一個點
handlePoints.push({
start: {x: json[json.length - 2].x, y: json[json.length - 2].y},
middle: {x: json[json.length - 1].x, y: json[json.length - 1].y},
end: {x: json[0].x, y: json[0].y},
radius: json[json.length - 1].radius
});
return handlePoints;
}
/**
* 將異常json的radius按照Sketch模式矯正
* sketch圓角異常數據處理方法:
用戶可設定radius值,當出現不合法值的時候,原值不變,圖像自行優化
說明:如100*50的矩形,可以在50像素的邊對應的角,各設置成radius=25,也可以其中一個設爲0,另一個設爲50,總之就是radius1+radius2=50。當radius1+radius2>50的時候,在繪製裏,radius_min不變,radius_max = 50/2,而在json數據中不改變這些數據,已保存用戶的數據記錄
*/
export function fixRadius(json) {
//第一個點
_fixRadius(json[json.length - 1], json[0], json[1]);
for (let i = 1; i < json.length - 1; i++) {
_fixRadius(json[i - 1], json[i], json[i + 1]);
}
//最後一個點
_fixRadius(json[json.length - 2], json[json.length - 1], json[0]);
}
export function _fixRadius(left, curr, right) {
let {pow, acos, tan} = Math;
const leftLine = pow(pow(left.x - curr.x, 2) + pow(left.y - curr.y, 2), 0.5);
const rightLine = pow(pow(right.x - curr.x, 2) + pow(right.y - curr.y, 2), 0.5);
//兩邊最短邊
const minLine = leftLine < rightLine ? leftLine : rightLine;
//中間角度
const angleCurr = acos(((left.x - curr.x) * (right.x - curr.x) + (left.y - curr.y) * (right.y - curr.y)) / (leftLine * rightLine));
//理論上能實現的最大弧度半徑
let maxRadius = minLine * tan(angleCurr / 2);
/*
當左右都爲0且,當前radius超過理論最大弧度半徑時
將radius縮減到最大弧度半徑
*/
if (left.radius === 0 && right.radius === 0) {
if (curr.radius > maxRadius) {
curr.radius = maxRadius;
}
}
/*
當左右任意一邊有值,且radius超過理論最大弧度半徑的一半時
將radius縮減到最大弧度半徑的一半
*/
if (left.radius > 0 || right.radius > 0) {
if (curr.radius > maxRadius / 2) {
curr.radius = maxRadius / 2;
}
}
}
export function drawShape(handlePoints,graphics) {
//重置路徑
graphics.beginFill(0x66CCFF);
let currPoint = undefined;
const origin = _toOrigin(handlePoints[0].start, handlePoints[0].middle, handlePoints[0].end, handlePoints[0].radius);
graphics.moveTo(origin.x, origin.y);
for (let i = 1; i < handlePoints.length; i++) {
currPoint = handlePoints[i];
//繪製圓弧
graphics.arcTo(currPoint.middle.x, currPoint.middle.y, currPoint.end.x, currPoint.end.y, currPoint.radius);
}
currPoint = handlePoints[0];
graphics.arcTo(currPoint.middle.x, currPoint.middle.y, currPoint.end.x, currPoint.end.y, currPoint.radius);
graphics.endFill();
graphics.closePath();
}
//繪製經布爾運算後的圖形
export function drawAll(arr,graphics) {
arr.forEach(element => {
drawShape(json2HandlePoint(element),graphics)
})
};