The American Bank Note Company Filigree Machine

Happy New Year, all. A belated gift:

rose_03

which should find its way into Testing > G’MIC Tutorials in the sweet by-and-by.

Just shy of a year ago, Tutorial Land sprouted Arabesques, which was a lot of fun to write. Alas, it was fairly clear at the time that there was little in the way of playing with arabesques in an intuitive way: coming up with wheelie radial, angular displacement, and spin frequency was a tad obtuse. There was a place for a G’MIC plug-in to bridge the gap. Days short of the article’s first anniversary, here it is. It amused me to pay homage to the American Bank Note Company, a fixture of Wall Street of old from the mid-nineteenth century to the first few decades of the twentieth. The filigrees which they applied to the borders of stock certificates and sundry fungible papers of record reached astounding levels of complexity. I stand in awe of the abilities of those engravers of yesteryear.

The key feature are wheelies that can be dragged around, with the preview providing immediate feedback of the kind of arabesque that can be obtained. Results can be obtained quite suddenly when frequencies of spinning wheelies start resonating. Once a pleasing pattern seems at hand, scroll down to pen settings and experiment with those.

Here is a gallery of sample runs and the parameters which produced them:

rose_01

rose_02

palmette_01

palmette_03

filigree_01

Have fun!

6 Likes

Hello Garry! Thanks ! That looks really interesting.

The Pen Settings does not seem to work for me :

Do you know why ?

1 Like

TL;DR:
In the interest of speed, the interactive preview display only incorporates a single pixel line width.

Read at/after breakfast:
Pen settings have no effect on the preview display. In the interest of displaying images rapidly, the plug-in preview is driven primarily by polygon() which has no intrinsic notion of line width — but goes about its business very quickly.

Pen settings come into play when applying a final render (Apply or OK). In that setting, the rendering agent is thicklines. I iterate over the plotting data by plot-point pairs, determining the line thickness for the pair by dotting the orientation of that pair against a global direction (Pen tilt), which obtains a local “shrinkage factor” that can be applied to the line width just at that segment. Pen Shape moderates the extremeness of this dotting approach. As Shape approaches 100, the shrinkage factor is phased out and a constant, unvarying line width prevails for all segments (lerp() behind the scenes). All of this is well-and-good, but is too elaborate for a fast, interactive display scheme.

Polygon() Question

driven primarily by polygon() which has no intrinsic notion of line width

I did entertain an approach of plotting an envelope for polygon() with an outward edge suitably offset from a return edge so as to obtain a calligraphic effect that would appear in the interactive display. I went about rendering such an ensemble with polygon() in fill mode. Alas, when such a polygon self-intersects, such that the polygon overlays itself, it is rendered in the clear. This made the calligraphic effect incomprehensible for complex arabesques. I understand that is a feature, not a bug; you are following a particular winding-rule policy. Is there some manner to change winding rule policy such that regions overlapping in self intersecting polygons are not rendered in the clear? Am I overlooking some winding rule flag? Or would that be a parameter appearing in some future version of polygon()?

1 Like

Thanks for the detailed explanation.

I’ve looked at the code of the polygon command, and this won’t be easy to add an option for changing the “flipped” drawing behavior.
Anyway, I think that tracing a path with a “rotating” brush as you’d like to do in your case could be done in reasonnable time, using a custom math expression.
I may propose something to do that later today.

1 Like

Maybe something like this:

foo :
  srand 11 8,1,1,4,"u(1000)" s. c,2 rbf[-2,-1] 1000 a c n. 20,780  # [x,y] coordinates of the curve
  800,800 # Canvas to draw the curve on

  eval.. " # Draw the curve with specified thickness
    const thickness = 3;
    x>0?(
      pP = J[-1];
      P = I;
      T = unitnorm(P - pP);
      N = [ -T[1], T[0] ]*thickness;
      A = pP - N; B = pP + N;
      C = P - N; D = P + N;
      polygon(#-1,4,A[0],A[1],B[0],B[1],D[0],D[1],C[0],C[1],1,1);
    )"

EDIT: For your filter, forget about the x>0 part, as the curves are periodic, you can just set const boundary = 2 for reading the previous point pP.

Sorry, this does not work well when thickness is large and curvature is locally high.
Working on a fix…

Here is a corrected version, with tilt option included:

foo :
  srand 11 8,1,1,4,"u(1000)" s. c,2 rbf[-2,-1] 1000 a c n. 20,780  # [x,y] coordinates of the curve
  800,800 # Canvas to draw the curve on

  eval.. "> # Draw the curve with specified thickness
    begin(
      const thickness = 10;
      const boundary = 1;
      const tilt = 45°;
      const tilt_strength = 0.75;
      A = B = [ 0,0 ];
      S = [ cos(tilt), sin(tilt) ];
    );
    pP = J[-1]; P = I;
    T = unitnorm(P - pP);
    N = lerp([ -T[1],T[0] ],S,tilt_strength)*thickness;
    C = round(P - N); D = round(P + N);
    x>0?polygon(#-1,4,A[0],A[1],B[0],B[1],D[0],D[1],C[0],C[1],1,1);
    A = C; B = D"

1 Like

The funny thing is that this piece of code can be used as a signature generator :stuck_out_tongue: !

View Code

Here is the code:

foo :
  repeat 25 {
    {round(u(4,16))},1,1,4,"u(1000)" s. c,2 rbf[-2,-1] 1000 a[-2,-1] c n. 20,780  # [x,y] coordinates of the curve
    800,800 # Canvas to draw the curve on

    eval.. "> # Draw the curve with specified thickness
      begin(
        const boundary = 1;
        thickness = u(3,10);
        tilt = u(75)°;
        tilt_strength = u(0,1);
        A = B = [ 0,0 ];
        S = [ cos(tilt), sin(tilt) ];
      );
      pP = J[-1]; P = I;
      T = unitnorm(P - pP);
      N = lerp([ -T[1],T[0] ],S,tilt_strength)*thickness;
      C = round(P - N); D = round(P + N);
      x>0?polygon(#-1,4,A[0],A[1],B[0],B[1],D[0],D[1],C[0],C[1],1,1);
      A = C; B = D"
    r2dx. 30%
    rm.. autocrop. n. 0,255 r. 250,250,1,1,0,0,0.5,0.5 negate.
    frame. 1,1,0
  }
  append_tiles ,
3 Likes

I think I have to create a new command curve that draws a parameterized curve on selected images. I’m starting to do that.

2 Likes

Chicken scratch from a doctor’s note :stuck_out_tongue:


@grosgood Animation necessary :wink:

2 Likes

Hi,

Will we be able to control the curve like the spline command?

OK, so I’ve written a first draft for the function curve:

$ gmic h curve

  curve:
      [xy_coordinates],_thickness>0,_tilt,_tilt_strength[%],_is_closed={ 0:no | 1:yes },_opacity,_color1,...

    Draw specified parameterized curve on selected images.
    Arguments are:
     * '[xy_coordinates]' is the set of XY-coordinates of the curve, specified as a 2-channels image.
     * 'thickness' is the thickness of the drawing, specified in pixels.
     * 'tilt' is an angle, specified in degrees.
     * 'tilt_strength' must be a float value in [0,1] (or in [0,100] if specified as a percentage).
     * 'is_closed' is a boolean which tells if the curve is closed or not.

    Default values: 'thickness=0', 'tilt=45'

should be available in the stdlib in a few moments. It’s probably a bit buggy, as it appeared to me it’s not that easy to draw a curve with a given thickness correctly in an image.

Yes, definitely, the curve points are actually defined in a 2-channels image, so you can put anything you want there.


Signature generator:

With the new function, the code of the previous signature generator becomes quite simple:

foo :
  repeat 25 {
    {round(u(4,16))},1,1,4,"u(1000)" s. c,2 rbf[-2,-1] 1000 a[-2,-1] c n. 20,780  # [x,y] coordinates of the curve
    800,800,1,1,255 # Canvas to draw the curve on
    curve. ..,{u(6,20)},{u(75)},{u},0
    r2dx. 30%
    rm..
    autocrop. n. 0,255 -. 255 r. 250,250,1,1,0,0,0.5,0.5 +. 255 frame. 1,1,0
  }
  append_tiles ,

And as I said, this command has bugs :

foo :
  16,1,1,2,"ang = lerp(0,360°,x/w); [ cos(ang),sin(ang) ]"
  n. 40,740
  800,800,1,3 curve. ..,10,0,0,1,1,255,0,128
  eval.. "ellipse(#-1,i0,i1,6,6,0,0.5,255); run('t ',x,',',i0,',',i1 + 8,',18,1,255');"

I’ll try to fix that tonight!
EDIT: Fixed!

1 Like

New filter Rendering / Random Signature has been added to the plug-in! :slight_smile:

1 Like

Me trying to tame the beast with turbulence, plasma, noise, cut, blur BUT NO RBF. I don’t get much curves but i still like the angry scribbles (Last one is kinda weird) :slight_smile: :

Show










3 Likes

Radial Basis Functions (rbf)! If @David_Tschumperle wants to draw my attention to a singularly obscure G’MIC code passage, he only has to throw in a rbf call. Works like a charm. My obsessions are too well known here.

S = [ cos(tilt), sin(tilt) ];

An aside: Leonhard Euler said I could do this:

foobar : skip ${1}
    ang:=$1°
    eval " const tilt=$ang;
           S = [ cos(tilt), sin(tilt) ];
	   print(S);
	   U = cexp([0,tilt]);
	   print(U)"

A nifty way of getting sine/cosine pairs, without explicit calls to the same.

gosgood@bertha ~ $ gmic -m /dev/shm/foobar.gmic foobar 57
[gmic]./ Start G'MIC interpreter (v.3.3.3).
[gmic]./ Import commands from file '/dev/shm/foobar.gmic', with debug info (1 new, total: 4711).
[gmic_math_parser] S = (uninitialized) (mem[37]: vector2)
[gmic_math_parser] U = (uninitialized) (mem[43]: vector2)
[gmic_math_parser] S = [ 0.5446390350150272,0.83867056794542394 ] (size: 2)
[gmic_math_parser] U = [ 0.5446390350150272,0.83867056794542394 ] (size: 2)
[gmic]./ End G'MIC interpreter.
gosgood@bertha ~ $ gmic -m /dev/shm/foobar.gmic foobar -134
[gmic]./ Start G'MIC interpreter (v.3.3.3).
[gmic]./ Import commands from file '/dev/shm/foobar.gmic', with debug info (1 new, total: 4711).
[gmic_math_parser] S = (uninitialized) (mem[37]: vector2)
[gmic_math_parser] U = (uninitialized) (mem[43]: vector2)
[gmic_math_parser] S = [ -0.69465837045899703,-0.71933980033865141 ] (size: 2)
[gmic_math_parser] U = [ -0.69465837045899703,-0.71933980033865141 ] (size: 2)
[gmic]./ End G'MIC interpreter.

Thanks be to Euler’s Identity.

   S = [ cos(tilt), sin(tilt) ];
…
   pP = J[-1];
   P  = I;
   T  = unitnorm(P - pP);
   N  = lerp([ -T[1],T[0] ],S,tilt_strength)*thickness;
   C  = round(P - N);
   D  = round(P + N);
…

Now. This is a gem. But — I must confess — I didn’t tumble onto it immediately. Dotless (crossless) calligraphy? Had to draw a little diagram:

dtdiscuss

In the routine case, your approach offsets points C and D along courses parallel to vectors ±N. Depending on the blending parameter tilt_strength, this orients +N somewhere in the midsts of the pink arc, being — segment by segment — oriented through a mix between the globally fixed direction S and the locally varying perpendicular vectors ±T. –N obediently follows along 180° out of phase.

Points C and D are offset at most by the distance 2×thickness, when ±N are perpendicular to the segment P—Pp and coincident (parallel) to the perpendiculars ±T. That particular case arises when (a.) the user has set tilt_strength to the blending extreme of zero, so that the vectors ±N are, in practice, aliases of perpendiculars ±T, the direction S being blended out entirely. By definition, ±T are always perpendicular to the segment P—Pp, their orientations are invariant with respect to the segment, so, segment-by-segment, C and D are always offset by the same distance from segment P—Pp. This is the “non-calligraphic case”. I could be drawing with a felt pen that has a circular cross section.

Then there is case (b.). The user has set tilt_strength to the other blending extreme of one, so that the vectors ±N always coincide with globally invariant S. Should a particular segment, P—Pp, run perpendicular to S then the situation is cut from the same cloth as (a.), in that vectors ±N parallel ±T and those circumstances offset points C and D to the maximum separating distance of 2×thickness. That obtains the maximum calligraphic width. The minimum arises on those occasions when the orientation of P—Pp is exactly that of S. ±N is locked to S, so is coincident with P—Pp. Points C and D extrude along the length of P—Pp, so the green quadrilateral becomes, perhaps, a triangle, if the priors A and B had non-zero perpendicular offsets, or a line otherwise. In any case, we witness the minimum calligraphic width. In the general run of affairs ±N coincides with a blended direction betwixt the orientations of S and ±T and exhibits the blended behavior of both. It is, all and all, a nice bit of work. Permit me to admire it for a spell.

Now, what sets me to glancing out the window and sighing a bit is this affair of feeding polygon() over-and-over-and-over again, four-point data sets per throw, for hundreds of times. That makes me a bit sad. You are a good deal better acquainted with its internals than I am, so, perhaps my performance worries regarding polygon() are founded on sandy substraits, but in the run-up to the Arabesques article last year, I learned that if I could fashion up some way to feed polygon() an entire curve plotting data set in one polygon() call, it performed roughly 2× to 3× faster over plotting points pairs at a time.

But, of course, perhaps that is the rent to pay for getting around the self-intersection quibble: hundreds of tiny quadrilaterals instead of one big one (that self-intersects). In any case, it is probably cheaper than hundreds of calls to thicklines, which happened to be the first thing I saw on the shelf yesterday afternoon.

Now I see that you have posted a bevy of new things whilst I’ve been working my way through this. Perhaps I should wait a bit before posting this.

Then again, perhaps not.

1 Like

Hi Garry, thanks for the really detailled analysis of what I’ve done :slight_smile:
When I read it like that, I almost feel it was clever of me :slight_smile:

And in the end you raised an important point that gave me an idea for improvement:

Indeed, a single call to polygon() (with lot of points) will be way faster than multiple calls of “smaller” polygon() (with fewer points, here, 4).
But, I forgot that there is one case where polygon() is fast : triangles.
And in the case of curve, it’s definitely better to draw 2 triangles rather than 1 quadrangle, because we know we don’t want the quadrangle ABDC to be self-interesting.
So in my command curve, I’ve replaced the single call to polygon() (for the quadrangle ABDC), by two calls of polygon() (for the triangles ABC and DBC), and that’s faster at the end!

For drawing 10000 times the same curve on an image:

$ gmic sp bottles srand 3 16,1,1,4,u s. c,2 rbf[-2,-1] 1000,0,1 n[-2] 10,{w#0-10} n[-1] 10,{h#0-10} a[-2,-1] c v 0 tic repeat 1000 curve.. .,6,0,0,0,1,0,128,0 done toc
  • With one quadrangle : Elapsed time: 0.676 s
  • With two triangles: Elapsed time: 0.442 s

Now I think that 0.442s for drawing 1000 times a curve is already correct (I mean for G’MIC), without needing to worry about optimizing it further.

Oh, and yes, I use you cexp() trick as well :slight_smile:

Cheers.

1 Like

Better than a Spiralgraph, Garry. :slight_smile:

OK; what would be cool is to be able to stroke with brush; sort of faked it with stroke by selection in this case (and added rotated and darkened background to get a 3D orb result). :slight_smile:

And my apologies, Garry; just cannot help myself. :slight_smile:

3 Likes