A full deck of dynamically generated SVG playing cards in 47 KB

Dynamic SVG playing card generation on SolitaireCat.com

last updated: Aug 9, 2024

SVG cards are beautiful on hi-DPI devices, but tend to be as large, or larger than raster versions. This doesn’t need be the case. Given the quantity of repeated elements, the information required to encode a complete set of cards is not too great.

There are:

  1. 4 unique pips (Clubs, Diamonds, Hearts, Spades).
  2. 13 unique glyphs for the (2-10, J, Q, K, A).
    (the “10” is treated as a single glyph)
  3. 12 unique court card images (Jack, Queen, King in 4 suits).
  4. 13 pip layout patterns (2-10, J, Q, K, A) and other layout information.
    (glyph position, margins, size etc.)

With these primitives in a single SVG file, all posisble cards can be generated dynamically in JS.

Creating the primitives

1. Pips (0.2 KB gzip)

I hand drew the pips using Inkscape and the wonderful SvgPathEditor.

SvgPathEditor is great for maintaining zero precision (i.e whole numbers) for path elements. I believe that Inkscape can be made to snap to whole numbers when editing paths, but I recall it requiring extensions. I may be wrong or out of date on this.

Pips with uncompressed sizes

177 bytes
93 bytes
99 bytes
116 bytes

The SVG to encode an infinitely scalable heart pip is only 99-bytes uncompressed, with resolution to match whatever device you’re using.

<svg id='sc' viewBox='0 0 20 24'>
    <circle cx='10' cy='5' r='5'/>
    <circle cx='5' cy='13' r='5'/>
    <circle cx='15' cy='13' r='5'/>
    <path d='M6 24L14 24Q9 18 12 8L8 8Q11 18 6 24'/>
</svg>
<svg id='sd' viewBox='0 0 10 12'>
    <path d='M5 0Q6 2 10 6Q6 10 5 12Q4 10 0 6Q4 2 5 0'/>
</svg>
<svg id='sh' viewBox='0 0 10 12'>
    <path d='M0 3c0 4 3 6 5 9 2-3 5-5 5-9S5-1 5 3c0-4-5-4-5 0'/>
</svg>
<svg id='ss' viewBox='0 0 10 12'>
    <path d='M7 12c-1-1-2-2-1.5-3C8 12 14 7 5 0c-9 7-3 12-.5 9C5 10 4 11 3 12z'/>
</svg>
SVG source for the four pips

2. Glyphs (1.2 KB gzip)

SVG supports <text> elements, and even allows embedded fonts for consistent rendering, but for this application, where there are only 13 unique glyphs and efficiency is paramount, it makes more sense to implement them directly as SVG paths.

The required glyphs are:

2,3,4,5,6,7,8,9,10,J,Q,K,A

Converting a TTF → SVG symbols

The easiest way to create the glyph paths is to use an existing font, and most fonts are available in TrueType font (TTF) files. A quick search for TTF → SVG tools turned up the font_to_svg library by donbright. From the description, it sounded likely precisely what I needed:

This code will convert a single character in a TrueType(R) font file into an SVG ‘path’ shape.

It’s a C++ library, with several example CLI programs. None of them suited my application exactly, so I forked it and added another CLI program that produces individual, outline only, no transform SVG files for a given TTF file and list of glyph IDs.

Multiple SVGs → one optimized SVG with correct viewBoxes

I then fed these SVG files into this Node script, which optimizes them (via SVGO), sets the viewBox, and concatenates them into a single SVG file for use as symbols.

Manual refinement

The output of the above two steps was good, but it was clear from looking at the paths that there was room for further size reduction, without a meaningful loss of fidelity.

I reduced the coordinate space to 2 or 3 digits, and manually removed nodes where possible.

The final size for the 13 glyphs is 2.8 KB (1.2 KB compressed).

3. Court card images (44.8 KB gzip)

The 12 images for the court cards (Jack, Queen, and King in 4 suits) represent the bulk (>90%) of the data.

