<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Yurii L</title>
    <description>The latest articles on DEV Community by Yurii L (@astic_ai).</description>
    <link>https://dev.to/astic_ai</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3979169%2F69b4c459-0ca4-45e9-8044-e839de71702f.png</url>
      <title>DEV Community: Yurii L</title>
      <link>https://dev.to/astic_ai</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/astic_ai"/>
    <language>en</language>
    <item>
      <title>Why we built 78 tarot cards in SVG — and the export bug that nearly killed it</title>
      <dc:creator>Yurii L</dc:creator>
      <pubDate>Thu, 11 Jun 2026 11:05:41 +0000</pubDate>
      <link>https://dev.to/astic_ai/why-we-built-78-tarot-cards-in-svg-and-the-export-bug-that-nearly-killed-it-31p5</link>
      <guid>https://dev.to/astic_ai/why-we-built-78-tarot-cards-in-svg-and-the-export-bug-that-nearly-killed-it-31p5</guid>
      <description>&lt;p&gt;We built &lt;a href="https://astic.ai" rel="noopener noreferrer"&gt;astic.ai&lt;/a&gt; — an app where you pull tarot cards and get a written reading. The hero of the whole thing is a deck you fan out and draw from, and every card has its own face. Seventy-eight of them.&lt;/p&gt;

&lt;p&gt;The interesting engineering decision wasn't the AI. It was the deck. Here's why we rendered all 78 cards as &lt;strong&gt;SVG&lt;/strong&gt;, what made it hard, and the export bug that nearly shipped a PDF full of gold rectangles.&lt;/p&gt;

&lt;h2&gt;
  
  
  The constraints that killed the obvious options
&lt;/h2&gt;

&lt;p&gt;A tarot deck on the web sounds like an asset problem: draw 78 cards, export PNGs, done. We didn't, for four reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;One deck, many sizes.&lt;/strong&gt; The same card shows up huge in the hero fan, medium in a three-card spread, and tiny as a thumbnail. Raster means re-exporting 78 cards at several resolutions — and they still blur during the flip/scale animations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every card is themeable.&lt;/strong&gt; Each card is tinted by an &lt;code&gt;accent&lt;/code&gt; color depending on context (suit, spread type). In SVG that's one prop: &lt;code&gt;fill={accent}&lt;/code&gt;. As images you'd pre-render every card × every tint. No thanks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payload.&lt;/strong&gt; The art is line-based — thin strokes, negative space. As inline SVG the whole deck is a few KB of vectors, not 78 image requests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It has to feel alive.&lt;/strong&gt; Stars twinkle, a glow pulses, cards draw in. You can animate individual nodes inside an SVG. You can't animate the inside of a flat PNG.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So: SVG. All art lives in a &lt;code&gt;100×100&lt;/code&gt; viewBox and gets tinted by an &lt;code&gt;accent&lt;/code&gt; prop. Simple in theory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hard part #1: 78 unique faces without drawing 78
&lt;/h2&gt;

&lt;p&gt;You do not want to hand-illustrate 78 cards. The trick is realizing the deck isn't 78 unique things — it's &lt;strong&gt;22 + (4 × 14)&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;22 Major Arcana&lt;/strong&gt; get bespoke motifs — each one is its own little scene (The Tower, The Star, The Moon…). Those we authored individually.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;56 Minor Arcana&lt;/strong&gt; are &lt;em&gt;generated&lt;/em&gt;: a suit glyph laid out as pips by rank, and a throned glyph for the courts (Page/Knight/Queen/King). One function draws "five Cups" or "eight Wands" by placing N suit glyphs; the courts share a layout with a different emblem.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That single decision — procedural minors, bespoke majors — is the difference between an afternoon and a month. You write the &lt;em&gt;system&lt;/em&gt; once and 56 cards fall out of it, coherent because they share the same geometry.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// shared decor every card reuses — tinted by `accent`&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Stars&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;accent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pts&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;accent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;pts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;][]&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;g&lt;/span&gt; &lt;span class="na"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;accent&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;pts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(([&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;circle&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;cx&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;r&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mf"&gt;1.1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"tarot-twinkle"&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;animationDelay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;s`&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;g&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Glow&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;accent&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;accent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;circle&lt;/span&gt; &lt;span class="na"&gt;cx&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;r&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;accent&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mf"&gt;0.16&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"tarot-pulse"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The "alive" feeling is almost free: &lt;code&gt;tarot-twinkle&lt;/code&gt; and &lt;code&gt;tarot-pulse&lt;/code&gt; are two CSS keyframes, and a staggered &lt;code&gt;animationDelay&lt;/code&gt; per star stops them blinking in unison. No JS animation loop, no library.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hard part #2: making line-art look like more than line-art
&lt;/h2&gt;

