🗻

【フラクタル図形】シェルピンスキーのギャスケットの書き方

2023/08/25に公開

class SierpinskiGasket < Base
  def call
    depth = 5                      # 複雑度

    radius = wh.min_element * 0.65 # 大きさ
    vertex_n = 3                   # 頂点数 (固定)
    center = V[0.5, 0.66]          # 中心 (適当)

    vertexes = vertex_n.times.collect do |i|
      wh * center + V.from_angle((270 + 360 / vertex_n * i).deg_to_rad) * radius
    end

    draw(*vertexes, depth)
  end

  def draw(v0, v1, v2, depth)
    if depth <= 0
      return
    end

    v3 = (v0 + v1) / 2          # 右辺の中央
    v4 = (v0 + v2) / 2          # 左辺の中央
    v5 = (v1 + v2) / 2          # 山の最深部

    # 中央にある三角形に適用するのではない
    # draw(v3, v4, v5, depth - 1)

    draw(v0, v3, v4, depth - 1) # 上
    draw(v1, v3, v5, depth - 1) # 左
    draw(v2, v4, v5, depth - 1) # 右

    triangle(v0, v1, v2)
    snapshot
  end

  # 正三角形の高さは 辺 * sin(60度) で求まる
  def canvas_wh
    V[super.x, super.x * Math.sin(2 * Math::PI * 60 / 360)]
  end
end

アルゴリズム

  1. △の3辺のそれぞれの中央の点を求める
  2. その3点を結ぶと逆三角形ができる
  3. その逆三角形を除いたときにできる3つの三角形を再分割する

具体的に書くと

  1. △の頂点を v0, v1, v2 とする
  2. v0→v1 と v1→v2 と v2→v0 の間の点を求める
  3. この点を順に v3, v4, v5 とする
  4. v0→v3→v4 と v1→v3→v5 と v2→v4→v5 の三角形を再度分割する

ここで勘違いしやすいというか自分が勘違いしていたのだけど、真ん中にできた逆三角形に対して再分割するのではない。真ん中にできた逆三角形を除いたときにできる3つの三角形を再分割する。そこさえ間違えなければ、とくに難しいところはない。

共通コード
require "#{__dir__}/../../物理/ベクトル/vec2"
require "rmagick"

include Magick

class Base
  include Math

  def call
  end

  def write(path)
    layer.write(path)
    puts path
    open path
  end

  def animation_write(path, delay: 2)
    av = image_list.optimize_layers(Magick::OptimizeLayer)
    av.delay = 100.0 / 60 * delay
    av.write(path)
    puts path
    open path
  end

  private

  def canvas_wh
    V[800, (800 / 1.618033988749895).to_i]
  end

  def wh
    canvas_wh
  end

  def title
    self.class.name.underscore
  end

  def snapshot
    image_list << layer.dup
  end

  def snapshot_counter
    @snapshot_counter ||= 0
    @snapshot_counter += 1
  end

  def open(path)
    system "open -a 'Google Chrome' #{path}"
  end

  def layer
    @layer ||= Image.new(*canvas_wh) do |e|
      e.background_color = background_color
    end
  end

  def image_list
    @image_list ||= ImageList.new
  end

  def background_color
    "white"
  end

  def foreground_color
    "grey30"
  end

  def line_width
    2
  end

  def color_from(iter)
    r = iter % 32 *  8
    b = iter %  8 * 32
    g = iter % 16 * 16
    "#%02x%02x%02x" % [r, g, b]
  end

  def draw_context
    g = Draw.new
    yield g
    g.draw(layer)
  end

  def line(v0, v1)
    draw_context do |g|
      g.stroke_width(line_width)
      g.stroke(foreground_color)
      g.line(*v0, *v1)
    end
  end

  def triangle(v0, v1, v2)
    draw_context do |g|
      g.fill("transparent")
      g.stroke_width(line_width)
      g.stroke(foreground_color)
      g.polygon(*v0, *v1, *v2)
    end
  end

  def pixel(v0, color)
    draw_context do |g|
      g.fill(color)
      g.rectangle(*v0, *v0)
    end
  end

  def rectangle(v0, v1, color: foreground_color)
    draw_context do |g|
      g.fill(color)
      g.rectangle(*v0, *v1)
    end
  end

  def rectangle_border(v0, v1)
    draw_context do |g|
      g.fill("transparent")
      g.stroke_width(1)
      g.stroke(foreground_color)
      g.rectangle(*v0, *v1)
    end
  end
end

if $0 == __FILE__
  require "rspec/autorun"
  RSpec.configure do |config|
    config.expect_with :test_unit
  end

  describe do
    it "works" do
    end
  end
end
# >> .
# >>
# >> Finished in 0.00282 seconds (files took 0.08633 seconds to load)
# >> 1 example, 0 failures
# >>

Discussion