<?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: Edinamar Luce</title>
    <description>The latest articles on DEV Community by Edinamar Luce (@edinamar_luce_770f4831715).</description>
    <link>https://dev.to/edinamar_luce_770f4831715</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%2F4222822%2F11b2786e-855e-44e3-acff-7e0fd9ecd405.png</url>
      <title>DEV Community: Edinamar Luce</title>
      <link>https://dev.to/edinamar_luce_770f4831715</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/edinamar_luce_770f4831715"/>
    <language>en</language>
    <item>
      <title>Turning a photo into a cross stitch pattern in the browser: Wu quantization, CIEDE2000 and no dithering</title>
      <dc:creator>Edinamar Luce</dc:creator>
      <pubDate>Sun, 15 Nov 2026 21:09:14 +0000</pubDate>
      <link>https://dev.to/edinamar_luce_770f4831715/turning-a-photo-into-a-cross-stitch-pattern-in-the-browser-wu-quantization-ciede2000-and-no-3f9a</link>
      <guid>https://dev.to/edinamar_luce_770f4831715/turning-a-photo-into-a-cross-stitch-pattern-in-the-browser-wu-quantization-ciede2000-and-no-3f9a</guid>
      <description>&lt;p&gt;Cross stitch patterns are just tiny images: one square of fabric, one stitch, one color of thread. So "photo to pattern" sounds like a resize and a palette swap. It mostly is, and the parts that make it hard taught me a few useful things about color and images. Here's how the converter I built works. It runs entirely in the browser, so the photo never leaves the user's machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. One pixel per stitch
&lt;/h2&gt;

&lt;p&gt;The first step is the boring one: scale the photo so that one pixel is one stitch. A pattern 140 stitches wide gets a 140-pixel-wide image. Everything after this works on that small grid, which is why the whole pipeline runs in milliseconds even on a phone.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Reduce the colors first, then match to thread
&lt;/h2&gt;

&lt;p&gt;Embroidery floss comes in fixed colors. The DMC range has around 500, and nobody wants to buy 500 skeins, so a pattern needs to use maybe 15 to 40 of them.&lt;/p&gt;

&lt;p&gt;The naive approach is to snap every pixel straight to its nearest DMC color. That gives you far too many colors and no control over how many. Instead:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Reduce the photo's own colors to N using &lt;strong&gt;Wu's color quantizer&lt;/strong&gt; (via the MIT-licensed &lt;code&gt;image-q&lt;/code&gt; library). Wu's method is fast and keeps the colors that cover lots of pixels, which is what you want for stitching.&lt;/li&gt;
&lt;li&gt;Match each of those N colors to the nearest DMC floss. Two palette colors sometimes land on the same floss, and when that happens they merge.&lt;/li&gt;
&lt;li&gt;Assign every stitch to the nearest color in that small floss palette.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  3. "Nearest" means perceptual distance, not RGB distance
&lt;/h2&gt;

&lt;p&gt;Measuring color distance in RGB is a classic trap. Two colors can be close in RGB numbers and look clearly different to a person, or the other way round. So colors are converted from sRGB to &lt;strong&gt;CIE Lab&lt;/strong&gt; (D65 white point), a space designed so that distances roughly match what people see.&lt;/p&gt;

&lt;p&gt;There are two formulas in use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CIEDE2000&lt;/strong&gt; for choosing which floss represents each palette color. It's the most accurate standard color-difference formula, and it's also expensive. That's fine here, because it runs only N × ~500 times.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plain Lab distance (CIE76)&lt;/strong&gt; for assigning each of the tens of thousands of stitches to one of the N chosen floss. That step is fast, and with only a few dozen well-separated candidates the cheaper formula picks the same answer almost every time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you implement CIEDE2000 yourself, test it against the published test pairs in Sharma, Wu &amp;amp; Dalal (2005). The formula has several places where a sign or a hue-angle wrap goes wrong silently.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. No dithering
&lt;/h2&gt;

&lt;p&gt;In normal image work, dithering is how you hide a small palette: scatter pixels of two colors so the eye blends them. For stitching it's the worst possible choice. Every isolated pixel becomes a single stitch that needs its own start and stop of thread. So the converter deliberately does &lt;strong&gt;no dithering&lt;/strong&gt;, and you accept some banding in exchange for a pattern that's pleasant to stitch.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Count and clean up the "confetti"
&lt;/h2&gt;

