<?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: mcinstinct</title>
    <description>The latest articles on DEV Community by mcinstinct (@mcinstinct).</description>
    <link>https://dev.to/mcinstinct</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4121786%2F4aa8bee2-fa94-4a0b-aec7-1623fc99e709.png</url>
      <title>DEV Community: mcinstinct</title>
      <link>https://dev.to/mcinstinct</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mcinstinct"/>
    <language>en</language>
    <item>
      <title>Printing a cross-stitch chart is the hard part: symbols, skein math, and tiling 262,144 cells</title>
      <dc:creator>mcinstinct</dc:creator>
      <pubDate>Mon, 28 Sep 2026 15:36:38 +0000</pubDate>
      <link>https://dev.to/mcinstinct/printing-a-cross-stitch-chart-is-the-hard-part-symbols-skein-math-and-tiling-262144-cells-4687</link>
      <guid>https://dev.to/mcinstinct/printing-a-cross-stitch-chart-is-the-hard-part-symbols-skein-math-and-tiling-262144-cells-4687</guid>
      <description>&lt;p&gt;Nobody frames a screenshot of a cross-stitch app. The artifact that matters is a printed chart taped to the wall next to the couch, and print is unforgiving in ways screens are not. Symbols must stay legible at 6 points. Page edges must not eat stitches. And the legend has to answer the only question that costs money: how many skeins do I buy?&lt;/p&gt;

