Last post I said:

… likely next time I will look at adding the use of random or user selectable marker shapes.

But after finishing that post, I jumped on trying to code cycling line widths using matplotlib’s fill_between function. And, I got something working reasonably quickly. Though it took me some time to tune the output image to my liking. And, because, this image type was never part of my earlier development and playing, I thought I would document it sooner rather than later.

I will return to using different marker types in the image type covered last post next time.

Cycling Line Width II

New Image Function

Okay, since this image type was never part of my previous work, I will cover the new image function, cycle_lw_btw(), which I added to the app library module. Though it still relies on code/functions from my earlier efforts. And, those will not be covered here.

What I decided to do was go through the curve data for the basic spirograph and create two new lists of y data. For some cycle length, I will incrementally modify the upper y value so that it grows from some starting value to some maximum value and back down again. At the same time I would modify the lower y value so that if shrinks from some starting value to some minimum value and back up again.

I will at each loop iteration generate the new multipliers, apply them to the base y value and save to the appropriate list. Once done, I will plot them using fill_between.

The tuning part is primarily the number of cycles per image, maximum change per cycle, and number of colours per cycle. But, let’s start slow. I will, for now, use the global variables to sort the cycle length. But rather than track the cycle length/half length, I just monitor the multiplier in each loop to determine whether it has reached its maximum or minimum. If so, I change direction until that happens again.

def cycle_lw_btw(axt, rxs, rys):
  # rxs, rys 1D arrays
  n_dots = len(rxs)
  mx_grw = .1   # 10%
  c_inc = mx_grw / g.hf_frq
  ys_cyc = [[], []]
  mlt_1 = 1
  max_1 = mlt_1 + mx_grw
  i_dir = +1

  for i in range(n_dots):
    mlt_1 += c_inc * i_dir
    if mlt_1 > max_1 or mlt_1 < 1:
      i_dir *= -1
      mlt_1 += (c_inc * i_dir)
    ys_cyc[0].append(rys[i] * mlt_1)
    ys_cyc[1].append(rys[i] * 1 / mlt_1)

  axt.autoscale()
  axt.fill_between(rxs, ys_cyc[0], ys_cyc[1], alpha=g.alph)

New Route

Well, let’s quickly put together a basic route so we can test the above. I pretty much copied the route from the previous post and refactored it. I am for now sticking with the increased number of data points.

@app.route('/spirograph/basic_clw_btw', methods=['GET', 'POST'])
def sp_basic_clw_btw():
  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()
    sal.cycle_lw_btw(ax, t_xs[-1], t_ys[-1])
    # create variables used by page data dictionary
    m1, m2 = 'o', 'o'
    mx_sz = 6500
    t_fst = 0
    p_step = 1
    hf_frq = g.hf_frq

    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, 'c_nbr': g.lc_frq, 'hf_frq': hf_frq, 'ax_mrgn': ax_mrgn}

    return render_template('disp_image.html', sp_img=data, type='basic', c_data=c_data, i_data=i_data, p_data=p_data)

And a quick test would indicate that seems to work.

Curve Parameters

    Wheels: 12 wheels (shape(s): circle (c))
    Symmetry: k_fold = 10, congruency = 9
    Frequencies: [19, 39, -1, 39, 19, 19, -11, 49, 49, -21, 9, 49]
    Widths: [1, 0.5038030681543041, 0.295511401680075, 0.2629067021370173, 0.2610137314865527, 0.2216161722496215, 0.125, 0.125j, 0.125j, 0.125, 0.125j, 0.125j]
    Heights: [1, 0.6371085099365557j, 0.6252277388563419, 0.6153258885743479, 0.37368941318254445j, 0.3650193581535417, 0.2961584285589239j, 0.17875621396963892, 0.14242834635573573j, 0.1348790363542926j, 0.12808809666184695j, 0.125j]

Drawing Parameters
    Colour map: PuRd
    BG colour map: bone
    Line width (if used): 1

