Sashiko: Running Stitches from Binary Sequences

Part 2 of 5 in Broken Lines — projects that treat the stroke not as paint, but as geometry you can hold.

Series: Broken Lines

  1. Stroke geometry — dashes, outlines, and start points as real paths
  2. Sashiko (this post) — running stitches from binary sequences
  3. Leathercraft — stitch holes that can't disagree
  4. Stencils — bridges are just gaps
  5. What Broken Lines taught the language — the friction log, resolved

Prerequisites: Part 1's dash() — partitioning a path into dash and gap pieces — is the whole toolkit here. The weave and patch examples also use hash01, the deterministic hash that turns an index into a repeatable 0-to-1 value.

What it does

Sashiko is Japanese running-stitch embroidery: even stitches, even spaces, white thread on indigo cloth. It began as mending — rural workwear reinforced stitch by stitch until reinforcement became a decorative tradition of its own — which is why this post ends with a patch. Look at one stitched line the way a needle does and it is exactly a dash pattern — thread on top of the fabric where the dashes are, thread passing underneath where the gaps are. dash() isn't simulating the craft; the craft and the method describe the same thing.

(And if you'll never hold a needle: what follows is also just a deterministic, seedable, tileable pattern generator with a two-integer control surface — the craft is the story, not a requirement.)

That identity means one honest caveat up front: everything on this page is template generation — where the stitches should go — not stitching. Sashiko's soul is the hand process. What Pathogen offers is the part stitchers already do on paper: gridding, counting, and transferring, at exact pitch.

Why you'd use it

The pattern family this post builds, hitomezashi, is a small miracle of emergence. Every horizontal line carries the same stitch pattern; every vertical line does too. The only decision, per line, is a single bit: start on a stitch, or start on a space. Crosses, steps, and interlocking blocks appear where the bits interact — nobody draws them. It's a known math-art crossover, and it lives one stroke-dashoffset away from part 1.

Example 1 — Both sides of the cloth

One seam, one dash array, shown from both sides of the cloth: the front is the dashes, and the reverse is the inverse pattern — the same thread surfacing where the front has gaps. Same array, one if on kind. One craft detail worth copying: the seam length is chosen so the run starts and ends on a stitch — stitchers plan a seam to come out even, and here that's one number.

//-- A running stitch IS a dash pattern: thread on top of the fabric //-- (the dashes), thread passing underneath (the gaps). dash() gives //-- both sides of the cloth. define ViewBox(0, 0, 480, 170); let fabric = PathLayer('fabric') #{ fill: #1e3a5f; stroke: none; }; fabric.apply { rect(0, 0, 480, 170); } let scene = GroupLayer('scene') #{}; let underside = PathLayer('underside') #{ stroke: #64748b; stroke-width: 3; stroke-linecap: round; fill: none; }; let topThread = PathLayer('top-thread') #{ stroke: #f1f5f9; stroke-width: 3.5; stroke-linecap: round; fill: none; }; let labels = TextLayer('labels') #{ font-family: monospace; font-size: 10; fill: #cbd5e1; }; scene.append(underside, topThread, labels); // 371 = 17 full cycles + one last stitch: the seam starts AND ends on a // stitch, the way a stitcher plans it. let seam = @{ h 371 }; let stitches = seam.dash(#{ stroke-dasharray: 14 7; }); for (stitch in stitches) { if (stitch.kind == 'dash') { topThread.apply { M 55 70 stitch.path.draw() } } else { underside.apply { M 55 112 stitch.path.draw() } } } labels.apply { text(55, 44)`front — thread on top: the dashes`; text(55, 140)`reverse — the inverse pattern: the gaps`; } A running stitch is a dash pattern: 14 on top, 7 underneath.

Example 2 — One bit per row

Example 1's 14-on, 7-under is the classic sashiko proportion — the stitch on top runs longer than the thread beneath. Hitomezashi is the special case where stitch and space are equal, and that equality is exactly what lets a half-cycle shift make neighboring rows interlock. Which is hitomezashi's entire control surface: shift a row's pattern by half a cycle, or don't. Six rows, six bits, stroke-dashoffset: calc(bit * 16). Where neighboring rows disagree, the stitches interlock.

//-- Hitomezashi's whole secret: every row is the same stitch pattern, //-- shifted — or not — by half a cycle. One bit per row. define ViewBox(0, 0, 480, 210); let fabric = PathLayer('fabric') #{ fill: #1e3a5f; stroke: none; }; fabric.apply { rect(0, 0, 480, 210); } let scene = GroupLayer('scene') #{}; let thread = PathLayer('thread') #{ stroke: #f1f5f9; stroke-width: 3; stroke-linecap: round; fill: none; }; let labels = TextLayer('labels') #{ font-family: monospace; font-size: 10; fill: #cbd5e1; }; scene.append(thread, labels); let seam = @{ h 336 }; let rowBits = [ 0, 1, 0, 0, 1, 1, ]; for ([bit, rowIndex] in rowBits) { let rowY = calc(52 + rowIndex * 22); let stitches = seam.dash(#{ stroke-dasharray: 16 16; stroke-dashoffset: calc(bit * 16); }); for (stitch in stitches) { if (stitch.kind == 'dash') { thread.apply { M 96 rowY stitch.path.draw() } } } labels.apply { text(56, calc(rowY + 3))`bit ${bit}`; } } labels.apply { text(96, 30)`stroke-dashoffset: bit * 16 — half a cycle, or nothing`; } The same seam six times — the only difference is one bit of dash offset.

Example 3 — The full weave

Now both directions: sixteen rows and thirty columns, each line's bit drawn from hash01. round(hash01(rowIndex, 7)) is a repeatable coin flip — a lookup, not a roll: the same index and seed produce the same bit, every compile. Rows and columns draw from two different seeds because the streams must be independent — a single seed would hand row i and column i the same bit and print a diagonal symmetry into the cloth. The stitch length equals the grid cell, so horizontal and vertical stitches meet at corners — and the crosses, steps, and boxes are all emergent.

//-- Full hitomezashi: horizontal rows and vertical columns of the same //-- stitch, each line phase-shifted by its own bit. The blocks and //-- steps are emergent — nobody drew them. define ViewBox(0, 0, 480, 300); let fabric = PathLayer('fabric') #{ fill: #1e3a5f; stroke: none; }; fabric.apply { rect(0, 0, 480, 300); } let scene = GroupLayer('scene') #{}; let thread = PathLayer('thread') #{ stroke: #f1f5f9; stroke-width: 2.5; stroke-linecap: round; fill: none; }; let labels = TextLayer('labels') #{ font-family: monospace; font-size: 10; fill: #cbd5e1; }; scene.append(thread, labels); let cell = 12; let cols = 30; let rows = 16; let originX = 60; let originY = 62; let rowSeam = @{ h calc(cols * cell) }; let colSeam = @{ v calc(rows * cell) }; fn sew(seam, anchorX, anchorY, bit) { let stitches = seam.dash(#{ stroke-dasharray: ${cell} ${cell}; stroke-dashoffset: calc(bit * cell); }); for (stitch in stitches) { if (stitch.kind == 'dash') { thread.apply { M anchorX anchorY stitch.path.draw() } } } } for (rowIndex in 0..rows) { let bit = round(hash01(rowIndex, 7)); sew(rowSeam, originX, calc(originY + rowIndex * cell), bit); } for (colIndex in 0..cols) { let bit = round(hash01(colIndex, 11)); sew(colSeam, calc(originX + colIndex * cell), originY, bit); } labels.apply { text(60, 36)`one bit per line — the pattern is emergent`; } hash01 decides each line's phase; the geometry decides everything else.

The sew helper is the post's whole engine — dash the seam, keep the dashes, place them at the line's anchor:

fn sew(seam, anchorX, anchorY, bit) {
  let stitches = seam.dash(#{
    stroke-dasharray: ${cell} ${cell};
    stroke-dashoffset: calc(bit * cell);
  });
  for (stitch in stitches) {
    if (stitch.kind == 'dash') {
      thread.apply {
        M anchorX anchorY stitch.path.draw()
      }
    }
  }
}

