I wrote a thing forever ago that generates SVG images of the Penrose tiling.
Note to self: in order to generate this animation, I’ve generated the output for every step in svg using said tool, then I’ve converted the svg
output to png
using Imagick:
convert output/svg/penrose1.svg output/png/penrose1.png
Then I used ffmpeg to generate the gif:
ffmpeg -framerate 1 -i "output/png/penrose%d.png" output/animation.gif
Starting from 10 red triangles distributed along a circle, each triangle is subdivided.
There are 2 kinds of triangles, red and blue, and each are subdivided differently, always creating new red or blue triangles with the same proportions:
IMG_WIDTH = 1000
IMG_HEIGHT = 1000
if ARGV.count < 3
depth = 5
nb_triangles = 10
start_color = :red
else
depth = ARGV[0].to_i
nb_triangles = ARGV[1].to_i
start_color = ARGV[2].to_sym
end
GOLDEN_RATIO = (1 + Math.sqrt(5)) / 2.0
class Point
attr_accessor :x, :y
def initialize(x = 0, y = 0)
@x = x
@y = y
end
def self.transform(p)
Point.new((IMG_WIDTH/2 + p.x * 0.8 * IMG_WIDTH/2).to_i, (IMG_HEIGHT/2 + p.y * 0.8 * IMG_HEIGHT/2).to_i)
end
def self.lerp(a, b, alpha)
Point.new(a.x + (b.x - a.x)*alpha, a.y + (b.y - a.y)*alpha)
end
end
class Triangle
attr_accessor :a, :b, :c
attr_accessor :color
def initialize(a, b, c, color)
@a = a
@b = b
@c = c
@color = color
end
def subdivide()
result = []
if color == :red
p = Point.lerp(a, b, 1/GOLDEN_RATIO)
result << [Triangle.new(c, p, b, :red), Triangle.new(p, c, a, :blue)]
else
q = Point.lerp(b, a, 1/GOLDEN_RATIO)
r = Point.lerp(b, c, 1/GOLDEN_RATIO)
result << [Triangle.new(r, c, a, :blue), Triangle.new(q, r, b, :blue), Triangle.new(r, q, a, :red)]
end
return result
end
end
initial_triangles = []
nb_triangles.times do |i|
b_complex = Complex.polar(1, ((2*i - 1)*Math::PI/10))
c_complex = Complex.polar(1, ((2*i + 1)*Math::PI/10))
b_complex, c_complex = c_complex, b_complex if i.even?
b_point = Point.new(b_complex.real, b_complex.imaginary)
c_point = Point.new(c_complex.real, c_complex.imaginary)
initial_triangles << Triangle.new(Point.new, b_point, c_point, start_color)
end
triangles = initial_triangles
(depth-1).times do
triangles = triangles.map { |t| t.subdivide }.flatten
end
def line(a, b, stroke_color)
"<line x1=\"#{a.x}\" y1=\"#{a.y}\" x2=\"#{b.x}\" y2=\"#{b.y}\" stroke=\"#{stroke_color}\" />\n"
end
def triangle(a, b, c, fill_color)
"<polygon points=\"#{a.x},#{a.y} #{b.x},#{b.y} #{c.x},#{c.y}\" fill=\"#{fill_color}\" />\n"
end
red_fill_color = '#FF6060'
blue_fill_color = '#6060FF'
stroke_color = '#404040'
img = "<svg viewBox=\"0 0 #{IMG_WIDTH} #{IMG_HEIGHT}\" xmlns=\"http://www.w3.org/2000/svg\">\n"
triangles.each do |t|
a = Point.transform(t.a)
b = Point.transform(t.b)
c = Point.transform(t.c)
img << triangle(a, b, c, t.color == :red ? red_fill_color : blue_fill_color)
img << line(a, b, stroke_color)
img << line(c, a, stroke_color)
end
img << "</svg>"
puts img
You can provide the depth (1 to 10, default 5), the number of subdivisions of the circle (10 for penrose), and the start color (red or blue, default red)
$ ruby main.rb > penrose-depth-5.svg
$ ruby main.rb 3 10 blue > test.svg
Here is the output at depth 4: