Lissajous orbit
Two sines at right angles, three beats to two. natiny.render.line draws all of
it: the track once, then sixty segments chasing the head.
The ribbon is those same lines, thinner and fainter the further back they sit - straight alpha lays them into a taper. The thin guides crossing at the head are the two sines themselves.
local natiny = require("natiny")
local demo = require("common.modules.demo")
local BG = { 21 / 255, 30 / 255, 44 / 255 } -- #151E2C
local OBJECT = { 169 / 255, 179 / 255, 197 / 255 } -- #A9B3C5
local ACCENT = { 248 / 255, 249 / 255, 253 / 255 } -- #F8F9FD
local function color(c, a)
natiny.render.set_color(c[1], c[2], c[3], a or 1.0)
end
local function mix(a, b, k)
return { a[1] + (b[1] - a[1]) * k,
a[2] + (b[2] - a[2]) * k,
a[3] + (b[3] - a[3]) * k }
end
natiny.backend.init()
local win = natiny.window.create("Lissajous orbit", 900, 600)
natiny.window.set_scale_mode(win, natiny.window.SCALE_MODE_STRETCH)
-- The two frequencies. Whole numbers close the curve; 3:2 is the classic
-- knot, and 5:4 or 7:4 are worth a try.
local FREQ_X, FREQ_Y = 3, 2
local cx, cy = 450, 300
local rx, ry = 330, 210
local function point(u)
return cx + math.sin(FREQ_X * u) * rx,
cy + math.sin(FREQ_Y * u) * ry
end
local t = 0.0
local function frame(dt)
natiny.window.bind(win, function()
t = t + dt
local u = t * 0.9
natiny.render.clear(BG[1], BG[2], BG[3], 1.0)
-- ── The track: the whole closed curve, once ─────────────────
color(OBJECT, 0.8)
local px, py = point(0)
for i = 1, 180 do
local qx, qy = point(i / 180 * math.pi * 2)
natiny.render.line(px, py, qx, qy, 1)
px, py = qx, qy
end
-- ── The ribbon: sixty segments behind the head ──────────────
local N = 60
for i = N, 1, -1 do
local k = 1 - i / N -- 0 at the tail, 1 at the head
local ax, ay = point(u - i * 0.016)
local bx, by = point(u - (i - 1) * 0.016)
color(mix(OBJECT, ACCENT, k), k * k)
natiny.render.line(ax, ay, bx, by, 1 + 7 * k)
end
-- a dot every few samples, so the ribbon keeps a beat
for i = 0, 8 do
local k = 1 - i / 9
local dx, dy = point(u - i * 0.10)
color(ACCENT, k * 0.7)
natiny.render.circle(dx, dy, 3 + 5 * k)
end
-- ── The head, and the two sines it is made of ───────────────
local hx, hy = point(u)
color(OBJECT, 0.9)
natiny.render.line(hx, 60, hx, 540, 1)
natiny.render.line(60, hy, 840, hy, 1)
color(OBJECT)
natiny.render.circle(hx, hy, 8)
-- a ring breathing out of it, fading as it grows
local pulse = (t * 1.6) % 1.0
color(ACCENT, 1.0 - pulse)
natiny.render.circle_outline(hx, hy, 10 + pulse * 34, 3)
-- ── A second body, half a turn behind on the same track ─────
local sx, sy = point(u + math.pi)
color(OBJECT)
natiny.render.circle(sx, sy, 6)
color(OBJECT, 0.35)
natiny.render.circle_outline(sx, sy, 13, 3)
end)
end
demo:run({
window = win,
title = "Lissajous orbit",
text = "Two sines, one point: 3 across against 2 down",
loop = frame,
})
natiny.backend.shutdown()