Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Line of sight from point

Need to create simple line of sight from point. Length of this line would be adapt to the size of canvas. If line directed to any object (circle, rectangle etc) it must be interrupted after this. I don't know exactly how to describe this, but behavior should be something like this. It's like laser aim in video-games.

Demo jsfiddle. Target line has red color. I think that line must have dynamic length depending on where I will direct it.

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>
like image 551
rmpstmp Avatar asked Jan 07 '23 07:01

rmpstmp


1 Answers

Ray casting

The given answer is a good answer but this problem is better suited to a ray casting like solution where we are only interested in the distance to an intercept rather than the actual point of interception. We only need one point per cast ray so not calculating points will reduce the math and hence the CPU load giving more rays and objects per second.

A ray is a point that defines the start and a normalised vector that represents the direction of the ray. Because the ray uses a normalised vector that is a unit length many calculations are simplified because 1 * anything changes nothing.

Also the problem is about looking for the closest intercept so the intercept functions return a distance from the ray's origin. If no intercept is found then Infinity is returned to allow a valid distance comparison to be made. Every number is less than Infinity.

A nice feature of JavaScript is that it allows divide by zero and returns Infinity if that happens, this further reduces the complexity of the solution. Also if the intercept finds a negative intercept that means the object is behind that raycast origin and thus will return infinity as well.

So first let's define our objects by creating functions to make them. They are all ad hoc objects.

The Ray

// 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);
}

A circle

// 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
     };
}

A wall

Note I changed the wall code in the demo

// 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);
}

So those are the objects, they can be static or moving through the circle should have a setRadius function because I have added a property that holds the square of the radius but I will leave that up to you if you use that code.

Now the intercept functions.

Ray Intercepts

The stuff that matters. In the demo these functions are bound to the objects so that the ray casting code need not have to know what type of object it is checking.

Distance to circle.

// 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;
}

Distance to wall

// 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
}

That covers the objects. If you need to have a circle that is inside out (you want the inside surface then change the second last line of the circle ray function to v += rather than v -=

The ray casting

Now it is just a matter of iterating all the objects against the ray and keeping the distant to the closest object. Set the ray to that distance and you are done.

// 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;
}

A demo

And a demo of all the above in action. THere are some minor changes (drawing). The important stuff is the two intercept functions. The demo creates a random scene each time it is resized and cast 16 rays from the mouse position. I can see in your code you know how to get the direction of a line so I made the demo show how to cast multiple rays that you most likely will end up doing

    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);

An alternative use of above draws a polygon using the end points of the cast rays can be seen at codepen

like image 148
Blindman67 Avatar answered Jan 19 '23 10:01

Blindman67