从点开始的视线

时间:2016-04-10 21:55:38

标签: javascript canvas html5-canvas

需要从点创建简单的视线。这条线的长度将适应画布的大小。如果线指向任何对象(圆形,矩形等),则必须在此之后中断。我不确切地知道如何描述这一点,但行为应该像this。这就像激光瞄准电子游戏。

Demo jsfiddle。目标线有红色。我认为该行必须具有动态长度,具体取决于我将指向它的位置。

var canvas = document.querySelector("canvas");
canvas.width = 500;
canvas.height = 300;
var ctx = canvas.getContext("2d"),

line = {
	x1: 190, y1: 170,
    x2: 0, y2: 0,
    x3: 0, y3: 0
};
var length = 100;

var circle = {
	x: 400,
    y: 70
};

window.onmousemove = function(e) {
  //get correct mouse pos
  var rect = ctx.canvas.getBoundingClientRect(),
      x = e.clientX - rect.left,
      y = e.clientY - rect.top;

  // calc line angle
  var dx = x - line.x1,
      dy = y - line.y1,
      angle = Math.atan2(dy, dx);

  //Then render the line using 100 pixel radius:
  line.x2 = line.x1 - length * Math.cos(angle);
  line.y2 = line.y1 - length * Math.sin(angle);
  
  line.x3 = line.x1 + canvas.width * Math.cos(angle);
  line.y3 = line.y1 + canvas.width * Math.sin(angle);
 
  // render
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  
  ctx.beginPath();
  ctx.moveTo(line.x1, line.y1);
  ctx.lineTo(line.x2, line.y2);
  ctx.strokeStyle = '#333';
  ctx.stroke();
  
  ctx.beginPath();
  ctx.moveTo(line.x1, line.y1);
  ctx.lineTo(line.x3, line.y3);
  ctx.strokeStyle = 'red';
  ctx.stroke();
  
  ctx.beginPath();
  ctx.arc(circle.x, circle.y, 20, 0, Math.PI * 2, true);
  ctx.fillStyle = '#333';
  ctx.fill();
  
}
<canvas></canvas>

2 个答案:

答案 0 :(得分:5)

光线投射

给出的答案是一个很好的答案,但这个问题更适合于光线投射,如解决方案,我们只对截距的距离而不是实际拦截点感兴趣。我们每个投射光线只需要一个点,所以不计算点数会减少数学运算,从而减少每秒钟产生更多光线和物体的CPU负荷。

光线是定义开始的点和表示光线方向的标准化矢量。因为光线使用的是一个单位长度的归一化向量,所以很多计算都被简化了,因为1 *任何东西都没有变化。

此外,问题在于寻找最接近的截距,因此截距函数返回距离光线原点的距离。如果未找到拦截,则返回Infinity以允许进行有效的距离比较。每个数字都小于Infinity

JavaScript的一个很好的特性是它允许除以零并且如果发生这种情况则返回无限,这进一步降低了解决方案的复杂性。此外,如果拦截发现负截距,则表示该对象位于该光线投射原点后面,因此也将返回无穷大。

首先,让我们通过创建函数来定义我们的对象。它们都是临时对象。

// Ad Hoc method for ray to set the direction vector
var updateRayDir = function(dir){
    this.nx = Math.cos(dir);
    this.ny = Math.sin(dir);
    return this;
}
// Creates a ray objects from 
// x,y start location
// dir the direction in radians
// len the rays length
var createRay = function(x,y,dir,len){
    return ({
       x : x,
       y : y,
       len : len,
       setDir : updateRayDir, // add function to set direction
    }).setDir(dir);
}

圆圈

// returns a circle object 
// x,y is the center
// radius is the you know what..
// Note r2 is radius squared if you change the radius remember to set r2 as well
var createCircle = function(x , y, radius){
     return {
         x : x,
         y : y,
         rayDist : rayDist2Circle, // add ray cast method
         radius : radius,
         r2 : radius * radius,   // ray caster needs square of radius may as well do it here
     };
}

注意我在演示中更改了墙上的代码

// Ad Hoc function to change the wall position
// x1,y1 are the start coords
// x2,y2 are the end coords
changeWallPosition = function(x1, y1, x2, y2){
    this.x = x1;
    this.y = y1;
    this.vx = x2 - x1;
    this.vy = y2 - y1;
    this.len = Math.hypot(this.vx,this.vy);
    this.nx = this.vx / this.len;
    this.ny = this.vy / this.len;
    return this;
}

