@tile
Cuts a shape into pieces and gives each cell one of them, as a value for clip-path.
When every cell covers the whole doodle, the pieces fit back together into a mosaic.
Syntax
- @tile([<preset>; ]<commands>)
- @tile.<kind>([<preset>; ]<commands>)
- @tile.x, @tile.y, @tile.xy, @tile.kind, @tile.r
The shape is a preset from the @shape property, such as heart or circle,
or @shape commands that draw one. It defaults to square.
The kind picks how the shape is cut; @tile on its own is @tile.slice.
The commands are listed in the table below.
The kinds
Each one below cuts the default square into 30 pieces, and its name links to the section that covers it:
One piece per cell
Each cell gets the piece at its index, clipped from the whole doodle, so every cell has to cover the doodle.
@tile sees to that: it adds @place: center and @size: 100%, leaving out whichever of the two the doodle sets itself.
gap leaves space between the pieces, in percent of the element, and the doodle's background shows through it:
@grid: 24x1 / 60vmin / #111;
background: @p(
#f2efe6, #f2efe6, #f2efe6,
#d7301f, #1f4e9c, #f5c400
);
clip-path: @tile(gap: 1.5);
The number of cells is the number of pieces. Their rows and columns don't matter, since every cell covers the doodle,
so the examples here put them in one row: @grid: 60x1 gives 60 pieces, where @grid: 60 would give 3600.
points sets a smaller number of pieces,
and cells without a piece get polygon(0 0), which shows nothing; a larger number changes nothing.
It counts pieces, not the outline's corners as in @shape,
so leave it out of a body copied from there.
The pieces are laid out on a square, so on a doodle twice as wide as it is tall they come out twice as wide too.
aspect gives the element's proportions, as one number or a fraction:
@grid: 60x1 / 12rem 6rem / #3b2410;
background: hsl(@r(34, 44) 92% @r(52, 64)%);
clip-path: @tile.hex(aspect: 2 / 1; gap: 1.2); Slices
@tile.slice starts from the shape's bounding box and keeps cutting a random piece in two, across its longer side.
Bigger pieces are more likely to be cut. The cuts are level or upright; spread lets them tilt by up to that many degrees.
Each piece here is filled with stripes at its own angle:
@grid: 30x1 / 60vmin;
background: repeating-linear-gradient(
@r(180)deg, #000 0 2px, #fff 0 6px
);
clip-path: @tile(spread: 90); density works here too: pieces where it is larger are cut more often, so they end up smaller.
Around scattered points
@tile.voronoi spreads points inside the shape, the same points as @plot.scatter,
and gives each point the region closer to it than to any other. The regions fill the whole box, not only the shape:
@grid: 60x1 / 60vmin / #1b1b1b;
background: @p(
#264653, #2a9d8f, #e9c46a, #f4a261, #e76f51
);
clip-path: @tile.voronoi(gap: 1); @tile.delaunay joins the same kind of points into triangles and keeps the ones inside the shape.
For these two kinds, points is the number of points, not pieces.
delaunay starts from about half as many points as cells, since there are about twice as many triangles as points.
When there are still more triangles than cells, the largest ones on the outside go first, so the inside keeps no holes:
@grid: 60x1 / 60vmin;
background: #000;
clip-path: @tile.delaunay(circle; points: 9; gap: 1); density is an expression in x and y, the -1 to 1 space with y pointing up.
More points land where it is larger, so the pieces get smaller there:
@grid: 120x1 / 60vmin / #000;
background: #fff;
clip-path: @tile.voronoi(density: 1 - y; gap: .6);
Where the points land depends on the doodle's seed,
or on the seed command when given, so the same seed gives the same pieces.
slice and circle use the seed the same way.
The points are evened out in 10 rounds. relax sets how many, up to 50; relax: 0 leaves them where they fell,
so the pieces vary more in size and shape.
Shaping the regions
crack groups the regions of @tile.voronoi into larger ones, about that many regions each.
With two values, gap sets the space between the groups and then the space inside them;
the second defaults to a quarter of the first. @tile.kind is the group's, so a group shares a color:
@grid: 300x1 / 60vmin / #1b1b1b;
clip-path: @tile.voronoi(crack: 12; relax: 0; gap: 1.6 .3);
background: oklch(.8 .1 calc(@tile.kind * 72)); stretch makes the regions that many times longer, along an angle that turns clockwise like rotate.
stretch: 3 lies level, stretch: 3 90 stands upright:
@grid: 160x1 / 60vmin / #0c1d1f;
background: linear-gradient(#5fd3c7, #155e75);
clip-path: @tile.voronoi(stretch: 2.5 90; round: 1; gap: .7); Lattices
@tile.grid, @tile.hex, @tile.triangle, @tile.cube and @tile.penrose lay a regular tiling over the shape.
They pick the largest piece size that fits the cells and keep every piece that reaches into the shape, so the edge is jagged:
@grid: 60x1 / 60vmin / #3b2410;
background: hsl(@r(34, 44) 92% @r(52, 64)%);
clip-path: @tile.hex(circle; gap: 1.2); grid is a grid of squares, cube splits each hexagon into the three faces of a cube,
and penrose is a Penrose tiling of thin and thick rhombs.
A lattice depends only on the shape and the count, so seed does not change it,
unless it has jitter or its edge calls random() or @R.t.
shift slides every other row of a grid by that part of a square, so .5 lays bricks.
With two values, shift: 0 .3, the first is zero and every other column slides down instead.
Whole numbers change nothing:
@grid: 60x1 / 60vmin / #eee6dc;
background: hsl(@R(120, 240, 2, 8) 60% 50%);
clip-path: @tile.grid(shift: .5; gap: .8); jitter moves every corner of a lattice in a random direction, by up to that part of a piece's size.
Neighbours move the shared corner together, so the pieces still fit.
Above about .3, pieces start to fold over each other. It follows the seed.
Bending the edges
edge bends every edge of a grid, hex or triangle lattice along a curve.
It is an expression in t, which runs from 0 to 2π along the edge,
and its value pushes the edge sideways, as a part of the edge's length.
Both neighbours of an edge share the same curve, so the pieces still fit together:
@grid: 40x1 / 60vmin / #222;
background: hsl(@r(360) 60% 70%);
clip-path: @tile.grid(edge: .15 * sin(t); gap: .6);
The curve is tilted so it starts and ends on the corners of the edge, whatever its value is at 0 and 2π.
On a triangle, only the part that is the same turned half around the middle of the edge is kept,
so sin(t) keeps its shape but a bump like sin(t / 2) flattens out.
e is the direction of the edge. On a grid, 1 is level and 2 upright,
so this bends only the upright edges:
@grid: 40x1 / 60vmin;
background: @p(#0f4c5c, #e36414, #fb8b24, #9a031e);
clip-path: @tile.grid(edge: (e - 1) * .12 * sin(t));
On a triangle, 1 is level; on a hex, 2 is upright. The other values are the two slanted directions.
random() is drawn once per edge, so every edge gets its own curve, like the cuts of a jigsaw puzzle.
It follows the doodle's seed, or the seed command:
@grid: 60x1 / 60vmin / #1b1b1b;
background: hsl(@r(190, 230) 55% @r(45, 70)%);
clip-path: @tile.hex(
edge: .25 * sin(t) * (random() - .5);
gap: .5
); @R.t gives every edge a wobble of its own instead of one curve made larger or smaller. The noise follows the seed too:
@grid: 40x1 / 60vmin / #222;
background: hsl(@r(360) 60% 70%);
clip-path: @tile.grid(edge: @R.t(-.2, .2); gap: .6); x and y are the middle of the edge, in the -1 to 1 space with y pointing up,
so the curve can change across the shape. Here the edges are straight in the middle and wavier towards the sides:
@grid: 80x1 / 60vmin;
background: #000;
clip-path: @tile.triangle(
edge: .12 * x * sin(2t);
gap: .5
);
The expression can read other commands, as in a: .1 * x; edge: a * sin(2t).
edge has no effect on cube, penrose or the other kinds, nor with op or star.
slide moves the points of an edge along it instead of across it, as a part of the edge's length.
It reads the same values as edge. Together they draw curves that fold back, which edge alone cannot:
@grid: 40x1 / 60vmin / #222;
background: hsl(@r(360) 60% 70%);
clip-path: @tile.hex(
edge: .15 * sin(t);
slide: .2 * sin(t / 2)^2;
gap: .6
);
Each edge is matched with the opposite edge of its neighbour, so every piece is the same shape, moved.
pair: turn matches it with the next edge around a corner instead, as in Escher's lizards:
every piece is the same shape, turned. @tile.kind tells the turns apart,
in steps of 120° on a hex and 90° on a grid, and e numbers the pairs of edges.
It works on hex and on grid without shift:
@grid: 48x1 / 60vmin / #222;
clip-path: @tile.hex(
pair: turn;
edge: .3 * sin(t / 2)^2 * (e - 2) + .1 * sin(t);
gap: .5
);
background: oklch(.78 .11 calc(@tile.kind * 120)); Operators
op turns the pieces into a new tiling, one step per letter.
The letters come from Conway's polyhedron notation.
op: t cuts every corner off, so the squares of a grid become octagons with small squares between them:
@grid: 100x1 / 60vmin / #222;
clip-path: @tile.grid(op: t; gap: .8);
background: @match(@tile.kind == 1, #ebbf4d, #e6437d);
It works on grid, hex, triangle, cube, penrose and voronoi.
Here is every letter on a hex; click one to open it in the playground:
-
hexbefore any op -
op: da piece for each corner -
op: acorners cut to the middle of each side -
op: tcorners cut off -
op: keach piece split into triangles -
op: epieces pulled apart -
op: btandetogether -
op: spulled apart with a twist -
op: gthe same asds -
op: jthe same asda -
op: mthe same askda -
op: othe same asde
The colors come from @tile.kind, which tells the new shapes apart.
Pieces grown from the old ones keep their number, 1 in yellow.
Pieces at the old corners get the next number, 2 in pink,
and the pieces that e, b and s put along the old sides get the one after, 3 in white.
d makes every piece 1, and k keeps the number of the piece it splits.
Letters combine, and run right to left as in Conway's notation: op: ds applies s first, then d.
On a grid that gives the Cairo tiling of pentagons, which g writes in one letter:
@grid: 80x1 / 60vmin / #222;
clip-path: @tile.grid(op: g; gap: .8);
background: hsl(@r(190, 230) 55% @r(45, 70)%); The letters table at the end describes every step in full. A few more details:
pointscounts the pieces after the last step.- On
grid,hexandtriangle, every letter butk,g,mandokeeps all sides the same length. - Other letters are skipped, so
tQcopied from a list of tilings works ast. - On a
voronoi,opdoes nothing whencrackorstretchis set.
Stars
star draws a star inside every piece, the way Islamic star patterns are drawn.
Two lines leave the middle of each side at the given angle, between 0 and 90 degrees, and meet inside the piece.
Small angles give fat stars and large ones thin stars. The space left around each corner becomes a piece of its own, in pink here:
star works on the same kinds as op and comes after it, so the two combine.
Each star keeps the @tile.kind of its piece and the corner pieces get the next number.
On the octagons and squares of op: t, 67.5 gives eight-pointed stars and crosses, with the corner pieces 3 in white:
@grid: 150x1 / 60vmin / #000;
clip-path: @tile.grid(op: t; star: 67.5; gap: .5);
background: @match(@tile.kind == 3, #fff, #e6437d); Packed circles
@tile.circle packs circles into the shape, largest first, and returns each as an ellipse().
The shape can be drawn with @shape commands. Here r: cos(2t) is a four-petal rose:
@grid: 160x1 / 60vmin;
background: red;
clip-path: @tile.circle(r: cos(2t); gap: .4); size limits how big a circle can grow where it is centered, in average radii.
By default it is 1.5 (less below nine circles), and a rare few circles may grow larger, more so the more circles there are,
so the largest step down in size rather than all stopping at one. A single circle fills the shape.
It is an expression in x and y, and 0 leaves no circle centered there.
random() in it follows the seed.
Round corners
round turns the corners of every piece into curves, from 0 to 1:
the curve starts that part of the way to the middle of each edge, so 1 leaves no straight edge at all.
It works on every kind except circle, after the gap is taken.
A rounded piece comes out as a CSS shape() with true curves, not a polygon():
@grid: 100x1 / 60vmin / #2d2a26;
background: hsl(30 8% @r(62, 80)%);
clip-path: @tile.voronoi(round: 1; gap: 1.4); Reading the piece
Once a cell has its piece, @tile.x and @tile.y give the piece's center, as percentages of the element.
They make good amounts for color-mix():
@grid: 60x1 / 60vmin;
clip-path: @tile.voronoi(gap: 1);
background: color-mix(in oklch, #264653 @tile.y, #e9c46a);
In an expression the percent sign is dropped, so @match(@tile.x < 50, …) picks the pieces on the left half.
A little randomness on top of the height shades the triangles like a low-poly model:
@grid: 100x1 / 60vmin;
clip-path: @tile.delaunay(circle);
background: color-mix(in oklch,
#0b2545 calc(@tile.y + @r(-12, 12)%), #b9d6f2
); @tile.xy gives both as x y, the way a position is written, as in a gradient's at.
Each region here is lit from its center:
@grid: 60x1 / 60vmin / #000;
clip-path: @tile.voronoi(gap: .6);
background: radial-gradient(8% 8% at @tile.xy,
#fff 15%, #48cae4 30%, #023e8a
); @tile.kind tells pieces apart. In cube, 1 is the top face, 2 the left and 3 the right:
@grid: 90x1 / 60vmin;
clip-path: @tile.cube();
background: oklch(calc(1 - @tile.kind * .22) .05 250); In penrose, 1 is a thin rhomb and 2 a thick one:
@grid: 150x1 / 60vmin;
clip-path: @tile.penrose(gap: .4);
background: color-mix(in oklch,
#00539c calc((@tile.kind - 1) * 100%), #ffd662
); In voronoi, no two neighboring regions share a kind, so coloring by kind never puts one color next to itself:
@grid: 100x1 / 60vmin;
clip-path: @tile.voronoi(gap: .5);
background: oklch(.75 .12 calc(@tile.kind * 72)); @tile.r is the radius of a circle piece. Bigger circles are redder here:
@grid: 150x1 / 60vmin;
clip-path: @tile.circle(circle; gap: .6);
background: color-mix(in oklch,
#d00000 calc(@tile.r * 8), #ffb3c1
);
These read the cell's last @tile, so put them after it.
Before it, or in a cell without a piece, they are empty.
Moving the pieces
In clip-path, @tile also sets the cell's transform-origin to the center of its piece.
So scale, rotate and their animations work on each piece in place:
@grid: 40x1 / 60vmin;
background: #000;
clip-path: @tile.hex(circle);
animation: pulse 2s infinite alternate;
animation-delay: @r(-2s);
@keyframes pulse {
to { scale: .3 }
} Reference
Kinds
| Kind | Pieces | Kind values |
|---|---|---|
slice | the bounding box cut by straight lines (the default) | 1 |
voronoi | regions around points scattered inside the shape, filling the box | 1, 2, 3… (usually up to 5), never the same as a neighbor; with crack, the group's |
delaunay | triangles between scattered points, inside the shape | 1 |
grid | a grid of squares | 1 |
hex | a grid of hexagons | 1 |
triangle | a grid of triangles | 1 pointing down, 2 up |
cube | hexagons split into three rhombs | 1 top, 2 left, 3 right |
penrose | a Penrose tiling of rhombs | 1 thin, 2 thick |
circle | circles packed inside the shape, largest first | 1 |
Commands
| Command | Kinds | Meaning |
|---|---|---|
gap | every kind | space between pieces, in percent of the element. With crack, a second value sets the space inside a group (default a quarter of the first). |
aspect | every kind | the element's width over its height, as 2 or 16 / 9, so pieces keep their shape. Default 1. |
points | every kind | at most this many pieces; for voronoi and delaunay, the number of points. Defaults to the number of cells, or about half of it for delaunay; a larger value changes nothing. |
seed | every kind | a fixed seed for slice, voronoi, delaunay, circle and jitter, for random() in edge, size and density, and for @R.t in edge. Defaults to the doodle's seed. |
round | every kind but circle | curves the corners, from 0 to 1, so the piece is a shape() |
| shape commands | every kind | r, rotate, scale, move and the rest of @shape's commands change the shape being cut |
spread | slice | how far a cut may tilt, in degrees. Default 0. |
density | slice, voronoi, delaunay | where pieces are smaller, as an expression in x and y |
relax | voronoi, delaunay | rounds that even out the points, up to 50. Default 10; 0 keeps them random. |
crack | voronoi | groups the regions, about this many each |
stretch | voronoi | <ratio> [<angle>], regions this many times longer along the angle (degrees, clockwise), up to 1000 |
shift | grid | slides every other row by this part of a square; 0 <n> slides every other column instead. Default 0. |
jitter | grid, hex, triangle, cube, penrose | moves each corner by up to this part of a piece's size. Keep it below about .3. |
edge | grid, hex, triangle | bends each edge sideways, as a part of its length. An expression in t (0 to 2π along the edge), e (the edge's direction), x and y (its middle), random() (drawn once per edge) and @R.t (noise of its own on each edge). No effect with op or star. |
slide | grid, hex, triangle | moves each edge's points along it, as a part of its length. The same values as edge. |
pair | grid, hex | turn matches each edge with the next one around a corner, so the pieces are one shape turned by @tile.kind. Not on a grid with shift. |
op | grid, hex, triangle, cube, penrose, voronoi | builds a new tiling, one step per letter, applied right to left; see the letters. Not on a voronoi with crack or stretch. |
star | the same as op | turns every piece into a star, with lines leaving each side's middle at this angle, in degrees between 0 and 90. Applied after op. |
size | circle | the largest radius where a circle is centered, in average radii, as an expression in x and y. Default about 1.5, with a few larger. |
Operator letters
| Letter | Step |
|---|---|
d | the dual: one piece for each corner, joining the centers of the pieces around it |
a | cuts every corner off, down to the middle of each side |
t | cuts every corner off |
k | splits every piece into triangles around its center |
e | pulls the pieces apart, with a square along each side and a piece at each corner |
b | t and e at once |
s | pulls the pieces apart with a twist, with triangles in the gaps |
g | short for ds, the dual of s |
j | short for da, the dual of a |
m | short for kda, k of j |
o | short for de, the dual of e |
Values of the cell's piece
| Function | Value |
|---|---|
@tile.x, @tile.y | the center of the piece, in percent of the element |
@tile.xy | the same center as x y |
@tile.kind | which kind of piece it is, from the table above |
@tile.r | the radius of a circle piece, in percent of the element's shorter side; empty for other kinds |