Celtic knotwork
This is the kolam line given width, plus a rule about which cord passes over which.
A plait is one line, or a few lines, running at 45 degrees between the dots of a field. The edge of the field turns the line, and so does any wall placed between two dots. Wherever two passes meet, one goes over and the other goes under. The whole design holds together because that choice alternates. Follow any cord and it goes over, under, over, under, the whole way around. A knot drawn that way is called alternating, and that is what the eye reads as woven rather than as a heap of lines.
the two rules, and nothing else:
over/under alternates along every cord -> it reads as woven
a wall turns the line where it stands -> the plait becomes a knot
The bands come back already broken where they dive under, so stroking them draws the weave. You have nothing to mask, and there is no draw order to get right.
Contents
knotwork
knotwork(in bounds: Rectangle? = nil,
columns: Int, rows: Int,
mirrors: [Kolam.Mirror] = []) -> Knotwork
This returns a knot woven over a columns × rows field of dots that fills bounds, which is the whole canvas by default. Nothing here is random, so the same field always weaves the same knot.
let knot = knotwork(columns: 7, rows: 5)
strokeCap(.round); strokeJoin(.round)
for band in knot.bands(gap: 30) {
let line = band.smoothed(iterations: 3)
withState {
stroke(.black); strokeWeight(22) // the outline
drawPolyline(line.points, closed: line.isClosed)
stroke(.white); strokeWeight(15) // the cord inside it
drawPolyline(line.points, closed: line.isClosed)
}
}
The withState around each band keeps the second color from becoming the next band's outline. Drawing one band at a time, outline first and then cord, matters too. It lets the next band's outline cut the last one cleanly where it passes over.
drawKnotwork
drawKnotwork(in bounds: Rectangle? = nil,
columns: Int, rows: Int,
mirrors: [Kolam.Mirror] = [],
weight: Double = 18,
cordColor: Color = .white,
rounding: Int = 3)
This draws the two-tone band above in one call. The current stroke is the outline at weight, cordColor fills it, and the gap is sized to the band.
The faces
bands(gap:)gives the visible pieces, one openContourper piece, already broken where the cord dives under.gapis how much cord is taken out at each dive, in canvas points. Left to itself it is a little over half the distance between two crossings, which suits a band about as wide as that gap. A cord that dives nowhere comes back whole and closed, because a single dot's loop has nothing to cross.cordsgives the whole cords, unbroken, one closedContoureach. This is the same line work the kolam gives, so draw it when you do not want the weave.crossingsgives the points where two passes meet. A plain field ofrowsbycolumnsdots has2 × rows × columns - rows - columnsof them. That is every point of the field except the ones on the outside edge, where the line turns and only one pass ever arrives. Each wall takes one more away.
Walls
Walls are Kolam.Mirror values, and they work exactly as they do for a kolam. Each one is a short wall between two neighboring dots, and the line turns at it. Walls are what turn a plait into a knot with a shape. A wall joins or splits a cord, and it also takes away the crossing that would have been there.
let knot = knotwork(columns: 6, rows: 6,
mirrors: [.rightOf(column: 2, row: 2), .below(column: 2, row: 2),
.rightOf(column: 2, row: 3), .below(column: 3, row: 2)])
Related: Kolam and sona is the same walk drawn as a single line. For the Grid, Contour, and shape booleans that the bands use, see Geometry. To take the bands to a pen plotter, see Fabrication.