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

``````

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:

• Mod
Posts: 9,725

``````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
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
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.
• Mod
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.

• 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}]

``````

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

• 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
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)
dx = x2 - x1
dy = y2 - y1
end

function draw()
background(0)
fill(80, 161, 233)
for z=0,360*repeats do
x2=z
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)
dx = x2 - x1
dy = y2 - y1
end

function draw()
background(0)
fill(80, 161, 233)
points = {}
for z=0,360*repeats do
x2=z
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

``````
• Mod
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
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

``````
• 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
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
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

``````
• Mod
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
``````
• Mod
Posts: 1,275

I see the thread name changed. Confused me

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.

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

• Mod
Posts: 9,725

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

• Mod
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.

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

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

• Mod
Posts: 1,275

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

• Mod
Posts: 9,725

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

``````https://academo.org/demos/rotation-about-point/
``````
• Mod
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.

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

• Mod
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
pt = vec2(size*z,math.sin(r)*150)
if prev~=curr then
table.insert(points, curr)
prev=curr
else
end
end
if physCurve then physCurve:destroy() end
physCurve = physics.body(CHAIN, false, table.unpack(points))
shouldRemake = false
end
``````
• Mod
edited October 10 Posts: 1,275

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

• 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
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

``````
• 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.

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

• Mod
edited October 10 Posts: 1,275

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

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

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

• Mod
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.

``````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
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
``````
• Mod
Posts: 1,275

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

• Mod
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.