2025-12-21
Symmetry is nice, and six-fold symmetry is even nicer, so let it snow…
As well as making the above little app, this little journey took us into the fediverse over on fedibot.club from where a lua tootbot emerged. Not having a user interface makes for much nicer reading, and lua is a nice language (see below). Mostly vibes by vibe-cli, it's a bit long-winded and weird, but it was fun making a project in two languages at once.
Following much hectoring, the approach that vibe-cli and I eventually settled on was:
- draw one side of a branch with varied number of sub-branches (complexity);
- repeat it five more times, each rotated at 60 degree intervals;
- then mirror each one by inverting the
ycoordinates.
The image api of https://fedibot.club/api#images keeps things simple by working in integers which pixelates things nicely:
-- Lua Snowflake Generator
-- Creates a six-fold symmetrical snowflake using the fedibot.club image API
-- https://fedibot.club/bots/vakoudqgeqqqltdj
-- Set random seed based on current time
math.randomseed(os.time())
local size = 512
local complexity = 5
local centerX = size / 2
local centerY = size / 2
local radius = math.min(centerX, centerY) * 0.85
local function generateOneSideBranches(radius, complexity, random)
local branches = {}
-- Add main sub-branches based on complexity
local numMainBranches = 2 + math.floor(complexity * 0.8)
for i = 1, numMainBranches do
-- Position along main branch (0 to 1)
local position = (i + 1) / (numMainBranches + 1)
-- Branch parameters
local angle = math.random() * (math.pi / 3) -- 0 to 60 degrees
local length = radius * (0.2 + math.random() * 0.3)
-- Calculate end point
local endX = position * radius + length * math.cos(angle)
local endY = 0 + length * math.sin(angle)
table.insert(branches, {
start = { x = math.floor(position * radius + 0.5), y = math.floor(0 + 0.5) },
finish = { x = math.floor(endX + 0.5), y = math.floor(endY + 0.5) },
width = 2 + complexity * 0.3
})
-- Add sub-sub-branches (0-6 per main branch)
local numSubBranches = math.floor(math.random() * 7) -- 0 to 6
for j = 1, numSubBranches do
local subPosition = (j + 1) / (numSubBranches + 1)
local subAngle = math.random() * (math.pi / 4) -- 0 to 45 degrees
local subLength = length * (0.2 + math.random() * 0.4)
-- Calculate sub-branch end point
local subEndX = position * radius +
(endX - position * radius) * subPosition +
subLength * math.cos(angle + subAngle)
local subEndY = 0 +
(endY - 0) * subPosition +
subLength * math.sin(angle + subAngle)
table.insert(branches, {
start = {
x = math.floor(position * radius + (endX - position * radius) * subPosition + 0.5),
y = math.floor(0 + (endY - 0) * subPosition + 0.5)
},
finish = { x = math.floor(subEndX + 0.5), y = math.floor(subEndY + 0.5) },
width = 1 + complexity * 0.2
})
end
end
-- Add 5 more main branches at 60 degree intervals
for i = 1, 5 do
local angle = i * (math.pi / 3)
local endX = radius * math.cos(angle)
local endY = radius * math.sin(angle)
table.insert(branches, {
start = { x = 0, y = 0 },
finish = { x = endX, y = endY },
width = 3 + complexity * 0.4
})
end
return branches
end
-- Generate complete symmetrical branch pattern
local function generateBranchPattern(radius, complexity)
local branches = {}
-- Main branch
table.insert(branches, {
start = { x = math.floor(centerX + 0.5), y = math.floor(centerY + 0.5) },
finish = { x = math.floor(centerX + radius + 0.5), y = math.floor(centerY + 0.5) },
width = 3 + complexity * 0.4
})
-- Generate right side branches
local rightBranches = generateOneSideBranches(radius, complexity)
-- Add right branches (they should already be relative to center)
for _, branch in ipairs(rightBranches) do
table.insert(branches, branch)
end
-- Create left branches by mirroring right branches over the main branch (X axis)
for _, branch in ipairs(rightBranches) do
-- Mirror: keep X coordinate, negate Y coordinate (branches are already relative to center)
table.insert(branches, {
start = { x = branch.start.x, y = -branch.start.y },
finish = { x = branch.finish.x, y = -branch.finish.y },
width = branch.width
})
end
return branches
end
-- Generate the snowflake as a Fedibot API image
local function generateSnowflakeImage()
local branches = generateBranchPattern(radius, complexity)
-- Create the image data structure for Fedibot API
local imageData = {
w = size,
h = size,
steps = {},
description = "a generative snowflake inspired pattern using six-fold symmetry" -- alt text
}
-- Add all branches 6 times with 60 degree rotation for six-fold symmetry
for i = 0, 5 do
local rotation = 90 + i * 60 -- 60 degrees per rotation
-- Draw all branches for this rotation
for _, branch in ipairs(branches) do
-- Apply rotation to branch coordinates
local startX = branch.start.x
local startY = branch.start.y
local endX = branch.finish.x
local endY = branch.finish.y
-- Rotate points around origin
local rad = math.rad(rotation)
local cos = math.cos(rad)
local sin = math.sin(rad)
local rotatedStartX = startX * cos - startY * sin
local rotatedStartY = startX * sin + startY * cos
local rotatedEndX = endX * cos - endY * sin
local rotatedEndY = endX * sin + endY * cos
-- Translate to center
rotatedStartX = math.floor(rotatedStartX + centerX + 0.5)
rotatedStartY = math.floor(rotatedStartY + centerY + 0.5)
rotatedEndX = math.floor(rotatedEndX + centerX + 0.5)
rotatedEndY = math.floor(rotatedEndY + centerY + 0.5)
table.insert(imageData.steps, {
"line",
rotatedStartX,
rotatedStartY,
rotatedEndX,
rotatedEndY,
{173, 216, 230}
})
end
end
return imageData
end
return {
status = "season's greetings. here's a snowflake inspired procedural pattern; with thanks to fedibot.club",
images = {generateSnowflakeImage()},
key = os.time()
}
A few of these fedibot generated images have been tooted hourly to my fedi feed:
After a run of a couple of days of hourly running, the bot was switched to daily just before the solstice [15:03 GMT]. Using a bot to toot one's own feed might be a bit out-of-keeping, but it gave a sense of being a semi-automated person: half man; half robot.
The code is freed:
https://git.sr.ht/~joeldn/snowflaker
🕊 keep hope alive in 2026 🕊