&lt;p&gt;I work on Threaded (&lt;a href="https://threaded.diy" rel="noopener noreferrer"&gt;https://threaded.diy&lt;/a&gt;), a browser studio that turns photos into cross-stitch and needlepoint patterns. Our first post walked the image pipeline; this one is about the PDF at the end of it. All the code is real.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ninety symbols, and every one has to survive a laser printer
&lt;/h2&gt;

&lt;p&gt;Each thread color in a chart gets a symbol. Screens make this easy (just color the cells); paper makes it hard, because your chart prints in black and white more often than not. We keep a fixed glyph alphabet:&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;LABEL_CHARS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;●■▲▼◀▶◆⬟⬢⬣⯁○□△▽◁▷◇⬠⬡◯◐◑◒◓◧◨◩◪⬗⬖★☆✦✧✪✯✶✷❂✚✕✖✜⊕⊗⊙⊠♥♦♣♠♡♢♧♤⬮⬯⚑⚐☀☁☂☘✔✿ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩ⋈⋂⋃⊖⊘⋉⋊≈$%#@&amp;amp;!?&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ninety-ish glyphs, ordered so the most visually distinct pairs land first: filled vs. outline (●/○), orthogonal vs. diagonal (■/◆), half-filled circles (◐◑◒◓) for the middle of the alphabet where confusion is most likely. Filled and outline variants of the same shape are never assigned to adjacent colors - adjacent palette entries are usually near each other in color space, and near-identical colors with near-identical symbols is how miscounts happen.&lt;/p&gt;

&lt;p&gt;The symbol ink also adapts to the cell. A dark thread gets a white glyph, a light thread gets black, decided by plain luma:&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isDarkColor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hex&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="nx"&gt;boolean&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;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&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;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&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="mi"&gt;16&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;g&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&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="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;16&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;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mf"&gt;0.299&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.587&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.114&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Yes, it is the ITU-R 601 luma from 1982. It outperforms fancier contrast formulas here because the decision is binary and the input is a solid fill, not text on a gradient.&lt;/p&gt;

&lt;h2&gt;
  
  
  The half-pixel grid line trick
&lt;/h2&gt;

&lt;p&gt;Grid lines help you count; grid lines also drown symbols when cells get small. Two rules came out of real print tests:&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;shouldDrawGridLines&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cellSize&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="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;cellSize&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Below 3 pixels per cell the lines cost more legibility than they add, so they vanish. And when lines do draw, they are stroked at &lt;code&gt;x * cellSize + 0.5&lt;/code&gt; - the classic canvas half-pixel offset. Without it a 1px stroke straddles two physical pixels and prints as a fuzzy gray 2px line. With it, the line lands exactly on one row of device pixels. This one character (&lt;code&gt;+ 0.5&lt;/code&gt;) is the difference between a chart that looks printed and a chart that looks photocopied.&lt;/p&gt;

&lt;h2&gt;
  
  
  Skein math, or: the legend is a shopping list
&lt;/h2&gt;

&lt;p&gt;The thread table is the page stitchers photograph before driving to the craft store. For every color it shows symbol, brand and code (DMC 310, Anchor 403, ...), stitch count, yardage, and skeins. The skein estimate is where most apps hand-wave; we made the constants explicit and honest:&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SKEIN_PIECES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;        &lt;span class="c1"&gt;// a pulled-apart skein yields ~17 pieces&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;USABLE_PER_PIECE_IN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// usable inches per piece after tails&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;THREAD_PER_STITCH_NUM&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// inches of floss per stitch = 6 / fabric count&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;STRANDS_PER_SKEIN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DEFAULT_STRANDS&lt;/span&gt; &lt;span class="o"&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;// most people stitch with 2 of 6 strands&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;YARDS_PER_SKEIN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;8.7&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;    &lt;span class="c1"&gt;// a standard DMC skein&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The math: one stitch on 14-count aida at 2 strands consumes 6/14 inches of a strand. A skein gives you 17 pieces x 15 usable inches x (6 strands / 2 used). So stitches-per-skein is computed per color, divided into that color's actual stitch count, and rounded UP - because "you need 1.1 skeins" means you buy 2, and a chart that tells you 1 sends you back to the store mid-project. Every constant is wrong for somebody (needlepointers on 13-mesh use more thread, some stitchers waste less), which is why the fabric count and strand count are inputs, not assumptions baked into the output.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tiling 262,144 cells across Letter pages
&lt;/h2&gt;

&lt;p&gt;A 512x512 pattern does not fit on a page. It becomes a poster you assemble:&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SECTION_COUNTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="c1"&gt;// 1x1, 2x2, 3x3, or 4x4 pages&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DPI&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;150&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                       &lt;span class="c1"&gt;// raster resolution per section&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The grid splits into tiles with integer boundary math (&lt;code&gt;x0 = floor(col * grid.w / cols)&lt;/code&gt;) so no stitch ever lands on two pages or none. Each section renders to canvas at 150 DPI, and the PDF assembles the pages in reading order with the section grid labeled on the key page. The document also embeds the Merriweather family directly, because a PDF that substitutes fonts at the print shop is a PDF that lies about symbol alignment.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we deliberately did not build
&lt;/h2&gt;

&lt;p&gt;No overlap duplication between sections. Overlaps sound helpful until you realize every duplicated stitch is a stitch you can count twice; clean tile boundaries plus a labeled key page do the job with zero ambiguity. No color blocks behind symbols on the key page (wastes toner, and the symbol IS the reference). No vector charts (canvas raster at 150 DPI prints identically everywhere and renders in milliseconds on a five-year-old phone).&lt;/p&gt;

&lt;p&gt;Try it at &lt;a href="https://threaded.diy" rel="noopener noreferrer"&gt;https://threaded.diy&lt;/a&gt; - the full editor is free, so you can run your own photo through the pipeline and watch the chart take shape before deciding whether the printable PDF is worth Pro. If your estimate differs, the comments are open; the constants are right there to argue with.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>typescript</category>
      <category>javascript</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Turning a photo into a cross-stitch pattern in the browser: the actual pipeline</title>
      <dc:creator>mcinstinct</dc:creator>
      <pubDate>Sat, 26 Sep 2026 15:30:31 +0000</pubDate>
      <link>https://dev.to/mcinstinct/turning-a-photo-into-a-cross-stitch-pattern-in-the-browser-the-actual-pipeline-1mc4</link>
      <guid>https://dev.to/mcinstinct/turning-a-photo-into-a-cross-stitch-pattern-in-the-browser-the-actual-pipeline-1mc4</guid>
      <description>&lt;p&gt;A photo you took this morning contains millions of distinct colors. A skein of DMC embroidery floss comes in a few hundred. A stitchable pattern wants maybe 10 to 40. Bridging that gap - automatically, in a browser tab, fast enough to feel instant - is the core engineering problem of a pattern maker.&lt;/p&gt;

&lt;p&gt;I work on Threaded (&lt;a href="https://threaded.diy" rel="noopener noreferrer"&gt;https://threaded.diy&lt;/a&gt;), a browser-based studio that turns photos into cross-stitch and needlepoint patterns. This post walks through the real pipeline in our codebase, with the actual tradeoffs we made. No pseudocode: this is what ships.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: median-cut quantization, capped at 64 colors
&lt;/h2&gt;

&lt;p&gt;The first job is palette reduction. We use median-cut quantization (via the &lt;code&gt;quantize&lt;/code&gt; package) over the opaque pixels of the source image:&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;quantizeImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ImageData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;n&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="nl"&gt;palette&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;pixels&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Pixels&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;data&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;image&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;samples&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="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;let&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&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;y&lt;/span&gt;&lt;span class="o"&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;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&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;x&lt;/span&gt;&lt;span class="o"&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="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;4&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;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&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="c1"&gt;// fully transparent pixels don't stitch&lt;/span&gt;
      &lt;span class="nx"&gt;samples&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="nx"&gt;data&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="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;2&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="c1"&gt;// ...median cut down to n cluster centroids, then snap every pixel&lt;/span&gt;
  &lt;span class="c1"&gt;// to its nearest centroid&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details matter here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The cap is 64 colors (MAX_N), not a marketing number.&lt;/strong&gt; More than ~64 distinct threads makes a pattern physically unenjoyable to stitch - every new color is another needle to thread and another symbol to track on the chart. Most stitchers stay under 30.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transparent pixels are excluded before sampling.&lt;/strong&gt; A PNG with a knocked-out background should not waste palette slots on a background you'll never stitch.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The grid itself is capped at 512x512 cells and we accept source images up to 50 megapixels, since phone cameras don't ask permission.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: snapping the palette to real thread you can buy
&lt;/h2&gt;

&lt;p&gt;A quantized palette is useless if it says "#B7414E" and your local shop sells skeins, not hex codes. So every palette color gets matched to the nearest color in a real thread library - DMC, Anchor, Cosmo, Madeira, and 45 more, 49 libraries in total:&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;nearestBrandColor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hex&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="nx"&gt;shorthand&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="nx"&gt;ThreadColor&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;brand&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;BRAND_BY_SHORTHAND&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;shorthand&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;brand&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;brand&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Unknown or empty brand: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;shorthand&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="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hexToRgb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;best&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;brand&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;bestD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;Infinity&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;c&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;brand&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;colors&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="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dist2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;hexToRgb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// squared Euclidean distance in RGB&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;d&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;bestD&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;bestD&lt;/span&gt; &lt;span class="o"&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;best&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;c&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="nx"&gt;best&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's the honest part: &lt;strong&gt;squared Euclidean distance in RGB is not perceptually uniform.&lt;/strong&gt; CIEDE2000 exists precisely because two colors can be far apart in RGB but look identical to the eye, or vice versa. We know this. We ship it anyway, deliberately:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It's O(palette x brand) with zero dependencies, and it runs on every keystroke in the editor. A CIEDE2000 implementation with Lab conversion is heavier, and the difference at thread-palette granularity is smaller than the difference between dye lots.&lt;/li&gt;
&lt;li&gt;The match is &lt;em&gt;palette-relative&lt;/em&gt;. Even if a red lands one skein off from perceptually perfect, every red in the photo lands on the same skein, so the pattern stays internally consistent. Consistency is what the eye reads.&lt;/li&gt;
&lt;li&gt;Stitchers routinely swap one or two colors by preference anyway. The editor is built around that: click a cell, flood-fill a region, reassign a color to a different skein, and the whole legend updates.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you're building something similar and only take one thing from this post: perfect colorimetry is not the bottleneck. Iteration speed is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: killing confetti stitches
&lt;/h2&gt;

&lt;p&gt;Quantized photos produce "confetti" - single isolated stitches of one color marooned inside a field of another. Confetti is the number one reason printed patterns get abandoned halfway through: each orphan stitch means threading a needle for exactly one X.&lt;/p&gt;

&lt;p&gt;We run a despeckle pass that examines each cell's 8-connected neighborhood and relabels orphans into the surrounding region:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;NEIGHBORS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;           &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;despeckleRegion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pixels&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Pixels&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cx&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="nx"&gt;cy&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="nx"&gt;reach&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// flood-fill the region around (cx, cy), then reassign cells whose&lt;/span&gt;
  &lt;span class="c1"&gt;// neighborhood votes overwhelmingly for a different color&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This one pass does more for "does the finished piece look like the photo" than any color-space upgrade ever will. Faces survive. Text survives. The 400 singleton stitches that made your grandmother's pattern software unusable do not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: the PDF is the product
&lt;/h2&gt;

&lt;p&gt;The on-screen editor is only half the job. The artifact people actually use is a printed chart, and print has hard requirements a screen doesn't:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;every color gets a distinct, legible symbol (and 64 distinguishable glyphs is genuinely hard),&lt;/li&gt;
&lt;li&gt;a legend maps symbol to thread brand, code, name, and stitch count,&lt;/li&gt;
&lt;li&gt;the chart tiles across pages with overlap marks so you don't lose your place,&lt;/li&gt;
&lt;li&gt;fonts must be embedded or Kinko's will freelance for you.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We generate the PDF client-side with the Merriweather family embedded directly in the document, so the file prints identically anywhere. Stitch counts per color are computed during export, because that's what tells you how many skeins to buy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why all of this runs in your browser
&lt;/h2&gt;

&lt;p&gt;Everything above - quantization, thread matching, despeckle, PDF generation - executes locally in the tab. Your photos never touch our servers. That started as a privacy decision (people stitch their kids, their dogs, their weddings), and it turned out to be an architecture decision too: no upload round-trip means the editor can re-run the pipeline on every slider movement.&lt;/p&gt;

&lt;p&gt;If you want to see the pipeline from the other side, Threaded is free to try at &lt;a href="https://threaded.diy" rel="noopener noreferrer"&gt;https://threaded.diy&lt;/a&gt; - upload a photo, drag the color-count slider, and watch steps 1 through 3 re-run in the time it takes to move your thumb.&lt;/p&gt;

&lt;p&gt;Happy stitching. And if you've solved the CIEDE2000-at-60fps problem in a way you're proud of, I genuinely want to read about it in the comments.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>typescript</category>
      <category>javascript</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>I Built a Free Library of 100,000+ Custom Emojis for Microsoft Teams</title>
      <dc:creator>mcinstinct</dc:creator>
      <pubDate>Sat, 12 Sep 2026 06:43:46 +0000</pubDate>
      <link>https://dev.to/mcinstinct/i-built-a-free-library-of-100000-custom-emojis-for-microsoft-teams-4kgj</link>
      <guid>https://dev.to/mcinstinct/i-built-a-free-library-of-100000-custom-emojis-for-microsoft-teams-4kgj</guid>
      <description>&lt;p&gt;If you've ever tried to add a custom emoji to Microsoft Teams, you know the two problems: finding anything good, and getting it sized right. Teams, Slack and Discord all have different limits, and half the "emoji resizer" tools out there are built for Twitch streamers.&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://www.teamsmoji.com" rel="noopener noreferrer"&gt;Teamsmoji&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;100,000+ free custom emojis, memes and reaction GIFs, searchable and organized into packs (party parrot, blob cats, dancing bananas...)&lt;/li&gt;
&lt;li&gt;Every emoji downloads at the right size for Teams, Slack or Discord&lt;/li&gt;
&lt;li&gt;A free &lt;a href="https://www.teamsmoji.com/emoji-maker" rel="noopener noreferrer"&gt;Emoji Maker&lt;/a&gt; that turns any image, GIF or video into a perfectly sized emoji, entirely in your browser: crop, GIF speed control, video-to-GIF, and party effects (rainbow, spin, shake, pulse, wave)&lt;/li&gt;
&lt;li&gt;Short guides on adding custom emojis to Teams, Slack and Discord, including what to do when your admin has locked uploads down&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No login, no watermark, and nothing leaves your device in the maker.&lt;/p&gt;

&lt;p&gt;Would love feedback from anyone who runs a workspace - what's missing?&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>productivity</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
