计算2个移动球将碰撞的x / y点

时间:2010-09-11 20:24:23

标签: math language-agnostic collision-detection physics

我试图制作(基本上)简单的游泳池游戏,并且希望能够预测一旦它击中另一个球后射击的位置。

第一部分是,我相信,计算是否会撞击任何东西,如果碰撞,它会碰撞到哪里。我可以计算出一条线和一个球的碰撞点,但不是2个球。

因此,考虑到2个球的x / y位置和速度,我如何计算它们碰撞的点?

(PS:我知道我可以通过计算沿途每一步两个球之间的距离来做到这一点,但我希望能有一些更优雅和最优的东西。)

设置示例:尝试计算红点

http://dl.dropbox.com/u/6202117/circle.PNG

4 个答案:

答案 0 :(得分:14)

需要注意的一些事项:

  • 当两个球,每个半径r碰撞时,它们的中心相距2r
  • 你可以假设你的第一个球是直线行进的(好的,先是近似,但从这开始),你可以找到这个路径与从第一个球到第一个球的方向之间的角度alpha第二个。
  • 你知道固定球的中心,不是吗?

现在你有一些几何学要做。

做这个结构:

  1. 将第一个(移动)球的当前中心标记为点A
  2. 将静止球的中心标记为点B
  3. 构建线段AB
  4. 在移动方向上从R构建光线A
  5. 2r周围构建一个半径B的圆圈。
  6. B垂直于R垂下一段,调用交叉点C
  7. 您知道距离AB,您可以在alphaAB之间找到角度R,正弦律找到BC的长度
  8. 从该长度确定是否有0,1或2个解决方案。如果有0或1你就完成了。
  9. 构造点D,其中圆圈与R更接近A,并再次使用正弦法来找到距离AD。
  10. 碰撞点是BD
  11. 的中点

    现在你知道了一切。

    从中构建有效的代码是一种练习。


    BTW--如果两个球都在移动,这种结构将不起作用,但你可以变换成一个静止的框架,以那种方式解决,然后转换回来。只需确保在反向转换之后检查解决方案是否在允许的区域 ...

    /物理学家不能做出这样的评论。我试图抵制。我真的做了。

答案 1 :(得分:12)

绘制@dmckee的答案

alt text

修改

为了回应@ArtB necromancer的回答,可以写出上图中D点的解决方案:

1/2 {(Ax+Bx+2 d Dx Cos[alpha]- Dx Cos[2 alpha]+ 2 Dy (Cos[alpha]-d) Sin[alpha]), 
     (Ay+By+2 d Dy Cos[alpha]- Dy Cos[2 alpha]- 2 Dx (Cos[alpha]-d) Sin[alpha])
     }  

其中

Dx = Ax - Bx 
Dy = Ay - By   

并且

d = Sqrt[4 r^2 - (Dx^2 + Dy^2) Sin[alpha]^2]/Sqrt[Dx^2 + Dy^2]  

HTH!

答案 2 :(得分:1)

