🧖 half man; half snowflake ❄️

21 Dec 2025 • 6 min read

midwinter in the fediversity

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:

  1. draw one side of a branch with varied number of sub-branches (complexity);
    1. repeat it five more times, each rotated at 60 degree intervals;
  2. then mirror each one by inverting the y coordinates.

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 🕊