I didn’t fancy drawing my own images. An Internet deep-dive revealed a few original projects, and many derived from them, which I assembled into the below - cherry picked - history:

  • 2004
    David Bellot produces a set of SVG playing cards.

    Weighing in at around 900 KB uncompressed and with the court cards “based on the french representation”, they are unique and beautiful and in the author’s words, have “been used everywhere: games, websites, illustrations for books, courses, and even to make decorations for cakes and huge playing cards for children in summer camps!”

    330 KB | LGPL 2.1

  • 2011
    Byron Knoll creates a set of SVG cards for an App Engine card game he is working on.

    They are nice, but enormous.

    3.5 MB | Public domain

  • 2011
    Chris Aguilar creates a set of cards complete with court card images.

    These cards are also pretty, but weighing in at ~2.2 MB uncompressed, very large. They appear to be raw unoptimzed Inkscape output.

    (The cards are later updated several times, with the current version at 3.2, released in June 2019 )

    834 KB | LGPL 3.0

    • 2014
      Digitaldesignlabs take Chris Aguilar’s cards and optimize them, resulting in court cards that are around 65 KB each uncompressed.

      350 KB | LGPL 3.0

  • 2018
    Adrian Kennard (AKA RevK) surveys the landscape of SVG playing cards, notes Chris Aguilar’s effort, and decides to create his own on the basis that he can:

    1. Release them with a less restrictive license
    2. Reduce the bloat significantly.

    …SVG has a lot of bloat. Making a ten of clubs resulted in a file that was 29k. One I picked on wikipedia was 33k. By making the SVG myself, not using inkscape I was able to make use of symbol objects, so only drawing the club once, then using it 12 times for example. My SVG is probably not quite as compact as I would like, but my ten of clubs is only 1.7k, so much leaner.

    He goes on to hand create a set of beautiful cards in individual SVG files based on a physical set of Goodall & Son cards from the late 19th century! You can read all about the process here and download a customized copy of the cards here.

    211 KB | CC0 Public Domain

    • 2020
      Danny Englelman looks at RevK’s cards and decides to optimize them further. This is not a lossless operation, and some detail is removed, but the end product is still a very nice looking set of cards. The optimizations are primarily:

      • Lowering precision with SVGO.
      • Manually removing very fine detail.

      He also packages them as a custom <card-t> element web component.

      The result is cardmeister.github.io and the the SVG + JS comes in at 62.6 KB gzipped if all court cards are included.

      63 KB | Unlicense

  • 2020
    Daniel S. Fowler produces yet another set of original cards including jokers. These have great detail, but are too large for our application.

    638 KB | Public domain

At 63 KB, Danny Englelman’s 2020 court card images are a good starting point for our own cards, but as always there is room to improve.

To reduce the size further, I:

  • Removed redundant nodes.
  • Reduced the scale to 3-digit coordinates vs 4.
  • Mirrored the top half as a group instead of by layer
  • Removed unecessary element dimensions and positions
  • Removed lower layer detail that is not visible.
  • Removed unecessary sizes and positions (which also fixed some minor alignment isues)

In the end, this produced a set of 12 court-card images identical to Danny’s, but only 44.8 KB gzipped.

Adding in the pips and glyphs gives a full set of SVG primitives at 46.3 KB gzipped

Sizes (KB) raw gzip
Letters 2.8 1.2
Pips 0.5 0.2
Court card images 103.8 44.8
Total 107.3 46.3

4. Pip layout pattern

The layout information for the pips, court card images, glyphs etc. is encoded in JS.

// ******************************
// All pos / dims are percentages
// ******************************

type Coord = [x: number, y: number];

const mh = 26; // pip margin horiz
const mv = 20; // pip margin vert

// PIP grid

const x1 = mh;
const x2 = 100-mh;

const yr = 100 - 2*mv; // y range

const y1 = mv;
const y2 = y1 + yr / 6
const y3 = y1 + yr / 4
const y4 = y1 + yr / 3

export const crtPicSize = { w:69, h:78 };

export interface PipLayout {
    std: Coord[];
    mirror: Coord[]; // these pips will be mirrored (i.e. 2 pips per entry)
}

const center:Coord = [50, 50];

/** Layout of pips (ace -> 10) */
export const pipLayout: PipLayout[] = [
    { std: [ center ],                     mirror: [] }, // A
    { std: [],                             mirror: [[50, y1]] }, // 2
    { std: [ center ],                     mirror: [[50, y1]] }, // 3
    { std: [],                             mirror: [[x1, y1], [x2, y1]] }, // 4
    { std: [ center ],                     mirror: [[x1, y1], [x2, y1]] }, // 5
    { std: [[x1, 50], [x2, 50]],           mirror: [[x1, y1], [x2, y1]] }, // 6
    { std: [[50, y3], [x1, 50], [x2, 50]], mirror: [[x1, y1], [x2, y1]] }, // 7
    { std: [[x1, 50], [x2, 50]],           mirror: [[x1, y1], [x2, y1], [50, y3]] }, // 8
    { std: [ center ],                     mirror: [[x1, y1], [x2, y1], [x1, y4], [x2, y4]] }, // 9
    { std: [],                             mirror: [[x1, y1], [x2, y1], [x1, y4], [x2, y4], [50, y2]] }, // 10
]

