Skip to content

Under the hood

One line at a time, greedily.

There is no neural network here and no cloud. The whole thing is a loop that asks the same question a few thousand times: from where the thread is now, which single nail would remove the most remaining darkness?

Line0 / 2,400Nail001

Step one

Turn the photo into a debt.

The cropped square is reduced to a 500-pixel working image and converted to luma. The tones are stretched between the 1st and 99th percentile, a mild gamma darkens the midtones, and everything outside the board outline is set to white so no line ever tries to help there.

What comes out is a residual map: for every pixel, how much darkness the image still owes. Thread only ever adds darkness, so the whole job is paying that debt down.

Very dark photographs would demand more darkness than a board can physically hold, so if the mean debt is above 150 out of 255 the whole map is scaled down. That is why an under-exposed photo comes back flatter than you expected rather than as a black disc.

nowbest
Every candidate chord is scored by walking its pixels and averaging what is still owed. The highest average wins. With 240 nails that is a couple of hundred candidates per line, each a few hundred pixels — cheap enough to run on a phone.

Step two

Pick, subtract, repeat.

01

Score every candidate

Walk the pixels along each possible chord with a DDA line and average the residual. No pixel cache, which keeps memory flat on a phone.

02

Penalise over-darkening

Where the residual has already gone negative, the pixel counts double against the score. Without that, tones smear and light areas fill in.

03

Refuse silly chords

No chord shorter than a tenth of the ring, no nail pair reused, no nail revisited within the last 24 moves. These stop the thread rattling around one arc.

04

Subtract and move on

The chosen chord is subtracted from the residual at its physical weight, the thread is now at that nail, and the loop runs again.

Where the weight comes from

Every chord subtracts a fixed amount from the residual, and that amount is not a taste knob — it is derived from the thread. A 0.15 mm thread on a 533 mm board rendered at 500 pixels covers 0.14 of a pixel, so it can only darken that pixel by so much:

weight = k × 255 × threadmm × S ÷ boardmmk = 0.36 light · 0.50 balanced · 0.68 intense

k = 0.5 was calibrated against the rendered output rather than guessed. Change the thread thickness or the board size in the studio and the whole solve changes with it, because the physics did.

On Auto, the loop stops when the best available chord would remove less than 0.7 of one weight — the point where an extra line stops reducing error and starts adding grey. There is a hard cap of 4,000 lines so a pathological photo cannot run away with your afternoon.

The rendering constraint

Every chord is stroked separately.

This sounds like an implementation detail. It is the difference between a portrait and a flat grey disc.

Canvas paints a path once, as a union. Batch three thousand overlapping segments into a single path and every crossing gets painted at the same alpha as a single thread — so nothing ever gets darker, which is the one thing string art depends on. Safari showed this immediately; desktop Chrome hid it on large paths, which made it a genuinely nasty bug.

So the renderer here calls beginPath() and stroke() once per chord, several thousand times. It is slower and it is correct. If you ever fork this and the preview goes flat, that is what you changed.

Colour

CMYK mode splits the image into yellow, cyan, magenta and black targets, solves each one separately with a share of the line budget proportional to how much of that colour the image contains, and previews them multiplied together. You wind them in that order — light to dark — because the last thread laid sits on top.

What it is not

  • Not optimal. Greedy algorithms take the best step now, not the best sequence overall. A globally optimal solution exists and is far more expensive to find; the difference is not visible on a board.
  • Not face-aware. "Favour the centre" is a radial weight, not face detection. It helps because faces are usually in the middle.
  • Not deterministic across settings. Nail count, board size and thread thickness all feed the same equation. Change one and expect a different sequence.

How it was tuned

The constants were not chosen by eye. A harness renders each solve, blurs it to the resolution a human eye resolves at viewing distance, and measures RMSE against the target. Tuning the over-darkening penalty, the gamma and the stop threshold moved that error from 44 to 29 on a cat photo and from 49 to 28 on a dark portrait.

That harness is the reason the numbers on this page are specific. It is also the reason they should not be changed casually.

Ten seconds is a cheap experiment.