<?xml version="1.0" encoding="utf-8"?>

<feed xmlns="http://www.w3.org/2005/Atom" >
  <generator uri="https://jekyllrb.com/" version="4.3.3">Jekyll</generator>
  <link href="https://yuanchuan.dev/feed.xml" rel="self" type="application/atom+xml" />
  <link href="https://yuanchuan.dev/" rel="alternate" type="text/html" />
  
  
  <updated>2026-04-11T00:00:00+08:00</updated>
  <id>https://yuanchuan.dev/feed.xml</id>

  
    <title type="html">yuanchuan.dev</title>
  

  

  

  
  
    <entry>
      <title type="html">Multi-stroke text effect in CSS</title>
      <link href="https://yuanchuan.dev/multi-stroke-text-effect-in-css" rel="alternate" type="text/html" title="Multi-stroke text effect in CSS" />
      <published>2026-04-11T00:00:00+08:00</published>
      <updated>2026-04-11T00:00:00+08:00</updated>
      <id>https://yuanchuan.dev/multi-stroke-text-effect-in-css</id>
      <content type="html" xml:base="https://yuanchuan.dev/multi-stroke-text-effect-in-css"><![CDATA[<article>
  <p>
    I used to see that retro multi-stroke text effect quite often and
    tried to replicate it using the CSS <code>text-stroke</code> property,
    but the results never quite matched.
    Because <code>text-stroke</code> accepts a single value, stacking elements was the only workaround I could think of, though it didn't seem to work.
  </p>
  <!--more-->
  <p>
    One evening late last year,
    I was eager to give it another shot after seeing the text effect again in the book <a href="https://archive.org/details/graphicjapanfrom0000avel">Graphic Japan : from woodblock and zen to manga and kawaii</a>.
  </p>
  <figure>
    <img alt="pattern from book Graphic Japan : from woodblock and zen to manga and kawaii" src="https://yuanchuan.dev/assets/images/post/multiple-text-stroke/from-book.webp" />
    <figcaption><span>Text stroke effect from the book</span></figcaption>
  </figure>

  <p>
    I kept stacking several elements and accidentally varied the <code>text-stroke-width</code> for each layer.
    To my suprise, the result was getting closer this time.
  </p>

  <div class="example">
    <css-doodle><style>
      --c: #cc0d55;
      --n: @i(-1);
      @grid: 36x1 / 240px;
      @content: '✱';

      position: absolute;
      inset: 0;
      font: 100px/0 sans-serif;
      color: var(--c);
      will-change: transform;
      z-index: @I(-@i);

      -webkit-text-stroke-color: @pn(--c, #f4e1e8);
      -webkit-text-stroke-width: $em(.08n+.02(1-(-1)^n));
    </style></css-doodle>

<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="nt">--c</span><span class="o">:</span> <span class="nf">#cc0d55</span><span class="o">;</span>
<span class="nt">--n</span><span class="o">:</span> <span class="k">@i</span><span class="p">(</span><span class="m">-1</span><span class="p">);</span>

<span class="k">@grid</span><span class="p">:</span> <span class="m">36</span><span class="n">x1</span> <span class="p">/</span> <span class="m">240px</span><span class="p">;</span>
<span class="k">@content</span><span class="p">:</span> <span class="s2">'✱'</span><span class="p">;</span>

<span class="nt">position</span><span class="o">:</span> <span class="nt">absolute</span><span class="o">;</span>
<span class="nt">inset</span><span class="o">:</span> <span class="err">0</span><span class="o">;</span>
<span class="nt">font</span><span class="o">:</span> <span class="err">100</span><span class="nt">px</span><span class="o">/</span><span class="err">0</span> <span class="nt">sans-serif</span><span class="o">;</span>
<span class="nt">color</span><span class="o">:</span> <span class="nt">var</span><span class="o">(</span><span class="nt">--c</span><span class="o">);</span>
<span class="nt">z-index</span><span class="o">:</span> <span class="k">@I</span><span class="p">(</span><span class="n">-</span><span class="err">@</span><span class="n">i</span><span class="p">);</span>

<span class="nt">-webkit-text-stroke-color</span><span class="o">:</span> <span class="k">@pn</span><span class="p">(</span><span class="n">--c</span><span class="p">,</span> <span class="m">#f4e1e8</span><span class="p">);</span>
<span class="nt">-webkit-text-stroke-width</span><span class="o">:</span> <span class="err">$</span><span class="nt">em</span><span class="o">(.</span><span class="err">08</span><span class="nt">n</span><span class="o">+.</span><span class="err">02</span><span class="o">(</span><span class="err">1</span><span class="nt">-</span><span class="o">(</span><span class="nt">-1</span><span class="o">)^</span><span class="nt">n</span><span class="o">));</span></code></pre></figure>
  </div>

  <h2>How it works</h2>

  <p>
    For different values of the <code>text-stroke-width</code>,
    browsers will automatically draw outlines of the charater,
    The larger you set the stroke width, the thicker the outline will get,
    while still maintain its original shape.
  </p>

  <p>
    <css-doodle>
      @grid: 7x1/ 360px auto noclip;
      @content: '✱';
      font: 50px/0 sans-serif;
      color: transparent;
      -webkit-text-stroke-color: #cc0d55;
      -webkit-text-stroke-width: @i(*1.8px);
    </css-doodle>
  </p>

  <p>
    The next step is to use different colors and put them in order.
  </p>

  <p>
    <css-doodle>
      @grid: 12x1/ 100px ß #f4e1e8;
      @content: '✱';
      position: absolute;
      inset: 0;
      z-index: @I(-@i);
      font: 50px/0 sans-serif;
      color: transparent;
      -webkit-text-stroke-color: @pn(#f4e1e8, #cc0d55);
      -webkit-text-stroke-width: @i(*3px);
    </css-doodle>
  </p>

  <p>The interesting part is how the browsers outlining the character shapes differently.
    FireFox offers more smoother rendering than in Chrome and Safari.
  </p>

  <div class="compare">
    <figure>
      <img alt="" src="https://yuanchuan.dev/assets/images/post/multiple-text-stroke/chrome.webp" />
      <figcaption><span>Chrome/Safari</span></figcaption>
    </figure>

    <figure>
      <img alt="" src="https://yuanchuan.dev/assets/images/post/multiple-text-stroke/firefox.webp" />
      <figcaption><span>Firefox</span></figcaption>
    </figure>
  </div>

  <p>
    Another interesting part is when there are more text put inline, the character shapes will be merged.
  </p>

  <div>

    <css-doodle class="merge"><style>
      --c: #cc0d55;
      --n: @i(-1);

      @grid: 12x1 / 600px 300px / @p(--c);
      @content: '秋收冬藏';

      position: absolute;
      inset: 0;
      font: 70px/0 sans-serif;
      white-space: nowrap;
      color: var(--c);
      z-index: @I(-@i);

      -webkit-text-stroke-color: @pn(--c, #f4e1e8);
      -webkit-text-stroke-width: @i(*4px);
    </style></css-doodle>

<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="c">/* ... */</span>
<span class="k">@content</span><span class="p">:</span> <span class="s2">'秋收冬藏'</span><span class="p">;</span></code></pre></figure>

  </div>

  <h2>Trying different fonts</h2>

  <p>
    The final result really depends on the font you choose.
    To help experimenting with different fonts more quickly,
    I added the <code>@google-font</code> function for faster font loading.
  </p>

  <div>
    <css-doodle>
      --c: #cc0d55,#fff;
      @grid: 34x1 / 320px;
      @content: 'b';
      font: 150px/0 @google-font(Matemasie);
      @place: center;
      z-index: @I(-@i);
      color: @pn(--c);
      -webkit-text-stroke-color: @pn(--c);
      -webkit-text-stroke-width: @i(-1, ease, *12px);
    </css-doodle>
<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="nt">font-family</span><span class="o">:</span> <span class="k">@google-font</span><span class="p">(</span><span class="n">Matemasie</span><span class="p">);</span>
<span class="k">@content</span><span class="p">:</span> <span class="s2">'b'</span><span class="p">;</span></code></pre></figure>

  </div>


  <div>
    <css-doodle seed="1774291354234">
      --c: #cc0d55,#fff;
      @grid: 30x1 / 320px;
      :after {
         content: 'Love';
         position: absolute;
      }
      font: 80px/0 @google-font("Pacifico");
      @place: center;
      z-index: @I(-@i);
      color: @pn(--c);

      -webkit-text-stroke-color: @pn(--c);
      -webkit-text-stroke-width: @i(ease, -1, *15px);
    </css-doodle>
<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="nt">font-family</span><span class="o">:</span> <span class="k">@google-font</span><span class="p">(</span><span class="n">Tangerine</span><span class="p">);</span>
<span class="k">@content</span><span class="p">:</span> <span class="s2">'Love'</span><span class="p">;</span></code></pre></figure>

  </div>

  <div>
    <css-doodle seed="1774291354234">
      --c: #fff9e0,#f1c550,#ff6600,#ce2525;
      --c: #cc0d55,#fff;
      @grid: 30x1 / 320px +2 ~0 -10%;
      :after {
         content: '+';
         position: absolute;
      }
      @place: 50% 50%;
      font: 120px/0 @google-font("Cherry Bomb One");
      z-index: @I(-@i);
      color: @pn(--c);

      -webkit-text-stroke-color: @pn(--c);
      -webkit-text-stroke-width: @i(ease, -1, *12px);
    </css-doodle>
<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="nt">font-family</span><span class="o">:</span> <span class="k">@google-font</span><span class="p">(</span><span class="s2">'Cherry Bomb One'</span><span class="p">);</span>
<span class="k">@content</span><span class="p">:</span> <span class="s2">'+'</span><span class="p">;</span></code></pre></figure>

  </div>

  <p>
    Unfortunately, the performance is as bad as CSS filters, especially when the font-size is getting bigger,
    you may have noticed some flicking above.
    It's fine for experiments like this, or for generating images with css-doodle,
    but it's not well-suited for production usage.
  </p>

  <h2>More examples</h2>

  <p>
    Here are two more examples to play around with different colors and characters, generated with css-doodle, just for fun.
  </p>

  <div class="example image-row">
    <img alt="" src="https://yuanchuan.dev/assets/images/post/multiple-text-stroke/poster-1.webp" />
    <img alt="" src="https://yuanchuan.dev/assets/images/post/multiple-text-stroke/poster-2.webp" />
  </div>

  <p>
    CodePen link for the first one: <a href="https://codepen.io/yuanchuan/pen/ogzarGo">https://codepen.io/yuanchuan/pen/ogzarGo</a>
  </p>

</article>

<style>
  .article {
    max-width: 46em;
  }
  css-doodle {
    display: block;
    max-width: 100%;
  }

  css-doodle.merge {
    width: 480px;
  }

  css-doodle:not(:defined) {
    width: 240px;
    height: 240px;
    visibility: hidden;
  }

  .compare {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 5px;
  }

  .example {
    display: flex;
    margin-bottom: 2em;
  }

  .example figure {
    margin: 0;
    order: 1;
    overflow: auto;
  }
  .example figure pre {
    margin: 0;
  }
  .example .doodle,
  .example css-doodle {
    order: 2;
    margin-left: auto;
    flex-shrink: 0;
  }

  .image-row {
    display: grid;
    gap: 5px;
    grid-template-columns: 1fr 1fr;
  }
  .image-row img {
    width: 100%;
  }

  @media screen and (max-width: 720px) {
    .example {
      display: block;
    }
    .example css-doodle {
      margin: 0 0 1em 0;
    }
    .example .doodle css-doodle {
      margin: 0;
    }
  }
</style>

<script type="module">import 'https://esm.sh/css-doodle@0.50.2'</script>
]]></content>

      

      
        
      

      <summary type="html"><![CDATA[
  
    I used to see that retro multi-stroke text effect quite often and
    tried to replicate it using the CSS text-stroke property,
    but the results never quite matched.
    Because text-str...]]></summary>
    </entry>
  
    <entry>
      <title type="html">Introducing aivo</title>
      <link href="https://yuanchuan.dev/introducing-aivo" rel="alternate" type="text/html" title="Introducing aivo" />
      <published>2026-03-22T00:00:00+08:00</published>
      <updated>2026-03-22T00:00:00+08:00</updated>
      <id>https://yuanchuan.dev/introducing-aivo</id>
      <content type="html" xml:base="https://yuanchuan.dev/introducing-aivo"><![CDATA[<article>
  <p>
    I created a small command line tool named <a href="https://getaivo.dev">aivo</a>,
    it helps to manage API keys from OpenRouter, Vercel AI Gateway or any other providers.
  </p>

  <!--more-->

<figure class="highlight"><pre><code class="language-bash" data-lang="bash">aivo keys add</code></pre></figure>

  <p>
    After adding your keys, you can launch Claude code, Codex or Gemini CLI tools
    without manually setting up all the env variables.
  </p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash">aivo claude</code></pre></figure>

  <h2>Protocol transformation</h2>

  <p>
    Aivo will automatically transform protocols from one to another whenever necessary,
    so you can use DeepSeek, MiniMax, Kimi or the locally running models in either Claude code, Codex, or Gemini.
  </p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="c"># add the key</span>
aivo keys add <span class="nt">--base-url</span><span class="o">=</span>https://api.deepseek.com/v1 <span class="nt">--key</span><span class="o">=</span>xxx

<span class="c"># run claude with deepseek</span>
aivo claude <span class="nt">--model</span><span class="o">=</span>deepseek-chat

<span class="c"># run gemini with deepseek</span>
aivo gemini <span class="nt">--model</span><span class="o">=</span>deepseek-chat</code></pre></figure>

  <p>
    If you're unsure of the model name after adding the key, just leave it blank,
    aivo will load the available models for you to select.
  </p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash">aivo claude <span class="nt">--model</span></code></pre></figure>


  <h2>GitHub Copilot</h2>

  <p>
    You can also use your GitHub Copilot subscriptions.
    Choose the coding agent you like after connecting.
  </p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash">aivo keys add copilot</code></pre></figure>

  <h2>Ollama</h2>

  <p>
    To connect Ollama, either point to the lcoal URL or just add a key named <code>ollama</code>.
  </p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash">aivo keys add ollama</code></pre></figure>

  <p>After connecting, the model will automatically pull if not present</p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash">aivo claude <span class="nt">-m</span> kimi-k2.5:cloud
aivo claude <span class="nt">-m</span> stable-code:latest</code></pre></figure>

  <h2>Other features I like</h2>

  <p>
    Instant chat.
  </p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash">aivo <span class="nt">-x</span> <span class="s1">'tell me a story'</span></code></pre></figure>

  <p>
    Summarize git changes.
  </p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash">git diff <span class="nt">--cached</span> | aivo <span class="nt">-x</span> <span class="s2">"summarize the changes"</span></code></pre></figure>

  <p>
  Get the token statistics.
  </p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash">aivo stats</code></pre></figure>

  <h2>Give it a try</h2>

  <p>
    While there are many alternatives, I've found most of them too complex or heavy to use.
    I hope aivo will improve the overall user experience.
  </p>

  <p>
    Get the install link or learn more about its usage on <a href="https://github.com/yuanchuan/aivo">GitHub</a>.
  </p>

</article>
]]></content>

      

      
        
      

      <summary type="html"><![CDATA[
  
    I created a small command line tool named aivo,
    it helps to manage API keys from OpenRouter, Vercel AI Gateway or any other providers.
  

  ]]></summary>
    </entry>
  
    <entry>
      <title type="html">Remove black color with shaders</title>
      <link href="https://yuanchuan.dev/remove-black-color-with-shaders" rel="alternate" type="text/html" title="Remove black color with shaders" />
      <published>2025-12-16T00:00:00+08:00</published>
      <updated>2025-12-16T00:00:00+08:00</updated>
      <id>https://yuanchuan.dev/remove-black-color-with-shaders</id>
      <content type="html" xml:base="https://yuanchuan.dev/remove-black-color-with-shaders"><![CDATA[<article>
  <p>
    A while back, I coded the <a href="https://vis.social/@yuanchuan/114656996842005435">CBC (Canadian Broadcasting Corporation) logo</a> with a quick <em>css-doodle</em> implementation.
    There are certainly better ways to do it, but I chose to stack several black boxes and circles, because the background color was black, those black boxes ended up becoming "invisible" as desired.
  </p>
  <!--more-->

  <p>
    <css-doodle><style>
      --bg: #000;
      --fill: red;

      @grid: 13x1 / 240px / @p(--bg);
      @place: center;
      @size: 30%;

      background: @p(--bg) @svg(
        viewBox: -1 -1 2 2;
        circle { r: .9; fill: @p(--fill) }
      );

      translate: @pn(
        -75% 0, 75% 0, 0 -75%, 0 75%,
        -50% 50%, 50% 50%, 50% -50%, -50% -50%,
        -50% 0, 50% 0, 0 -50%, 0 50%,
        0 0
      );
    </style></css-doodle>
  </p>

  <p>
    You can see how it works by using a different background other than black.
  </p>

  <p>
    <css-doodle><style>
      --bg: #000;
      --fill: red;

      @grid: 13x1 / 240px / #f90;
      @place: center;
      @size: 30%;

      background: @p(--bg) @svg(
        viewBox: -1 -1 2 2;
        circle { r: .9; fill: @p(--fill) }
      );

      translate: @pn(
        -75% 0, 75% 0, 0 -75%, 0 75%,
        -50% 50%, 50% 50%, 50% -50%, -50% -50%,
        -50% 0, 50% 0, 0 -50%, 0 50%,
        0 0
      );
    </style></css-doodle>
  </p>

  <p>
    I couldn't find an elegant way to not use the black color, and I was reluctant to use <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/mask">CSS masks</a> since it would require more calculations.
  </p>

  <h2>One solution</h2>

  <p>
    It just hit me tonight that I could use shaders to get rid of the black color, and it's so simple.
  </p>

  <p>
    In <em>css-doodle</em>, code can generate images in real time, which can be used as textures and fed into shaders for additional effects.
  </p>

<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="nt">background</span><span class="o">:</span> <span class="k">@shaders</span><span class="p">(</span>
  <span class="n">texture_0</span> <span class="p">{</span>
    <span class="c">/* css-doodle code */</span>
  <span class="p">}</span>

  <span class="nt">fragment</span> <span class="p">{</span>
    <span class="c">/* glsl code */</span>
  <span class="p">}</span>
<span class="o">)</span></code></pre></figure>

  <p>
    This is the old implementation.
  </p>

  <div class="example">
    <css-doodle><style>
      @grid: 13x1 / 240px;
      @place: center;
      @size: 30%;

      background: #000 @svg(
        viewBox: -1 -1 2 2;
        circle { r: .9; fill: red }
      );

      translate: @pn(
        -75% 0, 75% 0, 0 -75%, 0 75%,
        -50% 50%, 50% 50%, 50% -50%, -50% -50%,
        -50% 0, 50% 0, 0 -50%, 0 50%,
        0 0
      );
    </style></css-doodle>

<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="k">@grid</span><span class="p">:</span> <span class="m">13</span><span class="n">x1</span> <span class="p">/</span> <span class="m">240px</span><span class="p">;</span>
<span class="k">@place</span><span class="p">:</span> <span class="nb">center</span><span class="p">;</span>
<span class="k">@size</span><span class="p">:</span> <span class="m">30%</span><span class="p">;</span>

<span class="nt">background</span><span class="o">:</span> <span class="err">#000</span> <span class="k">@svg</span><span class="p">(</span>
  <span class="n">viewBox</span><span class="p">:</span> <span class="m">-1</span> <span class="m">-1</span> <span class="m">2</span> <span class="m">2</span><span class="p">;</span>
  <span class="nt">circle</span> <span class="p">{</span> <span class="py">r</span><span class="p">:</span> <span class="m">.9</span><span class="p">;</span> <span class="py">fill</span><span class="p">:</span> <span class="no">red</span> <span class="p">}</span>
<span class="o">);</span>

<span class="nt">translate</span><span class="o">:</span> <span class="k">@pn</span><span class="p">(</span>
  <span class="m">-75%</span> <span class="m">0</span><span class="p">,</span> <span class="m">75%</span> <span class="m">0</span><span class="p">,</span> <span class="m">0</span> <span class="m">-75%</span><span class="p">,</span> <span class="m">0</span> <span class="m">75%</span><span class="p">,</span>
  <span class="m">-50%</span> <span class="m">50%</span><span class="p">,</span> <span class="m">50%</span> <span class="m">50%</span><span class="p">,</span> <span class="m">50%</span> <span class="m">-50%</span><span class="p">,</span> <span class="m">-50%</span> <span class="m">-50%</span><span class="p">,</span>
  <span class="m">-50%</span> <span class="m">0</span><span class="p">,</span> <span class="m">50%</span> <span class="m">0</span><span class="p">,</span> <span class="m">0</span> <span class="m">-50%</span><span class="p">,</span> <span class="m">0</span> <span class="m">50%</span><span class="p">,</span>
  <span class="m">0</span> <span class="m">0</span>
<span class="p">);</span></code></pre></figure>
  </div>

  <p>
    Turn it into a texture and mask out all the black pixels.
  </p>

  <div class="example">
    <css-doodle><style>
      @grid: 1 / 240px;
      background: @shaders(
        texture_0 {
          @grid: 13x1 / 100%;
          @place: center;
          @size: 30%;
          background: #000 @svg(
            viewBox: -1 -1 2 2;
            circle { r: .9; fill: red }
          );
          translate: @pn(
            -75% 0, 75% 0, 0 -75%, 0 75%,
            -50% 50%, 50% 50%, 50% -50%, -50% -50%,
            -50% 0, 50% 0, 0 -50%, 0 50%,
            0 0
          );
        }
        fragment {
          void main() {
            vec2 uv = gl_FragCoord.xy / u_resolution.xy;
            vec4 color = texture(texture_0, uv);
            float alpha = smoothstep(0.0, 1.0, length(color.rgb));
            FragColor = vec4(color.rgb, color.a * alpha);
          }
        }
      );
    </style></css-doodle>

<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="k">@grid</span><span class="p">:</span> <span class="m">1</span> <span class="p">/</span> <span class="m">240px</span> <span class="p">/</span> <span class="err">@</span><span class="n">shaders</span><span class="p">(</span>
  <span class="n">texture_0</span> <span class="p">{</span>
    <span class="k">@grid</span><span class="p">:</span> <span class="m">13</span><span class="n">x1</span> <span class="p">/</span> <span class="m">100%</span><span class="p">;</span>
    <span class="k">@place</span><span class="p">:</span> <span class="nb">center</span><span class="p">;</span>
    <span class="k">@size</span><span class="p">:</span> <span class="m">30%</span><span class="p">;</span>
    <span class="nt">background</span><span class="o">:</span> <span class="err">#000</span> <span class="k">@svg</span><span class="p">(</span>
      <span class="n">viewBox</span><span class="p">:</span> <span class="m">-1</span> <span class="m">-1</span> <span class="m">2</span> <span class="m">2</span><span class="p">;</span>
      <span class="nt">circle</span> <span class="p">{</span> <span class="py">r</span><span class="p">:</span> <span class="m">.9</span><span class="p">;</span> <span class="py">fill</span><span class="p">:</span> <span class="no">red</span> <span class="p">}</span>
    <span class="o">);</span>
    <span class="nt">translate</span><span class="o">:</span> <span class="k">@pn</span><span class="p">(</span>
      <span class="m">-75%</span> <span class="m">0</span><span class="p">,</span> <span class="m">75%</span> <span class="m">0</span><span class="p">,</span> <span class="m">0</span> <span class="m">-75%</span><span class="p">,</span> <span class="m">0</span> <span class="m">75%</span><span class="p">,</span>
      <span class="m">-50%</span> <span class="m">50%</span><span class="p">,</span> <span class="m">50%</span> <span class="m">50%</span><span class="p">,</span> <span class="m">50%</span> <span class="m">-50%</span><span class="p">,</span> <span class="m">-50%</span> <span class="m">-50%</span><span class="p">,</span>
      <span class="m">-50%</span> <span class="m">0</span><span class="p">,</span> <span class="m">50%</span> <span class="m">0</span><span class="p">,</span> <span class="m">0</span> <span class="m">-50%</span><span class="p">,</span> <span class="m">0</span> <span class="m">50%</span><span class="p">,</span>
      <span class="m">0</span> <span class="m">0</span>
    <span class="p">);</span>
  <span class="p">}</span>
  <span class="nt">fragment</span> <span class="p">{</span>
    <span class="err">void</span> <span class="err">main()</span> <span class="err">{</span>
      <span class="err">vec2</span> <span class="err">uv</span> <span class="err">=</span> <span class="err">gl_FragCoord.xy</span> <span class="err">/</span> <span class="err">u_resolution.xy;</span>
      <span class="err">vec4</span> <span class="err">c</span> <span class="err">=</span> <span class="err">texture(texture_0,</span> <span class="err">uv);</span>
      <span class="err">float</span> <span class="err">b</span> <span class="err">=</span> <span class="err">length(c.rgb);</span>
      <span class="err">float</span> <span class="err">alpha</span> <span class="err">=</span> <span class="err">smoothstep(0.0,</span> <span class="err">1.0,</span> <span class="err">b);</span>
      <span class="err">FragColor</span> <span class="err">=</span> <span class="err">vec4(c.rgb,</span> <span class="err">c.a</span> <span class="err">*</span> <span class="err">alpha);</span>
    <span class="p">}</span>
  <span class="err">}</span>
<span class="o">);</span></code></pre></figure>
  </div>


  <h2>The <code>difference</code> blending mode</h2>

  <p>
    I often use blend modes to create some combinations of shapes.
    The tricky part, though, is working with color. While there are some predictable patterns,
    the results of different color combinations can be hard to anticipate.
    So most of the time, I just use white color with the <code>difference</code> blend mode,
    which produces black-and-white patterns.
  </p>

  <div class="example">
    <css-doodle>
      @grid: 8x1 / 240px / #f90 +.8;
      @place: @plot(r: .5);
      background: #fff;
      border-radius: 50%;
      mix-blend-mode: difference;
      @size: 50%;
    </css-doodle>
<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="k">@grid</span><span class="p">:</span> <span class="m">8</span><span class="n">x1</span> <span class="p">/</span> <span class="m">240px</span> <span class="p">/</span> <span class="m">#f90</span> <span class="err">+</span><span class="m">.8</span><span class="p">;</span>
<span class="k">@place</span><span class="p">:</span> <span class="err">@</span><span class="n">plot</span><span class="p">(</span><span class="n">r</span><span class="p">:</span> <span class="m">.5</span><span class="p">);</span>
<span class="k">@size</span><span class="p">:</span> <span class="m">50%</span><span class="p">;</span>
<span class="nt">background</span><span class="o">:</span> <span class="nf">#fff</span><span class="o">;</span>
<span class="nt">border-radius</span><span class="o">:</span> <span class="err">50</span><span class="o">%;</span>
<span class="nt">mix-blend-mode</span><span class="o">:</span> <span class="nt">difference</span><span class="o">;</span></code></pre></figure>
  </div>

   <p>
    Now I can directly remove the black color and make the pattern transparent, it becomes much more usable.
  </p>

  <div class="example">

  <css-doodle><template>
    @grid: 1 / 240px / #f90;
    background: @shaders(
      texture_0 {
        @grid: 8x1 / 100% +.8;
        @place: @plot(r: .5);
        background: #fff;
        border-radius: 50%;
        mix-blend-mode: difference;
        @size: 50%;
      }
      fragment {
        void main() {
          vec2 uv = gl_FragCoord.xy / u_resolution.xy;
          vec4 c = texture(texture_0, uv);
          float b = length(c.rgb);
          float alpha = smoothstep(0.1, 2.0, b);
          FragColor = vec4(c.rgb, c.a * alpha);
        }
      }
    );
  </template></css-doodle>

<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="k">@grid</span><span class="p">:</span> <span class="m">1</span> <span class="p">/</span> <span class="m">240px</span> <span class="p">/</span> <span class="m">#f90</span><span class="p">;</span>
<span class="nt">background</span><span class="o">:</span> <span class="k">@shaders</span><span class="p">(</span>
  <span class="n">texture_0</span> <span class="p">{</span>
    <span class="k">@grid</span><span class="p">:</span> <span class="m">8</span><span class="n">x1</span> <span class="p">/</span> <span class="m">100%</span> <span class="err">+</span><span class="m">.8</span><span class="p">;</span>
    <span class="k">@place</span><span class="p">:</span> <span class="err">@</span><span class="n">plot</span><span class="p">(</span><span class="n">r</span><span class="p">:</span> <span class="m">.5</span><span class="p">);</span>
    <span class="k">@size</span><span class="p">:</span> <span class="m">50%</span><span class="p">;</span>
    <span class="nt">background</span><span class="o">:</span> <span class="nf">#fff</span><span class="o">;</span>
    <span class="nt">border-radius</span><span class="o">:</span> <span class="err">50</span><span class="o">%;</span>
    <span class="nt">mix-blend-mode</span><span class="o">:</span> <span class="nt">difference</span><span class="o">;</span>
  <span class="p">}</span>
  <span class="nt">fragment</span> <span class="p">{</span>
    <span class="err">void</span> <span class="err">main()</span> <span class="err">{</span>
      <span class="err">vec2</span> <span class="err">uv</span> <span class="err">=</span> <span class="err">gl_FragCoord.xy</span> <span class="err">/</span> <span class="err">u_resolution.xy;</span>
      <span class="err">vec4</span> <span class="err">c</span> <span class="err">=</span> <span class="err">texture(texture_0,</span> <span class="err">uv);</span>
      <span class="err">float</span> <span class="err">b</span> <span class="err">=</span> <span class="err">length(c.rgb);</span>
      <span class="err">float</span> <span class="err">alpha</span> <span class="err">=</span> <span class="err">smoothstep(0.0,</span> <span class="err">2.0,</span> <span class="err">b);</span>
      <span class="err">FragColor</span> <span class="err">=</span> <span class="err">vec4(c.rgb,</span> <span class="err">c.a</span> <span class="err">*</span> <span class="err">alpha);</span>
    <span class="p">}</span>
  <span class="err">}</span>
<span class="o">);</span></code></pre></figure>

  </div>

  <p>
    This approach isn’t anything special, after all, shaders are naturally good at handling colors, but it still brings me joy.
  </p>

</article>

<style>
  .article {
    max-width: 46em;
  }
  css-doodle {
    display: block;
    width: 240px;
    height: 240px;
    max-width: 100%;
  }

  css-doodle:not(:defined) {
    visibility: hidden;
  }

  .example {
    display: flex;
    margin-bottom: 2em;
  }

  .example figure {
    margin: 0;
    order: 1;
    overflow: auto;
  }
  .example figure pre {
    margin: 0;
  }
  .example .doodle,
  .example css-doodle {
    order: 2;
    margin-left: auto;
    flex-shrink: 0;
  }

  @media screen and (max-width: 720px) {
    .example {
      display: block;
    }
    .example css-doodle {
      margin: 0 0 1em 0;
    }
    .example .doodle css-doodle {
      margin: 0;
    }
  }
</style>

<script type="module">import 'https://esm.sh/css-doodle@0.48.0'</script>
]]></content>

      

      
        
      

      <summary type="html"><![CDATA[
  
    A while back, I coded the CBC (Canadian Broadcasting Corporation) logo with a quick css-doodle implementation.
    There are certainly better ways to do it, but I chose to stack several bla...]]></summary>
    </entry>
  
    <entry>
      <title type="html">CSS Animation with offset-path</title>
      <link href="https://yuanchuan.dev/css-animation-with-offset-path" rel="alternate" type="text/html" title="CSS Animation with offset-path" />
      <published>2025-03-20T00:00:00+08:00</published>
      <updated>2025-03-20T00:00:00+08:00</updated>
      <id>https://yuanchuan.dev/css-animation-with-offset-path</id>
      <content type="html" xml:base="https://yuanchuan.dev/css-animation-with-offset-path"><![CDATA[<article>
  <p>
    This is from last year when I tried to recreate <a href="https://mastodon.social/@rachsmith/112426114091222967">Rach Smith's P5.js animation</a> using CSS.
    The animation seemed like it could be implemented in a breeze, but there's an illusion of simplicity.
    The process of manually updating the positions and directions of each box was
    a bit tricky.
  </p>
  <!--more-->

  <figure>
    <video autoplay loop muted playsinline>
      <source src="https://yuanchuan.dev/assets/mp4/rach-smith-box-animation.mp4" type="video/mp4">
    </video>
    <figcaption>
      Rach Smith's P5.js animation
    </figcaption>
  </figure>

  <p>
    After some effort, I realized that I only needed to track the positions of a single box in <em>keyframes</em>,
    the rest would follow and be evenly separated through different values of <code>animation-delay</code>.
    Negative delays were used to start the animation immediately.
  </p>

<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="nc">.box</span> <span class="p">{</span>
  <span class="c">/* ... */</span>

  <span class="nl">position</span><span class="p">:</span> <span class="nb">absolute</span><span class="p">;</span>
  <span class="nl">animation</span><span class="p">:</span> <span class="n">move</span> <span class="m">10s</span> <span class="n">linear</span> <span class="n">infinite</span><span class="p">;</span>
  <span class="nl">animation-delay</span><span class="p">:</span> <span class="n">calc</span><span class="p">(</span><span class="n">var</span><span class="p">(</span><span class="n">--index</span><span class="p">)</span> <span class="p">/</span> <span class="n">var</span><span class="p">(</span><span class="n">--total</span><span class="p">)</span> <span class="err">*</span> <span class="m">-10s</span><span class="p">);</span>
<span class="p">}</span>

<span class="k">@keyframes</span> <span class="n">move</span> <span class="p">{</span>
  <span class="err">0</span><span class="o">%,</span> <span class="err">100</span><span class="o">%</span> <span class="p">{</span> <span class="nl">left</span><span class="p">:</span> <span class="m">25%</span><span class="p">;</span> <span class="nl">top</span><span class="p">:</span> <span class="m">25%</span> <span class="p">}</span>
  <span class="err">25</span><span class="o">%</span>      <span class="p">{</span> <span class="nl">left</span><span class="p">:</span> <span class="m">75%</span><span class="p">;</span> <span class="nl">top</span><span class="p">:</span> <span class="m">25%</span> <span class="p">}</span>
  <span class="err">50</span><span class="o">%</span>      <span class="p">{</span> <span class="nl">left</span><span class="p">:</span> <span class="m">75%</span><span class="p">;</span> <span class="nl">top</span><span class="p">:</span> <span class="m">75%</span> <span class="p">}</span>
  <span class="err">75</span><span class="o">%</span>      <span class="p">{</span> <span class="nl">left</span><span class="p">:</span> <span class="m">25%</span><span class="p">;</span> <span class="nl">top</span><span class="p">:</span> <span class="m">75%</span> <span class="p">}</span>
<span class="p">}</span></code></pre></figure>

  <p>
    A few days later, I found a simpler method and got it working
    with the <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/offset-path">offset-path</a> property.
    The <code>inset()</code> function would create a square path and then the boxes would move along with it
    by animating the <code>offset-distance</code> values.
  </p>

  <p>
    I was excited because I had previously thought that <code>offset-path</code> only supported SVG paths.
  </p>

<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="nc">.box</span> <span class="p">{</span>
  <span class="c">/* ... */</span>

  <span class="py">offset-path</span><span class="p">:</span> <span class="nb">inset</span><span class="p">(</span><span class="m">25%</span><span class="p">);</span>

  <span class="nl">animation</span><span class="p">:</span> <span class="n">move</span> <span class="m">10s</span> <span class="n">linear</span> <span class="n">infinite</span><span class="p">;</span>
  <span class="nl">animation-delay</span><span class="p">:</span> <span class="n">calc</span><span class="p">(</span><span class="n">var</span><span class="p">(</span><span class="n">--index</span><span class="p">)</span> <span class="p">/</span> <span class="n">var</span><span class="p">(</span><span class="n">--total</span><span class="p">)</span> <span class="err">*</span> <span class="m">-10s</span><span class="p">);</span>
<span class="p">}</span>

<span class="k">@keyframes</span> <span class="n">move</span> <span class="p">{</span>
  <span class="nt">to</span> <span class="p">{</span>
    <span class="py">offset-distance</span><span class="p">:</span> <span class="m">100%</span><span class="p">;</span>
  <span class="p">}</span>
<span class="p">}</span></code></pre></figure>

  <h2>Linear scale</h2>

  <p>
    Since the boxes were evenly spaced,
    the <code>animation-delay</code> values needed to be equally distributed proportionally according to each box's index.
    The calculation process is referred to as <em>linear scaling</em>, or a simplified form of <em>linear interpolation</em>.
  </p>

<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="c">/* -0.5s, -1s, -1.5s, -2s, ... , -9.5s, -10s */</span>
<span class="nt">var</span><span class="o">(</span><span class="nt">--index</span><span class="o">)</span> <span class="o">/</span> <span class="nt">var</span><span class="o">(</span><span class="nt">--total</span><span class="o">)</span> <span class="o">*</span> <span class="nt">-10s</span></code></pre></figure>

  <p>
    Browers are now shipping <a href="https://www.w3.org/TR/css-values-5/#tree-counting">tree counting functions</a>,
    so that the predefined variables can be replaced with native
    <code>sibling-index()</code> and <code>sibling-count()</code> functions.
    <!--
    In <a href="https://css-doodle">css-doodle</a>, the <code>@i</code> and <code>@I</code> are used to represent the same values respectively.
    -->
  </p>

<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="nt">sibling-index</span><span class="o">()</span> <span class="o">/</span> <span class="nt">sibling-count</span><span class="o">()</span> <span class="o">*</span> <span class="nt">-10s</span></code></pre></figure>

  <p>
    <em>Linear scaling</em> has many use cases, such as creating a range of opacities or colors.
  </p>

  <p>
    <css-doodle>
      @grid: 10x1 / 400px auto;
      background: #000;
      opacity: @iI();
    </css-doodle>
  </p>
<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="nt">opacity</span><span class="o">:</span> <span class="nt">calc</span><span class="o">(</span><span class="nt">sibling-index</span><span class="o">()</span> <span class="o">/</span> <span class="nt">sibling-count</span><span class="o">())</span></code></pre></figure>

  <p>
    <css-doodle>
      @grid: 36x1 / 400px 30px;
      background: hsl(@iI(*360deg), 80%, 50%);
    </css-doodle>
  </p>

<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="nt">background</span><span class="o">:</span> <span class="nt">hsl</span><span class="o">(</span>
  <span class="nt">calc</span><span class="o">(</span><span class="nt">sibling-index</span><span class="o">()</span> <span class="o">/</span> <span class="nt">sibling-count</span><span class="o">()</span> <span class="o">*</span> <span class="err">360</span><span class="nt">deg</span><span class="o">),</span>
  <span class="err">80</span><span class="o">%,</span> <span class="err">50</span><span class="o">%</span>
<span class="o">)</span></code></pre></figure>

  <p>
    Compared to <code>calc()</code> expressions, the <code>@iI()</code> function
    in <a href="https://css-doodle.com">css-doodle</a> provides a more concise and readable way to achieve the same result.
  </p>

<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="c">/* opacity: calc(sibling-index() / sibling-count()) */</span>
<span class="nt">opacity</span><span class="o">:</span> <span class="k">@iI</span><span class="p">()</span>

<span class="c">/* animation-delay: calc(sibling-index() / sibling-count() * -10s) */</span>
<span class="n">animation-delay</span><span class="p">:</span> <span class="err">@</span><span class="n">iI</span><span class="p">(</span><span class="err">*</span><span class="m">-10s</span><span class="p">)</span></code></pre></figure>

  <h2>Rewrite with css-doodle</h2>

  <p>
    When using <em>css-doodle</em>, the code becomes even simpler.
    The function <code>@t</code> can replace both <code>animation</code> and <code>keyframes</code>,
    as explained in the post <a href="https://yuanchuan.dev/time-based-css-animations">Time-based CSS Animations</a>.
  </p>

<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="nt">offset-distance</span><span class="o">:</span> <span class="k">@t</span><span class="p">(/</span><span class="m">100%</span><span class="p">,</span> <span class="err">+@</span><span class="n">iI</span><span class="p">(</span><span class="err">*</span><span class="m">100%</span><span class="p">));</span></code></pre></figure>

  <p>
    Below is the complete code used for the animation. (<a href="https://codepen.io/yuanchuan/pen/YzbeEBZ">CodePen</a>)
  </p>

  <div class="example">
    <css-doodle><template>
      @grid: 20x1 / 240px / #f2f0e9 ß2;
      @size: 6%;
      position: absolute;
      border: 2px solid hsl(@iI(*360deg), 100%, 34%);
      offset-distance: @t(/100%, +@iI(*100%));
      offset-path: inset(25%);
    </template></css-doodle>

<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="k">@grid</span><span class="p">:</span> <span class="m">20</span><span class="n">x1</span> <span class="p">/</span> <span class="m">240px</span> <span class="p">/</span> <span class="m">#f2f0e9</span> <span class="n">ß2</span><span class="p">;</span>
<span class="k">@size</span><span class="p">:</span> <span class="m">6%</span> <span class="nb">auto</span> <span class="m">1</span><span class="p">;</span>

<span class="nt">border</span><span class="o">:</span> <span class="nt">solid</span> <span class="err">2</span><span class="nt">px</span> <span class="nt">hsl</span><span class="o">(</span>
  <span class="k">@iI</span><span class="p">(</span><span class="err">*</span><span class="m">360deg</span><span class="p">),</span> <span class="m">100%</span><span class="p">,</span> <span class="m">34%</span>
<span class="p">);</span>

<span class="nt">offset-distance</span><span class="o">:</span> <span class="k">@t</span><span class="p">(/</span><span class="m">100%</span><span class="p">,</span> <span class="err">+@</span><span class="n">iI</span><span class="p">(</span><span class="err">*</span><span class="m">100%</span><span class="p">));</span>
<span class="nt">offset-path</span><span class="o">:</span> <span class="nt">inset</span><span class="o">(</span><span class="err">25</span><span class="o">%);</span></code></pre></figure>

  </div>

  <p>
    Note that the <code>inset()</code> function supports a <code>round</code> keyword,
    adding a rounded corner to the path will get the boxes a subtle flipping effect as they turn the corners.
  </p>

  <div class="example">
    <css-doodle>
      <template>
        @grid: 20x1 / 240px / #f2f0e9 ß2;
        @size: 6% auto 1;
        position: absolute;
        border: 2px solid hsl(@iI(*360deg), 100%, 34%);
        offset-distance: @t(/100%, +@iI(*100%));
        offset-path: inset(25% round 1%);
      </template>
    </css-doodle>

<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="c">/* ... */</span>

<span class="nt">offset-path</span><span class="o">:</span> <span class="nt">inset</span><span class="o">(</span><span class="err">25</span><span class="o">%</span> <span class="nt">round</span> <span class="err">1</span><span class="o">%);</span></code></pre></figure>

  </div>

  <h2>Offset-path with polygon()</h2>
  <p>
    I recently discovered that <code>offset-path</code> accepts <code>polygon()</code> and other shape functions.
    I can't believe I hadn't tried it earlier -- there are <a href="https://yuanchuan.dev/polygon-shapes">so many shapes</a>
    that <code>polygon()</code> can create and apply to <code>offset-path</code>,
    some of them may result in interesting animations.
  </p>

  <p>
    The tool <em>css-doodle</em> also offers the <code>@shape</code> function to generate polygons with ease.
  </p>

  <p>
    <css-doodle>
      <template>
        @grid: 3x1 / 300px auto;
        --shape: @pn(
          @shape(heart),
          @shape(hexagon),
          @shape(
            points: 12;
            r: seq(.5, 1);
          );
        );
        background: @doodle(
          @grid: 20x1 / 100% ^.9;
          @size: 5%;
          position: absolute;
          background: #000;
          border-radius: 50%;
          offset-distance: @t(/100%, +@iI(*100%));
          offset-path: @p(--shape);
        );
      </template>
    </css-doodle>
  </p>

<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="c">/* heart */</span>
<span class="nt">offset-path</span><span class="o">:</span> <span class="k">@shape</span><span class="p">(</span><span class="n">heart</span><span class="p">);</span>

<span class="c">/* hexagon */</span>
<span class="nt">offset-path</span><span class="o">:</span> <span class="k">@shape</span><span class="p">(</span><span class="n">hexagon</span><span class="p">);</span>

<span class="c">/* custom shapes */</span>
<span class="nt">offset-path</span><span class="o">:</span> <span class="k">@shape</span><span class="p">(</span>
  <span class="n">points</span><span class="p">:</span> <span class="m">12</span><span class="p">;</span>
  <span class="nt">r</span><span class="o">:</span> <span class="nt">seq</span><span class="o">(.</span><span class="err">5</span><span class="o">,</span> <span class="err">1</span><span class="o">);</span>
<span class="o">)</span></code></pre></figure>

  <p>
    A trailing effect will be produced when making a slight change to the <code>offset-distance</code> ratio.
   (<a href="https://codepen.io/yuanchuan/pen/xbxWvNm">CodePen</a>)
  </p>

  <div class="example">
    <css-doodle>
      <template>
        @grid: 30x1 / 240px ß1;
        position: absolute;
        @size: @iI(*5%) auto 1;
        background: #f60;
        border-radius: 50%;
        opacity: @iI();
        offset-distance: @t(/50%, +@iI(*8%));
        offset-path: @shape(
          points: 180;
          scale: .012;
          r: 50*abs.sin(2.5t)-81;
        );
      </template>
    </css-doodle>
<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="c">/* ... */</span>

<span class="k">@size</span><span class="p">:</span> <span class="err">@</span><span class="n">iI</span><span class="p">(</span><span class="err">*</span><span class="m">5%</span><span class="p">);</span>
<span class="nt">opacity</span><span class="o">:</span> <span class="k">@iI</span><span class="p">();</span>

<span class="nt">offset-distance</span><span class="o">:</span> <span class="k">@t</span><span class="p">(/</span><span class="m">50%</span><span class="p">,</span> <span class="err">+@</span><span class="n">iI</span><span class="p">(</span><span class="err">*</span><span class="m">8%</span><span class="p">));</span>

<span class="nt">offset-path</span><span class="o">:</span> <span class="k">@shape</span><span class="p">(</span>
  <span class="n">points</span><span class="p">:</span> <span class="m">180</span><span class="p">;</span>
  <span class="nt">scale</span><span class="o">:</span> <span class="o">.</span><span class="err">012</span><span class="o">;</span>
  <span class="nt">r</span><span class="o">:</span> <span class="err">50</span><span class="o">*</span><span class="nt">abs</span><span class="nc">.sin</span><span class="o">(</span><span class="err">2</span><span class="o">.</span><span class="err">5</span><span class="nt">t</span><span class="o">)</span><span class="nt">-81</span><span class="o">;</span>
<span class="o">);</span></code></pre></figure>
  </div>

  <p>
    Another example of custom shapes with polygons.
    The performance is not as good as in canvas, but it's still fun to play with.
   (<a href="https://codepen.io/yuanchuan/pen/ogNqKVR">CodePen</a>)
  </p>

  <div class="example">
    <div class="doodle">
      <css-doodle click-to-update>
        <template>
          @grid: 20x1 / 240px / #051d1b;
          @place: center;
          @size: @iI(*50%, +40%);
          filter: drop-shadow(0 0 5px #9bff9b);
          background: @doodle(
            @grid: 20x1 / 100%;
            @size: @r(1%) @r(2%);
            position: absolute;
            background: @p(#93ffe4, #84ff92);
            offset-rotate: auto 90deg;
            offset-distance: @iI(*200%, @r(50%, 500%));
            offset-path: @shape(points: 180; r: cos(4t));
          );
        </template>
      </css-doodle>
      <button class="start-pause">animate</button>
    </div>

<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="k">@grid</span><span class="p">:</span> <span class="m">20</span><span class="n">x1</span> <span class="p">/</span> <span class="m">240px</span> <span class="p">/</span> <span class="m">#051d1b</span><span class="p">;</span>
<span class="k">@place</span><span class="p">:</span> <span class="nb">center</span><span class="p">;</span>
<span class="k">@size</span><span class="p">:</span> <span class="err">@</span><span class="n">iI</span><span class="p">(</span><span class="err">*</span><span class="m">50%</span><span class="p">,</span> <span class="err">+</span><span class="m">50%</span><span class="p">);</span>
<span class="nt">filter</span><span class="o">:</span> <span class="nt">drop-shadow</span><span class="o">(</span><span class="err">0</span> <span class="err">0</span> <span class="err">5</span><span class="nt">px</span> <span class="err">#9</span><span class="nt">bff9b</span><span class="o">);</span>
<span class="nt">background</span><span class="o">:</span> <span class="k">@doodle</span><span class="p">(</span>
  <span class="err">@</span><span class="n">grid</span><span class="p">:</span> <span class="m">20</span><span class="n">x1</span> <span class="p">/</span> <span class="m">100%</span><span class="p">;</span>
  <span class="k">@size</span><span class="p">:</span> <span class="err">@</span><span class="n">r</span><span class="p">(</span><span class="m">1%</span><span class="p">)</span> <span class="err">@</span><span class="n">r</span><span class="p">(</span><span class="m">2%</span><span class="p">);</span>
  <span class="nt">position</span><span class="o">:</span> <span class="nt">absolute</span><span class="o">;</span>
  <span class="nt">background</span><span class="o">:</span> <span class="k">@p</span><span class="p">(</span><span class="m">#93ffe4</span><span class="p">,</span> <span class="m">#84ff92</span><span class="p">);</span>
  <span class="nt">offset-rotate</span><span class="o">:</span> <span class="nt">auto</span> <span class="err">90</span><span class="nt">deg</span><span class="o">;</span>
  <span class="nt">offset-distance</span><span class="o">:</span> <span class="k">@t</span><span class="p">(</span>
    <span class="p">/</span><span class="err">@</span><span class="n">r</span><span class="p">(</span><span class="m">50%</span><span class="p">,</span> <span class="m">500%</span><span class="p">),</span>
    <span class="err">+@</span><span class="n">iI</span><span class="p">(</span><span class="err">*</span><span class="m">100%</span><span class="p">)</span>
  <span class="p">);</span>
  <span class="nt">offset-path</span><span class="o">:</span> <span class="k">@shape</span><span class="p">(</span>
    <span class="n">points</span><span class="p">:</span> <span class="m">180</span><span class="p">;</span>
    <span class="nt">r</span><span class="o">:</span> <span class="nt">cos</span><span class="o">(</span><span class="err">4</span><span class="nt">t</span><span class="o">)</span>
  <span class="o">);</span>
<span class="o">);</span></code></pre></figure>
  </div>

  <h2>Beesandbombs's spiral animation</h2>

  <p>
    I once saw a cool animation made by <a href="https://x.com/beesandbombs">@beesandbombs</a>.
    I couldn't find a simple way to recreate it with CSS before,
    but now it's possible to do it with <code>offset-path</code>.
  </p>

  <figure>
    <video autoplay loop muted playsinline>
      <source src="https://yuanchuan.dev/assets/mp4/beesandbombs-animation.mp4" type="video/mp4">
    </video>
    <figcaption>
      Beesandbombs's spiral animation
    </figcaption>
  </figure>

  <p>
    Let's start by creating polygons with sides ranging from <em>3</em> to <em>13</em> and place them in the background.
  </p>

  <div class="example">
    <css-doodle>
      <template>
        @grid: 1 / 240px / #f5f5f5 @svg(
          viewBox: -5 -5 10 10;
          fill: none;
          stroke-width: .035;
          stroke: #000;
          polygon*13 {
            points: @Plot(
              points: @n2;
              r: @nN(*3.5);
              rotate: @n(*90, /@n2);
            );
          }
        );
      </template>
    </css-doodle>

<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="k">@grid</span><span class="p">:</span> <span class="m">1</span> <span class="p">/</span> <span class="m">240px</span> <span class="p">/</span> <span class="m">#f5f5f5</span> <span class="err">@</span><span class="n">svg</span><span class="p">(</span>
  <span class="n">viewBox</span><span class="p">:</span> <span class="m">-5</span> <span class="m">-5</span> <span class="m">10</span> <span class="m">10</span><span class="p">;</span>
  <span class="nt">fill</span><span class="o">:</span> <span class="nt">none</span><span class="o">;</span>
  <span class="nt">stroke-width</span><span class="o">:</span> <span class="o">.</span><span class="err">035</span><span class="o">;</span>
  <span class="nt">stroke</span><span class="o">:</span> <span class="err">#000</span><span class="o">;</span>
  <span class="nt">polygon</span><span class="o">*</span><span class="err">13</span> <span class="p">{</span>
    <span class="py">points</span><span class="p">:</span> <span class="err">@</span><span class="n">Plot</span><span class="p">(</span>
      <span class="n">points</span><span class="p">:</span> <span class="err">@</span><span class="n">n2</span><span class="p">;</span>
      <span class="py">r</span><span class="p">:</span> <span class="err">@</span><span class="n">nN</span><span class="p">(</span><span class="err">*</span><span class="m">3.2</span><span class="p">);</span>
      <span class="py">rotate</span><span class="p">:</span> <span class="err">@</span><span class="n">n</span><span class="p">(</span><span class="err">*</span><span class="m">90</span><span class="p">,</span> <span class="p">/</span><span class="err">@</span><span class="n">n2</span><span class="p">);</span>
    <span class="err">);</span>
  <span class="p">}</span>
<span class="o">);</span></code></pre></figure>
  </div>

  <p>
    In the original animation, the length of each polygon side is the same,
    so the corresponding <em>r</em> should be adjusted based on the fixed side length.
  </p>

  <div class="example">
    <css-doodle>
      <template>
        @grid: 1 / 240px / #f5f5f5 @svg(
          viewBox: -5 -5 10 10;
          fill: none;
          stroke-width: .035;
          stroke: #000;
          polygon*13 {
            points: @Plot(
              points: @n2;
              r: $(.7 / cos(@n(*π, /@n2, /2)));
              rotate: @n(*90, /@n2);
            );
          }
        );
      </template>
    </css-doodle>

<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="k">@grid</span><span class="p">:</span> <span class="m">1</span> <span class="p">/</span> <span class="m">240px</span> <span class="m">#f5f5f5</span> <span class="err">@</span><span class="n">svg</span><span class="p">(</span>
  <span class="n">viewBox</span><span class="p">:</span> <span class="m">-5</span> <span class="m">-5</span> <span class="m">10</span> <span class="m">10</span><span class="p">;</span>
  <span class="nt">fill</span><span class="o">:</span> <span class="nt">none</span><span class="o">;</span>
  <span class="nt">stroke-width</span><span class="o">:</span> <span class="o">.</span><span class="err">035</span><span class="o">;</span>
  <span class="nt">stroke</span><span class="o">:</span> <span class="err">#000</span><span class="o">;</span>
  <span class="nt">polygon</span><span class="o">*</span><span class="err">13</span> <span class="p">{</span>
    <span class="py">points</span><span class="p">:</span> <span class="err">@</span><span class="n">Plot</span><span class="p">(</span>
      <span class="n">points</span><span class="p">:</span> <span class="err">@</span><span class="n">n2</span><span class="p">;</span>
      <span class="py">r</span><span class="p">:</span> <span class="err">$</span><span class="p">(</span><span class="m">.7</span> <span class="p">/</span> <span class="n">cos</span><span class="p">(</span><span class="err">@</span><span class="n">n</span><span class="p">(</span><span class="err">*</span><span class="n">π</span><span class="p">,</span> <span class="p">/</span><span class="err">@</span><span class="n">n2</span><span class="p">,</span> <span class="p">/</span><span class="m">2</span><span class="p">)));</span>
      <span class="py">rotate</span><span class="p">:</span> <span class="err">@</span><span class="n">n</span><span class="p">(</span><span class="err">*</span><span class="m">90</span><span class="p">,</span> <span class="p">/</span><span class="err">@</span><span class="n">n2</span><span class="p">);</span>
    <span class="err">);</span>
  <span class="p">}</span>
<span class="o">);</span></code></pre></figure>
  </div>

  <p>
    The painting order in SVG is determined by the order of the elements,
    after adding colors to the polygons, the smaller polygons appear to be covered by the larger ones.
    Therefore, the order of the polygons needs to be reversed.
  </p>

  <div class="example">
    <css-doodle>
      <template>
        @grid: 1 / 240px / #f5f5f5 @svg(
          viewBox: -5 -5 10 10;
          fill: none;
          stroke-width: .035;
          polygon*13-1 {
            stroke: hsl(@nN(*360), 80%, 50%);
            points: @Plot(
              points: @n2;
              r: $(.7 / cos(@n(*π, /@n2, /2)));
              rotate: @n(*90, /@n2);
            );
          }
        );
      </template>
    </css-doodle>

<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="k">@grid</span><span class="p">:</span> <span class="m">1</span> <span class="p">/</span> <span class="m">240px</span> <span class="p">/</span> <span class="m">#fff</span> <span class="err">@</span><span class="n">svg</span><span class="p">(</span>
  <span class="n">viewBox</span><span class="p">:</span> <span class="m">-5</span> <span class="m">-5</span> <span class="m">10</span> <span class="m">10</span><span class="p">;</span>
  <span class="nt">fill</span><span class="o">:</span> <span class="nt">none</span><span class="o">;</span>
  <span class="nt">stroke-width</span><span class="o">:</span> <span class="o">.</span><span class="err">035</span><span class="o">;</span>
  <span class="nt">polygon</span><span class="o">*</span><span class="err">13</span><span class="nt">-1</span> <span class="p">{</span>
    <span class="py">stroke</span><span class="p">:</span> <span class="n">hsl</span><span class="p">(</span><span class="err">@</span><span class="n">nN</span><span class="p">(</span><span class="err">*</span><span class="m">360</span><span class="p">),</span> <span class="m">80%</span><span class="p">,</span> <span class="m">50%</span><span class="p">);</span>
    <span class="py">points</span><span class="p">:</span> <span class="err">@</span><span class="n">Plot</span><span class="p">(</span>
      <span class="n">points</span><span class="p">:</span> <span class="err">@</span><span class="n">n2</span><span class="p">;</span>
      <span class="py">r</span><span class="p">:</span> <span class="err">$</span><span class="p">(</span><span class="m">.7</span> <span class="p">/</span> <span class="n">cos</span><span class="p">(</span><span class="err">@</span><span class="n">n</span><span class="p">(</span><span class="err">*</span><span class="n">π</span><span class="p">,</span> <span class="p">/</span><span class="err">@</span><span class="n">n2</span><span class="p">,</span> <span class="p">/</span><span class="m">2</span><span class="p">)));</span>
      <span class="py">rotate</span><span class="p">:</span> <span class="err">@</span><span class="n">n</span><span class="p">(</span><span class="err">*</span><span class="m">90</span><span class="p">,</span> <span class="p">/</span><span class="err">@</span><span class="n">n2</span><span class="p">);</span>
    <span class="err">);</span>
  <span class="p">}</span>
<span class="o">);</span></code></pre></figure>
  </div>

  <p>
    The final step is to add small dots and create identical offset paths corresponding to the polygons in the background,
    and then animate them using <code>offset-distance</code>.
    (<a href="https://codepen.io/yuanchuan/pen/ZYExggo">CodePen</a>)
  </p>

  <div class="example">
    <css-doodle>
      <template>
        @grid: 1x13 / 240px / #f5f5f5 @svg(
          viewBox: -5 -5 10 10;
          fill: none;
          stroke-width: .035;
          polygon*13-1 {
            stroke: hsl(@nN(*360), 80%, 50%);
            points: @Plot(
              points: @n2;
              r: $(.7 / cos(@n(*π, /@n2, /2)));
              rotate: @n(*90, /@n2);
            );
          }
        );

        @size: 2% auto 1;
        border-radius: 50%;
        background: #000;

        offset-path: @shape(
          points: @i2;
          turn: -1;
          r: $(.14 / cos(@i(*π, /@i2, /2)));
          rotate: @i(*90, /@i2);
        );
        offset-distance: calc(240px/@I3 + @t(/160%, *@I2(-@i)));
      </template>
    </css-doodle>

