弑天下
圆多边形截距如果球在移动并且你可以确保球总是在多边形之外开始,那么解决方案就相当简单了。我们将球及其运动称为球线。它从球的当前位置开始,到球将在下一帧的位置结束。要解决此问题,您需要找到距球线起点最近的截距。拦截有两种。线段(球线)与线段(多边形边)带圆的线段(球线)(每个(仅凸)多边形角处的圆)示例代码有一个Lines2对象,其中包含两个相关的拦截函数。截距以Vec2包含两个单位距离的形式返回。截距函数用于线(无限长)而不是线段。如果没有拦截,则返回未定义。对于线截距Line2.unitInterceptsLine(line, result = new Vec2()),单位值 (in result) 是从起点开始沿每条线的单位距离。负值落后于开始。考虑到球半径,每个多边形边沿其法线偏移球半径。多边形边缘具有一致的方向很重要。在示例中,法线位于直线的右侧,多边形点位于顺时针方向。对于线段/圆的截距Line2.unitInterceptsCircle(center, radius, result = new Vec2()),单位值 (in result) 是沿直线与圆相交的单位距离。result.x将始终包含最近的截距(假设您从圆圈外开始)。如果有一个拦截,那么即使它们在同一点,也总是有两个。例子该示例包含所有需要的内容感兴趣的对象是ball和polyball定义球及其运动。也有代码来绘制它的例子poly保存多边形的点。根据球半径将点转换为偏移线。它被优化为仅在球半径发生变化时才计算线条。函数poly.movingBallIntercept是完成所有工作的函数。它需要一个球对象和一个可选的结果向量。Vec2如果它接触多边形,它将返回作为球的位置。它通过找到到偏移线和点(作为圆)的最小单位距离来做到这一点,并使用该单位距离来定位结果。请注意,如果球在多边形内,则与角的截距是相反的。该函数Line2.unitInterceptsCircle确实提供了线进入和退出圆圈的 2 个单位距离。但是,您需要知道您是在室内还是室外才能知道使用哪一个。该示例假设您在多边形之外。指示移动鼠标来改变球的路径。单击以设置球的起始位置。Math.EPSILON = 1e-6;Math.isSmall = val => Math.abs(val) < Math.EPSILON;Math.isUnit = u => !(u < 0 || u > 1);Math.TAU = Math.PI * 2;/* export {Vec2, Line2} */ // this should be a modulevar temp;function Vec2(x = 0, y = (temp = x, x === 0 ? (x = 0 , 0) : (x = x.x, temp.y))) { this.x = x; this.y = y;}Vec2.prototype = { init(x, y = (temp = x, x = x.x, temp.y)) { this.x = x; this.y = y; return this }, // assumes x is a Vec2 if y is undefined copy() { return new Vec2(this) }, equal(v) { return (this.x - v.x) === 0 && (this.y - v.y) === 0 }, isUnits() { return Math.isUnit(this.x) && Math.isUnit(this.y) }, add(v, res = this) { res.x = this.x + v.x; res.y = this.y + v.y; return res }, sub(v, res = this) { res.x = this.x - v.x; res.y = this.y - v.y; return res }, scale(val, res = this) { res.x = this.x * val; res.y = this.y * val; return res }, invScale(val, res = this) { res.x = this.x / val; res.y = this.y / val; return res }, dot(v) { return this.x * v.x + this.y * v.y }, uDot(v, div) { return (this.x * v.x + this.y * v.y) / div }, cross(v) { return this.x * v.y - this.y * v.x }, uCross(v, div) { return (this.x * v.y - this.y * v.x) / div }, get length() { return this.lengthSqr ** 0.5 }, set length(l) { this.scale(l / this.length) }, get lengthSqr() { return this.x * this.x + this.y * this.y }, rot90CW(res = this) { const y = this.x; res.x = -this.y; res.y = y; return res; },};const wV1 = new Vec2(), wV2 = new Vec2(), wV3 = new Vec2(); // pre allocated work vectors used by Line2 functionsfunction Line2(p1 = new Vec2(), p2 = (temp = p1, p1 = p1.p1 ? p1.p1 : p1, temp.p2 ? temp.p2 : new Vec2())) { this.p1 = p1; this.p2 = p2;}Line2.prototype = { init(p1, p2 = (temp = p1, p1 = p1.p1, temp.p2)) { this.p1.init(p1); this.p2.init(p2) }, copy() { return new Line2(this) }, asVec(res = new Vec2()) { return this.p2.sub(this.p1, res) }, unitDistOn(u, res = new Vec2()) { return this.p2.sub(this.p1, res).scale(u).add(this.p1) }, translate(vec, res = this) { this.p1.add(vec, res.p1); this.p2.add(vec, res.p2); return res; }, translateNormal(amount, res = this) { this.asVec(wV1).rot90CW().length = -amount; this.translate(wV1, res); return res; }, unitInterceptsLine(line, res = new Vec2()) { // segments this.asVec(wV1); line.asVec(wV2); const c = wV1.cross(wV2); if (Math.isSmall(c)) { return } wV3.init(this.p1).sub(line.p1); res.init(wV1.uCross(wV3, c), wV2.uCross(wV3, c)); return res; }, unitInterceptsCircle(point, radius, res = new Vec2()) { this.asVec(wV1); var b = -2 * this.p1.sub(point, wV2).dot(wV1); const c = 2 * wV1.lengthSqr; const d = (b * b - 2 * c * (wV2.lengthSqr - radius * radius)) ** 0.5 if (isNaN(d)) { return } return res.init((b - d) / c, (b + d) / c); },};/* END of file */ // Vec2 and Line2 module /* import {vec2, Line2} from "whateverfilename.jsm" */ // Should import vec2 and line2const POLY_SCALE = 0.5;const ball = { pos: new Vec2(-150,0), delta: new Vec2(10, 10), radius: 20, drawPath(ctx) { ctx.beginPath(); ctx.arc(this.pos.x, this.pos.y, this.radius, 0, Math.TAU); ctx.stroke(); },}const poly = { bRadius: 0, lines: [], set ballRadius(radius) { const len = this.points.length this.bRadius = ball.radius; i = 0; while (i < len) { let line = this.lines[i]; if (line) { line.init(this.points[i], this.points[(i + 1) % len]) } else { line = new Line2(new Vec2(this.points[i]), new Vec2(this.points[(i + 1) % len])) } this.lines[i++] = line.translateNormal(radius); } this.lines.length = i; }, points: [ new Vec2(-200, -150).scale(POLY_SCALE), new Vec2(200, -100).scale(POLY_SCALE), new Vec2(100, 0).scale(POLY_SCALE), new Vec2(200, 100).scale(POLY_SCALE), new Vec2(-200, 75).scale(POLY_SCALE), new Vec2(-150, -50).scale(POLY_SCALE), ], drawBallLines(ctx) { if (this.lines.length) { const r = this.bRadius; ctx.beginPath(); for (const l of this.lines) { ctx.moveTo(l.p1.x, l.p1.y); ctx.lineTo(l.p2.x, l.p2.y); } for (const p of this.points) { ctx.moveTo(p.x + r, p.y); ctx.arc(p.x, p.y, r, 0, Math.TAU); } ctx.stroke() } }, drawPath(ctx) { ctx.beginPath(); for (const p of this.points) { ctx.lineTo(p.x, p.y) } ctx.closePath(); ctx.stroke(); }, movingBallIntercept(ball, res = new Vec2()) { if (this.bRadius !== ball.radius) { this.ballRadius = ball.radius } var i = 0, nearest = Infinity, nearestGeom, units = new Vec2(); const ballT = new Line2(ball.pos, ball.pos.add(ball.delta, new Vec2())); for (const p of this.points) { const res = ballT.unitInterceptsCircle(p, ball.radius, units); if (res && units.x < nearest && Math.isUnit(units.x)) { // assumes ball started outside poly so only need first point nearest = units.x; nearestGeom = ballT; } } for (const line of this.lines) { const res = line.unitInterceptsLine(ballT, units); if (res && units.x < nearest && units.isUnits()) { // first unit.x is for unit dist on line nearest = units.x; nearestGeom = ballT; } } if (nearestGeom) { return ballT.unitDistOn(nearest, res) } return; },}const ctx = canvas.getContext("2d");var w = canvas.width, cw = w / 2;var h = canvas.height, ch = h / 2requestAnimationFrame(mainLoop);// line and point for displaying mouse interaction. point holds the result if anyconst line = new Line2(ball.pos, ball.pos.add(ball.delta, new Vec2())), point = new Vec2();function mainLoop() { ctx.setTransform(1,0,0,1,0,0); // reset transform if(w !== innerWidth || h !== innerHeight){ cw = (w = canvas.width = innerWidth) / 2; ch = (h = canvas.height = innerHeight) / 2; }else{ ctx.clearRect(0,0,w,h); } ctx.setTransform(1,0,0,1,cw,ch); // center to canvas if (mouse.button) { ball.pos.init(mouse.x - cw, mouse.y - ch) } line.p2.init(mouse.x - cw, mouse.y - ch); line.p2.sub(line.p1, ball.delta); ctx.lineWidth = 1; ctx.strokeStyle = "#000" poly.drawPath(ctx) ctx.strokeStyle = "#F804" poly.drawBallLines(ctx); ctx.strokeStyle = "#F00" ctx.beginPath(); ctx.arc(ball.pos.x, ball.pos.y, ball.radius, 0, Math.TAU); ctx.moveTo(line.p1.x, line.p1.y); ctx.lineTo(line.p2.x, line.p2.y); ctx.stroke(); ctx.strokeStyle = "#00f" ctx.lineWidth = 2; ctx.beginPath(); if (poly.movingBallIntercept(ball, point)) { ctx.arc(point.x, point.y, ball.radius, 0, Math.TAU); } else { ctx.arc(line.p2.x, line.p2.y, ball.radius, 0, Math.TAU); } ctx.stroke(); requestAnimationFrame(mainLoop);}const mouse = {x:0, y:0, button: false};function mouseEvents(e) { const bounds = canvas.getBoundingClientRect(); mouse.x = e.pageX - bounds.left - scrollX; mouse.y = e.pageY - bounds.top - scrollY; mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button;}["mousedown","mouseup","mousemove"].forEach(name => document.addEventListener(name,mouseEvents));#canvas { position: absolute; top: 0px; left: 0px;}<canvas id="canvas"></canvas>Click to position ball. Move mouse to test trajectory Vec2和Line2为了使其更容易,矢量库将有所帮助。对于示例,我编写了一个快速Vec2和Line2对象(注意仅示例中使用的函数已经过测试,注意该对象是为性能而设计的,没有经验的编码人员应避免使用这些对象并选择更标准的矢量和线库)