Skip to content

Instantly share code, notes, and snippets.

@JoshCheek
Created November 27, 2016 16:16
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save JoshCheek/7e333067da09bc286858c86c78a9f860 to your computer and use it in GitHub Desktop.
Save JoshCheek/7e333067da09bc286858c86c78a9f860 to your computer and use it in GitHub Desktop.
Parametric L Systems
require 'graphics'
require 'strscan'
class LSystem
attr_accessor :rules, :cache
def initialize(start:, rules:)
self.rules = rules
self.cache = [[start]]
end
def at_depth(n)
return cache[n] if n < cache.length
cache[n] = at_depth(n-1).flat_map do |fn, *args|
if rules.key?(fn)
rules[fn].call(*args) # substitute
else
[[fn, *args]] # do not substitute
end
end
end
end
class Triangle < Graphics::Simulation
include Math
def π
PI
end
def º2ø(º)
º * π / 180
end
def initialize
width = 800
height = 600
bits_per_pixel = 24
super width, height, bits_per_pixel
end
def draw(n)
return if n > 14
stack = []
stride = h/2 - 30
x = w/2
y = 30
ø = π/2
∆ø = º2ø(90)
colour = :white
# http://algorithmicbotany.org/papers/abop/abop-ch1.pdf
# pages 48 - 50
r = 1.456
@triangle_system ||= LSystem.new(
start: [:A, 1],
rules: {
A: -> s {[
[:F, s],
[:"["], [:+], [:A, s/r], [:"]"],
[:"["], [:-], [:A, s/r], [:"]"],
]}
}
)
@triangle_system.at_depth(n).each do |fn_name, *args|
case fn_name
when :A
# noop
when :F
multipler = args[0]
distance = stride * multipler
old_x = x
old_y = y
x = x + distance*cos(ø)
y = y + distance*sin(ø)
line(old_x, old_y, x, y, colour)
when :+
ø += ∆ø
when :-
ø -= ∆ø
when :"["
stack.push [x, y, ø]
when :"]"
x, y, ø = stack.pop
else
raise "wat: #{fn_name.inspect} #{args.map(&:inspect).join(', ')}"
end
end
end
end
Triangle.new.run
require 'graphics'
# p47 - p48 of http://algorithmicbotany.org/papers/abop/abop-ch1.pdf
class Triangle < Graphics::Simulation
include Math
def initialize
width = 800
height = 600
bits_per_pixel = 24
super width, height, bits_per_pixel
end
def draw(iteration)
return if iteration > 6
clear :white
stride = 400 # how long the base line is
x = 100 # from left
y = 100 # from bottom
ø = 0 # facing right
∆ø = 86*PI/180 # 86º places gaps in the result (1.37a)
# ∆ø = 90*PI/180 # 90º everything is flush (1.37b)
c = 1
p = 0.3
q = c - p
h = (p * q) ** 0.5
instructions = [['F', 1]]
rules = {
'F' => lambda do |x|
[ ['F', x * p],
['+'],
['F', x * h],
['-'],
['-'],
['F', x * h],
['+'],
['F', x * q],
]
end
}
iteration.times do
instructions = instructions.flat_map do |fn, *args|
if rules.key?(fn)
rules[fn].call(*args) # substitute
else
[[fn, *args]] # do not substitute
end
end
end
instructions.each do |fn_name, *args|
case fn_name
when "A"
# noop
when "F"
multipler = args[0]
distance = stride * multipler
old_x = x
old_y = y
x = x + distance*cos(ø)
y = y + distance*sin(ø)
line(old_x, old_y, x, y, :black)
when "+"
ø += ∆ø
when "-"
ø -= ∆ø
when "["
stack.push [x, y, ø]
when "]"
x, y, ø = stack.pop
else
raise "wat: #{fn_name.inspect} #{args.inspect}"
end
end
end
end
Triangle.new.run
# encoding: utf-8
# p47 - p48 of http://algorithmicbotany.org/papers/abop/abop-ch1.pdf
require 'graphics'
class Triangle < Graphics::Simulation
include Math
def initialize
width = 800
height = 600
bits_per_pixel = 24
super width, height, bits_per_pixel
∆ø = 86*PI/180 # 86º places gaps in the result (1.37a)
# ∆ø = 90*PI/180 # 90º everything is flush (1.37b)
ø = 0 # direction the turtle is facing
x = 50 # left margin
y = 200 # bottom margin
stride = 450 # how long the base line is
c = 1
p = 0.3
q = c - p
h = (p * q) ** 0.5
instructions = [['F', 1, 0]]
rules = {
'F' => lambda do |x, t|
# It splits the triangle into 4 parts, like this:
#
# /C\
# / | \
# / | 3 /\.
# / 2 | / \.
# / \ | / 4 \.
# / 1 \|/ \
# A------B---------------D
#
# And it move like this:
# A->B for 0.3 (this will spawn triangle 1 later)
# turn left to face C
# B->C for 0.7 (this will spawn triangle 2 later)
# turn right twice to face B
# C->B for 0.7 (this will spawn triangle 3 later)
# turn left to face D
# B->D for 0.7 (this will spawn triangle 4 later)
#
# "t" is a timer it uses to delay writing triangle 1 for 2 iterations
# and triangles 2 and 3 for 1 iteration. I assume this number is picked
# because it will take 3 more iterations before triangle 4's subtraingle
# at D is the same size as 1. Thus it will be relatively sparse by comparison.
# I don't really understand how delaying 1 fixes this since that subtraingle
# will be delayed, as well. I suspect the answer is just that at any given point,
# the things line up such that the difference is minimized.
if t == 0
[ ['F', x * p, 2],
['+'],
['F', x * h, 1],
['-'],
['-'],
['F', x * h, 1],
['+'],
['F', x * q, 0],
]
else
[['F', x, t-1]]
end
end
}
10.times do
instructions = instructions.flat_map do |fn, *args|
if rules.key?(fn)
rules[fn].call(*args) # substitute
else
[[fn, *args]] # do not substitute
end
end
end
@draw_function = lambda do
instructions.each do |fn_name, *args|
case fn_name
when "A"
# noop
when "F"
multipler = args[0]
distance = stride * multipler
old_x = x
old_y = y
x = x + distance*cos(ø)
y = y + distance*sin(ø)
line(old_x, old_y, x, y, :black)
when "+"
ø += ∆ø
when "-"
ø -= ∆ø
when "["
stack.push [x, y, ø]
when "]"
x, y, ø = stack.pop
else
raise "wat: #{fn_name.inspect} #{args.inspect}"
end
end
end
end
def draw(n)
return if n > 1
clear :white
@draw_function.call
end
end
Triangle.new.run
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment