Penrose tiling

From Rosetta Code
Penrose tiling is a draft programming task. It is not yet considered ready to be promoted as a complete task, for reasons that should be found in its talk page.

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



Go

Library: Go Graphics
Translation of: Java

<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

Works with: Java version 8

<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>

Kotlin

Translation of: Java

<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>

Perl 6

Works with: Rakudo version 2018.05

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

Phix

Translation of the original Python code <lang Phix>-- 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

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*/, integer /*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 esc_close(Ihandle /*ih*/, atom c)

   if c=K_ESC then return IUP_CLOSE end if
   if c=' ' then
       triangles = subdivide(triangles)
       IupUpdate(canvas)
   elsif upper(c)='C' then
       yellow_orange = not yellow_orange
       IupUpdate(canvas)
   end if
   return IUP_CONTINUE

end function

procedure main()

   IupOpen()
   
   canvas = IupCanvas(NULL)
   IupSetAttribute(canvas, "RASTERSIZE", "600x600") -- initial size
   IupSetCallback(canvas, "MAP_CB", Icallback("map_cb"))
   dlg = IupDialog(canvas)
   IupSetAttribute(dlg, "TITLE", "Penrose tiling")
   IupSetCallback(dlg, "K_ANY",     Icallback("esc_close"))
   IupSetCallback(canvas, "ACTION", Icallback("redraw_cb"))
   IupMap(dlg)
   IupSetAttribute(canvas, "RASTERSIZE", NULL) -- release the minimum limitation
   IupShowXY(dlg,IUP_CENTER,IUP_CENTER)
   IupMainLoop()
   IupClose()

end procedure main()</lang> Output can be toggled to look like the java or perl output