I was inspired by the blog post 6 Useful Snippets – Naming Things and implemented in it Codea.
--# Main
-- balls - inspired by https://blog.bruce-hill.com/6-useful-snippets
function setup()
viewer.mode = FULLSCREEN_NO_BUTTONS
W, H = WIDTH, HEIGHT
MIN_RADIUS = math.min(W, H)/40
MAX_RADIUS = math.min(W, H)/12
collision_scale = MIN_RADIUS
balls = {}
dragging = false
GRAVITY = vec2(0, -2000)
K = 0.5
for i = 0, 40 do
local ball = {
id=i,
pos=vec2(math.random()*W, math.random()*H),
radius=mix(MIN_RADIUS, MAX_RADIUS, math.random()),
color=hsl(360*((i*GOLDEN_RATIO) % 1), .5, .7, 1.)
}
ball.prevpos = ball.pos
ball.mass = ball.radius*ball.radius
table.insert(balls, ball)
end
-- rotating ball
stirrer = balls[1]
stirrer.color = color(255, 0, 225)
stir_angle = 0
ellipseMode(CENTER)
end
function draw()
background(40, 40, 50)
strokeWidth(1)
dt = DeltaTime
for i, b in ipairs(balls) do
local nextpos = ((b.pos * 2) - b.prevpos) + GRAVITY * dt*dt
b.prevpos = b.pos
b.pos = nextpos
end
stir_angle = stir_angle + dt*6.28/4
-- solve contraints
for i = 1, 5 do
if dragging then
dragging.pos = mix(dragging.pos, CurrentTouch.pos, .35)
end
if stirrer then
stirrer.pos = mix(stirrer.pos, vec2(W/2+H/3*math.cos(stir_angle), H/2+H/3*math.sin(stir_angle)), .35)
end
local collisions = collisions_between(balls)
for j, cs in ipairs(collisions) do
local a = cs[1];
local b = cs[2];
if a.pos:dist(b.pos) < a.radius + b.radius then
local a2b = (b.pos -a.pos):normalize()
local needed_dist = (a.radius + b.radius) - a.pos:dist(b.pos)
a.pos = a.pos - (a2b * (K*needed_dist*(b.mass/(a.mass+b.mass))))
b.pos = b.pos + (a2b * (K*needed_dist*(a.mass/(a.mass+b.mass))))
end
end
for i, b in ipairs(balls) do
local clamped = vec2(clamp(b.pos.x, b.radius, W-b.radius),
clamp(b.pos.y, b.radius, H-b.radius))
if clamped.x ~= b.pos.x or clamped.y ~= b.pos.y then
b.pos = mix(b.pos, clamped, K)
-- Damping:
b.prevpos = mix(b.prevpos, b.pos, .001)
end
end
end
for i, ball in ipairs(balls) do
fill(ball.color)
if ball == dragging then
strokeWidth(8)
else
strokeWidth(1)
end
ellipse(ball.pos.x, ball.pos.y, ball.radius*2, ball.radius*2)
end
end
function touched(touch)
if touch.state == BEGAN then
dragging = ball_at(touch.pos)
elseif touch.state == ENDED then
dragging = false
end
end
--# Utils
GOLDEN_RATIO = (math.sqrt(5) - 1)/2
function hsl(h, s, v, a)
local r, g, b
local i = math.floor(h * 6);
local f = h * 6 - i;
local p = v * (1 - s);
local q = v * (1 - f * s);
local t = v * (1 - (1 - f) * s);
i = i % 6
if i == 0 then r, g, b = v, t, p
elseif i == 1 then r, g, b = q, v, p
elseif i == 2 then r, g, b = p, v, t
elseif i == 3 then r, g, b = p, q, v
elseif i == 4 then r, g, b = t, p, v
elseif i == 5 then r, g, b = v, p, q
end
return color(r * 255, g * 255, b * 255, a * 255)
end
function mix(a, b, amount)
return (1-amount)*a + amount*b
end
function clamp(x, min, max)
if x < min then
return min
end
if x > max then
return max
end
return x
end
function collisions_between(things)
local S = collision_scale;
local buckets = {}
local collisions = {}
for i, t in ipairs(things) do
local collided = {}
for x = math.floor((t.pos.x-t.radius)/S), math.floor((t.pos.x+t.radius)/S) do
for y = math.floor((t.pos.y-t.radius)/S), math.floor((t.pos.y+t.radius)/S) do
local key = x .. "," .. y;
if buckets[key] == nil then
buckets[key] = {}
end
for j, other in ipairs(buckets[key]) do
table.insert(collisions, {other, t})
collided[other.id] = true
end
table.insert(buckets[key], t)
end
end
end
return collisions
end
function ball_at(pos)
for j, b in ipairs(balls) do
if b.pos:dist(pos) <= b.radius then
return b
end
end
end