Cycling Line Width
  Half marker cycle frequency: 25 based on 51 cycles
basic spirograpn image using colour between idea to simulate a cycling line width

We will eventually get around to cycling the colours as well. But, I would like to see the maximum a little thicker. Increasing a variable ought to take care of that. I also refactored the function to return some information about some of its variables, so I could show them on the display image page. Which I have refactored to do so.

The function now looks like the following.

def cycle_lw_btw(axt, rxs, rys):
  # rxs, rys 1D arrays
  n_dots = len(rxs)
  min_i = 1
  mx_grw = .5   # 50%
  c_inc = mx_grw / g.hf_frq
  ys_cyc = [[], []]
  mlt_1 = min_i
  max_1 = mlt_1 + mx_grw
  i_dir = +1

  # count number of times the maximum value is reached
  nbr_max = 0
  for i in range(n_dots):
    mlt_1 += c_inc * i_dir
    if mlt_1 > max_1 or mlt_1 < min_i:
      i_dir *= -1
      mlt_1 += (c_inc * i_dir)
      nbr_max += 1
    ys_cyc[0].append(rys[i] * mlt_1)
    ys_cyc[1].append(rys[i] * (1 / mlt_1))

  axt.autoscale()
  axt.fill_between(rxs, ys_cyc[0], ys_cyc[1], alpha=g.alph)

  return min_i, max_1, c_inc, nbr_max

And, here’s an example. I am thinking there are two many cycles. For this one, the maximum multiplier was reached 81 times. That’s a lot of cycles.

Curve Parameters
  Wheels: 6 wheels (shape(s): circle (c))
  Symmetry: k_fold = 3, congruency = 2
  Frequencies: [5, -7, 2, 5, -4, 14]
  Widths: [1, 0.6273391413064091j, 0.5384816539423078j, 0.3161649885284256j, 0.2649472854616658, 0.18292126198313424]
  Heights: [1, 0.5062062384091081j, 0.26200187044227835j, 0.2557389310078645, 0.19842444116003208j, 0.16219741452305064]

Drawing Parameters
  Colour map: hot
  BG colour map: gnuplot
  Line width (if used): 1

Cycling Line Width
  Min line width multiplier: 1
  Max line width multiplier: 1.5
  Half cycle frequency: 25
  Multiplier increment: 0.02
  Number of maximums: 81
basic spirograpn image using colour between idea to simulate a cycling line width

But, before dealing with that and any other fine tuning, let’s add a colour cycle. I expect that will help us see things a little better.

Add Cycling Colours

To start, I am going to use the same colour for 16 sequential datapoints. I will start by setting up some variables. A starting index into the colour cycle. And a jump value. That is, how many steps to jump in the colour cycle to get the next colour for each iteration of the drawing loop. At the end of each loop iteration I will add the jump value to the current index and ensure I don’t exceed the maximum index (modulus very handy for that purpose).

def cycle_lw_btw(axt, rxs, rys):
  # rxs, rys 1D arrays
  n_dots = len(rxs)
  min_i = 1
  mx_grw = .5   # 50%
  c_inc = mx_grw / g.hf_frq
  ys_cyc = [[], []]
  mlt_1 = min_i
  max_1 = mlt_1 + mx_grw
  i_dir = +1

  c_len = len(g.cycle)
  c_ndx = rng.integers(0, c_len-2)
  c_jmp = int(c_len * 0.15)
  # make sure odd, don't want colours repeating too quickly
  if c_jmp % 2 == 0:
    c_jmp += 1
  l_alph = 1

  nbr_max = 0
  for i in range(n_dots):
    mlt_1 += c_inc * i_dir
    if mlt_1 > max_1 or mlt_1 < min_i:
      i_dir *= -1
      mlt_1 += (c_inc * i_dir)
      nbr_max += 1
    ys_cyc[0].append(rys[i] * mlt_1)
    ys_cyc[1].append(rys[i] * (1 / mlt_1))

  axt.autoscale()
  pp_clr = 16
  for i in range(0, n_dots, pp_clr):
    c_end = i + pp_clr
    axt.fill_between(rxs[i:c_end], ys_cyc[0][i:c_end], ys_cyc[1][i:c_end], alpha=l_alph, color=g.cycle[c_ndx])
    c_ndx = (c_ndx + c_jmp) % c_len
    axt.autoscale()
  # make sure all points have been used
  if c_end < n_dots:
    i += pp_clr - 1
    axt.fill_between(rxs[i:], ys_cyc[0][i:], ys_cyc[1][i-1:], alpha=l_alph, color=g.cycle[c_ndx])
    axt.autoscale()
  
  return min_i, max_1, c_inc, nbr_max

