Coordinates locked in a circle

So, I’m working a project that’s basically a fish bowl with one fish in it (at the moment, in the future you will train massive fish armies and conquer other fish bowls!) when I realized the fish could swim put of the bowl, and that would be bad. So I tried doing a bunch of stuff to make it so if you dragged your fingers outside of the bowl he would still try and swim to them but be blocked. The only time I got it working was when he would touch the edges of the bowl and freeze. But that looks and works stupid. I need to make it so if you moved your finger down a little bit he would still be pushed by the sides of the bowl but get as close to your finger as possible. So, coordinates locked in a circle. How?

Current code:
https://gist.github.com/SkyTheCoder/6599163

the easy answer is to use something like polar coordinates.

You know the center of your circle: let’s say it’s (0,0) and the radius is 10. You know the fish’s origin: say it’s (3,4)

So you can describe the fish’s position in 2 ways: in its distance in x/y units, or its angle and straight-line distance from the center of the bowl.

X/Y coordinates are called Cartesian coordinates. Angle/Distance coordinates are called Polar Coordinates. Think of polar coordinates as radar bearings: when the radar officer says to the captain “target bearing 93 degrees at 45 nautical miles”, he’s using polar coordinates with his own ship as the center.

To convert to polar coordinates:
distance = sqrt(xx + yy)
Angle = asin(x/y)

In this case, the fish at (3,4) is 5 units away from the center at an angle of 48º. (Assuming 0º is straight up.)

Converting polar coordinates to cartesian coordinates is also fairly simple:
x = cos(angle) * distance
y = sin(angle) * distance

To get what you want (clamp the fish’s position inside the bowl), convert the finger position to polar coordinates, clamp the distance to the bowl’s radius, then convert it back to cartesian coordinates. That’s the fish’s new target coordinate.

@SkyTheCoder Try this. Just move your finger around the screen. You can change the dimensions of the bowl to whatever size you need.

EDIT: changed the size of the ellipse (bowl) so the circle stayed inside of the ellipse instead of going halfway out.


function setup()
    a=200 -- width of bowl
    b=100 -- height of bowl
    xc=WIDTH/2
    yc=HEIGHT/2
    tx=xc
    ty=yc
    sx=tx
    sy=ty
    st=false
    hx=sx
    hy=sy
end

function draw()
    background(40, 40, 50)
    
    fill(0, 255, 186, 255)
    ellipse(xc,yc,a*2+30,b*2+30)
    
    if not st then
        v1=vec2(tx,ty)
        d=v1:dist(vec2(sx,sy))
        dx=(tx-sx)/d*3
        dy=(ty-sy)/d*3
        sx=sx+dx
        sy=sy+dy       
        if (sx-xc)^2/a^2+(sy-yc)^2/b^2 <= 1 then 
            hx=sx
            hy=sy
        else
            sx=hx
            sy=hy            
        end
    end
    fill(255, 4, 0, 255)
    ellipse(sx,sy,30)  -- circle  
end

function touched(t)
    if t.state==MOVING then
        st=false
        tx=t.x
        ty=t.y
    end
    if t.state==ENDED then
        st=true
    end    
end

@dave1707 I’m gonna test out @tomxp411’s solution, but just because I’m curious, can you explain what xc, yc, tx, ty, sx, sy, st, hx, and hy are?

@SkyTheCoder xc,yc are the center x,y values of the ellipse (fish bowl). tx,ty are the touched x,y values as you move your finger around the screen. sx,sy are the x,y coordinates of the red circle (fish). hx,hy are the hold x,y values so the red circle (fish) doesn’t move outside of the ellipse (bowl). st is to stop the red circle (fish) from moving if you lift your finger from the screen.

@tomxp411 - you should write tutorials, you write very clearly =D>

thanks, @Ignatz. I appreciate that. I do have to write technical docs for work, so I’ve had some practice. I want to go back to school, get my Master’s degree, and teach Computer Science. I have been giving that a lot of thought lately.

@tomxp411 - well if you want to practise, you can write guest posts on my blog, I have a lot of Codea followers…

Professional integrity compels me to step in here …

@SkyTheCoder Use @dave1707’s code.

@dave1707 I’m curious. Why don’t you make more use of vectors? I think it would make things clearer.

