Dev.to WebDev 🛠 Dev 👁 0 📖 4 min read

Knitting charts aren't square: stitch-accurate grids and photo-to-colorwork in the browser

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 rig

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.

1. Stitches are wider than they are tall

Knitters measure gauge: 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.

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:

/** Height of one stitch relative to its width, from the gauge. */
export const stitchAspect = (gauge: { stitches: number; rows: number }) =>
  gauge.stitches / gauge.rows;

// layout
const cellH = proportional ? cellW * stitchAspect(chart.gauge) : cellW;

With 18 × 24, stitchAspect is 0.75, so a 20 px wide cell is drawn 15 px tall. Everything else (hit testing, row numbers, outlines) has to use cellW and cellH separately instead of a single cellSize:

const x = Math.floor((px - layout.left) / layout.cellW);
const y = Math.floor((py - layout.top) / layout.cellH);

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.

2. Photo to chart: resample in stitches, not pixels

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 rows has to come from the photo's aspect ratio and the gauge:

export const rowsForStitches = (img: HTMLImageElement, stitches: number, gauge: Gauge) =>
  Math.max(1, Math.round(
    stitches * (img.naturalHeight / img.naturalWidth) * (gauge.rows / gauge.stitches)
  ));

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.

The rest of the pipeline runs on a canvas in the browser, so the photo never leaves the device:

  1. Draw the image into a canvas sized stitches × rows with imageSmoothingQuality = 'high'. Each pixel is now one stitch.
  2. Build a palette with image-q 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.
  3. Map every pixel to its nearest palette color, with no dithering.

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.

Cleaning up single stitches

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:

if (same || neighbours < 2) continue;      // keep stitches that touch their own color
if (bestCount >= Math.min(3, neighbours)) out[i] = best;

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.

3. Turning the grid into written instructions

Many knitters prefer written rows to a chart. Generating them is a run-length encoding problem with one twist: direction.

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.

const ws = isWrongSide(chart, y);
const xs = ws ? [...Array(width).keys()] : [...Array(width).keys()].reverse();

Then walk xs, merge consecutive cells with the same stitch and color into a run, and format each run in standard abbreviations: k5 A, k2 B, p3 A. 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.

What I'd tell anyone building tools for crafters

  • Model the physical object, not the picture. Gauge, direction of work and "no stitch" cells are the domain; pixels are an implementation detail.
  • Optimise for the person holding the needles. 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.
  • Keep it local. Everything above runs in the browser. People upload family photos to make sweaters; not sending those to a server is a feature.

You can try all of this in Knitting Chart, the free tool I built: draw a chart in true stitch proportions, or use the photo to knitting chart converter 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.

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.