Maybe the surface of the water is like a wire: If you pull on some point of the wire, the points next to that point will be pulled down too. All points are also attracted back to a baseline.
I sketched that idea in Lua using LÖVE and got this:
It's a plausible result.
I'm sure I've butchered the physics though: I have no idea what I'm doing beyond initial intuitions and lots of Hooke's Law!
Anyway, here's my main.lua
. Feel free to read, copy or use as toilet paper.
-- Resolution of simulation
NUM_POINTS = 200
-- Width of simulation
WIDTH = 700
-- Spring constant for forces applied by adjacent points
SPRING_CONSTANT = 0.1
-- Sprint constant for force applied to baseline
SPRING_CONSTANT_BASELINE = 0.1
-- Vertical draw offset of simulation
Y_OFFSET = 300
-- Make points to go on the wave
function makeWavePoints(numPoints)
local t = {}
for n = 1,numPoints do
-- This represents a point on the wave
local newPoint = {
x = n / numPoints * WIDTH,
y = Y_OFFSET,
spd = {y=0}, -- speed with vertical component zero
mass = 1
}
t[n] = newPoint
end
return t
end
wavePoints = makeWavePoints(NUM_POINTS)
-- Update the positions of each wave point
function updateWavePoints(points, dt)
for n,p in ipairs(points) do
-- force to apply to this point
local force = 0
-- forces caused by the point immediately to the left or the right
local forceFromLeft, forceFromRight
if n == 1 then -- wrap to left-to-right
local dy = points[# points].y - p.y
forceFromLeft = SPRING_CONSTANT * dy
else -- normally
local dy = points[n-1].y - p.y
forceFromLeft = SPRING_CONSTANT * dy
end
if n == # points then -- wrap to right-to-left
local dy = points[1].y - p.y
forceFromRight = SPRING_CONSTANT * dy
else -- normally
local dy = points[n+1].y - p.y
forceFromRight = SPRING_CONSTANT * dy
end
-- Also apply force toward the baseline
local dy = Y_OFFSET - p.y
forceToBaseline = SPRING_CONSTANT_BASELINE * dy
-- Sum up forces
force = force + forceFromLeft
force = force + forceFromRight
force = force + forceToBaseline
-- Calculate acceleration
local acceleration = force / p.mass
-- Apply acceleration (with damping)
p.spd.y = 0.89 * p.spd.y + acceleration
-- Apply speed
p.y = p.y + p.spd.y
end
end
-- Callback when updating
function love.update(dt)
-- On click: Pick nearest point to mouse position
if love.mouse.isDown("l") then
local mouseX, mouseY = love.mouse.getPosition()
local closestPoint = nil
local closestDistance = nil
for _,p in ipairs(wavePoints) do
local distance = math.abs(mouseX-p.x)
if closestDistance == nil then
closestPoint = p
closestDistance = distance
else
if distance <= closestDistance then
closestPoint = p
closestDistance = distance
end
end
end
closestPoint.y = love.mouse.getY()
end
-- Update positions of points
updateWavePoints(wavePoints, dt)
end
local circle = love.graphics.circle
local line = love.graphics.line
local color = love.graphics.setColor
love.graphics.setBackgroundColor(0xff,0xff,0xff)
-- Callback for drawing
function love.draw(dt)
-- Draw baseline
color(0xff,0x33,0x33)
line(0, Y_OFFSET, WIDTH, Y_OFFSET)
-- Draw points and line
color(0x00,0x33,0xbb)
for n,p in ipairs(wavePoints) do
circle("line", p.x, p.y, 3)
if n == 1 then
else
local leftPoint = wavePoints[n-1]
line(leftPoint.x, leftPoint.y, p.x, p.y)
end
end
end