In touched:

if t.state==MOVING then
        st=false
        tpt = vec2(t.x,t.y) -- "touch point"
end

Then in draw, make spt the centre of the “fish” and cpt the centre of the “bowl”:

local opt = spt          -- save the original point, no need for this to be global
d=tpt:dist(spt)
dv = (tpt - spt)/d*3  -- delta velocity, move towards the touch point
spt = spt + dv          -- update the position
local rpt = spt - cpt  -- vector from the centre of the bowl to the fish
rpt.x = rpt.x/a          -- rescale to "bowl" coordinates
rpt.y = rpt.y/b
if rpt:lenSq() > 1 then
   spt = opt               -- outside the bowl, so undo all we've done
end

@tomxp411 One mathematical mistake: the angle is the inverse tangent, not the inverse sine. Moreover, atan(y/x) only gives the angle up to a half circle ambiguity since tan(&theta;) = tan(&theta; + &pi;) (or 180 if you prefer degrees). Fortunately there is a math.atan2 function which resolves this: math.atan2(y,x) gives the angle with no ambiguity.

But as @dave1707’s code shows, there is no need to use angles for this. Angles are, as a rule, messy and should be avoided whenever possible. In particular, any workflow that involves converting to polar coordinates and then back again is probably better worked to avoid the conversion in the first place.

I didn’t like the fact that once it reaches the edge of the bowl then it stays there. This still isn’t all that great - it would be better with forces instead of simply updating the position. But at least the “fish” tries to follow the touch a bit more when it is at the edge of the bowl.

If the movement of the “fish” would take it out of the bowl then this code moves it back inside, but rather than just put it back where it was it moves it back to (an approximation of) the nearest point. This allows it to “roll” along the inside of the bowl.

function setup()
    a=200 -- width of bowl
    b=100 -- height of bowl
    fb = vec2(WIDTH/2,HEIGHT/2)
    fpt = fb
end

function draw()
    background(40, 40, 50)
    
    fill(0, 255, 186, 255)
    ellipse(fb.x,fb.y,a*2+30,b*2+30)


    if st then
        local opt = fpt
        fpt = fpt + (tpt - fpt):normalize()*3
        local rpt = fpt - fb
        rpt.x = rpt.x/a
        rpt.y = rpt.y/b
        if rpt:lenSqr() > 1 then
            fpt = fpt - vec2(b^2 * rpt.x, a^2 * rpt.y):normalize()*4
        end
    end
    fill(255, 4, 0, 255)
    ellipse(fpt.x,fpt.y,30)  -- circle  
end

function touched(t)
    if t.state==MOVING then
        st=true
        tpt = vec2(t.x,t.y)
    end
    if t.state==ENDED then
        st=false
    end    
end

(Interestingly, I tried to replace the enlargening of the ellipse with stroke(30) but it appears that the stroked part is entirely inside the ellipse, rather than half inside and half outside.)

@Andrew_Stacey My not using vectors is more from habit than anything else. I’ve coded in ‘C’ for so long that I actually think in ‘C’. When someone posts a question and I read it, the ‘C’ code is already written in my head. The thing that takes the time is keying the code and converting any needed ‘C’ code to Lua. Since I never used vectors in any of my ‘C’ programs, it’s not part of my thinking. I always used x,y or something x, something y for coordinates. I don’t even think that much about a lot of the code I post here, it’s almost automatic. Getting back to the fish bowl program, the circle is bouncing against the side too fast. Watching a real fish, it touches the bowl 1 or 2 times a second when it’s at the edge. That might make it look more realistic when a fish image is used.

Yes, the bouncing is because I’m just updating position (as did your original code) rather than working out collisions and forces. If I adjust the parameters to make the bounce less then there’s a danger it’ll tunnel out of the bowl! So this isn’t a great solution.

@Andrew_Stacey I don’t want you to get caught up writing posts explaining it to me, but I have no idea how yours or @dave1707’s code works. At each line with math, could you add a comment explaining what it does, how it works, and why it is needed? I want to memorize it so I can use it later, and I can’t remember it if it doesn’t make sense.

I got my own code kind of working to clamp the fish into a circle, but it’s glitchy and the fish sometimes drifts to the top of the bowl. It does look like a fish’s behavior, and I might use it, but I want your opinions. Code: https://gist.github.com/SkyTheCoder/6616978

