Function

@shape

A polygon() for clip-path, from a preset name, from equations in the angle t, from letters or SVG path data, or from shapes joined in groups. With rounded corners it is a shape().

Syntax

  • @shape(<preset> [<k>])
  • @shape(<command>: <value>; ...)
  • @shape(<preset> [<k>]; <command>: <value>; ...)
  • @shape(text: <text>[; font: <font>]; ...)
  • @shape(path: <path data>; ...)
  • @shape(<group> { <shape> { ... } ... })

The shape is drawn in a square from -1 to 1, with y pointing up, and mapped to percentages of the element. The commands are listed in the table below, and // starts a comment that runs to the end of the line.

Polar curves

The points go once around the center at evenly spaced angles t, from 0 to 2π. points sets how many there are, and r how far each one is from the center, 1 by default. Without points there are 3, or 180 when r, x or y changes with t. So six points with nothing else make a hexagon:

@grid: 1 / 60vmin;
background: #000;
clip-path: @shape(
  points: 6;
  scale: .8;
);

With enough points and r as an expression in t, the polygon follows a curve. A polar rose:

@grid: 1 / 60vmin;
background: #000;
clip-path: @shape(
  points: 300;
  r: cos(4t);
  scale: .9;
);

Parametric curves

x and y give the coordinates of each point directly, as expressions in t. They are used only when there is no r. A heart:

@grid: 1 / 60vmin;
background: #000;
clip-path: @shape(
  points: 200;
  x: sin(t)^3;
  y: (13cos(t) - 5cos(2t) - 2cos(3t) - cos(4t))/16;
  scale: .9;
);

Presets

A preset name, the same ones the @shape property takes, starts from a ready-made shape. Commands after it add to the preset or override it, as in heart; frame: .1. The preset's own steps come first, so square; scale: .5 halves the square. A bare circle gives the native ellipse(50% 50%); any command after it, as in circle; points: 180, gives the polygon.

@grid: 4 / 60vmin _2px;
background: #000;
clip-path: @shape(@p(star, heart, drop, clover 4));
rotate: @r(360deg);

Letters and paths

text traces its letters into the outline, in the browser. font sets their style, weight and family as the CSS font shorthand does, without a size; the family is sans-serif by default. The ink is fitted to the box, centered, with its longer side across the whole -1 to 1 space, and the counters of letters such as A and O are holes:

@grid: 4 / 60vmin _2px;
background: #000;
clip-path: @shape(
  text: @p([A-Z]);
  font: 900 serif;
  scale: .8;
);

An emoji draws as its silhouette. Quotes keep commas, ; and parentheses, so text: ';)' is a wink. A family name needs no quotes, even font: Press Start 2P. font: @google-font(Pacifico) loads a font from Google Fonts, and the doodle draws again once the letters have loaded.

path reads SVG path data, the d of a <path>, and fits it to the box the same way. Its y points down as in SVG, so it comes out the same way up. Each subpath is a contour, and data with commas in it needs quotes:

@grid: 1 / 60vmin;
background: #000;
clip-path: @shape(
  path: M4 4 H20 A2 2 0 0 1 22 6 V16 A2 2 0 0 1 20 18
        H9 L4 22 V18 A2 2 0 0 1 2 16 V6 A2 2 0 0 1 4 4 Z;
  scale: .9;
);

The outline then takes the other commands, such as scale, frame or mirror, and shape { text: A } puts it in a group. @plot, @tile and the polygon and lathe of @scene read it too.

Size and position

scale takes one factor, or two for x and y. rotate turns the shape counterclockwise, in degrees. move shifts it in the -1 to 1 space, with y pointing up. They run in the order written, each on the shape so far, so here the circle is squashed first and then turned:

@grid: 1 / 60vmin;
background: #000;
clip-path: @shape(
  points: 300;
  scale: .9 .5;
  rotate: 30;
);

Mirroring

mirror: x keeps the shape and its left-right reflection as one shape, so two lobes drawn on the right become a butterfly. mirror: y adds the top-bottom reflection, and mirror: x y both. It is a step like move, so circle; scale: .3; move: .5 .5; mirror: x y draws four circles. The result is always a polygon():

@grid: 3 / 60vmin _2px;
background: #000;
clip-path: @shape(
  points: 240;
  r: .2 + .8exp(3 * (cos(t - @r(.2, 1)) - 1))
     + .4exp(6 * (cos(t + @r(.3, 1)) - 1));
  scale: .8;
  mirror: x;
);

Going around more than once

turn sends the points around the center that many times. The rose cos(5t/3) comes back to where it started only after three turns; with the default of one, the curve stops a third of the way through and cuts straight back to the start:

@grid: 1 / 60vmin;
background: #000;
clip-path: @shape(
  points: 300;
  turn: 3;
  r: cos(5t/3);
  scale: .9;
);