&lt;p&gt;Thin gold strokes on a near-black background read as cheap unless you add depth. We leaned on SVG's real superpowers — &lt;strong&gt;gradients, &lt;code&gt;mask&lt;/code&gt;, and filters&lt;/strong&gt; (&lt;code&gt;feMorphology&lt;/code&gt; to erode/outline shapes and fake an engraved edge). This is where SVG pulls ahead of canvas: it's declarative, it's in the DOM, it's inspectable, and it scales without a single blurry pixel.&lt;/p&gt;

&lt;p&gt;It's also exactly where things broke.&lt;/p&gt;

&lt;h2&gt;
  
  
  The war story: our cards became gold rectangles in the PDF
&lt;/h2&gt;

&lt;p&gt;Readings are exportable to PDF. We rasterize the report DOM with &lt;a href="https://github.com/bubkoo/html-to-image" rel="noopener noreferrer"&gt;&lt;code&gt;html-to-image&lt;/code&gt;&lt;/a&gt; (&lt;code&gt;toPng&lt;/code&gt;) and lay the image out with jsPDF.&lt;/p&gt;

&lt;p&gt;First export: every card was a &lt;strong&gt;solid gold block&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The cause: &lt;code&gt;html-to-image&lt;/code&gt; serializes the DOM and rasterizes it itself — and it &lt;strong&gt;cannot render SVG &lt;code&gt;mask&lt;/code&gt; + &lt;code&gt;feMorphology&lt;/code&gt;&lt;/strong&gt;. Those collapse to filled rectangles. The exact two techniques that made the cards look good were the two the rasterizer couldn't handle.&lt;/p&gt;

&lt;p&gt;The browser, of course, renders them perfectly — when &lt;em&gt;it&lt;/em&gt; draws the SVG to a &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt;. So the fix is to let the browser do the rasterization before &lt;code&gt;html-to-image&lt;/code&gt; ever sees it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before capture: swap each card &amp;lt;svg&amp;gt; for a browser-rendered PNG, then restore.&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;rasterizeSvgs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HTMLElement&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;restores&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;svg&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;svg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;svg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBoundingClientRect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;xml&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;XMLSerializer&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;serializeToString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;svg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;data:image/svg+xml;charset=utf-8,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;xml&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;png&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;svgUrlToPng&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// draws to &amp;lt;canvas&amp;gt;, toDataURL&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;img&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;png&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;svg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parentNode&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replaceChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;svg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;restores&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replaceChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;svg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;restores&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;r&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt; &lt;span class="c1"&gt;// undo after capture&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Capture flow becomes: &lt;code&gt;rasterizeSvgs()&lt;/code&gt; → &lt;code&gt;toPng()&lt;/code&gt; → restore. The browser renders the masks and filters correctly into each PNG, &lt;code&gt;html-to-image&lt;/code&gt; just copies pixels, and the cards survive into the PDF looking like cards.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell past-me
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SVG is the correct tool for a themeable, animated, multi-size illustration system.&lt;/strong&gt; One source, infinite sizes, prop-driven theming, CSS animation on individual nodes. None of that is pleasant with raster or canvas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Procedural beats bespoke for combinatorial sets.&lt;/strong&gt; 56 minor cards = one layout function, not 56 drawings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Know your rasterizer's blind spots.&lt;/strong&gt; &lt;code&gt;mask&lt;/code&gt; and &lt;code&gt;feMorphology&lt;/code&gt; are first-class in browsers but second-class in DOM-to-image libraries. If you have an export path, test the &lt;em&gt;fancy&lt;/em&gt; elements early, not the easy ones.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can fan the deck and pull a card yourself at &lt;strong&gt;&lt;a href="https://astic.ai" rel="noopener noreferrer"&gt;astic.ai&lt;/a&gt;&lt;/strong&gt; — the hero on the homepage is the exact deck described here. Open DevTools on a card; it's all vectors. 🔮&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>svg</category>
      <category>react</category>
      <category>frontend</category>
    </item>
  </channel>
</rss>