@tomxp411 Also, when I added in polar/Cartesian coordinate conversion, I tried printing it to see how it looked. Most of the time it prints (nan, nan). Any idea why?

Sky, I didn’t actually get a chance to write any code for your solution, so there’s probably something I missed. My last geometry class was 30 years ago, so I am having to re-discover some of this as I go. But in thinking about it, I realized the polar coordinate conversion isn’t actually necessary. Just create a vector from the bowl center to your finger and then clamp it to the length of the bowl’s radius… I think that’s what Andrew’s code is doing. Better variable names and some commenting would clear that up a lot.

Come to think of it, I’m not sure why I wanted to use polar coordinates when you can do this just by normalizing the vector from the bowl center to the touch.

  • Get the distance from the center to the touch point

  • is that distance > than bowl radius?

  • if yes, get the vector between the center and the touch point and normalize it. Then multiply by the radius and nudge your fish toward that point.

As to tutorials, you see what happens when I rush out half-baked ideas, and I simply don’t have the time to devote to doing it properly. I wish I had more free time, but I like having a job and a paycheck, too. :slight_smile:

Just ran your demo. That’s a cute little fish you got there!

Here’s some commented code.

function setup()
    --[[
    Define a "fishbowl", by rights I'd make this an object of a class but this is a
    simple demo.  We define its characteristics with hopefully obvious names.
    Though "height" and "width" are actually the half-height and half-width.
    --]]
    fishbowl = {
        width = 200,
        height = 100,
        centre = vec2(WIDTH/2,HEIGHT/2),
        colour = color(0, 255, 186, 255)
    }
    --[[
    Now the same for the fish.  The "thrust" is how much force it can produce when
    swimming.  It would be possible to make this variable in some way to
    correspond to the fish getting tired.
    --]]
    fish = {
        position = vec2(WIDTH/2,HEIGHT/2),
        velocity = vec2(0,0),
        size = 30,
        colour = color(255, 4, 0, 255),
        thrust = 100,
    }
    -- friction is the amount of drag by the water on the fish.
    friction = 1
    --[[
    When it bumps its head on the edge, we remember the time.  If it bumps
    twice in quick succession then it doesn't bounce but stays by the glass.
    Fish don't really bounce when they bump against the edge of a tank, but they
    do flinch and swim backwards away which is sort of like bouncing.  But as it
    is voluntary we assume that if the two collisions are close enough in time then
    it remembers the first and doesn't flinch.
    --]]
    collisionTime = 0
    -- This is how far apart collisions can be for the fish to not flinch.
    collisionDelta = 1
end

function draw()
    background(40, 40, 50)

    --[[
    Draw the fishbowl, making it larger by the fish's size so that when the
    position of the fish is at the edge of the bowl then the whole of the fish
    is still inside the bowl.
    --]]

    fill(fishbowl.colour)
    ellipse(fishbowl.centre.x,
            fishbowl.centre.y,
            fishbowl.width*2+fish.size,
            fishbowl.height*2+fish.size)

    --[[
    Now we update the fish's data.  We start by computing the force on the
    fish.
    --]]
    local force = vec2(0,0)
    --[[
    If the touch is active then the fish feels an attractive force towards it.
    The direction of the force is the vector from its current position to the
    touch point.  This is given by the vector `touchpt - fish.position`.
    However, that could be of arbitrary magnitude so we normalise it to
    make it unit length.  Then we multiply by `fish.thrust` to get it to be
    the right magnitude.
    --]]
    if touchpt then
        force = (touchpt - fish.position):normalize()*fish.thrust
    end
    --[[
    A crude model of friction is that it is proportional to the speed of the object.
    This is opposes the motion, so is ` - friction * fish.velocity`.
    --]]
    force = force - friction * fish.velocity
    --[[
    Now we update the position using the OLD velocity.  For small time steps,
    speed = distance/time so distance = speed * time.  This is the distance
    travelled in the time step so we add it to our current position.
    --]]
    fish.position = fish.position + DeltaTime * fish.velocity
    --[[
    We do the same with the velocity.  We're using Newton's equation here,
    F = m a, where a is the acceleration.  So we should divide by the fish's
    mass here, but we can just assume it to be 1 unit for simplicity.
    --]]
    fish.velocity = fish.velocity + DeltaTime * force
    --[[
    The next bit deals with the possibility that the fish has bumped against
    the edge of the bowl.  We start by figuring this out.  The equations are
    simpler if we work relative to the centre of the fishbowl so we define
    a vector as the relative position of the fish to the fishbowl.
    --]]
    local relativept = fish.position - fishbowl.centre
    --[[
    As our fishbowl might be elliptical, the equations are simpler if we
    scale so that the units are "fishbowl units".  That is, 1 unit across is
    half the fishbowl width, and 1 unit up is half the fishbowl height.
    --]]
    relativept.x = relativept.x/fishbowl.width
    relativept.y = relativept.y/fishbowl.height
    --[[
    We want to know the "normal" vector to the fishbowl at the point of
    collision.  This is the vector that points straight out of the fishbowl.
    A bit of geometry or calculus says that it is (b x/a, a y/b) where (x,y)
    is the position on the fishbowl edge and (a,b) are the half-width and
    half-height.  As "relativept" has already been rescaled, we've already
    done the divisions and just need to do the multiplications.  We
    renormalise to make it unit length (which makes life easier later).
    --]]
    local normal = vec2(relativept.x*fishbowl.height,
                        relativept.y*fishbowl.width):normalize()
    --[[
    Now we test to see if we've collided with the bowl edge.  This happens
    if the current position (which has been updated but not yet drawn) is
    outside the fishbowl.  The test for this is that the length of the relative
    position vector in fishbowl-coordinates is greater than 1.
    But it might be that we're outside but the collision happened last cycle.
    (The amounts we move in each cycle are so small that the user won't
    have noticed that we're outside.)
    We detect this by looking at the direction the fish is swimming in.  If it
    is heading outwards then the collision hasn't happened yet so we
    register it now.  If it is heading inwards then the collision has already
    been dealt with and we don't need to do anything.
    We test for this by taking the dot product of the velocity vector and the
    normal vector.  The dot product can be used to test if two vectors are
    in roughly the same direction or not.  If it is positive then they point
    roughly the same way, if negative then opposite ways.
    --]]
    if relativept:lenSqr() > 1 and
        fish.velocity:dot(normal) > 0
        then
    --[[
    Okay, so there was a collision.  If there wasn't a lot of time since the
    last collision then we just move back to the fishbowl edge.
    This is not completely accurate, but future draw cycles will adjust it
    so that the user can't tell that it isn't perfect.
    If there was a lot of time then we "bounce".  This is done by adjusting the
    velocity vector so that whatever pointed out of the fishbowl is now pointing
    back into it.  Technically, we reflect the velocity vector in the line tangential
    to the fishbowl at the point of contact.  Turns out this is dot products again.
    --]]
        if ElapsedTime - collisionTime < collisionDelta then
            fish.velocity = fish.velocity - fish.velocity:dot(normal) * normal
        else
            fish.velocity = fish.velocity - 2*fish.velocity:dot(normal) * normal
        end
    --[[
    Now we store the latest collision time so that we can make the right choice
    next time around.
    --]]
        collisionTime = ElapsedTime
    end

    --[[
    After all that, we draw the "fish".
    --]]
    fill(fish.colour)
    ellipse(fish.position.x,fish.position.y,fish.size)
end

function touched(t)
    --[[
    If a touch is active, we set "touchpt" to be its vector, otherwise to nil.
    --]]
    if t.state ~= ENDED then
        touchpt = vec2(t.x,t.y)
    else
        touchpt = nil
    end    
end

@Andrew_Stacey You forgot to include code for when a fish dies or when the other fish are eating the dead fish and ignoring where your finger is. Then there’s the code for when a fly falls in the water and all the fish are going after it and again ignoring your finger. There’s a lot more code you need to document. Nice detail. :slight_smile:

@dave1707 :))
@Andrew_Stacey - nice!

@Ignatz Well, you keep complaining that my code isn’t adequately commented! Be careful what you wish for …

@dave1707 Fish = class(DumbAnimal) should fix that.

@Andrew_Stacey - we like to understand what’s behind the mathemagic. :smiley: