8

I saw that TikZ can draw cloud shapes, even asymmetric ones: Asymmetric cloud shape in TikZ.

But as I am not familiar with TikZ and prefer Asymptote's C++ like syntax, I would like to know how to draw cloud shapes in Asymptote.

2
  • Do you just want to draw a cloud shape, or do you also want to fit it to surround some text? May 16, 2015 at 17:27
  • @CharlesStaats a cloud shape with optional text in its center.
    – alick
    May 17, 2015 at 10:29

2 Answers 2

8

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}
10
  • How about support ellipse envelope? Is it possible to adjust the symmetry level?
    – alick
    May 24, 2015 at 3:57
  • @alick: In fact, ellipse envelope was the first try, but I replaced it with a roundbox since it looks more suitable to surround a label - ellipse leaves long empty spaces on the left and right for relatively long strings. Anyway, it's a proof-of-concept, addition of more envelopes is a straightforward procedure. Btw, the CloudShape can already be used with arbitrary base path, and the label can be drawn on top of it, the envelope is just a convenient way to fit the label automatically.
    – g.kov
    May 24, 2015 at 4:41
  • Can you show us what it looks like with an outline only? (using draw rather than fill) May 24, 2015 at 16:56
  • @Charles Staats: see example outlines added
    – g.kov
    May 24, 2015 at 18:43
  • 1
    @Khaaba: check out Curve through a sequence of points with Metapost and TikZ.
    – g.kov
    Oct 26, 2020 at 12:06
5

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

1
  • 2
    For an explanation of the behavior of buildcycle, see pages 30ff of the metapost manual. (The Asymptote buildcycle command was designed to imitate the Metapost command.) But it seems the command is really designed to be used when adjacent paths only intersect in a single point. in this case, I'd say you are better off trying to eliminate the portion of each circle inside the red trapezoid and only then applying buildcycle. May 23, 2015 at 3:59

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .