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.
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.

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:

%
% 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}
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 04:41
draw rather than fill)
– Charles Staats
May 24 '15 at 16:56
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 00:54
n points
along the base path, so it is not applicable,
at least in the current form.
However, the curvature of the path
probably could be used to improve the point distribution.
– g.kov
May 25 '15 at 01:24
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.
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 03:59