// returns a wall object
// x1,y1 are the star coords
// x2,y2 are the end coords
var createWall = function(x1, y1, x2, y2){
    return({
       x : x1, y : y1,
       vx : x2 - x1,
       vy : y2 - y1,
       rayDist : rayDist2Wall, // add ray cast method

       setPos : changeWallPosition,
    }).setPos(x1, y1, x2, y2);
}

所以那些是对象,它们可以是静态的,或者通过圆圈移动应该有一个setRadius函数,因为我添加了一个包含半径方形的属性,但是如果你使用那个代码,我会把它留给你。

现在拦截功能。

Ray Intercepts

重要的事情。在演示中,这些函数绑定到对象,以便光线投射代码不必知道它正在检查的对象类型。

与圈子的距离。

// Self evident 
// returns a distance or infinity if no valid solution
var rayDist2Circle = function(ray){
    var vcx, vcy, v;
    vcx = ray.x - this.x; // vector from ray to circle
    vcy = ray.y - this.y;
    v = -2 * (vcx * ray.nx + vcy * ray.ny);
    v -= Math.sqrt(v * v - 4 * (vcx * vcx + vcy * vcy - this.r2)); // this.r2 is the radius squared
    // If there is no solution then Math.sqrt returns NaN we should return Infinity
    // Not interested in intercepts in the negative direction so return infinity
    return isNaN(v) || v < 0 ? Infinity : v / 2;
}

与墙的距离

// returns the distance to the wall
// if no valid solution then return Infinity
var rayDist2Wall = function(ray){
    var x,y,u;
    rWCross = ray.nx * this.ny - ray.ny * this.nx;
    if(!rWCross) { return Infinity; } // Not really needed.
    x = ray.x - this.x; // vector from ray to wall start
    y = ray.y - this.y;
    u = (ray.nx * y - ray.ny * x) / rWCross; // unit distance along normalised wall
    // does the ray hit the wall segment
    if(u < 0 || u > this.len){ return Infinity;}  /// no
    // as we use the wall normal and ray normal the unit distance is the same as the
    u = (this.nx * y - this.ny * x) / rWCross;
    return u < 0 ? Infinity : u;  // if behind ray return Infinity else the dist
}

这涵盖了对象。如果你需要一个内在的圆(你想要内表面,然后将圆光线函数的倒数第二行改为v +=而不是v -=

光线投射

现在只需要针对光线迭代所有对象并将距离保持在最近的对象上即可。将光线设置为该距离,您就完成了。

// Does a ray cast.
// ray the ray to cast
// objects an array of objects 
var castRay = function(ray,objects)
    var i,minDist;

    minDist = ray.len; // set the min dist to the rays length
    i = objects.length; // number of objects to check
    while(i > 0){
        i -= 1;
        minDist = Math.min(objects[i].rayDist(ray),minDist);
    }
    ray.len = minDist;
}

演示

以及上述所有内容的演示。这是一些小的改变(绘图)。重要的是两个拦截功能。每次调整大小时,演示都会创建一个随机场景,并从鼠标位置投射16条光线。我可以在你的代码中看到你知道如何获得一条线的方向,所以我让演示展示了如何投射你最有可能最终做的多条光线

