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.

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

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}
  • How about support ellipse envelope? Is it possible to adjust the symmetry level? – alick May 24 '15 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 '15 at 4:41
  • Can you show us what it looks like with an outline only? (using draw rather than fill) – Charles Staats May 24 '15 at 16:56
  • @Charles Staats: see example outlines added – g.kov May 24 '15 at 18:43
  • This is great! Another suggestion: It might helpful if you could somehow ensure that every "curl" has radius less than the radii of curvature of the base path at its endpoints (computed using radius(path, t)). Since I don't understand the algorithm you're using to compute the radii, I can't tell how hard this change would be. – Charles Staats May 25 '15 at 0:54
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

  • 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. – Charles Staats May 23 '15 at 3:59

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.