The style block reads the cell size from a variable, so pitch is one knob.

Example 4 — Stitches on curves

Nothing about a running stitch requires a grid. dash() measures distance along the curve itself, not the straight line across it, so stitches follow curves at even pitch for free — here, staggered fans of stitched arcs make the classic seigaiha wave.

//-- Stitches follow curves as readily as grids: dash() measures along //-- arc length. Staggered fans of stitched arcs — the seigaiha wave. define ViewBox(0, 0, 480, 260); let fabric = PathLayer('fabric') #{ fill: #1e3a5f; stroke: none; }; fabric.apply { rect(0, 0, 480, 260); } let scene = GroupLayer('scene') #{}; let thread = PathLayer('thread') #{ stroke: #f1f5f9; stroke-width: 2.5; stroke-linecap: round; fill: none; }; let labels = TextLayer('labels') #{ font-family: monospace; font-size: 10; fill: #cbd5e1; }; scene.append(thread, labels); fn stitchArc(radius, anchorX, anchorY) { let arcSpan = @{ m calc(0 - radius) 0 a radius radius 0 0 1 calc(radius * 2) 0 }; let stitches = arcSpan.dash(#{ stroke-dasharray: 7 4; }); for (stitch in stitches) { if (stitch.kind == 'dash') { thread.apply { M anchorX anchorY stitch.path.draw() } } } } let radii = [ 42, 30, 18, ]; for (rowIndex in 0..3) { let fanY = calc(84 + rowIndex * 42); let stagger = calc(rowIndex % 2 * 42); for (fanIndex in 0..3) { let fanX = calc(94 + stagger + fanIndex * 84); for (radius in radii) { stitchArc(radius, fanX, fanY); } } } labels.apply { text(94, 30)`same dash(), measured along the curve`; } Three arcs per fan, dashed at 7-and-4; the stagger makes the wave.