<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="k">@grid</span><span class="p">:</span> <span class="m">1</span><span class="n">x13</span> <span class="p">/</span> <span class="m">240px</span> <span class="p">/</span> <span class="m">#f5f5f5</span> <span class="err">@</span><span class="n">svg</span><span class="p">(</span>
  <span class="n">viewBox</span><span class="p">:</span> <span class="m">-5</span> <span class="m">-5</span> <span class="m">10</span> <span class="m">10</span><span class="p">;</span>
  <span class="nt">fill</span><span class="o">:</span> <span class="nt">none</span><span class="o">;</span>
  <span class="nt">stroke-width</span><span class="o">:</span> <span class="o">.</span><span class="err">035</span><span class="o">;</span>
  <span class="nt">polygon</span><span class="o">*</span><span class="err">13</span><span class="nt">-1</span> <span class="p">{</span>
    <span class="py">stroke</span><span class="p">:</span> <span class="n">hsl</span><span class="p">(</span><span class="err">@</span><span class="n">nN</span><span class="p">(</span><span class="err">*</span><span class="m">360</span><span class="p">),</span> <span class="m">80%</span><span class="p">,</span> <span class="m">50%</span><span class="p">);</span>
    <span class="py">points</span><span class="p">:</span> <span class="err">@</span><span class="n">Plot</span><span class="p">(</span>
      <span class="n">points</span><span class="p">:</span> <span class="err">@</span><span class="n">n2</span><span class="p">;</span>
      <span class="py">r</span><span class="p">:</span> <span class="err">$</span><span class="p">(</span><span class="m">.7</span> <span class="p">/</span> <span class="n">cos</span><span class="p">(</span><span class="err">@</span><span class="n">n</span><span class="p">(</span><span class="err">*</span><span class="n">π</span><span class="p">,</span> <span class="p">/</span><span class="err">@</span><span class="n">n2</span><span class="p">,</span> <span class="p">/</span><span class="m">2</span><span class="p">)));</span>
      <span class="py">rotate</span><span class="p">:</span> <span class="err">@</span><span class="n">n</span><span class="p">(</span><span class="err">*</span><span class="m">90</span><span class="p">,</span> <span class="p">/</span><span class="err">@</span><span class="n">n2</span><span class="p">);</span>
    <span class="err">);</span>
  <span class="p">}</span>
