Created
January 1, 2019 13:42
-
-
Save takehiko/e74d23b7b8e8e73c144ee45b2f6bada6 to your computer and use it in GitHub Desktop.
Drawing SVG images of a regular icosahedron with "kuku" forms along the sides
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
#!/usr/bin/env ruby | |
# icosa-drawer.rb : drawing SVG images of a regular icosahedron | |
# with "kuku" forms along the sides | |
# by takehikom | |
module IcosaDrawer | |
class Triangle | |
def initialize(param = {}) | |
@label = param[:label] | |
@x = param[:x] || 0.0 | |
@y = param[:y] || 0.0 | |
@inverted = param[:inv] | |
@side = param[:side] || 100.0 | |
@fontstyle = param[:fontstyle] # "font-size:16px; font-family:Serif; font-style:normal; font-weight:normal; fill:#000000; stroke:none;" | |
@rot0 = param[:rot0] | |
@rot60 = param[:rot60] | |
@rot120 = param[:rot120] | |
@rot180 = param[:rot180] | |
@rot240 = param[:rot240] | |
@rot300 = param[:rot300] | |
end | |
attr_accessor :label, :x, :y, :inverted, :side, :fontstyle | |
attr_accessor :rot0, :rot60, :rot120, :rot180, :rot240, :rot300 | |
def svg_transform(indent_level = 0) | |
' ' * indent_level + | |
'<g transform="translate(%f,%f)%s">' % [@x, @y, @inverted ? " rotate(180)" : ""] + "\n" | |
end | |
def svg_center(indent_level = 0) | |
return "" | |
' ' * indent_level + | |
'<text x="%f" y="%f">%s</text>' % [0, @side * 0.05, @label] + | |
"\n" | |
end | |
def svg_sidetext(rot, indent_level = 0) | |
text = self.send("rot#{rot}") | |
return "" if text.nil? | |
# inverted | |
# rot180 => rotate(360) | |
# rot300 => rotate(240) | |
# rot60 => rotate(120) | |
# !inverted | |
# rot0 => rotate(360) | |
# rot120 => rotate(240) | |
# rot240 => rotate(120) | |
' ' * indent_level + | |
'<g transform="rotate(%d)">' % [inverted ? (180 - rot) : (360 - rot)] + | |
'<text x="%f" y="%f">%s</text>' % [0, @side * 0.24, text] + | |
'</g>' + "\n" | |
end | |
def svg_end_transform(indent_level = 0) | |
' ' * indent_level + | |
'</g>' + "\n" | |
end | |
end | |
class Drawer | |
def initialize(param = {}) | |
@filename_svg = param[:svg] || "icosa.svg" | |
# @mode == 0 : 面と頂点に番号 | |
# @mode == 1 : 同一の辺にはa×bとb×a | |
# @mode == 2 : 同一の辺には同じ積の九九 | |
@mode = (param[:mode] || 1).to_i | |
@margin = 10 | |
@s = @side = 100 # 正三角形の一辺の長さ | |
@s_h = @s / 2.0 | |
@s_r3 = @s.to_f * Math.sqrt(3.0) | |
@s_r3h = @s_r3 / 2.0 # 正三角形の高さ | |
end | |
def start | |
@width = (@s * 6 + @margin * 2).ceil | |
@height = (@s_r3h * 3 + @margin * 2).ceil | |
draw_header | |
draw_expansion | |
setup_surfaces | |
draw_surfaces | |
draw_footer | |
save | |
end | |
def draw_header | |
# SVGはじめ | |
@src = <<EOS | |
<?xml version="1.0" encoding="UTF-8" standalone="no"?> | |
<svg xmlns="http://www.w3.org/2000/svg" | |
width="#{@width}" height="#{@height}" id="icosa#{@mode}"> | |
<style type="text/css" > | |
<![CDATA[ | |
.thinline { fill:none; stroke:black; stroke-width:1; } | |
.thickline { fill:none; stroke:black; stroke-width:2; } | |
text { font-size:16px; font-family:Serif; font-style:normal; font-weight:normal; fill:#000000; stroke:none; text-anchor:middle } | |
]]> | |
</style> | |
<rect x="0" y="0" width="#{@width}" height="#{@height}" fill="white" stroke="none"/> | |
EOS | |
end | |
def draw_expansion | |
# http://polyhedra.cocolog-nifty.com/blog/2017/11/post-dcc9.html | |
# http://polyhedra.cocolog-nifty.com/polyhedra20.pdf | |
# 横線4本 | |
0.upto(3) do |i| | |
x1 = @margin + @s_h * ((i + 1) % 2) | |
y1 = @margin + @s_r3h * i | |
x2 = x1 + @s * (i == 1 ? 6 : 5) | |
y2 = y1 | |
@src += '<path d="M %f,%f L %f,%f" class="thickline"/>' % [x1, y1, x2, y2] | |
@src += "\n" | |
end | |
# 右下がり斜め線5本 | |
-1.upto(4) do |i| | |
x1 = @margin + @s_h + @s * i | |
y1 = @margin | |
x2 = x1 + @s * 1.5 | |
y2 = y1 + @s_r3h * 3 | |
if i == -1 | |
x1 += @s * 0.5 | |
y1 += @s_r3h | |
elsif i == 4 | |
x2 -= @s * 0.5 | |
y2 -= @s_r3h | |
end | |
@src += '<path d="M %f,%f L %f,%f" class="thickline"/>' % [x1, y1, x2, y2] | |
@src += "\n" | |
end | |
# 左下がり斜め線7本 | |
0.upto(6) do |i| | |
x1 = @margin + @s_h + @s * i | |
y1 = @margin | |
x2 = x1 - @s * 1.5 | |
y2 = y1 + @s_r3h * 3 | |
if i == 0 | |
x2 += @s * 0.5 * 2 | |
y2 -= @s_r3h * 2 | |
elsif i == 6 | |
x1 -= @s * 0.5 | |
y1 += @s_r3h | |
end | |
@src += '<path d="M %f,%f L %f,%f" class="thickline"/>' % [x1, y1, x2, y2] | |
@src += "\n" | |
end | |
# のりしろに◎ | |
if @mode == 0 | |
0.upto(4) do |i| | |
x0 = @margin + @s_h + @s * i | |
y0 = @margin + @s_r3h * 2.0 / 3.0 | |
@src += '<g transform="translate(%f,%f)">' % [x0, y0] | |
@src += "\n" | |
@src += '<circle cx="0" cy="0" r="6" class="thinline"/>' + "\n" | |
@src += '<circle cx="0" cy="0" r="9" class="thinline"/>' + "\n" | |
@src += '</g>' | |
@src += "\n" | |
end | |
0.upto(5) do |i| | |
x0 = @margin + @s + @s * i | |
y0 = @margin + @s_r3h * (2.0 + 1.0 / 3.0) | |
if i == 5 | |
x0 -= @s_h | |
y0 -= @s_r3h | |
end | |
@src += '<g transform="translate(%f,%f)">' % [x0, y0] | |
@src += "\n" | |
@src += '<circle cx="0" cy="0" r="6" class="thinline"/>' + "\n" | |
@src += '<circle cx="0" cy="0" r="9" class="thinline"/>' + "\n" | |
@src += '</g>' | |
@src += "\n" | |
end | |
end | |
end | |
def draw_footer | |
# SVGおわり | |
@src += <<EOS | |
</svg> | |
EOS | |
end | |
def save | |
open(@filename_svg, "w") do |f_out| | |
f_out.print @src | |
end | |
end | |
def setup_surfaces | |
@surf = [] | |
# 上段の5つ(逆三角形) | |
0.upto(4) do |i| | |
param = {} | |
param[:label] = i.to_s | |
param[:x] = @margin + @s * (i + 1) | |
param[:y] = @margin + @s_r3h / 3.0 | |
param[:inv] = true | |
param[:side] = @s | |
@surf << IcosaDrawer::Triangle.new(param) | |
end | |
# 中段の5つ(逆三角形) | |
0.upto(4) do |i| | |
param = {} | |
param[:label] = (5 + i).to_s | |
param[:x] = @margin + @s_h + @s * i | |
param[:y] = @margin + @s_r3h + @s_r3h / 3.0 | |
param[:inv] = true | |
param[:side] = @s | |
@surf << IcosaDrawer::Triangle.new(param) | |
end | |
# 中段の5つ(通常の向きの三角形) | |
0.upto(4) do |i| | |
param = {} | |
param[:label] = (10 + i).to_s | |
param[:x] = @margin + @s * (i + 1) | |
param[:y] = @margin + @s_r3h + @s_r3h * 2.0 / 3.0 | |
param[:side] = @s | |
@surf << IcosaDrawer::Triangle.new(param) | |
end | |
# 下段の5つ(通常の向きの三角形) | |
0.upto(4) do |i| | |
param = {} | |
param[:label] = (15 + i).to_s | |
param[:x] = @margin + @s_h + @s * i | |
param[:y] = @margin + @s_r3h * 2 + @s_r3h * 2.0 / 3.0 | |
param[:side] = @s | |
@surf << IcosaDrawer::Triangle.new(param) | |
end | |
# 正二十面体で同一になる辺の対応付け | |
case @mode | |
when 1 | |
match_edge_mode1 | |
when 2 | |
match_edge_mode2 | |
else | |
match_edge_mode0 | |
end | |
end | |
def match_edge_mode0 | |
# 正二十面体で同一になる辺の対応付け | |
# 同一の辺にはカッコ書きで同じ番号を記載 | |
0.upto(4) do |i| | |
j = (i + 1) % 5 | |
@surf[i].rot60 = @surf[5 + j].rot180 = "(#{i + 1})" | |
@surf[i].rot180 = @surf[j].rot300 = "(#{i + 6})" | |
@surf[5 + i].rot60 = @surf[10 + i].rot240 = "(#{i + 11})" | |
@surf[10 + i].rot120 = @surf[5 + j].rot300 = "(#{i + 16})" | |
@surf[10 + i].rot0 = @surf[15 + j].rot240 = "(#{i + 21})" | |
@surf[15 + i].rot120 = @surf[15 + j].rot0 = "(#{i + 26})" | |
end | |
end | |
def match_edge_mode1 | |
# 正二十面体で同一になる辺の対応付け | |
# 同一の辺にはa×bとb×a | |
srand(9981) | |
kuku = (2..8).map {|p| ((p + 1)..9).map {|q| [p, q]}}.inject(:+) | |
kuku << [1, 8] << [1, 9] | |
kuku.shuffle! | |
puts "kuku = #{kuku.inspect}" if $DEBUG | |
0.upto(4) do |i| | |
j = (i + 1) % 5 | |
@surf[i].rot60, @surf[5 + j].rot180 = kuku_forms(kuku[i]) | |
@surf[i].rot180, @surf[j].rot300 = kuku_forms(kuku[i + 5]) | |
@surf[5 + i].rot60, @surf[10 + i].rot240 = kuku_forms(kuku[i + 10]) | |
@surf[10 + i].rot120, @surf[5 + j].rot300 = kuku_forms(kuku[i + 15]) | |
@surf[10 + i].rot0, @surf[15 + j].rot240 = kuku_forms(kuku[i + 20]) | |
@surf[15 + i].rot120, @surf[15 + j].rot0 = kuku_forms(kuku[i + 25]) | |
end | |
end | |
def kuku_forms(param) | |
a = ["#{param[0]}×#{param[1]}", "#{param[1]}×#{param[0]}"] | |
if rand(2) == 1 | |
a.reverse! | |
end | |
a | |
end | |
def match_edge_mode2 | |
# 正二十面体で同一になる辺の対応付け | |
# 同一の辺には同じ積の九九 | |
srand(9981) | |
kuku = (1..9).map {|p| (p == 1 ? [4, 6, 8, 9] : (1..9)).map {|q| [p, q]}}.inject(:+) | |
# kuku = (1..9).map {|p| (1..9).map {|q| [p, q]}}.inject(:+) | |
product = {} | |
puts "kuku = #{kuku.inspect}" if $DEBUG | |
kuku.each do |p, q| | |
r = p * q | |
s = "%d×%d" % [p, q] | |
if product.key?(r) | |
product[r] << s | |
else | |
product[r] = [s] | |
end | |
end | |
kuku_a = [] | |
product.each_key do |r| | |
a = product[r].dup | |
while a.length >= 2 | |
a.shuffle! | |
s1 = a.pop | |
s2 = a.pop | |
kuku_a << [s1, s2] | |
end | |
end | |
kuku_a.shuffle! | |
kuku_a = kuku_a[0, 30] | |
puts "kuku_a = #{kuku_a.inspect}" if $DEBUG | |
0.upto(4) do |i| | |
j = (i + 1) % 5 | |
@surf[i].rot60, @surf[5 + j].rot180 = kuku_a[i] | |
@surf[i].rot180, @surf[j].rot300 = kuku_a[i + 5] | |
@surf[5 + i].rot60, @surf[10 + i].rot240 = kuku_a[i + 10] | |
@surf[10 + i].rot120, @surf[5 + j].rot300 = kuku_a[i + 15] | |
@surf[10 + i].rot0, @surf[15 + j].rot240 = kuku_a[i + 20] | |
@surf[15 + i].rot120, @surf[15 + j].rot0 = kuku_a[i + 25] | |
end | |
end | |
def draw_surfaces | |
@surf.each_with_index do |t, i| | |
@src += t.svg_transform | |
@src += t.svg_center | |
if t.inverted | |
@src += t.svg_sidetext(180) | |
@src += t.svg_sidetext(300) | |
@src += t.svg_sidetext(60) | |
else | |
@src += t.svg_sidetext(0) | |
@src += t.svg_sidetext(120) | |
@src += t.svg_sidetext(240) | |
end | |
@src += t.svg_end_transform | |
end | |
end | |
end | |
end | |
if __FILE__ == $0 | |
puts "mode 0" | |
IcosaDrawer::Drawer.new(svg: "icosa-test.svg", mode: 0).start | |
puts "mode 1" | |
IcosaDrawer::Drawer.new(svg: "icosa1.svg", mode: 1).start | |
puts "mode 2" | |
IcosaDrawer::Drawer.new(svg: "icosa2.svg", mode: 2).start | |
# IcosaDrawer::Drawer.new.start | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment