I am liking this latest image type—cycling line widths using fill_between()—enough that I think I will continue playing with it. So, I thought I’d look at applying 2D transforms to see what we can get.

2D Transforms with Cycling Line Width Images

I already have that affine_transformation() function in the app library module and some experience generating transforms. So, this will hopefully be a fairly quick and painless process.

New/Refactored Functions

I thought I’d start by copying one of the previous 2D transform route functions and refactoring it to work for this image type. If it works, I will then go on to refactor the rest of the app to provide access to and support for the route. I am hoping the existing global variables will get the job done, without needing to add any new ones.

Well, this will in fact require some considerable refactoring and/or a new function or two. The function I wrote for this style of cycling line width image, plotted the data it generated and did not return it to the caller. To generate the transforms, I need the initial image data to which I can apply the desired number of transforms.

I don’t, at this point, really want to refactor the function and the route from the last post. So, I am going to add another parameter to the function definition. It will default to false, but if set to true, the function will not plot anything, but will instead return the 2 new sets of y values to the caller. A bit of a pain to have two different sets of return values depending on a function argument, but such is life when one adds functionality on the fly. I.E. no design/plan for the app was developed before I started coding.

I am not going to show the refactored function, as it is a simple and straightforward modification.

Trouble! As I went on, I realized I’d have to add all the plotting code in the cycle_lw_btw() function to the route’s function. Not really a great idea. So, I am going to refactor the function afterall. I will break it into two separate functions. One to generate the cycling line width data and one to plot a given set of cycling line width data. For now I will leave the original function so I don’t have to immediately refactor the earlier route. So two new functions with different names: clw_btw_make() and clw_btw_plot().

Mostly just copy & paste, with a touch of refactoring.

def clw_btw_make(rxs, rys, u_pcnt=False, n_cyc=8, dppc=12, mx_grw=.25):
  # rxs, rys 1D arrays
  n_dots = len(rxs)
  f_cyc = n_dots // n_cyc
  h_cyc = f_cyc // 2
  # need return values even if not used to generate image
  rtn_vals = {
    'h_cyc': h_cyc,
    'min_i': None,
    'mx_sz': None,
    'c_inc': None,
    'nbr_max': None,
  }
  ys_cyc = [[], []]
  if u_pcnt:
    min_i = 1
    c_inc = mx_grw / h_cyc
    mlt_1 = min_i
    mx_sz = mlt_1 + mx_grw
    rtn_vals['min_i'] = min_i
  else:
    mx_sz = rys.max() * mx_grw
    c_inc = mx_sz / h_cyc
    f_inc = 0
  rtn_vals['mx_sz'] = mx_sz
  rtn_vals['c_inc'] = c_inc

  i_dir = +1

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

  rtn_vals['nbr_max'] = nbr_max
  
  return ys_cyc[0], ys_cyc[1], rtn_vals


