272 lines
7.4 KiB
Python
Executable File
272 lines
7.4 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
# Copyright (c) 2014 Ludger Sandig
|
|
# This file is part of apollon.
|
|
|
|
# Apollon is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
|
|
# Apollon is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with Apollon. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
import cgi
|
|
import cgitb
|
|
|
|
from apollon import ApollonianGasket
|
|
from coloring import ColorScheme, ColorMap
|
|
from ag import ag_to_svg, impossible_combination
|
|
|
|
|
|
|
|
# Container for sanitized settings
|
|
class Settings(object):
|
|
def __init__(self, form):
|
|
self.color = form.getvalue("color", "Blues")
|
|
self.resolution = int(form.getvalue("res", 8))
|
|
self.c1 = float(form.getvalue("c1", 1))
|
|
self.c2 = float(form.getvalue("c2", 2))
|
|
self.c3 = float(form.getvalue("c3", 2))
|
|
self.rad_or_curv = form.getvalue("roc", "curvature")
|
|
self.depth = int(form.getvalue("depth", 3))
|
|
|
|
# Sanity check
|
|
|
|
if (self.resolution > 8) or (self.resolution < 3):
|
|
self.resolution = 8
|
|
|
|
if self.c1 <= 0: self.c1 = 1
|
|
if self.c2 <= 0: self.c2 = 1
|
|
if self.c3 <= 0: self.c3 = 1
|
|
|
|
if not (self.rad_or_curv == "curvature" or self.rad_or_curv == "radius"):
|
|
self.rad_or_curvature = "curvature"
|
|
|
|
if (self.depth < 0) or (self.depth > 5):
|
|
self.depth = 3
|
|
|
|
# For a more usable form
|
|
if self.rad_or_curv == "curvature":
|
|
self.rad_checked = ""
|
|
self.curv_checked = "checked"
|
|
else:
|
|
self.rad_checked = "checked"
|
|
self.curv_checked = ""
|
|
|
|
# Curvature or Radius?
|
|
if self.rad_or_curv == "radius":
|
|
self.c1 = 1/self.c1
|
|
self.c2 = 1/self.c2
|
|
self.c3 = 1/self.c3
|
|
|
|
# AG possible in the first place?
|
|
self.impossible = impossible_combination(self.c1, self.c2, self.c3)
|
|
|
|
# What to print: With form or only svg
|
|
action = form.getvalue("submit","Update")
|
|
self.onlysvg = False
|
|
if action == "Save":
|
|
self.onlysvg = True
|
|
|
|
|
|
def print_with_form(svg, settings, schemes):
|
|
# Print first chunk of html
|
|
print("Content-type: text/html")
|
|
|
|
print("""
|
|
<html>
|
|
|
|
<head>
|
|
<title>Apollonian Gasket Generator</title>
|
|
|
|
<style>
|
|
|
|
body {
|
|
font-family: sans-serif;
|
|
width: 800px;
|
|
margin: auto;
|
|
}
|
|
|
|
h1 {
|
|
padding: 10px;
|
|
border: 2px solid #ccc;
|
|
border-radius: 25px;
|
|
text-align: center;
|
|
width: 780px;
|
|
}
|
|
|
|
#gasket {
|
|
width: 500px;
|
|
float: left;
|
|
padding: 10px;
|
|
border: 2px solid #ccc;
|
|
border-radius: 25px;
|
|
}
|
|
|
|
#settings {
|
|
width: 250px;
|
|
float: right;
|
|
border: 2px solid #ccc;
|
|
border-radius: 25px;
|
|
padding: 10px;
|
|
}
|
|
|
|
#about {
|
|
width: 780px;
|
|
border: 2px solid #ccc;
|
|
border-radius: 25px;
|
|
padding: 10px;
|
|
clear: both;
|
|
}
|
|
|
|
div.wrapper {
|
|
padding-top: 2px;
|
|
clear: both;
|
|
}
|
|
|
|
input[type=number] {
|
|
width: 75px;
|
|
|
|
}
|
|
|
|
input, label, select {
|
|
display: table-cell;
|
|
}
|
|
|
|
form {
|
|
display: table;
|
|
width: inherit;
|
|
}
|
|
|
|
#settings p { display: table-row; }
|
|
|
|
</style>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<h1>Apollonian Gasket Generator</h1>
|
|
""")
|
|
|
|
print('<div id="gasket">\n%s\n</div>' % svg)
|
|
|
|
# Print form
|
|
print("""
|
|
<div id="settings">
|
|
<form method="post" action="index.cgi" id="params">
|
|
|
|
<p><label for="c1">First circle</label><input type="number" name="c1" id="c1" step="any" min="0" value="%.2f"/></p>
|
|
<p><label for="c2">Second circle</label> <input type="number" name="c2" id="c2" step="any" min="0" value="%.2f"/></p>
|
|
<p><label for="c3">Third circle</label><input type="number" name="c3" id="c3" step="any" min="0" value="%.2f"/></p>
|
|
<p><label for="curv">Curvature</label><input type="radio" name="roc" id="curv" value="curvature" %s/></p>
|
|
<p><label for="rad">Radius</label><input type="radio" name="roc" id="rad" value="radius" %s/></p>
|
|
<p><label for="depth">Recursion depth</label> <input type="number" name="depth" value="%d" min="0" max="5"/></p>
|
|
<p><label for="num">No. of colors</label><input type="number" name="res" value="%d" min="3" max="8"/> </p>
|
|
<p><label>Color scheme</label><select name="color">
|
|
""" % (settings.c1, settings.c2, settings.c3, settings.curv_checked, settings.rad_checked, settings.depth, settings.resolution))
|
|
|
|
# Sort color scheme names
|
|
info = schemes.info()
|
|
info.sort(key=lambda d: d["name"])
|
|
info.insert(0, {"name" : "none"})
|
|
for s in info:
|
|
if s["name"] == settings.color:
|
|
selected = "selected"
|
|
else:
|
|
selected = ""
|
|
print('<option value="%s" %s>%s</value>' % (s["name"], selected, s["name"]))
|
|
|
|
# Print last chunk of form and help text
|
|
print("""
|
|
</select></p>
|
|
<p><input type="submit" name="submit" value="Update"> <input type="submit" name="submit" value="Save"></p>
|
|
</form>
|
|
</div>
|
|
<div class="wrapper">
|
|
<div id="about">
|
|
<h2>About</h2>
|
|
|
|
<p> Apollonian Gaskets are groups of circles in which three are
|
|
mutally tangent to each other. You can think of it as tightly filling
|
|
a big circle with lots of smaller cicles. These sets of circles can be
|
|
computed recusively with relative ease. If you are interested in the
|
|
mathematical part, see <a
|
|
href="https://en.wikipedia.org/wiki/Apollonian_gasket">Wikipedia on
|
|
this subject</a>. </p>
|
|
|
|
<p>This site showcases a small command line program I wrote to
|
|
generate svg images of those circles. It can be found on <a
|
|
href="https://github.com/lsandig/apollon">github</a>.</p>
|
|
|
|
<p>For a more in-depth explanation of this implementation see <a
|
|
href="http://lsandig.org/blog/2014/08/apollon-python/en/">this
|
|
post</a> on my blog (English and German version)</p>
|
|
|
|
<p>Please note that the online version has a recursion limit of depth
|
|
5 to reduce the time and memory consumption on the server. With the
|
|
command line version only your RAM is the limit.</p>
|
|
|
|
<p>This is free software published under the GPLv3.</p>
|
|
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
""")
|
|
|
|
|
|
def print_only_image(svg):
|
|
print('Content-type: image/svg+xml\nContent-Disposition: attachment; filename="apollonian_gasket.svg"\n\n')
|
|
print(svg)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# Debugging
|
|
#cgitb.enable()
|
|
|
|
# Get settings from form
|
|
form = cgi.FieldStorage()
|
|
param = Settings(form)
|
|
|
|
# Construct color map
|
|
schemes = ColorScheme("colorbrewer.json")
|
|
|
|
if not param.impossible:
|
|
# Magic
|
|
ag = ApollonianGasket(param.c1, param.c2, param.c3)
|
|
|
|
ag.generate(param.depth)
|
|
|
|
# Get smallest and biggest radius
|
|
smallest = abs(min(ag.genCircles, key=lambda c: abs(c.r.real)).r.real)
|
|
biggest = abs(max(ag.genCircles, key=lambda c: abs(c.r.real)).r.real)
|
|
|
|
|
|
if param.color == 'none':
|
|
mp = ColorMap('none')
|
|
else:
|
|
mp = schemes.makeMap(smallest, biggest, param.color, param.resolution)
|
|
|
|
# Convert to svg
|
|
svg = ag_to_svg(ag.genCircles, mp, tresh=0.005)
|
|
|
|
# Output
|
|
if param.onlysvg:
|
|
print_only_image(svg)
|
|
else:
|
|
print_with_form(svg, param, schemes)
|
|
else:
|
|
errortext = "<h2>No Apollonian gasket possible for these curvatures :(</2>"
|
|
print_with_form(errortext, param, schemes)
|
|
|
|
|
|
|
|
|