Penrose tiling
A Penrose tiling can cover an entire plane without creating a pattern that periodically repeats.
There are many tile sets that can create non-periodic tilings, but those can typically also be used to create a periodic
tiling. What makes Penrose tiles special is that they can only be used to produce non-periodic tilings.
The two best-known Penrose tile sets are Kite and Dart (P2)
and Thin Rhombus and Fat Rhombus (P3)
These so-called prototiles are usually depicted with smooth edges, but in reality Penrose tiles have interlocking tabs and cut-outs like the pieces of a jigsaw puzzle. For convenience these deformations are often replaced with matching rules, which ensure that the tiles are only connected in ways that guarantee a non-periodic tiling. (Otherwise, for instance, you could combine the kite and dart to form a rhombus, and easily create a periodic tiling from there.)
You can construct a Penrose tiling by setting up some prototiles, and adding tiles through trial and error, backtracking whenever you get stuck.
More commonly a method is used that takes advantage of the fact that Penrose tilings, like fractals, have a self-similarity on different levels. When zooming out it can be observed that groups of tiles are enclosed in areas that form exactly the same pattern as the tiles on the lower level. Departing from an inflated level, the prototiles can be subdivided into smaller tiles, always observing the matching rules. The subdivision may have to be repeated several times, before the desired level of detail is reached. This process is called deflation.
More information can be found through the links below.
The task: fill a rectangular area with a Penrose tiling.
- See also
- A good introduction (ams.org)
- Deflation explained for both sets (tartarus.org)
- Deflation explained for Kite and Dart, includes Python code (preshing.com)
C++
<lang cpp>#include <cmath>
- include <cstdlib>
- include <fstream>
- include <iomanip>
- include <iostream>
- include <set>
- include <sstream>
- include <stack>
- include <string>
- include <tuple>
int main() {
std::ofstream out("penrose_tiling.svg"); if (!out) { std::cerr << "Cannot open output file.\n"; return EXIT_FAILURE; } std::string penrose("[N]++[N]++[N]++[N]++[N]"); for (int i = 1; i <= 4; ++i) { std::string next; for (char ch : penrose) { switch (ch) { case 'A': break; case 'M': next += "OA++PA----NA[-OA----MA]++"; break; case 'N': next += "+OA--PA[---MA--NA]+"; break; case 'O': next += "-MA++NA[+++OA++PA]-"; break; case 'P': next += "--OA++++MA[+PA++++NA]--NA"; break; default: next += ch; break; } } penrose = std::move(next); } const double r = 30; const double pi5 = 0.628318530717959; double x = r * 8, y = r * 8, theta = pi5; std::set<std::string> svg; std::stack<std::tuple<double, double, double>> stack; for (char ch : penrose) { switch (ch) { case 'A': { double nx = x + r * std::cos(theta); double ny = y + r * std::sin(theta); std::ostringstream line; line << std::fixed << std::setprecision(3) << "<line x1='" << x << "' y1='" << y << "' x2='" << nx << "' y2='" << ny << "'/>"; svg.insert(line.str()); x = nx; y = ny; } break; case '+': theta += pi5; break; case '-': theta -= pi5; break; case '[': stack.push({x, y, theta}); break; case ']': std::tie(x, y, theta) = stack.top(); stack.pop(); break; } } out << "<svg xmlns='http://www.w3.org/2000/svg' height='" << r * 16 << "' width='" << r * 16 << "'>\n" << "<rect height='100%' width='100%' fill='black'/>\n" << "<g stroke='rgb(255,165,0)'>\n"; for (const auto& line : svg) out << line << '\n'; out << "</g>\n</svg>\n"; return EXIT_SUCCESS;
}</lang>
- Output:
Writes a file in SVG format similar to that produced by the Perl solution.
Go
<lang go>package main
import (
"github.com/fogleman/gg" "math"
)
type tiletype int
const (
kite tiletype = iota dart
)
type tile struct {
tt tiletype x, y float64 angle, size float64
}
var gr = (1 + math.Sqrt(5)) / 2 // golden ratio
const theta = math.Pi / 5 // 36 degrees in radians
func setupPrototiles(w, h int) []tile {
var proto []tile // sun for a := math.Pi/2 + theta; a < 3*math.Pi; a += 2 * theta { ww := float64(w / 2) hh := float64(h / 2) proto = append(proto, tile{kite, ww, hh, a, float64(w) / 2.5}) } return proto
}
func distinctTiles(tls []tile) []tile {
tileset := make(map[tile]bool) for _, tl := range tls { tileset[tl] = true } distinct := make([]tile, len(tileset)) for tl, _ := range tileset { distinct = append(distinct, tl) } return distinct
}
func deflateTiles(tls []tile, gen int) []tile {
if gen <= 0 { return tls } var next []tile for _, tl := range tls { x, y, a, size := tl.x, tl.y, tl.angle, tl.size/gr var nx, ny float64 if tl.tt == dart { next = append(next, tile{kite, x, y, a + 5*theta, size}) for i, sign := 0, 1.0; i < 2; i, sign = i+1, -sign { nx = x + math.Cos(a-4*theta*sign)*gr*tl.size ny = y - math.Sin(a-4*theta*sign)*gr*tl.size next = append(next, tile{dart, nx, ny, a - 4*theta*sign, size}) } } else { for i, sign := 0, 1.0; i < 2; i, sign = i+1, -sign { next = append(next, tile{dart, x, y, a - 4*theta*sign, size}) nx = x + math.Cos(a-theta*sign)*gr*tl.size ny = y - math.Sin(a-theta*sign)*gr*tl.size next = append(next, tile{kite, nx, ny, a + 3*theta*sign, size}) } } } // remove duplicates tls = distinctTiles(next) return deflateTiles(tls, gen-1)
}
func drawTiles(dc *gg.Context, tls []tile) {
dist := [2][3]float64{{gr, gr, gr}, {-gr, -1, -gr}} for _, tl := range tls { angle := tl.angle - theta dc.MoveTo(tl.x, tl.y) ord := tl.tt for i := 0; i < 3; i++ { x := tl.x + dist[ord][i]*tl.size*math.Cos(angle) y := tl.y - dist[ord][i]*tl.size*math.Sin(angle) dc.LineTo(x, y) angle += theta } dc.ClosePath() if ord == kite { dc.SetHexColor("FFA500") // orange } else { dc.SetHexColor("FFFF00") // yellow } dc.FillPreserve() dc.SetHexColor("A9A9A9") // dark gray dc.SetLineWidth(1) dc.Stroke() }
}
func main() {
w, h := 700, 450 dc := gg.NewContext(w, h) dc.SetRGB(1, 1, 1) dc.Clear() tiles := deflateTiles(setupPrototiles(w, h), 5) drawTiles(dc, tiles) dc.SavePNG("penrose_tiling.png")
}</lang>
- Output:
Image same as Java entry.
Java
<lang java>import java.awt.*; import java.util.List; import java.awt.geom.Path2D; import java.util.*; import javax.swing.*; import static java.lang.Math.*; import static java.util.stream.Collectors.toList;
public class PenroseTiling extends JPanel {
// ignores missing hash code class Tile { double x, y, angle, size; Type type;
Tile(Type t, double x, double y, double a, double s) { type = t; this.x = x; this.y = y; angle = a; size = s; }
@Override public boolean equals(Object o) { if (o instanceof Tile) { Tile t = (Tile) o; return type == t.type && x == t.x && y == t.y && angle == t.angle; } return false; } }
enum Type { Kite, Dart }
static final double G = (1 + sqrt(5)) / 2; // golden ratio static final double T = toRadians(36); // theta
List<Tile> tiles = new ArrayList<>();
public PenroseTiling() { int w = 700, h = 450; setPreferredSize(new Dimension(w, h)); setBackground(Color.white);
tiles = deflateTiles(setupPrototiles(w, h), 5); }
List<Tile> setupPrototiles(int w, int h) { List<Tile> proto = new ArrayList<>();
// sun for (double a = PI / 2 + T; a < 3 * PI; a += 2 * T) proto.add(new Tile(Type.Kite, w / 2, h / 2, a, w / 2.5));
return proto; }
List<Tile> deflateTiles(List<Tile> tls, int generation) { if (generation <= 0) return tls;
List<Tile> next = new ArrayList<>();
for (Tile tile : tls) { double x = tile.x, y = tile.y, a = tile.angle, nx, ny; double size = tile.size / G;
if (tile.type == Type.Dart) { next.add(new Tile(Type.Kite, x, y, a + 5 * T, size));
for (int i = 0, sign = 1; i < 2; i++, sign *= -1) { nx = x + cos(a - 4 * T * sign) * G * tile.size; ny = y - sin(a - 4 * T * sign) * G * tile.size; next.add(new Tile(Type.Dart, nx, ny, a - 4 * T * sign, size)); }
} else {
for (int i = 0, sign = 1; i < 2; i++, sign *= -1) { next.add(new Tile(Type.Dart, x, y, a - 4 * T * sign, size));
nx = x + cos(a - T * sign) * G * tile.size; ny = y - sin(a - T * sign) * G * tile.size; next.add(new Tile(Type.Kite, nx, ny, a + 3 * T * sign, size)); } } } // remove duplicates tls = next.stream().distinct().collect(toList());
return deflateTiles(tls, generation - 1); }
void drawTiles(Graphics2D g) { double[][] dist = {{G, G, G}, {-G, -1, -G}}; for (Tile tile : tiles) { double angle = tile.angle - T; Path2D path = new Path2D.Double(); path.moveTo(tile.x, tile.y);
int ord = tile.type.ordinal(); for (int i = 0; i < 3; i++) { double x = tile.x + dist[ord][i] * tile.size * cos(angle); double y = tile.y - dist[ord][i] * tile.size * sin(angle); path.lineTo(x, y); angle += T; } path.closePath(); g.setColor(ord == 0 ? Color.orange : Color.yellow); g.fill(path); g.setColor(Color.darkGray); g.draw(path); } }
@Override public void paintComponent(Graphics og) { super.paintComponent(og); Graphics2D g = (Graphics2D) og; g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); drawTiles(g); }
public static void main(String[] args) { SwingUtilities.invokeLater(() -> { JFrame f = new JFrame(); f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); f.setTitle("Penrose Tiling"); f.setResizable(false); f.add(new PenroseTiling(), BorderLayout.CENTER); f.pack(); f.setLocationRelativeTo(null); f.setVisible(true); }); }
}</lang>
Julia
<lang julia>using Printf
function drawpenrose()
lindenmayer_rules = Dict("A" => "", "M" => "OA++PA----NA[-OA----MA]++", "N" => "+OA--PA[---MA--NA]+", "O" => "-MA++NA[+++OA++PA]-", "P" => "--OA++++MA[+PA++++NA]--NA")
rul(x) = lindenmayer_rules[x]
penrose = replace(replace(replace(replace("[N]++[N]++[N]++[N]++[N]", r"[AMNOP]" => rul), r"[AMNOP]" => rul), r"[AMNOP]" => rul), r"[AMNOP]" => rul)
x, y, theta, r, svglines, stack = 160, 160, π / 5, 20.0, String[], Vector{Real}[]
for c in split(penrose, "") if c == "A" xx, yy = x + r * cos(theta), y + r * sin(theta) line = @sprintf("<line x1='%.1f' y1='%.1f' x2='%.1f' y2='%.1f' style='stroke:rgb(255,165,0)'/>\n", x, y, xx, yy) x, y = xx, yy push!(svglines, line) elseif c == "+" theta += π / 5 elseif c == "-" theta -= π / 5 elseif c == "[" push!(stack, [x, y, theta]) elseif c == "]" x, y, theta = pop!(stack) end end
svg = join(unique(svglines), "\n") fp = open("penrose_tiling.svg", "w") write(fp, """<svg xmlns="http://www.w3.org/2000/svg" height="350" width="350"> <rect height="100%" """ * """width="100%" style="fill:black" />""" * "\n$svg</svg>") close(fp)
end
drawpenrose() </lang>
Kotlin
<lang scala>// version 1.1.2
import java.awt.* import java.awt.geom.Path2D import javax.swing.*
class PenroseTiling(w: Int, h: Int) : JPanel() {
private enum class Type { KITE, DART }
private class Tile( val type: Type, val x: Double, val y: Double, val angle: Double, val size: Double ) { override fun equals(other: Any?): Boolean { if (other == null || other !is Tile) return false return type == other.type && x == other.x && y == other.y && angle == other.angle && size == other.size } }
private companion object { val G = (1.0 + Math.sqrt(5.0)) / 2.0 // golden ratio val T = Math.toRadians(36.0) // theta }
private val tiles: List<Tile>
init { preferredSize = Dimension(w, h) background = Color.white tiles = deflateTiles(setupPrototiles(w, h), 5) }
private fun setupPrototiles(w: Int, h: Int): List<Tile> { val proto = mutableListOf<Tile>() var a = Math.PI / 2.0 + T while (a < 3.0 * Math.PI) { proto.add(Tile(Type.KITE, w / 2.0, h / 2.0, a, w / 2.5)) a += 2.0 * T } return proto }
private fun deflateTiles(tls: List<Tile>, generation: Int): List<Tile> { if (generation <= 0) return tls val next = mutableListOf<Tile>() for (tile in tls) { val x = tile.x val y = tile.y val a = tile.angle var nx: Double var ny: Double val size = tile.size / G if (tile.type == Type.DART) { next.add(Tile(Type.KITE, x, y, a + 5.0 * T, size)) var sign = 1 for (i in 0..1) { nx = x + Math.cos(a - 4.0 * T * sign) * G * tile.size ny = y - Math.sin(a - 4.0 * T * sign) * G * tile.size next.add(Tile(Type.DART, nx, ny, a - 4.0 * T * sign, size)) sign *= -1 } } else { var sign = 1 for (i in 0..1) { next.add(Tile(Type.DART, x, y, a - 4.0 * T * sign, size)) nx = x + Math.cos(a - T * sign) * G * tile.size ny = y - Math.sin(a - T * sign) * G * tile.size next.add(Tile(Type.KITE, nx, ny, a + 3.0 * T * sign, size)) sign *= -1 } } } // remove duplicates and deflate return deflateTiles(next.distinct(), generation - 1) }
private fun drawTiles(g: Graphics2D) { val dist = arrayOf( doubleArrayOf(G, G, G), doubleArrayOf(-G, -1.0, -G) ) for (tile in tiles) { var angle = tile.angle - T val path = Path2D.Double() path.moveTo(tile.x, tile.y) val ord = tile.type.ordinal for (i in 0..2) { val x = tile.x + dist[ord][i] * tile.size * Math.cos(angle) val y = tile.y - dist[ord][i] * tile.size * Math.sin(angle) path.lineTo(x, y) angle += T } path.closePath() with(g) { color = if (ord == 0) Color.pink else Color.red fill(path) color = Color.darkGray draw(path) } } }
override fun paintComponent(og: Graphics) { super.paintComponent(og) val g = og as Graphics2D g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON) drawTiles(g) }
}
fun main(args: Array<String>) {
SwingUtilities.invokeLater { val f = JFrame() with (f) { defaultCloseOperation = JFrame.EXIT_ON_CLOSE title = "Penrose Tiling" isResizable = false add(PenroseTiling(700, 450), BorderLayout.CENTER) pack() setLocationRelativeTo(null) isVisible = true } }
}</lang>
Nim
This is a translation of the Lindenmayer Phix version translated itself from Perl. <lang Nim>import math, strformat, tables
const Lindenmayer = {'A': "",
'M': "OA++PA----NA[-OA----MA]++", 'N': "+OA--PA[---MA--NA]+", 'O': "-MA++NA[+++OA++PA]-", 'P': "--OA++++MA[+PA++++NA]--NA"}.toTable
var penrose = "[N]++[N]++[N]++[N]++[N]"
for _ in 1..4:
var next = "" for ch in penrose: next.add Lindenmayer.getOrDefault(ch, $ch) penrose = move(next)
var
x, y = 160.0 theta = PI / 5 r = 20.0
var svg = "" var stack: seq[(float, float, float)]
for ch in penrose:
case ch of 'A': let (nx, ny) = (x + r * cos(theta), y + r * sin(theta)) svg.add &"<line x1='{x:.1f}' y1='{y:.1f}' x2='{nx:.1f}' y2='{ny:.1f}'" svg.add " style='stroke:rgb(255,165,0)'/>\n" (x, y) = (nx, ny) of '+': theta += PI / 5 of '-': theta -= PI / 5 of '[': stack.add (x, y, theta) of ']': (x, y, theta) = stack.pop() else: discard
let svgFile = "penrose_tiling.svg".open(fmWrite) svgFile.write """ <svg xmlns="http://www.w3.org/2000/svg" height="350" width="350"> <rect height="100%%" width="100%%" style="fill:black" /> """ svgFile.write svg, "</svg>" svgFile.close()</lang>
- Output:
Same output as Perl.
Perl
<lang perl>use constant pi => 2 * atan2(1, 0);
- Generated with a P3 tile set using a Lindenmayer system.
%rules = (
A => , M => 'OA++PA----NA[-OA----MA]++', N => '+OA--PA[---MA--NA]+', O => '-MA++NA[+++OA++PA]-', P => '--OA++++MA[+PA++++NA]--NA'
); $penrose = '[N]++[N]++[N]++[N]++[N]'; $penrose =~ s/([AMNOP])/$rules{$1}/eg for 1..4;
- Draw the curve in SVG
($x, $y) = (160, 160); $theta = pi/5; $r = 20;
for (split //, $penrose) {
if (/A/) { $line = sprintf "<line x1='%.1f' y1='%.1f' ", $x, $y; $line .= sprintf "x2='%.1f' ", $x += $r * cos($theta); $line .= sprintf "y2='%.1f' ", $y += $r * sin($theta); $line .= "style='stroke:rgb(255,165,0)'/>\n"; $SVG{$line} = 1; } elsif (/\+/) { $theta += pi/5 } elsif (/\-/) { $theta -= pi/5 } elsif (/\[/) { push @stack, [$x, $y, $theta] } elsif (/\]/) { ($x, $y, $theta) = @{pop @stack} }
} $svg .= $_ for keys %SVG; open $fh, '>', 'penrose_tiling.svg'; print $fh qq{<svg xmlns="http://www.w3.org/2000/svg" height="350" width="350"> <rect height="100%" width="100%" style="fill:black" />\n$svg</svg>}; close $fh;</lang> Penrose tiling (offsite image)
Phix
Translation of the original Python code. Output can be toggled to look like the java or perl output.
You can run this online here.
-- -- demo\rosetta\Penrose_tiling.exw -- =============================== -- -- Resizeable. Press space to iterate/subdivide, C to toggle colour scheme -- bool yellow_orange = true -- false = magenta on black, outlines only with javascript_semantics constant title = "Penrose tiling" include pGUI.e Ihandle dlg, canvas cdCanvas cddbuffer, cdcanvas include builtins\complex.e constant golden_ratio = (1 + sqrt(5)) / 2 function subdivide(sequence triangles) sequence result = {} integer colour complex A, B, C, P, Q, R for i=1 to length(triangles) do {colour, A, B, C} = triangles[i] if colour == 0 then -- Subdivide orange triangle P = complex_add(A,complex_div(complex_sub(B,A),golden_ratio)) result &= {{0, C, P, B}, {1, P, C, A}} else -- Subdivide yellow triangle Q = complex_add(B,complex_div(complex_sub(A,B),golden_ratio)) R = complex_add(B,complex_div(complex_sub(C,B),golden_ratio)) result &= {{1, R, C, A}, {1, Q, R, B}, {0, R, Q, A}} end if end for return result end function function initial_wheel() -- Create an initial wheel of yellow triangles around the origin sequence triangles = {} complex B, C atom phi for i=0 to 9 do phi = (2*i-1)*PI/10 B = {cos(phi),sin(phi)} phi = (2*i+1)*PI/10 C = {cos(phi),sin(phi)} if mod(i,2)==0 then {B, C} = {C, B} -- mirror every second triangle end if triangles &= {{0, {0,0}, B, C}} end for return subdivide(triangles) -- ... and iterate once end function sequence triangles = initial_wheel() integer hw, hh, h procedure draw_one(sequence triangle, integer colour, mode) if yellow_orange then cdCanvasSetForeground(cddbuffer, colour) end if cdCanvasBegin(cddbuffer, mode) for i=2 to 4 do atom {x,y} = triangle[i] cdCanvasVertex(cddbuffer, x*h+hw, y*h+hh) end for cdCanvasEnd(cddbuffer) end procedure function redraw_cb(Ihandle /*ih*/, integer /*posx*/, /*posy*/) {hw, hh} = sq_floor_div(IupGetIntInt(canvas, "DRAWSIZE"),2) h = min(hw,hh) if yellow_orange then cdCanvasSetBackground(cddbuffer, CD_WHITE) else cdCanvasSetBackground(cddbuffer, CD_BLACK) cdCanvasSetForeground(cddbuffer, CD_MAGENTA) end if cdCanvasActivate(cddbuffer) cdCanvasClear(cddbuffer) for i=1 to length(triangles) do sequence triangle = triangles[i] if yellow_orange then integer colour = iff(triangle[1]?CD_ORANGE:CD_YELLOW) draw_one(triangle,colour,CD_FILL) end if draw_one(triangle,CD_DARK_GREY,CD_CLOSED_LINES) end for cdCanvasFlush(cddbuffer) return IUP_DEFAULT end function function map_cb(Ihandle ih) cdcanvas = cdCreateCanvas(CD_IUP, ih) cddbuffer = cdCreateCanvas(CD_DBUFFER, cdcanvas) return IUP_DEFAULT end function function key_cb(Ihandle /*ih*/, atom c) if c=K_ESC then return IUP_CLOSE end if if c=' ' then if length(triangles)<=6100 then -- sane limit triangles = subdivide(triangles) IupUpdate(canvas) else IupSetAttribute(dlg,"TITLE",title & " (sane limit reached)") end if elsif upper(c)='C' then yellow_orange = not yellow_orange IupUpdate(canvas) end if return IUP_CONTINUE end function procedure main() IupOpen() canvas = IupCanvas("RASTERSIZE=600x600") IupSetCallbacks(canvas, {"MAP_CB", Icallback("map_cb"), "ACTION", Icallback("redraw_cb")}) dlg = IupDialog(canvas, `TITLE="%s"`,{title}) IupSetCallback(dlg, "KEY_CB", Icallback("key_cb")) IupShow(dlg) IupSetAttribute(canvas, "RASTERSIZE", NULL) -- release the minimum limitation if platform()!=JS then IupMainLoop() IupClose() end if end procedure main()
Lindenmayer/svg
Same output, obviously the resulting file can be opened in a separate browser.
without js constant Lindenmayer = new_dict({{'A',""}, {'M',"OA++PA----NA[-OA----MA]++"}, {'N',"+OA--PA[---MA--NA]+"}, {'O',"-MA++NA[+++OA++PA]-"}, {'P',"--OA++++MA[+PA++++NA]--NA"}}) string penrose = "[N]++[N]++[N]++[N]++[N]" for n=1 to 4 do string next = "" for i=1 to length(penrose) do integer ch = penrose[i] object l = getd(ch,Lindenmayer) next &= iff(l=NULL?ch:l) end for penrose = next end for atom x=160, y=160, theta=PI/5, r = 20 string svg = "" constant line = "<line x1='%.1f' y1='%.1f' x2='%.1f' y2='%.1f' style='stroke:rgb(255,165,0)'/>\n" sequence stack = {} for i=1 to length(penrose) do integer ch = penrose[i] switch ch do case 'A': atom nx = x+r*cos(theta), ny = y+r*sin(theta) svg &= sprintf(line,{x,y,nx,ny}) {x,y} = {nx,ny} case '+': theta += PI/5 case '-': theta -= PI/5 case '[': stack = append(stack,{x,y,theta}) case ']': {x,y,theta} = stack[$] stack = stack[1..$-1] end switch end for constant svgfmt = """ <svg xmlns="http://www.w3.org/2000/svg" height="350" width="350"> <rect height="100%%" width="100%%" style="fill:black" /> %s </svg>""" integer fn = open("penrose_tiling.svg","w") printf(fn,svgfmt,svg) close(fn)
Racket
<lang racket>#lang racket
(require racket/draw)
(define rules '([M . (O A + + P A - - - - N A < - O A - - - - M A > + +)]
[N . (+ O A - - P A < - - - M A - - N A > +)] [O . (- M A + + N A < + + + O A + + P A > -)] [P . (- - O A + + + + M A < + P A + + + + N A > - - N A)] [S . (< N > + + < N > + + < N > + + < N > + + < N >)]))
(define (get-cmds n cmd)
(cond [(= 0 n) (list cmd)] [else (append-map (curry get-cmds (sub1 n)) (dict-ref rules cmd (list cmd)))]))
(define (make-curve DIM N R OFFSET COLOR BACKGROUND-COLOR)
(define target (make-bitmap DIM DIM)) (define dc (new bitmap-dc% [bitmap target])) (send dc set-background BACKGROUND-COLOR) (send dc set-pen COLOR 1 'solid) (send dc clear) (for/fold ([x 160] [y 160] [θ (/ pi 5)] [S '()]) ([cmd (in-list (get-cmds N 'S))]) (define (draw/values x* y* θ* S*) (send/apply dc draw-line (map (curry + OFFSET) (list x y x* y*))) (values x* y* θ* S*)) (match cmd ['A (draw/values (+ x (* R (cos θ))) (+ y (* R (sin θ))) θ S)] ['+ (values x y (+ θ (/ pi 5)) S)] ['- (values x y (- θ (/ pi 5)) S)] ['< (values x y θ (cons (list x y θ) S))] ['> (match-define (cons (list x y θ) S*) S) (values x y θ S*)] [_ (values x y θ S)])) target)
(make-curve 500 4 20 80 (make-color 255 255 0) (make-color 0 0 0))</lang>
Raku
(formerly Perl 6)
Generated with a P3 tile set using a Lindenmayer system.
<lang perl6>use SVG;
role Lindenmayer {
has %.rules; method succ {
self.comb.map( { %!rules{$^c} // $c } ).join but Lindenmayer(%!rules)
}
}
my $penrose = '[N]++[N]++[N]++[N]++[N]' but Lindenmayer(
{ A => , M => 'OA++PA----NA[-OA----MA]++', N => '+OA--PA[---MA--NA]+', O => '-MA++NA[+++OA++PA]-', P => '--OA++++MA[+PA++++NA]--NA' }
);
$penrose++ xx 4;
my @lines; my @stack;
for $penrose.comb {
state ($x, $y) = 300, 200; state $d = 55 + 0i; when 'A' { @lines.push: 'line' => [:x1($x.round(.01)), :y1($y.round(.01)), :x2(($x += $d.re).round(.01)), :y2(($y += $d.im).round(.01))] } when '[' { @stack.push: ($x.clone, $y.clone, $d.clone) } when ']' { ($x, $y, $d) = @stack.pop } when '+' { $d *= cis -π/5 } when '-' { $d *= cis π/5 } default { }
}
say SVG.serialize(
svg => [ :600width, :400height, :style<stroke:rgb(250,12,210)>, :rect[:width<100%>, :height<100%>, :fill<black>], |@lines, ],
);</lang> See: Penrose tiling image
Scala
Java Swing Interoperability
<lang Scala>import java.awt.{BorderLayout, Color, Dimension, Graphics, Graphics2D, RenderingHints} import java.awt.geom.Path2D
import javax.swing.{JFrame, JPanel}
import scala.math._
object PenroseTiling extends App {
private val (φ, ϑ) = ((1 + sqrt(5)) / 2, toRadians(36)) // golden ratio and 36 degrees private val dist: Array[Array[Double]] = Array(Array(φ, φ, φ), Array(-φ, -1, -φ))
class PenroseTiling extends JPanel { private val (w, h) = (700, 450) private val tiles: Set[Tile] = deflateTiles(setupPrototiles(w, h), 5)
override def paintComponent(og: Graphics): Unit = { def drawTiles(g: Graphics2D): Unit = for (tile <- tiles) { val path: Path2D = new Path2D.Double() val distL = dist(tile.tileType.id)
path.moveTo(tile.x, tile.y) for {i <- 0 until 3 ω = tile.α + (i - 1) * ϑ} path.lineTo( tile.x + distL(i) * tile.size * cos(ω), tile.y - distL(i) * tile.size * sin(ω))
path.closePath() g.setColor(if (tile.tileType == Type.Kite) Color.orange else Color.yellow) g.fill(path) g.setColor(Color.darkGray) g.draw(path) }
super.paintComponent(og) val g: Graphics2D = og.asInstanceOf[Graphics2D] g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON) drawTiles(g) }
private def setupPrototiles(w: Int, h: Int): Set[Tile] = (0 to 5).map(n => Tile(Type.Kite, (w / 2).toDouble, (h / 2).toDouble, Pi / 2 + ϑ + n * 2 * ϑ, w / 2.5)).toSet
@scala.annotation.tailrec private def deflateTiles(tls: Set[Tile], generation: Int): Set[Tile] = if (generation > 0) { val next = for { tile <- tls size = tile.size / φ } yield {
def nx(factor: Int) = tile.x + cos(tile.α - factor * ϑ) * φ * tile.size def ny(factor: Int) = tile.y - sin(tile.α - factor * ϑ) * φ * tile.size
tile.tileType match { case Type.Dart => Seq(Tile(Type.Kite, tile.x, tile.y, tile.α + 5 * ϑ, size)) ++ (for (sign <- -1 to 1 by 2) yield Tile(Type.Dart, nx(sign * 4), ny(sign * 4), tile.α - 4 * ϑ * sign, size))
case Type.Kite => (for (sign <- 1 to -1 by -2) yield { Seq(Tile(Type.Dart, tile.x, tile.y, tile.α - 4 * ϑ * sign, size), Tile(Type.Kite, nx(sign), ny(sign), tile.α + 3 * ϑ * sign, size)) }).flatten } } deflateTiles(next.flatten, generation - 1) } else tls
private case class Tile(tileType: Type.Type, x: Double, y: Double, α: Double, size: Double)
private object Type extends Enumeration { type Type = Value val Kite, Dart = Value }
setPreferredSize(new Dimension(w, h)) setBackground(Color.white) }
new JFrame("Penrose Tiling") { add(new PenroseTiling(), BorderLayout.CENTER) pack() setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE) setLocationRelativeTo(null) setResizable(false) setVisible(true) }
}</lang>
Sidef
Using the LSystem class defined at Hilbert curve. <lang ruby>var rules = Hash(
a => 'cE++dE----bE[-cE----aE]++', b => '+cE--dE[---aE--bE]+', c => '-aE++bE[+++cE++dE]-', d => '--cE++++aE[+dE++++bE]--bE', E => ,
)
var lsys = LSystem(
width: 1000, height: 1000,
scale: 1, xoff: -500, yoff: -500,
len: 40, angle: 36, color: 'dark blue',
)
lsys.execute('[b]++[b]++[b]++[b]++[b]', 5, "penrose_tiling.png", rules)</lang>
Output image: Penrose tiling
Wren
<lang ecmascript>import "graphics" for Canvas, Color import "dome" for Window import "math" for Math import "./dynamic" for Enum, Tuple import "./set" for Set import "./polygon" for Polygon
var Type = Enum.create("Type", ["KITE", "DART"])
var Tile = Tuple.create("Tile", ["type", "x", "y", "angle", "size"])
var DistinctTiles = Fn.new { |tiles|
var tileStr = tiles.map { |t| t.toString }.toList var tileSet = Set.new(tileStr) var tileDst = [] for (tile in tiles) { var str = tile.toString if (tileSet.contains(str)) { tileDst.add(tile) tileSet.remove(str) } } return tileDst
}
var Radians = Fn.new { |d| d * Num.pi / 180 }
var G = (1 + 5.sqrt) / 2 // golden ratio var T = Radians.call(36) // theta
class PenroseTiling {
construct new(width, height) { Window.title = "Penrose Tiling" Window.resize(width, height) Canvas.resize(width, height) _w = width _h = height }
init() { var tiles = deflateTiles_(setupPrototiles_(_w, _h), 5) drawTiles(tiles) }
setupPrototiles_(w, h) { var proto = [] var a = Num.pi / 2 + T while (a < 3 * Num.pi) { proto.add(Tile.new(Type.KITE, w / 2, h / 2, a, w / 2.5)) a = a + 2 * T } return proto }
deflateTiles_(tiles, generation) { if (generation <= 0) return tiles var next = [] for (tile in tiles) { var x = tile.x var y = tile.y var a = tile.angle var nx var ny var size = tile.size / G if (tile.type == Type.DART) { next.add(Tile.new(Type.KITE, x, y, a + 5 * T, size)) var sign = 1 for (i in 0..1) { nx = x + Math.cos(a - 4 * T * sign) * G * tile.size ny = y - Math.sin(a - 4 * T * sign) * G * tile.size next.add(Tile.new(Type.DART, nx, ny, a - 4 * T * sign, size)) sign = -sign } } else { var sign = 1 for (i in 0..1) { next.add(Tile.new(Type.DART, x, y, a - 4 * T * sign, size)) nx = x + Math.cos(a - T * sign) * G * tile.size ny = y - Math.sin(a - T * sign) * G * tile.size next.add(Tile.new(Type.KITE, nx, ny, a + 3 * T * sign, size)) sign = -sign } } } // remove duplicates and deflate return deflateTiles_(DistinctTiles.call(next), generation - 1) }
drawTiles(tiles) { var dist = [ [G, G, G], [-G, -1, -G] ] for (tile in tiles) { var angle = tile.angle - T var x0 = tile.x var y0 = tile.y var ord = tile.type var vertices = x0, y0 for (i in 0..2) { var x1 = tile.x + dist[ord][i] * tile.size * Math.cos(angle) var y1 = tile.y - dist[ord][i] * tile.size * Math.sin(angle) vertices.add([x1, y1]) angle = angle + T x0 = x1 y0 = y1 } var poly = Polygon.quick(vertices) poly.drawfill((ord == 0) ? Color.orange : Color.yellow) poly.draw(Color.darkgray) } }
update() {}
draw(alpha) {}
}
var Game = PenroseTiling.new(700, 450)</lang>