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.

basic spirograpn image with attempt to simulate a cycling line width

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.

basic spirograpn image with attempt to simulate a cycling line width

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
basic spirograpn image with attempt to simulate a cycling line width
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
basic spirograpn image with attempt to simulate a cycling line width
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
basic spirograpn image with attempt to simulate a cycling line width

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
basic spirograpn image with attempt to simulate a cycling line width

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
basic spirograpn image with attempt to simulate a cycling line width

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.