How to draw cloud shape in Asymptote

enter image description here

This module cloudshape.asy is an attempt to provide a class CloudShape, which can be used to draw labels inside the cloud-shaped envelope (code for the envelope is borrowed from the roundbox envelope routine from plain_boxes.asy):

// tested with Asymptote 2.35
//
// cloudshape.asy
//
import graph;

struct CloudShape{
  Label L;
  int n;
  pen borderPen, fillPen;
  guide base;
  guide cloud;
  pair[] CurlPoint; 
  real[] r;
  pair[] center;

  private real[] phi;
  private real a[],b[],c[];
  private real[] alpha;

  void makeRandomPoints(){
    CurlPoint=sequence(
        new pair(int k){
          return relpoint(base,k/n
           +1/3/n*(2*unitrand()-1)
           );
        }
        ,n
      );
  }

  void precond(){
    makeRandomPoints();

    int inext, iprev, sign;
    sign=1;
    alpha=array(n,0);
    for(int i=0;i<n;++i){
      iprev=(i-1+n)%n;
      inext=(i+1)%n;
      a[i]=abs(CurlPoint[i]-CurlPoint[iprev]);
      b[i]=abs(CurlPoint[inext]-CurlPoint[i]);
      c[i]=abs(CurlPoint[inext]-CurlPoint[iprev]);
      phi[i]=pi-acos(max(-1,min((a[i]^2+b[i]^2-c[i]^2)/(2*a[i]*b[i]),1)));

      alpha[0]+=sign*phi[i]/2;
      sign=-sign;
    }


    for(int i=1;i<=(n-1)/2;++i){
      alpha[i]  =phi[i-1]-alpha[i-1];
      alpha[n-i]=phi[n-i]-alpha[(n-i+1)%n];  
    }
    b.delete(); c.delete(); phi.delete();      
  }

  void makeCurls(){
    int inext, iprev;
    for(int i=0;i<n;++i){
      iprev=(i-1+n)%n;
      inext=(i+1)%n;
      r[i]=a[i]/2/cos(alpha[i]);

      center[i]=extension(
        CurlPoint[iprev], rotate(-degrees(alpha[i]),CurlPoint[iprev])*CurlPoint[i]
       ,CurlPoint[i],     rotate( degrees(alpha[i]),CurlPoint[i])*CurlPoint[iprev]
      );

      if((degrees(CurlPoint[i]-center[i])-degrees(CurlPoint[iprev]-center[i]))%360>180){
        center[i]=reflect(CurlPoint[iprev],CurlPoint[i])*center[i];
      }
      cloud=cloud--arc(center[i],CurlPoint[iprev],CurlPoint[i]);
    }
    cloud=cloud--cycle;    
    a.delete();
  }

  void operator init(Label L="", int n=11
    ,guide base=circle((0,0),1)
    ,pen borderPen=currentpen, pen fillPen=nullpen){
    assert(n>2 ,"Expect n>2, but n="+string(n)+" found.");
    this.L=L;
    this.n         = n+1-(n%2); // ensure that n is odd
    this.borderPen = borderPen;
    this.fillPen   = fillPen;
    this.base=base;
    precond();
    makeCurls();
  }

  void operator init(Label L="", pair[] CurlPoint
    ,pen borderPen=currentpen, pen fillPen=nullpen){
    this.L=L;
    this.CurlPoint=copy(CurlPoint);
    this.n=CurlPoint.length;
    assert(n>2 ,"Expect n>2, but n="+string(n)+" found.");
    if(this.n%2==0){
      CurlPoint.push((CurlPoint[0]+CurlPoint[n-1])/2);
      ++this.n;      
    }
    this.borderPen = borderPen;
    this.fillPen   = fillPen;
    this.base=graph(CurlPoint)..cycle;
    precond();
    makeCurls();
  }

}

envelope MakeCloud(int n=11){
  return new
      path (frame dest, frame src=dest, real xmargin=0, real ymargin=xmargin,
                   pen p=currentpen, filltype filltype=NoFill, bool above=true)
      {
          pair m=min(src);
          pair M=max(src);
          pair bound=M-m;
          int sign=filltype == NoFill ? 1 : -1;
          real a=bound.x+2*xmargin;
          real b=bound.y+2*ymargin;
          real ds=0;
          real dw=min(a,b)*0.3;
          path g=shift(m-(xmargin,ymargin))*((0,dw)--(0,b-dw){up}..{right}
          (dw,b)--(a-dw,b){right}..{down}
          (a,b-dw)--(a,dw){down}..{left}
          (a-dw,0)--(dw,0){left}..{up}cycle);

        frame F;        
        CloudShape cl=CloudShape(n,reverse(g));      
        if(above == false) {
          filltype.fill(F,cl.cloud,p);
          prepend(dest,F);
        } else filltype.fill(dest,cl.cloud,p);

        return cl.cloud;
      };
}

It splits a base closed path into n points and builds a closed sequence of arcs. The nodes of the base path must follow counter-clockwise order.

A complete MWE (needs lualatex to use Humor-Sans font):

