Keith Vetter 2017-04-07 : Here's an updated version of my Celtic Braid page. That one demonstrated how to draw an interlocking figure which I later learned is called Solomon's Knot .
This page extends that one in allowing larger sized knots with more interlocking loops. It also draws a related knot called an Endless Knot .
This isn't PC - it should be called SoloPERSON's Knot!
##+########################################################################## # # Solomon's Knot.tcl -- Draws either Solomon Knot and Endless Knot with variable sizes # by Keith Vetter 2018-03-27 # # https://en.wikipedia.org/wiki/Solomon%27s_knot # https://en.wikipedia.org/wiki/Endless_knot # # Terminology # The figure is described by a path and is drawn as a number of cages # path: position_segment segment [segment]* # segment: dir len [dir len]* # cage: connected set of cells # cell: single 1x1 box, drawn with sides of length Z(unit) package require Tk set Z(unit) 50 set Z(bg,color) yellow set Z(edge,size) 5 set Z(edge,color) black set Z(row,size) 2 set Z(col,size) 2 set Z(row,color,0) navy set Z(row,color,1) turquoise set Z(col,color,0) seagreen1 set Z(col,color,1) green3 set Z(type) "Solomon's Knot" set Z(angle) 45 set S(title) "Solomon's and Endless Knot" set S(colors) {purple red magenta yellow green blue cyan white black random} set S(margin) 10 proc SolomonKnot {rows cols angle} { set unitRows [expr {3 + 4*$rows}] set unitCols [expr {3 + 4*$cols}] ComputeSize $unitRows $unitCols set rowColors [Gradient $::Z(row,color,0) $::Z(row,color,1) $rows] set colColors [Gradient $::Z(col,color,0) $::Z(col,color,1) $cols] .c delete all set paths [MakeSKPaths $rows $cols] for {set i 0} {$i < $rows} {incr i} { lassign [lrotate $rowColors] rowColor rowColors DrawPath [dict get $paths row,$i] knot $rowColor } for {set i 0} {$i < $cols} {incr i} { lassign [lrotate $colColors] colColor colColors DrawPath [dict get $paths col,$i] knot $colColor } RotateKnot $angle } proc EndlessKnot {rows cols angle} { set rows [expr {max(3, $rows)}] set cols [expr {max(3, $cols)}] set unitRows [expr {-3 + 4*$rows}] set unitCols [expr {-3 + 4*$cols}] ComputeSize $unitRows $unitCols set path [MakeEKPaths $rows $cols] set steps [expr {[llength $path] - 1}] set colors [Gradient3 $::Z(row,color,0) $::Z(row,color,1) $::Z(row,color,0) $steps] lappend colors {*}[lreverse $colors] .c delete all DrawPath $path knot $colors RotateKnot $angle } proc DoDisplay {} { image create bitmap ::img::star -data { #define plus_width 11 #define plus_height 9 static char plus_bits[] = { 0x00,0x00, 0x24,0x01, 0xa8,0x00, 0x70,0x00, 0xfc,0x01, 0x70,0x00, 0xa8,0x00, 0x24,0x01, 0x00,0x00 }} wm title . $::S(title) ::ttk::frame .cp -relief ridge -borderwidth 2 label .title -textvariable ::Z(type) -font {Helvetica 36 bold} canvas .c -width 500 -height 500 -bd 0 -highlightthickness 0 -bg $::Z(bg,color) . config -bg [.c cget -bg] .title config -bg [.c cget -bg] pack .cp -side right -fill y pack .title -side top -fill y # NB. we create a margin around the canvas via pack's -padx and -pady pack .c -side bottom -fill both -expand 1 -padx $::S(margin) -pady $::S(margin) ::ttk::labelframe .cp.type -text "Knot Type" ::ttk::radiobutton .cp.type.solomon -text "Solomon's Knot" \ -command Redraw -variable ::Z(type) -value "Solomon's Knot" ::ttk::radiobutton .cp.type.endless -text "Endless Knot" \ -command Redraw -variable ::Z(type) -value "Endless Knot" grid .cp.type -sticky ew grid .cp.type.solomon -sticky w grid .cp.type.endless -sticky w -pady .1i ::ttk::labelframe .cp.row -text "Row Configuration" ::ttk::label .cp.row.slbl -text "Size:" -anchor e ::ttk::spinbox .cp.row.sbox -from 1 -to 10 -command Redraw -textvariable ::Z(row,size) \ -width 4 -justify center -exportselection 0 set row0 [ColorWidget "First Color:" .cp.row.col0 row,color,0] set row1 [ColorWidget "Second Color:" .cp.row.col1 row,color,1] grid .cp.row.slbl .cp.row.sbox -sticky ew grid config .cp.row.sbox -sticky w grid {*}$row0 grid configure [lindex $row0 0] -sticky e grid {*}$row1 grid configure [lindex $row1 0] -sticky e grid .cp.row -sticky ew -pady .1i ::ttk::labelframe .cp.col -text "Column Configuration" ::ttk::label .cp.col.slbl -text "Size:" -anchor e ::ttk::spinbox .cp.col.sbox -from 1 -to 10 -command Redraw -textvariable ::Z(col,size) \ -width 4 -justify center -exportselection 0 set row0 [ColorWidget "First Color:" .cp.col.col0 col,color,0] set row1 [ColorWidget "Second Color:" .cp.col.col1 col,color,1] grid .cp.col.slbl .cp.col.sbox -sticky ew grid config .cp.col.sbox -sticky w grid {*}$row0 grid configure [lindex $row0 0] -sticky e grid {*}$row1 grid configure [lindex $row1 0] -sticky e grid .cp.col -sticky ew -pady .1i ::ttk::labelframe .cp.bg -text "Background Configuration" set row [ColorWidget "Color:" .cp.bg.col bg,color] grid {*}$row grid .cp.bg -sticky ew -pady .1i ::ttk::labelframe .cp.edge -text "Edge Configuration" ::ttk::label .cp.edge.slbl -text "Size:" -anchor e ::ttk::spinbox .cp.edge.sbox -from 0 -to 20 -command {Redraw edge} \ -textvariable ::Z(edge,size) -width 4 -justify center -exportselection 0 set row [ColorWidget "Color:" .cp.edge.col edge,color] grid .cp.edge.slbl .cp.edge.sbox -sticky ew grid config .cp.edge.sbox -sticky w grid {*}$row grid .cp.edge -sticky ew -pady .1i ::ttk::labelframe .cp.rotate -text "Rotation" scale .cp.rotate.rotate -from -180 -to 180 -command {Redraw rotate} \ -variable ::Z(angle) -orient horizontal -showvalue 0 -relief ridge pack .cp.rotate.rotate -side top grid .cp.rotate -sticky ew -pady .1i ::ttk::button .cp.about -text About -command About grid rowconfigure .cp 100 -weight 1 grid .cp.about -row 101 -pady .1i bind .c <Configure> {Configure %W %h %w} } proc ColorWidget {label f var} { ::ttk::label ${f}lbl -text $label -anchor e ::ttk::combobox ${f}cb -values $::S(colors) -state readonly \ -textvariable ::Z($var) -justify center -width 10 -exportselection 0 ::ttk::button ${f}btn -image ::img::star -command [list PickColor $var] UniqueTrace ::Z($var) NewColor return [list ${f}lbl ${f}cb ${f}btn] } proc Configure {W h w} { # Handle configure events, making 0,0 the center of the canvas set h [expr {$h / 2.0}] set w [expr {$w / 2.0}] $W config -scrollregion [list -$w -$h $w $h] Redraw } proc UniqueTrace {varName {function ""}} { # Adds a trace to a variable, removing any existing ones foreach tr [trace info variable $varName] { trace remove variable $varName {*}$tr } if {$function ne ""} { trace variable $varName w $function } } proc NewColor {var1 var2 op} { # Handle trace on combobox's variable (since it lacks a command option) if {$::Z($var2) eq "random"} { set ::Z($var2) [format "#%06x" [expr {int(rand() * 0xFFFFFF)}]] } if {$var2 eq "bg,color"} { .c config -bg $::Z(bg,color) . config -bg [.c cget -bg] .title config -bg [.c cget -bg] } elseif {$var2 eq "edge,color"} { if {$::Z(edge,size) > 0} { .c itemconfig knot -outline $::Z(edge,color) } } else { Redraw } } proc PickColor {var} { set color [tk_chooseColor -initialcolor $::Z($var)] if {$color eq ""} return set ::Z($var) $color # Redraw done by trace } proc ComputeSize {unitRows unitCols} { # Computes Z(unit) so image fits for all rotations set diag [expr {hypot($unitRows, $unitCols)}] set smallSide [expr {min([winfo height .c], [winfo width .c])}] set pixels [expr {$smallSide / $diag}] set ::Z(unit) [expr {int($pixels)}] } proc Redraw {args} { if {[lindex $args 0] eq "edge"} { set outline [expr {$::Z(edge,size) > 0 ? $::Z(edge,color) : ""}] .c itemconfig knot -width $::Z(edge,size) -outline $outline return } if {$::Z(type) eq "Solomon's Knot"} { SolomonKnot $::Z(row,size) $::Z(col,size) $::Z(angle) } else { set ::Z(row,size) [expr {max(3, $::Z(row,size))}] set ::Z(col,size) [expr {max(3, $::Z(col,size))}] EndlessKnot $::Z(row,size) $::Z(col,size) $::Z(angle) } .cp.rotate.rotate config -label "Angle: $::Z(angle)" } proc DrawPath {path tag colors} { # path is a list of segments; a segment is a list of dir len pairs # The first segment in path is to position from 0,0 and is not drawn set lastCell {0 0} set segments [lassign $path position] lassign [ProcessSegmentToPolygon $lastCell $position] . lastCell set outline [expr {$::Z(edge,size) > 0 ? $::Z(edge,color) : ""}] foreach segment $segments { lassign [ProcessSegmentToPolygon $lastCell $segment] xy lastCell lassign [lrotate $colors] color colors .c create poly $xy -fill $color -tag $tag -outline $outline -width $::Z(edge,size) # Move past the "underpass" cell set lastCell [MoveOneCell $lastCell [lindex $segment end-1]] } } proc ProcessSegmentToPolygon {lastCell segment} { # Converts segment into the XY coordinates of a polygon starting at lastCell # Builds up coordinates for the opposite sides of the polygon, then joins them # when done. set corners [CellToCorners {*}$lastCell] set lastDir [lindex $segment 0] set nextDir $lastDir set side1 {} set side2 {} lassign [ExtendCage $lastDir $nextDir $corners $side1 $side2] side1 side2 foreach {nextDir len} $segment { set cage [SegmentToCage $lastCell $nextDir $len] set corners [CageCorners $cage] lassign [ExtendCage $lastDir $nextDir $corners $side1 $side2] side1 side2 set lastDir $nextDir set lastCell [lindex $cage end] } set xy [concat {*}$side1 {*}[lreverse $side2] {*}[lindex $side1 0]] return [list $xy $lastCell] } array set EXTEND { n,n {{nw} {ne}} n,e {{ne} {ch sw se}} n,w {{ch se sw} {nw}} e,e {{ne} {se}} e,n {{ch sw nw} {ne}} e,s {{se} {ch nw sw}} s,s {{se} {sw}} s,e {{ch nw ne} {se}} s,w {{sw} {ch ne nw}} w,w {{sw} {nw}} w,n {{nw} {ch se ne}} w,s {{ch ne se} {sw}} } proc ExtendCage {lastDir nextDir corners side1 side2} { set sides(side1) $side1 set sides(side2) $side2 foreach steps $::EXTEND($lastDir,$nextDir) side {side1 side2} { foreach step $steps { if {$step eq "ch"} { set sides($side) [lrange $sides($side) 0 end-1] } else { lappend sides($side) [dict get $corners $step] } } } return [list $sides(side1) $sides(side2)] } proc CellToCorners {row col} { # Returns a dictionary of the four corners of a cell set x0 [expr {$col * $::Z(unit) - $::Z(unit)/2}] set y0 [expr {$row * $::Z(unit) - $::Z(unit)/2}] set x1 [expr {$x0 + $::Z(unit)}] set y1 [expr {$y0 + $::Z(unit)}] set d [dict create \ nw [list $x0 $y0] \ ne [list $x1 $y0] \ sw [list $x0 $y1] \ se [list $x1 $y1]] return $d } proc SegmentToCage {lastCell dir len} { lassign $lastCell row col if {$dir eq "n"} { set box [list -1 0] } if {$dir eq "s"} { set box [list +1 0] } if {$dir eq "e"} { set box [list 0 +1] } if {$dir eq "w"} { set box [list 0 -1] } set row0 [expr {$row + [lindex $box 0]}] set col0 [expr {$col + [lindex $box 1]}] set result {} for {set i 0} {$i < $len} {incr i} { incr row [lindex $box 0] incr col [lindex $box 1] lappend result [list $row $col] } return $result } proc MoveOneCell {lastCell dir} { return [lindex [SegmentToCage $lastCell $dir 1] 0] } proc CageCorners {cage} { lassign [Cage2XY $cage] x0 y0 x1 y1 set d [dict create \ nw [list $x0 $y0] \ ne [list $x1 $y0] \ sw [list $x0 $y1] \ se [list $x1 $y1]] return $d } proc Cage2XY {cage} { lassign [Cell2XY [lindex $cage 0]] x0 y0 x1 y1 foreach cell [lrange $cage 1 end] { lassign [Cell2XY $cell] x_0 y_0 x_1 y_1 set x0 [expr {min($x0, $x_0)}] set x1 [expr {max($x1, $x_1)}] set y0 [expr {min($y0, $y_0)}] set y1 [expr {max($y1, $y_1)}] } return [list $x0 $y0 $x1 $y1] } proc Cell2XY {cell} { set corners [CellToCorners {*}$cell] return [concat [dict get $corners nw] [dict get $corners se]] } proc MakeSKPaths {rows cols} { # Create a dictionary of paths for the Solomon's Knot global paths unset -nocomplain paths set rows1 [expr {$rows - 1}] set cols1 [expr {$cols - 1}] set midRows [expr {(3 + 4*$rows)/2}] set midCols [expr {(3 + 4*$cols)/2}] set toNWcol [list n $midRows w $midCols] set toNWrow [list w $midCols n $midRows] set topCap {n 2 e 2 s 3} set bottomCap {s 2 w 2 n 3} set leftCap {w 2 n 2 e 3} set rightCap {e 2 s 2 w 3} set down [lrepeat $rows1 {s 3}] set up [lrepeat $rows1 {n 3}] set right [lrepeat $cols1 {e 3}] set left [lrepeat $cols1 {w 3}] for {set i 0} {$i < $cols} {incr i} { # Column weave set offset [expr {2 + $i * 4}] set position [list {*}$toNWcol s 2 e $offset] set path [list $position $topCap {*}$down $bottomCap {*}$up] lappend paths col,$i $path } for {set i 0} {$i < $rows} {incr i} { # Row weave set offset [expr {4 + $i * 4}] set position [list {*}$toNWrow e 2 s $offset] set path [list $position $leftCap {*}$right $rightCap {*}$left] lappend paths row,$i $path } return $paths } proc MakeEKPaths {rows cols} { # Create the path for the Endless Knot global path set midRows [expr {(-3 + 4*$rows)/2}] set midCols [expr {(-3 + 4*$cols)/2}] set toNW [list w $midCols n $midRows] set position [list {*}$toNW e 2 s 2] set topLeft {n 2 w 2 s 2 e 3} set hAdjust [lrepeat [expr {$cols-3}] {e 3}] set horizontal [list {*}$hAdjust {e 4 s 2 w 1} \ {*}[lrepeat [expr {$cols-2}] {w 3}] \ {w 2 s 2 e 3}] set bottomRight {e 4 s 2 w 2 n 1} set vAdjust [lrepeat [expr {$rows-3}] {s 3}] set vertical [list {*}[lrepeat [expr {$rows-2}] {n 3}] \ {n 2 w 2 s 3} \ {*}$vAdjust \ {s 4 w 2 n 1}] set path {} lappend path $position lappend path $topLeft for {set row 2} {$row < $rows} {incr row} { lappend path {*}$horizontal } lappend path {*}$hAdjust lappend path $bottomRight for {set col 2} {$col < $cols} {incr col} { lappend path {*}$vertical } lappend path {*}[lrepeat [expr {$rows-2}] {n 3}] return $path } proc Gradient {fromColor toColor steps} { # Creates gradient fromColor -> toColor lassign [winfo rgb . $fromColor] r1 g1 b1 lassign [winfo rgb . $toColor] r2 g2 b2 set steps [expr {$steps <= 1 ? 1 : double($steps - 1)}] set gradient {} for {set step 0} {$step <= $steps} {incr step} { set r [expr {int(($r2 - $r1) * $step / $steps + $r1) * 255 / 65535}] set g [expr {int(($g2 - $g1) * $step / $steps + $g1) * 255 / 65535}] set b [expr {int(($b2 - $b1) * $step / $steps + $b1) * 255 / 65535}] lappend gradient [format "#%.2x%.2x%.2x" $r $g $b] } return $gradient } proc Gradient3 {color0 color1 color2 steps} { # Creates gradient from color0 -> color1 -> color2 set first [expr {($steps + 1) / 2}] set second [expr {$steps - $first + 2}] set gradient1 [Gradient $color0 $color1 $first] set gradient2 [Gradient $color1 $color2 $second] set gradient [concat $gradient1 [lrange $gradient2 1 end-1]] return $gradient } proc lrotate {l} { set rest [lassign $l first] return [list $first [concat $rest $first]] } proc About {} { set msg "Solomon's and Endless Knot\nby Keith Vetter\nApril, 2018\n\n" append msg "The Solomon's Knot is also known as sigillum Salomis, Foundation Knot, Imbolo " append msg "or Nodo di Salomone. " append msg "It has been found in ancient Roman mosaics, on central Asian prayer rugs, " append msg "and on textiles of the Kuba people of Congo.\n\n" append msg "The Endless Knot or eternal knot is a symbolic knot and one of the Eight " append msg "Auspicious Symbols. It is an important cultural marker in places influenzed by " append msg "Tibetan Buddhism, such as Tibet, Mongolia, Tuva, Kalmykia. Technically it is a " append msg "7\u2084 knot." tk_messageBox -message $msg } proc RotateKnot {angle} { if {($angle % 360) != 0} { RotateItem .c knot 0 0 $angle } } proc RotateItem {w tagOrId Ox Oy angle} { set angle [expr {$angle * atan(1) * 4 / 180.0}] ;# Radians set cos [expr {cos($angle)}] set sin [expr {sin($angle)}] foreach id [$w find withtag $tagOrId] { ;# Do each component separately set xy {} foreach {x y} [$w coords $id] { # rotates vector (Ox,Oy)->(x,y) by angle clockwise set x [expr {$x - $Ox}] ;# Shift to origin set y [expr {$y - $Oy}] set xx [expr {$x * $cos - $y * $sin}] ;# Rotate set yy [expr {$x * $sin + $y * $cos}] set xx [expr {$xx + $Ox}] ;# Shift back set yy [expr {$yy + $Oy}] lappend xy $xx $yy } $w coords $id $xy } } DoDisplay update Redraw return