&#13;
&#13;
    const COLOUR = "BLACK";
    const RAY_COLOUR = "RED";
    const LINE_WIDTH = 4;   
    const RAY_LINE_WIDTH = 2;   
    const OBJ_COUNT = 20; // number of object in the scene;
    const NUMBER_RAYS = 16; // number of rays 
    const RAY_DIR_SPACING = Math.PI / (NUMBER_RAYS / 2);
    const RAY_ROTATE_SPEED = Math.PI * 2 / 31000;    
    if(typeof Math.hypot === "undefined"){ // poly fill for Math.hypot
        Math.hypot = function(x, y){
            return Math.sqrt(x * x + y * y);
        }
    }

    var ctx, canvas, objects, ray, w, h, mouse, rand, ray, rayMaxLen, screenDiagonal;
    // create a canvas and add to the dom
    var canvas = document.createElement("canvas"); 
    canvas.width          = w = window.innerWidth;
    canvas.height         = h = window.innerHeight;
    canvas.style.position = "absolute";
    canvas.style.left     = "0px";
    canvas.style.top      = "0px";
    document.body.appendChild(canvas);
    // objects to ray cast 
    objects = [];
    // mouse object
    mouse = {x :0, y: 0};
    //========================================================================
    // random helper
    rand = function(min, max){
        return Math.random() * (max - min) + min;
    }
    //========================================================================
    // Ad Hoc draw line method
    // col is the stroke style
    // width is the storke width
    var drawLine = function(col,width){
        ctx.strokeStyle = col;
        ctx.lineWidth = width;
        ctx.beginPath();
        ctx.moveTo(this.x,this.y);
        ctx.lineTo(this.x + this.nx * this.len, this.y + this.ny * this.len);
        ctx.stroke();
    }
    //========================================================================
    // Ad Hoc draw circle method
    // col is the stroke style
    // width is the storke width
    var drawCircle = function(col,width){
        ctx.strokeStyle = col;
        ctx.lineWidth = width;
        ctx.beginPath();
        ctx.arc(this.x , this.y, this.radius, 0 , Math.PI * 2);
        ctx.stroke();
    }
    //========================================================================
    // Ad Hoc method for ray to set the direction vector
    var updateRayDir = function(dir){
        this.nx = Math.cos(dir);
        this.ny = Math.sin(dir);
        return this;
    }
    //========================================================================
    // Creates a ray objects from 
    // x,y start location
    // dir the direction in radians
    // len the rays length
    var createRay = function(x,y,dir,len){
        return ({
           x : x,
           y : y,
           len : len,
           draw : drawLine,
           setDir : updateRayDir, // add function to set direction
        }).setDir(dir);
    }
    //========================================================================
    // returns a circle object 
    // x,y is the center
    // radius is the you know what..
    // Note r2 is radius squared if you change the radius remember to set r2 as well
    var createCircle = function(x , y, radius){
         return {
             x : x,
             y : y,
             draw : drawCircle,  // draw function
             rayDist : rayDist2Circle, // add ray cast method
             radius : radius,
             r2 : radius * radius,   // ray caster needs square of radius may as well do it here
         };
    }
    //========================================================================
    // Ad Hoc function to change the wall position
    // x1,y1 are the start coords
    // x2,y2 are the end coords
    changeWallPosition = function(x1, y1, len, dir){
        this.x = x1;
        this.y = y1;
        this.len = len;
        this.nx = Math.cos(dir);
        this.ny = Math.sin(dir);
        return this;
    }
    //========================================================================
    // returns a wall object
    // x1,y1 are the star coords
    // len is the length 
    // dir is the direction
    var createWall = function(x1, y1, len, dir){
        return({
           x : x1, y : y1,
           rayDist : rayDist2Wall, // add ray cast method
           draw : drawLine,
           setPos : changeWallPosition,
        }).setPos(x1, y1, len, dir);
    }
    //========================================================================
    // Self evident 
    // returns a distance or infinity if no valid solution
    var rayDist2Circle = function(ray){
        var vcx, vcy, v;
        vcx = ray.x - this.x; // vector from ray to circle
        vcy = ray.y - this.y;
        v = -2 * (vcx * ray.nx + vcy * ray.ny);
        v -= Math.sqrt(v * v - 4 * (vcx * vcx + vcy * vcy - this.r2)); // this.r2 is the radius squared
        // If there is no solution then Math.sqrt returns NaN we should return Infinity
        // Not interested in intercepts in the negative direction so return infinity
        return isNaN(v) || v < 0 ? Infinity : v / 2;
    }
    //========================================================================
    // returns the distance to the wall
    // if no valid solution then return Infinity
    var rayDist2Wall = function(ray){
        var x,y,u;
        rWCross = ray.nx * this.ny - ray.ny * this.nx;
        if(!rWCross) { return Infinity; } // Not really needed.
        x = ray.x - this.x; // vector from ray to wall start
        y = ray.y - this.y;
        u = (ray.nx * y - ray.ny * x) / rWCross; // unit distance along normal of wall
        // does the ray hit the wall segment
        if(u < 0 || u > this.len){ return Infinity;}  /// no
        // as we use the wall normal and ray normal the unit distance is the same as the
        u = (this.nx * y - this.ny * x) / rWCross;
        return u < 0 ? Infinity : u;  // if behind ray return Infinity else the dist
    }
    //========================================================================
    // does a ray cast
    // ray the ray to cast
    // objects an array of objects 
    var castRay = function(ray,objects){
        var i,minDist;
        minDist = ray.len; // set the min dist to the rays length
        i = objects.length; // number of objects to check
        while(i > 0){
            i -= 1;
            minDist = Math.min(objects[i].rayDist(ray), minDist);
        }
        ray.len = minDist;
    }
    //========================================================================
    // Draws all objects
    // objects an array of objects 
    var drawObjects = function(objects){
        var i = objects.length; // number of objects to check
        while(i > 0){
            objects[--i].draw(COLOUR, LINE_WIDTH);
        }
    }
    //========================================================================
    // called on start and resize
    // creats a new scene each time
    // fits the canvas to the avalible realestate
    function reMakeAll(){
        w = canvas.width  = window.innerWidth;
        h = canvas.height = window.innerHeight;
        ctx = canvas.getContext("2d"); 
        screenDiagonal = Math.hypot(window.innerWidth,window.innerHeight);
        if(ray === undefined){
            ray = createRay(0,0,0,screenDiagonal);
        }

        objects.length = 0;
        var i = OBJ_COUNT;
        while( i > 0 ){
            if(Math.random() < 0.5){ // half circles half walls
                objects.push(createWall(rand(0, w), rand(0, h), rand(screenDiagonal * 0.1, screenDiagonal * 0.2), rand(0, Math.PI * 2)));
            }else{
                objects.push(createCircle(rand(0, w), rand(0, h), rand(screenDiagonal * 0.02, screenDiagonal * 0.05)));
            }
            i -= 1;
        }
    }
    //========================================================================
    function mouseMoveEvent(event){
        mouse.x = event.clientX;
        mouse.y = event.clientY;
    }
    //========================================================================
    // updates all that is needed when needed
    function updateAll(time){
        var i;
        ctx.clearRect(0,0,w,h);
        ray.x = mouse.x;
        ray.y = mouse.y;
        drawObjects(objects);
        i = 0;
        while(i < NUMBER_RAYS){
            ray.setDir(i * RAY_DIR_SPACING + time * RAY_ROTATE_SPEED);
            ray.len = screenDiagonal;
            castRay(ray,objects);
            ray.draw(RAY_COLOUR, RAY_LINE_WIDTH);
            i ++;
        }
        requestAnimationFrame(updateAll);
    }
    // add listeners
    window.addEventListener("resize",reMakeAll);   
    canvas.addEventListener("mousemove",mouseMoveEvent);
    // set it all up
    reMakeAll();
    // start the ball rolling
    requestAnimationFrame(updateAll);
