❄️

【フラクタル図形】コッホ雪片の書き方

2023/08/28に公開

class KochFlake < Base
  def call
    # この2つのパラメータだけでかなり変わる
    depth = 3                   # キザギザ度
    vertex_n = 3                # 頂点数

    radius = wh.length / 2 * 0.65    # 大きさ

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

    (vertexes + vertexes.take(1)).each_cons(2) { |v| draw(*v, depth) }
  end

  def draw(v1, v2, depth)
    if depth == 0
      line(v1, v2)
      snapshot
      return
    end

    d = v2 - v1
    v3 = v1 + d * 1.0 / 3       # 左底辺
    v5 = v1 + d * 1.5 / 3       # 高さ0の頂点
    v4 = v1 + d * 2.0 / 3       # 右底辺

    # 辺の中央から垂直方向に1/3の長さの斜めの辺が60度になるように持ち上げる
    v5 += d.perp.perp.perp.normalize * (d.length / 3) * sin(60.deg_to_rad)

    draw(v1, v3, depth - 1)     # 駅→麓
    draw(v3, v5, depth - 1)     # 登山
    draw(v5, v4, depth - 1)     # 下山
    draw(v4, v2, depth - 1)     # 隣の麓→隣駅
  end

  def canvas_wh
    V[800, 800]
  end
end

山頂の求め方

__/\__ の部分の頂点を求めるのが少し難しかった。

もともと depth = 0 の場合は v1 ________ v2 のようにまっすぐ線が引かれるので全体で見ればただの△になる。depth = 1 にするとそこから辺の途中が v1 __/\__ v2 のように起伏し、途中に山ができる。ここで v1→v2 の間をベクトル d として山頂を v5 としたときの v5 の求め方を順に書くと、

山の斜辺の長さを求める

山の斜辺は d の長さの3分の1なので d.length / 3 で求まる。これは頭で考えるより図で書いた方がわかりやすい。別に3分の1でなくてもいいけど3分の1にすると整って見える。

山の斜辺の角度を決める

頂点が3つある場合に60度だと山から降りるときの辺が水平になって綺麗だから60度にする。言いかえると、☆の肩の部分が水平になって綺麗だから、になる。別に60度でなくてもいい。

山の高さを求める

sinθ = 対辺 / 斜辺 で、知りたいのが「山の高さ」こと「対辺」になる。斜辺は d.length / 3 で角度は「60度」なので代入すると 対辺 = (d.length / 3) * sin(60度) になる。

傾ける

d が時計の12時を指しているとして9時に向けるには3時に向く法線を3回呼ぶ。つまり (-y, x) を3回呼べば (y, -x) になる。これは最初から (d.y, -d.x) としてもいい。山頂にこの9時方向の単位ベクトルを掛けると傾く。もし3時に向けると☆の尖った部分が内側に尖るようになるので奇妙な図形が出来上がる。

高さ0の位置を足す

v1→v2 間の半分の位置なので v1 + (d / 2) で求まる。これを最後に足す。

共通コード
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