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

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.

@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

@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

@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.

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.

@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.

@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.

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%2FSqrt[2]+-+Sin[t]%2FSqrt[2]%2C+t%2FSqrt[2]+%2B+Sin[t]%2FSqrt[2]}%2C{t%2C-10%2C10}]

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

@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

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

@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.

!!!

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

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

@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?

@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


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

@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

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

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.

@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.