Gerando imagens legais, ou arte generativa com uma fórmula simples em Go (Golang) - a refatoração

Imagem de uma fórmula matemática gerada pelo genart

Imagem gerada pelo genart, com os parâmetros fórmula:xcyc, size:1000, constant:2, colors:200

Historinha

Recentemente, para aprender um pouco mais de Go, e fazer algo divertido, criei um novo repositĂłrio com um projeto para desenhar imagens baseadas em fĂłrmulas (anteriormente tinha usado racket:

https://github.com/paulohrpinheiro/genart/

Mas da forma como estava, era muito difícil alterar de forma simples, para incluir novas fórmulas. E o código estava bem zoado em sua organização. Agora, depois de refatorar, está apenas zoado :)

Explorando o resultado final

Depois de muito bater a cabeça, cheguei à versão v0.0.1. A primeira versão está com a tag v0.0.0.

Aqui, todas as alterações feitas:

https://github.com/paulohrpinheiro/genart/compare/v0.0.0...v0.0.1

Os arquivos ccxy/ccxy.go e types/types.go foram apagados. A pasta formula/ agora contém o pacote de mesmo nome, em que o arquivo formula/formula.go contém as funções principais e os novos formula/ccxy.go e formula/xcyc.go, que comparados à estrutura anterior, agora contém apenas as funções de aplicação das fórmulas específicas para geração das imagens.

A estrutura de arquivos e diretĂłrios ficou dessa forma:

$ tree
.
├── cmd
│   ├── api
│   │   └── main.go
│   └── cli
│       └── main.go
├── formula
│   ├── ccxy.go
│   ├── formula.go
│   └── xcyc.go
├── go.mod
├── images
│   ├── ccxy_size:1000_constant:10_colors:100.png
│   ├── ccxy_size:1000_constant:20_colors:200.png
│   ├── ccxy_size:1000_constant:2_colors:70.png
│   └── xcyc_size:1000_constant:2_colors:70.png
├── LICENSE
├── Makefile
└── README.md

Uma primeira alteração, foi atualizar o release cadidate da versão 1.22 para o rc2:

$ git diff go.mod
diff --git a/go.mod b/go.mod
index 73586c8..4e887c3 100644
--- a/go.mod
+++ b/go.mod
@@ -1,3 +1,3 @@
 module genart

-go 1.22rc1
+go 1.22rc2

Já no uso, uma primeira alteração foi realizada no cli:

diff --git a/cmd/cli/main.go b/cmd/cli/main.go
index 5866133..40d7dfd 100644
--- a/cmd/cli/main.go
+++ b/cmd/cli/main.go
@@ -6,7 +6,7 @@ import (
  "image/color/palette"
  "os"

- "genart/ccxy"
+ "genart/formula"
 )

 const (
@@ -19,6 +19,7 @@ const (
 func main() {
  colors := palette.WebSafe

+ formulaName := flag.String("formula", "ccxy", "formula to use")
  fileName := flag.String("filename", defaultFileName, "filename to save image (.png)")
  imageSize := flag.Int("size", defaultImageSize, "size of image")
  constant := flag.Int("constant", defaultConstant, "constant for formula (c): c-c*x*y")
@@ -26,8 +27,9 @@ func main() {

  flag.Parse()

- newImage := ccxy.CcxyStruct{}
- newImage.Init(*imageSize, *constant, *maxColors, colors)
+ newImage := formula.New()
+
+ newImage.Init(*formulaName, *imageSize, *constant, *maxColors, colors)
  newImage.Draw()

  f, err := os.Create(*fileName)

Como se pode ler, os imports foram adaptados, acrescentou-se um novo parâmetro (o nome da fórmula ser usada), e adaptou-se o uso do package formula, que ao invés de chamar diretamente a fórmula, passa seu nome como parâmetro na inicialização, que determina a função a ser utilizada.

Outra alteração importante, foi refatorar o server para a nova estrutura:

$ git --no-pager diff cmd/api/main.go
diff --git a/cmd/api/main.go b/cmd/api/main.go
index 8c466ac..e89e4b4 100644
--- a/cmd/api/main.go
+++ b/cmd/api/main.go
@@ -1,6 +1,7 @@
 package main

 import (
+ "fmt"
  "image/png"
  "image/color/palette"
  "log"
@@ -8,7 +9,7 @@ import (
  "os"
  "strconv"

- "genart/ccxy"
+ "genart/formula"
 )

 func sendError(w http.ResponseWriter, msg string) {
@@ -17,7 +18,8 @@ func sendError(w http.ResponseWriter, msg string) {
  return
 }

-func ccxyHandler(w http.ResponseWriter, r *http.Request) {
+func formulaHandler(w http.ResponseWriter, r *http.Request) {
+ formulaName := r.PathValue("formulaName")
  size, err := strconv.Atoi(r.PathValue("size"))
  if err != nil {
   sendError(w, "Invalid size value")
@@ -32,12 +34,12 @@ func ccxyHandler(w http.ResponseWriter, r *http.Request) {

  colors, err := strconv.Atoi(r.PathValue("colors"))
  if err != nil {
-  sendError(w, "Invalid colors value")
+   sendError(w, "Invalid colors value")
   return
  }

- newImage := ccxy.CcxyStruct{}
- newImage.Init(size, constant, colors, palette.WebSafe)
+ newImage := formula.New()
+ newImage.Init(formulaName, size, constant, colors, palette.WebSafe)
  newImage.Draw()

  w.Header().Set("Content-Type", "image/png")
@@ -52,8 +54,8 @@ func main() {

  mux := http.NewServeMux()
  mux.HandleFunc(
-  "GET /ccxy/size/{size}/constant/{constant}/colors/{colors}/",
-  ccxyHandler,
+  "GET /{formulaName}/size/{size}/constant/{constant}/colors/{colors}/",
+  formulaHandler,
  )

  fmt.Printf("Listening on %s\n", listenStr)

No server, basicamente foram as mesmas alterações do cli.

O package formula

Cada fórmula deve estar contida em um arquivo, com sua própria função de desenho, por exemplo formula/ccxy.go:

package formula

import (
 "image"
)

func CcxyDraw(fs *FormulaStruct) {
 rect := image.Rect(0, 0, fs.ImageSize, fs.ImageSize)
 img := image.NewPaletted(rect, fs.Palette)

 for x := range fs.ImageSize {
  for y := range fs.ImageSize {
   value := (fs.Constant - fs.Constant*x*y)
   color := uint8(value % fs.MaxColors)

   img.SetColorIndex(x, y, color)
  }
 }

 fs.Image = img
}

A função recebe a estrutura que contém os dados, para acessar os parâmetros necessários aos cálculos, e armazenamento da imagem.

E, no formula/formula.go, temos o código que relaciona a função a seu nome. Cada nova fórmula, além de seu arquivo, deve incluir um elemento nesse mapeamento:

func functionByName(name string) (fn, error) {
 formulasMap := map[string]fn{
  "ccxy": CcxyDraw,
  "xcyc": XcycDraw,
 }

 newFunction := formulasMap[name]
 if newFunction == nil {
  return nil, errors.New("Unknown formula.")
 }

 return newFunction, nil
}

Planos

O código está bem frágil, sem verificações sobre os dados informados, mas o cerne está aí. É possível adicionar novas fórmulas sem grande esforço. Outras questão que está em minha cabeça, para facilitar a vida, é no cli, quando não informar o nome do arquivo em que a imagem deve ser salva, gerar um nome significativo que informe os parâmetros usados.