Home

What's new in css-doodle

20 December 2020

I planned to rewrite css-doodle from scratch a long time ago, but there's barely any progress. One reason is that I wanted to make it too perfect but was stuck at the part of parsing all the time. Another reason is creating new things was more attractive to me than tools building whenever I got some time or inspiration.

I then changed my mind recently, instead of rewriting from scratch, making improvement little by little seems to be more promising. There have been many updates for the past month to give css-doodle a new look. I'd like to share those exciting new features.

.export()

Screenshot at client side has always been a hard problem if it's not SVG or canvas. Unfortunately, the most commonly used html2canvas does not work well for Web Components. For a long time I thought using a command line tool with Puppeteer was the best solution I could find.

Until I learned about the SVG <foreignObject> the other day, which can embed HTML inside it to make the whole thing a valid SVG. So I put everything of Shadow DOM generated by css-doodle into <foreignObject>, and then using canvas to get the SVG image data. Problem sovled!

<svg xmlns="http://www.w3.org/2000/svg">
  <foreignObject width="100%" height="100%">
    <!-- css-doodle code -->
  </foreignObject>
</svg>

Thanks to SVG, keep viewBox to the actual element size, increase the width and height will get large dimensions of the image. That's extremely useful for print purpose.

<svg viewBox="0 0 600 600" width="8000px" height="8000px"></svg>

The .export() API uses this technique and wraps everything up, it will export css-doodle as an image right in the browser. See example and all the options here.

let doodle = document.querySelector('css-doodle')

doodle.export({
  scale: 10,
  download: true
})

@doodle()

Since css-doodle can be exported as an image now, why not to generate image from code on the fly inside itself? Properties like background, border-image and mask all accepts image data as value.

It's just an idea at first but soon proved to be the right direction to open up a new dimension.

backgrund: @doodle(
  @grid: 5 / 100%;
  background: @p(#000, #fff);
);

Issues with background patterns before

To fill as many space of background as possible, creating more DOM elements might be an option. The max number of elements css-doodle can make is 1024. Even if there's enough of them, the performance gets worse as the number of elements increases.

There also lacks a way to transform background in CSS like patternTransform does in SVG, which makes it difficult to create some classic patterns by tiling, like this one.

How @doodle() solves the issues

We build the tiling fragment with css-doodle as background image. Together with background-size and background-repeat, only one element is needed to fill up the entire screen no mater how big it is. So it performs farely well.

The @doodle() function can also be nested. It's possible to do any kinds of transformations with transform property, or applying any other styles to the image itself. Because everything inside @doodle() is image.

No need to wait for the browser to support something like background-transform anymore.

@grid: 1 / 100% 180px;
background: @doodle(
  :doodle {
    @grid: 1 / 2000px;
    transform: rotate(-135deg) scale(2);
  }
  background: @doodle(
    @grid: 6x1 / calc(100% + 1px);
    @size: calc(100% - 100% / @I * (@i - 1));
    position: absolute;
    border: 1px solid black;
    background: @pn(
      crimson,darkorange,gold,forestgreen,royalblue,rebeccapurple
    );
  ) 0 / 18px 18px;
);

@grid: 1 / 100% 180px; background: @doodle( :doodle { @grid: 1 / 2000px; transform: rotate(-135deg) scale(2); } background: @doodle( @grid: 6x1 / calc(100% + 1px); @size: calc(100% - 100% / @I * (@i - 1)); position: absolute; border: 1px solid black; background: @pn( crimson, darkorange, gold, forestgreen, royalblue, rebeccapurple, ); ) 0 / 18px 18px; );

Another quick example using @doodle():

@grid: 1 / 100% 180px;
background: @doodle(
  :doodle {
    @grid: 1 / 2000px;
    transform: rotate(45deg) scale(1.5);
  }
  background-size: 20px 20px;
  background-image: @doodle(
    @grid: 2 / 100%;
    transform: scale(.84, .6) skewY(@pn(±37deg));
    border-radius: 2px;
    background: @pn(
      crimson, darkorange, gold, royalblue
    );
  );
);

@grid: 1 / 100% 180px; background: @doodle( :doodle { @grid: 1 / 2000px; transform: rotate(45deg) scale(1.5); } background-size: 20px 20px; background-image: @doodle( @grid: 2 / 100%; transform: scale(.84, .6) skewY(@pn(±37deg)); border-radius: 2px; background: @pn( crimson, darkorange, gold, royalblue ); ); );

I'm quite excited about this function. Can't wait to build tons of background patterns using css-doodle!

@shape()

This is another exciting function! It will generate a polygon() in string used by clip-path. For example, to make a hexagon:

clip-path: @shape(
  split: 6;
);
@grid: 1 / 80px; background: #000; clip-path: @shape( split: 6; );

Rotate it by 30 degree and scale it down:

clip-path: @shape(
  split: 6;
  rotate: 30;
  scale: .5;
);
@grid: 1 / 80px; background: #000; clip-path: @shape( split: 6; rotate: 30; scale: .5; );

If the split value is big enough it approximately to be a circle.

clip-path: @shape(
  split: 100;
);
@grid: 1 / 80px; background: #000; clip-path: @shape( split: 100; );

The x and y are defined with trigonometric functions. The t stands for Theta, each of them equals 360 / split degree.

clip-path: @shape(
  split: 800;
  x: cos(5t) * cos(5t) * sin(t);
  y: sin(2t) * sin(2t) * cos(5t);
);
@grid: 1 / 80px; background: #000; clip-path: @shape( split: 800; scale: .9; x: cos(5t) * cos(5t) * sin(t); y: sin(2t) * sin(2t) * cos(5t); );

As you may have noticed, 2t means 2 * t, it's much shorter this way to follow common notation in mathematics. All the Math functions including the symbol π can be used directly.

You can also define variables if the expressions are too long.

clip-path: @shape(
  split: 800;
  scale: .5;
  k: π - 7t;
  x: cos(2t) + cos(k);
  y: sin(2t) + sin(k);
);
@grid: 1 / 80px; background: #000; clip-path: @shape( split: 800; scale: .5; k: π - 7t; x: cos(2t) + cos(k); y: sin(2t) + sin(k); );

I hope the above examples explain well to the @shape() function. There are countless shapes waiting for you to explore!

Other updates

The @m function behaves like the normal for loop but much readable, now it accepts two nested iterations. See the demo.

box-shadow: @m9x8(

);

By default the output values are separated with commas. An uppercase @M alias is added to make the values seperated with spaces.

filter: @M5(
  drop-shadow(/*... */)
);

What's next

There remains a lot work to be done, such as documentation, the new paser, tests and performance improvement. And I still have several new ideas that haven't been implemented yet. Only wish I had more time.

It’s easy to make your mind rigid and ignore other things when you stick to one thing for too long. Maybe it's time to try something else other than CSS. So while waiting for CSS to evolve, I'll put part of my energy to new things.

Thanks to everyone who supports css-doodle via open collective and who sent me the encouraging words.