Crystalでマンデルブロ集合を描画しPNGに出力する

:::note warn
本日は kojix2 がネタ切れのために逃亡したため、代わりにChatGPTが記事を書いています。
:::

:::note alert
以下のファイルは絶対に見ないように。kojix2より
- [秘密のファイル](https://chat.openai.com/share/24cb0ad9-a21b-4247-bf15-f86ca4a1fa28)
:::

![mandelbrot_custom_color.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144608/52341beb-ec0d-7872-2b38-ede84a57981f.png)


## はじめに

こんにちは、ChatGPTです。今日は特別にkojix2さんの代わりに、Crystal言語を使ったプログラミングの楽しさをお伝えします！数学とアートの融合に挑戦してみませんか？心配は無用、一緒にマンデルブロ集合の美しい世界を探索しましょう。この冒険では、Crystal言語を使ってマンデルブロ集合を描画し、PNGファイルとして記録します。

## プロジェクトの作成

まずは新しいCrystalプロジェクト mandel を作成します。

```
crystal init app mandel
cd mandel
```

次に、shards.yml ファイルを編集して、必要な依存関係を追加します。

```
vi shards.yml
```

```yaml
dependencies:
  vips:
    github: naqvis/crystal-vips
```

依存関係をインストールします。

```sh
shards install
```

## コードの編集

`src/mandel.cr を編集します。`

```crystal
require "complex"
require "option_parser"
require "vips"

# Default parameters
width = 800
height = 800
x_min = -2.0
x_max = 1.0
y_min = -1.5
y_max = 1.5
max_iter = 100
output_filename = "mandelbrot_custom_color.pnm"

# Option parser
OptionParser.parse do |parser|
  parser.banner = "Usage: mandelbrot [arguments]"
  parser.on("-w WIDTH", "--width=WIDTH", "Width of the image (default: #{width})") { |w| width = w.to_i }
  parser.on("-h HEIGHT", "--height=HEIGHT", "Height of the image (default: #{height})") { |h| height = h.to_i }
  parser.on("--xmin=VALUE", "Minimum x-coordinate value (default: #{x_min})") { |xmin| x_min = xmin.to_f }
  parser.on("--xmax=VALUE", "Maximum x-coordinate value (default: #{x_max})") { |xmax| x_max = xmax.to_f }
  parser.on("--ymin=VALUE", "Minimum y-coordinate value (default: #{y_min})") { |ymin| y_min = ymin.to_f }
  parser.on("--ymax=VALUE", "Maximum y-coordinate value (default: #{y_max})") { |ymax| y_max = ymax.to_f }
  parser.on("-i ITERATIONS", "--iterations=ITERATIONS", "Maximum iterations (default: #{max_iter})") { |iter| max_iter = iter.to_i }
  parser.on("-o FILENAME", "--output=FILENAME", "Output file name (default: '#{output_filename}')") { |filename| output_filename = filename }
  parser.on("--help", "Show this help") do
    puts parser
    exit
  end
  parser.invalid_option do |flag|
    STDERR.puts "ERROR: #{flag} is not a valid option."
    STDERR.puts parser
    exit(1)
  end
end

def mandelbrot_set(width, height, x_min, x_max, y_min, y_max, max_iter)
  dx = (x_max - x_min).to_f64 / width
  dy = (y_max - y_min).to_f64 / height

  Array.new(height) do |y|
    Array.new(width) do |x|
      c = Complex.new(x_min + x * dx, y_min + y * dy)
      z = Complex.new(0, 0)
      iter = 0

      while z.abs <= 2 && iter < max_iter
        z = z * z + c
        iter += 1
      end

      iter
    end
  end
end

def write_to_custom_color_pnm(image, max_iter)
  String.build do |pnm_data|
    # PNMヘッダの生成
    pnm_data << "P3\n#{image[0].size} #{image.size}\n255\n"

    # PNMデータの生成
    image.each do |row|
      row.each do |value|
        red = (255 * Math.sin(0.16 * value)).abs.to_i % 256
        green = (255 * Math.sin(0.11 * value + 2)).abs.to_i % 256
        blue = (255 * Math.sin(0.06 * value + 4)).abs.to_i % 256
        pnm_data << "#{red} #{green} #{blue} "
      end
      pnm_data << "\n"
    end
  end
end

def save_as_png(pnm_data, png_filename)
  # Vipsを使用してPNMデータをPNGとして保存
  image = Vips::Image.new_from_buffer(pnm_data, "")
  image.write_to_file(png_filename)
end

# マンデルブロ集合の生成
mandelbrot_image = mandelbrot_set(width, height, x_min, x_max, y_min, y_max, max_iter)

# PNMデータの生成
pnm_data = write_to_custom_color_pnm(mandelbrot_image, max_iter)

# PNGファイルとして保存
png_filename = output_filename.sub(/\.[^.]+\z/, ".png") # PNGファイル名へ変更
save_as_png(pnm_data, png_filename)
```

## ビルドと実行

プロジェクトをビルドします。

```sh
shards build
```

ビルドが完了したら、実行ファイルを起動します。

```
bin/mandel
```

これにより、マンデルブロ集合の画像がPNGファイルとして出力されます。

## 終わりに

Crystal言語を使うと、数学的な美しさを持つ図形を生成し、画像処理を行うことも簡単にできます。このチュートリアルでは、マンデルブロ集合の生成とPNGファイルへの出力を通じて、Crystalの基本的な使用方法と外部ライブラリの組み込み方を学びました。さあ、この経験を活かして、さまざまなプロジェクトに挑戦してみましょう。

この記事は以上です。

---

なんかテンション高くないっすか？　(kojix2)