And, here’s an example.

Curve Parameters
  Wheels: 9 wheels (shape(s): circle (c))
  Symmetry: k_fold = 6, congruency = 4
  Frequencies: [4, 16, 10, -8, 16, -2, 4, 4, 4]
  Widths: [1, 0.630502254226869j, 0.4349065802566133, 0.2646303445671649j, 0.1391907527205493, 0.125, 0.125, 0.125, 0.125]
  Heights: [1, 0.6726595363538985, 0.5307745071041609j, 0.446965855805183, 0.34856831556806284, 0.23986012520100353, 0.19878439824589478, 0.1506717824640207, 0.125j]

Drawing Parameters
  Colour map: plasma
  BG colour map: inferno
  Line width (if used): 2

Cycling Line Width
  Min line width multiplier: 1
  Max line width multiplier: 1.5
  Half cycle frequency: 25
  Multiplier increment: 0.02
  Number of maximums: 81
basic spirograpn image using colour between idea to simulate a cycling line width

A number of the size cycles look rather stubby, giving this image a mosiac-like look and feel. And, until looking at this image I failed to realize that with the approach I am using the line width is in fact a function of the y values. The smaller the y values in that segment of the spirograph, the smaller the maximum line width. And, the slower the change.

But first let’s see if we can get rid of those gaps between colour sections. I will after the first fill_between, go back one datapoint for the starting index of the current colour section and see if that sorts the gaps.

  for i in range(0, n_dots, pp_clr):
    c_end = i + pp_clr
    if i == 0:
      axt.fill_between(rxs[i:c_end], ys_cyc[0][i:c_end], ys_cyc[1][i:c_end], alpha=l_alph, color=g.cycle[c_ndx])
    else:
      axt.fill_between(rxs[i-1:c_end], ys_cyc[0][i-1:c_end], ys_cyc[1][i-1:c_end], alpha=l_alph, color=g.cycle[c_ndx])
    c_ndx = (c_ndx + c_jmp) % c_len
    axt.autoscale()
  if c_end < n_dots:
    i += pp_clr - 1
    axt.fill_between(rxs[i:], ys_cyc[0][i:], ys_cyc[1][i:], alpha=l_alph, color=g.cycle[c_ndx])
    axt.autoscale()

And, apparently it does.

Curve Parameters
  Wheels: 7 wheels (shape(s): equilateral triangle (q))
  Symmetry: k_fold = 2, congruency = 1
  Frequencies: [1, -1, -3, -5, -5, -3, 3]
  Widths: [1, 0.7479638538788268, 0.46390242606273896, 0.446030330211376, 0.3077303166749479, 0.27526030869307017j, 0.22070942518781475]
  Heights: [1, 0.6440335679944061j, 0.6228860356543944j, 0.46756503993960974j, 0.450739943137015, 0.2700345423595428j, 0.14887727947228274]

Drawing Parameters
  Colour map: magma
  BG colour map: plasma
  Line width (if used): 3

Cycling Line Width
  Min line width multiplier: 1
  Max line width multiplier: 1.5
  Half cycle frequency: 25
  Multiplier increment: 0.02
  Number of maximums: 81
basic spirograpn image using colour between idea to simulate a cycling line width