&lt;p&gt;Even without dithering, photos (fur especially) produce lots of &lt;strong&gt;confetti&lt;/strong&gt;: single stitches whose eight neighbors are all other colors. This is the biggest complaint stitchers have about computer-generated patterns, so the tool measures it and shows it up front:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Easy to stitch · 19 colors · &amp;lt;1% confetti
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The count is simple: for every stitched cell, check its 8 neighbors; if none share its color, it's confetti. The percentage is confetti over stitched cells. From our own side-by-side tests, under about 5% feels calm to stitch and over about 12% means constant thread changes. These are working numbers, not science, so the raw percentage is always shown too.&lt;/p&gt;

&lt;p&gt;The basic clean-up is one pass of a majority rule. Each confetti stitch takes the color that most of its stitched neighbors share, but only if at least 3 of them share it, and the pass reads from the old grid and writes to a new one so the result doesn't depend on scan order. Stronger levels go further: they flood-fill small same-color patches (up to 2, 3 or 5 stitches touching edge to edge) and merge each patch into the color that borders it most. Afterwards, any floss that no longer has stitches is dropped, and the remaining ones are sorted by stitch count and given chart symbols in that order.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Skein counts from geometry
&lt;/h2&gt;

&lt;p&gt;A pattern also needs a shopping list. On fabric with &lt;code&gt;count&lt;/code&gt; holes per inch, a stitch has side &lt;code&gt;s = 2.54 / count&lt;/code&gt; cm. A full cross uses two diagonals on the front (&lt;code&gt;2√2·s&lt;/code&gt;) and roughly two sides on the back (&lt;code&gt;2·s&lt;/code&gt;). Add 50% for tails and travel between stitches. A DMC skein is 8 m of six strands, so stitched two strands at a time it gives 24 m of working thread.&lt;/p&gt;

&lt;p&gt;That works out to about 1,830 stitches per skein on 14-count with 2 strands, within 3% of the old needlework rule of thumb "stitches per skein = 255 × count ÷ strands" (1,785). It's reassuring when first principles agree with decades of stitchers' experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;The tool is &lt;a href="https://thecrossstitchpatternmaker.com/" rel="noopener noreferrer"&gt;The Cross Stitch Pattern Maker&lt;/a&gt;. Disclosure: I built it. You upload a photo, see the stitchability check before anything else, and get a symbol chart, a DMC floss list with skein counts and a printable PDF. The first PDF is free and there's no sign-up. If you only want the size and floss math, there's also a standalone &lt;a href="https://thecrossstitchpatternmaker.com/cross-stitch-calculator" rel="noopener noreferrer"&gt;cross stitch calculator&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I'd love to hear from anyone who has worked on palette reduction for physical media (beads, LEGO mosaics, knitting), because the "no dithering, fewer isolated pixels" trade-off shows up everywhere.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>algorithms</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Fitting a whole game build into a URL fragment, stamped with the patch it was made for</title>
      <dc:creator>Edinamar Luce</dc:creator>
      <pubDate>Sun, 15 Nov 2026 20:42:16 +0000</pubDate>
      <link>https://dev.to/edinamar_luce_770f4831715/fitting-a-whole-game-build-into-a-url-fragment-stamped-with-the-patch-it-was-made-for-o29</link>
      <guid>https://dev.to/edinamar_luce_770f4831715/fitting-a-whole-game-build-into-a-url-fragment-stamped-with-the-patch-it-was-made-for-o29</guid>
      <description>&lt;p&gt;I run a small fan-made build planner for Azeroth Survivors, a browser survivor game. The planner has no accounts and no database of builds. A build lives entirely in the URL, so "save" and "share" are the same action: copy the link.&lt;/p&gt;

&lt;p&gt;That constraint shaped two pieces of code I want to walk through: how a full build fits into a short URL fragment, and how a link made months ago tells you which parts of it a later game patch may have broken.&lt;/p&gt;

&lt;h2&gt;
  
  
  What has to fit in the link
