Aperiodic monotiles

The spectre aperiodic monotile, with "odd" tiles shaded.
2023 was a good year for tiling! The Einstein hat and Sprectre aperiodic monotiles were discovered and documented. These are special because they've been proven to never repeat. There's something really quite wonderful about that!
I've designed 3D-printed cookie cutters that could make ceramic tiles with these, but I've not reached out to my local potters to ask for some time to make them โ and I haven't figured out the 'rules' for placing a tile next to others, some placings definitely end up making tilings that can't be completed.
Spectre + Tantrix
I wrote a little code a few weekends ago to try and figure out if I could make a Spectre tiling that also had lines that travelled across the tiles โ something like a tiling game I used to play called Tantrix.
Though there are some lines that close (you can see some here) I have a hunch that there may be provably non-zero many that never close. I have no idea how to go about proving that though!
My spectre & tantrix crossover tiling, each one with a colour chosen for its angle of rotation.
The code for making this is below; but it's definitely not polished!
Code for creating Spectre + Tantrix crossover
require 'victor'
include Victor
class Spectre
attr_reader :points
# Angle 1 is the angle between side 1 and side 2 etc.
ANGLES = [90, 60, -90, 60, 0, 60, 90, -60, 90, -60, 90, 60, -90, 60].freeze
SIDE_LENGTH = 25
LINE_WIDTH = 2.5
CONTROL_LEN_MULT = 0.4
SHOW_NAME = false
ARCS = [
[7, 9],
[3, 11],
[0, 13],
[2, 8],
[5, 10],
[4, 6],
[1, 12],
]
def initialize(origin, corner_index, rotation, name: nil, show_corner_indeces: false)
@show_corner_indeces = show_corner_indeces
@hue = ANGLES[corner_index..].sum(-rotation) % 360
@indexes = (0...ANGLES.length).cycle.take(corner_index + ANGLES.length)[corner_index..]
@name = name || (@@i ||= 1).to_s
previous_angle = -rotation
@points = @indexes.each_with_object([[origin[0], origin[1]]]) do |idx, acc|
previous_angle += ANGLES[idx]
ang = previous_angle / 180.0 * Math::PI
acc.push([
acc.last[0] + SIDE_LENGTH*Math.sin(ang),
acc.last[1] + SIDE_LENGTH*Math.cos(ang)
])
end
@background = "hsl(#{@hue}, 100%, 85%)"
# @background = "hsl(#{(@hue / 90).floor * 90}, 100%, 85%)"
# @background = "hsl(23, 100%, 72%)"
@@i += 1
end
def point(idx) = @points[@indexes.index(idx)]
def between(idx1, idx2) = [
point(idx1)[0] + (point(idx2)[0] - point(idx1)[0])/2,
point(idx1)[1] + (point(idx2)[1] - point(idx1)[1])/2
]
def normal_point(idx1, idx2, n_dist)
b = between(idx1, idx2)
p1 = point(idx1)
p2 = point(idx2)
dx = p2[0] - p1[0]
dy = p2[1] - p1[1]
b_ang = Math.atan2(dy, dx)
b_dist = Math.sqrt(dx*dx + dy*dy) / 2
n_ang = Math.atan2(n_dist, b_dist)
t_ang = b_ang - n_ang
t_dist = Math.sqrt(n_dist*n_dist + b_dist*b_dist)
n_x = Math.cos(t_ang) * t_dist
n_y = Math.sin(t_ang) * t_dist
[p1[0] + n_x, p1[1] + n_y]
end
def svg
SVG.new.tap do |svg|
svg.polygon points: @points, stroke: 'none', fill: @background
ARCS.each do |(i1, i2)|
i1next = (i1 + 1) % ANGLES.length
i2next = (i2 + 1) % ANGLES.length
p1 = between(i1, i1next)
p2 = between(i2, i2next)
dx = (p2[0] - p1[0])
dy = (p2[1] - p1[1])
mid_dist = Math.sqrt(dx*dx + dy*dy)
c1 = normal_point(i1, i1next, mid_dist*CONTROL_LEN_MULT)
c2 = normal_point(i2, i2next, mid_dist*CONTROL_LEN_MULT)
show_control_points = false
if show_control_points
svg.circle cx: p1[0], cy: p1[1], r: 2, fill: 'green'
svg.line x1: p1[0], y1: p1[1], x2: c1[0], y2: c1[1], stroke: 'green'
svg.circle cx: c1[0], cy: c1[1], r: 2, fill: 'green'
svg.circle cx: c2[0], cy: c2[1], r: 2, fill: 'green'
svg.line x1: p2[0], y1: p2[1], x2: c2[0], y2: c2[1], stroke: 'green'
svg.circle cx: p2[0], cy: p2[1], r: 2, fill: 'green'
end
svg.path d: "M#{p1[0]},#{p1[1]}C#{c1[0]} #{c1[1]},#{c2[0]} #{c2[1]},#{p2[0]} #{p2[1]}", stroke: @background, stroke_width: LINE_WIDTH*2, fill: 'none'
svg.path d: "M#{p1[0]},#{p1[1]}C#{c1[0]} #{c1[1]},#{c2[0]} #{c2[1]},#{p2[0]} #{p2[1]}", stroke: 'white', stroke_width: LINE_WIDTH, fill: 'none'
end
centre = between(2, 7)
if SHOW_NAME
svg.text(
@name,
x: centre[0],
y: centre[1],
font_family: 'arial',
font_size: 16,
text_anchor: "middle",
dominant_baseline: "middle"
)
end
svg.polygon points: @points, stroke: 'black', fill: 'none'
if @show_corner_indeces
@points[0..-2].each.with_index do |p, idx|
svg.circle cx: p[0], cy:p[1], r: 5, fill: "hsla(#{@hue}, 100%, 95%, 85%)"
svg.text(
@indexes[idx],
x: p[0],
y: p[1],
font_family: 'arial',
font_size: 6,
text_anchor: "middle",
dominant_baseline: "middle"
)
end
end
end
end
def into(svg)
svg << self.svg
self
end
end
svg = Victor::SVG.new width: 475, height: 400
s1 = Spectre.new([100, 100], 0, 0).into(svg)
s2 = Spectre.new(s1.point(2), 8, 120).into(svg)
s3 = Spectre.new(s1.point(4), 8, 60).into(svg)
s4 = Spectre.new(s3.point(2), 10, 30).into(svg)
s5 = Spectre.new(s3.point(10), 8, 120).into(svg)
s6 = Spectre.new(s3.point(0), 2, 0).into(svg)
s7 = Spectre.new(s5.point(2), 0, 90).into(svg)
s8 = Spectre.new(s7.point(2), 10, 210).into(svg)
s9 = Spectre.new(s5.point(12), 0, -150).into(svg)
s10 = Spectre.new(s2.point(12), 6, -150).into(svg)
s11 = Spectre.new(s6.point(9), 3, 90).into(svg)
s12 = Spectre.new(s6.point(12), 0, 30).into(svg)
s13 = Spectre.new(s11.point(7), 3, 120).into(svg)
s14 = Spectre.new(s11.point(12), 6, 30).into(svg)
s15 = Spectre.new(s13.point(0), 6, 90).into(svg)
s16 = Spectre.new(s8.point(2), 0, 210).into(svg)
s17 = Spectre.new(s16.point(12), 6, 90).into(svg)
s18 = Spectre.new(s17.point(2), 0, 30).into(svg)
s19 = Spectre.new(s7.point(6), 0, 90).into(svg)
s20 = Spectre.new(s13.point(10), 10, 120).into(svg)
s21 = Spectre.new(s4.point(2), 10, 30).into(svg)
s22 = Spectre.new(s15.point(4), 8, 60).into(svg)
svg.save 'spectre'
Mosaics
I've been toying with the idea of creating ceramic tiles with glazes specially chosen/mixed to have levels of reflectivity to sodium light at ฮป = ~589 nm that are different to their white/solar light grayscale mapping.
This would allow me to make a day/night split mosaic; in the day the full-colour mosaic would be of one picture, but by night the street lights' singular frequency lamps (if I can find anywhere that still uses sodium lamps!) would create a different grayscale image that would jump out!