When the outline crosses itself, fill-rule picks the fill rule. Five points going around twice skip every other one and draw a pentagram. The default, nonzero, fills the middle; evenodd leaves it empty:

@grid: 1 / 60vmin;
background: #000;
clip-path: @shape(
  points: 5;
  turn: 2;
  rotate: 90;
  fill-rule: evenodd;
);

Several contours

A comma in points, r, t, x, y, rotate or grow starts another contour. The first contour takes the first item of each list, the second the second, and a shorter list repeats. t and i start over in each contour. With evenodd, the inner contours cut holes:

@grid: 1 / 60vmin;
background: #000;
clip-path: @shape(
  points: 120;
  r: 1, .75, .5, .25;
  fill-rule: evenodd;
  scale: .9;
);

x and y lists put a contour anywhere, like the holes of a button:

@grid: 1 / 60vmin;
background: #000;
clip-path: @shape(
  points: 90;
  x: cos(t), .14cos(t) + .3, .14cos(t) - .3,
     .14cos(t) - .3, .14cos(t) + .3;
  y: sin(t), .14sin(t) + .3, .14sin(t) + .3,
     .14sin(t) - .3, .14sin(t) - .3;
  fill-rule: evenodd;
  scale: .9;
);

Each contour can have its own number of points. A gear takes four: the teeth, a contour that reaches out between the spokes and cuts the windows against the circle at .34, that circle, and the axle hole:

@grid: 1 / 60vmin;
background: #000;
clip-path: @shape(
  points: 240, 200, 200, 36;
  r: .92 + .08 * max(-1, min(1, 2.5cos(24t))),
     .34 + .36 * max(0, min(1, 4 * (.15 - cos(5t)))),
     .34, .09;
  fill-rule: evenodd;
  scale: .9;
);

Under the default nonzero, a contour cuts a hole only when it runs the other way, as with t: t, -t. points: 0 leaves a contour out, and @m writes long lists: r: @m8(@n / 8) is eight circles. frame and grow take each contour as a shape of its own, and @tile and the @plot variants keep the holes. Contours suit rings and other shapes around one center; for holes elsewhere, a subtract group is plainer.

Combining shapes

A shape can be written as a block with its own commands: a preset such as circle { scale: .9 }, polygon { points: x y, … } for corners, or shape { … } for the commands on this page. union, subtract, intersect and exclude join the shapes inside them. subtract takes the rest away from the first one, and exclude keeps what an odd number of them cover. A crescent:

@grid: 1 / 60vmin;
background: #000;
clip-path: @shape(
  subtract {
    circle;
    circle { scale: .9; move: .45 .135 }
  }
);

smooth rounds every place where two shapes meet with a curve of that radius. A group's own commands apply to the joined shape, and so do the commands at the top of the body. With a group in it, the body draws only the group:

@grid: 1 / 60vmin;
background: #000;
clip-path: @shape(
  union {
    smooth: .2;
    circle { scale: .5; move: -.3 0 }
    circle { scale: .35; move: .35 .15 }
  }
);

With a group in the body, points at the top spreads that many points over the outlines by length, and a list such as points: 180, 20 gives each outline its own count. Two groups with the same counts morph into each other in @keyframes.

Names and copies

name#id keeps a shape or a group for later instead of drawing it, and #id draws it, with any commands given there added. A step given there, such as scale, runs after the shape's own steps; any other command replaces the shape's own. An #id may come before the shape that defines it. name*N repeats a shape, and name*XxY repeats it in a grid. Each copy sees n from 1 and the count N, or its column and row as nx and ny. A gear with twelve copies of one tooth:

@grid: 1 / 60vmin;
background: #000;
clip-path: @shape(
  polygon#tooth { points: -.11 0, .11 0, .07 .22, -.07 .22 }
  subtract {
    union {
      circle { r: .72 }
      #tooth*12 { move: 0 .66; rotate: n * 30 }
    }
    circle { r: .26 }
  }
  round: .03;
);

@ functions in a copy run once per copy, so circle*2 { move: @pn(-.6 0, .6 0) } draws two circles apart. The same blocks, names, copies and groups other than exclude build 3D models in @scene.

A group joins at most its first 32 shapes.

Reusing a shape

A body in parentheses reads as commands, so a shape kept in a custom property is reused with $name. Commands after it add to it or override it, so a body with a free variable works like a preset of your own:

:doodle {
  --petal: (points: 240; r: cos(k * t)^2 + .25; scale: .75);
}
@grid: 3 / 60vmin _2px;
background: #000;
clip-path: @shape($petal; k: @i + 1);

Declared on :doodle, the body is written once for every cell. @plot, @tile and @svg-polygon read it the same way, and so does a clip-path inside @keyframes. In a group, $name or a body in parentheses is a shape like any other, as in union { $petal { k: 2 } $petal { k: 3 } }. A custom property keeps its value on one line, so a // comment there would run to the end of the body; use /* */ instead.