&lt;/h2&gt;

&lt;p&gt;A build in this game is a class (nine of them), up to six skills, a set of skills you "banish" from the level-up pool, and talent points spread across three trees. Every class has 39 or 40 talents, each with a maximum rank of 5, and a full build spends exactly 51 points with 5 points needed per tier before the next tier opens.&lt;/p&gt;

&lt;p&gt;The naive version is JSON in a query string. It works, but a talent map alone is several hundred characters once it is URL-encoded, and the link breaks in chat apps that wrap long lines.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bit-packing the first format
&lt;/h2&gt;

&lt;p&gt;The first share format is a fixed byte layout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;byte 0      version (4 bits) | class id (4 bits)
bytes 1-6   one byte per skill slot: 0 = empty, else 1 + ability index
bytes 7-8   banished skills as a bit mask, in class ability order
byte 9...   talent ranks in class-data order, 3 bits each
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three bits cover ranks 0 to 7, which leaves headroom over the current maximum of 5. The talent block is written with a small &lt;code&gt;writeBits&lt;/code&gt; helper that walks a bit offset across the byte array, most significant bit first:&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;writeBits&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bitOffset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;number&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="n"&gt;i&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="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&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="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;bit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;position&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bitOffset&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;position&lt;/span&gt; &lt;span class="o"&gt;&amp;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="o"&gt;|=&lt;/span&gt; &lt;span class="n"&gt;bit&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;position&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mi"&gt;7&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="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;For a 40-talent class that is 9 fixed bytes plus 15 bytes of talents: 24 bytes, which base64url turns into 32 characters. The whole thing goes after &lt;code&gt;#p=&lt;/code&gt; in the fragment, so it never reaches a server log and the page stays fully static.&lt;/p&gt;

&lt;p&gt;Two details mattered more than I expected:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Indices, not names.&lt;/strong&gt; Skills are stored as offsets into the class's generated ability list. That keeps each slot to one byte, but it means the order of that list is part of the format. If a patch inserts a skill in the middle, old links point at the wrong skill. The rule in the code comment is blunt: bump the version whenever an offset or slot width changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The version nibble comes first.&lt;/strong&gt; The decoder reads byte 0, checks the version and class id, and returns &lt;code&gt;null&lt;/code&gt; on a mismatch instead of guessing. The page can also read just the class id from that first byte and fetch only that class's data module before decoding the rest.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The current format carries the game version
&lt;/h2&gt;

&lt;p&gt;The planner later grew per-stage plans (early, mid, late, endless), gear, run items, an optional race and a build name. That became format 8, which keeps the same bit-packing approach but adds a short metadata header: a length-prefixed name (up to 80 bytes) and a length-prefixed game version string (up to 32 bytes), followed by the four stage blocks.&lt;/p&gt;

&lt;p&gt;Putting the game version inside the link was the useful part. A build shared during one patch is stamped with that patch, for example &lt;code&gt;v0.143.3&lt;/code&gt;, and the planner knows which version its own data was last checked against (at the time of writing, data checked on 2026-11-13).&lt;/p&gt;

&lt;h2&gt;
  
  
  Telling the reader what a patch may have broken
&lt;/h2&gt;

&lt;p&gt;When a link opens, a function compares the stamped version with the current one and walks every stage of the build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;each talent id must still exist in the class data,&lt;/li&gt;
&lt;li&gt;each skill key must still exist, and a chosen upgrade path must still be one of that skill's options,&lt;/li&gt;
&lt;li&gt;each equipment and run-item key must still resolve.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Anything that does not resolve becomes an issue tagged with its stage and category, and the page shows a short report instead of silently dropping the choice. If the versions match, the report just confirms that every saved choice still resolves.&lt;/p&gt;

&lt;p&gt;What it deliberately does not do is pretend to know old numbers. The message for an older link says it can verify which choices still exist, but it cannot reconstruct values from a game build that is no longer available. That honesty is cheaper than shipping historical data for every patch, and it tells the player exactly where to look.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the tier list fits
&lt;/h2&gt;

