Function

@shape

A polygon() for clip-path, from a preset name or from polar equations in the angle t. With rounded corners it is a shape().

Syntax

  • @shape(<preset> [<k>])
  • @shape(<command>: <value>; ...)
  • @shape(<preset> [<k>]; <command>: <value>; ...)

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.

Polar curves

The points go once around the center at evenly spaced angles t, from 0 to 2π. points sets how many there are, 3 by default, and r how far each one is from the center, 1 by default. 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: 8. 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);

Size and position

scale takes one factor, or two for x and y. rotate turns the shape clockwise, in degrees. move shifts it in the -1 to 1 space, before the scale is applied:

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

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 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: evenodd;
);

Several contours

A comma in points, r, t, x, y, rotate or edge 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: 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: 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: 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 outlines each contour, @plot spreads its cells over the contours by length, @tile and @plot.scatter read the region with its holes, and @svg-polygon draws each contour as its own subpath.

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.

Outlines

frame draws only the outline, this thick in hundredths of the -1 to 1 space. Any other name declares a variable, like k here, for the expressions that follow it:

@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: 12;
  scale: .6;
);

Corners and edges

round curves every corner, from 0 to 1: the curve starts that part of the way to the middle of each side, so 1 leaves no straight side at all. The result is a CSS shape() that draws the curves exactly, instead of a polygon(). It stays a polygon with edge or frame, or when the corners are too slight to need a curve, as on a circle.

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

edge pushes the outline out along its normal, or in when negative. It is 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;
  edge: .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, or an expression such as 4n. Default 3. 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
rotateclockwise rotation in degrees, or an expression
scalescale factor, or two factors for x and y
moveoffset of the origin, in units of the -1 to 1 space
turnhow many times around the circle the points go. Default 1.
fillevenodd or nonzero for the polygon fill rule
framedraw the shape as an outline of this thickness, in hundredths of the -1 to 1 space (doubled when turn is above 1)
roundcurves every corner, from 0 to 1: the curve starts that part of the way to the middle of each side. Gives a shape() instead of a polygon().
edgepushes the outline along its normal, outward when positive, in units of the -1 to 1 space. 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
seedthe seed of @R.t noise. Defaults to the doodle's.
dirno effect on the polygon; used by @plot
nameany other name declares a variable for the expressions that follow

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. A - before a command name negates its value. A comma list in points, r, t, x, y, rotate or edge 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.

See also