&#13;
&#13;
&#13;

答案 1 :(得分:2)

为此,您需要一条线到圆的交叉算法以及墙的线到线交叉点。

对于球你可以使用这个函数 - 我这样做是为了在没有交点的情况下返回数组为空,如果没有交点则返回一个点,如果是正切则返回两个点。

简单地将其开始直线,视线终点以及球的中心位置和半径。在您的情况下,您可能只需要第一点:

function lineIntersectsCircle(x1, y1, x2, y2, cx, cy, r) {

  x1 -= cx; 
  y1 -= cy;
  x2 -= cx; 
  y2 -= cy;

  // solve quadrant    
  var a = (x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1),
      b = 2 * ((x2 - x1) * x1 + (y2 - y1) * y1),
      c = x1 * x1 + y1 * y1 - r * r,
      d = b * b - 4 * a * c,
      dq, p1, p2, t1, t2;

  if (d <= 0 || !a) return [];

  dq = Math.sqrt(d);
  t1 = (-b - dq) / (2 * a);
  t2 = (-b + dq) / (2 * a);

  // calculate actual intersection points
  if (t1 >= 0 && t1 <= 1)
    p1 = {
      x: x1 + t1 * (x2 - x1) + cx,
      y: y1 + t1 * (y2 - y1) + cy
    };

  if (t2 >= 0 && t2 <= 1)
    p2 = {
      x: x1 + t2 * (x2 - x1) + cx,
      y: y1 + t2 * (y2 - y1) + cy
    };

  return p1 && p2 ? [p1, p2] : p1 ? [p1] : [p2]
};

然后对于墙壁,您需要一条线到线的交叉点 - 为矩形的每一边定义一条线。如果存在线重叠,则可能会遇到两个交点,只需忽略第二个交点。

这将返回交叉点的单个点,如果没有交叉点,则返回null:

function getLineIntersection(p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y) {

    var d1x = p1x - p0x,
        d1y = p1y - p0y,
        d2x = p3x - p2x,
        d2y = p3y - p2y,
        d = d1x * d2y - d2x * d1y,
        px, py, s, t;

    if (Math.abs(d) < 1e-14) return null;

    px = p0x - p2x;
    py = p0y - p2y;

    s = (d1x * py - d1y * px) / d;

    if (s >= 0 && s <= 1) {
        t = (d2x * py - d2y * px) / d;
        if (t >= 0 && t <= 1) {
            return {
                x: p0x + (t * d1x),
                y: p0y + (t * d1y)
            }
        }
    }

    return null
}

然后只是通过球阵列迭代,如果没有击中,则遍历墙阵列。

修改小提琴