我在看@dmckee的解决方案,我花了很多工作来完成它。以下是我对那些可能寻找更实际答案的人的说明,它是直接采取的 从他的帖子,所以信用归他/她,但任何错误都是我的。我通常使用Pascal类似的分配运算符(即:=)来区分显示我的工作和实际必需的代码。 我使用标准Y = mX +b格式和准oop表示法。我确实对段和结果行使用BC。也就是说,这几乎应该是'copy'n'paste-able Python代码 (删除“;”,用适当的版本替换“sqrt”和“sqr”等。)

  1. A.x和A.y是x& y位置,A.r是A的半径,A.v是速度,其中A.v.x是它的x分量,而A.v.y是它的y分量。
  2. B是相同但没有速度(或更准确地说,从A中减去B的速度,因此B相对来说是静止的)。
  3. AB.m := (b.y - a.y) / (b.x - a.x); AB.b := A.y - AB.m * A.x;
  4. R.m := A.v.y / A.v.x; R.b := A.y - R.m * A.x;
  5. 没必要
  6. BC.m := -A.v.x/A.v.y;这是垂直斜率的标准公式BC.b := B.y - BC.m * B.x;现在CAB遇到BC所以我们知道它们是相等的所以让我们让将C.y等同于C.y == AB.m * C.x + AB.b == BC.m * C.x + BC.b;所以C.x := (AB.m - BC.M) / (BC.b - AB.b);然后插入C.x以获取C.y := AB.m * C.x + AB.b;
  7. 你可以忽略正弦定律,因为我们有AB和BC所以我们可以使用毕达哥拉斯定理得到BCBC.l := sqrt( sqr(B.x-C.x) + sqr(B.y-C.y) );
  8. 的长度
  9. 如果BC.l > A.r + B.r,则解决方案为零,并且这些圈子不会触及,因为CA的路径s perigee with respect to B . If BC.l == Ar + Br , there is only one solution, and C == D . Otherwise, if BC.l< A.r + B.r then there are two solutions. You can think of this as such, if there are zero solutions the bullet missed, if there is one the bullet grazed, and if there are two then there is both an entry and exit wound. The one closer to A`是我们想要的。
    1. 现在,数学变得丑陋所以我会展示我的工作以防万一我做错了。
    2. DAC上距离B A.r + B.r(又名2r)的点,因此: sqrt( sqr(D.x - B.x) + sqr(D.y - B.y) ) == 2r
    3. 因此sqr(D.x - B.x) + sqr(D.y - B.y) == 4*r*r。现在有一个方程的2个变量(即D.xD.y)很麻烦,但我们也知道D是 在ACD.y == AC.m*D.x + AC.b
    4. 我们可以替换D.y sqr(D.x - B.x) + sqr(AC.m*C.x + AC.b - B.y) == 4*r*r
    5. 这扩展为可爱的:sqr(D.x) + 2*D.x - sqr(B.x) + sqr(AC.m*D.x) + 2*AC.b*D.x - 2*AC.m*D.x*B.y + sqr(AC.b) - 2*AC.b*B.y + sqr(B.y) == 4*r*r这个是我很可能犯了错误的部分如果我根本就这样做了。)
    6. 我们可以收集这些术语(记住,此时只有D.x是未知的;其余的我们可以将它们视为常量)得到 sqr(D.x) + 2*D.x - sqr(B.x) + sqr(AC.m*D.x) + 2*AC.b*D.x - 2*AC.m*D.x*B.y + sqr(AC.b) - 2*AC.b*B.y + sqr(B.y) == 4*r*r
    7. 改写成整理者 可以重构的(sqr(D.x) + sqr(AC.m*D.x)) + ( 2*D.x + 2*AC.b*D.x - 2*AC.m*B.y*D.x ) + ( - sqr(B.x) + sqr(AC.b) - 2*AC.b*B.y + sqr(B.y) ) == 4*r*r (1 + sqr(AC.m)) * sqr(D.x) + 2*( 1 + AC.b - AC.m*B.y ) * D.x + ( sqr(B.y) - sqr(B.x) + sqr(AC.b) - 2*AC.b*B.y - 4*r*r ) == 0
    8. 现在可以很好地融入二次公式(即x == (-bb +- sqrt( sqr(bb) - 4*aa*cc ) / 2*aa)(使用aa以避免与早期变量混淆),aa := 1 + sqr(AC.m);bb := 2*( 1 + AC.b - AC.m*B.y );和{{ 1}}。
    9. 现在我们可能会得到两个解决方案,因此我们可以使用cc := sqr(B.y) - sqr(B.x) + sqr(AC.b) - 2*AC.b*B.y - sqr(A.r+B.r);保存部件:-bb/2aa +- sqrt(sqr(bb)-4*aa*cc)/2*aafirst_term := -bb/(2*a);
    10. 第一个解决方案second_term := sqrt(sqr(bb)-4*aa*cc)/2*aa;包含D1.x = first_term + second_term;,第二个解决方案D1.y = AC.m * D1.x + AC.b包含D2.x = first_term + second_term;
    11. 找到D2.y = AC.m * D2.x + AC.b的距离:AD1.l := sqrt( sqr(D1.x-A.x) + sqr(D1.y-A.y) );(实际上,跳过两个平方根更有效,但没有区别。)
    12. 越接近你想要的D2.l = sqrt( sqr(D2.x-A.x) + sqr(D2.y-A.y) );
    1. D := D1 if D1.l < D2. l else D2;的中点,我们称之为E,是碰撞(如果半径不相等,我不知道这是如何推断的。)
    2. 因此构建了行DBDB.m := (B.y-D.y)/(B.x-D.x);
    3. 我们不需要长度来确定长度,因为它应该是DB.b = B.y - DB.m*B.x;,所以BD.l == A.r + B.r
    4. 同样,我们可以替换sqrt( sqr(E.x-B.x) + sqr(E.y-B.y) ) == B.r,因为我们知道它位于E所以BD,正在E.y == BD.m * E.x + BD.b
    5. 扩展为sqrt( sqr(E.x-B.x) + sqr(BD.m * E.x + BD.b-B.y) ) == B.r
    6. 收集到
    7. sqr(E.x) - 2*E.x*B.x + sqr(B.x) + sqr(BD.m)*sqr(E.x) + 2*BD.m*E.x*BD.b - 2*BD.m*B.y + sqr(B.y) - 2*BD.b*B.y + sqr(B.y) == sqr(B.r) sqr(E.x) + sqr(BD.m)*sqr(E.x) + 2*BD.m*E.x*BD.b - 2*E.x*B.x + sqr(B.x) - 2*BD.m*B.y + sqr(B.y) - 2*BD.b*B.y + sqr(B.y) == sqr(B.r) (1 + sqr(BD.m)) * sqr(E.x) + 2*(BD.m*BD.b - B.x) * E.x + sqr(B.x) + sqr(B.y) - 2*BD.m*B.y + sqr(B.y) - 2*BD.b*B.y + sqr(B.y) - sqr(B.r) == 0,二次公式,然后得到你的两个点并选择一个接近B的点。

      我希望我只是因为业力或死灵法师徽章而嫖娼,但我真的需要解决这个问题,并认为我会分享。呃,我想我现在需要躺下。

答案 3 :(得分:1)

计算碰撞时间

物理模拟的一部分是冲突解决。通常最好使用时间步长过程来完成此操作。从最早的碰撞开始,解决并在碰撞后重复进行更多的碰撞。

时间是单位时间。 0是最后一帧的时间,而1是我们正在渲染的帧。

因此,我们实际上仅对碰撞时间感兴趣,并且可以忽略实际位置,直到知道哪个对象首先碰撞。

此答案提供的功能将返回两个运动球的所有可能的碰撞时间(最多两个),非常适合基于时间的碰撞解决方案。

对于 TLDR ,请转到解决方案

有关如何继续阅读。

解决u

首先定义球(使用JS)

const ball = {
     x, y,   // position start of frame
     r,      // radius
     vx, vy, // velocity vector per frame
}
// Two balls A, and B
A = {...ball, r: 20, etc}  
B = {...ball, r: 20, etc}

如果我们想象球沿着其向量行进为一个点。我们首先看一下如何解决只有一个球移动的情况。

A球不动,B球不动,(以适应去除点符号A.x的线为AxB.vxBvx另外* *表示JS中的功能)

通过以下两个步骤,我们可以找到一个点到直线的距离。

最接近点A的直线上的第一个单位距离,然后使用距离dist u来获得与球A的距离dA与B线沿着的距离

u = (Bvx * (Ax - Bx) + Bvy * (Ay - By)) / ((Ax - Bx) ** 2 + (Ay - By) ** 2);
dA = (((Bx + Bvx * u) - Ax) ** 2 + ((By + Bvy * u) - Ay) ** 2) ** 0.5;

如果球“ B”不动且球A不变,我们可以做同样的事情

u = (Avx * (Bx - Ax) + Avy * (By - Ay)) / ((Bx - Ax) ** 2 + (By - Ay) ** 2);
dB = (((Ax + Avx * u) - Bx) ** 2 + ((Ay + Avy * u) - By) ** 2) ** 0.5;

如果我们现在考虑如果两个球都在运动,那么如果发生碰撞,则必须同时发生碰撞。因此,以上两个函数中的值u必须相同。

我们现在有两个方程,它们具有相同的值u,但是还需要解决更多的问题。

我们还知道在时间u(碰撞时),两个球必须是它们的半径之和。那就是dA + dB == Ar + Br。要摆脱平方根,dA * dA + dB * dB == Ar * Ar + Br * Br

同样适用

现在我们只有1个未知的u和一个函数...

Ar*Ar+Br*Br = (((Bx+Bvx*u)-Ax)**2+((By+Bvy*u)-Ay)**2)+(((Ax+Avx*u)-Bx)**2+((Ay+Avy*u)-By)**2);

f(u) = Ar*Ar+Br*Br-(((Bx+Bvx*u)-Ax)**2+((By+Bvy*u)-Ay)**2)+(((Ax+Avx*u)-Bx)**2+((Ay+Avy*u)-By)**2)

这可能并不明显,但是该函数只是二次函数,例如f(u) = au^2 + bu + c,如果我们知道系数abc,我们可以使用二次方程式(我们都是在高中学习的)

解决方案

要解决这个问题,我们需要重新排列公式,以便我们可以插入abc

// return array if 0, 1, or 2 roots
Math.quadRoots = (a, b, c) => {
    if (Math.abs(a) < 1e-6) { return b != 0 ? [-c / b] : []  }
    b /= a;
    var d = b * b - 4 * (c / a);
    if (d > 0) {
        d = d ** 0.5;
        return  [0.5 * (-b + d), 0.5 * (-b - d)]
    }
    return d === 0 ? [0.5 * -b] : [];
}

重新排列会变成一整页,因此为了节省痛苦,以下函数返回两个运动球的值u

// For line segements
// args (x1, y1, x2, y2, x3, y3, x4, y4, r1, r2)

Math.interceptTime = (a, e, b, f, c, g, d, h, r1, r2) => { 
    const A = a*a, B = b*b, C = c*c, D = d*d;
    const E = e*e, F = f*f, G = g*g, H = h*h;
    var R = (r1 + r2) ** 2;
    const AA = A + B + C + F + G + H + D + E + b*c + c*b + f*g + g*f + 2*(a*d - a*b - a*c - b*d - c*d - e*f + e*h - e*g - f*h - g*h);
    const BB = 2 * (-A + a*b + 2*a*c - a*d - c*b - C + c*d - E + e*f + 2*e*g - e*h - g*f - G + g*h);
    const CC = A - 2*a*c + C + E - 2*e*g + G - R;
    return Math.quadRoots(AA, BB, CC);
}   

使用

获取时间

const res = Math.interceptTime(A.x, A.y, A.x + A.vx, A.y + A.vy, B.x, B.y, B.x + B.vx, B.y + B.vy, A.r, B.r);
// remove out of range values and use the smallest as time of first contact
const time = Math.min(...res.filter(u => u >= 0 && u < 1));

使用该时间查找最早的碰撞,一旦发现该时间即可获取球的位置

// point of contact for balls A and B
ax = A.x + A.vx * time;
ay = A.y + A.vy * time;
bx = B.x + B.vx * time;
by = B.y + B.vy * time;

用法示例

上面用于鲁棒的迭代冲突解决。注意小球可以以很高的速度行进。由于JS的运行速度很慢,因此每帧的迭代总数上限为200。

// See answer for details
Math.circlesInterceptUnitTime = (a, e, b, f, c, g, d, h, r1, r2) => { // args (x1, y1, x2, y2, x3, y3, x4, y4, r1, r2)
    const A = a * a, B = b * b, C = c * c, D = d * d;
    const E = e * e, F = f * f, G = g * g, H = h * h;
    var R = (r1 + r2) ** 2;
    const AA = A + B + C + F + G + H + D + E + b * c + c * b + f * g + g * f + 2 * (a * d - a * b - a * c - b * d - c * d - e * f + e * h - e * g - f * h - g * h);
    const BB = 2 * (-A + a * b + 2 * a * c - a * d - c * b - C + c * d - E + e * f + 2 * e * g - e * h - g * f - G + g * h);
    const CC = A - 2 * a * c + C + E - 2 * e * g + G - R;
    return Math.quadRoots(AA, BB, CC);
}   
Math.quadRoots = (a, b, c) => { // find roots for quadratic
    if (Math.abs(a) < 1e-6) { return b != 0 ? [-c / b] : []  }
    b /= a;
    var d = b * b - 4 * (c / a);
    if (d > 0) {
        d = d ** 0.5;
        return  [0.5 * (-b + d), 0.5 * (-b - d)]
    }
    return d === 0 ? [0.5 * -b] : [];
}



canvas.width = innerWidth -20;
canvas.height = innerHeight -20;
mathExt(); // creates some additional math functions
const ctx = canvas.getContext("2d");
const GRAVITY = 0;
const WALL_LOSS = 1;
const BALL_COUNT = 40;  // approx as will not add ball if space can not be found
const MIN_BALL_SIZE = 6;
const MAX_BALL_SIZE = 20;
const VEL_MIN = 1;
const VEL_MAX = 5; 
const MAX_RESOLUTION_CYCLES = 200; // Put too many balls (or too large) in the scene and the 
                                   // number of collisions per frame can grow so large that
                                   // it could block the page.

                                   // If the number of resolution steps is above this value
                                   // simulation will break and balls can pass through lines,
                                   // get trapped, or worse. LOL
const SHOW_COLLISION_TIME = 30;
const balls = [];
const lines = [];
function Line(x1,y1,x2,y2) {
    this.x1 = x1;
    this.y1 = y1;
    this.x2 = x2;
    this.y2 = y2;
}
Line.prototype = {
    draw() {
        ctx.moveTo(this.x1, this.y1);
        ctx.lineTo(this.x2, this.y2);
    },
    reverse() {
        const x = this.x1;
        const y = this.y1;
        this.x1 = this.x2;
        this.y1 = this.y2;
        this.x2 = x;
        this.y2 = y;
        return this;
    }
}
    
function Ball(x, y, vx, vy, r = 45, m = 4 / 3 * Math.PI * (r ** 3)) {
    this.r = r;
    this.m = m
    this.x = x;
    this.y = y;
    this.vx = vx;
    this.vy = vy;
}
Ball.prototype = {
    update() {
        this.x += this.vx;
        this.y += this.vy;
        this.vy += GRAVITY;
    },
    draw() {
        ctx.moveTo(this.x + this.r  - 1.5, this.y);
        ctx.arc(this.x, this.y, this.r - 1.5, 0, Math.PI * 2);
    },
    interceptLineTime(l, time) {
        const u = Math.interceptLineBallTime(this.x, this.y, this.vx, this.vy, l.x1, l.y1, l.x2, l.y2,  this.r);
        if (u >= time && u <= 1) {
            return u;
        }
    },
    checkBallBallTime(t, minTime) {
        return t > minTime && t <= 1;
    },
    interceptBallTime(b, time) {
        const x = this.x - b.x;
        const y = this.y - b.y;
        const d = (x * x + y * y) ** 0.5;
        if (d > this.r + b.r) {
            const times = Math.circlesInterceptUnitTime(
                this.x, this.y, 
                this.x + this.vx, this.y + this.vy, 
                b.x, b.y,
                b.x + b.vx, b.y + b.vy, 
                this.r, b.r
            );
            if (times.length) {
                if (times.length === 1) {
                    if(this.checkBallBallTime(times[0], time)) { return times[0] }
                    return;
                }
                if (times[0] <= times[1]) {
                    if(this.checkBallBallTime(times[0], time)) { return times[0] }
                    if(this.checkBallBallTime(times[1], time)) { return times[1] }
                    return
                }
                if(this.checkBallBallTime(times[1], time)) { return times[1] }                
                if(this.checkBallBallTime(times[0], time)) { return times[0] }
            }
        }
    },
    collideLine(l, time) {
        const x1 = l.x2 - l.x1;
        const y1 = l.y2 - l.y1;
        const d = (x1 * x1 + y1 * y1) ** 0.5;
        const nx = x1 / d;
        const ny = y1 / d;            
        const u = (this.vx  * nx + this.vy  * ny) * 2;
        this.x += this.vx * time;   
        this.y += this.vy * time;   
        this.vx = (nx * u - this.vx) * WALL_LOSS;
        this.vy = (ny * u - this.vy) * WALL_LOSS;
        this.x -= this.vx * time;
        this.y -= this.vy * time;
    },
    collide(b, time) { 
        const a = this;
        const m1 = a.m;
        const m2 = b.m;

        a.x = a.x + a.vx * time;
        a.y = a.y + a.vy * time;
        b.x = b.x + b.vx * time;
        b.y = b.y + b.vy * time;

        const x = a.x - b.x
        const y = a.y - b.y  
        const d = (x * x + y * y);
        const u1 = (a.vx * x + a.vy * y) / d
        const u2 = (x * a.vy - y * a.vx ) / d
        const u3 = (b.vx * x + b.vy * y) / d
        const u4 = (x * b.vy - y * b.vx ) / d
        const mm = m1 + m2;
        const vu3 = (m1 - m2) / mm * u1 + (2 * m2) / mm * u3;
        const vu1 = (m2 - m1) / mm * u3 + (2 * m1) / mm * u1;

        b.vx = x * vu1 - y * u4;
        b.vy = y * vu1 + x * u4;
        a.vx = x * vu3 - y * u2;
        a.vy = y * vu3 + x * u2;
        a.x = a.x - a.vx * time;
        a.y = a.y - a.vy * time;
        b.x = b.x - b.vx * time;
        b.y = b.y - b.vy * time;
    },
    doesOverlap(ball) {
        const x = this.x - ball.x;
        const y = this.y - ball.y;
        return  (this.r + ball.r) > ((x * x + y * y) ** 0.5);  
    }       
}

function canAdd(ball) {
    for(const b of balls) {
        if (ball.doesOverlap(b)) { return false }
    }
    return true;
}
function create(bCount) {
    lines.push(new Line(-10, 20, ctx.canvas.width + 10, 5));
    lines.push((new Line(-10, ctx.canvas.height - 30, ctx.canvas.width + 10, ctx.canvas.height - 3)).reverse());
    lines.push((new Line(30, -10, 4, ctx.canvas.height + 10)).reverse());
    lines.push(new Line(ctx.canvas.width - 3, -10, ctx.canvas.width - 30, ctx.canvas.height + 10)); 
    while (bCount--) {
        let tries = 100;
        while (tries--) {
            const dir = Math.rand(0, Math.TAU);
            const vel = Math.rand(VEL_MIN, VEL_MAX);
            const ball = new Ball(
                Math.rand(MAX_BALL_SIZE + 30, canvas.width - MAX_BALL_SIZE - 30), 
                Math.rand(MAX_BALL_SIZE + 30, canvas.height - MAX_BALL_SIZE - 30),
                Math.cos(dir) * vel,
                Math.sin(dir) * vel,
                Math.randP(MIN_BALL_SIZE, MAX_BALL_SIZE),
            );
            if (canAdd(ball)) {
                balls.push(ball);
                break;
            }
        }
    }
}
function resolveCollisions() {
    var minTime = 0, minObj, minBall, resolving = true, idx = 0, idx1, after = 0, e = 0;
    while (resolving && e++ < MAX_RESOLUTION_CYCLES) { // too main ball may create very lone resolution cycle. e limits this
        resolving = false;
        minObj = undefined;
        minBall = undefined;
        minTime = 1;
        idx = 0;
        for (const b of balls) {
            idx1 = idx + 1;
            while (idx1 < balls.length) {
                const b1 = balls[idx1++];
                const time = b.interceptBallTime(b1, after);
                if (time !== undefined) {
                    if (time <= minTime) {
                        minTime = time;
                        minObj = b1;
                        minBall = b;
                        resolving = true;
                    }
                }
            }
            for (const l of lines) {
                const time = b.interceptLineTime(l, after);
                if (time !== undefined) {
                    if (time <= minTime) {
                        minTime = time;
                        minObj = l;
                        minBall = b;
                        resolving = true;
                    }
                }
            }
            idx ++;
        }
        if (resolving) {
            if (minObj instanceof Ball) {
                minBall.collide(minObj, minTime);
            } else {
                minBall.collideLine(minObj, minTime);
            }
            after = minTime;
        }
    }
}
create(BALL_COUNT);
mainLoop();
function mainLoop() {
    ctx.clearRect(0,0,ctx.canvas.width, ctx.canvas.height);

    resolveCollisions();
    for (const b of balls) { b.update() }
    
    ctx.beginPath();
    ctx.strokeStyle = "#00F";
    ctx.lineWidth = 3;    
    for (const b of balls) { b.draw() }
    ctx.stroke();
     
    ctx.lineWidth = 1;
    ctx.strokeStyle = "#000";
    ctx.beginPath();
    for(const l of lines) { l.draw() }
    ctx.stroke();

    requestAnimationFrame(mainLoop);
}

function mathExt() {
    Math.TAU = Math.PI * 2;
    Math.rand = (min, max) => Math.random() * (max - min) + min;
    Math.randP = (min, max, pow = 2) => Math.random() ** pow * (max - min) + min;
    Math.randI = (min, max) => Math.random() * (max - min) + min | 0; // only for positive numbers 32bit signed int
    Math.randItem = arr => arr[Math.random() * arr.length | 0]; // only for arrays with length < 2 ** 31 - 1

    Math.interceptLineBallTime = (x, y, vx, vy, x1, y1, x2, y2,  r) => {
        const xx = x2 - x1;
        const yy = y2 - y1;
        const d = vx * yy - vy * xx;
        if (d > 0) {  // only if moving towards the line
            const dd = r / (xx * xx + yy * yy) ** 0.5;
            const nx = xx * dd;
            const ny = yy * dd;
            return (xx * (y - (y1 + nx)) - yy * (x -(x1 - ny))) / d;
        }
    }
}
<canvas id="canvas"></canvas>