I am planning on adding an image that attempts to generate a cycling line width when drawing the spirograph curve. It was something I saw on the Rotae site and I liked the look. So I decided to try and recreate it.
But definitely harder than it looked. Almost certainly Nadieh Bremer has better drawing/imaging tools than me. And definitely more knowledge and experience with image creation. That said I did try a few different approaches using geometry to try and get the job done. Finally I settled on a completely different approach. Instead of plotting lines, I used scatter plots altering the size of the marker as I drew the curve. That actually seemed to work quite well. (And it just occurred to me that I might also be able get the job done using the colour between concept. May have to give that a try.)
But, before I add that to the web app, I will add that extra field I mentioned in the last post.
New Field, Some Minor Code Refactoring
As I said at the time:
But, I looked back at my earlier work on this image type, and realized some of the images I liked best had overlapping transform pairs that were not drawn to the images. In fact some of them only had half the possible pairings drawn to the image.
New Field
So, a new field in the 2D Transforms section of the form.
<div>
<p style="margin-left:1rem;margin-top:0;"><b>Allow overlapping transforms for T2T images: </p>
<p>
<label for="tolno">No: </label>
<input type="radio" name="do_drpt" id="tolno" value="false" />
<label for="tolyes">Yes: </label>
<input type="radio" name="do_drpt" id="tolyes" value="true" />
</p>
</div>
Refactor From Processing Functions
Added some more global variables needed by the functions and the routing code.
u_dt2t = None
... ...
drp_tr = False # allow dropping transform pairs in t2t images
In fini_curve_form()
all I had to do was add the appropriate entries to the two dictionairies.
dflts = {
... ...,
'do_drpt': 'false'
}
fld_2_glbl = {
... ...,
'do_drpt': g.u_dt2t
}
Not much more work in proc_curve_form()
. Added the following at the bottom of the if i_typ == 'mosaic':
block.
if 'do_drpt' in f_data:
g.drp_tr = True if f_data['do_drpt'] == 'true' else False
Refactor Route Code
I thought all I’d have to do is put the overlap check/fix block in a suitable if
block. But after doing that I got images without curves when dropping transform pairs was permited. So, I added the function lists_equal()
to the library module just before lists_overlap()
. (I am sure you know how to write that one.)
Then a little reworking of that section of code did the rest. A bit more code than I was expecting, but nothing demanding.
# get rand quadrant orders
do_qd = sal.get_rnd_qds(list(range(g.nbr_tr)))
do_qd2 = sal.get_rnd_qds(do_qd)
if not g.drp_tr:
chk_fn = sal.lists_overlap
else:
chk_fn = sal.lists_equal
qlcnt = 0
while chk_fn(do_qd, do_qd2) and qlcnt < 5:
do_qd2 = sal.get_rnd_qds(do_qd)
qlcnt += 1
I won’t bother with any example images. Last post had examples of both situations.
favicon.ico
I kept getting seeing 127.0.0.1 - - [23/Nov/2022 12:51:17] "GET /favicon.ico HTTP/1.1" 404 -
in the output in the terminal running my app. And had also noticed a similar error in the App Engine dashboard. So I used favicon.io to create an favicon image. Then added a suitable line in the head
of the base page template.
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
Also getting one for background image; but, haven’t yet decided what to do about that. Though have an idea.
Images with Cycling Line Width
Okay let’s add that new image type. Needless to say another function based on my previous work added to the library module: cycle_lw()
. It takes the plot axes and the datasets as its parameters.
New Route
Now that I have the image drawing code from my previous work in the library module, let’s start with the new route, /spirograph/basic_clw
. Again, a fair bit of duplication.
You might notice the addition of 'marker': 'o'
to the f_data
dictionary. I don’t currently use this in the form page template. In my earlier code, I allowed for different marker shapes when generating the image. I may do that for this image type down the road. So, I am jumping the gun with that addition. No refactoring of the form processing function is required at this time.
Also, while trying to get things working, I decided I didn’t like the images with only 1024 data points being used. So, for now, I am hardcoding a jump to 2048 datapoints for this image type. Which of course involves updating a global or two.
And, I am showing the information I am getting back from the image drawing function on the image display page. So, some refactoring of that template was needed.
@app.route('/spirograph/basic_clw', methods=['GET', 'POST'])
def sp_basic_clw():
pg_ttl = "Basic Spirograph Image with Cycling Line Width"
if request.method == 'GET':
f_data = {'pttl': pg_ttl, 'shapes': splt.shp_nm, 'cxsts': g.n_whl is not None,
'cmaps': g.clr_opt, 'marker': 'o'
}
return render_template('get_curve.html', type='basic', f_data=f_data)
elif request.method == 'POST':
g.t_pts = 2048
g.rds = np.linspace(0, 2*np.pi, g.t_pts)
sal.proc_curve_form(request.form, 'basic')
t_xs, t_ys, t_shp = sal.init_curve()
ax.cla()
g.rcm, g.cycle = sal.set_clr_map(ax, g.lc_frq, g.rcm)
ax.autoscale()
m1, m2, mx_sz, t_fst, p_step, hf_frq = sal.cycle_lw(ax, t_xs, t_ys)
ax.autoscale()
ax.text(0.5, 0.01, "© koach (barkncats) 2022", ha="center", transform=ax.transAxes,
fontsize=10, alpha=.3, c=g.cycle[len(g.cycle)//2])
ax.autoscale()
# set number of datapoints back to default in case another image type is selected
g.t_pts = 1024
g.rds = np.linspace(0, 2*np.pi, g.t_pts)
bax2, _ = sal.set_bg(ax)
# convert image to base64 for embedding in html page
data = sal.fig_2_base64(fig)
c_data = sal.get_curve_dtl()
i_data = sal.get_image_dtl(pg_ttl)
p_data = {'m1': m1, 'm2': m2, 'mx_sz': mx_sz, 't_fst': t_fst, 'p_step': p_step, 'hf_frq': hf_frq}
return render_template('disp_image.html', sp_img=data, type='basic', c_data=c_data, i_data=i_data, p_data=p_data)
And here’s an example image.
The above was sort of what I was looking for. But you can see that the image is running off the top of the figure. The really sad part is I have absolutely no idea why. Autoscale
is on. I searched the web, no luck. I messed around with various things. No real success. So, I am using a hack, that seems to prevent this happening in a good percentage of images.
Based on the maximum size of the marker (randomly generated in the function), I apply a margin to the plotting axes. I am making the margin larger than necessary, but I want to make sure the images fit within the figure. For the purposes of this app, some extra whitespace is not an issue. Here’s the added code with some context.
... ...
m1, m2, mx_sz, t_fst, p_step, hf_frq = sal.cycle_lw(ax, t_xs, t_ys)
ax.autoscale()
ax_mrgn = 0.025
if mx_sz >= 6500:
ax_mrgn = 0.15
elif mx_sz > 1000:
ax_mrgn = 0.09
ax.margins(ax_mrgn)
ax.autoscale()
ax.text(0.5, 0.01, "© koach (barkncats) 2022", ha="center", transform=ax.transAxes,
fontsize=10, alpha=.3, c=g.cycle[len(g.cycle)//2])
... ...
But, I am not really getting the images I expect. Nor am I liking what I get.
All too many of the images have those stubby endings in the centre of the image. And the width cycle itself looks somewhat stubby. In the image above the marker size went from smallest to largest 20 times. And the same marker size was used for 25 datapoints at at time.
Random Cycle Frequency
Not to sure what to do about those stubby ends, but probably easy enough to mess with the cycle frequency. Let’s give that a try.
Well, I am actually more interested in the cycle length. Or more specifically the half length. But, I figured I’d start by generating a random frequency and calculate the lengths of interest. Turns out cycle_lw()
uses the two global variables g.lc_frq
and g.hf_frq
to determine how many datapoints get the same marker size and when to switch the direction in which it changes the marker size.
So, in our route code, let’s randomly select different values each time we draw the image. c_div
defines how many cycles are used when drawing the image. Based on what I saw above, I expect the higher numbers will not look as good as the lower numbers.
And we will need to return those values to their previous state after drawing the image in case a different image type is selected.
... ...
tmp_lcf = g.lc_frq
tmp_hff = g.hf_frq
c_div = rng.choice([4, 6, 8, 10, 12, 14, 16, 18, 20])
g.lc_frq = g.t_pts // c_div
g.hf_frq = g.lc_frq // 2
... ...
g.lc_frq = tmp_lcf
g.hf_frq = tmp_hff
Examples
Curve Parameters
Wheels: 6 wheels (shape(s): circle (c))
Symmetry: k_fold = 3, congruency = 2
Frequencies: [-1, 14, 14, -10, -1, -10]
Widths: [1, 0.5907262432932319, 0.498204755150422, 0.41963188268404555, 0.35218762516563756, 0.21466744507467247j]
Heights: [1, 0.6831481066705599j, 0.5941771239892026j, 0.38501124186140895j, 0.2852023646565789j, 0.21254934316576282]
Drawing Parameters
Colour map: gnuplot
BG colour map: viridis
Line width (if used): 4
Cycling Line Width
Marker 1: o
Marker 2: o
Maximum marker size: 2500
Half marker cycle frequency: 256 based on 4 cycles
Starting position: 0
Axes margin: 0.09
Curve Parameters
Wheels: 6 wheels (shape(s): tetracuspid (t))
Symmetry: k_fold = 2, congruency = 1
Frequencies: [1, -3, -3, 3, -3, 1]
Widths: [1, 0.6688972433541296, 0.3803503667252792, 0.2973546918665232j, 0.27608108969057404, 0.18445135539053564j]
Heights: [1, 0.5897553496824748j, 0.3660974096746692, 0.18772951098767685j, 0.12958487458098986, 0.125j]
Drawing Parameters
Colour map: rainbow
BG colour map: cool
Line width (if used): 4
Cycling Line Width
Marker 1: o
Marker 2: o
Maximum marker size: 5500
Half marker cycle frequency: 170 based on 6 cycles
Starting position: 0
Axes margin: 0.09
Curve Parameters
Wheels: 9 wheels (shape(s): square (s))
Symmetry: k_fold = 4, congruency = 3
Frequencies: [3, -9, 19, 7, 11, -13, 19, 19, -13]
Widths: [1, 0.6948454768640558, 0.4067834685326895, 0.24137261683395295, 0.23082049735647983j, 0.12811756007299827, 0.125, 0.125, 0.125j]
Heights: [1, 0.5012029369716414, 0.4522422000958517, 0.3186973589076762, 0.30674842517569645j, 0.23464562505153477j, 0.16423389647662895, 0.125, 0.125]
Drawing Parameters
Colour map: turbo
BG colour map: jet
Line width (if used): 3
Cycling Line Width
Marker 1: o
Marker 2: o
Maximum marker size: 8500
Half marker cycle frequency: 51 based on 20 cycles
Starting position: 0
Axes margin: 0.175
It now seems pretty obvious form the above that more cycles generate more stubbiness. I will leave the larger numbers of cycles in for now. Just as a reminder that more is not always better.
Though there is always an exception or two to any rule.
Curve Parameters
Wheels: 7 wheels (shape(s): ellipse (e))
Symmetry: k_fold = 2, congruency = 1
Frequencies: [-1, 7, 7, 7, -7, -1, -7]
Widths: [1, 0.5522058025151322, 0.3086820264329621, 0.2724073050861452j, 0.23831662234279155j, 0.22487250338503403, 0.17150108142410317]
Heights: [1, 0.6089145994039912, 0.33951321102461945, 0.2920118780229452, 0.19578296526825295, 0.1863593052042107, 0.125j]
Drawing Parameters
Colour map: summer
BG colour map: ocean
Line width (if used): 8
Cycling Line Width
Marker 1: o
Marker 2: o
Maximum marker size: 3500
Half marker cycle frequency: 56 based on 18 cycles
Starting position: 0
Axes margin: 0.09
You may have noticed in the image above that there is a bit of an unexpected lump on the rightmost curve. This is due to the fact that the cycle size does not evenly divide the number of datapoints. I.E. 2048 % 18 != 0
And, here’s one using multiple wheel shapes.
Curve Parameters
Wheels: 7 wheels (shape(s): ['c', 't', 'c', 'e', 'r', 'r', 'c'])
Symmetry: k_fold = 3, congruency = 1
Frequencies: [-2, 13, 10, 1, 7, 1, -5]
Widths: [1, 0.5263707749405728, 0.5198791979752154j, 0.315891888769667, 0.25914920459828633j, 0.2564999361693947j, 0.18211430551134794]
Heights: [1, 0.6011874341964587j, 0.5255793709245029, 0.28252003167583567j, 0.16573984087450114j, 0.125, 0.125j]
Drawing Parameters
Colour map: twilight_shifted
BG colour map: gist_earth
Line width (if used): 6
Cycling Line Width
Marker 1: o
Marker 2: o
Maximum marker size: 1500
Half marker cycle frequency: 102 based on 10 cycles
Starting position: 0
Axes margin: 0.09
Done
There is something to be said for this image type, but it really isn’t one of my favourites. I think I am going to play with that colour between idea and see if I can get something I might like better. Though, this does do a reasonable job of simulating a cycling line width.
But, likely next time I will look at adding the use of random or user selectable marker shapes.
Until next time, may you have happy fingers.