Example 5 — The mend patch

The finished artifact: a patch template at real pitch — one cell is 6 mm, a common sashiko stitch length — with the seeds recorded on the template itself, so any print of it carries its own provenance. The stitched field is 138 × 90 mm (5.4 × 3.5 in) — a jeans-knee size — and the patch outline adds a 6 mm margin around it. Print it scaled so the stitched field measures 138 mm across — measure, don't trust a percentage — then transfer and stitch.

//-- The finished artifact: a mend-patch template at real stitch pitch. //-- One cell = 6 mm; print at 200% and the grid is true to the iron. define ViewBox(0, 0, 480, 320); let denim = PathLayer('denim') #{ fill: #27466b; stroke: none; }; denim.apply { rect(0, 0, 480, 320); } let scene = GroupLayer('scene') #{}; let patch = PathLayer('patch') #{ fill: #16304f; stroke: #94a3b8; stroke-width: 1; }; let thread = PathLayer('thread') #{ stroke: #f1f5f9; stroke-width: 2.5; stroke-linecap: round; fill: none; }; let labels = TextLayer('labels') #{ font-family: monospace; font-size: 10; fill: #cbd5e1; }; scene.append(patch, thread, labels); let cell = 12; let cols = 23; let rows = 15; let panelX = 96; let panelY = 66; let inset = 12; patch.apply { roundRect(calc(panelX - inset), calc(panelY - inset), calc(cols * cell + inset * 2), calc(rows * cell + inset * 2), 10); } let rowSeam = @{ h calc(cols * cell) }; let colSeam = @{ v calc(rows * cell) }; fn sew(seam, anchorX, anchorY, bit) { let stitches = seam.dash(#{ stroke-dasharray: ${cell} ${cell}; stroke-dashoffset: calc(bit * cell); }); for (stitch in stitches) { if (stitch.kind == 'dash') { thread.apply { M anchorX anchorY stitch.path.draw() } } } } for (rowIndex in 0..rows) { let bit = round(hash01(rowIndex, 3)); sew(rowSeam, panelX, calc(panelY + rowIndex * cell), bit); } for (colIndex in 0..cols) { let bit = round(hash01(colIndex, 5)); sew(colSeam, calc(panelX + colIndex * cell), panelY, bit); } labels.apply { text(84, 34)`hitomezashi mend patch — 1 cell = 6 mm stitch pitch`; text(84, 296)`rows seeded hash01(row, 3), columns hash01(col, 5)`; } A reproducible patch — open it in the playground and change either seed for a different fabric of blocks.

Where to go next

Everything here was one method — the dash() reference covers the rest of its surface. The stitches here were centerlines — ink where thread goes. The next post moves to leather, where a stitch is a hole: the dashes get shorter, outline() turns them into punchable dots, and two mating seams have to agree hole-for-hole.