And, this image makes it pretty clear there are just way too many width cycles in the images. Let’s tackle that next.

Fine Tuning Width Cycle Length

We need to mess with the cycle frequency and half frequency to get a suitable width increment for the number of data points in the spirograph dataset. I am going to add another parameter to the function definition. It specifies the number of cycles for the image and defaults to 8. I may eventually allow users to choose a value they like, or just use random values on each route request. While I am at it I will also add a maximum growth paramter, mx_grw=.5. The default being 50%. Here’s the changed code.

def cycle_lw_btw(axt, rxs, rys, n_cyc=8, mx_grw=.5):
  # rxs, rys 1D arrays
  n_dots = len(rxs)
  f_cyc = n_dots // n_cyc
  h_cyc = f_cyc // 2
  min_i = 1
  c_inc = mx_grw / h_cyc
... ...

And based on the following image, it would appear to work as desired. Though based on many others I looked at you’d be hard pressed to tell. Also, I think the colour blocks may be a bit too large. I’ll play with that next.

Curve Parameters
  Wheels: 10 wheels (shape(s): ellipse (e))
  Symmetry: k_fold = 7, congruency = 5
  Frequencies: [12, -2, 19, -23, -16, 19, -16, -16, -16, 33]
  Widths: [1, 0.6929645905833033, 0.3750856357011709, 0.20267620024430746j, 0.13273572446048165, 0.125, 0.125j, 0.125j, 0.125j, 0.125]
  Heights: [1, 0.666312481447258, 0.3336199459551245, 0.22675700154772882j, 0.211702306219633, 0.18316195907646274, 0.125j, 0.125, 0.125, 0.125]

Drawing Parameters
  Colour map: jet
  BG colour map: turbo
  Line width (if used): 6

Cycling Line Width
  Min line width multiplier: 1
  Max line width multiplier: 1.5
  Half cycle frequency: 25
  Multiplier increment: 0.00390625
  Number of maximums: 8
basic spirograpn image using colour between idea to simulate a cycling line width

Fine Tune Colour Section Size

This, I expect, is one of those things that are in the eye of the beholder. After some trial and error, I settled on the following:

  pp_clr = int(3.25 * (n_dots // 512))

I also added the number of datapoints per colour block to the returned values so I could display that information on the image display page. Won’t bother showing those code/template changes.

Here’s an example with rhombus shaped wheels.

Curve Parameters
  Wheels: 9 wheels (shape(s): rhombus (r))
  Symmetry: k_fold = 2, congruency = 1
  Frequencies: [1, 9, -5, -7, 1, 1, 1, -7, -5]
  Widths: [1, 0.63300912693523, 0.3822856858108871j, 0.28101844734814396j, 0.16276993531466805, 0.15075064148697978, 0.125, 0.125, 0.125]
  Heights: [1, 0.671338228745219, 0.5912722423306065j, 0.3112127180416511, 0.17502660025079753, 0.14152912108276042j, 0.125j, 0.125j, 0.125j]

Drawing Parameters
  Colour map: terrain
  BG colour map: terrain
  Line width (if used): 5

Cycling Line Width
  Min line width multiplier: 1
  Max line width multiplier: 1.5
  Half cycle frequency: 25
  Multiplier increment: 0.00390625
  Number of maximums: 8
  Datapoints per colour section: 13
basic spirograpn image using colour between idea to simulate a cycling line width

Too my eye, the colour sections are a reasonable size. More or less in balance with the spriograph itself.

Done

And, I think that is it for this one. I believe I have covered my development of this image type in reasonable detail. Discussing some of my thought process along the way. And, there’s 6 nice images in the post as well. Which more or less document the steps taken to get this image type to a state I am happy with.

But I am having some second thoughts about using a percentage to alter the line width size. It produces some, to me, undesireable results in the images. I may look at trying to use more consistent width cycle sizes and see what happens.

Lots to think about before the next post.

Until next time, may your keyboard sing a happy song.