Snake, lizard, fish, robot (train) procedural animation

General discussion about LÖVE, Lua, game development, puns, and unicorns.
Post Reply
User avatar
darkfrei
Party member
Posts: 1236
Joined: Sat Feb 08, 2020 11:09 pm

Snake, lizard, fish, robot (train) procedural animation

Post by darkfrei »

Hi all! I've found a video with something interesting for me! Let's make similar simulatios together!

https://www.youtube.com/watch?v=qlfh_rv6khY
:awesome: in Lua we Löve
:awesome: Platformer Guide
:awesome: freebies
SETTORB
Prole
Posts: 1
Joined: Thu Feb 20, 2025 11:44 am

Re: Snake, lizard, fish, robot (train) procedural animation

Post by SETTORB »

I am also interested in making these kind of animations with love2d.

I tried ton convert the source code from java (I have no skill in this langage) : (https://github.com/argonautcode/animal-proc-anim) to lua/love2d, with chat Gpt and (https://app.codeconvert.ai/home) but it's kind of a mess (I am a beginner). The fish animation was working but the final rendering was quite far from the one on the video.

Instead of doing that, I am heading to start from skratch, using my brain.
User avatar
darkfrei
Party member
Posts: 1236
Joined: Sat Feb 08, 2020 11:09 pm

Re: Snake, lizard, fish, robot (train) procedural animation

Post by darkfrei »

First is the simplest working simulation:
Animation (99).gif
Animation (99).gif (294.96 KiB) Viewed 295 times

Code: Select all

local chain = {}
local segmentLength = 20
local chainLength = 10

function love.load()
	-- create the chain with evenly spaced nodes
	for i = 1, chainLength do
		local x = 100 + (chainLength - i) * segmentLength
		local y = 100
		chain[i] = { x = x, y = y }
	end
end

function love.update(dt)
	-- move the first node to follow the mouse
	local mx, my = love.mouse.getPosition()
	local prev = chain[1]
	prev.x, prev.y = mx, my

	-- update the rest of the chain


	for i = 2, #chain do
		local node = chain[i]

		-- calculate the vector between nodes
		local dx, dy = node.x - prev.x, node.y - prev.y
		local dist = math.sqrt(dx * dx + dy * dy)
		if dist > 0 then 
			local move = segmentLength-dist
			if dist > 0 then
				-- adjust the position to maintain segment length
				node.x = node.x + (dx / dist) * move
				node.y = node.y + (dy / dist) * move
			end
		end

		prev = node
	end
end

function love.draw()
	-- draw nodes and connect them with lines
	love.graphics.setColor (1,1,1)
	for i = 1, #chain do
		love.graphics.circle('fill', chain[i].x, chain[i].y, 5)
		if i > 1 then
			love.graphics.line(chain[i].x, chain[i].y, chain[i - 1].x, chain[i - 1].y)
		end
	end
end
Or with Lissajous figure:
Animation (100).gif
Animation (100).gif (310.08 KiB) Viewed 272 times

Code: Select all

local chain = {}
local segmentLength = 20
local chainLength = 20
local time = 0


function love.load()
	-- create the chain with evenly spaced nodes
	for i = 1, chainLength do
		local x = 100 + (chainLength - i) * segmentLength
		local y = 100
		chain[i] = { x = x, y = y }
	end
end

function love.update(dt)
	-- move the first node to follow the mouse
--	local mx, my = love.mouse.getPosition()

	-- move the first node along a lissajous curve (figure-eight)
	local a, b = 150, 150 -- amplitude
	local omegaX, omegaY = 1, 2 -- frequency
--	local phase = math.pi / 2 -- phase shift
	local phase =0 -- phase shift

	time = time + dt -- control speed of movement

	-- lissajous figure parameters
	local delta = math.pi / 2

	local prev = chain[1]
	-- move the first node along a lissajous figure
	prev.x = 400 + a * math.sin(omegaX * time)
	prev.y = 300 + b * math.sin(omegaY * time + phase)

	-- update the rest of the chain
	for i = 2, #chain do
		local node = chain[i]

		-- calculate the vector between nodes
		local dx, dy = node.x - prev.x, node.y - prev.y
		local dist = math.sqrt(dx * dx + dy * dy)
		if dist > 0 then 
			local move = segmentLength-dist
			if dist > 0 then
				-- adjust the position to maintain segment length
				node.x = node.x + (dx / dist) * move
				node.y = node.y + (dy / dist) * move
			end
		end

		prev = node
	end
end

function love.draw()
	-- draw nodes and connect them with lines
	love.graphics.setColor (1,1,1)
	for i = 1, #chain do
		love.graphics.circle('fill', chain[i].x, chain[i].y, 5)
		if i > 1 then
			love.graphics.line(chain[i].x, chain[i].y, chain[i - 1].x, chain[i - 1].y)
		end
	end
end
Attachments
chain-movement-01.love
(664 Bytes) Downloaded 4 times
Last edited by darkfrei on Sat Feb 22, 2025 10:51 am, edited 1 time in total.
:awesome: in Lua we Löve
:awesome: Platformer Guide
:awesome: freebies
User avatar
darkfrei
Party member
Posts: 1236
Joined: Sat Feb 08, 2020 11:09 pm

Re: Snake, lizard, fish, robot (train) procedural animation

Post by darkfrei »

Added hard angle limit for the chain links:
Animation (101).gif
Animation (101).gif (303.36 KiB) Viewed 277 times

Code: Select all

local chain = {}
local segmentLength = 20
local chainLength = 20
local time = 0
local maxAngle = math.rad(20)

-- lissajous figure parameters
local a, b = 150, 150 -- amplitude
local omegaX, omegaY = 1, 2 -- frequency
local phase =0 -- phase shift
local delta = math.pi / 2

function love.load()
	-- create the chain with evenly spaced nodes
	for i = 1, chainLength do
		local x = 100 + (chainLength - i) * segmentLength
		local y = 100
		chain[i] = { x = x, y = y }
	end
end

function math.sign(v)
	if v > 0 then
		return 1
	elseif v < 0 then
		return -1
	else
		return 0
	end
end

-- function to return node position if the angle between segments exceeds maxAngle
local function adjustNodePosition(x1, y1, x2, y2, x3, y3)
	-- calculate the vectors between the points
	local dx1, dy1 = x2 - x1, y2 - y1
	local dx2, dy2 = x3 - x2, y3 - y2

	-- calculate the angles of the vectors
	local angle1 = math.atan2(dy1, dx1)  -- angle of the vector from (x1, y1) to (x2, y2)
	local angle2 = math.atan2(dy2, dx2)  -- angle of the vector from (x2, y2) to (x3, y3)

	-- calculate the difference between the two angles
	local angleDiff = angle2 - angle1

	-- normalize the angle difference to the range [-pi, pi]
	if angleDiff > math.pi then
		angleDiff = angleDiff - 2 * math.pi
	elseif angleDiff < -math.pi then
		angleDiff = angleDiff + 2 * math.pi
	end

	-- if the angle difference exceeds the max allowed angle, adjust the node position
	if math.abs(angleDiff) > maxAngle then
		-- limit the angle difference to the max angle
		angleDiff = math.sign(angleDiff) * maxAngle

		-- calculate the new angle for the second vector
		local newAngle = angle1 + angleDiff

		-- calculate the new position for the third node using the new angle
		local length = math.sqrt(dx2 * dx2 + dy2 * dy2)
		local newDx = math.cos(newAngle) * length
		local newDy = math.sin(newAngle) * length

		-- set the new position for the third node
		x3 = x2 + newDx
		y3 = y2 + newDy
	end

	return x3, y3
end

function love.update(dt)
	time = time + dt -- control speed of movement

	local prev = chain[1]
	-- move the first node along a lissajous figure
	prev.x = 400 + a * math.sin(omegaX * time)
	prev.y = 300 + b * math.sin(omegaY * time + phase)

	-- update the rest of the chain
	for i = 2, #chain do
		local node = chain[i]

		-- calculate the vector between nodes
		local dx, dy = node.x - prev.x, node.y - prev.y
		local dist = math.sqrt(dx * dx + dy * dy)
		if dist > 0 then 
			local move = segmentLength-dist
			if dist > 0 then
				-- adjust the position to maintain segment length
				node.x = node.x + (dx / dist) * move
				node.y = node.y + (dy / dist) * move
			end
		end

		-- check the angle between segments and adjust if necessary
		if i > 1 then
			-- use the adjustNodePosition function to modify node[3] if needed
			local nextNode = chain[i+1]

			if nextNode then
				local x3, y3 = adjustNodePosition(
					prev.x, prev.y, 
					node.x, node.y, 
					nextNode.x, nextNode.y)

				nextNode.x = x3
				nextNode.y = y3
			end
		end

		prev = node
	end
end

function love.draw()
	-- draw nodes and connect them with lines
	love.graphics.setColor (1,1,1)
	for i = 1, #chain do
		love.graphics.circle('fill', chain[i].x, chain[i].y, 5)
		if i > 1 then
			love.graphics.line(chain[i].x, chain[i].y, chain[i - 1].x, chain[i - 1].y)
		end
	end
end
With soft limits:
Animation (102).gif
Animation (102).gif (255.62 KiB) Viewed 268 times

Code: Select all

local chain = {}
local segmentLength = 20
local chainLength = 20
local time = 0
local maxAngle = math.rad(20)
local maxOmega = math.rad(180*14)
print (maxOmega)

-- lissajous figure parameters
local a, b = 150, 150 -- amplitude
local omegaX, omegaY = 1, 2 -- frequency
local phase =0 -- phase shift
local delta = math.pi / 2

function love.load()
	-- create the chain with evenly spaced nodes
	for i = 1, chainLength do
		local x = 100 + (chainLength - i) * segmentLength
		local y = 100
		chain[i] = { x = x, y = y }
	end
end

function math.sign(v)
	if v > 0 then
		return 1
	elseif v < 0 then
		return -1
	else
		return 0
	end
end


-- function to return node position with soft influence if the angle between segments exceeds maxAngle
local function adjustNodePosition(x1, y1, x2, y2, x3, y3, dt)
	-- calculate the vectors between the points
	local dx1, dy1 = x2 - x1, y2 - y1
	local dx2, dy2 = x3 - x2, y3 - y2

	-- calculate the angles of the vectors
	local angle1 = math.atan2(dy1, dx1)  -- angle of the vector from (x1, y1) to (x2, y2)
	local angle2 = math.atan2(dy2, dx2)  -- angle of the vector from (x2, y2) to (x3, y3)

	-- calculate the difference between the two angles
	local angleDiff = angle2 - angle1

	-- normalize the angle difference to the range [-pi, pi]
	if angleDiff > math.pi then
		angleDiff = angleDiff - 2 * math.pi
	elseif angleDiff < -math.pi then
		angleDiff = angleDiff + 2 * math.pi
	end

	-- if the angle difference exceeds the max allowed angle, adjust the node position
	if math.abs(angleDiff) > maxAngle then
		-- limit the angle change to maxOmega * dt (soft adjustment)
		local maxDelta = maxOmega * dt

		local newMaxAngle = math.max (maxAngle, maxAngle + (math.abs(angleDiff)-maxAngle)*maxDelta)
--		print (maxDelta, maxAngle, math.abs(maxAngle)-maxDelta)

		angleDiff = math.sign(angleDiff) * newMaxAngle
--		angleDiff = math.sign(angleDiff) * math.min(math.abs(angleDiff), maxDelta)

		-- calculate the new angle for the second vector
		local newAngle = angle1 + angleDiff

		-- calculate the new position for the third node using the new angle
		local length = math.sqrt(dx2 * dx2 + dy2 * dy2)
		local newDx = math.cos(newAngle) * length
		local newDy = math.sin(newAngle) * length

		-- set the new position for the third node
		x3 = x2 + newDx
		y3 = y2 + newDy
	end

	return x3, y3
end

function love.update(dt)
	time = time + dt -- control speed of movement

	local prev = chain[1]
	-- move the first node along a lissajous figure
	prev.x = 400 + a * math.sin(omegaX * time)
	prev.y = 300 + b * math.sin(omegaY * time + phase)


--	prev.x, prev.y = love.mouse.getPosition()


	-- update the rest of the chain
	for i = 2, #chain do
		local node = chain[i]

		-- calculate the vector between nodes
		local dx, dy = node.x - prev.x, node.y - prev.y
		local dist = math.sqrt(dx * dx + dy * dy)
		if dist > 0 then 
			local move = segmentLength-dist
			if dist > 0 then
				-- adjust the position to maintain segment length
				node.x = node.x + (dx / dist) * move
				node.y = node.y + (dy / dist) * move
			end
		end

		-- check the angle between segments and adjust if necessary
		if i > 1 then
			-- use the adjustNodePosition function to modify node[3] if needed
			local nextNode = chain[i+1]

			if nextNode then
				local x3, y3 = adjustNodePosition(
					prev.x, prev.y, 
					node.x, node.y, 
					nextNode.x, nextNode.y, dt)

				nextNode.x = x3
				nextNode.y = y3
			end
		end

		prev = node
	end
end

function love.draw()
	-- draw nodes and connect them with lines
	love.graphics.setColor (1,1,1)
	for i = 1, #chain do
		love.graphics.circle('fill', chain[i].x, chain[i].y, 5)
		if i > 1 then
			love.graphics.line(chain[i].x, chain[i].y, chain[i - 1].x, chain[i - 1].y)
		end
	end
end
Attachments
chain-movement-02.love
(1.45 KiB) Downloaded 4 times
:awesome: in Lua we Löve
:awesome: Platformer Guide
:awesome: freebies
User avatar
darkfrei
Party member
Posts: 1236
Joined: Sat Feb 08, 2020 11:09 pm

Re: Snake, lizard, fish, robot (train) procedural animation

Post by darkfrei »

The train has other movement: the hard track without corner shortening. Also no limits for angles for the links, just do what the rail do.

:awesome:
Animation (104).gif
Animation (104).gif (144.97 KiB) Viewed 234 times

Code: Select all

local chain = {}         -- array of wagons (locomotive + cars)
local trail = {}         -- array of locomotive position records (trail)
local trailMaxLength = 370  -- max number of positions in the trail
local segmentLength = 20     -- distance between wagons
local chainLength = 10       -- number of nodes (locomotive + wagons)

local a, b = 150, 150        -- amplitudes for the trajectory (Lissajous)
local omegaX, omegaY = 1, 2   -- frequencies for the trajectory
local phase = 0             -- phase shift
local time = 0

function love.load()
	-- initialize the chain nodes. the first node is the locomotive, the rest are wagons
	for i = 1, chainLength do
		chain[i] = { x = 400, y = 300 }
	end
end

-- function returns the position along the trail corresponding to the given distance from the start
local function getPositionAtDistance(distance)
	local accumulated = 0
	for j = 1, #trail - 1 do
		local p1 = trail[j]
		local p2 = trail[j + 1]
		local dx = p1.x - p2.x
		local dy = p1.y - p2.y
		local d = math.sqrt(dx * dx + dy * dy)
		if accumulated + d >= distance then
			local ratio = (distance - accumulated) / d
			local x = p1.x + (p2.x - p1.x) * ratio
			local y = p1.y + (p2.y - p1.y) * ratio
			return { x = x, y = y }, j
		end
		accumulated = accumulated + d
	end
end

-- function to delete trail elements after a specified index
local function deleteTrailAfterIndex(endIndex)
	for i = #trail, endIndex + 3, -1 do -- small tail overlap
		table.remove(trail, i)
	end
end

function love.update(dt)
	time = time + dt

	-- update the position of the locomotive (first node) along the Lissajous trajectory
	local head = chain[1]
	head.x = 400 + a * math.sin(omegaX * time)
	head.y = 300 + b * math.sin(omegaY * time + phase)

	-- the first wagon creates the path by recording its position
	table.insert(trail, 1, { x = head.x, y = head.y })
	if #trail > trailMaxLength then
		table.remove(trail)
	end

	-- for each wagon, calculate its position along the trail
	local pos, lastTrailIndex
	for i = 2, chainLength do
		local desiredDistance = (i - 1) * segmentLength
		pos, lastTrailIndex = getPositionAtDistance(desiredDistance)
		if pos then
			chain[i].x = pos.x
			chain[i].y = pos.y
		end
	end

    -- delete the trail elements after the last wagon
	if lastTrailIndex then
--		print ('delete lastTrailIndex', lastTrailIndex)
--		deleteTrailAfterIndex (lastTrailIndex)
	end

end

function love.draw()
	-- optionally: draw the locomotive trail for visibility
	love.graphics.setColor(0, 1, 0)
	for i = 1, #trail - 1 do
--		love.graphics.circle('line', trail[i].x, trail[i].y, 2)
		love.graphics.line(trail[i].x, trail[i].y, trail[i + 1].x, trail[i + 1].y)
	end

	-- draw nodes and connect them with lines
	love.graphics.setColor (1,1,1)
	for i = 1, #chain do
		love.graphics.circle('fill', chain[i].x, chain[i].y, 5)
--		if i > 1 then
--			love.graphics.line(chain[i].x, chain[i].y, chain[i - 1].x, chain[i - 1].y)
--		end
	end
end
Attachments
train-movement-01.love
(1.23 KiB) Downloaded 4 times
:awesome: in Lua we Löve
:awesome: Platformer Guide
:awesome: freebies
RNavega
Party member
Posts: 439
Joined: Sun Aug 16, 2020 1:28 pm

Re: Snake, lizard, fish, robot (train) procedural animation

Post by RNavega »

Cool thread. Reminds me of the "Pulled String" mode from Lazy Nezumi, a pen stabilizer utility for artists using graphics tablets. Notice how the blue gizmo bends when the mouse isn't stretching it.
lnpPulledString.gif
lnpPulledString.gif (15.72 KiB) Viewed 125 times
(I appreciate that some art software like Krita, Paint Tool SAI and Clip Studio Paint have these pen stabilization features built-in.)
Post Reply

Who is online

Users browsing this forum: No registered users and 3 guests