要使用这些,您必须在每次移动(或每帧更新)时运行这些线。

提示:您可以使函数递归,以便您可以找到交点,根据命中角度计算反射矢量,然后使用最后一次找到下一个交点n次(或者镜头可以移动的总长度)相交点和新角度作为下一行的开始。这样你就可以建立镜头所遵循的路径。

&#13;
&#13;
var canvas = document.querySelector("canvas");
canvas.width = 500;
canvas.height = 300;
var ctx = canvas.getContext("2d"),

  line = {
    x1: 190, y1: 170,
    x2: 0, y2: 0,
    x3: 0, y3: 0
  };

var length = 100;

var circle = {
    x: 400,
    y: 70
};

var wall = {
    x1: 440, y1: 0,
    x2: 440, y2: 100
};

window.onmousemove = function(e) {
  //get correct mouse pos
  var rect = ctx.canvas.getBoundingClientRect(),
      x = e.clientX - rect.left,
      y = e.clientY - rect.top;

  // calc line angle
  var dx = x - line.x1,
      dy = y - line.y1,
      angle = Math.atan2(dy, dx);

  //Then render the line using length as pixel radius:
  line.x2 = line.x1 - length * Math.cos(angle);
  line.y2 = line.y1 - length * Math.sin(angle);
  
  line.x3 = line.x1 + canvas.width * Math.cos(angle);
  line.y3 = line.y1 + canvas.width * Math.sin(angle);
 
  // does it intersect?
  var pts = lineIntersectsCircle(line.x1, line.y1, line.x3, line.y3, circle.x, circle.y, 20);
  if (pts.length) {
    line.x3 = pts[0].x;
    line.y3 = pts[0].y
  }
  else {
    pts = getLineIntersection(line.x1, line.y1, line.x3, line.y3, wall.x1, wall.y1, wall.x2, wall.y2);
    if (pts) {
      line.x3 = pts.x;
      line.y3 = pts.y
    }
  }
  // render
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  
  ctx.beginPath();
  ctx.moveTo(line.x1, line.y1);
  ctx.lineTo(line.x2, line.y2);
  ctx.strokeStyle = '#333';
  ctx.stroke();
  
  ctx.beginPath();
  ctx.moveTo(line.x1, line.y1);
  ctx.lineTo(line.x3, line.y3);
  ctx.strokeStyle = 'red';
  ctx.stroke();
  
  ctx.beginPath();
  ctx.arc(circle.x, circle.y, 20, 0, Math.PI * 2, true);
  ctx.fillStyle = '#333';
  ctx.fill();
  
  // render example wall:
  ctx.fillRect(wall.x1, wall.y1, 4, wall.y2-wall.y1);
}

function lineIntersectsCircle(x1, y1, x2, y2, cx, cy, r) {

  x1 -= cx; 
  y1 -= cy;
  x2 -= cx; 
  y2 -= cy;

  // solve quadrant    
  var a = (x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1),
      b = 2 * ((x2 - x1) * x1 + (y2 - y1) * y1),
      c = x1 * x1 + y1 * y1 - r * r,
      d = b * b - 4 * a * c,
      dq, p1, p2, t1, t2;

  if (d <= 0 || !a) return [];

  dq = Math.sqrt(d);
  t1 = (-b - dq) / (2 * a);
  t2 = (-b + dq) / (2 * a);

  // calculate actual intersection points
  if (t1 >= 0 && t1 <= 1)
    p1 = {
      x: x1 + t1 * (x2 - x1) + cx,
      y: y1 + t1 * (y2 - y1) + cy
    };

  if (t2 >= 0 && t2 <= 1)
    p2 = {
      x: x1 + t2 * (x2 - x1) + cx,
      y: y1 + t2 * (y2 - y1) + cy
    };

  return p1 && p2 ? [p1, p2] : p1 ? [p1] : [p2]
};

function getLineIntersection(p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y) {

  var d1x = p1x - p0x,
      d1y = p1y - p0y,
      d2x = p3x - p2x,
      d2y = p3y - p2y,
      d = d1x * d2y - d2x * d1y,
      px, py, s, t;

  if (Math.abs(d) < 1e-14) return null;

  px = p0x - p2x;
  py = p0y - p2y;

  s = (d1x * py - d1y * px) / d;

  if (s >= 0 && s <= 1) {
    t = (d2x * py - d2y * px) / d;
    if (t >= 0 && t <= 1) {
      return {
        x: p0x + (t * d1x),
        y: p0y + (t * d1y)
      }
    }
  }

  return null
}
&#13;
<canvas></canvas>
&#13;
&#13;
&#13;