// PIP layout for court cards

const cx1 = 25;        // left
const cx2 = 100 - cx1; // right
const cy = 19;         // top

/** Layout of court card pips (J/Q/K) */
export const pipLayoutCrt: Coord[][] = [
    [[cx2, cy], [cx2, cy], [cx2, cy], [cx1, cy]], //  JC JD JH JS
    [[cx2, cy], [cx2, cy], [cx1, cy], [cx2, cy]], //  QC QD QH QS
    [[cx1, cy], [cx1, cy], [cx1, cy], [cx1, cy]], //  KC KD KH KS
];

With these primitives and a kilobyte of JS, we can produce any card

The SVG primitives are stored in a single SVG file and referenced as an <object>*. A data attribute is used to track the loaded state.

* Note: This was later changed to an <iframe> as most browsers do not cache the file when using an <object> tag.

<script>
    const setLoadedAttr = (o) => o.setAttribute("data-loaded", "true");
</script>
<object
    id="svgroot" class="hidden" aria-hidden="true" data-loaded="false"
    onload="setLoadedAttr(this)"
    type="image/svg+xml"
    data="card-src.svg"
    >
</object>

Individual cards are created like so:

const svgDoc = document.getElementById('svgroot')!.contentDocument,

card9c = makeCard(
    svgDoc,
    Suit.clubs, 9);

cardQc = makeCard(
    svgDoc,
    Suit.clubs, 12);

This creates the following two SVG elements (CSS border/shadow added):

With the following content::

<svg xmlns="http://www.w3.org/2000/svg" id="9c" width="1" height="1.4">
    <use href="card-src.svg#0x39" fill="#000" width="10%" height="10%" x="4%" y="3%"></use>
    <use href="card-src.svg#0x39" fill="#000" width="10%" height="10%" x="-96%" y="-97%" style="transform: scale(-1);"></use>
    <use href="card-src.svg#sc" fill="#000" width="6%" height="6%" x="6%" y="15%"></use>
    <use href="card-src.svg#sc" fill="#000" width="6%" height="6%" x="-94%" y="-85%" style="transform: scale(-1);"></use>
    <use href="card-src.svg#sc" fill="#000" width="17%" height="17%" x="41.5%" y="41.5%"></use>
    <use href="card-src.svg#sc" fill="#000" width="17%" height="17%" x="17.5%" y="11.5%"></use>
    <use href="card-src.svg#sc" fill="#000" width="17%" height="17%" x="65.5%" y="11.5%"></use>
    <use href="card-src.svg#sc" fill="#000" width="17%" height="17%" x="17.5%" y="31.5%"></use>
    <use href="card-src.svg#sc" fill="#000" width="17%" height="17%" x="65.5%" y="31.5%"></use>
    <use href="card-src.svg#sc" fill="#000" width="17%" height="17%" x="-82.5%" y="-88.5%" style="transform: scale(-1);"></use>
    <use href="card-src.svg#sc" fill="#000" width="17%" height="17%" x="-34.5%" y="-88.5%" style="transform: scale(-1);"></use>
    <use href="card-src.svg#sc" fill="#000" width="17%" height="17%" x="-82.5%" y="-68.5%" style="transform: scale(-1);"></use>
    <use href="card-src.svg#sc" fill="#000" width="17%" height="17%" x="-34.5%" y="-68.5%" style="transform: scale(-1);"></use>
</svg>

<svg xmlns="http://www.w3.org/2000/svg" id="qc" width="1" height="1.4">
    <use href="card-src.svg#qc" width="69%" height="78%" x="15.5%" y="11%"></use>
    <rect rx=".5%" ry=".5%" stroke="#44F" fill="none" stroke-width=".5%" width="69%" height="78%" x="15.5%" y="11%">
    </rect>
    <use href="card-src.svg#0x51" fill="#000" width="10%" height="10%" x="4%" y="3%"></use>
    <use href="card-src.svg#0x51" fill="#000" width="10%" height="10%" x="-96%" y="-97%" style="transform: scale(-1);"></use>
    <use href="card-src.svg#sc" fill="#000" width="6%" height="6%" x="6%" y="15%"></use>
    <use href="card-src.svg#sc" fill="#000" width="6%" height="6%" x="-94%" y="-85%" style="transform: scale(-1);"></use>
    <use href="card-src.svg#sc" fill="#000" width="14%" height="14%" x="68%" y="14%"></use>
    <use href="card-src.svg#sc" fill="#000" width="14%" height="14%" x="-32%" y="-86%" style="transform: scale(-1);"></use>
</svg>
Prev in section: