Guide

Addons

Extra functions for css-doodle, released on their own in @css-doodle/addons. Name one with @use and the doodle loads it before it draws.

Using an addon

@use: marble;
@grid: 1 / 60vmin;
background: @marble(
  colors: #f4ecd8, #1d3557, #e63946, #f1c453, #2a9d8f
);

Several names go in one list, as in @use: pigment, wfc, next to any var(--rule) items of @use. Click an example on this page to draw it again with a new seed.

AddonFor
gradient-2d mesh and freeform 2D gradients
marble paper marbling and ink in water
pigment colors that mix like paint
spring a spring-like easing
trace the outline of an image as an SVG path
wfc wave function collapse

gradient-2d

@mesh-gradient() and @freeform-gradient() draw the 2D gradients proposed for CSS with WebGL.

  • @mesh-gradient([in <space>,] <row>, <row>, …)
  • @freeform-gradient([in <space>,] <color> [<spread>] at <position>, …)

A mesh is rows of colors, top row first. <color> at <x> <y> moves one point:

@use: gradient-2d;
@grid: 1 / 60vmin;
background: @mesh-gradient(
  #f43 gold #0bf,
  #a0f white at 40% 60% #0c6,
  navy #f0a teal
);

In a freeform gradient each color spreads out from its position. A position or a spread also takes math in parentheses, as in @pattern, and t, the time in seconds, moves it. A moving gradient draws faster in @content, a live canvas, than in background. Each call of $drift() below gets its own @r() speeds, so every point wanders its own way, and a click sends them on new paths:

@use: gradient-2d;
@grid: 1 / 60vmin;
--drift(): (50% + 30% * sin(t * @r(.2, .6) + @r(9))
  + 15% * sin(t * @r(.6, 1.2) + @r(9)));
@content: @freeform-gradient(
  #f56 at $drift() $drift(),
  gold at $drift() $drift(),
  #0c9 at $drift() $drift(),
  #2b2d86 at $drift() $drift()
);

marble

@marble() is paper marbling, ink drops combed into feathers. @marble.ink() is clouds of ink in water. Where the inks meet they mix like paint.

  • @marble(colors: <color>, <color>, …; drops: <n>; size: <percentage>; comb: <spacing> [<angle>], …; flow: <n>)
  • @marble.ink(colors: <color>, <color>, …; scale: <n>; warp: <n>; flow: <n>)

colors is the paper, then the inks. drops and size set how many drops fall and how big they are, and each comb is pulled over the one before it:

@use: marble;
@grid: 1 / 60vmin;
background: @marble(
  colors: white, #002185, #fcd200;
  drops: 60;
  size: 6%;
  comb: 5% 90deg, 14% 0deg;
);

In @marble.ink() the first color is the water. scale sets how many clouds fit, and warp how much they curl. flow makes them drift:

@use: marble;
@grid: 1 / 60vmin;
@content: @marble.ink(
  colors: #111, #ffd166, #ef476f, #06d6a0;
  scale: 1.5;
  warp: 3;
  flow: 1;
);

In @marble(), flow ripples the feathers. Any number option also takes math with t, as in gradient-2d, so drops: min(t * 5, 60) lets the drops fall in one by one.

pigment

@pigment() mixes colors the way paint does, so blue and yellow make green instead of gray. Percentages work as in color-mix().

  • @pigment(<color> [<percentage>], <color> [<percentage>], …)

Blue gets more to the right and yellow more to the bottom, and red has what is left. At the bottom right, where color-mix() would make gray, they make green:

@use: pigment;
@grid: 7 / 60vmin;
@size: 86%;
border-radius: 50%;
background: @pigment(
  #002185 $((@x - 1) / 6 * 100)%,
  #fcd200 $((@y - 1) / 6 * 100)%,
  #d7192e
);

spring

@spring() is an easing that overshoots and settles, for animation or transition.

  • @spring([<bounce>])

<bounce> goes from 0 to .95, and is .3 by default. Each row below bounces more than the one above:

@use: spring;
@grid: 1x5 / 60vmin;
background: radial-gradient(
  closest-side, #1d3557 85%, #0000 90%
) 0 0 / 20% 100% no-repeat;
animation: slide 2s @spring($((@y - 1) * .2)) infinite;
@keyframes slide {
  from { translate: 0 }
  70%, to { translate: 50% }
}

trace

@trace() turns an image into an outline, for path: anywhere a shape goes.

  • @trace(<image>, [<level>])

The ink is the opaque part of the image, or the dark part when it has no transparency. <level> is where the ink starts, 50% by default:

@use: trace;
@grid: 1 / 60vmin;
background: #1d3557;
clip-path: @shape(path: @trace(/assets/wukong.jpg, 70%));

The path is in the image's own pixels, so in an @svg as big as the image, outlines at two levels line up as a print in two inks:

@use: trace;
@grid: 1 / 60vmin;
background: #fbe9d0 @svg(
  viewBox: 0 0 640 640;
  path*2 {
    d: @trace(/assets/wukong.jpg, @pn(30%, 70%));
    fill: @pn(#e63946, #1d3557);
  }
);

An image from another site needs CORS headers.

wfc

Wave function collapse, after Maxim Gumin's WaveFunctionCollapse. @wfc() fills the grid so it looks like a small sample, and @wfc.tiles() so tiles match at their edges.

  • @wfc(sample: <sample>; legend: <key> <value>, …; n: <2|3>; symmetry: <1-8>; ground)
  • @wfc.tiles(<value> <sides> [<weight>], …)
  • @wfc.turn(), @wfc.order()

The sample has one character per pixel, and legend gives characters a value. ground keeps the bottom row at the bottom; use it with symmetry: 2 so nothing is turned upside down:

@use: wfc;
@grid: 24 / 60vmin;
background: @wfc(
  sample:
    ................
    ..ooo...........
    ..o*o......ooo..
    ..ooo......o*o..
    ...|.......ooo..
    ...|l.......|...
    ..l|........|...
    ...|.......l|...
    ...|........|l..
    ################;
  legend: . #fbe9d0, o #e63946, * #f1c453,
    | #2d6a4f, l #52b788, # #6b4423;
  symmetry: 2;
  ground;
);

A tile is a value and the labels of its top, right, bottom and left sides. Every turn of a tile is tried, and @wfc.turn() gives the one the cell needs, so here the crossing turns to put one rope or the other on top:

@use: wfc;
@grid: 12 / 60vmin;
:doodle { background: #1d3557 }
--rope: #0000 35%, #f1c453 0 65%, #0000 0;
--line: linear-gradient(90deg, var(--rope));
--corner: radial-gradient(
  circle at 100% 0, #0000 24.7%, #f1c453 0 46%, #0000 0
);
--cross: linear-gradient(90deg,
  #0000 25%, #1d3557 0 35%, #f1c453 0 65%,
  #1d3557 0 75%, #0000 0
), linear-gradient(var(--rope));
background: @wfc.tiles(
  var(--line) 1010, var(--corner) 1100, var(--cross) 1111
);
rotate: @wfc.turn();

@wfc.order() is when the cell was solved, from 0 to 1, so as an animation delay the doodle fills in that order. When a sample doesn't fit the grid it warns contradiction; another size or seed usually works.

Without @use

An addon is also a plain script, added after css-doodle:

<script src="https://unpkg.com/css-doodle/css-doodle.min.js"></script>
<script src="https://unpkg.com/@css-doodle/addons@1/marble.js"></script>
import 'css-doodle';
import '@css-doodle/addons/marble';

To serve the files yourself, set where they are before the first doodle draws:

customElements.get('css-doodle').addonBase = '/vendor/css-doodle-addons/';

Writing your own

CSSDoodle.use() adds a function. A value function gets the arguments and the cell, and returns CSS text:

customElements.whenDefined('css-doodle').then(() => {
  const CSSDoodle = customElements.get('css-doodle');
  CSSDoodle.use('stripes', {
    value: ([color = 'black'], { i }) => i % 2 ? color : 'transparent',
  });
});

Then @stripes(red) works in any doodle on the page. The addons README covers the rest.

See also