- A+
所属分类:Web前端
1 /* Polygon 多边形 2 3 parameter: 4 path: Array[x, y]; 5 6 attribute: 7 8 //只读属性 9 path: Array[x, y]; 10 11 method: 12 add(x, y): this; //x,y添加至path; 13 containsPoint(x, y): Bool; //x,y是否在多边形的内部(注意: 在路径上也返回 true) 14 merge(polygon): this; //合并; 假设polygon与this存在重叠的部分 15 16 */ 17 class Polygon{ 18 19 #position = null; 20 #path2D = null; 21 22 get path(){ 23 24 return this.#position; 25 26 } 27 28 constructor(path = []){ 29 this.#position = path; 30 31 this.#path2D = new Path2D(); 32 33 var len = path.length; 34 if(len >= 2){ 35 if(len % 2 !== 0){ 36 len -= 1; 37 path.splice(len, 1); 38 } 39 40 const con = this.#path2D; 41 con.moveTo(path[0], path[1]); 42 for(let k = 2; k < len; k+=2) con.lineTo(path[k], path[k+1]); 43 44 } 45 46 } 47 48 add(x, y){ 49 this.#position.push(x, y); 50 this.#path2D.lineTo(x, y); 51 return this; 52 } 53 54 containsPoint(x, y){ 55 56 return UTILS.emptyContext.isPointInPath(this.#path2D, x, y); 57 58 } 59 60 toPoints(){ 61 const path = this.path, len = path.length, result = []; 62 63 for(let k = 0; k < len; k += 2) result.push(new Point(path[k], path[k+1])); 64 65 return result; 66 } 67 68 toLines(){ 69 const path = this.path, len = path.length, result = []; 70 71 for(let k = 0, x = NaN, y; k < len; k += 2){ 72 73 if(isNaN(x)){ 74 x = path[k]; 75 y = path[k+1]; 76 continue; 77 } 78 79 const line = new Line(x, y, path[k], path[k+1]); 80 81 x = line.x1; 82 y = line.y1; 83 84 result.push(line); 85 86 } 87 88 return result; 89 } 90 91 merge(polygon){ 92 93 const linesA = this.toLines(), linesB = polygon.toLines(), nodes = [], newLines = []; 94 95 //收集所有的交点 (保存至 line) 96 for(let k = 0, lenB = linesB.length, lenA = linesA.length, point = new Point(), pointB = new Point(); k < lenA; k++){ 97 const lineA = linesA[k]; 98 lineA.nodes = []; 99 100 for(let i = 0; i < lenB; i++){ 101 const lineB = linesB[i]; 102 if(lineB.nodes === undefined) lineB.nodes = []; 103 if(lineA.intersectPoint(lineB, point) === point){ 104 const node = { 105 lineA: lineA, 106 lineB: lineB, 107 point: point.clone(), 108 disA: point.distanceCompare(pointB.set(lineA.x, lineA.y)), 109 disB: point.distanceCompare(pointB.set(lineB.x, lineB.y)), 110 } 111 lineA.nodes.push(node); 112 lineB.nodes.push(node); 113 nodes.push(node); 114 } 115 116 } 117 118 } 119 120 //获取介入点 121 var startLine = null; 122 for(let k = 0, len = nodes.length, node; k < len; k++){ 123 node = nodes[k]; 124 if(node.lineA.nodes.length === 1 || node.lineA.nodes.length === 2 && 125 node.lineB.nodes.length === 1 || node.lineB.nodes.length === 2){ 126 startLine = node.lineA; 127 break; 128 } 129 } 130 131 if(startLine === null){ 132 console.warn('Polygon: 找不到介入点, 终止了合并'); 133 return newLines; 134 } 135 136 //交点以原点为目标排序 137 for(let k = 0, sotr = function (a,b){return a.disA - b.disA;}, countA = linesA.length; k < countA; k++) linesA[k].nodes.sort(sotr); 138 for(let k = 0, sotr = function (a,b){return a.disB - b.disB;}, countB = linesB.length; k < countB; k++) linesB[k].nodes.sort(sotr); 139 140 const result_loop = { 141 lines: linesA, 142 loopType: 'next', 143 line: startLine, 144 count: startLine.nodes.length, 145 indexed: linesA.indexOf(startLine), 146 }, 147 148 //遍历某条线 149 loop = (lines, index, loopType) => { 150 const length = lines.length, indexed = index, model = lines === linesA ? polygon : this; 151 152 var line = lines[index]; 153 154 while(true){ 155 if(loopType === 'back' && newLines.includes(line) === false && model.containsPoint(line.x, line.y) === false) newLines.push(line); 156 if(loopType === 'next') index = index === length - 1 ? 0 : index + 1; 157 else if(loopType === 'back') index = index === 0 ? length - 1 : index - 1; 158 159 line = lines[index]; 160 if(loopType === 'next' && newLines.includes(line) === false && model.containsPoint(line.x, line.y) === false) newLines.push(line); 161 162 result_loop.count = line.nodes.length; 163 if(result_loop.count !== 0){ 164 result_loop.lines = lines; 165 result_loop.loopType = loopType; 166 result_loop.line = line; 167 result_loop.indexed = index; 168 return result_loop; 169 } 170 171 if(indexed === index) break; 172 173 } 174 175 }, 176 177 //更新或创建交点的索引 178 setNodeIndex = (lines, index, loopType) => { 179 const line = lines[index], count = line.nodes.length; 180 if(loopType === undefined) loopType = line.loopType; 181 else if(line.loopType === undefined) line.loopType = loopType; 182 183 if(loopType === undefined) return; 184 185 if(line.nodeIndex === undefined){ 186 line.nodeIndex = loopType === 'next' ? 0 : count - 1; 187 line.nodeState = count === 1 ? 'end' : 'start'; 188 line._loopType = loopType; 189 } 190 191 else{ 192 if(line.nodeState === 'end' || line.nodeState === ''){ 193 line.nodeState = ''; 194 return; 195 } 196 197 if(line._loopType === 'next'){ 198 line.nodeIndex += 1; 199 200 if(line.nodeIndex === count - 1) line.nodeState = 'end'; 201 else line.nodeState = 'run'; 202 } 203 204 else if(line._loopType === 'back'){ 205 line.nodeIndex -= 1; 206 207 if(line.nodeIndex === 0) line.nodeState = 'end'; 208 else line.nodeState = 'run'; 209 } 210 211 } 212 213 }, 214 215 //只有在跳线的时候才执行此方法, 如果跳线的话: 某条线上的交点必然在两端; 216 getLoopType = (lines, index, nodePoint) => { 217 const line = lines[index], lineNext = lines[index === lines.length - 1 ? 0 : index + 1], 218 219 model = lines === linesA ? polygon : this, 220 isLineBack = newLines.includes(line) === false && model.containsPoint(line.x, line.y) === false, 221 isLineNext = newLines.includes(lineNext) === false && model.containsPoint(lineNext.x, lineNext.y) === false; 222 223 if(isLineBack && isLineNext){ 224 const len = line.nodes.length; 225 if(len >= 2){ 226 if(line.nodes[len - 1].point.equals(nodePoint)) return 'next'; 227 else if(line.nodes[0].point.equals(nodePoint)) return 'back'; 228 } 229 230 else console.warn('路径复杂', line); 231 232 } 233 234 else if(isLineNext){ 235 return 'next'; 236 } 237 238 else if(isLineBack){ 239 return 'back'; 240 } 241 242 return ''; 243 }, 244 245 //处理拥有交点的线 246 computeNodes = v => { 247 if(v === undefined) return; 248 249 setNodeIndex(v.lines, v.lines.indexOf(v.line), v.loopType); 250 251 // 252 const node = v.line.nodes[v.line.nodeIndex], model = v.lines === linesA ? polygon : this; 253 if(newLines.includes(node.point) === false) newLines.push(node.point); 254 255 var lines, index, loopType, line; 256 257 // 258 const lineR = v.lines === linesA ? node.lineB : node.lineA, 259 linesR = lineR === node.lineA ? linesA : linesB; 260 setNodeIndex(linesR, linesR.indexOf(lineR)); 261 262 const nodeState = lineR.nodeState !== undefined ? lineR.nodeState : v.line.nodeState; 263 264 switch(nodeState){ 265 266 //不跳线 267 case 'run': 268 lines = v.lines; 269 index = v.indexed; 270 loopType = v.loopType; 271 computeNodes(loop(lines, index, loopType)); 272 break; 273 274 //跳线 275 case 'start': 276 case 'end': 277 lines = model === polygon ? linesB : linesA; 278 line = lines === linesA ? node.lineA : node.lineB; 279 index = lines.indexOf(line); 280 loopType = getLoopType(lines, index, node.point); 281 282 if(loopType !== ''){ 283 line.loopType = loopType; 284 //if(loopType === 'back' && newLines.includes(line) === false && model.containsPoint(line.x, line.y) === false) newLines.push(line); 285 computeNodes(loop(lines, index, loopType)); 286 } 287 break; 288 289 290 default: break; 291 292 } 293 294 } 295 296 computeNodes(result_loop); 297 298 return newLines; 299 } 300 301 }
部分代码
注意: Polygon 目前并不完美
parameter:
path: Array[x, y];
attribute:
//只读属性
path: Array[x, y];
method:
add(x, y): this; //x,y添加至path;
containsPoint(x, y): Bool; //x,y是否在多边形的内部(注意: 在路径上也返回 true)
merge(polygon): this; //合并; 假设polygon与this存在重叠的部分
这个时专门用于测试bug的ui控件:
1 /* PolygonTest 调试类 Polygon (可视化调试: 多边形之合并) 2 3 无任何属性, new就完事了! 4 5 */ 6 class PolygonTest{ 7 8 constructor(){ 9 const path = [], box = new Box(), 10 11 car = new CanvasAnimateRender({ 12 width: window.innerWidth, 13 height: window.innerHeight, 14 }).render(), 15 16 cab = car.add(new CanvasAnimateBox()).size(car.box.w, car.box.h, true); 17 18 function draw(obj){ 19 console.log(obj); 20 21 target._redraw(); 22 23 obj.forEach((v, i) => { 24 const str = String(i), 25 size = cab.textWidth(str, 20), 26 width = size < 20 ? 20 : size; 27 28 cab.rect(2, box.set(v.x, v.y, width, width), 2).fill('#fff'); 29 cab.text(str, '#0000ff', box.pos(v.x + (width - size)/2, v.y)); 30 31 if(v.x1 === undefined) cab.stroke('#ffff00'); 32 33 }); 34 35 car.redraw(); 36 } 37 38 39 const target = { 40 title: '测试多边形之合并(此类并不完美)', 41 polygonA: null, 42 polygonB: null, 43 centerA: new Box(), 44 centerB: new Box(), 45 length: 0, 46 mergeType: 0, 47 48 merge(){ 49 if(this.polygonA === null || this.polygonB === null) return console.warn('必须已生成两个多边形'); 50 if(this.mergeType === 0) draw(this.polygonA.merge(this.polygonB)); 51 else draw(this.polygonB.merge(this.polygonA)); 52 53 }, 54 55 setPolygonA(){ 56 this.polygonA = new Polygon(path.concat()); 57 this.centerA.setFromPolygon(this.polygonA, false); 58 update_offsetNum(); 59 }, 60 61 setPolygonB(){ 62 this.polygonB = new Polygon(path.concat()); 63 this.centerB.setFromPolygon(this.polygonB, false); 64 update_offsetNum(); 65 }, 66 67 generate(){ 68 if(path.length < 6) return console.warn('路径至少需要3个点'); 69 70 if(this.polygonA !== null && this.polygonB !== null){ 71 const _path = path.concat(); 72 this.clear(); 73 _path.forEach(v => path.push(v)); 74 this.generate(); 75 return; 76 } 77 78 else{ 79 cab.path(path, true); 80 81 if(this.polygonA === null) this.setPolygonA(); 82 else this.setPolygonB(); 83 path.length = 0; 84 85 } 86 87 this._redraw(); 88 car.redraw(); 89 }, 90 91 clear(){ 92 path.length = 0; 93 this.polygonA = this.polygonB = null; 94 this.centerA.set(0,0,0,0); 95 this.centerB.set(0,0,0,0); 96 cab.clear(); 97 car.clear(); 98 }, 99 100 clearPath(){ 101 path.length = 0; 102 cab.clear(); 103 this._redraw(); 104 car.redraw(); 105 }, 106 107 _redraw(){ 108 if(this.polygonA !== null){ 109 cab.clear().path(this.polygonA.path).stroke('#00ff00', 1); 110 this.sign(this.polygonA.path, 'rgba(0,255,0,1)', 'rgba(0,255,0,0.6)'); 111 if(this.polygonB !== null){ 112 cab.path(this.polygonB.path).stroke('#ff0000', 1); 113 this.sign(this.polygonB.path, 'rgba(255,0,0,1)', 'rgba(255,0,0,0.6)'); 114 } 115 116 } 117 118 }, 119 120 //标记线的第一和第二个点 121 sign(path, color1, color2){ 122 cab.rect(5, box.set(path[0]-5, path[1]-5, 10, 10), 2).fill(color1); 123 cab.rect(5, box.set(path[2]-5, path[3]-5, 10, 10), 2).fill(color2); 124 }, 125 126 //偏移路径 127 offsetTimer: new Timer(()=>target.offset(), 300, 1, false), 128 offsetNum: new Point(0, 0), 129 offsetTarget: 'polygonA', 130 offsetVN: 'x', 131 offsetPoint: new Point(), 132 offset(){ 133 if(this[this.offsetTarget] === null) return; 134 path.length = 0; 135 136 const pathNew = path, pathOld = this[this.offsetTarget].path, point = this.offsetPoint, 137 center = this.offsetTarget === 'polygonA' ? 'centerA' : 'centerB', 138 x = this.offsetNum[this.offsetVN] * car.box[this.offsetVN === 'x' ? 'w' : 'h']; 139 140 for(let k = 0, len = pathOld.length; k < len; k+=2){ 141 point.set(pathOld[k], pathOld[k+1]); 142 point[this.offsetVN] = point[this.offsetVN] - this[center][this.offsetVN] + x; 143 pathNew.push(point.x, point.y); 144 } 145 146 this[this.offsetTarget === 'polygonA' ? 'setPolygonA' : 'setPolygonB'](); 147 path.length = 0; 148 this._redraw(); 149 car.redraw(); 150 }, 151 152 //克隆 153 clone(){ 154 if(this[this.offsetTarget] === null) return; 155 path.length = 0; 156 this[this.offsetTarget].path.forEach(v => path.push(v)); 157 this[this.offsetTarget === 'polygonA' ? 'setPolygonB' : 'setPolygonA'](); 158 159 path.length = 0; 160 this._redraw(); 161 car.redraw(); 162 163 }, 164 165 }, 166 167 lineData = {type: 'line', value: '.title'}, 168 169 data = [ 170 { 171 title: '项目', 172 valueUrl: '.title', 173 }, 174 { 175 explain: '路径长度', 176 title: 'length', 177 valueUrl: '.length', 178 disable: true, 179 }, 180 181 { 182 explain: '生成多边形', 183 title: 'generate', 184 valueUrl: '.generate', 185 }, 186 187 lineData, 188 { 189 title: 'mergeType', 190 valueUrl: '.mergeType', 191 selectList: [{name: 'lineB to lineA', value: 0}, {name: 'lineA to lineB', value: 1}] 192 }, 193 194 { 195 explain: '合并多边形', 196 title: 'merge', 197 valueUrl: '.merge', 198 }, 199 lineData, 200 201 { 202 title: 'line', 203 valueUrl: '.offsetTarget', 204 selectList: [{name: 'lineA', value: 'polygonA'}, {name: 'lineB', value: 'polygonB'}], 205 onChange: ()=> update_offsetNum(), 206 }, 207 208 { 209 title: 'clone', 210 valueUrl: '.clone', 211 explain: '克隆形状后它们会重叠, 可以通过offset控件移动它们', 212 }, 213 214 { 215 title: 'offset', 216 valueUrl: '.offsetNum', 217 range: {min: 0, max: 1, step: 0.01}, 218 onChange: v => { 219 v.scope.target.offsetVN = v.valueName; 220 v.scope.target.offsetTimer.start(); 221 }, 222 }, 223 lineData, 224 225 { 226 explain: '清理路径和多边形', 227 title: 'clearAll', 228 valueUrl: '.clear', 229 }, 230 231 { 232 explain: '清理路径', 233 title: 'clearPath', 234 valueUrl: '.clearPath', 235 }, 236 237 ], 238 239 ui = new CanvasAnimateUI(target, data, document.body, { 240 autoHeight: true, 241 width: 250, 242 globalUpdate: true, 243 defiendRange: {min: 0, max: car.box.w, step: 1}, 244 style: ` 245 position: absolute; 246 right: 2px; 247 top: 2px; 248 background: #fff; 249 `, 250 }).initUI(), 251 252 253 ui_length = ui.getView(ui.getData('.length')), 254 update_length = function (){ 255 target.length = path.length; //更新值 256 ui_length.update[0](); //更新ca 257 ui_length.redraw(); //更新画布 258 }, 259 260 ui_offsetNum = ui.getView(ui.getData('.offsetNum')), 261 update_offsetNum = function (){ 262 const center = target.offsetTarget === 'polygonA' ? 'centerA' : 'centerB'; 263 target.offsetNum.set(target[center].x / car.box.w, target[center].y / car.box.h); //更新值 264 ui_offsetNum.update[0](); //更新ca 265 ui_offsetNum.update[1](); //更新ca 266 ui_offsetNum.redraw(); //更新画布 267 }; 268 269 270 //event 271 const cae = new CanvasAnimateEvent(car), 272 eventBox = new CanvasAnimate(); 273 eventBox.box.copy(car.box); 274 275 cae.add(eventBox, 'click', event => { 276 if(path.length >= 2) cab.line(path[path.length - 2], path[path.length - 1], event.pageX, event.pageY).stroke('#ffff00', 1); 277 else cab.rect(20/2, box.size(20, 20).pos(event.pageX - 10, event.pageY - 10), 2).fill('#ffff00'); 278 279 path.push(event.pageX, event.pageY); 280 car.redraw(); 281 update_length(); 282 }); 283 284 285 } 286 287 }
可视化调试: 多边形之合并
提取地址: https://pan.baidu.com/s/1TV1j5BeZ7ZhidCq7aQXePA
提取码: 1111