(Trying to create) ThickSpline

Hello!
I’m trying to modify spline to create thickspline. I’m probably not the best person to do this but well… here’s what i got so far (most code comes from spline anywyay:

#@cli thickspline : x0[%],y0[%],u0[%],v0[%],x1[%],y1[%],u1[%],v1[%],_thickness,_opacity,_color1,...
#@cli : Draw specified colored thick spline curve on selected images (cubic hermite spline).
#@cli : Default values: 'thickness=2', 'opacity=1' and 'color1=0'.
#@cli : $ 400,400,1,3 repeat 100 thickspline {u([w,h,1000,1000,w,h,1000,1000,10])},1,${-rgb} done
 e[^-1] "Draw thick spline ($1,$2) - ($5,$6) on image$?, with thickness $9, opacity $10 and color (${11--1})."
thickspline : skip ${9=2},${10=1},${11=0}
  if !$9||$9==1 spline ${1-8},${10--1}
  else foreach {
    x0={if(${"is_percent $1"},$1*(w-1),$1)}
    y0={if(${"is_percent $2"},$2*(h-1),$2)}
    u0={if(${"is_percent $3"},$3*(w-1),$3)}
    v0={if(${"is_percent $4"},$4*(h-1),$4)}
    x1={if(${"is_percent $5"},$5*(w-1),$5)}
    y1={if(${"is_percent $6"},$6*(h-1),$6)}
    u1={if(${"is_percent $7"},$7*(w-1),$7)}
    v1={if(${"is_percent $8"},$8*(h-1),$8)}
    th={round($9*0.5)}
    repeat $th
    th=$>
    eval ${-math_lib}"spline(#0,["$x0-$th","$y0-$th"],["$u0-$th","$v0-$th"],["$x1-$th","$y1-$th"],["$u1-$th","$v1-$th"],$10,[${11--1}])"
    th+=1
    eval ${-math_lib}"spline(#0,["$x0+$th","$y0+$th"],["$u0+$th","$v0+$th"],["$x1+$th","$y1+$th"],["$u1+$th","$v1+$th"],$10,[${11--1}])"
    done
  } fi

The idea here is to draw splines above and below the original one, just with a repeat.
It seems to work (and can create a ribbon look or pen strokes (calligraphy?) with some modifications (increment only velocity values if i remember correctly) ) BUT sometimes you can see empty pixels inside the spline (It probably need an offset somewhere). And the wider the spline, the slower the command will execute :person_shrugging:


Looks like we’re back to the 80s.

I had another idea : draw 2 splines at +N and -N pixels (where N=Thickness/2) from the original spline (which doesn’t even need to be drawn I think) and then use floodto fill the gap between the two… but I can’t find the spot to fill it hahaha! :stuck_out_tongue:

2 Likes

One of my idea is to use a 3D image, and using crop/draw in order to create a spline. And this’ll work for variable thickness.

However, I don’t think you’re at that level yet. You’re starting to get there, I think.

@prawnsushi , I’ve added a command thickspline added to the gmic_stdlib. This is a first draft, probably to be improved over time. In particular, it does not handle opacity very well.

Commit: Add command 'thickspline'. · GreycLab/gmic@b36daf5 · GitHub

Update has been pushed with the command, so, as always, gmic update to get it.

Let le know if that helps.

Example of use:


foo :
  400,400,1,3
  thickspline 20,20,1000,0,380,380,1000,0,10,1,255,128,0

gmic_thickspline

3 Likes

Regarding coloring issues, wouldn’t it make sense to have hex arguments to define colors? That is what I done before.

Or an argument to specify numbers of colors and to insert color from those arguments. That’s why I would like custom expression to select arguments.

Thanks @David_Tschumperle !

It is faster than my version but looks also different (BTW thickspline’s help shows an example for spline).
Here’s mine, renamed to pr_thickspline :


gmic run './gmic_scripts/prawnsushi.gmic 800,800,1,3 tic pr_thickspline 5%,10%,1000,0,95%,95%,1000,0,15,1,255,128,0 toc to ${}'

Thickness 15 means the spline is drawn 15 times but I like the look… Transparency looks bad though.


#@cli pr_thickspline : x0[%],y0[%],u0[%],v0[%],x1[%],y1[%],u1[%],v1[%],_thickness,_opacity,_color1,...
#@cli : Draw specified colored thick spline curve on selected images (cubic hermite spline).
#@cli : Default values: 'thickness=2', 'opacity=1' and 'color1=0'.
#@cli : $ 400,400,1,3 repeat 100 pr_thickspline {u([w,h,1000,1000,w,h,1000,1000,10])},0.5,${-rgb} done
 e[^-1] "Draw thick spline ($1,$2) - ($5,$6) on image$?, with thickness $9, opacity $10 and color (${11--1})."
pr_thickspline : skip ${9=2},${10=1},${11=0}
  if $9<2 spline ${1-8},${10--1}
  else foreach {
    x0={if(${"is_percent $1"},$1*(w-1),$1)}
    y0={if(${"is_percent $2"},$2*(h-1),$2)}
    u0={if(${"is_percent $3"},$3*(w-1),$3)}
    v0={if(${"is_percent $4"},$4*(h-1),$4)}
    x1={if(${"is_percent $5"},$5*(w-1),$5)}
    y1={if(${"is_percent $6"},$6*(h-1),$6)}
    u1={if(${"is_percent $7"},$7*(w-1),$7)}
    v1={if(${"is_percent $8"},$8*(h-1),$8)}
    th={round($9*0.5,1,1)}
 
    repeat $th {
      th=$>
      eval
        th=$>;\
        ${-math_lib}"spline(#0,["$x0-$th","$y0"],["$u0","$v0"],["$x1-$th","$y1"],["$u1","$v1"],$10,[${11--1}])";\
        "spline(#1,["$x0","$y0"],["$u0+$th","$v0"],["$x1","$y1"],["$u1+$th","$v1"],$10,[${11--1}])"
      }
  } fi

I’m trying to squeeze repeat inside eval but have had no luck so far with the math_lib syntax. Couldn’t increment $theither inside eval (incrementing +100 every time wouldn’t even change a thing) but at least I got rid of the second call.

@Reptorian Not sure what you mean by drawing 3d meshes to draw a spline… Wouldn’t that be overkill? Still if you can do it, why not?
About the colors, would that fix the transparency issues?

1 Like

And here are some Spline Spams :


thickspline version


pr_thickspline version

Had to adjust the settings for my version since it looked quite thinner than David’s. It is also a lot slower…

2 Likes

3D image, not mesh. Images in G’MIC are 3D arrays of fixed size array. w,h,d,s,expression code will show that. This can enable varied thickness and effect with crop() and draw()

@prawnsushi Interesting that your spline doesn’t have uniform thickness. It looks nice for the most part except for the segments that thicken too much near the ends due to high curvature. David’s demo is what I would expect from a generic G’MIC command however.

@Reptorian Showing or linking an example of a 3d approach may be instructive.

One additional feature may be to have an option to include a fill and outline for the spline now that it has thickness.

It’s because the starting and ending coords for all splines are the same, so it thins out at the extremities. Maybe I’ll add the option to change this. My version is quite flawed actually but it looks nice to me :man_shrugging:

I meant to say that it looks good and I like it, except for the sections with high curvature, but as a generic command, David’s is more practical.

Well thanks, to be honest I’m even surprised that it works at all…

I see, I haven’t used images as arrays yet.

Splendid party. Here’s my ticket:

calispline.gmic

===

ribbons :
   ribboncnt=48
   -input 1024,1024,1,3,lerp([50,60,165],[65,140,225],y/(h-1))
   -name. canvas
   (\
    40,70,{cexp([log(1000),-50°])};\
    512,450,{cexp([log(2000),35°])};\
    870,13,{cexp([log(1000),35°])}^\
    130,970,{cexp([log(1000),-35°])};\
    490,600,{cexp([log(2000),-135°])};\
    1015,950,{cexp([log(1000),10°])}\
    )
   -name. boundrypts
   -resize[boundrypts] 100%,$ribboncnt,100%,100%,5
   -repeat $ribboncnt
      -calispline[canvas] {crop(#$boundrypts,0,$>,0,0,4,1,1,1)},\
                          {crop(#$boundrypts,0,$>,0,1,4,1,1,1)},\
                          8,1,45,1,\
                          {235+15*$>/($ribboncnt-1)},\
                          {100-35*$>/($ribboncnt-1)},\
                          {25+225*$>/($ribboncnt-1)}
   -done
   -keep[canvas]

#@cli calispline : x0[%],y0[%],u0[%],v0[%],x1[%],y1[%],u1[%],v1[%],_rad0,rad1,_ang,_opacity,_color1,...
#@cli : Draw specified colored calligraphic line, tracing an underlying cubic
#@cli : hermite spline, on selected images. Default values: 'rad0=rad1=3',
#@cli : 'ang=0', 'opacity=1' and 'color1=0'.
#@cli : $ image.jpg repeat 30 { calispline {u(100)}%,{u(100)}%,{u(-600,600)},{u(-600,600)},{u(100)}%,{u(100)}%,\
# {u(-600,600)},{u(-600,600)},0.6,255 }
calispline : skip ${9=3},${10=3},${11=0},${12=1},${13=0}
  e[^-1] "Draw a calligraphic spline from ($1,$2) tangent: [$3,$4] to ($5,$6) tangent: [$7,$8] on image$?, "\
   "with pen shape: $9-$10, angle $11, opacity: $12 and color (${13--1})."
  foreach {
    x0={if(${"is_percent $1"},$1*(w-1),$1)}
    y0={if(${"is_percent $2"},$2*(h-1),$2)}
    u0={if(${"is_percent $3"},$3*(w-1),$3)}
    v0={if(${"is_percent $4"},$4*(h-1),$4)}
    x1={if(${"is_percent $5"},$5*(w-1),$5)}
    y1={if(${"is_percent $6"},$6*(h-1),$6)}
    u1={if(${"is_percent $7"},$7*(w-1),$7)}
    v1={if(${"is_percent $8"},$8*(h-1),$8)}
    r0,r1,ang,opac=${9-12}
    eval ${-mysplines}"calispline(#0,["$x0","$y0"],["$u0","$v0"],["$x1","$y1"],["$u1","$v1"],$r0,$r1,$ang,$opac,[${13--1}])"
  }

mysplines :
   status "
   calispline(ind,P0,T0,P1,T1,rad0,rad1,ang,opacity,color) = ( # Draw calligraphic line around spline P0-P1 with tangents T0,T1 on image #ind
    unref(_ds_color);
    unref(_ds_mask);
    _P0 = P0;
    _P1 = P1;
    _rad0 = rad0;
    _rad1 = rad1;
    _ang = ang;
    _opacity = opacity;
    _ds_color = resize(color,s#ind)*=abs(_opacity);
    _omopacity = 1 - max(_opacity,0);
    _C = hermite_coef(_P0,T0,P1,T1);
    _dt = _dtmin = 1/max(abs(_P1 - _P0));
    _P0 = inf;
    for (_t = 0, _t<=1, _t+=_dt,
      _P = round(mul([_t^3,_t^2,_t,1],_C,2));
      _dP = abs(mul([3*_t^2,2*_t,1,0],_C,2));
      _dt = min(_dtmin,0.75/max(_dP));
      if (_P0!=_P,
        ellipse(#ind,_P[0],_P[1],_rad0,_rad1,ang,_opacity,_ds_color);
      );
      _P0 = _P;
    );
    nan;
  );
  
  hermite_coef(P0,T0,P1,T1) = ( # Given location/tangent pairs, return coefficients of cubic interpolating polynomial in Hermite form
      C=mul([ 2,-2,1,1,-3,3,-2,-1,0,0,1,0,1,0,0,0 ],[ P0,P1,T0,T1 ],2);
  );
"

===

===

$ gmic -command calispline.gmic -ribbons -output. ribbons.jpg,80

===

Remarks:

  1. splinethicksplinecalispline: The latter two are near identical variants of the first. ‘spline’ plots pixels as it goes along. With ‘thickspline’, @David_Tschumperle plots an entire ellipse instead, with radius_1=radius_2=thickness/2 and angle equals zero; this garners the constant thickness spline stroke. calispline goes just a tiny step further, allowing the major and minor ellipse radii to be set, along with the angle. That gives us @prawnsushi 's calligraphic effect, practically for no cost at all.
  1. math_lib syntax: math_lib syntax is just that of Mathematical expressions, no more, no less. So, “squeezing a repeat” inside of eval is just taking one of the math expression looping constructs — take your pick. In fact, (pro tip!!!) there is nothing special at all about math_lib. It is just a G’MIC custom command that returns a string as its status. So:
   eval ${-foo}"do_foo(3+7)"

is a kind of, sort of, an inclusion mechanism, operating somewhat like the ‘C/C++’ preprocessor ‘#include’ directive. foo is a GMIC custom command that returns a string in its status, i. e.:

foo:

   u "My name is foo! How do you do?!?"

which is fine, but it would probably be more useful for our purposes if the string that foo returns is a syntactically proper G’MIC math expression that defines a do_foo macro. When the G’MIC command line interpreter encounters this construct: ${-foo}, it evaluates the pipeline expression within the curly braces. -foo executes and returns a (hopefully syntactically correct math expression) string. G’MIC concatenates this returned string with whatever follows, if anything. The concatenated string then becomes the argument for eval, whose lot in life is to evaluate mathematical expressions, either once (when it has no selection decorator), or in iteration over the pixmaps of selected images — like fill but without the implicit assignment of expression results to pixels.

The takeaway is that you can write your own math_libs. They may be either modified sections of math_lib or whole cloth inventions. In many cases, you would rather not include math_lib. It embodies a Big Math Expression String. Do you really want to be JIT compiiling that big Math Expression string within a long-running inner loop? Didn’t think so. So you take as small a piece as possible and just include that. And then you can modify that little piece to do fiendish tricks and not worry about breaking math_lib. Take a look at the implementation of the custom command mysplines in calispline.gmic for a more practical example.

  1. What does {cexp([log(1000),-50°])} do?: It is a vector in radial form; with it, I can write the length of the vector and the angle it has with respect to a reference direction, and cexp() resolves it into rectangular components; that way I can write tangent vectors for hermite cubics in an intuitive way:
$ gmic run 'rect={cexp([log(1000),-50°])}'
[gmic]-0./ Start G'MIC interpreter.
[gmic]-0./run/__run/ Set local variable 'rect=642.78760968653921,-766.04444311897782'.
[gmic]-0./ End G'MIC interpreter.
  1. Semi-opaque ellipses that overlap invariably create more opaque patches on the overlaps. I cannot see a way around that so long as we are plotting ellipses, one-by-one, directly. I suppose one could consider rendering at full opacity to a transparent intermediary image, then alpha-blend the intermediary, at the desired opacity, to a base plate. Further along, I’ve been contemplating capturing the hull of the pen’s traversal as it proceeds along the spline. Rather than paint/plot at every Δt, capture the offset points in a dynamic array. Then pass the hull in just one call to polygon(), in fill mode, which renders the entire stroke in CImg. That would be pretty darn quick, me thinks.
3 Likes

@patdavid why isn’t it possible to mark a post with a double or triple like? :stuck_out_tongue:

1 Like

:bowing_man: Great! I hope it will be included in G’mic!
- Throws pr_thickspline in the trash bin -
Cannot wait to try this out on this :


G’micing on the beach

@David_Tschumperle Feeling déjà-vu here? :wink:

Please, do it? I’m not a dev, so… :stuck_out_tongue: But can polygon do smooth curves too?

You can… if you create 2 more accounts :slight_smile:

I ended up wirth this the other day :
eval it=0;${-math_lib}"repeat ( 100,spline(#0,["$x0-$it","$y0-$it"],["$u0","$v0"],["$x1-$it","$y1-$it"],["$u1","$v1"],$10,[${11--1}]);it+=10 )"
It’s probably wrong but it doesn’t throw an error either… repeat works but it+=10 does not increment here. So i’m still a bit in the dark :man_with_probing_cane:

Easy peasy.

arabesque
Plotted in one polygon() call. See spectralarabesque.gmic for code. If you have the time, read the Beginner’s Cookbook article: Arabesques for the whole backstory.

Recall that mathematical expressions evaluate in a separate environment to pipeline processing; both environments have their own systems of declaring and referencing variables; the syntax differs between the two systems. The two systems do not intersect; they never operate concurrently in one thread. Here it=0; initializes a math expression variable that is spelled like “it”. It has nothing to do with the pipeline processing variable “$it”. Regarding math expressions, as a preprocessing step, the G’MIC pipeline processor scans math expressions and replaces all pipeline processing variables within with their de-referenced values, as obtained via the $ operator. If the pipeline processing variable it has never been initialized in an upstream pipeline statement, then $it resolves to an empty string. Later on, when when the pipeline processor invokes the separate mathematical expression parser, all of the $it references have been replaced by their values; the mathematical expression parser never sees any string that looks like $it; it sees the value instead. That value could be an empty string. Entirely unrelated, there is a mathematical expression variable, it, that was initialized to zero and is incremented in the repeat loop, but it does not appear anywhere else in the mathematical expression, so this is for naught. Probably, that is not what you want. See the Substitution Cheatsheet for further discussion on the interplay between pipeline and math expression parsers.

1 Like

Thanks, it works now that i’ve removed the $s :slight_smile:
But since my version is not needed i’m trying to go another way:

script
#@cli pr_thickspline : x0[%],y0[%],u0[%],v0[%],x1[%],y1[%],u1[%],v1[%],_spacing,_opacity,_color1,...
#@cli : Draw specified colored thick spline curve on selected images (cubic hermite spline).
#@cli : Default values: 'spacing=2', 'opacity=1' and 'color1=0'.
#@cli : $ 400,400,1,3 repeat 100 thickspline {u([w,h,1000,1000,w,h,1000,1000,10])},0.5,${-rgb} done
 e[^-1] "Draw thick spline ($1,$2) - ($5,$6) on image$?, with thickness $9, opacity $10 and color (${11--1})."
pr_thickspline : skip ${9=2},${10=1},${11=0}
  if $9<2 spline ${1-8},${10--1}
  else foreach {
    x0={if(${"is_percent $1"},$1*(w-1),$1)}
    y0={if(${"is_percent $2"},$2*(h-1),$2)}
    u0={if(${"is_percent $3"},$3*(w-1),$3)}
    v0={if(${"is_percent $4"},$4*(h-1),$4)}
    x1={if(${"is_percent $5"},$5*(w-1),$5)}
    y1={if(${"is_percent $6"},$6*(h-1),$6)}
    u1={if(${"is_percent $7"},$7*(w-1),$7)}
    v1={if(${"is_percent $8"},$8*(h-1),$8)}
    # th={round($9*0.5,1,1)}
    th=$9
      eval
      it=0;${-math_lib}"repeat ( 10,spline(#0,["$x0-it","$y0-it"],["$u0-it*10","$v0+it*10"],["$x1-it","$y1-it"],["$u1-it*10","$v1+it*10"],$10,[${11--1}]);it+=$th )"
  } fi

1 Like

Always good to go off in orthogonal directions, if for no other reason than to see where you’ve been from a new perspective.

So — in that spirit — lets revisit Ribbons:

splinefun.gmic

===

ribbons:
   # Mediumres, 235 curves, 50% opacity and complex...
   ccnt=235
   -input 1024,1024,1,3,lerp([20,17,38],[33,38,46],y/(h-1))
   -name canvas
   (252,233,59^115,22,210^22,210,186)
   -permute. cyzx
   -resize. $ccnt,1,1,3,3
   -name palette
   -splinefun[canvas] [palette],128,5,3,65,0.5
   -keep[canvas]
   -text_outline. "Mediumres,\ 37 curves,\ 50% opacity\ and\ complex...",3%,90%,3%,1,1,255,240,200
   -text_outline. "Spline\ Fun:\ 128,5,3,65,0.5",3%,95%,3%,1,1,255,240,200
   -output[canvas] /dev/shm/ribbons_00.png
   -rm

   # Mediumres, 37 curves, opaque and simple...
   ccnt=37
   -input 1024,1024,1,3,lerp([20,17,38],[33,38,46],y/(h-1))
   -name canvas
   (252,233,59^115,22,210^22,210,186)
   -permute. cyzx
   -resize. $ccnt,1,1,3,3
   -name palette
   -splinefun[canvas] [palette],64,3,9,65,1
   -keep[canvas]
   -text_outline. "Mediumres,\ 37 curves,\ opaque\ and\ simple...",3%,90%,3%,1,1,255,240,200
   -text_outline. "Spline\ Fun:\ 64,3,9,65,1",3%,95%,3%,1,1,255,240,200
   -output[canvas] /dev/shm/ribbons_01.png
   -rm

   # Coarse, 8 curves, 25% opacity and simple...
   ccnt=8
   -input 1024,1024,1,3,lerp([20,17,38],[33,38,46],y/(h-1))
   -name canvas
   (252,233,59^115,22,210^22,210,186)
   -permute. cyzx
   -resize. $ccnt,1,1,3,3
   -name palette
   -splinefun[canvas] [palette],16,2,24,15,0.25
   -keep[canvas]
   -text_outline. "Coarse,\ 8 curves,\ opaque\ and\ simple...",3%,90%,3%,1,1,255,240,200
   -text_outline. "Spline\ Fun:\ 16,2,24,15,0.25",3%,95%,3%,1,1,255,240,200
   -output[canvas] /dev/shm/ribbons_02.png
   -rm

   # Hires, 256 curves, translucent and simple...
   ccnt=256
   -input 1024,1024,1,3,lerp([20,17,38],[33,38,46],y/(h-1))
   -name canvas
   (252,233,59^115,22,210^22,210,186)
   -permute. cyzx
   -resize. $ccnt,1,1,3,3
   -name palette
   -splinefun[canvas] [palette],256,3,3,45,0.125
   -keep[canvas]
   -text_outline. "Hires,\ 256\ curves,\ translucent\ and\ simple...",3%,90%,3%,1,1,255,240,200
   -text_outline. "Spline\ Fun:\ 256,3,3,45,0.125",3%,95%,3%,1,1,255,240,200
   -output[canvas] /dev/shm/ribbons_03.png
   -rm

   # Very hires, 512 curves, very translucent and complex...
   ccnt=512
   -input 1024,1024,1,3,lerp([20,17,38],[33,38,46],y/(h-1))
   -name canvas
   (252,233,59^115,22,210^22,210,186)
   -permute. cyzx
   -resize. $ccnt,1,1,3,3
   -name palette
   -splinefun[canvas] [palette],640,8,3,45,0.125
   -keep[canvas]
   -text_outline. "Very\ hires,\ 512\ curves,\ translucent\ and\ complex...",3%,90%,3%,1,1,255,240,200
   -text_outline. "Spline\ Fun:\ 640,8,3,45,0.125",3%,95%,3%,1,1,255,240,200
   -output[canvas] /dev/shm/ribbons_04.png
   -rm
   
#@cli splinefun : [palette],curve_plot_count,complexity_level,thickness,pen_width,pen_angle
splinefun: -check ${"is_image_arg $1"}
   -check "${2=64}>0              && \
           ${3=3}>0 && ${3}<12    && \
           ${4=4}>2               && \
           ${5=45}                && \
           ${6=0.25}>=0 && ${6}<=1"
   -pass$1 1
   -name. palette
   foreach[^palette] {
       nm={0,n}
      -name. canvas
      -pass$1
      -name. palette
      crvcnt={w#$palette}
      res=$2
      lvl={2^$3}
      thick=$4
      ang={deg2rad($5)}
      opacity=$6
      -input $lvl,1,1,4,[u($crvcnt),u($res),u(-1,1),u(-1,1)]
      -rbf. $crvcnt,$res
      -name. plotcoords
      -sub[plotcoords] {ia}
      -mul[plotcoords] {1.75/(iM-im)}
      -fill[plotcoords] "begin(
                                sw=min(w#$canvas,h#$canvas)/2;
                                id=eye(3);
                                id[0]=sw;
                                id[2]=sw;
                                id[4]=-sw;
                                id[5]=sw
                              );
                              (id*[I(x,y),1])[0,2]"
      -permute[plotcoords] cyzx
      -split[plotcoords] c
      lc=0
      -name[^canvas,palette] plot
      -foreach[plot] {
          -name. plots
          +mirror[plots] y
          -permute. cyzx
          -fill. begin(OFS=cexp([log($thick),$ang]));I(#-1,x,y)+OFS
          -permute. cyzx
          -append[-2,-1] y
          -pass[canvas]  1
          -pass[palette] 1
          -eval "PV=crop(#$plots);
                 polygon(#$canvas,size(PV)/2,PV,$opacity,I(#$palette,$lc,0))"
          -rm
          lc+=1
      }
      -keep[canvas]
      -name[canvas] $nm
   }
   -rm[palette]

===

So. In keeping with directions orthogonal, this Ribbons implementation (splinefun.gmic) eschews spline(), thickspline(), calispline() or any other variant layered on the backs of hermite cubic polynomials, which G’MIC’s spline() function embodies. Rather, this implementation favors a mathematical trope for a type of metal used in making clock springs.

Imagine a lumpy surface which cannot be directly seen because a plate of thin, springy sheet metal lies on top of it. Rivets attach the plate to the underlying surface at random points; otherwise the thin metal sheet more-or-less follows the underlying surface in a manner that minimizes the forces needed for bending.

G’MIC’s rbf command has, for its lot in life, the task of estimating the locations of points on the thin metal sheet, given the placements of a handful of rivets. Unless told otherwise, rbf employs the thin plate spline radial basis function, {r^2}\ln(r) , to establish the likelihood of deflection of a point on the sheet metal in the neighborhood of a rivet, with r>0 , the radial measure of separation. For very small r, regions close to rivets, the likelihood of deflection is minuscule — even negative! — because it is very, very! hard to bend a short scrap of sheet metal. On the other hand, for larger r, the sheet metal accommodates greater deflections.

And so it goes: for most locations, rbf estimates surface positions by assessing the locations of surrounding rivets, all scattered in different directions. In some locations, a nearby rivet dominates; other places may fall under the sway of numerous rivets, some near, others far, none dominating. In all cases, rbf runs with the minimum energy trope. Whatever the position of a given sheet metal point might be, it reflects a ‘minimum energy solution’ — the least deflection needed to obtain that position.

In this setting, the profiles of tracks running along the surface of such thin metal sheets assume lovely curves. They are not cubic, but infinitely differentiable — that is, rates of change, rates of change layered upon rates of change, ad infinitum… may be all smooth curves; cubic smoothness cuts off after the second go. It is simplistic to attach greater aesthetic appeal to infinitely differentialble surfaces, but some find it so — and, in any case, we seek paths orthogonal to the wonted spline(). Away we go.

The heart of ribbons, version two, is splinefun, and at the heart of splinefun is a handful of points — coefficients, now, not rivets — few in number and conjured up from the aether (they were made up). rbf calculates the least energy surface which embeds these coefficients. This calculation produces a two channel image — plotcoords — the minimum energy surface — well, actually, two: one for x coordinates, the other for y. The two minimum energy surfaces furnish parametric arguments for plotting curves on other surfaces: canvases. Mechanics follow.

  1. Each column of plotcoords embodies the (x, y) plots of a curve on canvas.
  2. The width of plotcoords yields the number of curves. Wider ⇒ more curves.
  3. The height yields the number of curve data points. Higher ⇒ smoother curves.
  4. Thickness arises from an imagined calligraphic pen following the curve on canvas.

That last point recommends further remarks. The pen has a width and an orientation at some angle to the curve’s course. These data allow an offset given by the pen tip’s width and its orientation. The mechanics duplicate curve data points and offsets them. The exigencies of polygon(), G’MIC’s polygon plotter, calls for a sub-path outward and a sub-path back. To meet this necessity, duplicate the curve data set and reverse it, offsetting it by the width and orientation of the pen. These mechanics gives polygon() a closed hull that may be filled by a semi-transparent color. It is uniform, not following from the stamping of semi-transparent shapes at each curve data point, with the collateral overlap of shapes leading to a variation in opacity.

Other matters — calculating the minimum energy surface in a parametric space, then translating such to the canvas, coloring curves in a graduated fashion — are off in the weeds and the details may be drawn from the script. Following, then, are various sets of ribbons plotted in the minimum energy style.

   gmic ccnt=37                                              \
   -input 1024,1024,1,3,lerp([20,17,38],[33,38,46],y/(h-1))  \
   -name canvas                                              \
   (252,233,59^115,22,210^22,210,186)                        \
   -permute. cyzx                                            \
   -resize. $ccnt,1,1,3,3                                    \
   -name palette                                             \
   -splinefun[canvas] [palette],64,3,9,65,1                  \

The above illustrates a few coarse strokes of a broad-tipped pen , 24 pixels, angled at 15°, each stroke at 25% opacity.

Complexity — splinefun’s third parameter, is 5 2^5 = 32 randomly positioned coefficients, which engenders a more complex surface, and, following on, more twisty curves on canvas.

Many very nearly transparent curves gives rise to the illusion of weirdly illuminated surfaces. At the end of the day, we are plotting discrete pixels, like it or not. With closely spaced curves, this reality brings about Moiré patterns.

More transparent curves, following more complicated courses, minimizes — but does not eliminate — such shortcomings. In other approaches, render large, then reduce to small: an anti-aliasing method for those with the memory chips installed.

That’s it for now. Have fun.

3 Likes

Wonderful Garry!

That’s a lot of work :open_mouth:
I don’t understand everything (yet?) but it looks nice.
I’ve just tried it but i don’t really know how to set the x,y coordinates of the curve(s) points…

This generates one random curve :
1400,1400,1,3,0 +fc 55,128,128 splinefun[0] [1],10,2,30,45,0.1

How can I “control” it?