Howdy, Stranger!

It looks like you're new here. If you want to get involved, click one of these buttons!

Making CHAIN physics bodies follow sine curves from any point to any other

edited October 9 in Questions Posts: 1,547

I’ve been trying to understand the “test edges and chains“ example in PhysicsLab.

I tried to pick the code apart and explicate the variables, but I’m not sure I did it right:


-- test edges & chains local points = {} local overallY = WIDTH * 0.08 local peaksMultiple = WIDTH * 0.00003 local waveHeight = HEIGHT * 0.025 local segmentLength = WIDTH/150 for i = 0, WIDTH, segmentLength do table.insert(points, vec2(i,(math.sin(i*peaksMultiple) * waveHeight) + overallY)) end local ground = physics.body(CHAIN, false, table.unpack(points)) self.lab:addBody(ground)

What I would like to do is create a gently sloping curve, just one, with its peak in the middle of the screen, kind of like the curve in the attached picture.

I’ve been able to make that happen on a single device in a single orientation, just by fiddling around with the variables until it looked right, but I can’t figure out how to do it on any device in any orientation.

Tagged:

Comments

  • dave1707dave1707 Mod
    Posts: 9,725

    @UberGoober You can start with this.

    viewer.mode=STANDARD
    
    function setup()
        parameter.integer("high",0,500,200)
        parameter.integer("step",45,720,280)
        fill(255)
    end
    
    function draw()
        background()   
        for x=0,WIDTH do
            y=math.sin(math.rad(x/(WIDTH/step)))*high
            ellipse(x,y+HEIGHT/2,4)        
        end
    end
    
  • Posts: 1,547

    @dave1707 thanks, that gave me what I needed to figure this out.

    The adapted version below shows the solution I needed.

    In the screen shots you can see that, in portrait mode, both waves look the same, because they both use the same height and step values—but the difference is that the top one is using hard-coded values and the bottom one is calculating those values relative to screen width.

    That’s why (I think) in landscape mode the curves look different. The hard-coded values result in a different width-to-height ratio for some reason.

    But the lower curve keeps the same ratio as in portrait because (I think) it uses a ratio to calculate its dimensions to begin with.


    viewer.mode=OVERLAY function setup() parameter.integer("high",0,500,50) parameter.integer("step",45,720,180) local widthRatio = 50 / math.min(WIDTH, HEIGHT) waveHeight = WIDTH * widthRatio local points = {} local overallY = 130 local thisStep = 180 for i = 0, WIDTH do table.insert(points, vec2(i, overallY + math.sin(math.rad(i/(WIDTH/thisStep))) * waveHeight)) end ground = physics.body(CHAIN, false, table.unpack(points)) end function draw() background() stroke(255) for x=0,WIDTH do y=math.sin(math.rad(x/(WIDTH/step)))*high ellipse(x,y+HEIGHT/2,4) end strokeWidth(4) stroke(167, 236, 67) local points = ground.points for j = 1,#points-1 do a, b = points[j], points[j+1] line(a.x, a.y, b.x, b.y) end end
  • edited October 9 Posts: 1,547
    @dave1707 kind of tangentially (heh?) I wonder if you know how to draw the curve between any two points. Like to go from the lower left of the screen to the upper right, for example. I’ve been trying to figure it out but I don’t think my code’s even worth posting. Thanks for your help always.
  • Posts: 1,275

    if you can draw it from, say, x=0 to x-n, you can scale by desired/n to get it 0-desired. you can translate (x,y) to get it to start at x,y. you can rotate to get it to any desired angle. don't forget to push and pop the matrix.

  • dave1707dave1707 Mod
    Posts: 9,725

    @UberGoober I guess that would depend on how much of the sine curve you want between the 2 points. In my example above, you can adjust the curve so the right side of the curve would pass through some point on the right side of the screen.

  • edited October 9 Posts: 1,547

    @RonJeffries I am working with Physics bodies, and translate easily changes where something is drawn (like in your article), but with Physics bodies I need to change where it actually is.

    The equivalent of using translate, I guess, would be to make the wave horizontally, like normal, move its origin to the midpoint of the desired points, and then change its angle so each end touches the desired points—that’s a lot of math, though, and if there’s a way to just directly calculate the same points using math.sin that would be better, I think.

    @dave1707 I’m trying to write a method along the lines of makeWaveBetweenPoints(pointA, pointB, numPeaks, heightOfPeaks) so I need to control start point, end point, and the wave properties separately.

  • edited October 9 Posts: 1,547

    I’ve found this formula on Stack Overflow:


    ParametricPlot[{t/Sqrt[2] - Sin[t]/Sqrt[2], t/Sqrt[2] + Sin[t]/Sqrt[2]},{t,-10,10}]

    Which looks like this: https://www.wolframalpha.com/input/?i=ParametricPlot[{t/Sqrt[2]+-+Sin[t]/Sqrt[2],+t/Sqrt[2]+++Sin[t]/Sqrt[2]},{t,-10,10}]

    …I’m trying to figure out how to apply it….

  • dave1707dave1707 Mod
    Posts: 9,725

    @UberGoober Try this for starters.

    viewer.mode=STANDARD
    
    function setup()
        parameter.integer("ang",0,90,30)
        parameter.number("size",.1,5,1)
        parameter.integer("high",50,300)
        parameter.integer("repeats",1,20,3)
        fill(255)
    end
    
    function draw()
        background(0)
        for z=0,360*repeats do
            x2=z
            y2=math.sin(math.rad(z))*high
            x1=x2*size*math.cos(math.rad(ang))-y2*math.sin(math.rad(ang))
            y1=y2*math.cos(math.rad(ang))+x2*size*math.sin(math.rad(ang))
            ellipse(x1,y1,3)
        end
    end
    
  • Posts: 1,547

    @dave1707 that’s fantastic! I’ve been playing with that formula I found for hours and I’ve been getting only hideous results.

  • edited October 9 Posts: 1,547

    @dave1707 ok I think this does everything I need, though I’m not sure my modifications are done in the best way:


    viewer.mode=OVERLAY function setup() ang = 30 parameter.number("size",.1,5,1) parameter.integer("high",0,300, 150) parameter.number("repeats",0,20,3, function() if not lastTouch then lastTouch = vec2(WIDTH, HEIGHT) end calculateSizeFromLastTouch() end) parameter.number("startX", 0, WIDTH) parameter.number("startY", 0, HEIGHT) fill(255) end function angleBetween(x1, y1, x2, y2) local dx, dy, radsToDegrees dx = x2 - x1 dy = y2 - y1 radsToDegrees = 180 / math.pi return math.atan2(dy, dx) * radsToDegrees end function draw() background(0) fill(80, 161, 233) for z=0,360*repeats do x2=z y2=math.sin(math.rad(z))*high x1=x2*size*math.cos(math.rad(ang))-y2*math.sin(math.rad(ang)) y1=y2*math.cos(math.rad(ang))+x2*size*math.sin(math.rad(ang)) ellipse(x1 + startX,y1 + startY,3) end end function calculateSizeFromLastTouch() size = vec2(startX, startY):dist(lastTouch) * 0.0027 / repeats end function touched(touch) lastTouch = touch.pos ang = angleBetween(startX, startY, touch.x, touch.y) calculateSizeFromLastTouch() end

    In particular the fact that I’m using the decimal 0.0027 as a multiplier in calculateSizeFromLastTouch seems completely arbitrary.

    There must be a way to make that calculation based only on known quantities.

  • Posts: 1,547

    !!!

    This is functionally exactly the same code as above, except I attempted to actually use it to create CHAIN-type physics bodies.

    It seems to work fine… except it crashes Codea really quickly.

    —any idea why??


    viewer.mode=OVERLAY function setup() ang = 30 parameter.number("size",.1,5,1) parameter.integer("high",0,300, 150) parameter.number("repeats",0,20,3, function() if not lastTouch then lastTouch = vec2(WIDTH, HEIGHT) end calculateSizeFromLastTouch() end) parameter.number("startX", 0, WIDTH) parameter.number("startY", 0, HEIGHT) fill(255) lastSize, lastHigh, lastRepeats, lastStartX, lastStartY = 0,0,0,0,0 end function makePhysicsBodyFromCurve() if physCurve then physCurve:destroy() end physCurve = nil if points then physCurve = physics.body(CHAIN, false, table.unpack(points)) end collectgarbage() end function angleBetween(x1, y1, x2, y2) local dx, dy, radsToDegrees dx = x2 - x1 dy = y2 - y1 radsToDegrees = 180 / math.pi return math.atan2(dy, dx) * radsToDegrees end function draw() background(0) fill(80, 161, 233) points = {} for z=0,360*repeats do x2=z y2=math.sin(math.rad(z))*high x1=x2*size*math.cos(math.rad(ang))-y2*math.sin(math.rad(ang)) y1=y2*math.cos(math.rad(ang))+x2*size*math.sin(math.rad(ang)) x1= x1 + startX y1 = y1 + startY ellipse(x1,y1,3) table.insert(points, vec2(x1,y1)) end if lastSize ~= size or lastHigh ~= high or lastRepeats ~= repeats or lastStartX ~= startX or lastStartY ~= startY then makePhysicsBodyFromCurve() lastSize = size lastHigh = high lastRepeats = repeats lastStartX = startX lastStartY = startY end pushStyle() strokeWidth(3.0) stroke(233, 80, 193) for j = 1,#physCurve.points-1 do a = points[j] b = points[j+1] line(a.x, a.y, b.x, b.y) end popStyle() end function calculateSizeFromLastTouch() size = vec2(startX, startY):dist(lastTouch) * 0.0027 / repeats end function touched(touch) lastTouch = touch.pos ang = angleBetween(startX, startY, touch.x, touch.y) calculateSizeFromLastTouch() end
  • Posts: 1,275

    ok, but why do the math for yourself when codea will do it for you?

  • edited October 9 Posts: 1,547

    @RonJeffries not sure what you mean—there’s math I gotta do either way, right?

    I have to calculate the sine wave either way, Codea won’t do that, and then to rotate it into the right position I have to manually calculate the midpoint between the target points, and calculate the angle, and yadda yadda yadda.

    But the separation of the creation and placement of the physics body is kind of weird, to me, in that it seems odd to open myself to an opportunity for a whole separate category of errors—errors rooted in creating the thing separately from placing it.

    I have to do the sine calculations no matter what (don’t I?) so why add an extra layer of opportunities for failure when I could just do all the calculations in the same formula that calculates the wave?

  • Posts: 1,547

    @John, @Simeon, I think this may be a bug in 2D physics CHAIN handling.

    This code boils it down: all it takes to cause a crash is to put your finger down anywhere and then drag it towards the start of the curve—at least, that’s what’s been crashing my iPad twenty times an hour this evening.

    The reason I think it’s a bug is that if you make the body a POLYGON instead of a CHAIN the crash does not happen—which makes it seem likely there’s something going wrong with CHAINs.


    viewer.mode=OVERLAY function setup() ang, size, shouldRemake = 30, 1, true stroke(255, 0, 174) strokeWidth(3.0) end function draw() background(0) points = {} for z=0,360 do x2=z y2=math.sin(math.rad(z))*150 x1=x2*size*math.cos(math.rad(ang))-y2*math.sin(math.rad(ang)) y1=y2*math.cos(math.rad(ang))+x2*size*math.sin(math.rad(ang)) x1= x1 + 300 y1 = y1 + 300 table.insert(points, vec2(x1,y1)) end if shouldRemake then if physCurve then physCurve:destroy() end physCurve = physics.body(CHAIN, false, table.unpack(points)) -- if you comment out the CHAIN version above and uncomment -- the POLYGON version below, the crash does not happen -- physCurve = physics.body(POLYGON, table.unpack(points)) shouldRemake = false end for j = 1,#physCurve.points-1 do a = points[j] b = points[j+1] if a and b then line(a.x, a.y, b.x, b.y) end end end function touched(touch) ang = math.atan2(touch.y - 300, touch.x - 300) * (180 / math.pi) size = vec2(300, 300):dist(touch.pos) * 0.0027 shouldRemake = true end
  • dave1707dave1707 Mod
    edited October 10 Posts: 9,725

    I think the reason is that CHAIN can’t handle duplicate points in a row. Run this code and drag your finger on the screen. When cnt reaches 80, I create a duplicate occurrence in the table tab. As long as you don’t exceed 80 it doesn’t crash. When you lift your finger, I create a chain.

    viewer.mode=STANDARD
    
    function setup()
        parameter.watch("cnt")
        fill(255)
        tab={}
    end
    
    function draw()
        background(0)
        for a,b in pairs(tab) do
            ellipse(b.x,b.y,4)
        end
    end
    
    function touched(t)
        if t.state==BEGAN then
            tab={}
            cnt=0
        end  
        if t.state==CHANGED and t.x>0 then
            cnt=cnt+1
            table.insert(tab,vec2(t.x,t.y))
            if cnt==80 then
                print("duplicate")
                table.insert(tab,vec2(t.x,t.y))
            end
        end
        if t.state==ENDED then  
            s=physics.body(CHAIN, false, table.unpack(tab))
        end
    end
    
  • Posts: 1,547

    @dave1707 your example looks solid, what’s wrong with mine, below?

    I’m trying to detect duplicate points and discard them, but it still crashes.

    Am I doing it wrong?


    viewer.mode=OVERLAY function setup() ang, size, shouldRemake = 30, 1, true stroke(255, 0, 174) strokeWidth(3.0) end function draw() background(0) points = {} if shouldRemake then for z=0,360 do x2=z y2=math.sin(math.rad(z))*150 x1=x2*size*math.cos(math.rad(ang))-y2*math.sin(math.rad(ang)) y1=y2*math.cos(math.rad(ang))+x2*size*math.sin(math.rad(ang)) x1= x1 + 300 y1 = y1 + 300 local notDuplicate = true for _, pnt in ipairs(points) do if pnt.x == x1 and pnt.y == y1 then notDuplicate = false end end if notDuplicate then table.insert(points, vec2(x1,y1)) else print("discarding duplicate") end end if physCurve then physCurve:destroy() end physCurve = physics.body(CHAIN, false, table.unpack(points)) shouldRemake = false end for j = 1,#physCurve.points-1 do a = physCurve.points[j] b = physCurve.points[j+1] if a and b then line(a.x, a.y, b.x, b.y) end end end function touched(touch) ang = math.atan2(touch.y - 300, touch.x - 300) * (180 / math.pi) size = vec2(300, 300):dist(touch.pos) * 0.0027 shouldRemake = true end
  • Posts: 1,275

    Here's an article I wrote on using the translate, rotate, and scale capabilities of Codea to draw a sine wave at various sizes and angles. It's not solving the problem here, but the techniques are those I would use to solve this one too.

    Note that it only calculates the curve once, in setup.

    Questions welcome. Code follows.

    Article: The Sines of the Fathers

    -- Demonstrate translate, rotate, scale
    -- RJ 20211010
    
    function setup()
        print("This example draws a two-cycle sine wave between the first two touches on the screen.")
        touches = {}
        sine = {}
        size = 50
        local step = 0.1
        for x = 0,4*math.pi,step do
            table.insert(sine, vec2(size*x,size*math.sin(x)))
        end
    end
    
    function touched(touch)
        if touch.state == ENDED or touch.state == CANCELLED then
            touches[touch.id] = nil
        else
            touches[touch.id] = touch
        end
    end
    
    function draw()
        background(0, 0, 0, 255)
        stroke(255)
        strokeWidth(3)
        for k,touch in pairs(touches) do
            math.randomseed(touch.id)
            fill(math.random(255),math.random(255),math.random(255))
            ellipse(touch.pos.x, touch.pos.y, 100, 100)
        end
        local myTouches = {}
        for k,touch in pairs(touches) do
            table.insert(myTouches,touch)
        end
        pushMatrix()
        local pos1, pos2
        if #myTouches == 0 then
            translate(WIDTH/4, HEIGHT/2)
        else 
            pos1 = myTouches[1].pos
            translate(pos1.x,pos1.y)
        end
        if #myTouches >= 2 then
            local pos2 = myTouches[2].pos
            local ang = math.atan(pos2.y-pos1.y, pos2.x-pos1.x)
            rotate(math.deg(ang))
            local dist = pos2:dist(pos1)
            scale(dist/(4*size*math.pi))
        end
        drawSine()
        popMatrix()
    end
    
    function drawSine()
        for i = 2,#sine do
            local p0 = sine[i-1]
            local p1 = sine[i]
            line(p0.x,p0.y, p1.x,p1.y)
        end
        left = vec2(-10,0)
        right = vec2(10 + size*4*math.pi, 0)
        line(left.x,left.y, right.x,right.y)
    end
    
  • Posts: 1,275

    I see the thread name changed. Confused me :smile:

    I'm not sure how one would do the same thing with physics, certainly not with the screen functions. Different problem, different tools. Sorry for any confusion, HTH as always.

  • dave1707dave1707 Mod
    Posts: 9,725

    @UberGoober You only need to check the current set of points with the last set of points. There can be duplicate points, just not 2 in a row. I don’t know if CHAIN subtracts the last 2 sets of points and if it’s 0 then it crashes.

  • dave1707dave1707 Mod
    Posts: 9,725

    @UberGoober Not sure why yours is crashing. Maybe there’s something else that’s happening that we don’t see.

  • edited October 10 Posts: 1,275

    @dave1707 can you explain easily how you got those equations? thanks!

    i changed @UberGoober 's to just check the last (prev) element, still crashes. no idea why yet.

  • dave1707dave1707 Mod
    Posts: 9,725

    @RonJeffries Read your latest article, The Sines of the Father. You mentioned that nobody wanted to use the translate, rotate, and scale functions. Using those functions would be an easy way to draw a sine curve of any size and angle. I think you missed what was needed. @UberGoober wanted a sine curve of any size and angle to create a physics CHAIN object of the sine curve. To create the CHAIN, a table of points are needed. To create the table, the points of the sine curve had to be calculated based on the size and angle of the sine curve. So each time the angle or size was changed, a new table was needed. Using the translate, rotate, and scale functions only altered what was being drawn and not the actual points.

  • dave1707dave1707 Mod
    Posts: 9,725

    @RonJeffries Just saw your question. That appeared while I was writing the above post. Anyways, I did a Google search for rotating a point around the origin. I’ll see if I can find the page I found.

  • Posts: 1,275

    .0027 is about 1/360. (370 really) so i think it's compensating for the width scale somehow.

  • dave1707dave1707 Mod
    Posts: 9,725

    @RonJeffries That didn’t take long, first try. Here’s the link.

    https://academo.org/demos/rotation-about-point/
    
  • edited October 10 Posts: 1,275

    yes, i missed the chain thing. still no idea why it crashes. i'll see if i can come up with anything interesting.

  • dave1707dave1707 Mod
    Posts: 9,725

    @RonJeffries I would calculate the points for the sine curve along the x axis. For each point I would then recalculate the rotated point based on the angle. That would give me a table for the CHAIN function and for drawing the curve.

  • Posts: 1,275

    this is equivalent to @UberGoober 's (and still crashes). Just showing relevant bits in context.

    if shouldRemake then
            prev=nil
            for z=0,360 do
                r = math.rad(z)
                pt = vec2(size*z,math.sin(r)*150)      
                curr = pt:rotate(math.rad(ang)) + vec2(300,300)
                if prev~=curr then
                    table.insert(points, curr) 
                    prev=curr
                else
                    print("discarding duplicate")
                end
            end
            if physCurve then physCurve:destroy() end
            physCurve = physics.body(CHAIN, false, table.unpack(points))
            shouldRemake = false
        end
    
  • edited October 10 Posts: 1,275

    mine also calcs sin on x axis, then rotates, then translates to 300,300.

  • dave1707dave1707 Mod
    Posts: 9,725

    @RonJeffries Let me play with your code. It might do the exact same thing mine did. If that’s the case then I didn’t need those 2 calculations.

  • Posts: 1,547

    With this iteration I think I have exactly what I need.

    @RonJeffries, turns out you’re exactly right—360 is the key number.

    This uses POLYGON instead of CHAIN to avoid the crash.


    viewer.mode=OVERLAY function setup() parameter.number("spaceBetweenPeaks",.1,5,0.5) parameter.integer("waveHeight",1,300, 90) parameter.number("numWaves",0,10,4) ang = 30 start = physics.body(CIRCLE, 30) start.type = STATIC start.position = vec2(300, 300) target = physics.body(CIRCLE, 30) target.type = STATIC target.position = vec2(600, 600) waveNeedsMaking = true end function makePhysicsWaveBetween(x1, y1, x2, y2) calculateSpacingToTarget() points = {} --angle between points: ang = math.atan2(y2 - y1, x2 - x1) * 180 / math.pi for z=0, 360*numWaves, 22 do x2=z y2=math.sin(math.rad(z))*waveHeight x1=x2*spaceBetweenPeaks*math.cos(math.rad(ang))-y2*math.sin(math.rad(ang)) y1=y2*math.cos(math.rad(ang))+x2*spaceBetweenPeaks*math.sin(math.rad(ang)) x1= x1 + start.x y1 = y1 + start.y ellipse(x1,y1,3) table.insert(points, vec2(x1,y1)) end if physCurve then physCurve:destroy() end physCurve = nil if points then --physCurve = physics.body(CHAIN, false, table.unpack(points)) physCurve = physics.body(POLYGON, table.unpack(points)) end end function draw() background(0) makePhysicsWaveBetween(start.x, start.y, target.x, target.y) fill(80, 233, 126) ellipse(start.x, start.y, 30) fill(80, 161, 233) ellipse(target.x, target.y, 30) pushStyle() strokeWidth(3.0) stroke(233, 80, 193) if physCurve then for j = 1,#physCurve.points-1 do a = points[j] b = points[j+1] line(a.x, a.y, b.x, b.y) end end popStyle() end function calculateSpacingToTarget() --this is: length / (size * repeats)) = 360 --so: size = length / repeats * 360 local dist = start.position:dist(target.position) spaceBetweenPeaks = dist / (numWaves * 360) end -- this function makes the project stop with an error, not sure why -- so it’s not actually used here function calculateNumWavesToTarget() --this is: length / (size * numWaves)) = 360 --so: length / size * 360 = numWaves local dist = start.position:dist(target.position) numWaves = dist / (spaceBetweenPeaks * 360) end function touched(touch) if target:testPoint(touch.pos) then target.position = touch.pos elseif start:testPoint(touch.pos) then start.position = touch.pos end end
  • dave1707dave1707 Mod
    edited October 10 Posts: 9,725

    @RonJeffries In your rotate function below, you’re converting ang to radians. The rotate command uses degrees, not radians.

    curr = pt:rotate(math.rad(ang)) + vec2(300,300)
    

    PS. The documentation says rotate uses degrees, but it looks like it really uses radians, so your calculation is correct.

  • dave1707dave1707 Mod
    Posts: 9,725

    @Simeon Something is wrong with trying to copy code from the forum. Sometimes I can’t even select code, other times I can select some code but when I try to expand to highlighted copy area, as soon as I move the drag icon, the screen just scrolls halfway up the discussion over several posts and I can’t copy anything.

  • edited October 10 Posts: 1,275

    vec2 rotate takes radians. screen rotate takes degrees. codea is weird. :wink:

    i'm sure it does same as yours. could be wrong, but i'd bet a coke.

  • dave1707dave1707 Mod
    Posts: 9,725

    @RonJeffries I don’t like it when something similar takes different arguments. I tried some calculations and the rotate was the same as the 2 calculations I was using. I totally forgot about the vec2 rotate and when I did the Google search and found the 2 simple calculations on the first web page, I just used them. It’s amazing how much stuff I forget until someone reminds me about them.

  • Posts: 1,275

    just messing around to understand, i might do this. not entirely happy with the remake flag, but don't see anything nicer.

    at my real keyboard i might try computing the wave just once, but i think there's little reason to do it other than just because, even if i can. :smile:


    viewer.mode=OVERLAY function setup() ang, size, shouldRemake = 0, 1, true stroke(255, 0, 174) strokeWidth(3.0) points = {} physCurve = physics.body(CIRCLE,10) end function draw() background(0) make() polyline(physCurve.points) end function touched(touch) ang = math.atan2(touch.y - 300, touch.x - 300) size = vec2(300, 300):dist(touch.pos)/360.0 shouldRemake = true end function polyline(pts) for i = 2,#pts do a = pts[i-1] b = pts[i] line(a.x,a.y, b.x,b.y) end end function make() if not shouldRemake then return end points = {} for z=0,360 do r = math.rad(z) pt = vec2(size*z,math.sin(r)*150) curr = pt:rotate(ang) + vec2(300,300) table.insert(points, curr) end physCurve:destroy() physCurve = physics.body(POLYGON, table.unpack(points)) shouldRemake = false end
  • Posts: 1,275

    no reason to make points up top, it turns out.

  • Posts: 1,275

    @dave1707 yes, i think it's a mistake. don't know if it's unique to codea, or in lua. suspect the former, but i think we're stuck with it.

Sign In or Register to comment.