summaryrefslogtreecommitdiff
path: root/doc/context/sources/general/manuals/ontarget/ontarget-metapost.tex
blob: 521b90b63ad97badb428fb6977ab24c2bfc1b8bd (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
% language=us runpath=texruns:manuals/ontarget

% Musical timestamp: while adding this to the engine and I listened to many John
% Medeski (Martin and Wood) concert videos, many several times ... it kept me in
% the flow!
%
% This wrapup was written with Dave Matthews Band - Seek Up - LIVE, 12/2/2018 in
% the background, as I like these live acts.
%
% This is just a summary from my point of view. A real article is written by
% Mikael Sundqvist who also explains how arctime and its companions actually
% work.

\startcomponent ontarget-metapost

\environment ontarget-style

\startchapter[title={To the point}]

In the 2022 \NTG\ \MAPS\ 53 there is a visual very attractive article about
generative graphics with \METAPOST\ by Fabrice Larribe. These graphics actually
use very little \METAPOST\ code that use randomized paths and points and the
magic is in getting the parameters right. This means that one has to process them
a lot to figure out what looks best. Here is an example of such a graphic
definition. I will show more variants so a rendering happens later on.

\startbuffer
\startMPdefinitions
    vardef agitate_a(expr thepath, S, n, fn, t, ft) =
        save R, nbpoints, noiselevel, rlength ;
        path R ; nbpoints := n ; noiselevel := t ;
        R := thepath ;
        for s=1 upto S :
            nbpoints := nbpoints * fn ;
            noiselevel := noiselevel * ft ;
            R := for i=1 upto nbpoints:
                point (i/nbpoints) along R
                    randomized noiselevel
                ..
            endfor cycle ;
        endfor ;
        R
    enddef ;
\stopMPdefinitions
\stopbuffer

\typebuffer[option=tex] \getbuffer

I will not explain the working of this because there is the article. Instead, I
will focus on something that came up when the \MAPS\ was prepared: performance.
Not only are these graphics large (which is no real problem) but they also take a
while to render (which is something that does matter when one wants to find the
best a parameters). For the first few variants we keep the same names of
variables as in the article.

\startbuffer[final:definition]
\startMPdefinitions
    vardef agitator(expr pth, iterations, points, pointfactor, noise, noisefactor) =
        save currentpath, currentpoints, currentnoise ; path currentpath ;
        currentpath   := pth ;
        currentpoints := points ;
        currentnoise  := noise ;
        for step = 1 upto iterations :
            currentpath := arcpointlist currentpoints of currentpath ;
            if currentnoise <> 0 :
                currentpath :=
                    for i within currentpath :
                        pathpoint
                        randomized currentnoise
                        ..
                    endfor
                cycle ;
            fi
            currentnoise  := currentnoise  * noisefactor ;
            currentpoints := currentpoints * pointfactor ;
        endfor ;
        currentpath
    enddef ;
\stopMPdefinitions
\stopbuffer

\startbuffer[final:graphic]
\startMPcode
  path pth ;
  nofcircles  := 15 ; iterations  := 10 ;
  points      := 10 ; pointfactor := 1.3 ;
  noise       :=  5 ; noisefactor := 0.8 ;

  nofcircles  :=  5 ; iterations  := 10 ;
  points      :=  5 ; pointfactor := 1.3 ;

% for c = nofcircles downto 1 :
%   pth    := fullcircle scaled (c * 6.5) scaled 3 ;
%   points := floor(arclength(pth) * 0.5) ;
%   pth    := agitator(pth, iterations, points, pointfactor, noise, noisefactor) ;
%   eofill pth
%     withcolor darkred
%     withtransparency(1,4/nofcircles) ;
%   draw pth
%     withpen pencircle scaled 0.1
%     withtransparency(1,4/nofcircles) ;
% endfor ;

% currentpicture := currentpicture xsized TextWidth ;

  for c = nofcircles downto 1 :
    pth    := fullcircle scaled (c * 6.5) scaled 3 ;
    points := floor(arclength(pth) * 0.5) ;
    pth    := agitator(pth, iterations, points, pointfactor, noise, noisefactor) ;
    draw pth
      withpen pencircle scaled 1
      withcolor (c/nofcircles)[darkgreen,darkred] ;
  endfor ;

  currentpicture := currentpicture xsized .5TextWidth ;
\stopMPcode
\stopbuffer

\startplacefigure
  [title={Fabrice's agitated circles, with reduced properties to keep this file small (see source).},
   reference=fig:agitated]
    \getbuffer[final:definition,final:graphic]
\stopplacefigure

In \in {figure} [fig:agitated] we show the (kind of) graphic that we are dealing
with. Such an agitator is used in a loop so that we agitate multiple circles,
where we go from large to small with for instance 4868, 4539, 4221, 3892, 3564,
3245, 2917, 2599, 2270, 1941, 1623, 1294, 966, 647 and 319 points. The article
uses a definition like below for the graphic where you can see the agitator being
applied to each of the circles.

\starttyping[option=mp,color=]
path P ; numeric NbCircles, S, nzero, fn, tzero, ft ;

randomseed   := 10 ;
defaultscale := .05 ;

NbCircles := 15 ; S := 10 ; nzero := 10 ; fn := 1.3 ; tzero := 5 ; ft := 0.8 ;

for c = NbCircles downto 1 :
    P := fullcircle scaled (c*6.5) scaled 3 ;
    P := agitate_a(P, S, nzero, fn, tzero, ft) ;
    eofill P
        withcolor transparent(1,4/NbCircles,col) ;
    draw P
        withpen pencircle scaled 0.1
        transparent(1,4/NbCircles,.90[black,col]) ;
endfor ;
\stoptyping

The first we noticed is that the graphics processes faster when double mode is
used: we gain 40--50\percent\ and the reason for this is that modern processors
are very good at handling doubles while \METAPOST\ in scaled mode has to do a lot
of juggling with pseudo fractions. In the timings shown later we leave that
improvement out. Also, because of this observation \CONTEXT\ \LMTX\ now defaults
its \METAPOST\ instances to method double.

When I stared at the agitator code I noticed that the \type {along} macro was
used. That macro returns a point at given percentage along a path. In order to do
that the macro calculates the length of the path and then locates that point. The
primitive operations involved are \type {arclength}, \type {arctime of} and \type
{point of} and each these takes some time to complete. A first improvement is to
inline the \type {along} and hoist the length calculation outside the loop.

\startbuffer
\startMPdefinitions
    vardef agitate_b(expr thepath, S, n, fn, t, ft) =
        save R, nbpoints, noiselevel, rlength ;
        path R ; nbpoints := n ; noiselevel := t ;
        R := thepath ;
        for s=1 upto S :
            nbpoints := nbpoints * fn ;
            noiselevel := noiselevel * ft ;
            rlength := (arclength R) / nbpoints;
            R := for i=1 upto nbpoints:
                (point (arctime (i * rlength) of R) of R)
                    randomized noiselevel
                ..
            endfor cycle ;
        endfor ;
        R
    enddef ;
\stopMPdefinitions
\stopbuffer

\typebuffer[option=tex] \getbuffer

There is not that much that we can improve here but because Mikael Sundqvist and
I had just extended \METAPOST\ with some intersection improvements, it made sense
to see what we could do in the engine. In the next variant the \type {arcpoint}
combines \type {arctime of} and \type {point of}. The reason this is much
faster is that we are already on the right spot when we got the time, and we save
a sequential \type {point of} lookup, something that takes more time when paths
are longer.

\startbuffer
\startMPdefinitions
    vardef agitate_c(expr thepath, S, n, fn, t, ft) =
        save R, nbpoints, noiselevel, rlength ;
        path R ; nbpoints := n ; noiselevel := t ;
        R := thepath ;
        for s=1 upto S :
            nbpoints := nbpoints * fn ;
            noiselevel := noiselevel * ft ;
            rlength := (arclength R) / nbpoints;
            R := for i=1 upto nbpoints:
                (arcpoint (i * rlength) of R)
                    randomized noiselevel
                ..
            endfor cycle ;
        endfor ;
        R
    enddef ;
\stopMPdefinitions
\stopbuffer

\typebuffer[option=tex] \getbuffer

At that stage we wondered if we could come up with a primitive like \typ
{intersectiontimelist} for these points; here a list refers to a path in which we
collect the points. Now, as with the intersection primitives, \METAPOST\ loops
over the segments of a path and works within such a segment. That is why the
following variant has an explicit start at point zero: we can now use offsets
(discrete points).

\startbuffer
\startMPdefinitions
    vardef agitate_d(expr thepath, S, n, fn, t, ft) =
        save R, nbpoints, noiselevel, rlength ;
        path R ; nbpoints := n ; noiselevel := t ;
        R := thepath ;
        for s=1 upto S :
            nbpoints := nbpoints * fn ;
            noiselevel := noiselevel * ft ;
            rlength := (arclength R) / nbpoints;
            R := for i=1 upto nbpoints:
                (arcpoint (0, i * rlength) of R)
                    randomized noiselevel
                ..
            endfor cycle ;
        endfor ;
        R
    enddef ;
\stopMPdefinitions
\stopbuffer

\typebuffer[option=tex] \getbuffer

During an evening zooming Mikael and I figured out, by closely looking at the
source, how the arc functions work and how we could indeed come up with a list
primitive. The main issue was to use the right information. Mikael sat down to
make a pure \METAPOST\ variant and I hacked the engine. Mikael came up with a
first variant similar to the following, where we use a new primitive \typ
{subarclength}.

\startbuffer
\startMPdefinitions
    vardef arcpoints_a(expr thepath, cnt) =
        save len, seg, tot, tim, stp, acc ;
        numeric len ; len := length thepath ;
        numeric seg ; seg := 0 ;
        numeric tot ; tot := 0 ;
        numeric tim ; tim := 0 ;
        %
        numeric acc[] ; acc[0] := 0 ;
        for i = 1 upto len:
            acc[i] := acc[i-1] + subarclength (i-1,i) of thepath ;
        endfor;
        %
        numeric stp ; stp := acc[len] / cnt;
        %
        point 0 of thepath
        for tot = stp step stp until acc[len] :
            hide(
                forever :
                    exitif ((tim < tot) and (tot < acc[seg+1])) ;
                    seg := seg + 1 ;
                    tim := acc[seg] ;
                endfor ;
            )
            -- (arcpoint (seg,tot-tim) of thepath)
        endfor if cycle thepath : -- cycle fi
    enddef ;
\stopMPdefinitions
\stopbuffer

\typebuffer[option=tex] \getbuffer

Getting points of a path is somewhat complicated by the fact that the length of a
closed path is different from that of an open path even if they have the same
number of so-called knots. Internally a path is always a closed loop. That way,
when \METAPOST\ runs over a path, it can easily access the first point when it's
at the end, something that is handy when that point has to be taken into account.
Therefore, the end condition of a loop over a path is the arrival at the
beginning. In the next graphic we show a bit how these first (zero) and last
points are located. One reason why the previous macros start at point one and not
at zero is that \type {arclength} can overflow due to the randomly growing path
otherwise.

\startlinecorrection
\startMPcode
    path p ; p := ((0,0) -- (1,0) -- (1,1) -- (0,1)) xyscaled (20EmWidth,5LineHeight) ;
    path q ; q := p -- cycle ;

    draw p withpen pencircle scaled 4mm withcolor .2white ;
    draw q withpen pencircle scaled 2mm withcolor .6white ;

    draw point length(p) of p withpen pencircle scaled 6mm withcolor green;
    draw point length(q) of q withpen pencircle scaled 6mm withcolor blue ;

    draw point 0         of p withpen pencircle scaled 4mm withcolor red ;
    draw point 0         of q withpen pencircle scaled 2mm withcolor yellow ;

    draw textext.lft("\strut\tttf point length of p") shifted (point length(p) of p) shifted (-6mm,0) ;
    draw textext.lft("\strut\tttf point length of q") shifted (point length(q) of q) shifted (-6mm,LineHeight) ;
    draw textext.lft("\strut\tttf point 0 of p")      shifted (point 0         of p) shifted (-6mm,0) ;
    draw textext.lft("\strut\tttf point 0 of q")      shifted (point 0         of q) shifted (-6mm,-LineHeight) ;

    draw textext("\strut\tttf p is open (dark)")    shifted (center p) shifted (0, LineHeight/2) ;
    draw textext("\strut\tttf q is closed (light)") shifted (center q) shifted (0,-LineHeight/2) ;
\stopMPcode
\stoplinecorrection

The difference between starting at zero or one for a cycle is show below, we get
more and more points!

\startlinecorrection
\startMPcode
    path p ; p := fullsquare ; path q ;

    for i=1 upto 100 :
        q := for j=1 upto length(p) : (point j of p) -- endfor cycle ;
        p := q ;
    endfor ;

    draw            p scaled 2cm withpen pencircle scaled 1mm withcolor "darkred" ;
    drawpointlabels p scaled 2cm ;

    path p ; p := fullsquare shifted (3/2,0) ; path q ;

    for i=1 upto 100 :
        q := for j=0 upto length(p) : (point j of p) -- endfor cycle ;
        p := q ;
    endfor ;

    draw            p scaled 2cm withpen pencircle scaled 1mm withcolor "darkred" ;
    drawpointlabels p scaled 2cm ;
\stopMPcode
\stoplinecorrection

% point of
%
%     if (mp_left_type(p) == mp_endpoint_knot) {
%         set_number_to_unity(n);
%         number_negate(n);
%     } else {
%         set_number_to_zero(n);
%     }
%     do {
%         p = mp_next_knot(p);
%         number_add(n, unity_t);
%     } while (p != cur_exp_knot);

% length:
%
%     mp_knot p = cur_exp_knot;
%     int l = mp_left_type(p) == mp_endpoint_knot ? -1 : 0;
%     do {
%         p = mp_next_knot(p);
%         ++l;
%     } while (p != cur_exp_knot);
%     set_number_from_int(*n, l);

In the next variants we will not loop over points but step to the \type
{arclength}. Watch the new \type {subarclength} primitive that starts at an
offset. This is much faster than taking a \type {subpath of}. We can move the
accumulator loop into the main loop:

\startbuffer
\startMPdefinitions
    vardef arcpoints_b(expr thepath, cnt) =
        save len, aln, seg, tot, tim, stp, acc ;
        numeric len ; len := length thepath ;
        numeric aln ; aln := arclength thepath ;
        numeric seg ; seg := 0 ;
        numeric tot ; tot := 0 ;
        numeric tim ; tim := 0 ;
        numeric stp ; stp := aln / cnt;
        numeric acc ; acc := subarclength (0,1) of thepath ;
        %
        point 0 of thepath
        for tot = stp step stp until aln :
            hide(
                forever :
                    exitif tot < acc ;
                    seg := seg + 1 ;
                    tim := acc ;
                    acc := acc + subarclength (seg,seg+1) of thepath ;
                endfor ;
            )
            -- (arcpoint (seg,tot-tim) of thepath)
        endfor if cycle thepath : -- cycle fi
    enddef ;
\stopMPdefinitions
\stopbuffer

\typebuffer[option=tex] \getbuffer

If you don't like the \type {hide} the next variant also works okay:

\startbuffer
\startMPdefinitions
    vardef mfun_arc_point(text tot)(text thepath) =
        forever :
            exitif tot < acc ;
            seg := seg + 1 ;
            tim := acc ;
            acc := acc + subarclength (seg,seg+1) of thepath ;
        endfor ;
        (arcpoint (seg,tot-tim) of thepath)
    enddef ;

    vardef arcpoints_c(expr thepath, cnt) =
        save len, aln, seg, tot, tim, stp, acc ;
        numeric len ; len := length thepath ;
        numeric aln ; aln := arclength thepath ;
        numeric seg ; seg := 0 ;
        numeric tot ; tot := 0 ;
        numeric tim ; tim := 0 ;
        numeric stp ; stp := aln / cnt;
        numeric acc ; acc := subarclength (0,1) of thepath ;
        %
        point 0 of thepath
        for tot = stp step stp until aln :
            -- mfun_arc_point(tot)(thepath)
        endfor if cycle thepath : -- cycle fi
    enddef ;
\stopMPdefinitions
\stopbuffer

\typebuffer[option=tex] \getbuffer

This got applied in three test agitators

\startbuffer
\startMPdefinitions
    vardef agitate_e_a(expr thepath, S, n, fn, t, ft) =
        save R, nbpoints, noiselevel ;
        path R ; nbpoints := n ; noiselevel := t ;
        R := thepath ;
        for s=1 upto S :
            nbpoints := nbpoints * fn ;
            noiselevel := noiselevel * ft ;
            R := arcpoints_a(R, nbpoints) ; % original Mikael
            R := for i=0 upto length R:
                (point i of R)
                    randomized noiselevel
                ..
            endfor cycle ;
        endfor ;
        R
    enddef ;

    vardef agitate_e_b(expr thepath, S, n, fn, t, ft) =
        save R, nbpoints, noiselevel ;
        path R ; nbpoints := n ; noiselevel := t ;
        R := thepath ;
        for s=1 upto S :
            nbpoints := nbpoints * fn ;
            noiselevel := noiselevel * ft ;
            R := arcpoints_b(R, nbpoints) ; % merged Mikael
            R := for i=0 upto length R:
                (point i of R)
                    randomized noiselevel
                ..
            endfor cycle ;
        endfor ;
        R
    enddef ;

    vardef agitate_e_c(expr thepath, S, n, fn, t, ft) =
        save R, nbpoints, noiselevel ;
        path R ; nbpoints := n ; noiselevel := t ;
        R := thepath ;
        for s=1 upto S :
            nbpoints := nbpoints * fn ;
            noiselevel := noiselevel * ft ;
            R := arcpoints_c(R, nbpoints) ; % split Mikael
            R := for i=0 upto length R:
                (point i of R)
                    randomized noiselevel
                ..
            endfor cycle ;
        endfor ;
        R
    enddef ;
\stopMPdefinitions
\stopbuffer

\typebuffer[option=tex] \getbuffer

The new engine primitive shortens these agitators:

\startbuffer
\startMPdefinitions
    vardef agitate_e_d(expr thepath, S, n, fn, t, ft) =
        save R, nbpoints, noiselevel ;
        path R ; nbpoints := n ; noiselevel := t ;
        R := thepath ;
        for s=1 upto S :
            nbpoints := nbpoints * fn ;
            noiselevel := noiselevel * ft ;
            R := arcpointlist nbpoints of R;
            R := for i=0 upto length R:
                (point i of R)
                    randomized noiselevel
                ..
            endfor cycle ;
        endfor ;
        R
    enddef ;
\stopMPdefinitions
\stopbuffer

\typebuffer[option=tex] \getbuffer

So are we done? Did we get rid of all bottlenecks? The answer is no! We still
loop over the list in order to randomize the points. For each point we start at
the beginning of the list. Let's first rewrite the agitator a little:

\startbuffer
\startMPdefinitions
    vardef agitate_f_a(expr pth, iterations, points, pointfactor, noise, noisefactor) =
        save currentpath, currentpoints, currentnoise ; path currentpath ;
        currentpath   := pth ;
        currentpoints := points ;
        currentnoise  := noise ;
        for step = 1 upto iterations :
            currentpath   := arcpointlist currentpoints of currentpath ;
            currentnoise  := currentnoise  * noisefactor ;
            currentpoints := currentpoints * pointfactor ;
            if currentnoise <> 0 :
                currentpath :=
                    for i = 0 upto length currentpath:
                        (point i of currentpath) randomized currentnoise ..
                    endfor
                cycle ;
            fi
        endfor ;
        currentpath
    enddef ;
\stopMPdefinitions
\stopbuffer

\typebuffer[option=tex] \getbuffer

One of the \LUAMETAFUN\ extensions is a fast path iterator. In the next variant
the \type {inpath} macro sets up an iterator (regular loop) with the length as
final value. In the process the given path gets passed to \LUA\ where we can
access it as array. The \type {pointof} macro (again a \LUA\ call) injects a
pair. You will be surprised that even with passing the path to \LUA\ and calling
out to \LUA\ to inject the pair this is way faster than the built|-|in \type
{point of}.

\startbuffer
\startMPdefinitions
    vardef agitate_f_b(expr pth, iterations, points, pointfactor, noise, noisefactor) =
        save currentpath, currentpoints, currentnoise ; path currentpath ;
        currentpath   := pth ;
        currentpoints := points ;
        currentnoise  := noise ;
        for step = 1 upto iterations :
            currentnoise  := currentnoise  * noisefactor ;
            currentpoints := currentpoints * pointfactor ;
            currentpath   := arcpointlist currentpoints of currentpath ;
            if currentnoise <> 0 :
                currentpath :=
                    for i inpath currentpath :
                        (pointof i) randomized currentnoise ..
                    endfor
                cycle ;
            fi
        endfor ;
        currentpath
    enddef ;
\stopMPdefinitions
\stopbuffer

\typebuffer[option=tex] \getbuffer

It was tempting to see if a more native solution pays of. One problem there is
that a path is not really suitable for that as we currently don't have a data
type that represents a point. Okay, actually we sort of have because we can use
the transform record that has six points but that is something I will look into
later (it just got added to the todo list).

The \typ {i within pth} iterator is no conceptual beauty but does the job. Just
keep in mind that it is just means for this kind of applications: run over a path
point by point. The \type {i} has the current point number. Because we run over a
path following the links we only run forward.

\startbuffer
\startMPdefinitions
    vardef agitate_f_c(expr pth, iterations, points, pointfactor, noise, noisefactor) =
        save currentpath, currentpoints, currentnoise ; path currentpath ;
        currentpath   := pth ;
        currentpoints := points ;
        currentnoise  := noise ;
        for step = 1 upto iterations :
            currentnoise  := currentnoise  * noisefactor ;
            currentpoints := currentpoints * pointfactor ;
            currentpath   := arcpointlist currentpoints of currentpath ;
            if currentnoise <> 0 :
                currentpath :=
                    for i within currentpath :
                        pathpoint
                        randomized currentnoise
                        ..
                    endfor
                cycle ;
            fi
        endfor ;
        currentpath
    enddef ;
\stopMPdefinitions
\stopbuffer

\typebuffer[option=tex] \getbuffer

Any primitive solution more complex than this, like first creating a fast access
data structure, of having a double linked list, or using some iterator larger
than a simple numeric is very likely to have no gain over the super fast \LUA\
variant.

\startMPdefinitions
    vardef agitate_x(expr thepath, S, n, fn, t, ft) =
        save R, nbpoints ;
        path R ; nbpoints := n ;
        R := thepath ;
        for s=1 upto S :
            nbpoints := nbpoints * fn ;
            R := arcpointlist nbpoints of R ;
        endfor ;
        R
    enddef ;
\stopMPdefinitions

\startMPdefinitions
    def TestMe (expr method, col, justtest) =
        path P ;

        randomseed   := 10;
      % maxknotpool  := 20000; % default is 1000
        defaultscale := .05;

      % NbCircles :=  1 ; S :=  1 ; nzero := 10 ; fn := 1.3 ; tzero := 5 ; ft := 0.8 ;
      % NbCircles :=  2 ; S := 10 ; nzero := 10 ; fn := 1.3 ; tzero := 5 ; ft := 0.8 ;
      % NbCircles := 10 ; S :=  5 ; nzero := 10 ; fn := 1.3 ; tzero := 5 ; ft := 0.8 ;
      % NbCircles := 10 ; S := 10 ; nzero := 20 ; fn := 1.3 ; tzero := 5 ; ft := 0.8 ;
        NbCircles := 15 ; S := 10 ; nzero := 10 ; fn := 1.3 ; tzero := 5 ; ft := 0.8 ;

        for c = NbCircles downto 1 :
            P := fullcircle scaled (c*6.5) scaled 3 ;
            nzero := floor(arclength(P)*0.5);
                if method ==  0 : P := agitate_x  (P, S, nzero, fn, tzero, ft) ;
            elseif method ==  1 : P := agitate_a  (P, S, nzero, fn, tzero, ft) ;
            elseif method ==  2 : P := agitate_b  (P, S, nzero, fn, tzero, ft) ;
            elseif method ==  3 : P := agitate_c  (P, S, nzero, fn, tzero, ft) ;
            elseif method ==  4 : P := agitate_d  (P, S, nzero, fn, tzero, ft) ;
            elseif method == 51 : P := agitate_e_a(P, S, nzero, fn, tzero, ft) ;
            elseif method == 52 : P := agitate_e_b(P, S, nzero, fn, tzero, ft) ;
            elseif method == 53 : P := agitate_e_c(P, S, nzero, fn, tzero, ft) ;
            elseif method == 54 : P := agitate_e_d(P, S, nzero, fn, tzero, ft) ;
            elseif method == 61 : P := agitate_f_a(P, S, nzero, fn, tzero, ft) ;
            elseif method == 62 : P := agitate_f_b(P, S, nzero, fn, tzero, ft) ;
            elseif method == 63 : P := agitate_f_c(P, S, nzero, fn, tzero, ft) ;
            else :
            fi ;
            if justtest :
                % do nothing
            elseif method == 0 :
                draw textext("no draw") ;
            else :
                eofill          P                              withcolor transparent(1,4/NbCircles,col) ;
                draw            P withpen pencircle scaled 0.1           transparent(1,4/NbCircles,.90[black,col]) ;
             %  drawpoints      P withpen pencircle scaled 0.2 withcolor white ;
             %  drawpointlabels P                              withcolor white;
            fi ;
        endfor ;
    enddef ;
\stopMPdefinitions

We show the average runtime for three runs. Here we don't render the paths, which
takes about one second, including conversion to \PDF. Of course measurements like
this can change a bit over time. To these times you need to add about a second
for the draw and fill operations as well as conversion to a \PDF\ stream with
transparencies. The improvement in runtime makes it possible to use agitators like
this at runtime especially because normally one will not use such (combinations
of) large paths.

\starttabulate[|T|r|i2T|r|i2T|r|]
    \BC agitate_a \NC 776.26 \BC agitate_e_a \NC 291.99 \BC agitate_f_a \NC 10.82 \NC \NR %  1  51 61
    \BC agitate_b \NC 276.43 \BC agitate_e_b \NC  76.06 \BC agitate_f_b \NC  2.55 \NC \NR %  2  52 62
    \BC agitate_c \NC 259.89 \BC agitate_e_c \NC  77.27 \BC agitate_f_c \NC  2.17 \NC \NR %  3  53 63
    \BC agitate_d \NC 260.41 \BC agitate_e_d \NC  18.67 \BC             \NC       \NC \NR %  4  54
\stoptabulate

The final version of the agitator is slightly different because it depends if we
start at zero or one but gives similar results and adapt the noise before or
after the loop.

\typebuffer[final:definition][option=tex]

We use a similar example as in the mentioned article but coded a bit differently:

\typebuffer[final:graphic][option=tex]

For Mikael and me, who both like \METAPOST, it was a nice distraction from
working months on extending math in \LUAMETATEX, but it also opens up the
possibilities to do more with rendering (math) functions and graphics, so in the
end we get paid back anyway.

\stopchapter

\stopcomponent

% see agitate-002.tex

% \startluacode
%     local t = {
%         0, -- also initialization
%         1, 2, 3, 4,
%         51, 52, 53, 54,
%         61, 62, 63,
%     }
%     for i=1,#t do
%         context.writestatus("TEST RUN",t[i])
%         context("\\testfeatureonce{3}{\\startMPcalculation TestMe (%i, blue, true) ; \\stopMPcalculation}",t[i])
%     end
% \stopluacode

% \testfeatureonce{1}{\startMPpage TestMe ( 0, \MPcolor{darkblue}  , false) ; \stopMPpage}
% \testfeatureonce{1}{\startMPpage TestMe ( 1, \MPcolor{darkblue}  , false) ; \stopMPpage}
% \testfeatureonce{1}{\startMPpage TestMe ( 2, \MPcolor{darkblue}  , false) ; \stopMPpage}
% \testfeatureonce{1}{\startMPpage TestMe ( 3, \MPcolor{darkred}   , false) ; \stopMPpage}
% \testfeatureonce{1}{\startMPpage TestMe ( 4, \MPcolor{darkgreen} , false) ; \stopMPpage}
% \testfeatureonce{1}{\startMPpage TestMe (51, \MPcolor{darkred}   , false) ; \stopMPpage}
% \testfeatureonce{1}{\startMPpage TestMe (52, \MPcolor{darkgreen} , false) ; \stopMPpage}
% \testfeatureonce{1}{\startMPpage TestMe (53, \MPcolor{darkblue}  , false) ; \stopMPpage}
% \testfeatureonce{1}{\startMPpage TestMe (54, \MPcolor{darkblue}  , false) ; \stopMPpage}
% \testfeatureonce{1}{\startMPpage TestMe (61, \MPcolor{darkyellow}, false) ; \stopMPpage}
% \testfeatureonce{1}{\startMPpage TestMe (62, \MPcolor{darkyellow}, false) ; \stopMPpage}
% \testfeatureonce{1}{\startMPpage TestMe (63, \MPcolor{darkyellow}, false) ; \stopMPpage}

% \stoptext