<span class="o">);</span>

<span class="k">@size</span><span class="p">:</span> <span class="m">2%</span> <span class="nb">auto</span> <span class="m">1</span><span class="p">;</span>
<span class="nt">border-radius</span><span class="o">:</span> <span class="err">50</span><span class="o">%;</span>
<span class="nt">background</span><span class="o">:</span> <span class="err">#000</span><span class="o">;</span>

<span class="nt">offset-path</span><span class="o">:</span> <span class="k">@shape</span><span class="p">(</span>
  <span class="n">points</span><span class="p">:</span> <span class="err">@</span><span class="n">i2</span><span class="p">;</span>
  <span class="nt">turn</span><span class="o">:</span> <span class="nt">-1</span><span class="o">;</span>
  <span class="nt">r</span><span class="o">:</span> <span class="err">$</span><span class="o">(.</span><span class="err">14</span> <span class="o">/</span> <span class="nt">cos</span><span class="o">(</span><span class="k">@i</span><span class="p">(</span><span class="err">*</span><span class="n">π</span><span class="p">,</span> <span class="p">/</span><span class="err">@</span><span class="n">i2</span><span class="p">,</span> <span class="p">/</span><span class="m">2</span><span class="p">)));</span>
  <span class="nt">rotate</span><span class="o">:</span> <span class="k">@i</span><span class="p">(</span><span class="err">*</span><span class="m">90</span><span class="p">,</span> <span class="p">/</span><span class="err">@</span><span class="n">i2</span><span class="p">);</span>
<span class="o">);</span>
<span class="nt">offset-distance</span><span class="o">:</span> <span class="nt">calc</span><span class="o">(</span>
  <span class="err">240</span><span class="nt">px</span><span class="o">/</span><span class="k">@I3</span> <span class="err">+</span> <span class="err">@</span><span class="n">t</span><span class="p">(/</span><span class="m">160%</span><span class="p">,</span> <span class="err">*@</span><span class="n">I2</span><span class="p">(</span><span class="n">-</span><span class="err">@</span><span class="n">i</span><span class="p">))</span>
<span class="p">);</span></code></pre></figure>
  </div>

  <p>
    Another puzzle solved, just like every time, it's been a lot of fun :)
  </p>

</article>

<style>
  .footnote.footnote {
    padding-top: 2em;
    color: #777;
    font-size: .9em;
    border-top: 1px solid var(--footnote-border-color);
    margin-top: 5em;
    line-height: 1.4;
  }

  video {
    width: 240px;
    height: 240px;
    display: block;
    background: #fafafa;
  }

  video, css-doodle {
    max-width: 100%;
  }

  figcaption {
    text-align: left;
    font-size: .8em;
    margin-top: .5em;
  }


  .example {
    display: flex;
    margin-bottom: 2em;
  }
  .example figure {
    margin: 0;
    order: 1;
    overflow: auto;
  }
  .example figure pre {
    margin: 0;
  }
  .example .doodle,
  .example css-doodle {
    order: 2;
    margin-left: auto;
    flex-shrink: 0;
  }

  .start-pause {
    margin-block: 1em;
  }

  @media screen and (max-width: 720px) {
    .example {
      display: block;
    }
    .example css-doodle {
      margin: 0 0 1em 0;
    }
    .example .doodle css-doodle {
      margin: 0;
    }
  }
</style>

<script type="module">import 'https://esm.sh/css-doodle@0.42.1'</script>

<script>
  if (/MicroMessenger/i.test(navigator.userAgent)) {
    document.querySelectorAll('video').forEach(v => {
      v.parentNode.style.position = 'relative';
      let button = document.createElement('button');
      button.textContent = 'Click to play video';
      button.style.position = 'absolute';
      button.style.top = '50%';
      button.style.left = '120px';
      button.style.transform = 'translate(-50%, -50%)';
      button.style.zIndex = 1;
      v.parentNode.appendChild(button);
      button.addEventListener('click', () => {
        v.play();
        button.style.display = 'none';
      });
    })
  }

  document.addEventListener('DOMContentLoaded', () => {
    let button = document.querySelector('.start-pause');

    function getCode(running) {
      let distance = '@t(/@r(50%, 500%), +@iI(*100%))';
      if (running) {
        distance =  '@iI(*200%, @r(50%, 500%))';
      }
      return `
        @grid: 20x1 / 240px / #051d1b;
        @place: center;
        @size: @iI(*50%, +40%);
        filter: drop-shadow(0 0 5px #9bff9b);
        background: @doodle(
          @grid: 20x1 / 100%;
          @size: @r(1%) @r(2%);
          position: absolute;
          background: @p(#93ffe4, #84ff92);
          offset-rotate: auto 90deg;
          offset-distance: ${distance};
          offset-path: @shape(points: 180; r: cos(4t));
        );
      `;
    }

    button.addEventListener('click', () => {
      let doodle = button.parentNode.querySelector('css-doodle');
      let running = doodle.dataset.state === 'running';
      doodle.update(getCode(running));
      doodle.dataset.state = (running ? 'paused' : 'running');
      button.textContent = (running ? 'animate' : 'pause');
    });
  });