// Example
//   this example uses Humor-Sans font 
//   from https://github.com/shreyankg/xkcd-desktop
//
import cloudshape;

settings.tex="lualatex";

real w=8cm,h=0.618w;
size(w,h);
import fontsize;defaultpen(fontsize(9pt));
texpreamble("\usepackage{fontspec}");

srand(1110011);

Label L=Label("{$\pi=\arctan(1)+\arctan(2)+\arctan(3)$}",align=plain.E);

draw("{\fontspec{Humor-Sans}Hello, World!}"
  ,MakeCloud(9),(0,1),xmargin=1mm,ymargin=3mm
  ,p=blue,filltype=Fill(paleblue));

draw(L,MakeCloud(39),(0.2,0),xmargin=5pt
  ,p=green, filltype=Fill(orange+opacity(0.5)));

draw(scale(4,1)*unitsquare,nullpen);    
shipout(bbox(Fill(paleyellow)));

Edit: An example showcase:

enter image description here

%
% showcase.tex
%
\documentclass{article}
\usepackage[inline]{asymptote}
\begin{asydef}
size(2cm);
import cloudshape;
pen basePen=orange+0.5bp;
pen cloudPen=darkblue+0.9bp;
void show(int n, guide g){ 
  CloudShape cs=CloudShape(n,base=g);
  draw(cs.base,basePen); 
  draw(cs.cloud,cloudPen); 
  label("$n="+string(n)+"$",(min(cs.cloud)+max(cs.cloud))/2);
}
guide[] case={
  scale(4,3)*unitcircle,
  (0,0)..(12,0)..(12,4)..(8,5)..(4,8)..(0,4)..cycle,
};
\end{asydef}
\usepackage{lmodern}
\begin{document}
\begin{tabular}{cc}
\begin{asy}
show(7,case[0]);
\end{asy}
&
\begin{asy}
show(7,case[1]);
\end{asy}
\\
\begin{asy}
show(9,case[0]);
\end{asy}
&
\begin{asy}
show(9,case[1]);
\end{asy}
\\
\begin{asy}
show(11,case[0]);
\end{asy}
&
\begin{asy}
show(11,case[1]);
\end{asy}
\\
\begin{asy}
show(21,case[0]);
\end{asy}
&
\begin{asy}
show(21,case[1]);
\end{asy}
\end{tabular}
\end{document}

Major edit to improve my solution, incorporating the @CharlesStaats comment.

The following cloudpath function creates arcs around the periphery of a non-intersecting cyclic path, then trims those arcs to one another with a call to buildcycle.

path cloudpath(path p, real minArcRadius, real maxArcScale = 1.0)

The arc radius is the second argument. The third argument allows random perterbations of the arc sizes.

unitsize(1inch);

path cloudpath(path p, real minArcRadius, real maxArcScale = 1.0)
{
    real overlap = 0.9;
    real pLength = arclength(p);

    // create cloud arc radii
    real[] radii;
    while(2*overlap * sum(radii) < pLength)
    {
        radii.push(minArcRadius * (1.0 + (unitrand() * (maxArcScale - 1.0))));
    }

    // scale radii to avoid large arc overlap at beginning and end of path p
    radii = radii * (pLength / (2*overlap * sum(radii)));

    // create overlapping arcs exterior to path p
    path arcs[];
    real currentTime = 0.0;
    for (int i = 0; i < radii.length; ++i)
    {
        pair circleCenter = (arcpoint(p, currentTime));
        path thisCircle = shift(circleCenter)*scale(radii[i])*unitcircle;
        pair[] intersects = intersectionpoints(thisCircle, p);
        path thisArc = arc(circleCenter, intersects[0], intersects[1], CW);
        if (inside(p, relpoint(thisArc, 0.1)))
        {
            thisArc = arc(circleCenter, intersects[0], intersects[1], CCW);
        }
        arcs.push(thisArc);
        if (i < radii.length - 1)
        {
            currentTime += overlap * (radii[i] + radii[i+1]);
        }
    }

    draw(p, red); // comment out to hide construction
    draw(arcs, mediumgray); // comment out to hide construction

    return buildcycle(... arcs);
}

path quadPath = slant(0.5)*unitsquare;
draw(cloudpath(quadPath, 0.2, 1.5), 2+black);

path ellipsePath = shift(4.0,0.5)*rotate(30)*scale(1,0.5)*unitcircle;
draw(cloudpath(ellipsePath, 0.2, 2.0), 2+black);

path crossingPath = shift(0,-3)*((0,0)--(2,0)--(0,2)--(2,2)--cycle);
draw(cloudpath(crossingPath, 0.2, 2.0), 2+black);

path concavePath = shift(3.0,-3)*((0,0)--(2,0)--(2,2)--(0,2)--(1,1)--cycle);
draw(cloudpath(concavePath, 0.2, 2.0), 2+black);

I didn't do much testing, so I'm not sure if the function is very robust. As shown below, intersecting paths fail. Comment out the draw commands in the cloudpath function to avoid drawing the red and gray curves.

enter image description here

Tags:

Asymptote