&lt;p&gt;The same "show your date" rule applies to the &lt;a href="https://azerothsurvivorsbuilds.com/tier-list?utm_source=devto&amp;amp;utm_medium=article" rel="noopener noreferrer"&gt;tier list page&lt;/a&gt;. There is no official tier list or public win-rate data for this game, so the page grades all nine classes three ways (overall, first-clear safety, endgame clear speed), explains why each grade differs, and prints the game version and last-checked date at the top. It is an editorial call, labelled as one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;If your app's state is small and structured, a versioned bit layout in the URL fragment is a full persistence layer with zero backend.&lt;/li&gt;
&lt;li&gt;Store the data version inside the payload, not next to it.&lt;/li&gt;
&lt;li&gt;When old data cannot be trusted, list what changed instead of guessing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The planner is &lt;a href="https://azerothsurvivorsbuilds.com/?utm_source=devto&amp;amp;utm_medium=article" rel="noopener noreferrer"&gt;here&lt;/a&gt; if you want to see the links in action. It is free, has no sign-up, and is an unofficial fan project not affiliated with Blizzard Entertainment or the game's creator. Disclosure: this is my site, and this post was written by an AI agent working from the site's actual source code.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>webdev</category>
      <category>typescript</category>
      <category>gamedev</category>
    </item>
    <item>
      <title>Knitting charts aren't square: stitch-accurate grids and photo-to-colorwork in the browser</title>
      <dc:creator>Edinamar Luce</dc:creator>
      <pubDate>Sun, 15 Nov 2026 11:03:57 +0000</pubDate>
      <link>https://dev.to/edinamar_luce_770f4831715/knitting-charts-arent-square-stitch-accurate-grids-and-photo-to-colorwork-in-the-browser-def</link>
      <guid>https://dev.to/edinamar_luce_770f4831715/knitting-charts-arent-square-stitch-accurate-grids-and-photo-to-colorwork-in-the-browser-def</guid>
      <description>&lt;p&gt;Knitters have a geometry problem that most charting software ignores: a knitted stitch is not square. I build a browser-based knitting chart tool, and three small pieces of code did most of the work of getting charts right: a gauge-based cell aspect ratio, a photo-to-chart pipeline that thinks in stitches instead of pixels, and a row walker that turns the grid into written instructions. Here is how each one works.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Stitches are wider than they are tall
&lt;/h2&gt;

&lt;p&gt;Knitters measure &lt;em&gt;gauge&lt;/em&gt;: how many stitches and how many rows fit in 10 cm (4 in). A typical worsted-weight gauge is 18 stitches × 24 rows. That means one stitch is 10/18 ≈ 0.56 cm wide but only 10/24 ≈ 0.42 cm tall.&lt;/p&gt;

&lt;p&gt;If you draw a motif on square graph paper, every row is drawn taller than it will knit. A circle charted on squares knits up as a squashed oval, and lettering looks short and wide. The fix is to draw each cell at the stitch's real proportions:&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="cm"&gt;/** Height of one stitch relative to its width, from the gauge. */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;stitchAspect&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gauge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;stitches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;number&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="n"&gt;gauge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stitches&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;gauge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// layout&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;cellH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;proportional&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;cellW&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;stitchAspect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gauge&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cellW&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With 18 × 24, &lt;code&gt;stitchAspect&lt;/code&gt; is 0.75, so a 20 px wide cell is drawn 15 px tall. Everything else (hit testing, row numbers, outlines) has to use &lt;code&gt;cellW&lt;/code&gt; and &lt;code&gt;cellH&lt;/code&gt; separately instead of a single &lt;code&gt;cellSize&lt;/code&gt;:&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="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;px&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;layout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;left&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;layout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cellW&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="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;py&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;layout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;layout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cellH&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;It sounds trivial, but once cells are not square you find every place in the code that quietly assumed they were. There is still a toggle for square cells for people who prefer them, but the default is proportional.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Photo to chart: resample in stitches, not pixels
&lt;/h2&gt;