</script>

]]></content>

      

      
        
      

      <summary type="html"><![CDATA[
  
    This is from last year when I tried to recreate Rach Smith's P5.js animation using CSS.
    The animation seemed like it could be implemented in a breeze, but there's an illusion of simplic...]]></summary>
    </entry>
  
    <entry>
      <title type="html">An Introduction to css-doodle</title>
      <link href="https://yuanchuan.dev/an-introduction-to-css-doodle" rel="alternate" type="text/html" title="An Introduction to css-doodle" />
      <published>2024-12-02T00:00:00+08:00</published>
      <updated>2024-12-02T00:00:00+08:00</updated>
      <id>https://yuanchuan.dev/an-introduction-to-css-doodle</id>
      <content type="html" xml:base="https://yuanchuan.dev/an-introduction-to-css-doodle"><![CDATA[<article>
  <p class="intro">
    This is a rough transcript based on my notes for the sharing session I presented
    at the online Groove meetup in June.
    I've shared the slides before, but I think it's worth putting the content in writing.
    The full slide deck is accessible on <a href="https://github.com/yuanchuan/intro-to-css-doodle">GitHub</a>.
  </p>
  <!--more-->

  <section>
    <article>
      <p>
        <a href="https://css-doodle.com">css-doodle</a> is a project I started many years ago.
        Initially, it was used to draw some simple graphic patterns,
        but over time, it evolved into a tool for creative coding and generative art.
        I’ve always wanted to write an article about css-doodle,
        and today I finally have the chance to give an introduction to it.
      </p>
    </article>
    <img src="https://yuanchuan.dev/assets/images/post/intro-to-css-doodle/1.png" loading="lazy" alt="slide cover" />
  </section>

  <section>
    <article>
      <p> So how did css-doodle begin? </p>
      <p>
        One day I came across a background pattern on Dribbble, by the product designer <a href="https://jpkoudstaal.com">Jan-Paul Koudstaal</a>.
        I was attracted by its colors and simplicity, and thought it might be a fun exercise to recreate it with CSS.
      <p>
      <p>
        I’m sure many people have had the same experience as me --
        seeing a graphic shape and wondering if it could be created with CSS.
      </p>
    </article>
    <img src="https://yuanchuan.dev/assets/images/post/intro-to-css-doodle/3.png" loading="lazy" alt="slide 3" />
  </section>

  <section>
    <article>
      <p>
        The first solution came to me was to use a grid of elements with different background colors and border radius values.
        The problem is that manually creating and assigning values to each element can be quite tedious.
        Although there are preprocessors helping me to generate HTML, there’s still a lot of code.
      </p>
      <p>
        In CSS, there are usually multiple ways to do just one thing.
        For example, the quarter circle in this pattern could also be made with <code>clip-path</code> or <code>radial-gradient</code>.
        What I want is to focus on these key properties.
      </p>
    </article>
    <img src="https://yuanchuan.dev/assets/images/post/intro-to-css-doodle/4.png" loading="lazy" alt="slide 4" />
  </section>

  <section>
    <article>
      <p>
        To explore CSS properties more efficiently, I used JavaScript to generate the grid and setup all the random variables.
        <a href="https://github.com/yuanchuan/css-shapes-patterns">This</a> is an earlier Web Component project prior to css-doodle that did the code generation.
      </p>
    </article>
    <img src="https://yuanchuan.dev/assets/images/post/intro-to-css-doodle/5.png" loading="lazy" alt="slide 5" />
  </section>

  <section>
    <article>
      <p>
        The idea behind css-doodle originated from that earlier project.
        The main difference is that it uses CSS directly rather than JavaScript to describe the styles.
        This is how the css-doodle code looks like to create the same pattern:
      </p>
      <p>
        First, we generate a <code>6x5</code> grid with the container width in <code>37vw</code> and a faint background color.
        Next, for each cell I pick a random color from the given color list using the <code>@p()</code> function.
        Then the same way to randomly pick <code>border-radius</code> values
        from another list generated with the <code>@cycle()</code> function.
      </p>
    </article>
    <img src="https://yuanchuan.dev/assets/images/post/intro-to-css-doodle/6.png" loading="lazy" alt="slide 6" />
  </section>

  <section>
    <article>
      <p>
        Another way to create this pattern is to use <code>clip-path</code>
        and the native <code>circle()</code> function.
      </p>
    </article>
    <img src="https://yuanchuan.dev/assets/images/post/intro-to-css-doodle/7.png" loading="lazy" alt="slide 7" />
  </section>

  <section>
    <article>
      <p>Or use <code>radial-gradient. </code></p>
      <p>
        Create the circle shape using <code>radial-graident</code> and
        then place the circle to 4 different places.
      </p>
      <p>
        As you can see the syntax of css-doodle is very similar to CSS, with the addition of a few extra functions.
      </p>
    </article>
    <img src="https://yuanchuan.dev/assets/images/post/intro-to-css-doodle/8.png" loading="lazy" alt="slide 8" />
  </section>

  <section>
    <article>
      <p>
        css-doodle doesn’t have a CSS parser like SASS or any other pre-processors.
        It doesn’t interpret the syntax of colors or gradients but takes a different approach.
      </p>
      <p>
        One reason for this is that I can’t catch up the evolution of CSS on my own.
        New features, such as units and color functions, are being added to CSS all the time.
        It would take time to sync the updates.
      </p>
      <p>
        This turned out to be the right decision, as I can always use the latest CSS features without upgrading css-doodle.
      </p>
    </article>
    <img src="https://yuanchuan.dev/assets/images/post/intro-to-css-doodle/9.png" loading="lazy" alt="slide 9" />
  </section>

  <section>
    <article>
      <p>
        A Typical CSS rule consists of three different parts: Selector, Property, and Value.
      </p>
    </article>
    <img src="https://yuanchuan.dev/assets/images/post/intro-to-css-doodle/10.png" loading="lazy" alt="slide 10" />
  </section>

  <section>
    <article>
      <p>
        css-doodle aims to extend CSS in these three parts.
        It doesn’t alter anything, only including several new selectors and properties,
        which begin with the <code>@</code> symbol in order to distinguish them from native properties and selectors.
      </p>
    </article>
    <img src="https://yuanchuan.dev/assets/images/post/intro-to-css-doodle/11.png" loading="lazy" alt="slide 11" />
  </section>

  <section>
    <article>
      <p>
        There is an example -- a grid where <code>@i</code> represents the index value of the current cell.
        setting the value to <code>@content</code> inserts the value as a text node,
        I often use it for debugging.
      </p>
      <p>
        The <code>@odd</code> selector applies the rules in its block only to cells with odd indices,
        so only the odd-numbered cells have a black background.
      </p>
      <p>
        You can also set a background color for a specific cell, the CSS cascade rules will apply as expected.
      </p>
    </article>
    <img src="https://yuanchuan.dev/assets/images/post/intro-to-css-doodle/12.png" loading="lazy" alt="slide 12" />
  </section>

  <section>
    <article>
      <p>
        The variables attached to the cells are quite useful for making differentiated changes.
        In this case, I give each cell a different size and rotation according to its index value.
      </p>
    </article>
    <img src="https://yuanchuan.dev/assets/images/post/intro-to-css-doodle/13.png" loading="lazy" alt="slide 13" />
  </section>

  <section>
    <article>
      <p>
        The <code>@i</code> is a variable as well as a function.
        Instead of using <code>calc()</code>,
        I can also put expression in function parameter to get the calculation with <code>@i</code> directly.
      </p>
    </article>
    <img src="https://yuanchuan.dev/assets/images/post/intro-to-css-doodle/14.png" loading="lazy" alt="slide 14" />
  </section>

  <section>
    <article>
      <p>
        Let’s get into the function syntax.
      </p>
    </article>
    <img src="https://yuanchuan.dev/assets/images/post/intro-to-css-doodle/15.png" loading="lazy" alt="slide 15" />
  </section>

  <section>
    <article>
      <p>
        If there’s no argument or parameter for the function, the parentheses can be omitted.
        Here the first and the second are literally the same.
      </p>
      <p>
        If there’s an operator attached at the head or the tail of the parameter it will try to do
        calculations with itself and the returned function value.
      </p>
    </article>
    <img src="https://yuanchuan.dev/assets/images/post/intro-to-css-doodle/16.png" loading="lazy" alt="slide 16" />
  </section>

  <section>
    <article>
      <p>
        This is another function where the previous three are equal.
      <p>
      <p>
        If the first parameter is a number,
        the function name and the number can be put together to form another function,
        It’s like <a href="https://en.wikipedia.org/wiki/Curry_(programming_language)">curry</a> in functional programming.
      </p>
      <p>
        Multiple functions can be chained together using dots, known as
        <a href="https://en.wikipedia.org/wiki/Function_composition_(computer_science)">function composition</a>.
        The output of each function will be the input of the previous one.
      </p>
    </article>
    <img src="https://yuanchuan.dev/assets/images/post/intro-to-css-doodle/17.png" loading="lazy" alt="slide 17" />
  </section>

  <section>
    <article>
      <p>
        Here is an example of the random distribution by calling the <code>@r</code> function four times.
      </p>
    </article>
    <img src="https://yuanchuan.dev/assets/images/post/intro-to-css-doodle/18.png" loading="lazy" alt="slide 18" />
  </section>

  <section>
    <article>
      <p>
        Random functions play an important role in generative art.
        In css-doodle there are two categories of random functions.
      </p>
    </article>
    <img src="https://yuanchuan.dev/assets/images/post/intro-to-css-doodle/19.png" loading="lazy" alt="slide 19" />
  </section>

  <section>
    <article>
      <p>
        The first group is picking, to pick a value from a given list.
        The name is inspired by the <a href="https://en.wikipedia.org/wiki/Logo_(programming_language)">Logo programming language</a>.
      </p>
    </article>
    <img src="https://yuanchuan.dev/assets/images/post/intro-to-css-doodle/20.png" loading="lazy" alt="slide 20" />
  </section>

  <section>
    <article>
      <p>
        There are five cells, each will pick a value from 1 to 5.
        The pick functions also recognize a shorthand input like regular expressions, to save some characters.
        The upper case <code>@P</code> will pick a random value but ensure the next one is different from the previous value.
        It can be useful in some scenarios like picking colors.
      </p>
    </article>
    <img src="https://yuanchuan.dev/assets/images/post/intro-to-css-doodle/21.png" loading="lazy" alt="slide 21" />
  </section>

  <section>
    <article>
      <p>
        css-doodle can be used to as a tool for learning CSS properties.
        It's easy to setup a grid and make a comparison to different values.
      </p>
    </article>
    <img src="https://yuanchuan.dev/assets/images/post/intro-to-css-doodle/22.png" loading="lazy" alt="slide 22" />
  </section>

  <section>
    <article>
      <p>
        Another group is random value generation, <code>@r</code> and <code>@R</code> (or <code>@rn</code>).
      </p>
      <p>
        The lower case <code>@r</code> is to get a random value from a given range, defaults to <code>0</code> to <code>1</code>.
      </p>
      <p>
        The uppercase <code>@R</code> is to get a random value using <a href="https://en.wikipedia.org/wiki/Perlin_noise">Perlin noise</a>.
      </p>
    </article>
    <img src="https://yuanchuan.dev/assets/images/post/intro-to-css-doodle/23.png" loading="lazy" alt="slide 23" />
  </section>

  <section>
    <article>
      <p>
        In <a href="https://p5js.org/">P5.js</a> you need to use <code>noise()</code> and map function together,
        while in css-doodle the <code>@R()</code> function will do the two actions in one place.
        After getting the noise value it will map the value to a given range according to the context.
      </p>
    </article>
    <img src="https://yuanchuan.dev/assets/images/post/intro-to-css-doodle/24.png" loading="lazy" alt="slide 24" />
  </section>

  <section>
    <article>
      <p>
        There's an example using the noise function <code>@R()</code>.
      </p>
    </article>
    <img src="https://yuanchuan.dev/assets/images/post/intro-to-css-doodle/25.png" loading="lazy" alt="slide 25" />
  </section>

  <section>
    <article>
      <p>
        By default css-doodle will use the current timestamp for random seed.
        If you forget to set a random seed and also want to keep the current result,
        you can always use devtools or JavaScript to get the <code>seed</code> property from the css-doodle element.
      </p>
    </article>
    <img src="https://yuanchuan.dev/assets/images/post/intro-to-css-doodle/26.png" loading="lazy" alt="slide 26" />
  </section>

  <section>
    <article>
      <p>
        There are some other kind of functions for generating. I call them generator functions.
      </p>
    </article>
    <img src="https://yuanchuan.dev/assets/images/post/intro-to-css-doodle/27.png" loading="lazy" alt="slide 27" />
  </section>

  <section>
    <article>
      <p>
        These functions generate values or transform the input to other forms,
        such as repeating or reversing a list.
        They are usually used in conjunction the picking group functions like <code>@p()</code>.
      </p>
    </article>
    <img src="https://yuanchuan.dev/assets/images/post/intro-to-css-doodle/28.png" loading="lazy" alt="slide 28" />
  </section>

  <section>
    <article>
      <p>
        The most frequently used is function <code>@m()</code>.
        It accepts two parameters, the first is the iteration count,
        the second is the value to be generated.
      <p>
        In CSS, properties like <code>background-image</code>, <code>box-shadow</code> accept multiple values,
        and these values are separated by commas.
        So the <code>@m()</code> function will join the values with commas automatically.
      </p>

    </article>
    <img src="https://yuanchuan.dev/assets/images/post/intro-to-css-doodle/29.png" loading="lazy" alt="slide 28" />
  </section>

  <section>
    <article>
      <p>
        The <code>@m()</code> function is much like a <a href="https://en.wikipedia.org/wiki/For_loop">for-loop</a> but in a declarative way.
      </p>
      <p>
        Sometimes I don't wish CSS to adopt for-loops just to mirror other programming languages.
        There are certainly different ways to achieve the same result, like <code>@m()</code>.
        We need to explore more possibilities in declarative syntax.
      </p>
    </article>
    <img src="https://yuanchuan.dev/assets/images/post/intro-to-css-doodle/30.png" loading="lazy" alt="slide 30" />
  </section>

  <section>
    <article>
      <p>
        OK next, background functions.
      </p>
    </article>
    <img src="https://yuanchuan.dev/assets/images/post/intro-to-css-doodle/31.png" loading="lazy" alt="slide 31" />
  </section>

  <section>
    <article>
      <p>
        I have experimented with many functions that generate images to CSS background.
        The <a href="https://developer.mozilla.org/en-US/docs/Web/API/CSS_Painting_API">CSS painting API</a> also uses background.
        It seems CSS background is a desirable place to extend the ability of CSS.
      </p>
      <p>
      </p>
    </article>
    <img src="https://yuanchuan.dev/assets/images/post/intro-to-css-doodle/32.png" loading="lazy" alt="slide 32" />
  </section>

  <section>
    <article>
      <p>
         To me CSS background is like a mirror or a digital screen,
         everything inside it is virtual and untouchable since there's no actual DOM inside there.
         But it gives us a window for imagination and a bridge to connect other things.
      </p>
    </article>
    <img src="https://yuanchuan.dev/assets/images/post/intro-to-css-doodle/33.png" loading="lazy" alt="slide 33" />
  </section>

  <section>
    <article>
      <p>
        With <code>@doodle</code> function I can use the rule of css-doodle to generate a background image.
        CSS itself can not generate new elements but I can open another dimension inside background.
        No gradient hacks anymore.
      </p>
    </article>
    <img src="https://yuanchuan.dev/assets/images/post/intro-to-css-doodle/34.png" loading="lazy" alt="slide 34" />
  </section>

  <section>
    <article>
      <p>
        They can even be nested.
      </p>
    </article>
    <img src="https://yuanchuan.dev/assets/images/post/intro-to-css-doodle/35.png" loading="lazy" alt="slide 35" />
  </section>

  <section>
    <article>
      <p>
        I added a <code>@shaders()</code> function to enable shader programming more straightforward. I haven’t used it much.
        The shader function and the removed <code>canvas()</code> function is quite low lever,
        which means one needs to write a lot of code to draw something.
      </p>
    </article>
    <img src="https://yuanchuan.dev/assets/images/post/intro-to-css-doodle/36.png" loading="lazy" alt="slide 36" />
  </section>

  <section>
    <article>
      <p>
        I prefer to use the <code>@pattern()</code> function.
        It was built on top of shaders and the syntax was designed similar to CSS.
        It’s much higher lever and has a single purpose.
        Maybe I should build more functions like this.
      </p>
    </article>
    <img src="https://yuanchuan.dev/assets/images/post/intro-to-css-doodle/37.png" loading="lazy" alt="slide 37" />
  </section>

  <section>
    <article>
      <p>
        Browsers support SVG as CSS background natively,
        so I simplified the syntax of SVG to make it more like writing CSS, encouraging me to use SVG more.
      </p>
    </article>
    <img src="https://yuanchuan.dev/assets/images/post/intro-to-css-doodle/38.png" loading="lazy" alt="slide 38" />
  </section>

  <section>
    <article>
      <p>
        If you're interested in this idea, there’s a detailed <a href="https://yuanchuan.dev/experimenting-a-new-syntax-to-write-svg">blog post</a> about this syntax:
      </p>
    </article>
    <img src="https://yuanchuan.dev/assets/images/post/intro-to-css-doodle/39.png" loading="lazy" alt="slide 39" />
  </section>

  <section>
    <article>
      <p>
        The last section is about shapes. I'll skim through it.
      </p>
    </article>
    <img src="https://yuanchuan.dev/assets/images/post/intro-to-css-doodle/40.png" loading="lazy" alt="slide 40" />
  </section>

  <section>
    <article>
      <p>
        These are the builtin shapes in css-doodle, which are generated with <code>clip-path</code>.
      </p>
    </article>
    <img src="https://yuanchuan.dev/assets/images/post/intro-to-css-doodle/41.png" loading="lazy" alt="slide 41" />
  </section>

  <section>
    <article>
      <p>
        To make one element into a shape, you can use the <code>@shape</code> property declaratively.
        Another option is to use the <code>@shape()</code> function.
      </p>
    </article>
    <img src="https://yuanchuan.dev/assets/images/post/intro-to-css-doodle/42.png" loading="lazy" alt="slide 42" />
  </section>

  <section>
    <article>
      <p>
        The <code>@shape()</code> function is also a generator,
        it provides several commands to define a customized shape with mathematical functions.
      </p>
    </article>
    <img src="https://yuanchuan.dev/assets/images/post/intro-to-css-doodle/43.png" loading="lazy" alt="slide 43" />
  </section>

  <section>
    <article>
      <p>
        Simple rules can create a wide variety of patterns.
        It’s fascinating to discover some new shapes this way.
      </p>
    </article>
    <img src="https://yuanchuan.dev/assets/images/post/intro-to-css-doodle/44.png" loading="lazy" alt="slide 44" />
  </section>

  <section>
    <article>
      <p>
        There’s the <a href="https://css-doodle.com/shapes/">playground</a>
        and a <a href="https://yuanchuan.dev/polygon-shapes">blog post</a> exploring more shapes.
      </p>
    </article>
    <img src="https://yuanchuan.dev/assets/images/post/intro-to-css-doodle/45.png" loading="lazy" alt="slide 45" />
  </section>

  <section>
    <article>
      <p>
        This is one of my favorite shapes,
        which demonstrates the power of <a href="https://en.wikipedia.org/wiki/Negative_space">negative space</a>.
      </p>
    </article>
    <img src="https://yuanchuan.dev/assets/images/post/intro-to-css-doodle/46.png" loading="lazy" alt="slide 46" />
  </section>

  <section>
    <article>
      <p>
        <a href="https://vimeo.com/45232468">This video</a> mentioned about the negative space and
        talked about the importance of setting limits, which reminded me of css-doodle,
        particularly its performance considerations and the limitations of CSS grids.
      </p>
      <p>
        Creating visually appealing graphic designs or artwork doesn’t require a lot of elements.
        The key is to view things differently.
        I highly recommend watching this video. It’s truely beautiful.
      </p>
    </article>
    <img src="https://yuanchuan.dev/assets/images/post/intro-to-css-doodle/47.png" loading="lazy" alt="slide 47" />
  </section>

  <section>
    <article>
      <p>
        Here is the <a href="https://css-doodle.com">project website</a> and
        a collection of <a href="https://codepen.io/collection/XyVkpQ">CodePen demos</a> using css-doodle.
        The documentation is not complete, and I still need to work on it.
      </p>
    </article>
    <img src="https://yuanchuan.dev/assets/images/post/intro-to-css-doodle/48.png" loading="lazy" alt="slide 48" />
  </section>

  <section>
    <article>
      <p>
        I hope you enjoy this introduction to css-doodle.
        Thank you!
      </p>
    </article>
    <img src="https://yuanchuan.dev/assets/images/post/intro-to-css-doodle/49.png" loading="lazy" alt="slide 49" />
  </section>

</article>

<style>
  section img {
    width: 100%;
    max-width: 480px;
    border: 3px solid #f0f0f0;
    border-radius: 8px;
    display: block;
  }
  section {
    margin-block-start: 2.4em;
  }
  section article p + p {
    margin-block-start: 1.5em;
  }
  @media screen and (min-width: 75em) {
    .intro {
      width: calc(50% - 2.4em);
    }
    .article {
      width: 75em;
      max-width: 84vw;
    }
    section {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 2.4em;
    }
    section img {
      max-width: 600px;
      margin-block-start: 0;
    }
  }
</style>


<style>
 /**
 * Skipped minification because the original files appears to be already minified.
 * Original file: /npm/medium-zoom@1.1.0/dist/style.css
 *
 * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
 */
.medium-zoom-overlay{position:fixed;top:0;right:0;bottom:0;left:0;opacity:0;transition:opacity .3s;will-change:opacity}.medium-zoom--opened .medium-zoom-overlay{cursor:pointer;cursor:zoom-out;opacity:1}.medium-zoom-image{cursor:pointer;cursor:zoom-in;transition:transform .3s cubic-bezier(.2,0,.2,1)!important}.medium-zoom-image--hidden{visibility:hidden}.medium-zoom-image--opened{position:relative;cursor:pointer;cursor:zoom-out;will-change:transform}
</style>
<script>
 /*! medium-zoom 1.1.0 | MIT License | https://github.com/francoischalifour/medium-zoom */
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e=e||self).mediumZoom=t()}(this,(function(){"use strict";var e=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var o=arguments[t];for(var n in o)Object.prototype.hasOwnProperty.call(o,n)&&(e[n]=o[n])}return e},t=function(e){return"IMG"===e.tagName},o=function(e){return e&&1===e.nodeType},n=function(e){return".svg"===(e.currentSrc||e.src).substr(-4).toLowerCase()},i=function(e){try{return Array.isArray(e)?e.filter(t):function(e){return NodeList.prototype.isPrototypeOf(e)}(e)?[].slice.call(e).filter(t):o(e)?[e].filter(t):"string"==typeof e?[].slice.call(document.querySelectorAll(e)).filter(t):[]}catch(e){throw new TypeError("The provided selector is invalid.\nExpects a CSS selector, a Node element, a NodeList or an array.\nSee: https://github.com/francoischalifour/medium-zoom")}},r=function(e){var t=document.createElement("div");return t.classList.add("medium-zoom-overlay"),t.style.background=e,t},d=function(e){var t=e.getBoundingClientRect(),o=t.top,n=t.left,i=t.width,r=t.height,d=e.cloneNode(),a=window.pageYOffset||document.documentElement.scrollTop||document.body.scrollTop||0,m=window.pageXOffset||document.documentElement.scrollLeft||document.body.scrollLeft||0;return d.removeAttribute("id"),d.style.position="absolute",d.style.top=o+a+"px",d.style.left=n+m+"px",d.style.width=i+"px",d.style.height=r+"px",d.style.transform="",d},a=function(t,o){var n=e({bubbles:!1,cancelable:!1,detail:void 0},o);if("function"==typeof window.CustomEvent)return new CustomEvent(t,n);var i=document.createEvent("CustomEvent");return i.initCustomEvent(t,n.bubbles,n.cancelable,n.detail),i};return function(e,t){void 0===t&&(t={});var o=t.insertAt;if(e&&"undefined"!=typeof document){var n=document.head||document.getElementsByTagName("head")[0],i=document.createElement("style");i.type="text/css","top"===o&&n.firstChild?n.insertBefore(i,n.firstChild):n.appendChild(i),i.styleSheet?i.styleSheet.cssText=e:i.appendChild(document.createTextNode(e))}}(".medium-zoom-overlay{position:fixed;top:0;right:0;bottom:0;left:0;opacity:0;transition:opacity .3s;will-change:opacity}.medium-zoom--opened .medium-zoom-overlay{cursor:pointer;cursor:zoom-out;opacity:1}.medium-zoom-image{cursor:pointer;cursor:zoom-in;transition:transform .3s cubic-bezier(.2,0,.2,1)!important}.medium-zoom-image--hidden{visibility:hidden}.medium-zoom-image--opened{position:relative;cursor:pointer;cursor:zoom-out;will-change:transform}"),function t(m){var l=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},c=window.Promise||function(e){function t(){}e(t,t)},u=function(e){var t=e.target;t!==N?-1!==x.indexOf(t)&&w({target:t}):E()},s=function(){if(!A&&k.original){var e=window.pageYOffset||document.documentElement.scrollTop||document.body.scrollTop||0;Math.abs(S-e)>T.scrollOffset&&setTimeout(E,150)}},f=function(e){var t=e.key||e.keyCode;"Escape"!==t&&"Esc"!==t&&27!==t||E()},p=function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=t;if(t.background&&(N.style.background=t.background),t.container&&t.container instanceof Object&&(n.container=e({},T.container,t.container)),t.template){var i=o(t.template)?t.template:document.querySelector(t.template);n.template=i}return T=e({},T,n),x.forEach((function(e){e.dispatchEvent(a("medium-zoom:update",{detail:{zoom:j}}))})),j},g=function(){var o=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return t(e({},T,o))},v=function(){for(var e=arguments.length,t=Array(e),o=0;o<e;o++)t[o]=arguments[o];var n=t.reduce((function(e,t){return[].concat(e,i(t))}),[]);return n.filter((function(e){return-1===x.indexOf(e)})).forEach((function(e){x.push(e),e.classList.add("medium-zoom-image")})),O.forEach((function(e){var t=e.type,o=e.listener,i=e.options;n.forEach((function(e){e.addEventListener(t,o,i)}))})),j},h=function(){for(var e=arguments.length,t=Array(e),o=0;o<e;o++)t[o]=arguments[o];k.zoomed&&E();var n=t.length>0?t.reduce((function(e,t){return[].concat(e,i(t))}),[]):x;return n.forEach((function(e){e.classList.remove("medium-zoom-image"),e.dispatchEvent(a("medium-zoom:detach",{detail:{zoom:j}}))})),x=x.filter((function(e){return-1===n.indexOf(e)})),j},z=function(e,t){var o=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return x.forEach((function(n){n.addEventListener("medium-zoom:"+e,t,o)})),O.push({type:"medium-zoom:"+e,listener:t,options:o}),j},y=function(e,t){var o=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return x.forEach((function(n){n.removeEventListener("medium-zoom:"+e,t,o)})),O=O.filter((function(o){return!(o.type==="medium-zoom:"+e&&o.listener.toString()===t.toString())})),j},b=function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},i=t.target,r=function(){var t={width:document.documentElement.clientWidth,height:document.documentElement.clientHeight,left:0,top:0,right:0,bottom:0},i=void 0,r=void 0;if(T.container)if(T.container instanceof Object)i=(t=e({},t,T.container)).width-t.left-t.right-2*T.margin,r=t.height-t.top-t.bottom-2*T.margin;else{var d=(o(T.container)?T.container:document.querySelector(T.container)).getBoundingClientRect(),a=d.width,m=d.height,l=d.left,c=d.top;t=e({},t,{width:a,height:m,left:l,top:c})}i=i||t.width-2*T.margin,r=r||t.height-2*T.margin;var u=k.zoomedHd||k.original,s=n(u)?i:u.naturalWidth||i,f=n(u)?r:u.naturalHeight||r,p=u.getBoundingClientRect(),g=p.top,v=p.left,h=p.width,z=p.height,y=Math.min(Math.max(h,s),i)/h,b=Math.min(Math.max(z,f),r)/z,E=Math.min(y,b),w="scale("+E+") translate3d("+((i-h)/2-v+T.margin+t.left)/E+"px, "+((r-z)/2-g+T.margin+t.top)/E+"px, 0)";k.zoomed.style.transform=w,k.zoomedHd&&(k.zoomedHd.style.transform=w)};return new c((function(e){if(i&&-1===x.indexOf(i))e(j);else{if(k.zoomed)e(j);else{if(i)k.original=i;else{if(!(x.length>0))return void e(j);var t=x;k.original=t[0]}if(k.original.dispatchEvent(a("medium-zoom:open",{detail:{zoom:j}})),S=window.pageYOffset||document.documentElement.scrollTop||document.body.scrollTop||0,A=!0,k.zoomed=d(k.original),document.body.appendChild(N),T.template){var n=o(T.template)?T.template:document.querySelector(T.template);k.template=document.createElement("div"),k.template.appendChild(n.content.cloneNode(!0)),document.body.appendChild(k.template)}if(k.original.parentElement&&"PICTURE"===k.original.parentElement.tagName&&k.original.currentSrc&&(k.zoomed.src=k.original.currentSrc),document.body.appendChild(k.zoomed),window.requestAnimationFrame((function(){document.body.classList.add("medium-zoom--opened")})),k.original.classList.add("medium-zoom-image--hidden"),k.zoomed.classList.add("medium-zoom-image--opened"),k.zoomed.addEventListener("click",E),k.zoomed.addEventListener("transitionend",(function t(){A=!1,k.zoomed.removeEventListener("transitionend",t),k.original.dispatchEvent(a("medium-zoom:opened",{detail:{zoom:j}})),e(j)})),k.original.getAttribute("data-zoom-src")){k.zoomedHd=k.zoomed.cloneNode(),k.zoomedHd.removeAttribute("srcset"),k.zoomedHd.removeAttribute("sizes"),k.zoomedHd.removeAttribute("loading"),k.zoomedHd.src=k.zoomed.getAttribute("data-zoom-src"),k.zoomedHd.onerror=function(){clearInterval(m),console.warn("Unable to reach the zoom image target "+k.zoomedHd.src),k.zoomedHd=null,r()};var m=setInterval((function(){k.zoomedHd.complete&&(clearInterval(m),k.zoomedHd.classList.add("medium-zoom-image--opened"),k.zoomedHd.addEventListener("click",E),document.body.appendChild(k.zoomedHd),r())}),10)}else if(k.original.hasAttribute("srcset")){k.zoomedHd=k.zoomed.cloneNode(),k.zoomedHd.removeAttribute("sizes"),k.zoomedHd.removeAttribute("loading");var l=k.zoomedHd.addEventListener("load",(function(){k.zoomedHd.removeEventListener("load",l),k.zoomedHd.classList.add("medium-zoom-image--opened"),k.zoomedHd.addEventListener("click",E),document.body.appendChild(k.zoomedHd),r()}))}else r()}}}))},E=function(){return new c((function(e){if(!A&&k.original){A=!0,document.body.classList.remove("medium-zoom--opened"),k.zoomed.style.transform="",k.zoomedHd&&(k.zoomedHd.style.transform=""),k.template&&(k.template.style.transition="opacity 150ms",k.template.style.opacity=0),k.original.dispatchEvent(a("medium-zoom:close",{detail:{zoom:j}})),k.zoomed.addEventListener("transitionend",(function t(){k.original.classList.remove("medium-zoom-image--hidden"),document.body.removeChild(k.zoomed),k.zoomedHd&&document.body.removeChild(k.zoomedHd),document.body.removeChild(N),k.zoomed.classList.remove("medium-zoom-image--opened"),k.template&&document.body.removeChild(k.template),A=!1,k.zoomed.removeEventListener("transitionend",t),k.original.dispatchEvent(a("medium-zoom:closed",{detail:{zoom:j}})),k.original=null,k.zoomed=null,k.zoomedHd=null,k.template=null,e(j)}))}else e(j)}))},w=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=e.target;return k.original?E():b({target:t})},L=function(){return T},H=function(){return x},C=function(){return k.original},x=[],O=[],A=!1,S=0,T=l,k={original:null,zoomed:null,zoomedHd:null,template:null};"[object Object]"===Object.prototype.toString.call(m)?T=m:(m||"string"==typeof m)&&v(m),T=e({margin:0,background:"#fff",scrollOffset:40,container:null,template:null},T);var N=r(T.background);document.addEventListener("click",u),document.addEventListener("keyup",f),document.addEventListener("scroll",s),window.addEventListener("resize",E);var j={open:b,close:E,toggle:w,update:p,clone:g,attach:v,detach:h,on:z,off:y,getOptions:L,getImages:H,getZoomedImage:C};return j}}));
</script>
<script>
  document.addEventListener('DOMContentLoaded', () => {
    mediumZoom('article img');
  });
