Created
January 14, 2021 04:25
-
-
Save waterskier2007/4584bdd4c0b2f3f90e765b9a50747e48 to your computer and use it in GitHub Desktop.
A swift implementation of Penrose tiling
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//MARK: - Playground code | |
//: A UIKit based Playground for presenting user interface | |
import UIKit | |
import PlaygroundSupport | |
class MyViewController : UIViewController { | |
override func loadView() { | |
let view = PenroseView(frame: CGRect(x: 0, y: 0, width: 1000, height: 800)) | |
view.backgroundColor = .white | |
self.view = view | |
} | |
} | |
class PenroseView: UIView { | |
let penrose: PenroseTiling | |
override init(frame: CGRect) { | |
self.penrose = PenroseTiling(width: frame.width, height: frame.height) | |
super.init(frame: frame) | |
} | |
required init?(coder: NSCoder) { | |
fatalError("init(coder:) has not been implemented") | |
} | |
override func draw(_ rect: CGRect) { | |
guard let graphics = UIGraphicsGetCurrentContext() else { | |
return | |
} | |
graphics.setStrokeColor(UIColor.darkGray.cgColor) | |
let dist: [[CGFloat]] = [ | |
[PenroseTiling.goldenRatio, PenroseTiling.goldenRatio, PenroseTiling.goldenRatio], | |
[-1.0 * PenroseTiling.goldenRatio, -1, -1.0 * PenroseTiling.goldenRatio] | |
] | |
for tile in penrose.tiles { | |
var angle = tile.angle - PenroseTiling.theta | |
let path = CGMutablePath() | |
path.move(to: CGPoint(x: tile.x, y: tile.y)) | |
let ord = tile.type.rawValue | |
for i in stride(from: 0, to: 3, by: 1) { | |
let x = tile.x + dist[ord][i] * tile.size * cos(angle) | |
let y = tile.y - dist[ord][i] * tile.size * sin(angle) | |
path.addLine(to: CGPoint(x: x, y: y)) | |
angle += PenroseTiling.theta | |
} | |
path.closeSubpath() | |
graphics.addPath(path) | |
graphics.setFillColor(ord == 0 ? UIColor.orange.cgColor : UIColor.yellow.cgColor) | |
graphics.drawPath(using: .fillStroke) | |
} | |
} | |
} | |
// Present the view controller in the Live View window | |
PlaygroundPage.current.liveView = MyViewController() | |
//MARK: - Penrose code | |
import Foundation | |
import CoreGraphics | |
public struct PenroseTiling { | |
public static let goldenRatio: CGFloat = (1.0 + sqrt(5.0)) / 2.0 | |
public static let theta: CGFloat = 36.degreesToRadians | |
public let tiles: [Tile] | |
public init(width: CGFloat, height: CGFloat) { | |
tiles = PenroseTiling.deflateTiles(tiles: PenroseTiling.setupPrototiles(width: width, height: height), generation: 5) | |
} | |
private static func setupPrototiles(width: CGFloat, height: CGFloat) -> [Tile] { | |
var proto = [Tile]() | |
for i in stride(from: CGFloat.pi / 2.0 + theta, to: CGFloat.pi * 3.0, by: 2.0 * theta) { | |
proto.append(Tile(t: .kite, x: width / 2.0, y: height / 2.0, a: i, s: width / 2.5)) | |
} | |
return proto | |
} | |
private static func deflateTiles(tiles: [Tile], generation: Int) -> [Tile] { | |
guard generation > 0 else { | |
return tiles | |
} | |
var next = [Tile]() | |
for tile in tiles { | |
let x = tile.x | |
let y = tile.y | |
let a = tile.angle | |
let size = tile.size / goldenRatio | |
switch tile.type { | |
case .dart: | |
next.append(Tile(t: .kite, x: x, y: y, a: a + 5.0 * theta, s: size)) | |
var sign: CGFloat = 1.0 | |
for _ in stride(from: 0, to: 2, by: 1) { | |
let nx = x + cos(a - 4.0 * theta * sign) * goldenRatio * tile.size | |
let ny = y - sin(a - 4.0 * theta * sign) * goldenRatio * tile.size | |
next.append(Tile(t: .dart, x: nx, y: ny, a: a - 4 * theta * sign, s: size)) | |
sign *= -1 | |
} | |
case .kite: | |
var sign: CGFloat = 1.0 | |
for _ in stride(from: 0, to: 2, by: 1) { | |
next.append(Tile(t: .dart, x: x, y: y, a: a - 4.0 * theta * sign, s: size)) | |
let nx = x + cos(a - theta * sign) * goldenRatio * tile.size | |
let ny = y - sin(a - theta * sign) * goldenRatio * tile.size | |
next.append(Tile(t: .kite, x: nx, y: ny, a: a + 3 * theta * sign, s: size)) | |
sign *= -1 | |
} | |
} | |
} | |
//remove duplicates | |
let dedup = next.distinct() | |
return deflateTiles(tiles: dedup, generation: generation - 1) | |
} | |
} | |
public struct Tile: Equatable { | |
public enum TileType: Int { | |
case kite | |
case dart | |
} | |
public let x, y, angle, size: CGFloat | |
public let type: TileType | |
init(t: TileType, x: CGFloat, y: CGFloat, a: CGFloat, s: CGFloat) { | |
self.type = t | |
self.x = x | |
self.y = y | |
self.angle = a | |
self.size = s | |
} | |
public static func ==(lhs: Tile, rhs: Tile) -> Bool { | |
return lhs.type == rhs.type | |
&& lhs.x == rhs.x | |
&& lhs.y == rhs.y | |
&& lhs.angle == rhs.angle | |
} | |
} | |
extension BinaryInteger { | |
var degreesToRadians: CGFloat { CGFloat(self) * .pi / 180 } | |
} | |
extension FloatingPoint { | |
var degreesToRadians: Self { self * .pi / 180 } | |
var radiansToDegrees: Self { self * 180 / .pi } | |
} | |
extension Array where Element: Equatable { | |
func distinct() -> Self { | |
var unique = [Element]() | |
for item in self | |
{ | |
if !unique.contains(item) | |
{ | |
unique.append(item) | |
} | |
} | |
return unique | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Nice work!
It is not clear why you are using
stride(from: 0, to: 2, by: 1)
instead of just computing the fixed number of tiles (the Python implementation here is much more simple).Is is also weird that you need to deduplicate, this should not be necessary. If it is required you can use a far more efficient implementation (make Tile conform to Hashable):
If the order of Tiles does not matter you can even replace the array of Tiles with a Set of Tiles in you code for faster speed.
Also, using complex number would clarify a lot the code, you can use the library swift-numerics for this (official implementation approved by Swift Core Dev Team).