&lt;p&gt;The naive way to turn a photo into a colorwork chart is to shrink it to N × M pixels and posterize. That gets the proportions wrong for the same reason as above: if the chart is 60 stitches wide, the number of &lt;em&gt;rows&lt;/em&gt; has to come from the photo's aspect ratio &lt;strong&gt;and&lt;/strong&gt; the gauge:&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="n"&gt;rowsForStitches&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;img&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;HTMLImageElement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stitches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gauge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Gauge&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&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="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;stitches&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;naturalHeight&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;naturalWidth&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="n"&gt;gauge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;gauge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stitches&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="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A square photo at 60 stitches and 18 × 24 gauge becomes 60 × 80: more rows than stitches, because rows are shorter. Knit it and the result is square again.&lt;/p&gt;

&lt;p&gt;The rest of the pipeline runs on a canvas in the browser, so the photo never leaves the device:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Draw the image into a canvas sized &lt;code&gt;stitches × rows&lt;/code&gt; with &lt;code&gt;imageSmoothingQuality = 'high'&lt;/code&gt;. Each pixel is now one stitch.&lt;/li&gt;
&lt;li&gt;Build a palette with &lt;a href="https://github.com/ibezkrovnyi/image-quantization" rel="noopener noreferrer"&gt;image-q&lt;/a&gt; using Wu quantization and BT.709-weighted Euclidean distance. Wu is fast and deterministic, which matters when someone drags a "colors" slider and expects the same result each time.&lt;/li&gt;
&lt;li&gt;Map every pixel to its &lt;strong&gt;nearest&lt;/strong&gt; palette color, with no dithering.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That last point is knitting-specific. Dithering makes photos look better on screen, but in stranded colorwork every isolated stitch of a different color is a strand you have to carry and catch. A dithered chart is technically accurate and miserable to knit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cleaning up single stitches
&lt;/h3&gt;

&lt;p&gt;Even without dithering, quantized photos are full of lone stitches. A small cleanup pass looks at each stitch's four neighbours: if none of them share its color, and one color holds the majority (at least three of the four, or all of them at an edge), the stitch takes that color:&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;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;same&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;neighbours&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="c1"&gt;// keep stitches that touch their own color&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bestCount&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;neighbours&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;best&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running it twice catches the pairs the first pass exposes. Finally the palette is sorted by how many stitches use each color, so the most-used color becomes color A, the main color, and unused colors are dropped. Knitters expect A to be the background.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Turning the grid into written instructions
&lt;/h2&gt;

&lt;p&gt;Many knitters prefer written rows to a chart. Generating them is a run-length encoding problem with one twist: &lt;strong&gt;direction&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When knitting flat, right-side rows are worked from right to left across the chart, and wrong-side rows from left to right, with every stitch turned into its wrong-side equivalent (a knit seen from the back is a purl). In the round, every round reads right to left.&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="n"&gt;ws&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;isWrongSide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&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="n"&gt;xs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ws&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="p"&gt;.&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&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="p"&gt;:&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="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reverse&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then walk &lt;code&gt;xs&lt;/code&gt;, merge consecutive cells with the same stitch and color into a run, and format each run in standard abbreviations: &lt;code&gt;k5 A, k2 B, p3 A&lt;/code&gt;. Cells marked "no stitch" (used when stitch counts change across a chart) are skipped and not counted, so each row ends with its true stitch total, which is the first thing a knitter checks when something goes wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell anyone building tools for crafters
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Model the physical object, not the picture.&lt;/strong&gt; Gauge, direction of work and "no stitch" cells are the domain; pixels are an implementation detail.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optimise for the person holding the needles.&lt;/strong&gt; No dithering, a cleanup pass, and the main color as A are all choices that make the output worse as an image and better as a pattern.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep it local.&lt;/strong&gt; Everything above runs in the browser. People upload family photos to make sweaters; not sending those to a server is a feature.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can try all of this in &lt;a href="https://knittingchart.com/?utm_source=devto&amp;amp;utm_medium=article" rel="noopener noreferrer"&gt;Knitting Chart&lt;/a&gt;, the free tool I built: draw a chart in true stitch proportions, or use the &lt;a href="https://knittingchart.com/photo-to-knitting-chart?utm_source=devto&amp;amp;utm_medium=article" rel="noopener noreferrer"&gt;photo to knitting chart converter&lt;/a&gt; and download a PDF with the key and written rows. Disclosure: I made it, and it has an optional one-time paid upgrade for cloud sync; everything described in this post is in the free version.&lt;/p&gt;

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