def clw_btw_plot(axt, rxs, rys1, rys2, dppc=12):
  # rxs, rys* 1D arrays
  n_dots = len(rxs)
  
  c_len = len(g.cycle)
  c_ndx = rng.integers(0, c_len-2)
  c_jmp = c_len // 40
  c_jmp = int(c_len * 0.15)
  if c_jmp % 2 == 0:
    c_jmp += 1
  l_alph = 1

  axt.autoscale()
  pp_clr = int(dppc * (n_dots // 512))
  print(f"\tclw_btw_plot(..., dppc={dppc} -> n_dots = {n_dots} & pp_clr = {pp_clr}")
  for i in range(0, n_dots, pp_clr):
    c_end = i + pp_clr
    if i == 0:
      axt.fill_between(rxs[i:c_end], rys1[i:c_end], rys2[i:c_end], alpha=l_alph, color=g.cycle[c_ndx])
    else:
      axt.fill_between(rxs[i-1:c_end], rys1[i-1:c_end], rys2[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:], rys1[i:], rys2[i:], alpha=l_alph, color=g.cycle[c_ndx])
    axt.autoscale()

  return pp_clr

New Route

Again, mostly copy & paste, with a touch of refactoring.

But I did run into a bug; well, an error on my part. Because I originally wrote get_transform_base() and get_transform_data() to work with gnarly style images, these two functions expect an array of arrays (or list of lists) for the x and y data arguments. Took me a bit of time to sort that out and get things working. After that things went rather smoothly.

@app.route('/spirograph/basic_clw_btw_t', methods=['GET', 'POST'])
def sp_clw_btw_t():
  pg_ttl = "Cycling Line Width Spirograph Image with 2D Transforms"
  if request.method == 'GET':
    f_data = {'pttl': pg_ttl, 'shapes': splt.shp_nm, 'cxsts': g.n_whl is not None,
        'cmaps': g.clr_opt, 'mntr': g.mn_tr, 'mxtr': g.mx_tr, 'subtyp': 'clw_btw'
    }
    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()
    clwy_1, clwy_2, i_param = sal.clw_btw_make(t_xs[-1], t_ys[-1], u_pcnt=g.use_pct, n_cyc=g.n_lwcyc, dppc=g.dpp_clr)

    # get angles and such
    lld_rot, lld_deg, trdy, y_mlt = sal.get_transform_base([t_xs[-1]], [clwy_1])
    
    # get rand quadrant order
    do_qd = list(range(g.nbr_tr))
    do_qd2 = sal.get_rnd_qds(do_qd)

    # Generate transformed datasets
    for i in do_qd2:
      dstx1, dsty1 = sal.get_transform_data(t_xs[-1], clwy_1, lld_rot[i], trdy)
      dstx2, dsty2 = sal.get_transform_data(t_xs[-1], clwy_2, lld_rot[i], trdy)

      pp_clr = sal.clw_btw_plot(ax, dstx1, dsty1, dsty2, dppc=g.dpp_clr)
      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()

    bax2, _ = sal.set_bg(ax)

    # convert image to base64 for embedding in html page
    data = sal.fig_2_base64(fig)

    d_end = "top" if g.drp_f else "bottom"
    c_data = sal.get_curve_dtl()
    i_data = sal.get_image_dtl(pg_ttl)
    p_data = {'mx_mlt': i_param['mx_sz'], 'mn_mlt': i_param['min_i'], 'm_inc': i_param['c_inc'],
              'hf_frq': i_param['h_cyc'], 'n_max': i_param['nbr_max'], 'pp_clr': pp_clr,
              'u_pct': g.use_pct, 'n_lwc': g.n_lwcyc,
              'ntr': g.nbr_tr, 'trang': lld_deg, 'trpt': f'(0, {round(trdy, 3)}) ({y_mlt})',
              'trord': do_qd2
              }

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

Examples

Curve Parameters
  Wheels: 8 wheels (shape(s): ellipse (e))
  Symmetry: k_fold = 7, congruency = 5
  Frequencies: [12, -2, -9, -23, -9, -16, -2, -23]
  Widths: [1, 0.61340685429815, 0.4704852896897198j, 0.36490132705418155j, 0.3184763890009844, 0.16227766668617918, 0.125, 0.125]
  Heights: [1, 0.5786476947298501, 0.5742218653462878j, 0.5128714750763494j, 0.39665236293192846j, 0.25710801322339905, 0.1338623541828563, 0.125]

Drawing Parameters
  Colour map: turbo
  BG colour map: rainbow
  Line width (if used): 2

Transformations
  Number of transforms: 5
  Angles: [36.0, 108.0, 180.0, 252.0, 324.0]
  Base linear translation point: (0, 2.136) (0.75)
  Order: [4, 3, 1, 2, 0]

Cycling Line Width
  Use percentage increment: False
  Number cycles: 8
  Half cycle points: 128
  Multiplier increment: 0.005084479531953572
  Number of maximums: 8
  Datapoints per colour section: 48
basic spirograpn image using colour between idea to simulate a cycling line width with 2D transforms added

Not bad!

Curve Parameters
  Wheels: 3 wheels (shape(s): equilateral triangle (q))
  Symmetry: k_fold = 2, congruency = 1
  Frequencies: [-1, 1, 5]
  Widths: [1, 0.6936470360208918, 0.5093037606701534]
  Heights: [1, 0.5409909237943519j, 0.48655539976755313]

Drawing Parameters
  Colour map: winter
  BG colour map: bone
  Line width (if used): 4

Transformations
  Number of transforms: 4
  Angles: [45.0, 135.0, 225.0, 315.0]
  Base linear translation point: (0, 0.826) (0.75)
  Order: [0, 3, 2, 1]

Cycling Line Width
  Use percentage increment: False
  Number cycles: 8
  Half cycle points: 128
  Multiplier increment: 0.0018575934496081262
  Number of maximums: 8
  Datapoints per colour section: 48
basic spirograpn image using colour between idea to simulate a cycling line width with 2D transforms added
Curve Parameters
      Wheels: 8 wheels (shape(s): rhombus (r))
      Symmetry: k_fold = 4, congruency = 3
      Frequencies: [-1, 7, -1, -13, 19, 19, 11, 11]
      Widths: [1, 0.673910461407562j, 0.4540126503471738, 0.367398104948511j, 0.23266584642107202, 0.16895498351999938j, 0.125, 0.125]
      Heights: [1, 0.7161225970165781, 0.6204515259999254j, 0.42579256108056995, 0.3176138434293651j, 0.16734833935138504j, 0.13824074330930122j, 0.125j]

Drawing Parameters
  Colour map: cubehelix
  BG colour map: Greys
  Line width (if used): 7

Transformations
  Number of transforms: 5
  Angles: [36.0, 108.0, 180.0, 252.0, 324.0]
  Base linear translation point: (0, 1.377) (0.875)
  Order: [3, 2, 4, 1, 0]

Cycling Line Width
  Use percentage increment: False
  Number cycles: 8
  Half cycle points: 128
  Multiplier increment: 0.0016208371736539007
  Number of maximums: 8
  Datapoints per colour section: 48
basic spirograpn image using colour between idea to simulate a cycling line width with 2D transforms added

I do like this image variation!

Bug?

I kept seeing unexpectedly thin lines in the images. Then, for an image with 3 transforms, one of the transforms had no cycling line width at all. What the h…? Here’s an example image with 6 transforms.

Curve Parameters
  Wheels: 11 wheels (shape(s): square (s))
  Symmetry: k_fold = 5, congruency = 2
  Frequencies: [-3, 22, -8, -18, 2, -13, -13, 17, 22, 22, 22]
  Widths: [1, 0.6548539846253743j, 0.3777398702102073, 0.28852912358429295j, 0.18948559280111515j, 0.125j, 0.125j, 0.125j, 0.125j, 0.125, 0.125j]
  Heights: [1, 0.7309820139844311j, 0.4351271752180745, 0.2782390032690119j, 0.15084091881220765, 0.14160123869674454, 0.125j, 0.125, 0.125, 0.125j, 0.125]

Drawing Parameters
  Colour map: viridis
  BG colour map: bone
  Line width (if used): 7

Transformations
  Number of transforms: 6
  Angles: [29.999999999999996, 90.0, 149.99999999999997, 210.0, 270.0, 330.0]
  Base linear translation point: (0, 1.609) (0.875)
  Order: [3, 0, 4, 5, 1, 2]

Cycling Line Width
  Use percentage increment: False
  Number cycles: 8
  Half cycle points: 128
  Multiplier increment: 0.0032829983886528058
  Number of maximums: 8
  Datapoints per colour section: 48
basic spirograpn image using colour between idea to simulate a cycling line width with 2D transforms added

And, there are 2 transforms without any cycling line width. ????

So I added some print statements. And, for a rotational transform at 90° or 270°, the two y datasets returned by get_transform_data() were the same. Here’s an example.

Curve Parameters
  Wheels: 3 wheels (shape(s): ellipse (e))
  Symmetry: k_fold = 3, congruency = 1
  Frequencies: [4, 10, 7]
  Widths: [1, 0.5827070566094672, 0.44746593095019244]
  Heights: [1, 0.591293217062774, 0.30797800190853897j]

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

Transformations
  Number of transforms: 8
  Angles: [45.0, 90.0, 135.0, 180.0, 225.0, 270.0, 315.0, 360.0]
  Base linear translation point: (0, 1.009) (0.75)
  Order: [6, 1, 0, 7, 3, 5, 4, 2]

Cycling Line Width
  Use percentage increment: False
  Number cycles: 8
  Half cycle points: 128
  Multiplier increment: 0.0021257311897505274
  Number of maximums: 8
  Datapoints per colour section: 48

== and the output of those debug print statements I mentioned

sum(clwy_1[1:10]) - sum(clwy_2[1:10]): 0.2295789684930567
do_qd: 6, angle: 315.0, trdy: 1.0094823483898667, sum(dsty1[1:10]) - sum(dsty2[1:10]): 0.16233684543925375
do_qd: 1, angle: 90.0, trdy: 1.0094823483898667, sum(dsty1[1:10]) - sum(dsty2[1:10]): 0.0
do_qd: 0, angle: 45.0, trdy: 1.0094823483898667, sum(dsty1[1:10]) - sum(dsty2[1:10]): 0.16233684543925264
do_qd: 7, angle: 360.0, trdy: 1.0094823483898667, sum(dsty1[1:10]) - sum(dsty2[1:10]): 0.22957896849305826
do_qd: 3, angle: 180.0, trdy: 1.0094823483898667, sum(dsty1[1:10]) - sum(dsty2[1:10]): -0.2295789684930547
do_qd: 5, angle: 270.0, trdy: 1.0094823483898667, sum(dsty1[1:10]) - sum(dsty2[1:10]): 0.0
do_qd: 4, angle: 225.0, trdy: 1.0094823483898667, sum(dsty1[1:10]) - sum(dsty2[1:10]): -0.1623368454392491
do_qd: 2, angle: 135.0, trdy: 1.0094823483898667, sum(dsty1[1:10]) - sum(dsty2[1:10]): -0.16233684543925286
basic spirograpn image using colour between idea to simulate a cycling line width with 2D transforms added

Similar situation to the one above it.

After some thinking, I realized that the cosine of 90° and 270° is 0. I figured, there might also a similar issue for 0° and 180° where the sine is 0. So for those cases I will change the angle for the rotational transform.

My first thought was to use one of the other angles in the list. But that would just result in the given transform being plotted twice in the same location and potentially empty space(s) in the final image.

Also, I didn’t want to change the angle used to determine the linear translation/transform. So, I refactored get_transform_data() to take two angles. One for the rotational transform and one for the linear one. Won’t bother showing that, as it is pretty straightforward. And, in my route I modified the section where the calls to get_transform_data() are made as follows. That 1.2 multiplier is just a guess. If it is too small the cosine or sine will still be too close to 0 to generate a decent maximum line width. If too large, the symmetry of the final image could be significantly compromised.

... ...
    # Generate transformed datasets
    for i in do_qd2:
      if lld_deg[i] in [0.0, 90.0, 180.0, 270.0]:
        tmp_rot = 1.2 * lld_rot[i]
        dstx1, dsty1 = sal.get_transform_data(t_xs[-1], clwy_1, tmp_rot, lld_rot[i], trdy)
        dstx2, dsty2 = sal.get_transform_data(t_xs[-1], clwy_2, tmp_rot, lld_rot[i], trdy)
      else:
        dstx1, dsty1 = sal.get_transform_data(t_xs[-1], clwy_1, lld_rot[i], lld_rot[i], trdy)
        dstx2, dsty2 = sal.get_transform_data(t_xs[-1], clwy_2, lld_rot[i], lld_rot[i], trdy)
... ...

And, that seems to get the job done.

Curve Parameters
  Wheels: 8 wheels (shape(s): circle (c))
  Symmetry: k_fold = 7, congruency = 5
  Frequencies: [-2, -9, -2, 12, 12, -23, -9, 5]
  Widths: [1, 0.6830826663611991j, 0.44957079238986053j, 0.27924678442078166j, 0.2177164807434988, 0.13604463796930064, 0.125, 0.125]
  Heights: [1, 0.6016346830352731j, 0.4611643062520087, 0.2776867678572044, 0.17998130272117854j, 0.1530489348145748, 0.125, 0.125]

Drawing Parameters
  Colour map: RdPu
  BG colour map: Greys
  Line width (if used): 1

Transformations
  Number of transforms: 6
  Angles: [29.999999999999996, 90.0, 149.99999999999997, 210.0, 270.0, 330.0]
  Base linear translation point: (0, 1.008) (0.625)
  Order: [2, 1, 3, 5, 4, 0]

Cycling Line Width
  Use percentage increment: False
  Number cycles: 8
  Half cycle points: 128
  Multiplier increment: 0.002876239835480084
  Number of maximums: 8
  Datapoints per colour section: 48

sum(clwy_1[1:10]) - sum(clwy_2[1:10]): 0.310633902231849
do_qd: 2, angle: 150.0, trdy: 1.0077281626198644, sum(dsty1[1:10]) - sum(dsty2[1:10]): -0.2690168506094729
do_qd: 1, angle: 90.0 ==> 108.0, trdy: 1.0077281626198644, sum(dsty1[1:10]) - sum(dsty2[1:10]): -0.09599115481865184
do_qd: 3, angle: 210.0, trdy: 1.0077281626198644, sum(dsty1[1:10]) - sum(dsty2[1:10]): -0.26901685060947145
do_qd: 5, angle: 330.0, trdy: 1.0077281626198644, sum(dsty1[1:10]) - sum(dsty2[1:10]): 0.2690168506094729
do_qd: 4, angle: 270.0 ==> 324.0, trdy: 1.0077281626198644, sum(dsty1[1:10]) - sum(dsty2[1:10]): 0.251308105934573
do_qd: 0, angle: 30.0, trdy: 1.0077281626198644, sum(dsty1[1:10]) - sum(dsty2[1:10]): 0.26901685060947145
basic spirograpn image using colour between idea to simulate a cycling line width with 2D transforms added

Done

Well, that was a bit of fun. But, I think that will be it for this post.

I did modify, as appropriate, all the app pages to provide access to this image variation. And, I did update gcloud (gcloud app deploy). Don’t think I need to include any of that in the post as the process has been covered in previous posts.

I hope you get the opportunity to have as much fun coding as I did with this image variation. Whether plain or with transforms.