Outlines

frame draws only the outline: it keeps a band this wide inside it, in units of the -1 to 1 space. Any other name declares a variable, like k here, for the expressions to read:

@grid: 3 / 60vmin _2px;
background: #000;
clip-path: @shape(
  k: @pn(3, 4, 5, 6, 7, 8, 9, 10, 12);
  points: 300;
  r: cos(k * t)^2 + .5;
  frame: .05;
  scale: .6;
);

Corners and edges

round turns every corner into a curve of this radius, in units of the -1 to 1 space, as far as the sides allow, so a large enough radius leaves no straight side at all. The result is a CSS shape() that draws the curves exactly, instead of a polygon(), except with grow, frame or mirror, or on corners too slight to need a curve. The corners are rounded before the steps run, wherever round is written, so a scale scales the radius too.

@grid: 3 / 60vmin _2px;
background: #000;
clip-path: @shape(
  triangle;
  round: @pn(0, .05, .1, .15, .2, .3, .4, .5, .6);
);

grow pushes the outline out by a length, or in when negative. It may also be an expression in t, which here runs from 0 to 2π by length along the outline, so a wave runs evenly around a square, corners included. The ends always meet:

@grid: 1 / 60vmin;
background: #000;
clip-path: @shape(
  square;
  scale: .8;
  grow: .03 * sin(24t);
);

Point by point

Each expression also sees i, the index of the point, starting at 1. seq(a, b, …) cycles its arguments from point to point, so seq(1, .5) pulls every other point in:

@grid: 1 / 60vmin;
background: #000;
clip-path: @shape(
  points: 16;
  r: seq(1, .5);
  scale: .9;
);

range(a, b) runs evenly from the smaller of the two to the larger across the points. The t command rewrites the angle of each point, as an expression in t and i, before r, x and y see it.

Noise

@R.t writes a noise curve that closes on itself, a different one in each cell. seed fixes it; otherwise it follows the doodle's seed:

@grid: 3 / 60vmin _2px;
background: #000;
clip-path: @shape(
  points: 200;
  r: @R.t(.6, 1);
);

Reference

CommandMeaning
pointsnumber of vertices, 3 to 3600. Default 3, or 180 when r, x or y changes with t. It may also list the corners as x y pairs: points: 0 1, 1 -1, -1 -1 is a triangle. split is another name for it.
rradius as an expression in t, the angle from 0 to 2π. Default 1.
x, ycoordinates as expressions in t, used when there is no r
rotatecounterclockwise rotation in degrees; .25turn is 90
scalescale factor, or two factors for x and y
moveoffset x y, in units of the -1 to 1 space, with y pointing up
mirrorx adds the left-right reflection, y the top-bottom one, and the shape and its reflections become one shape. Gives a polygon().
turnhow many times around the circle the points go. Default 1.
fill-ruleevenodd or nonzero for the polygon fill rule. fill is a legacy name for it, except in @svg-polygon, where fill is the paint.
framedraw the shape as an outline: a band this wide inside it, in units of the -1 to 1 space
roundturns every corner into a curve of this radius, in units of the -1 to 1 space, as far as the sides allow. Gives a shape() instead of a polygon().
growpushes the outline out by this much, or in when negative, in units of the -1 to 1 space. Also an expression in t, which here runs from 0 to 2π by length along the outline.
trewrites the angle of every point, as an expression in t and i
textletters traced into the outline in the browser and fitted to the box by their ink (Letters and paths). Quotes keep commas, ; and parentheses.
fontthe style, weight and family of text, as in the CSS font shorthand without a size. Default sans-serif.
pathSVG path data as the outline, fitted to the box, with y pointing down as in SVG. Quote it when it has commas.
smoothon a group: the radius of the curve where its shapes meet
seedthe seed of @R.t noise. Defaults to the doodle's.
dirno effect on the polygon; used by @plot. direction is another name for it.
unitthe unit of the coordinates, mostly for @plot. Default %; any other unit, also one on r such as r: 10px, gives raw numbers centered at 0 0.
nameany other name declares a variable for the expressions to read

Each expression sees the angle t (or θ) and the point index i, starting at 1. The arithmetic is the same as in Expressions, with seq(a, b, …) and range(a, b) on top. Inside a call such as cos(), a number in deg or turn reads as radians, so cos(60deg) is .5; radians() and degrees() convert between the two. Every number a command takes may be an expression in the variables, such as rotate: k * 30. The parts of move and scale split at spaces or commas, and an operator keeps its operands, so move: k * .5 0 has two parts. move, rotate, scale, mirror, grow and frame run in the order written; for every other command the last one wins. A comma list in points, r, t, x, y, rotate or grow draws one contour per item (Several contours).

In JavaScript

The same code gives the same value as a string, without a doodle, through shape() from css-doodle/generator. text needs a browser to trace the letters; elsewhere, as in Node, it draws nothing.

See also