</script>
]]></content>

      

      
        
      

      <summary type="html"><![CDATA[
  
    This is a rough transcript based on my notes for the sharing session I presented
    at the online Groove meetup in June.
    I've shared the slides before, but I think it's worth putting t...]]></summary>
    </entry>
  
    <entry>
      <title type="html">Generating images from css-doodle code</title>
      <link href="https://yuanchuan.dev/generating-images-from-css-doodle-code.html" rel="alternate" type="text/html" title="Generating images from css-doodle code" />
      <published>2024-10-15T00:00:00+08:00</published>
      <updated>2024-10-15T00:00:00+08:00</updated>
      <id>https://yuanchuan.dev/generating-images-from-css-doodle-code.html</id>
      <content type="html" xml:base="https://yuanchuan.dev/generating-images-from-css-doodle-code.html"><![CDATA[<article>
  <p>
    For the past week, I've been working on <a href="https://github.com/css-doodle/cli">a command-line tool</a>
    to generate images from <a href="https://css-doodle.com">css-doodle</a> source code.
    If you're familiar with command lines, try installing it with <code>npm</code>:
  </p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash">npm i <span class="nt">-g</span> @css-doodle/cli</code></pre></figure>

  <!--more-->
  <p>
    After that, you'll be able to generate images with one line of command.
  </p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash">cssd render code.css <span class="nt">-o</span> result.png</code></pre></figure>

  <p>
    If the code includes animations, you can also generate a video by using the <code>.webm</code> or <code>.mp4</code> extension for the output filename.
  </p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash">cssd render code.css <span class="nt">-o</span> result.webm</code></pre></figure>

  <p>
    Note that this tool is built on <a href="https://pptr.dev/">Puppeteer</a> and requires <a href="https://www.ffmpeg.org/">FFmpeg</a> to be installed for video generation.
  </p>
  <p>
    Read more available options <a href="https://github.com/css-doodle/cli?tab=readme-ov-file#render">here</a>.
  </p>

  <h2>For CSS artists</h2>

  <p>
    I kept CSS artists in mind since the every beginning of development, so this tool also supports generating images from
    local HTML files and <a href="https://codepen.io/">CodePen</a> links directly.
  </p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="c"># from local html file</span>
cssd render index.html

<span class="c"># from CodePen link</span>
cssd render https://codepen.io/yuanchuan/pen/MQEeJo</code></pre></figure>

  <p>
    If you ever need to generate an image for print, you can add the <code>-x</code> option to generate the image in a higher resolution.
  </p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="c"># 5x size to the original</span>
cssd render index.html <span class="nt">--selector</span> <span class="s1">'#graph'</span> <span class="nt">-x</span> 5</code></pre></figure>

  <h2>Preview</h2>

  <p>
    In the past, displaying <code>css-doodle</code> required a browser and wrapping the code inside the
    custom <code>&lt;css-doodle&gt;</code> element.
  </p>

<figure class="highlight"><pre><code class="language-html" data-lang="html"><span class="nt">&lt;css-doodle&gt;</span>
  <span class="c">&lt;!-- code here --&gt;</span>
<span class="nt">&lt;/css-doodle&gt;</span></code></pre></figure>
  <p>
    Now with command <code>cssd preview</code>, it will open a window to preview the result with
    live updates as you edit. The browser becomes invisible.
  </p>


<figure class="highlight"><pre><code class="language-bash" data-lang="bash">cssd preview example.css</code></pre></figure>

  <p>
    <img src="https://yuanchuan.dev/assets/images/post/generate-images/preview.webp" alt="preview">
  </p>

  <h2>Thoughts</h2>

  <p>
    After coding in the editor without <code>&lt;css-doodle&gt;</code> tags for a while,
    I realized there's a transition to make <code>css-doodle</code> a small programming language.
  </p>

  <p>
    It's quite different.
    Practices once considered effective, such as using short function names,
    are now seem to be a bad choice.
    It also lacks a structure to organize and group the code, which would make it more elegant and easier to read.
  </p>

  <p>
    There's still missing a lot and a long way to go, but I'm excited about the direction it is heading.
  </p>

  <p>GitHub: <a href="https://github.com/css-doodle/cli">https://github.com/css-doodle/cli</a></p>
</article>
]]></content>

      

      
        
      

      <summary type="html"><![CDATA[
  
    For the past week, I've been working on a command-line tool
    to generate images from css-doodle source code.
    If you're familiar with command lines, try installing it with npm:
  

np...]]></summary>
    </entry>
  
    <entry>
      <title type="html">Adding a shortcut for hue-rotate()</title>
      <link href="https://yuanchuan.dev/adding-shortcut-for-hue-rotate" rel="alternate" type="text/html" title="Adding a shortcut for hue-rotate()" />
      <published>2024-08-24T00:00:00+08:00</published>
      <updated>2024-08-24T00:00:00+08:00</updated>
      <id>https://yuanchuan.dev/adding-shortcut-for-hue-rotate</id>
      <content type="html" xml:base="https://yuanchuan.dev/adding-shortcut-for-hue-rotate"><![CDATA[<article>
  <p>
    I like the <code>background()</code> function in <a href="https://p5js.org/reference/p5/background/">P5.js</a>.
    It accepts one or multiple parameters and is very easy to use.
    Setting the background color is often one of the first steps when starting a new P5.js sketch.
  </p>

  <!--more-->

<figure class="highlight"><pre><code class="language-js" data-lang="js"><span class="kd">function</span> <span class="nf">setup</span><span class="p">()</span> <span class="p">{</span>
  <span class="nf">background</span><span class="p">(</span><span class="mi">200</span><span class="p">);</span>
<span class="p">}</span></code></pre></figure>

  <p>
    When comes to <a href="https://css-doodle.com/">CSS Doodle</a>,
    the background color is usually defined within the <code>:doodle</code> block,
    which indicates that the color is applied directly to the component element.
  </p>

<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="k">@grid</span><span class="p">:</span> <span class="m">1</span> <span class="p">/</span> <span class="m">400px</span><span class="p">;</span>

<span class="nd">:doodle</span> <span class="p">{</span>
  <span class="nl">background</span><span class="p">:</span> <span class="m">#f0f0f0</span><span class="p">;</span>
<span class="p">}</span></code></pre></figure>

  <p>
    I thought it would be convenient to have a short command for adding background color, to save time and make the code more concise.
    So I extended the <code>@grid</code> property to read a second <code>/</code> symbol.
  </p>

<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="k">@grid</span><span class="p">:</span> <span class="m">1</span> <span class="p">/</span> <span class="m">400px</span> <span class="p">/</span> <span class="m">#f0f0f0</span><span class="p">;</span></code></pre></figure>

  <p>
    I was happy with this new syntax and never looked back at the old style.
  </p>
  <p>
    Over the past year, I've gradually added several other shortcut commands for the <code>@grid</code> property.
    Some of them are visually intuitive -- at least for me.
  </p>

<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="o">*</span> <span class="err">45</span><span class="nt">deg</span>    <span class="o">//</span> <span class="nt">rotate</span>
<span class="o">+</span> <span class="err">2</span>        <span class="o">//</span> <span class="nt">scale</span>
<span class="o">~</span> <span class="err">100</span><span class="o">%</span>     <span class="o">//</span> <span class="nt">translate</span>
<span class="err">∆</span> <span class="err">1000</span><span class="nt">px</span>   <span class="o">//</span> <span class="nt">add</span> <span class="nt">perspective</span>
<span class="nt">_</span> <span class="err">1</span><span class="nt">px</span>      <span class="o">//</span> <span class="nt">add</span> <span class="nt">grid</span> <span class="nt">gap</span></code></pre></figure>

  <p>
    I chose those symbols based on their visual similarity to the action they represent.
    For exmaple, the <code>*</code> symbol looks like a spin and points toward to different directions.
  </p>

  <h2>Shortcut for <code>hue-rotate()</code></h2>

  <p>
    Yesterday, I found that the <code>hue-rotate()</code> of the <code>filter</code> property is another frequently used function
    when I want to add color variation to the entire element.
    To simplify the task, I decided to create a new command for it.
  </p>

<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="k">@grid</span><span class="p">:</span> <span class="m">1</span> <span class="p">/</span> <span class="m">400px</span><span class="p">;</span>

<span class="nd">:doodle</span> <span class="p">{</span>
  <span class="nl">filter</span><span class="p">:</span> <span class="n">hue-rotate</span><span class="p">(</span><span class="m">30deg</span><span class="p">);</span>
<span class="p">}</span></code></pre></figure>

  <p>
    I explored the characters available on the keyboard and those accessible with the <kbd>Alt</kbd> key on my computer, which I had done multiple times before. However, none of them seemed relevant to <strong>hue rotation</strong>.
  </p>

  <p class="symbols">
    !@#$%&*¡™£¢∞§¶•ªº–≠«`œ∑´®†\¨ˆøπ“‘åß∂ƒ©˙∆˚¬…æ¸√∫˜µ≤≥÷≈
  </p>

  <p>
    I was about to give up, then suddenly realized that the <code>rotate</code> property can accept additional axis values, like this:
  </p>

<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="nt">rotate</span><span class="o">:</span> <span class="nt">x</span> <span class="err">10</span><span class="nt">deg</span><span class="o">;</span>
<span class="nt">rotate</span><span class="o">:</span> <span class="nt">y</span> <span class="err">10</span><span class="nt">deg</span><span class="o">;</span>
<span class="nt">rotate</span><span class="o">:</span> <span class="nt">z</span> <span class="err">10</span><span class="nt">deg</span><span class="o">;</span></code></pre></figure>

  <p>
    So why not resue the existing <code>*</code> command and add a custom <code>h</code> for <strong>hue rotation</strong>?
    It's unlikely that CSS will add an axis named <code>h</code> to the <code>rotate</code> property in the future,
    so there shouldn't be any conflicts with existing CSS keywords.
  </p>

<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="o">*</span> <span class="err">30</span><span class="nt">deg</span>    <span class="o">//</span> <span class="nt">rotate</span><span class="o">:</span> <span class="err">30</span><span class="nt">deg</span><span class="o">;</span>
<span class="o">*</span> <span class="nt">x</span> <span class="err">30</span><span class="nt">deg</span>  <span class="o">//</span> <span class="nt">rotate</span><span class="o">:</span> <span class="nt">x</span> <span class="err">30</span><span class="nt">deg</span><span class="o">;</span>
<span class="o">*</span> <span class="nt">h</span> <span class="err">30</span><span class="nt">deg</span>  <span class="o">//</span> <span class="nt">filter</span><span class="o">:</span> <span class="nt">hue-rotate</span><span class="o">(</span><span class="err">30</span><span class="nt">deg</span><span class="o">);</span></code></pre></figure>
  <p>

  <p>
    This seemed a proper choice, and best of all, no new symbol to remember.
  </p>

<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="o">*</span> <span class="nt">h</span> <span class="o">&lt;</span><span class="nt">degree</span><span class="o">&gt;</span></code></pre></figure>

  <p>
    Now I can quickly add <code>hue-rotate()</code> to the component element with the command <code>*</code> followed by <code>h</code> and a degree.
  </p>

  <div class="example">
<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="k">@grid</span><span class="p">:</span> <span class="m">1</span><span class="n">x36</span> <span class="p">/</span> <span class="m">400px</span> <span class="p">/</span> <span class="m">#f90</span> <span class="err">+</span><span class="m">2</span> <span class="err">*</span><span class="m">45deg</span> <span class="err">*</span><span class="n">h</span> <span class="err">@</span><span class="n">r</span><span class="p">(</span><span class="err">±</span><span class="m">90deg</span><span class="p">);</span>
<span class="k">@place</span><span class="p">:</span> <span class="nb">center</span><span class="p">;</span>
<span class="k">@size</span><span class="p">:</span> <span class="m">100%</span><span class="p">;</span>
<span class="nt">content</span><span class="o">:</span> <span class="k">@svg-polygon</span><span class="p">(</span>
  <span class="n">points</span><span class="p">:</span> <span class="m">180</span><span class="p">;</span>
  <span class="nt">move</span><span class="o">:</span> <span class="err">0</span> <span class="err">$</span><span class="o">(</span><span class="k">@dy</span><span class="p">/</span><span class="err">@</span><span class="n">Y</span><span class="err">*</span><span class="m">2</span><span class="p">);</span>
  <span class="nt">fill</span><span class="o">:</span> <span class="k">@P</span><span class="p">(</span><span class="m">#f90</span><span class="p">,</span> <span class="m">#00f</span><span class="p">,</span> <span class="no">pink</span><span class="p">);</span>
  <span class="nt">a</span><span class="o">:</span> <span class="nt">sin</span><span class="o">((</span><span class="k">@Y</span><span class="p">/</span><span class="m">2</span><span class="n">-</span><span class="err">@</span><span class="n">abs</span><span class="p">.</span><span class="n">dy</span><span class="p">)/</span><span class="m">6.9</span><span class="p">)</span><span class="err">^</span><span class="m">1.2</span><span class="p">;</span>
  <span class="nt">x</span><span class="o">:</span> <span class="nt">a</span><span class="o">*</span><span class="nt">cos</span><span class="o">(</span><span class="nt">t</span><span class="o">)/(</span><span class="err">1</span><span class="o">+</span><span class="nt">sin</span><span class="o">(</span><span class="err">2</span><span class="nt">t</span><span class="o">)^</span><span class="err">2</span><span class="o">);</span>
  <span class="nt">y</span><span class="o">:</span> <span class="nt">x</span><span class="o">*</span><span class="nt">sin</span><span class="o">(</span><span class="err">3</span><span class="nt">t</span><span class="o">);</span>
<span class="o">);</span></code></pre></figure>
  <css-doodle click-to-update><style>
@grid: 1x36 / 400px / #f90 +2 *45deg *h @r(±90deg);
@place: center;
@size: 100%;
content: @svg-polygon(
  points: 180;
  move: 0 $(@dy/@Y*2);
  fill: @pn(#f90, #00f, pink);
  a: sin((@Y/2-@abs.dy)/6.9)^1.2;
  x: a*cos(t)/(1+sin(2t)^2);
  y: x*sin(3t);
);
  </style></css-doodle>
    <button id="update">Update</button>
  </div>

</article>

<style>
  .example + p {
    margin-top: 1.5em;
  }

  .example css-doodle {
    max-width: 100%;
    height: auto;
    aspect-ratio: 1;
  }

  button {
    margin-top: 1em;
  }

  .footnote.footnote {
    padding-top: 2em;
    color: #777;
    font-size: .9em;
    border-top: 1px solid var(--footnote-border-color);
    margin-top: 4em;
    line-height: 1.4;
  }

  .symbols {
    font-family: monospace;
  }
</style>

<script>
  let update = document.querySelector('#update');
  update.addEventListener('click', e => {
    let doodle = e.target.parentNode.querySelector('css-doodle');
    if (doodle) {
      doodle.update();
    }
  });
</script>

<script src="https://cdnjs.cloudflare.com/ajax/libs/css-doodle/0.40.0/css-doodle.min.js"></script>
]]></content>

      

      
        
      

      <summary type="html"><![CDATA[
  
    I like the background() function in P5.js.
    It accepts one or multiple parameters and is very easy to use.
    Setting the background color is often one of the first steps when starting ...]]></summary>
    </entry>
  
    <entry>
      <title type="html">Time-based CSS Animations</title>
      <link href="https://yuanchuan.dev/time-based-css-animations" rel="alternate" type="text/html" title="Time-based CSS Animations" />
      <published>2024-05-05T00:00:00+08:00</published>
      <updated>2024-05-05T00:00:00+08:00</updated>
      <id>https://yuanchuan.dev/time-based-css-animations</id>
      <content type="html" xml:base="https://yuanchuan.dev/time-based-css-animations"><![CDATA[<article>
  <p>
    In my earlier post <a href="/time-uniform-for-css-animation">Time Uniform For CSS Animation</a>,
    I took a note about a way to do CSS animations with <em>time ticks</em> instead of <em>keyframes</em>.
    It was limited applicable because CSS lacked the ability of doing complex Math calculations.
  </p>
  <p>
    After years of wait, CSS now has enough Math functions supported,
    particularly <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/mod">mod()</a>, <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/round">round()</a>,
    and <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Functions#trigonometric_functions">trigonometric functions</a>.
    It's time to revisit the time-based way of animation, hope it'll be more useful this time.
  </p>

  <!--more-->

  <div class="warning">
    You may need to enable the <em>Experimental feature flags</em> to view demos in this page.
  </div>

  <h2>The basic idea</h2>

  <p>
    Using <em>time</em> for animation is very common in shader programs and various other places.
    CSS can not start a timer like JavaScript does,
    but nowadays it's possible to define a custom variable
    with the <a href="https://developer.mozilla.org/en-US/docs/Web/API/CSS_Properties_and_Values_API/guide">CSS Houdini API</a> to track <em>time</em> in milliseconds.
  </p>

<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="k">@property</span> <span class="n">--t</span> <span class="p">{</span>
  <span class="py">syntax</span><span class="p">:</span> <span class="s1">"&lt;integer&gt;"</span><span class="p">;</span>
  <span class="py">initial-value</span><span class="p">:</span> <span class="m">0</span><span class="p">;</span>
  <span class="py">inherits</span><span class="p">:</span> <span class="n">true</span>
<span class="p">}</span>
<span class="k">@keyframes</span> <span class="n">tick</span> <span class="p">{</span>
  <span class="nt">from</span> <span class="p">{</span> <span class="py">--t</span><span class="p">:</span> <span class="m">0</span> <span class="p">}</span>
  <span class="nt">to</span>   <span class="p">{</span> <span class="py">--t</span><span class="p">:</span> <span class="m">86400000</span> <span class="p">}</span>
<span class="p">}</span>
<span class="nd">:root</span> <span class="p">{</span>
  <span class="nl">animation</span><span class="p">:</span> <span class="n">tick</span> <span class="m">86400000ms</span> <span class="n">linear</span> <span class="n">infinite</span>
<span class="p">}</span></code></pre></figure>
  <p>
    For each millisecond, the variable <code>--t</code> increments by <code>1</code>, which is <code>1000</code> in one second.
    There's a trick to show the variable with <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/counter">counter()</a> function.
  </p>

<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="nd">::after</span> <span class="p">{</span>
  <span class="nl">counter-reset</span><span class="p">:</span> <span class="n">t</span> <span class="n">var</span><span class="p">(</span><span class="n">--t</span><span class="p">);</span>
  <span class="nl">content</span><span class="p">:</span> <span class="n">counter</span><span class="p">(</span><span class="n">t</span><span class="p">);</span>
<span class="p">}</span></code></pre></figure>

  <div class="example">
    <demo-block animate="true">
      <template>
        <p></p>
        <button action="toggle">Start / Stop</button>
        <style>
          p:after {
            counter-reset: t calc(var(--t));
            content: counter(t);
          }
          p {
            margin: 0;
          }
        </style>
      </template>
    </demo-block>
  </div>

  <p>
    Other values which are based on <code>--t</code> will change along with it.
    That's how we get the <strong>animation</strong> effect.
  </p>

<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="nt">div</span> <span class="p">{</span>
  <span class="c">/* 1 turn per second */</span>
  <span class="py">rotate</span><span class="p">:</span> <span class="n">calc</span><span class="p">(</span><span class="n">var</span><span class="p">(</span><span class="n">--t</span><span class="p">)</span> <span class="err">*</span> <span class="m">.001turn</span><span class="p">);</span>
<span class="p">}</span></code></pre></figure>
  <div class="example">
    <demo-block>
      <template>
        <div class="demo">
          <div class="rect animate-object"></div>
        </div>
        <button action="toggle">Start / Stop</button>
        <style>
          .demo {
            width: 100px;
            height: 100px;
            display: grid;
            place-items: center;
            border: 1px dashed #aaa;
          }
          .demo .rect {
            width: 80%;
            height: 5%;
            background: #000;
            rotate: calc(var(--t) * .001turn);
          }
        </style>
      </template>
    </demo-block>
  </div>

  <h2>Controlling frame rate</h2>

  <p>
    Maintaining the update frequencey at 60 frames per second (FPS) is sufficient for a smooth animation.
    Browsers often have optimizations for rendering so there wouldn't be any problem if the frequencey is higher than 60 FPS.
    But one can manually control the frame rate using <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/animation-timing-function#stepsn_jumpterm">step()</a>
    function if needed.
  </p>
<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="c">/* ... */</span>

<span class="nd">:root</span> <span class="p">{</span>
  <span class="nl">animation</span><span class="p">:</span> <span class="n">tick</span><span class="p">;</span>
  <span class="nl">animation-duration</span><span class="p">:</span> <span class="m">86400000ms</span><span class="p">;</span>
  <span class="nl">animation-iteration-count</span><span class="p">:</span> <span class="n">infinite</span><span class="p">;</span>

  <span class="c">/* 8 fps */</span>
  <span class="nl">animation-timing-function</span><span class="p">:</span> <span class="n">step</span><span class="p">(</span><span class="n">calc</span><span class="p">(</span><span class="m">86400000</span><span class="p">/(</span><span class="m">1000</span><span class="p">/</span><span class="m">8</span><span class="p">)));</span>

  <span class="c">/* 24 fps */</span>
  <span class="nl">animation-timing-function</span><span class="p">:</span> <span class="n">step</span><span class="p">(</span><span class="n">calc</span><span class="p">(</span><span class="m">86400000</span><span class="p">/(</span><span class="m">1000</span><span class="p">/</span><span class="m">24</span><span class="p">)));</span>

  <span class="c">/* 60 fps */</span>
  <span class="nl">animation-timing-function</span><span class="p">:</span> <span class="n">step</span><span class="p">(</span><span class="n">calc</span><span class="p">(</span><span class="m">86400000</span><span class="p">/(</span><span class="m">1000</span><span class="p">/</span><span class="m">60</span><span class="p">)));</span>
<span class="p">}</span></code></pre></figure>

  <div class="example">
    <demo-block>
      <template>
        <div class="example-group">
          <div class="block">
            <div class="rect animate-object" style="--timer: var(--t8)"></div>
            <span>8 FPS</span>
          </div>
          <div class="block">
            <div class="rect animate-object" style="--timer: var(--t24)"></div>
            <span>24 FPS</span>
          </div>
          <div class="block">
            <div class="rect animate-object" style="--timer: var(--t)"></div>
            <span>60 FPS</span>
          </div>
        </div>
        <button action="toggle">Start / Stop</button>
        <style>
          .example-group {
            display: flex;
            gap: 5px;
          }
          .block {
            width: 100px;
            height: 120px;
            display: grid;
            place-items: center;
            border: 1px dashed #aaa;
            position: relative;
          }
          .block span {
            position: absolute;
            height: 20px;
            bottom: 2px;
            text-align: center;
            font-size: .75em;
            color: #aaa;
          }
          .block .rect {
            width: 80%;
            height: 5%;
            margin-top: -20px;
            background: #000;
            rotate: calc(var(--timer) * .001turn / 2)
          }
        </style>
      </template>
    </demo-block>
  </div>

  <h2>Transform time</h2>

  <p>
    The value of <code>--t</code> grows constantly in one direction.
    It's all right for the <code>angle</code> value that is bigger than <code>360deg</code>,
    however, not all CSS properties treat their values as cyclical.
  </p>
  <p>
    Let's say I want to animate a box from left to right,
    if the translate offset is associated to the <code>--t</code>
    it will increase constantly without stop.
  </p>

<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="nt">translate</span><span class="o">:</span> <span class="nt">calc</span><span class="o">(</span><span class="nt">var</span><span class="o">(</span><span class="nt">--t</span><span class="o">)</span> <span class="o">*</span> <span class="o">.</span><span class="err">001</span><span class="nt">px</span><span class="o">);</span></code></pre></figure>

  <h3>min()</h3>
  <p>
    One expected result is that when the offset reaches at a specific value, it stops immediately.
    This is how <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/min">min()</a> function can be useful.
  </p>
<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="nt">translate</span><span class="o">:</span> <span class="nt">min</span><span class="o">(</span><span class="err">270</span><span class="nt">px</span><span class="o">,</span> <span class="nt">calc</span><span class="o">(</span><span class="nt">var</span><span class="o">(</span><span class="nt">--t</span><span class="o">)</span> <span class="o">*</span> <span class="o">.</span><span class="err">5</span><span class="nt">px</span><span class="o">));</span></code></pre></figure>

  <div class="example">
    <demo-block>
      <template>
        <div class="demo">
          <div class="rect animate-object"></div>
        </div>
        <button action="toggle">Start / Stop</button>
        <style>
          .demo {
            width: 300px;
            height: 20px;
            border: 1px dashed #aaa;
          }
          .demo .rect {
            width: 30px;
            height: 100%;
            background: #000;
            translate: min(270px, calc(var(--t) * .5px));
          }
        </style>
      </template>
    </demo-block>
  </div>

  <p>
    To precisely control the animation duration we can restrict the value of <code>--t</code> instead.
  </p>
<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="c">/* 270px in 3s */</span>
<span class="nt">translate</span><span class="o">:</span> <span class="nt">calc</span><span class="o">(</span><span class="nt">min</span><span class="o">(</span><span class="err">3000</span><span class="o">,</span> <span class="nt">var</span><span class="o">(</span><span class="nt">--t</span><span class="o">))</span> <span class="o">*</span> <span class="o">(</span><span class="err">270</span><span class="nt">px</span> <span class="o">/</span> <span class="err">3000</span><span class="o">));</span></code></pre></figure>

  <div class="example">
    <demo-block>
      <template>
        <div class="demo">
          <div class="rect animate-object"></div>
        </div>
        <button action="toggle">Start / Stop</button>
        <style>
          .demo {
            width: 300px;
            height: 20px;
            border: 1px dashed #aaa;
          }
          .demo .rect {
            width: 30px;
            height: 100%;
            background: #000;
            translate: calc(min(3000, var(--t)) * (270px / 3000));
          }
        </style>
      </template>
    </demo-block>
  </div>

  <h3>mod()</h3>

  <p>
    After the box has moved to the right, another option is to restart the offset from beginning.
    Now we have <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/mod">mod()</a> function to achieve this.
  </p>

<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="nt">translate</span><span class="o">:</span> <span class="nt">calc</span><span class="o">(</span><span class="nt">mod</span><span class="o">(</span><span class="nt">var</span><span class="o">(</span><span class="nt">--t</span><span class="o">)/</span><span class="err">4</span><span class="o">,</span> <span class="err">270</span><span class="o">)</span> <span class="o">*</span> <span class="err">1</span><span class="nt">px</span><span class="o">);</span></code></pre></figure>

  <div class="example">
    <demo-block>
      <template>
        <div class="demo">
          <div class="rect"></div>
        </div>
        <button action="toggle">Start / Stop</button>
        <style>
          .demo {
            width: 300px;
            height: 20px;
            border: 1px dashed #aaa;
          }
          .rect {
            width: 30px;
            height: 100%;
            background: #000;
            translate: calc(mod(var(--t)/4, 270) * 1px);
          }
        </style>
      </template>
    </demo-block>
  </div>

  <h3>sin()</h3>

  <p>
    Or to make the movement back and forth.
  </p>

<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="nt">translate</span><span class="o">:</span> <span class="nt">calc</span><span class="o">(</span><span class="nt">sin</span><span class="o">(</span><span class="nt">mod</span><span class="o">(</span><span class="nt">var</span><span class="o">(</span><span class="nt">--t</span><span class="o">)/</span><span class="err">135</span><span class="o">,</span> <span class="err">270</span><span class="o">))</span> <span class="o">*</span> <span class="err">135</span><span class="nt">px</span><span class="o">);</span></code></pre></figure>

  <div class="example">
    <demo-block>
      <template>
        <div class="demo">
          <div class="rect"></div>
        </div>
        <button action="toggle">Start / Stop</button>
        <style>
          .demo {
            width: 300px;
            height: 20px;
            border: 1px dashed #aaa;
          }
          .rect {
            width: 30px;
            margin: auto;
            height: 100%;
            background: #000;
            translate: calc(sin(mod(var(--t)/135, 270)) * 135px);
          }
        </style>
      </template>
    </demo-block>
  </div>

  <h2>
    Custom easing function
  </h2>

  <p>
    We can create custom easing functions using Math functions and the <code>--t</code> variable,
    which might not be achievable with <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/easing-function#cubic_b%C3%A9zier_easing_function">cubic-bezier()</a>.
  </p>

  <h3>ease-out-cubic</h3>

  <p>
    The initial step is to bound the value <code>--t</code> between <em>0</em> and <em>1</em>.
  </p>

<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="c">/* from 0 to 1 in 1s */</span>
<span class="nt">--t01</span><span class="o">:</span> <span class="nt">calc</span><span class="o">(</span><span class="nt">min</span><span class="o">(</span><span class="err">1000</span><span class="o">,</span> <span class="nt">var</span><span class="o">(</span><span class="nt">--t</span><span class="o">))</span> <span class="o">/</span> <span class="err">1000</span><span class="o">);</span>

<span class="c">/* 1 - pow(1 - t, 3) */</span>
<span class="nt">--ease-out-cubic</span><span class="o">:</span> <span class="nt">calc</span><span class="o">(</span>
  <span class="err">1</span> <span class="nt">-</span> <span class="nt">pow</span><span class="o">(</span><span class="err">1</span> <span class="nt">-</span> <span class="nt">var</span><span class="o">(</span><span class="nt">--t01</span><span class="o">),</span> <span class="err">3</span><span class="o">)</span>
<span class="o">);</span>

<span class="nt">translate</span><span class="o">:</span> <span class="nt">calc</span><span class="o">(</span><span class="nt">var</span><span class="o">(</span><span class="nt">--ease-out-cubic</span><span class="o">)</span> <span class="o">*</span> <span class="err">270</span><span class="nt">px</span><span class="o">);</span></code></pre></figure>

  <div class="example">
    <demo-block>
      <template>
        <div class="demo">
          <div class="rect"></div>
        </div>
        <button action="toggle">Start / Stop</button>
        <style>
          .demo {
            width: 300px;
            height: 20px;
            border: 1px dashed #aaa;
          }
          .rect {
            width: 30px;
            height: 100%;
            background: #000;

            --t01: calc(min(1000, var(--t)) / 1000);
            --ease-out-cubic: calc(1 - pow(1 - var(--t01), 5));
            translate: calc(var(--ease-out-cubic) * 270px);
          }
        </style>
      </template>
    </demo-block>
  </div>

  <h3>ease-out-elastic</h3>

<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="c">/* from 0 to 1 in 1s */</span>
<span class="nt">--t01</span><span class="o">:</span> <span class="nt">calc</span><span class="o">(</span><span class="nt">min</span><span class="o">(</span><span class="err">1000</span><span class="o">,</span> <span class="nt">var</span><span class="o">(</span><span class="nt">--t</span><span class="o">))</span> <span class="o">/</span> <span class="err">1000</span><span class="o">);</span>

<span class="c">/* pow(2, -10t) * sin((10t - .75) * 2/3 * PI) + 1 */</span>
<span class="nt">--ease-out-elastic</span><span class="o">:</span> <span class="nt">calc</span><span class="o">(</span>
  <span class="nt">pow</span><span class="o">(</span><span class="err">2</span><span class="o">,</span> <span class="nt">-10</span> <span class="o">*</span> <span class="nt">var</span><span class="o">(</span><span class="nt">--t01</span><span class="o">))</span> <span class="o">*</span>
  <span class="nt">sin</span><span class="o">((</span><span class="nt">var</span><span class="o">(</span><span class="nt">--t01</span><span class="o">)</span> <span class="o">*</span> <span class="err">10</span> <span class="nt">-</span> <span class="o">.</span><span class="err">75</span><span class="o">)</span> <span class="o">*</span> <span class="err">2</span><span class="o">/</span><span class="err">3</span> <span class="o">*</span> <span class="nt">PI</span><span class="o">)</span> <span class="o">+</span> <span class="err">1</span>
<span class="o">);</span>

<span class="nt">translate</span><span class="o">:</span> <span class="nt">calc</span><span class="o">(</span><span class="nt">var</span><span class="o">(</span><span class="nt">--ease-out-elastic</span><span class="o">)</span> <span class="o">*</span> <span class="err">270</span><span class="nt">px</span><span class="o">);</span></code></pre></figure>

  <div class="example">
    <demo-block>
      <template>
        <div class="demo">
          <div class="rect"></div>
        </div>
        <button action="toggle">Start / Stop</button>
        <style>
          .demo {
            width: 300px;
            height: 20px;
            border: 1px dashed #aaa;
          }
          .rect {
            width: 30px;
            height: 100%;
            background: #000;

            /* 0 - 1 */
            --t01: calc(min(1000, var(--t)) / 1000);

            --ease-out-elastic: calc(
              pow(2, -10 * var(--t01)) *
              sin((var(--t01) * 10 - .75) * 2/3 * PI) + 1
            );
            translate: calc(var(--ease-out-elastic) * 270px);
          }
        </style>
      </template>
    </demo-block>
  </div>

  <h2>Experiment with CSS Doodle</h2>

  <p>
    As the expressions get complex, <code>var()</code> and <code>calc()</code> tend to make
    the code less readable. So I've added the <code>@t</code> function for representing variable <code>--t</code>.
    The latest version of <a href="https://css-doodle.com">css-doodle</a> also accepts simple Math expressions directly inside arguments.
  </p>

<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="c">/* rotate: calc(mod(var(--t) / 1000, 10) * 5deg); */</span>
<span class="nt">rotate</span><span class="o">:</span> <span class="k">@t</span><span class="p">(/</span><span class="m">1000</span><span class="p">,</span> <span class="err">%</span><span class="m">10</span><span class="p">,</span> <span class="err">*</span><span class="m">5deg</span><span class="p">);</span></code></pre></figure>

  <p>
    Code is short without writing <em>keyframes</em>.
  </p>

<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="k">@grid</span><span class="p">:</span> <span class="m">20</span><span class="n">x1</span> <span class="p">/</span> <span class="m">280</span><span class="n">x</span> <span class="m">60px</span><span class="p">;</span>
<span class="k">@gap</span><span class="p">:</span> <span class="m">1px</span><span class="p">;</span>
<span class="k">@size</span><span class="p">:</span> <span class="m">100%</span> <span class="m">20%</span><span class="p">;</span>
<span class="nt">background</span><span class="o">:</span> <span class="err">#000</span><span class="o">;</span>
<span class="nt">margin</span><span class="o">:</span> <span class="nt">auto</span><span class="o">;</span>
<span class="nt">translate</span><span class="o">:</span> <span class="err">0</span> <span class="nt">calc</span><span class="o">(</span><span class="err">20</span><span class="nt">px</span> <span class="o">*</span> <span class="nt">sin</span><span class="o">(</span><span class="err">4</span><span class="o">*</span><span class="k">@t</span><span class="p">(/</span><span class="m">20</span><span class="p">,</span> <span class="err">+@</span><span class="n">i</span><span class="p">(</span><span class="err">*</span><span class="m">6</span><span class="p">),</span> <span class="err">%</span><span class="m">360deg</span><span class="p">)));</span></code></pre></figure>

  <div class="example">
    <css-doodle class="stopped">
      <template>
        @grid: 20x1 / 280px 60px;
        @gap: 1px;
        @size: 100% 20%;
        background: #000;
        margin: auto;
        translate: 0 calc(20px * sin(4*@t(/20, +@i(*6), %360deg)));
      </template>
    </css-doodle>
    <button action="toggle">Start / Stop</button>
  </div>

  <p>
    And it's quick to experiment new parameters too.
  </p>

<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="nt">translate</span><span class="o">:</span> <span class="err">0</span> <span class="nt">calc</span><span class="o">(</span><span class="err">20</span><span class="nt">px</span> <span class="o">*</span> <span class="nt">sin</span><span class="o">(</span><span class="err">3</span><span class="o">*</span><span class="k">@t</span><span class="p">(/</span><span class="m">50</span><span class="p">,</span> <span class="err">*@</span><span class="n">i</span><span class="p">(</span><span class="err">*</span><span class="m">2</span><span class="p">),</span> <span class="err">%</span><span class="m">360deg</span><span class="p">)));</span></code></pre></figure>

  <div class="example">
    <css-doodle class="stopped">
      <template>
        @grid: 20x1 / 280px 60px;
        @gap: 1px;
        @size: 100% 20%;
        background: #000;
        margin: auto;
        translate: 0 calc(20px * sin(3*@t(/50, *@i(*2), %360deg)));
      </template>
    </css-doodle>
    <button action="toggle">Start / Stop</button>
  </div>

  <h3>Function @T and @TS</h3>
  <p>
    In addition to the <code>@t</code> function,
    the uppercase function <code>@T</code> represents another <em>time ticks</em> from beginning of the day.
    Function <code>@TS</code> is a shorthand for <code>@t(/1000)</code>, which tracks <code>time</code> in second.
  </p>

  <p>
    Here is a clock implemented in css-doodle. (<a href="https://codepen.io/yuanchuan/pen/RwOXrwM">CodePen link</a>)
  </p>

<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="c">/* ... */</span>

<span class="c">/* second */</span>
<span class="nt">rotate</span><span class="o">:</span> <span class="k">@TS</span><span class="p">(</span><span class="err">*</span><span class="m">6</span><span class="p">,</span> <span class="err">%</span><span class="m">360deg</span><span class="p">);</span>

<span class="c">/* minute */</span>
<span class="nt">rotate</span><span class="o">:</span> <span class="k">@TS</span><span class="p">(/</span><span class="m">60</span><span class="p">,</span> <span class="err">*</span><span class="m">6</span><span class="p">,</span> <span class="err">%</span><span class="m">360deg</span><span class="p">);</span>

<span class="c">/* hour */</span>
<span class="nt">rotate</span><span class="o">:</span> <span class="k">@TS</span><span class="p">(/</span><span class="m">60</span><span class="p">,</span> <span class="p">/</span><span class="m">12</span><span class="p">,</span> <span class="err">*</span><span class="m">6</span><span class="p">,</span> <span class="err">%</span><span class="m">360deg</span><span class="p">);</span></code></pre></figure>

  <div class="example">
    <css-doodle><template>
      @grid: 4x1 / 240px +.9;
      :container {
        border-radius: 50%;
        border: 4px solid #000;
        outline: 10px solid;
        outline-offset: 2px;
        background:
          @doodle(
            @grid: 12x1;
            @content: @I(- @i(-1));
            @place: @plot(r: .8; dir: -90; rotate: -90;);
            font-size: 30px;
            font-family: sans-serif;
            font-weight: bold;
          ),
          @doodle(
            @grid: 60x1;
            background: #000;
            @size: @pn(16px, @m4(8px)) @pn(2px, @m4(1px));
            @place: @plot(r: 1;);
          );
      }
      @place: center;
      clip-path: polygon(50% -50%, 100% 65%, 0 65%);
      background: #000;
      @nth(3) {
        @size: 4px 90%;
        background: red;
        rotate: @TS(*6, %360deg);
      }
      @nth(2) {
        @size: 6px  70%;
        rotate: @TS(/60, *6, %360deg);
      }
      @nth(1) {
        @size: 10px 45%;
        rotate: @TS(/60, /12, *6, %360deg);
      }
      @nth(4) {
        @size: 12px;
        border-radius: 50%;
        clip-path: none;
        background: red;
      }
    </template></css-doodle>
  </div>

  <h3>round()</h3>
  <p>
    How can we make a jumping motion for the second hand?
    Of course, the direct approach is to use <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/round">round()</a> function,
    where the third parameter specifies the rounding interval.
    In the context of a clock, each step equals <code>360 / 60 = 6deg</code>.
  </p>
<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="nt">rotate</span><span class="o">:</span> <span class="nt">round</span><span class="o">(</span><span class="nt">down</span><span class="o">,</span> <span class="k">@TS</span><span class="p">(</span><span class="err">*</span><span class="m">6</span><span class="p">,</span> <span class="err">%</span><span class="m">360deg</span><span class="p">),</span> <span class="m">6deg</span><span class="p">);</span></code></pre></figure>

  <div class="example">
     <css-doodle><template>
      @grid: 4x1 / 240px +.9;
      :container {
        border-radius: 50%;
        border: 4px solid #000;
        outline: 10px solid;
        outline-offset: 2px;
        background:
          @doodle(
            @grid: 12x1;
            @content: @I(- @i(-1));
            @place: @plot(r: .8; dir: -90; rotate: -90;);
            font-size: 30px;
            font-family: sans-serif;
            font-weight: bold;
          ),
          @doodle(
            @grid: 60x1;
            background: #000;
            @size: @pn(16px, @m4(8px)) @pn(2px, @m4(1px));
            @place: @plot(r: 1;);
          );
      }
      @place: center;
      clip-path: polygon(50% -50%, 100% 65%, 0 65%);
      background: #000;
      @nth(3) {
        @size: 4px 90%;
        background: red;
        rotate: round(down, @TS(*6, %360deg), 6deg);
      }
      @nth(2) {
        @size: 6px  70%;
        rotate: @TS(/60, *6, %360deg);
      }
      @nth(1) {
        @size: 10px 45%;
        rotate: @TS(/60, /12, *6, %360deg);
      }
      @nth(4) {
        @size: 12px;
        border-radius: 50%;
        clip-path: none;
        background: red;
      }
    </template></css-doodle>
  </div>

  <h2>More examples</h2>
  <p>
    Animate colors and positions together.
    (<a href="https://codepen.io/yuanchuan/full/LYvwGjQ">CodePen link</a>).
  </p>

<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="k">@grid</span><span class="p">:</span> <span class="m">100</span><span class="n">x1</span> <span class="p">/</span> <span class="m">100%</span> <span class="nb">auto</span> <span class="p">(</span><span class="m">4</span><span class="p">/</span><span class="m">3</span><span class="p">)</span> <span class="p">/</span> <span class="m">#10153e</span><span class="p">;</span>
<span class="k">@size</span><span class="p">:</span> <span class="err">@</span><span class="n">rn</span><span class="p">(</span><span class="m">1vmin</span><span class="p">,</span> <span class="m">5vmin</span><span class="p">,</span> <span class="m">10</span><span class="p">);</span>
<span class="nt">margin</span><span class="o">:</span> <span class="nt">auto</span><span class="o">;</span>
<span class="nt">border-radius</span><span class="o">:</span> <span class="err">50</span><span class="o">%;</span>

<span class="nt">background</span><span class="o">:</span> <span class="k">@p</span><span class="p">(</span>
  <span class="n">hsl</span><span class="p">(</span><span class="err">@</span><span class="n">t</span><span class="p">(/</span><span class="m">10</span><span class="p">,</span> <span class="err">+@</span><span class="n">i</span><span class="p">(</span><span class="err">*</span><span class="m">2</span><span class="p">),</span> <span class="err">%</span><span class="m">360</span><span class="p">),</span> <span class="m">90%</span><span class="p">,</span> <span class="m">80%</span><span class="p">)</span>
<span class="p">);</span>
<span class="nt">box-shadow</span><span class="o">:</span> <span class="k">@m5</span><span class="p">(</span>
  <span class="err">@</span><span class="n">r</span><span class="p">(</span><span class="err">±</span><span class="m">23vmin</span><span class="p">)</span> <span class="err">@</span><span class="n">r</span><span class="p">(</span><span class="err">±</span><span class="m">23vmin</span><span class="p">)</span> <span class="err">@</span><span class="n">r</span><span class="p">(</span><span class="m">2vmin</span><span class="p">)</span> <span class="err">@</span><span class="n">r</span><span class="p">(</span><span class="m">-40px</span><span class="p">)</span> <span class="err">@</span><span class="n">p</span>
<span class="p">);</span>
<span class="nt">translate</span><span class="o">:</span> <span class="k">@M2</span><span class="p">(</span>
  <span class="n">calc</span><span class="p">(</span><span class="m">100px</span> <span class="err">*</span> <span class="no">tan</span><span class="p">(</span><span class="m">6</span><span class="err">*</span><span class="n">cos</span><span class="p">(</span><span class="err">@</span><span class="n">t</span><span class="p">(/</span><span class="m">10</span><span class="p">,</span> <span class="err">+@</span><span class="n">i</span><span class="p">(</span><span class="err">*</span><span class="m">10</span><span class="p">),</span> <span class="p">/</span><span class="m">6</span><span class="p">,</span> <span class="err">%</span><span class="m">360deg</span><span class="p">))))</span>
<span class="p">);</span></code></pre></figure>

  <div class="example example-bg">
    <css-doodle>
      <template>
        @grid: 100x1 / 100% auto (4/3) / #10153e;
        @size: @rn(1vmin, 5vmin, 10);
        background: @p(hsl(@t(/10, +@i(*2), %360), 90%, 80%));
        box-shadow: @m5(@r(±23vmin) @r(±23vmin) @r(2vmin) @r(-40px) @p);
        translate: @M2(
          calc(100px * tan(6*cos(@t(/10, +@i(*10), /6, %360deg))))
        );
        margin: auto;
        border-radius: 50%;
      </template>
    </css-doodle>
    <button action="toggle-doodle">Start / Stop</button>
 </div>

  <p>
	  Animate text colors. (<a href="https://codepen.io/yuanchuan/pen/bGyEgdZ">CodePen link</a>).
  </p>

<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="k">@grid</span><span class="p">:</span> <span class="m">32</span> <span class="p">/</span> <span class="m">400px</span> <span class="p">/</span> <span class="m">#000</span> <span class="err">^</span><span class="m">.95</span><span class="p">;</span>
<span class="k">@content</span><span class="p">:</span> <span class="err">@</span><span class="n">pn</span><span class="p">([</span><span class="n">a-z0-9</span><span class="p">.]);</span>

<span class="nt">--x</span><span class="o">:</span> <span class="nt">abs</span><span class="o">(</span><span class="k">@dx</span><span class="p">/</span><span class="err">@</span><span class="n">X</span><span class="p">);</span>
<span class="nt">--y</span><span class="o">:</span> <span class="nt">abs</span><span class="o">(</span><span class="k">@dy</span><span class="p">/</span><span class="err">@</span><span class="n">Y</span><span class="p">);</span>

<span class="nt">line-height</span><span class="o">:</span> <span class="err">0</span><span class="o">;</span>
<span class="nt">font-size</span><span class="o">:</span> <span class="err">12</span><span class="nt">px</span><span class="o">;</span>
<span class="nt">font-family</span><span class="o">:</span> <span class="nt">monospace</span><span class="o">;</span>
<span class="nt">color</span><span class="o">:</span> <span class="nt">hsl</span><span class="o">(</span><span class="err">$</span><span class="o">(</span><span class="err">480</span><span class="nt">x</span><span class="o">),</span> <span class="err">100</span><span class="o">%,</span> <span class="nt">calc</span><span class="o">(</span><span class="nt">tan</span><span class="o">(</span><span class="err">$</span><span class="nt">x</span><span class="o">/</span><span class="nt">tan</span><span class="o">(</span><span class="err">$</span><span class="nt">y</span><span class="o">*</span><span class="err">4</span><span class="o">)*</span><span class="err">50</span> <span class="nt">-</span> <span class="k">@t</span><span class="p">(/</span><span class="m">400</span><span class="p">))</span><span class="err">*</span><span class="m">100%</span><span class="p">));</span></code></pre></figure>

  <div class="example example-bg">
    <css-doodle>
      <template>
@grid: 32 / 400px auto 1 / #000 ^.95;
:doodle {
  max-width: 100%;
}
@content: @pn([a-z0-9.]);
font-family: monospace;
font-size: 12px;
line-height: 0;

--x: abs(@dx/@X);
--y: abs(@dy/@Y);

color: hsl(
  $(480x)
  100%
  calc(tan($x/tan($y*4)*50 - @t(/400))*100%)
);
      </template>
    </css-doodle>
    <button action="toggle-doodle">Start / Stop</button>
 </div>


  <h2>Conclusion</h2>

  <p>
    I'm excited about this approach.
    Although using <em>keyframes</em> seems much straightforward,
    for a demo scene full of math calculations and input variables,
    using <em>time</em> as a variable is more likely to get diverse results.
  </p>


  <p class="footnote">
    2024.05.17 update: Add text color animation demo.
  </p>

</article>

<style>
  @property --t { syntax: "<integer>"; initial-value: 0; inherits: true }
  @property --t8 { syntax: "<integer>"; initial-value: 0; inherits: true }
  @property --t24 { syntax: "<integer>"; initial-value: 0; inherits: true }

  @keyframes tick { from { --t: 0 } to { --t: 86400000 } }
  @keyframes tick8 { from { --t8: 0 } to { --t8: 86400000 } }
  @keyframes tick24 { from { --t24: 0 } to { --t24: 86400000 } }

  :root {
    animation:
      tick 86400000ms steps(86400000) infinite,
      tick8 86400000ms steps(calc(86400000/(1000/8))) infinite,
      tick24 86400000ms steps(calc(86400000/(1000/24))) infinite,
  }
  button {
    margin-top: 1em;
  }

  .example {
    font-size: 1.2em;
  }

  .example + p {
    margin-top: 1.5em;
  }
  .stopped {
    animation: none;
  }
  .warning {
    background: #ffffdf;
    padding: .5em;
    border-radius: 4px;
  }

  .footnote.footnote {
    padding-top: 2em;
    color: #777;
    font-size: .9em;
    border-top: 1px solid var(--footnote-border-color);
    margin-top: 4em;
    line-height: 1.4;
  }

</style>

<script src="https://cdnjs.cloudflare.com/ajax/libs/css-doodle/0.39.1/css-doodle.min.js"></script>
<script src="https://yuanchuan.dev/assets/demo.js"></script>

<script>
  let container = document.querySelector('article');
  container.addEventListener('click', e => {
    if (e.target.tagName.toLowerCase() === 'button') {
      let doodle = e.target.parentNode.querySelector('css-doodle');
      if (doodle && e.target.getAttribute('action') === 'toggle') {
        doodle.classList.toggle('stopped');
      }

      if (doodle && e.target.getAttribute('action') === 'toggle-doodle') {
        if (doodle.hasAttribute('cssd-paused')) {
          doodle.resume();
        } else {
          doodle.pause();
        }
      }
    }
  });
</script>
]]></content>

      

      
        
      

      <summary type="html"><![CDATA[
  
    In my earlier post Time Uniform For CSS Animation,
    I took a note about a way to do CSS animations with time ticks instead of keyframes.
    It was limited applicable because CSS lacked ...]]></summary>
    </entry>
  
    <entry>
      <title type="html">Fun with stroke-dasharray</title>
      <link href="https://yuanchuan.dev/fun-with-stroke-dasharray" rel="alternate" type="text/html" title="Fun with stroke-dasharray" />
      <published>2023-09-27T00:00:00+08:00</published>
      <updated>2023-09-27T00:00:00+08:00</updated>
      <id>https://yuanchuan.dev/fun-with-stroke-dasharray</id>
      <content type="html" xml:base="https://yuanchuan.dev/fun-with-stroke-dasharray"><![CDATA[<article>
  <p>
    The <code>stroke-dasharray</code> attribute in SVG is more flexible compared to the dashed style in CSS.
    While there are numerous articles and examples about its usages,

    including <a href="https://css-tricks.com/svg-line-animation-works/">Line animations</a>,
    <a href="https://www.smashingmagazine.com/2015/07/designing-simple-pie-charts-with-css/">Pie charts</a>,
    <a href="https://blog.logrocket.com/build-svg-circular-progress-component-react-hooks/">Circular progress bars</a>,
    or even <a href="https://twitter.com/anatudor/status/1019867596479500288">Hamburger menu icons</a>,
    there's still something new to explore.
  </p>
  <!--more-->
  <p>
    Unlike CSS, <code>stroke-dasharray</code> accepts multiple values, which will be picked cyclically according to the stroke length.
    Imagine it as an infinite repeated value sequence,
    the odd-numbered positions are <strong>dashes</strong>, and the even-numbered positions are <strong>gaps</strong>.
  </p>

<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="nt">stroke-dasharray</span><span class="o">:</span> <span class="err">1</span> <span class="err">2</span> <span class="err">1</span><span class="o">;</span>

<span class="c">/* which means */</span>
<span class="nt">stroke-dasharray</span><span class="o">:</span> <span class="err">1</span> <span class="err">2</span> <span class="err">1</span> <span class="err">1</span> <span class="err">2</span> <span class="err">1</span> <span class="err">1</span> <span class="err">2</span> <span class="err">1</span> <span class="o">...</span></code></pre></figure>
  <p>
    <css-doodle><style>
      @grid: 1 / 100% 20px;
      @content: @svg(
        line {
          y1, y2: 50%;
          x2: 100%;
          stroke-width: 20;
          stroke: #000;
          pathLength: 81;
          stroke-dasharray: 1 2 1;
        }
      );
    </style></css-doodle>
  </p>

  <h2>pathLength</h2>

  <p>
    The total length of a line stroke or path determines where the dashes end.
    To precisely control the endpoints, you may want to use the
    <a href="https://developer.mozilla.org/en-US/docs/Web/API/SVGGeometryElement/getTotalLength">getTotalLength()</a>
    method to get the exact length with JavaScript.
  </p>

  <p>
    Manual <strong>estimation</strong> of path length is possible
    for relatively simple shapes. Though the actual result may differ between browsers.
  </p>

  <div class="example-inline">
    <css-doodle><style>
      @grid: 1 / 120px;
      @content: @svg(
        viewBox: -5 -5 10 10 p .1;
        circle {
          r: 5;
          stroke: #000;
          fill: none;
          stroke-width: .1;
        }
        text {
          content: r = 5;
          y: .5;
          font-size: 1.2;
          text-anchor: middle;
          font-family: verdana;
        }
      );
    </style></css-doodle>
    <ul>
      <li>Path length ≈ <code>2π * 5</code></li>
    </ul>
  </div>

  <h3>
    You know what?
  </h3>

  <p>
    There's the lesser-known <a href="https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/pathLength">pathLength</a>
    attribute for manually specifying the length value, browsers will handle the calculations automatically.
    Sometimes it's convienient to set an arbitrary value and don't rely on JavaScript at all.
    I first learned it from <a href="https://front-end.social/@AmeliaBR">Amelia Bellamy-Royds</a> in <a href="https://css-tricks.com/svg-line-animation-works/#comment-1262137">this comment</a>.
  </p>

  <div class="example">
    <css-doodle id="animate-circle"><style>
      @grid: 1 / 180px;
      cursor: pointer;
      -webkit-tap-highlight-color: transparent;
      @content: @svg(
        viewBox: -9 -9 18 18;
        circle {
          r: 8;
          fill: none;
          stroke: #000;
          stroke-dasharray: 1 2 1;
          pathLength: .99;
          animate {
            attributeName: pathLength;
            repeatCount: indefinite;
            values: 1;100;1;
            dur: 20s;
          }
        }
        text {
          font-size: 1.2;
          fill: #000;
          text-anchor: middle;
          font-family: verdana;
          tspan {
            content: "pathLength:";
            y: 0;
          }
          tspan#num {
            content: 1;
            x: 0;
            y: 2;
          }
        }
      );
    </style></css-doodle>
<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="nt">svg</span> <span class="p">{</span>
  <span class="py">viewBox</span><span class="p">:</span> <span class="m">-9</span> <span class="m">-9</span> <span class="m">18</span> <span class="m">18</span><span class="p">;</span>
  <span class="err">circle</span> <span class="err">{</span>
    <span class="py">r</span><span class="p">:</span> <span class="m">8</span><span class="p">;</span>
    <span class="py">fill</span><span class="p">:</span> <span class="nb">none</span><span class="p">;</span>
    <span class="py">stroke</span><span class="p">:</span> <span class="m">#000</span><span class="p">;</span>
    <span class="py">stroke-dasharray</span><span class="p">:</span> <span class="m">1</span> <span class="m">2</span> <span class="m">1</span><span class="p">;</span>
    <span class="py">pathLength</span><span class="p">:</span> <span class="m">.99</span><span class="p">;</span>
    <span class="err">animate</span> <span class="err">{</span>
      <span class="py">attributeName</span><span class="p">:</span> <span class="n">pathLength</span><span class="p">;</span>
      <span class="py">repeatCount</span><span class="p">:</span> <span class="n">indefinite</span><span class="p">;</span>
      <span class="py">values</span><span class="p">:</span> <span class="m">1</span><span class="p">;</span><span class="err">100;1;</span>
      <span class="py">dur</span><span class="p">:</span> <span class="m">20s</span><span class="p">;</span>
    <span class="p">}</span>
  <span class="err">}</span>
<span class="err">}</span></code></pre></figure>
  </div>

  <p>
    But still, each browser performs slightly different for <code>pathLength</code> and Firefox does the best.
    I guess Chrome has a bug at calculation, that's why <code>.99</code> was used instead of <code>1</code> in the above example.
  </p>
  <p>
    Nevertheless, it's easy to control the dashes with customized <code>pathLength</code>.
    Setting the <code>pathLength</code> to <code>40</code> in a rect and each side gets <code>10</code>.
  </p>
  <div class="example">
    <css-doodle><style>
      @grid: 1 / 160px;
      @content: @svg(
        viewBox: 0 0 50 50 p .5;
        rect {
          width, height: 50;
          fill: none;
          stroke: #000;
          pathLength: 40;
          stroke-dasharray: 2 8;
          stroke-dashoffset: 1;
        }
        text {
          content: Corner borders;
          x, y: 25;
          font-size: 3;
          text-anchor: middle;
        }
      );
    </style></css-doodle>
<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="nt">svg</span> <span class="p">{</span>
  <span class="py">viewBox</span><span class="p">:</span> <span class="m">0</span> <span class="m">0</span> <span class="m">50</span> <span class="m">50</span> <span class="n">p</span> <span class="m">.5</span><span class="p">;</span>
  <span class="err">rect</span> <span class="err">{</span>
    <span class="err">width,</span> <span class="nl">height</span><span class="p">:</span> <span class="m">50</span><span class="p">;</span>
    <span class="py">fill</span><span class="p">:</span> <span class="nb">none</span><span class="p">;</span>
    <span class="py">stroke</span><span class="p">:</span> <span class="m">#000</span><span class="p">;</span>
    <span class="py">pathLength</span><span class="p">:</span> <span class="m">40</span><span class="p">;</span>
    <span class="py">stroke-dasharray</span><span class="p">:</span> <span class="m">2</span> <span class="m">8</span><span class="p">;</span>
    <span class="py">stroke-dashoffset</span><span class="p">:</span> <span class="m">1</span><span class="p">;</span>
  <span class="p">}</span>
<span class="err">}</span></code></pre></figure>
  </div>

  <h2>Interesting Shapes</h2>

  <p>
    Some interesing shapes can be made by tweaking <code>stroke-dasharray</code> and its related atrributes.
  </p>

  <div class="example">
    <css-doodle><style>
      @grid: 1 / 180px;
      @content: @svg(
        viewBox: 0 0 10 10 p 10;
        rect {
          width, height: 10;
          stroke: #000;
          stroke-dasharray: 2 8;
          stroke-width: 20;
        }
      );
    </style></css-doodle>
<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="nt">svg</span> <span class="p">{</span>
  <span class="py">viewBox</span><span class="p">:</span> <span class="m">0</span> <span class="m">0</span> <span class="m">10</span> <span class="m">10</span> <span class="n">p</span> <span class="m">10</span><span class="p">;</span>
  <span class="err">rect</span> <span class="err">{</span>
    <span class="err">width,</span> <span class="nl">height</span><span class="p">:</span> <span class="m">10</span><span class="p">;</span>
    <span class="py">stroke</span><span class="p">:</span> <span class="m">#000</span><span class="p">;</span>
    <span class="py">stroke-dasharray</span><span class="p">:</span> <span class="m">2</span> <span class="m">8</span><span class="p">;</span>
    <span class="py">stroke-width</span><span class="p">:</span> <span class="m">20</span><span class="p">;</span>
  <span class="p">}</span>
<span class="err">}</span></code></pre></figure>
  </div>

  <p>
    Abstract forms.
  </p>

  <div class="example">
    <css-doodle><style>
      @grid: 1 / 180px;
      @content: @svg(
        viewBox: 0 0 10 10 p 2.5;
        rect {
          width, height: 10;
          stroke: #000;
          stroke-dasharray: .079 9.9;
          stroke-width: 10;
        }
      );
    </style></css-doodle>
<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="nt">svg</span> <span class="p">{</span>
  <span class="py">viewBox</span><span class="p">:</span> <span class="m">0</span> <span class="m">0</span> <span class="m">10</span> <span class="m">10</span> <span class="n">p</span> <span class="m">5</span><span class="p">;</span>
  <span class="err">rect</span> <span class="err">{</span>
    <span class="err">width,</span> <span class="nl">height</span><span class="p">:</span> <span class="m">10</span><span class="p">;</span>
    <span class="py">stroke</span><span class="p">:</span> <span class="m">#000</span><span class="p">;</span>
    <span class="py">stroke-dasharray</span><span class="p">:</span> <span class="m">.079</span> <span class="m">9.9</span><span class="p">;</span>
    <span class="py">stroke-width</span><span class="p">:</span> <span class="m">10</span><span class="p">;</span>
  <span class="p">}</span>
<span class="err">}</span></code></pre></figure>
  </div>

  <div class="example">
    <css-doodle><style>
      @grid: 1 / 180px;
      @content: @svg(
        viewBox: 0 0 10 10 p 10;
        rect {
          width, height: 10;
          stroke: #000;
          stroke-dasharray: .1 19 .1 .5 .31 .4;
          stroke-width: 20;
        }
      );
    </style></css-doodle>
<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="nt">svg</span> <span class="p">{</span>
  <span class="py">viewBox</span><span class="p">:</span> <span class="m">0</span> <span class="m">0</span> <span class="m">10</span> <span class="m">10</span> <span class="n">p</span> <span class="m">10</span><span class="p">;</span>
  <span class="err">rect</span> <span class="err">{</span>
    <span class="err">width,</span> <span class="nl">height</span><span class="p">:</span> <span class="m">10</span><span class="p">;</span>
    <span class="py">stroke</span><span class="p">:</span> <span class="m">#000</span><span class="p">;</span>
    <span class="py">stroke-dasharray</span><span class="p">:</span> <span class="m">.1</span> <span class="m">19</span> <span class="m">.1</span> <span class="m">.5</span> <span class="m">.31</span> <span class="m">.4</span><span class="p">;</span>
    <span class="py">stroke-width</span><span class="p">:</span> <span class="m">20</span><span class="p">;</span>
  <span class="p">}</span>
<span class="err">}</span></code></pre></figure>
  </div>


  <p>Hearts at the corners.</p>

  <div class="example">
    <css-doodle><style>
      @grid: 1 / 180px auto;
      @content: @svg(
        viewBox: 0 0 100 100 p 20;
        rect {
          fill: none;
          stroke-dasharray: 40 60;
          stroke-linecap: round;
          stroke-dashoffset: 20;
          stroke: #000;
          stroke-width: 40;
          width, height: 100;
        }
      );
    </style></css-doodle>
<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="nt">svg</span> <span class="p">{</span>
  <span class="py">viewBox</span><span class="p">:</span> <span class="m">0</span> <span class="m">0</span> <span class="m">10</span> <span class="m">10</span> <span class="n">p</span> <span class="m">2</span><span class="p">;</span>
  <span class="err">rect</span> <span class="err">{</span>
    <span class="err">width,</span> <span class="nl">height</span><span class="p">:</span> <span class="m">10</span><span class="p">;</span>
    <span class="py">fill</span><span class="p">:</span> <span class="nb">none</span><span class="p">;</span>
    <span class="py">stroke</span><span class="p">:</span> <span class="m">#000</span><span class="p">;</span>
    <span class="py">stroke-linecap</span><span class="p">:</span> <span class="n">round</span><span class="p">;</span>
    <span class="py">stroke-dasharray</span><span class="p">:</span> <span class="m">4</span> <span class="m">6</span><span class="p">;</span>
    <span class="py">stroke-dashoffset</span><span class="p">:</span> <span class="m">2</span><span class="p">;</span>
    <span class="py">stroke-width</span><span class="p">:</span> <span class="m">4</span><span class="p">;</span>
  <span class="p">}</span>
<span class="err">}</span></code></pre></figure>
  </div>

  <p>
    If you're using Safari, you might have noticed the top left corner has a rounded appearance.
    This could be a rendering bug or perhaps a feature.
  </p>

  <h3>Zero-sized dashes</h3>

  <p>
    When the <code>dash</code> values are set to <code>0</code>
    and the <code>stroke-linecap</code> attribute is set to either <code>square</code> or <code>round</code>,
    the caps will remain visible as if the dashes are still there.
  </p>
  <p>
    This is an unique feature which offers some creative usages.
  </p>

  <div class="example">
    <css-doodle><style>
      @grid: 1 / 180px;
      @content: @svg(
        viewBox: 0 0 100 100 p 24.5;
        rect {
          width, height: 100;
          fill: none;
          stroke: #000;
          stroke-linecap: square;
          stroke-dasharray: 0 50 50 0;
          stroke-width: 49;
        }
      );
    </style></css-doodle>
<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="nt">svg</span> <span class="p">{</span>
  <span class="py">viewBox</span><span class="p">:</span> <span class="m">0</span> <span class="m">0</span> <span class="m">100</span> <span class="m">100</span> <span class="n">p</span> <span class="m">24.5</span><span class="p">;</span>
  <span class="err">rect</span> <span class="err">{</span>
    <span class="err">width,</span> <span class="nl">height</span><span class="p">:</span> <span class="m">100</span><span class="p">;</span>
    <span class="py">fill</span><span class="p">:</span> <span class="nb">none</span><span class="p">;</span>
    <span class="py">stroke</span><span class="p">:</span> <span class="m">#000</span><span class="p">;</span>
    <span class="py">stroke-linecap</span><span class="p">:</span> <span class="nb">square</span><span class="p">;</span>
    <span class="py">stroke-dasharray</span><span class="p">:</span> <span class="m">0</span> <span class="m">50</span> <span class="m">50</span> <span class="m">0</span><span class="p">;</span>
    <span class="py">stroke-width</span><span class="p">:</span> <span class="m">49</span><span class="p">;</span>
  <span class="p">}</span>
<span class="err">}</span></code></pre></figure>
  </div>

  <p>Two half <code>round</code> caps will form a full circle.</p>

  <div class="example">
    <css-doodle><style>
      @grid: 1 / 180px;
      @content: @svg(
        viewBox: 0 0 100 100 p 5;
        rect {
          width, height: 100;
          fill: none;
          stroke: #000;
          stroke-dasharray: 0 10 10;
          stroke-linecap: round;
          stroke-width: 10;
        }
      );
    </style></css-doodle>
<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="nt">svg</span> <span class="p">{</span>
  <span class="py">viewBox</span><span class="p">:</span> <span class="m">0</span> <span class="m">0</span> <span class="m">100</span> <span class="m">100</span> <span class="n">p</span> <span class="m">10</span><span class="p">;</span>
  <span class="err">rect</span> <span class="err">{</span>
    <span class="err">width,</span> <span class="nl">height</span><span class="p">:</span> <span class="m">100</span><span class="p">;</span>
    <span class="py">fill</span><span class="p">:</span> <span class="nb">none</span><span class="p">;</span>
    <span class="py">stroke</span><span class="p">:</span> <span class="m">#000</span><span class="p">;</span>
    <span class="py">stroke-dasharray</span><span class="p">:</span> <span class="m">0</span> <span class="m">10</span> <span class="m">10</span><span class="p">;</span>
    <span class="py">stroke-linecap</span><span class="p">:</span> <span class="n">round</span><span class="p">;</span>
    <span class="py">stroke-width</span><span class="p">:</span> <span class="m">10</span><span class="p">;</span>
  <span class="p">}</span>
<span class="err">}</span></code></pre></figure>
  </div>

  <div class="example">
    <css-doodle><style>
      @grid: 1 / 180px;
      @content: @svg(
        viewBox: -50 -50 100 100 p 8;
        circle {
          r: 50;
          fill: none;
          stroke: #000;
          pathLength: 96;
          stroke-linecap: round;
          stroke-dasharray: 0 6 0 6 0;
          stroke-width: 16;
        }
      );
    </style></css-doodle>
<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="nt">svg</span> <span class="p">{</span>
  <span class="py">viewBox</span><span class="p">:</span> <span class="m">-50</span> <span class="m">-50</span> <span class="m">100</span> <span class="m">100</span> <span class="n">p</span> <span class="m">8</span><span class="p">;</span>
  <span class="err">circle</span> <span class="err">{</span>
    <span class="py">r</span><span class="p">:</span> <span class="m">50</span><span class="p">;</span>
    <span class="py">fill</span><span class="p">:</span> <span class="nb">none</span><span class="p">;</span>
    <span class="py">stroke</span><span class="p">:</span> <span class="m">#000</span><span class="p">;</span>
    <span class="py">pathLength</span><span class="p">:</span> <span class="m">96</span><span class="p">;</span>
    <span class="py">stroke-linecap</span><span class="p">:</span> <span class="n">round</span><span class="p">;</span>
    <span class="py">stroke-dasharray</span><span class="p">:</span> <span class="m">0</span> <span class="m">6</span> <span class="m">0</span> <span class="m">6</span> <span class="m">0</span><span class="p">;</span>
    <span class="py">stroke-width</span><span class="p">:</span> <span class="m">16</span><span class="p">;</span>
  <span class="p">}</span>
<span class="err">}</span></code></pre></figure>
  </div>

  <div class="example">
    <css-doodle><style>
      @grid: 1 / 180px;
      @content: @svg(
        viewBox: -50 -50 100 100 p 7;
        fill: none;
        circle {
          r: 50;
          stroke: #000;
          pathLength: 96;
          stroke-dasharray: 0 6 0 6 0 6 0 0 6 0 6 0 6 0 6;
          stroke-linecap: round;
          stroke-width: 14;
        }
      );
    </style></css-doodle>
<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="nt">svg</span> <span class="p">{</span>
  <span class="py">viewBox</span><span class="p">:</span> <span class="m">-50</span> <span class="m">-50</span> <span class="m">100</span> <span class="m">100</span> <span class="n">p</span> <span class="m">7</span><span class="p">;</span>
  <span class="err">circle</span> <span class="err">{</span>
    <span class="py">r</span><span class="p">:</span> <span class="m">50</span><span class="p">;</span>
    <span class="py">fill</span><span class="p">:</span> <span class="nb">none</span><span class="p">;</span>
    <span class="py">stroke</span><span class="p">:</span> <span class="m">#000</span><span class="p">;</span>
    <span class="py">pathLength</span><span class="p">:</span> <span class="m">96</span><span class="p">;</span>
    <span class="py">stroke-linecap</span><span class="p">:</span> <span class="n">round</span><span class="p">;</span>
    <span class="py">stroke-width</span><span class="p">:</span> <span class="m">14</span><span class="p">;</span>
    <span class="py">stroke-dasharray</span><span class="p">:</span>
      <span class="m">0</span> <span class="m">6</span> <span class="m">0</span> <span class="m">6</span> <span class="m">0</span> <span class="m">6</span> <span class="m">0</span> <span class="m">0</span>
      <span class="m">6</span> <span class="m">0</span> <span class="m">6</span> <span class="m">0</span> <span class="m">6</span> <span class="m">0</span> <span class="m">6</span><span class="p">;</span>
  <span class="p">}</span>
<span class="err">}</span></code></pre></figure>
  </div>

  <h2>Generating dots</h2>

  <p>
    With sufficiently long path we can generate lots of dots using
    <code>stroke-dasharray</code> and the round caps.
  </p>

  <div class="example">
    <css-doodle><style>
    @grid: 1 / 240px;
    @content: @svg(
      viewBox: -40 -40 80 80;
      style border: 5px solid #000;
      polyline {
        stroke: #000;
        fill: none;
        stroke-linecap: round;
        stroke-dasharray: 0 3;
        points: @m400.@Plot(
          turn: 40; r: t/3;
        );
      }
    );
    </style></css-doodle>
<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="nt">svg</span> <span class="p">{</span>
  <span class="py">viewBox</span><span class="p">:</span> <span class="m">-40</span> <span class="m">-40</span> <span class="m">80</span> <span class="m">80</span><span class="p">;</span>
  <span class="err">style</span> <span class="nl">border</span><span class="p">:</span> <span class="m">5px</span> <span class="nb">solid</span> <span class="m">#000</span><span class="p">;</span>
  <span class="err">polyline</span> <span class="err">{</span>
    <span class="py">stroke</span><span class="p">:</span> <span class="m">#000</span><span class="p">;</span>
    <span class="py">fill</span><span class="p">:</span> <span class="nb">none</span><span class="p">;</span>
    <span class="py">stroke-linecap</span><span class="p">:</span> <span class="n">round</span><span class="p">;</span>
    <span class="py">stroke-dasharray</span><span class="p">:</span> <span class="m">0</span> <span class="m">3</span><span class="p">;</span>
    <span class="py">points</span><span class="p">:</span> <span class="err">@</span><span class="n">m400</span><span class="p">.</span><span class="err">@</span><span class="n">Plot</span><span class="p">(</span>
      <span class="n">turn</span><span class="p">:</span> <span class="m">40</span><span class="p">;</span> <span class="py">r</span><span class="p">:</span> <span class="n">t</span><span class="p">/</span><span class="m">3</span><span class="p">;</span>
    <span class="err">);</span>
  <span class="p">}</span>
<span class="err">}</span></code></pre></figure>
  </div>

  <p>
    The dots are just caps with dashes length in <code>0</code>.
    They're positioned where the path leads,
    so adjust the path the dots will follow.
  </p>

  <div class="example">
    <css-doodle><style>
    @grid: 1 / 240px;
    @content: @svg(
      viewBox: -40 -40 80 80;
      style border: 5px solid #000;
      polyline {
        stroke: #000;
        fill: none;
        stroke-linecap: round;
        stroke-dasharray: 0 3;
        points: @m199.@Plot(
          turn: 40; r: t/3;
        );
      }
    );
    </style></css-doodle>
<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="c">/* ... */</span>

<span class="nt">points</span><span class="o">:</span> <span class="k">@m199</span><span class="p">.</span><span class="err">@</span><span class="n">Plot</span><span class="p">(</span>
  <span class="n">turn</span><span class="p">:</span> <span class="m">40</span><span class="p">;</span> <span class="nt">r</span><span class="o">:</span> <span class="nt">t</span><span class="o">/</span><span class="err">3</span><span class="o">;</span>
<span class="o">);</span></code></pre></figure>
  </div>

  <p>
    At this point, I genuinely wish
    SVG gradients could follow along the SVG path so that I can apply different colors to the dots.
    Currently using <code>mask</code> is the only option to get a similar result.
  </p>

  <div class="example">
    <css-doodle seed="1695480071074"><style>
      @grid: 1 / 240px;
      @content: @svg(
        viewBox: -40 -40 80 80;
        style background: #04195a;
        style border: 6px solid #04195a;
        g {
          circle*200 {
            r, cx, cy: @r20 @r(±40) @r(±40);
            fill: @p(crimson, yellow, deepskyblue);
          }
          mask: defs mask {
            polyline {
              stroke: #fff;
              fill: none;
              stroke-linecap: round;
              stroke-dasharray: 0 3;
              points: @m200.Plot(turn: -40; r: t/3)
            }
          }
        }
      );
    </style></css-doodle>
<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="nt">svg</span> <span class="p">{</span>
  <span class="py">viewBox</span><span class="p">:</span> <span class="m">-40</span> <span class="m">-40</span> <span class="m">80</span> <span class="m">80</span><span class="p">;</span>
  <span class="err">style</span> <span class="nl">background</span><span class="p">:</span> <span class="m">#04195a</span><span class="p">;</span>
  <span class="err">style</span> <span class="nl">border</span><span class="p">:</span> <span class="m">6px</span> <span class="nb">solid</span> <span class="m">#04195a</span><span class="p">;</span>
  <span class="err">g</span> <span class="err">{</span>
    <span class="err">circle*200</span> <span class="err">{</span>
      <span class="err">r,</span> <span class="err">cx,</span> <span class="py">cy</span><span class="p">:</span> <span class="err">@</span><span class="n">r20</span> <span class="err">@</span><span class="n">r</span><span class="p">(</span><span class="err">±</span><span class="m">40</span><span class="p">)</span> <span class="err">@</span><span class="n">r</span><span class="p">(</span><span class="err">±</span><span class="m">40</span><span class="p">);</span>
      <span class="py">fill</span><span class="p">:</span> <span class="err">@</span><span class="n">p</span><span class="p">(</span><span class="no">crimson</span><span class="p">,</span> <span class="no">yellow</span><span class="p">,</span> <span class="no">deepskyblue</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="nt">mask</span><span class="o">:</span> <span class="nt">defs</span> <span class="nt">mask</span> <span class="p">{</span>
      <span class="err">polyline</span> <span class="err">{</span>
        <span class="py">fill</span><span class="p">:</span> <span class="nb">none</span><span class="p">;</span>
        <span class="py">stroke</span><span class="p">:</span> <span class="m">#fff</span><span class="p">;</span>
        <span class="py">stroke-linecap</span><span class="p">:</span> <span class="n">round</span><span class="p">;</span>
        <span class="py">stroke-dasharray</span><span class="p">:</span> <span class="m">0</span> <span class="m">3</span><span class="p">;</span>
        <span class="py">points</span><span class="p">:</span> <span class="err">@</span><span class="n">m200</span><span class="p">.</span><span class="n">Plot</span><span class="p">(</span><span class="n">turn</span><span class="p">:</span> <span class="m">-40</span><span class="p">;</span> <span class="py">r</span><span class="p">:</span> <span class="n">t</span><span class="p">/</span><span class="m">3</span><span class="p">)</span>
      <span class="p">}</span>
    <span class="err">}</span>
  <span class="err">}</span>
<span class="err">}</span></code></pre></figure>
  </div>

  <h2>Curvy strokes</h2>

  <p>
    After generating different paths using Math equations, adding dashes with <code>stroke-dasharray</code>
    will result in some peculiar shapes.
  </p>

  <div class="example">
    <css-doodle><style>
      @grid: 1 / 200px;
      @content: @svg(
        viewBox: -25 -25 50 50 p 1;
        polyline {
          stroke: #000;
          fill: none;
          stroke-linecap: round;
          stroke-dasharray: 0 10;
          stroke-width: 1.5;
          points: @m200.Plot(
            turn: .37;
            x: 25 * cos(180t);
            y: 25 * sin(180t);
          );
        }
      );
    </style></css-doodle>
<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="nt">svg</span> <span class="p">{</span>
  <span class="py">viewBox</span><span class="p">:</span> <span class="m">-25</span> <span class="m">-25</span> <span class="m">50</span> <span class="m">50</span> <span class="n">p</span> <span class="m">1</span><span class="p">;</span>
  <span class="err">polyline</span> <span class="err">{</span>
    <span class="py">stroke</span><span class="p">:</span> <span class="m">#000</span><span class="p">;</span>
    <span class="py">fill</span><span class="p">:</span> <span class="nb">none</span><span class="p">;</span>
    <span class="py">stroke-linecap</span><span class="p">:</span> <span class="n">round</span><span class="p">;</span>
    <span class="py">stroke-dasharray</span><span class="p">:</span> <span class="m">0</span> <span class="m">5</span><span class="p">;</span>
    <span class="py">stroke-width</span><span class="p">:</span> <span class="m">1.5</span><span class="p">;</span>
    <span class="py">points</span><span class="p">:</span> <span class="err">@</span><span class="n">m200</span><span class="p">.</span><span class="n">Plot</span><span class="p">(</span>
      <span class="n">turn</span><span class="p">:</span> <span class="m">.37</span><span class="p">;</span>
      <span class="py">x</span><span class="p">:</span> <span class="m">25</span> <span class="err">*</span> <span class="n">cos</span><span class="p">(</span><span class="m">180</span><span class="n">t</span><span class="p">);</span>
      <span class="py">y</span><span class="p">:</span> <span class="m">25</span> <span class="err">*</span> <span class="n">sin</span><span class="p">(</span><span class="m">180</span><span class="n">t</span><span class="p">);</span>
    <span class="err">);</span>
  <span class="p">}</span>
<span class="err">}</span></code></pre></figure>
  </div>
  <p>
    I'm not sure if they can be anyting practical.
    It's just intriguing to see.
  </p>
  <div class="example">
    <css-doodle><style>
      @grid: 1 / 200px;
      @content: @svg(
        viewBox: -25 -25 50 50 p 1;
        polyline {
          stroke: #000;
          fill: none;
          stroke-linecap: round;
          stroke-dasharray: 0 10;
          stroke-width: 1.5;
          points: @m399.Plot(
            turn: .37;
            x: 25 * cos(180t);
            y: 25 * sin(180t);
          );
        }
      );
    </style></css-doodle>
<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="c">/* ... */</span>

<span class="nt">stroke-dasharray</span><span class="o">:</span> <span class="err">0</span> <span class="err">10</span><span class="o">;</span>
<span class="nt">points</span><span class="o">:</span> <span class="k">@m399</span><span class="p">.</span><span class="n">Plot</span><span class="p">(</span>
  <span class="n">turn</span><span class="p">:</span> <span class="m">.37</span><span class="p">;</span>
  <span class="nt">x</span><span class="o">:</span> <span class="err">25</span> <span class="o">*</span> <span class="nt">cos</span><span class="o">(</span><span class="err">180</span><span class="nt">t</span><span class="o">);</span>
  <span class="nt">y</span><span class="o">:</span> <span class="err">25</span> <span class="o">*</span> <span class="nt">sin</span><span class="o">(</span><span class="err">180</span><span class="nt">t</span><span class="o">);</span>
<span class="o">);</span></code></pre></figure>
  </div>

  <h2>Ornaments</h2>
  <p>
    Despite its interesting aspects, I find <code>stroke-dasharray</code> to be very useful in some specific cases.
  </p>
  <p>
    Here's a symmetrical shape resembling a window lattice, yet the central part seems a little dense.
  </p>

  <div class="example">
    <css-doodle><style>
      @grid: 1 / 240px;
      @content: @svg(
        viewBox: -50 -50 100 100;
        style border: 8px solid #000;
        fill: none;
        stroke: #000;
        xstroke-dasharray: 0 3 5 2 100;
        xstroke-linecap: round;
        stroke-width: 1.4;
        g*12 {
          transform: rotate($(360/@N*@n));
          polyline {
            points: @M12.Plot(r: t^4);
          }
          polyline {
            points: @M12.Plot(r: t^4; turn: -1);
          }
        }
      );
    </style></css-doodle>
<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="nt">svg</span> <span class="p">{</span>
  <span class="py">viewBox</span><span class="p">:</span> <span class="m">-50</span> <span class="m">-50</span> <span class="m">100</span> <span class="m">100</span><span class="p">;</span>
  <span class="err">style</span> <span class="nl">border</span><span class="p">:</span> <span class="m">8px</span> <span class="nb">solid</span> <span class="m">#000</span><span class="p">;</span>
  <span class="py">fill</span><span class="p">:</span> <span class="nb">none</span><span class="p">;</span>
  <span class="py">stroke</span><span class="p">:</span> <span class="m">#000</span><span class="p">;</span>
  <span class="py">stroke-width</span><span class="p">:</span> <span class="m">1.4</span><span class="p">;</span>
  <span class="err">g*12</span> <span class="err">{</span>
    <span class="nl">transform</span><span class="p">:</span> <span class="n">rotate</span><span class="p">(</span><span class="err">$</span><span class="p">(</span><span class="m">360</span><span class="p">/</span><span class="err">@</span><span class="n">N</span><span class="err">*@</span><span class="n">n</span><span class="p">));</span>
    <span class="err">polyline</span> <span class="err">{</span>
      <span class="py">points</span><span class="p">:</span> <span class="err">@</span><span class="n">M12</span><span class="p">.</span><span class="n">Plot</span><span class="p">(</span><span class="n">r</span><span class="p">:</span> <span class="n">t</span><span class="err">^</span><span class="m">4</span><span class="p">)</span>
    <span class="p">}</span>
    <span class="nt">polyline</span> <span class="p">{</span>
      <span class="py">points</span><span class="p">:</span> <span class="err">@</span><span class="n">M12</span><span class="p">.</span><span class="n">Plot</span><span class="p">(</span><span class="n">r</span><span class="p">:</span> <span class="n">t</span><span class="err">^</span><span class="m">4</span><span class="p">;</span> <span class="py">turn</span><span class="p">:</span> <span class="m">-1</span><span class="p">)</span>
    <span class="p">}</span>
  <span class="err">}</span>
<span class="err">}</span></code></pre></figure>
  </div>

  <p>
    How can it be improved?
    One option is to position a circle at the center.
    Another approach is to modify the lines with <code>stroke-dasharray</code>.
  </p>

  <div class="example">
    <css-doodle><style>
      @grid: 1 / 240px;
      @content: @svg(
        viewBox: -50 -50 100 100;
        style border: 8px solid #000;
        fill: none;
        stroke: #000;
        stroke-dasharray: 1.5 8.5 100;
        stroke-linecap: round;
        stroke-width: 1.4;
        g*12 {
          transform: rotate($(360/@N*@n));
          polyline {
            points: @M12.Plot(r: t^4);
          }
          polyline {
            points: @M12.Plot(r: t^4; turn: -1);
          }
        }
      );
    </style></css-doodle>
<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="c">/* ... */</span>

<span class="nt">stroke-linecap</span><span class="o">:</span> <span class="nt">round</span><span class="o">;</span>
<span class="nt">stroke-dasharray</span><span class="o">:</span> <span class="err">1</span><span class="o">.</span><span class="err">5</span> <span class="err">8</span><span class="o">.</span><span class="err">5</span> <span class="err">100</span><span class="o">;</span></code></pre></figure>
  </div>

  <p>
    There are many options. For example, the one below looks much like a flower in its middle.
  </p>

  <div class="example">
    <css-doodle><style>
      @grid: 1 / 240px;
      @content: @svg(
        viewBox: -50 -50 100 100;
        style border: 8px solid #000;
        fill: none;
        stroke: #000;
        stroke-dasharray: 0 3 5 2 100;
        stroke-linecap: round;
        stroke-width: 1.4;
        g*12 {
          transform: rotate($(360/@N*@n));
          polyline {
            points: @M12.Plot(r: t^4);
          }
          polyline {
            points: @M12.Plot(r: t^4; turn: -1);
          }
        }
      );
    </style></css-doodle>
<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="c">/* ... */</span>

<span class="nt">stroke-linecap</span><span class="o">:</span> <span class="nt">round</span><span class="o">;</span>
<span class="nt">stroke-dasharray</span><span class="o">:</span> <span class="err">0</span> <span class="err">3</span> <span class="err">5</span> <span class="err">2</span> <span class="err">100</span><span class="o">;</span></code></pre></figure>
  </div>

  <p>
    Using <code>stroke-dasharray</code> not only prevent lines from overlapping
    but also tend to change the feels based on different patterns.
  </p>

  <div class="example">
    <css-doodle><style>
      @grid: 1 / 240px;
      @content: @svg(
        viewBox: 0 0 100 100;
        fill: none;
        rect {
          fill: #0ce5f2;
          width, height: 100;
        }
        path*30 {
          stroke: @pn(#ff4ea5, yellow, @m4(#fff));
          stroke-linecap: round;
          stroke-dasharray: 0 8 5 6 3 12;
          d: M 37 40 Q @Plot(r: 80) @Plot(r: 150);
        }
      );
    </style></css-doodle>
<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="nt">svg</span> <span class="p">{</span>
  <span class="py">viewBox</span><span class="p">:</span> <span class="m">0</span> <span class="m">0</span> <span class="m">100</span> <span class="m">100</span><span class="p">;</span>
  <span class="py">fill</span><span class="p">:</span> <span class="nb">none</span><span class="p">;</span>
  <span class="err">style</span> <span class="nl">background</span><span class="p">:</span> <span class="m">#0ce5f2</span><span class="p">;</span>
  <span class="err">path*30</span> <span class="err">{</span>
    <span class="py">stroke-linecap</span><span class="p">:</span> <span class="n">round</span><span class="p">;</span>
    <span class="py">stroke-dasharray</span><span class="p">:</span> <span class="m">0</span> <span class="m">8</span> <span class="m">5</span> <span class="m">6</span> <span class="m">3</span> <span class="m">12</span><span class="p">;</span>
    <span class="py">stroke</span><span class="p">:</span> <span class="err">@</span><span class="n">pn</span><span class="p">(</span>
      <span class="m">#ff4ea5</span><span class="p">,</span> <span class="no">yellow</span><span class="p">,</span> <span class="err">@</span><span class="n">m4</span><span class="p">(</span><span class="m">#fff</span><span class="p">)</span>
    <span class="p">);</span>
    <span class="py">d</span><span class="p">:</span> <span class="n">M</span> <span class="m">37</span> <span class="m">40</span>
       <span class="n">Q</span> <span class="err">@</span><span class="n">Plot</span><span class="p">(</span><span class="n">r</span><span class="p">:</span> <span class="m">80</span><span class="p">)</span> <span class="err">@</span><span class="n">Plot</span><span class="p">(</span><span class="n">r</span><span class="p">:</span> <span class="m">150</span><span class="p">);</span>
  <span class="p">}</span>
<span class="err">}</span></code></pre></figure>
  </div>



  <h2>Text strokes</h2>
  <p>
    It's also easy to apply a dashed stroke to the texts in SVG.
  </p>
  <p>
    However, Safari again has a strange behavior for rendering <code>stroke-dasharray</code>.
    Maybe that's caused by the length calculation.
    The <code>pathLength</code> attribute is not available for text strokes so
    I can't get a consistant result in every browser.
  </p>

  <p>
    For the same <code>stroke-dasharray</code> value,
    dashes in Safari are about 5 times more compact than Chrome and Firefox.
    And when the size of SVG changes its dashes will turn into another shape.
  </p>

  <div class="compare">
    <figure>
      <img src="https://yuanchuan.dev/assets/images/post/stroke-dasharray/safari.png" alt="letter abcd in Safari" loading="lazy" />
      <figcaption>Safari</figcaption>
    </figure>
    <figure>
      <img src="https://yuanchuan.dev/assets/images/post/stroke-dasharray/chrome.png" alt="letter abcd in Chrome" loading="lazy" />
      <figcaption>Chrome, Firefox</figcaption>
    </figure>
  </div>

  <!--
  <css-doodle>
    @grid: 1 / 400px 100px;
    @content: @svg(
      viewBox: 0 0 400 100;
      text {
        font-size: 100;
        text-anchor: middle;
        fill: none;
        stroke-linejoin: round;
        stroke-linecap: round;
        font-family: sans-serif;
        tspan {
          content: @p("A B C D");
          stroke: #000;
          stroke-width: 20;
          x, y: 200 85;
        }
        tspan {
          content: @p;
          x, y: 200 85;
          stroke-dasharray: 20;
          stroke-width: 2;
          stroke: #fff;
        }
      }
    );
  </css-doodle>
  -->


  <h3>Design with dashes</h3>

  <p>
    If the character is complex enough, like 䲜 or 靐, it would contain more dashes,
    which is something that can be utilized for design.
  </p>

  <div class="example">
    <css-doodle><style>
      @grid: 1 / 240px;
      @content: @raw(
        <img width="100%" src="https://yuanchuan.dev/assets/images/post/stroke-dasharray/text-1.png" alt="" loading="lazy" />
      );
      /*
      @content: @svg(
        viewBox: 0 0 100 100 p -15;
        style border: 8px solid #000;
        text {
          content: "龘";
          font-size: 120;
          x, y: 50, 90;
          font-family: sans-serif;
          text-anchor: middle;
          fill: none;
          stroke: #000;
          stroke-linejoin: round;
          stroke-linecap: round;
          stroke-dasharray: 0 6 20;
          stroke-width: 2;
        }
      );
      */
    </style></css-doodle>

<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="nt">svg</span> <span class="p">{</span>
  <span class="py">viewBox</span><span class="p">:</span> <span class="m">0</span> <span class="m">0</span> <span class="m">100</span> <span class="m">100</span> <span class="n">p</span> <span class="m">-15</span><span class="p">;</span>
  <span class="err">style</span> <span class="nl">border</span><span class="p">:</span> <span class="m">8px</span> <span class="nb">solid</span> <span class="m">#000</span><span class="p">;</span>
  <span class="err">text</span> <span class="err">{</span>
    <span class="nl">content</span><span class="p">:</span> <span class="s1">"龘"</span><span class="p">;</span>
    <span class="nl">font-size</span><span class="p">:</span> <span class="m">120</span><span class="p">;</span>
    <span class="err">x,</span> <span class="py">y</span><span class="p">:</span> <span class="m">50</span><span class="p">,</span> <span class="m">90</span><span class="p">;</span>
    <span class="nl">font-family</span><span class="p">:</span> <span class="nb">sans-serif</span><span class="p">;</span>
    <span class="py">text-anchor</span><span class="p">:</span> <span class="nb">middle</span><span class="p">;</span>
    <span class="py">fill</span><span class="p">:</span> <span class="nb">none</span><span class="p">;</span>
    <span class="py">stroke</span><span class="p">:</span> <span class="m">#000</span><span class="p">;</span>
    <span class="py">stroke-linejoin</span><span class="p">:</span> <span class="n">round</span><span class="p">;</span>
    <span class="py">stroke-linecap</span><span class="p">:</span> <span class="n">round</span><span class="p">;</span>
    <span class="py">stroke-dasharray</span><span class="p">:</span> <span class="m">0</span> <span class="m">6</span> <span class="m">20</span><span class="p">;</span>
    <span class="py">stroke-width</span><span class="p">:</span> <span class="m">2</span><span class="p">;</span>
  <span class="p">}</span>
<span class="err">}</span></code></pre></figure>
  </div>

  <p>
    The original text becomes hard to recognize after adding dashes.
    Its meaning fades away, preserving only the shapes.
  </p>

  <div class="example">
    <css-doodle><style>
      @grid: 1 / 240px;
      @content: @raw(
        <img width="100%" src="https://yuanchuan.dev/assets/images/post/stroke-dasharray/text-2.png" alt="" loading="lazy" />
      );
      /*
      @content: @svg(
        viewBox: 0 0 100 100 p -15;
        style border: 8px solid #000;
        text {
          content: "M";
          font-size: 120;
          font-family: sans-serif;
          text-anchor: middle;
          fill: none;
          x, y: 50 -14;
          stroke: #000;
          stroke-linejoin: round;
          stroke-linecap: round;
          stroke-dasharray: 0 6 0 4 20;
          stroke-width: 2;
          transform: rotate(90);
        }
      );
      */
    </style></css-doodle>

<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="nt">text</span> <span class="p">{</span>
  <span class="c">/* ... */</span>

  <span class="err">content</span> <span class="err">"M";</span>
  <span class="py">stroke-dasharray</span><span class="p">:</span> <span class="m">0</span> <span class="m">6</span> <span class="m">0</span> <span class="m">4</span> <span class="m">20</span><span class="p">;</span>
  <span class="nl">transform</span><span class="p">:</span> <span class="n">rotate</span><span class="p">(</span><span class="m">90</span><span class="p">);</span>
<span class="p">}</span></code></pre></figure>
  </div>

  <p>
    Another complex character.
  </p>

  <div class="example">
    <css-doodle><style>
      @grid: 1 / 240px;
      @content: @raw(
        <img width="100%" src="https://yuanchuan.dev/assets/images/post/stroke-dasharray/text-3.png" alt="" loading="lazy" />
      );
      /*
      @content: @svg(
        viewBox: 0 0 100 100 p -16;
        style border: 8px solid #000;
        text {
          content: "ﷺ";
          font-size: 120;
          x, y: 70, 85;
          font-family: sans-serif;
          text-anchor: middle;
          fill: none;
          stroke: #000;
          stroke-linejoin: round;
          stroke-linecap: round;
          stroke-dasharray: 0 2 9;
          stroke-width: 1;
        }
      );
      */
    </style></css-doodle>

<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="nt">text</span> <span class="p">{</span>
  <span class="c">/* ... */</span>

  <span class="nl">content</span><span class="p">:</span> <span class="s1">"ﷺ"</span><span class="p">;</span>
  <span class="py">stroke-dasharray</span><span class="p">:</span> <span class="m">0</span> <span class="m">2</span> <span class="m">9</span><span class="p">;</span>
<span class="p">}</span></code></pre></figure>
  </div>

    <p>
      There are plenteous Unicode characters and different fonts to try.
      But it's important to note that fonts in different platforms may not look the same.
      The result is highly dependant on the shape of the text.
    </p>

    <div class="notes">

      <p>
        <a href="/experimenting-a-new-syntax-to-write-svg">This article</a>
        describes the syntax that was used to write SVG here.
        All the code examples can be run at <a href="https://css-doodle.com/svg">css-doodle.com/svg</a>.
      </p>
    </div>

  </article>

  <style>
    css-doodle {
      max-width: 100%;
    }
    .example {
      display: flex;
      margin-bottom: 2em;
    }
    .example figure {
      margin: 0;
      order: 1;
      overflow: auto;
    }
    .example figure pre {
      margin: 0;
  }
  .example css-doodle {
    order: 2;
    margin-left: auto;
    flex-shrink: 0;
  }

  .notes {
    padding-top: 2em;
    font-size: .9em;
    border-top: 1px solid var(--footnote-border-color);
    margin-top: 5em;
    line-height: 1.4;
  }
  .notes p {
    color: #777;
  }

  .example-inline {
    display: flex;
    margin-bottom: 1.5em;
    align-items: center;
    gap: 1em;
  }

  @media screen and (max-width: 640px) {
    .example {
      display: block;
    }
    .example css-doodle {
      margin: 0 0 1em 0;
    }
    figcaption {
      font-size: .65em;
    }
  }


  .compare {
    display: flex;
    gap: 1em;
    margin-top: 2em;
  }
</style>

<script src="https://yuanchuan.dev/assets/lib/css-doodle@0.37.3/css-doodle.min.js"></script>
<script>document.addEventListener('DOMContentLoaded', function() {let doodle=document.querySelector("#animate-circle");if(doodle&&doodle.doodle){function createAnimationFrame(e){let t,o=1e3/60,n=0,a=0,d=0,l=!1;function r(l){n||(n=l),e(n);let i=l-a;i<o&&(i=o),i>1e3&&(i=d||1e3),a&&(n+=i),d=i,a=l,t=requestAnimationFrame(r)}return t=requestAnimationFrame(r),{resume(){t&&l&&(l=!1,t=requestAnimationFrame(r))},pause(){t&&(cancelAnimationFrame(t),l=!0)}}}let e=doodle.doodle.querySelector("tspan#num"),t=doodle.doodle.querySelector("animate"),o=doodle.doodle.querySelector("circle"),n=createAnimationFrame((()=>{let t=o.pathLength.animVal;t=t<1?.99:Math.round(t),e.textContent=t})),a=!0;doodle.addEventListener("click",(o=>{a?(t.endElement(),a=!1,n.pause(),e.textContent=.99):(t.beginElement(),a=!0,n.resume())}))}});</script>
]]></content>

      

      
        
      

      <summary type="html"><![CDATA[
  
    The stroke-dasharray attribute in SVG is more flexible compared to the dashed style in CSS.
    While there are numerous articles and examples about its usages,

    including Line animatio...]]></summary>
    </entry>
  
    <entry>
      <title type="html">Simplifying CSS Variable Calculations</title>
      <link href="https://yuanchuan.dev/simplifying-css-variable-calculations" rel="alternate" type="text/html" title="Simplifying CSS Variable Calculations" />
      <published>2023-09-13T00:00:00+08:00</published>
      <updated>2023-09-13T00:00:00+08:00</updated>
      <id>https://yuanchuan.dev/simplifying-css-variable-calculations</id>
      <content type="html" xml:base="https://yuanchuan.dev/simplifying-css-variable-calculations"><![CDATA[<article>
  <p>
    If you often use CSS variables (custom properties),
    you may have found the code referenced by <code>var()</code>
    looks less shining or readable, particularly combined with <code>calc()</code> functions.
    Not only because the expression is usually long,
    the prefixed double hypens <code>--</code> also diminish the readability.
  </p>
  <!--more-->

<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="nt">--a</span><span class="o">:</span> <span class="err">3</span><span class="o">;</span>
<span class="nt">--b</span><span class="o">:</span> <span class="err">5</span><span class="o">;</span>
<span class="nt">--c</span><span class="o">:</span> <span class="nt">calc</span><span class="o">(</span><span class="nt">var</span><span class="o">(</span><span class="nt">--a</span><span class="o">)</span> <span class="o">*</span> <span class="err">7</span> <span class="o">+</span> <span class="nt">var</span><span class="o">(</span><span class="nt">--b</span><span class="o">)</span> <span class="o">*</span> <span class="err">4</span><span class="o">);</span></code></pre></figure>

  <p>
    In <a href="https://css-doodle.com">css-doodle</a>, I'm experimenting a way to eliminate all the visual noises
    while doing calculation to numerical variables.
  </p>
  <p>
    To avoid conflicts with the native <code>calc()</code> function,
    I've introduced the symbol <code>$</code> as a replacement to <code>calc</code>.
    Variables inside <code>$()</code> do not require the <code>var()</code> or <code>--</code>,
    just use the variable names directly:
  </p>

<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="nt">--c</span><span class="o">:</span> <span class="err">$</span><span class="o">(</span><span class="nt">a</span> <span class="o">*</span> <span class="err">7</span> <span class="o">+</span> <span class="nt">b</span> <span class="o">*</span> <span class="err">7</span><span class="o">);</span></code></pre></figure>

  <h2>Unit</h2>

  <p>
    One difference from <code>calc()</code> is that the <code>$()</code> function doesn't support values with units inside it.
    It returns <code>0</code> if the input includes non-numeric variables or values.
  </p>

<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="nt">--index</span><span class="o">:</span> <span class="err">4</span><span class="o">;</span>

<span class="c">/* 0 */</span>
<span class="nt">rotate</span><span class="o">:</span> <span class="err">$</span><span class="o">(</span><span class="err">20</span><span class="nt">deg</span> <span class="o">*</span> <span class="nt">index</span><span class="o">);</span></code></pre></figure>

  <p>
    <strong>Always returning a number is by design.</strong> However, if you want to attach a unit to the result,
    put the unit right behind the symbol <code>$</code> or at the end of the function.
  </p>

<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="nt">--index</span><span class="o">:</span> <span class="err">4</span><span class="o">;</span>

<span class="c">/* 80deg */</span>
<span class="nt">rotate</span><span class="o">:</span> <span class="err">$</span><span class="nt">deg</span><span class="o">(</span><span class="err">20</span> <span class="o">*</span> <span class="nt">index</span><span class="o">);</span>

<span class="c">/* 80deg */</span>
<span class="nt">rotate</span><span class="o">:</span> <span class="err">$</span><span class="o">(</span><span class="err">20</span> <span class="o">*</span> <span class="nt">index</span><span class="o">)</span><span class="nt">deg</span><span class="o">;</span></code></pre></figure>

  <h2>Algebraic expression</h2>

  <p>All the <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math">Math functions</a>
  can be used inside <code>$()</code> function.
  Multiplication signs are sometimes optional like in Algebraic expressions:</p>

<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="nt">--a</span><span class="o">:</span> <span class="err">2</span><span class="o">;</span>
<span class="nt">--b</span><span class="o">:</span> <span class="err">3</span><span class="o">;</span>

<span class="c">/* 95 */</span>
<span class="nt">--c</span><span class="o">:</span> <span class="err">$</span><span class="o">(</span><span class="err">7</span><span class="nt">a</span> <span class="o">+</span> <span class="nt">b</span><span class="o">^</span><span class="err">4</span><span class="o">);</span>

<span class="c">/* 285.73017356099484 */</span>
<span class="nt">--d</span><span class="o">:</span> <span class="err">$</span><span class="o">(</span><span class="nt">cos</span><span class="o">(</span><span class="nt">c</span><span class="o">)</span> <span class="o">+</span> <span class="err">3</span><span class="nt">c</span><span class="o">);</span>

<span class="c">/* 571 */</span>
<span class="nt">--e</span><span class="o">:</span> <span class="err">$</span><span class="o">(</span><span class="nt">round</span><span class="o">(</span><span class="err">2</span><span class="nt">d</span><span class="o">));</span></code></pre></figure>


  <h2>Referencing variables</h2>

  <p>
    When making references to variables, prefix the variable names with <code>$</code>,
    as it is shorter compared to using <code>var()</code>.
  </p>
<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="nt">--r</span><span class="o">:</span> <span class="err">20</span><span class="o">;</span>

<span class="nt">svg</span> <span class="p">{</span>
  <span class="py">d</span><span class="p">:</span> <span class="n">M</span> <span class="m">0</span> <span class="m">0</span> <span class="n">A</span> <span class="err">$</span><span class="n">r</span> <span class="err">$</span><span class="n">r</span> <span class="m">0</span> <span class="m">0</span> <span class="m">1</span> <span class="m">10</span> <span class="m">10</span><span class="p">;</span>
<span class="p">}</span></code></pre></figure>
  </p>

  <p>
    The symbol <code>$</code> shares a common analogy in many programming languages,
    which is why it's been chosen.
  </p>

  <h2>Example</h2>

  <p>
    I previously shared this code of OpenAI logo at <a href="https://vis.social/@yuanchuan/110959087691702860">vis.social</a>.
    Now the readability of the code has improved with the help of <code>$</code>.
  </p>

  <div class="example">
    <css-doodle><style>
  @grid: 1 / 200px;

  --r: 14;
  --r2: 22;
  --x: -r * cos(π/6);
  --y: r * sin(π/6);

  @content: @svg(
    viewBox: -50 -50 100 100;
    stroke-linecap: round;
    stroke: #000;
    fill: none;
    g*6 {
      transform: rotate(@n(*60));
      path {
        stroke-width: 4.2;
        d: M $(x - 1) $y $(x - 1) $(-3.4y)
      }
      path {
        stroke-width: 6;
        d: M $x $(-3.4y) $(-x) $(-5.4y)
           A $r2 $r2 0 0 1 $(-3.1x) $(-1.4y);
      }
    }
  );
</style></css-doodle>

<figure class="highlight"><pre><code class="language-css" data-lang="css"><span class="nt">svg</span> <span class="p">{</span>
  <span class="py">viewBox</span><span class="p">:</span> <span class="m">-50</span> <span class="m">-50</span> <span class="m">100</span> <span class="m">100</span><span class="p">;</span>
  <span class="py">stroke-linecap</span><span class="p">:</span> <span class="n">round</span><span class="p">;</span>
  <span class="py">stroke</span><span class="p">:</span> <span class="m">#000</span><span class="p">;</span>
  <span class="py">fill</span><span class="p">:</span> <span class="nb">none</span><span class="p">;</span>

  <span class="py">--x</span><span class="p">:</span> <span class="m">14</span> <span class="err">*</span> <span class="n">cos</span><span class="p">(</span><span class="n">π</span><span class="p">/</span><span class="m">6</span><span class="p">)</span> <span class="err">*</span> <span class="m">-1</span><span class="p">;</span>
  <span class="py">--y</span><span class="p">:</span> <span class="m">14</span> <span class="err">*</span> <span class="n">sin</span><span class="p">(</span><span class="n">π</span><span class="p">/</span><span class="m">6</span><span class="p">);</span>

  <span class="err">g*6</span> <span class="err">{</span>
    <span class="nl">transform</span><span class="p">:</span> <span class="n">rotate</span><span class="p">(</span><span class="err">@</span><span class="n">n</span><span class="p">(</span><span class="err">*</span><span class="m">60</span><span class="p">));</span>
    <span class="err">path</span> <span class="err">{</span>
      <span class="py">stroke-width</span><span class="p">:</span> <span class="m">4.2</span><span class="p">;</span>
      <span class="py">d</span><span class="p">:</span> <span class="n">M</span> <span class="err">$</span><span class="p">(</span><span class="n">x</span> <span class="n">-</span> <span class="m">1</span><span class="p">)</span> <span class="err">$</span><span class="n">y</span>
         <span class="n">L</span> <span class="err">$</span><span class="p">(</span><span class="n">x</span> <span class="n">-</span> <span class="m">1</span><span class="p">)</span> <span class="err">$</span><span class="p">(</span><span class="m">-3.4</span><span class="n">y</span><span class="p">)</span>
    <span class="p">}</span>
    <span class="nt">path</span> <span class="p">{</span>
      <span class="py">stroke-width</span><span class="p">:</span> <span class="m">6</span><span class="p">;</span>
      <span class="py">d</span><span class="p">:</span> <span class="n">M</span> <span class="err">$</span><span class="n">x</span> <span class="err">$</span><span class="p">(</span><span class="m">-3.4</span><span class="n">y</span><span class="p">)</span>
         <span class="n">L</span> <span class="err">$</span><span class="p">(</span><span class="n">-x</span><span class="p">)</span> <span class="err">$</span><span class="p">(</span><span class="m">-5.4</span><span class="n">y</span><span class="p">)</span>
         <span class="n">A</span> <span class="m">22</span> <span class="m">22</span> <span class="m">0</span> <span class="m">0</span> <span class="m">1</span> <span class="err">$</span><span class="p">(</span><span class="m">-3.1</span><span class="n">x</span><span class="p">)</span> <span class="err">$</span><span class="p">(</span><span class="m">-1.4</span><span class="n">y</span><span class="p">);</span>
    <span class="p">}</span>
  <span class="err">}</span>
<span class="err">}</span></code></pre></figure>

  </div>
  <p>
     <a href="https://css-doodle.com/svg/?name=openai%20logo">https://css-doodle.com/svg/?name=openai+logo</a>
  </p>
</article>


<style>
  css-doodle {
    max-width: 100%;
  }
  .example {
    display: flex;
    margin-bottom: 1.5em;
  }
  .example figure {
    margin: 0;
    order: 1;
    overflow: auto;
  }
  .example figure pre {
    margin: 0;
  }
  .example css-doodle {
    order: 2;
    margin-left: auto;
  }

  @media screen and (max-width: 640px) {
    .example {
      display: block;
    }
    .example css-doodle {
      margin: 0 0 1em 0;
    }
  }
</style>

<script src="https://yuanchuan.dev/assets/lib/css-doodle@0.37.1/css-doodle.min.js"></script>
]]></content>

      

      
        
      

      <summary type="html"><![CDATA[
  
    If you often use CSS variables (custom properties),
    you may have found the code referenced by var()
    looks less shining or readable, particularly combined with calc() functions.
    ...]]></summary>
    </entry>
  
</feed>

