(Trying to create) ThickSpline

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