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
- Stroke geometry — dashes, outlines, and start points as real paths
- Sashiko (this post) — running stitches from binary sequences
- Leathercraft — stitch holes that can't disagree
- Stencils — bridges are just gaps
- 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 usehash01, 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`;
}
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`;
}
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`;
}
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`;
}
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)`;
}
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.