ZB 2023-06-26 Inspiration was the image on this page: https://pl.pinterest.com/pin/461759768057576269/
Jeff Smith 2023-07-16 : Below is an online demo using CloudTk. This demo runs "Fun with canvas 2" in an Alpine Linux Docker Container. It is a 27.4MB image which is made up of Alpine Linux + tclkit + Fun-with-canvas-2.kit + libx11 + libxft + fontconfig + ttf-linux-libertine. It is run under a user account in the Container. The Container is restrictive with permissions for "Other" removed for "execute" and "read" for certain directories.
ZB 2023-07-16 It works very good indeed. I hope soon TCL/Tk replaces Java(script) on WWW pages. ;)
#!/usr/bin/env tclsh
package require Tk
# We're going to use "expr" a lot, so let's suppress that verbosity a little
# ("m" stands for "math") -- on a second thought: also "lindex --> l"
rename expr m
rename lindex l
proc in { li me } { return [m {([lsearch $li $me] < 0) ? 0 : 1}] }
proc ni { li me } { return [m {([lsearch $li $me] < 0) ? 1 : 0}] }
proc firstFont {fontlist default} {
set avail {}
foreach f [font families] { lappend avail [string tolower $f] }
foreach try $fontlist { if { [in $avail [string tolower $try]] } {
return $try
}
}
return $default
}
# Having x1, y1, x2, y2 calculate circle's center coordinates and its radius
proc box2cir { x1 y1 x2 y2 } {
set radius [m {($x2 - $x1)/2}]
set xc [m {($x1+$x2)/2}]
set yc [m {($y1+$y2)/2}]
return [list $xc $yc $radius]
}
# Having center's x, y and radius calculate x1, y1, x2, y2 of the circle
proc circ2box { xc yc radius } {
set x1 [m {$xc-$radius}]
set x2 [m {$xc+$radius}]
set y1 [m {$yc+$radius}]
set y2 [m {$yc-$radius}]
return [list $x1 $y1 $x2 $y2]
}
# Cartesian to polar
proc c2p { x y } {
set r [m {sqrt($x * $x + $y * $y)}]
set a [m {atan($y / $x)}]
if { $x < 0 || $y < 0 } { set a [m {$a + 3.14159}] }
if { $x < 0 && $y < 0 } { set a [m {$a + 3.14159}] }
return [list $a $r]
}
# Polar to Cartesian
proc p2c { a r } { return [list [m {$r * cos($a)}] [m {$r * sin($a)}]] }
# deg to rad
proc d2r { d } { return [m {$d * 3.14159 / 180}] }
# rad to deg
proc r2d { r } { return [m {$r * 180 / 3.14159 }] }
proc prepareEnvironment { path orgX orgY {colour white} {direction ccw} } {
global circlesTable axisTable
set fontFamily [firstFont {Verdana Arial} Helvetica]
set width [m {[$path cget -width] - $orgX*2}]
set height [m {[$path cget -height] - $orgY*2}]
set oneSeventh [m {$width/7}]
set halfHeight [m {$height/2}]
for { set i 0 } { $i < 8 } { incr i } {
$path create line [m {$oneSeventh*$i+$orgX}] $orgY \
[m {$oneSeventh*$i+$orgX}] [m {$height+$orgY}] -fill $colour \
-tag "vbar_$i"
}
for { set i 0 } { $i < 3 } { incr i } {
$path create line $orgX [m {$halfHeight*$i+$orgY}] \
[m {$orgX+$width}] [m {$halfHeight*$i+$orgY}] -fill $colour \
-tag "hbar_$i"
}
$path delete "vbar_6"
# Axis of rotation ("Z" axis) -- its coords
set zx [m {$orgX + $oneSeventh * 5 / 2 }]
set zy [m {$orgY + $height / 2}]
# Still outer circle
set x1 [m {$orgX + $oneSeventh * 5 / 2 - $halfHeight}]
set y1 $orgY
set x2 [m {$orgX + $oneSeventh * 5 / 2 + $halfHeight}]
set y2 [m {$orgY+$height}]
$path create oval $x1 $y1 $x2 $y2 -outline $colour -tag "still"
# Big rolling circle's coords (at 0 deg)
set x1b [m {$x1 + ($x2-$x1) / 4}]
set y1b [m {$y1 + ($y2-$y1) / 8}]
set x2b $x2
set y2b [m {$y2 - ($y2-$y1) / 8}]
$path create oval $x1b $y1b $x2b $y2b -outline $colour -tag "big"
# Medium rolling circle's coords
set x1m [m {$x1 + ($x2-$x1) / 2}]
set y1m [m {$y1 + ($y2-$y1) / 4}]
set x2m $x2
set y2m [m {$y2 - ($y2-$y1) / 4}]
$path create oval $x1m $y1m $x2m $y2m -outline $colour -tag "medium"
# Small rolling circle's coords
set x1s [m {$x1 + 3*($x2-$x1) / 4}]
set y1s [m {$y1 + 3*($y2-$y1) / 8}]
set x2s $x2
set y2s [m {$y2 - 3*($y2-$y1) / 8}]
$path create oval $x1s $y1s $x2s $y2s -outline $colour -tag "small"
# Center of the small circle
set x1c [m {($x1s+$x2s)/2}]
set y1c [m {($y1s+$y2s)/2}]
# "Satellite"
set frac [m {($x2s-$x1s) / 8}]
set x1sa [m {$x1c-$frac}]
set y1sa [m {$y1c-$frac}]
set x2sa [m {$x1c+$frac}]
set y2sa [m {$y1c+$frac}]
$path create oval $x1sa $y1sa $x2sa $y2sa -outline red -tag "satellite"
# Moving bars
$path create line $x1c $orgY $x1c [m {$height+$orgY}] -fill $colour \
-tag "vertical_bar"
$path create line $orgX $y1c [m {$width+$orgX-$oneSeventh-$oneSeventh}] $y1c \
-fill $colour -tag "horizontal_bar"
# The counters
set xtpos [m {int(6.33 * $oneSeventh)}]
set ytpos [m {int(0.4 * $halfHeight)}]
set smallFontSize [m {int(0.1 * $halfHeight)}]
set bigFontSize [m {int(0.3 * $halfHeight)}]
$path create text $xtpos $ytpos -text "input" -fill $colour -tag "input" \
-font "$fontFamily $smallFontSize"
$path create text $xtpos [m {$ytpos + 0.4 * $halfHeight}] -text "000" \
-fill $colour -tag "upper" -font "$fontFamily $bigFontSize"
set ytpos [m {int($ytpos + $halfHeight)}]
$path create text $xtpos $ytpos -text "output" -fill $colour -tag "output" \
-font "$fontFamily $smallFontSize"
$path create text $xtpos [m {$ytpos + 0.4 * $halfHeight}] -text "000" \
-fill $colour -tag "lower" -font "$fontFamily $bigFontSize"
# The first entry of the final circlesTable (for "0 deg") and axisTable
lappend circlesTable [list $x1b $y1b $x2b $y2b $x1m $y1m $x2m $y2m \
$x1s $y1s $x2s $y2s $x1sa $y1sa $x2sa $y2sa]
lappend axisTable [list $x1c $y1c]
# Translation of coordinates
set tmp1 [list] ; set tmp2 [list]
for { set i 0 } { $i < 16 } { incr i 2 } {
set j [m {$i + 1}] ;# Index for corresponding y
lappend tmp1 [m {[l [l $circlesTable 0] $i] - $zx}]
lappend tmp1 [m {$zy - [l [l $circlesTable 0] $j]}]
}
lappend tmp2 [m {[l [l $axisTable 0] 0] - $zx}]
lappend tmp2 [m {$zy - [l [l $axisTable 0] 1]}]
# "Box coordinates" to "center + radius"
set tmp3 [list]
set howMany [llength $tmp1]
for { set i 0 } { $i < $howMany } { incr i 4 } {
set j [m {$i + 1}] ; set k [m {$i + 2}] ; set l [m {$i + 3}]
lappend tmp3 [box2cir [l $tmp1 $i] [l $tmp1 $j] [l $tmp1 $k] [l $tmp1 $l]]
}
while { [set zz [join $tmp3]] ne $tmp3 } { set tmp3 $zz } ;# Flatten the list
# Cartesian to polar
set tmp4 [list]
set howMany [llength $tmp3]
for { set i 0 } { $i < $howMany } { incr i 3 } {
set j [m {$i + 1}] ; set k [m {$i + 2}]
lappend tmp4 [list [c2p [l $tmp3 $i] [l $tmp3 $j]] [l $tmp3 $k]]
}
while { [set zz [join $tmp4]] ne $tmp4 } { set tmp4 $zz } ;# Flatten the list
set tmp5 [c2p [l $tmp2 0] [l $tmp2 1]]
# Now let's calculate all possible coordinates (step 1 deg)
set howMany [llength $tmp4]
for { set d 1 } { $d < 360 } { incr d } {
if { $direction eq "ccw" } {
set a [d2r $d]
} else {
set a [d2r [m {360 - $d}]]
}
set v [list]
for { set i 0 } { $i < $howMany } { incr i 3 } {
set j [m {$i + 1}] ; set k [m {$i + 2}]
set xy [p2c [m {$a + [l $tmp4 $i]}] [l $tmp4 $j]] ;# New Cart. coords
set xyxy [circ2box [l $xy 0] [l $xy 1] [l $tmp4 $k]] ;# x,y,r --> x1,y1,x2.y2
# Now back-translation
lappend v [m {int([l $xyxy 0] + $zx)}]
lappend v [m {int($zy - [l $xyxy 1])}]
lappend v [m {int([l $xyxy 2] + $zx)}]
lappend v [m {int($zy - [l $xyxy 3])}]
}
# Row completion
lappend circlesTable $v
# Same calculation for "axis" table
set xya [p2c [m {$a + [l $tmp5 0]}] [l $tmp5 1]] ;# New axis coords
lappend axisTable [list [m {int([l $xya 0] + $zx)}] [m {int($zy - [l $xya 1])}]]
}
}
proc rollIt { path twinkle interval } {
global circlesTable axisTable
set satc [$path itemcget satellite -outline]
set fgc [$path itemcget horizontal_bar -fill]
set bgc [$path cget -background]
set orgX [l [$path coords horizontal_bar] 0]
set orgY [l [$path coords vertical_bar ] 1]
set x2 [l [$path coords horizontal_bar] 2]
set y2 [l [$path coords vertical_bar ] 3]
set width [m {[$path cget -width] - $orgX*2}]
set height [m {[$path cget -height] - $orgY*2}]
set i 0
while { 1 } {
$path coords big [lrange [l $circlesTable $i] 0 3]
$path coords medium [lrange [l $circlesTable $i] 4 7]
$path coords small [lrange [l $circlesTable $i] 8 11]
$path coords satellite [lrange [l $circlesTable $i] 12 15]
set x1 [l [l $axisTable $i] 0]
set y1 [l [l $axisTable $i] 1]
$path coords horizontal_bar $orgX $y1 $x2 $y1
$path coords vertical_bar $x1 $orgY $x1 $y2
if { ! [m {$i % 5}] } {
$path itemconfigure upper -text $i
$path itemconfigure lower -text [m {int(sin([l [l $circlesTable $i] 0])*180)}]
}
if { $twinkle } {
if { ! [m {$i % 2}] } {
$path itemconfigure horizontal_bar -fill $bgc
$path itemconfigure vertical_bar -fill $bgc
} else {
$path itemconfigure horizontal_bar -fill $fgc
$path itemconfigure vertical_bar -fill $fgc
}
if { ! [m {$i % 30}] } {
$path itemconfigure satellite -outline $bgc
} elseif { ! [m {$i % 35}] } {
$path itemconfigure satellite -outline $satc
}
}
update
after $interval
incr i
set i [m {$i % 360}]
}
}
# A table that'll hold the data of all points
set circlesTable [list]
# A list that'll will hold the data of rotation axis
set axisTable [list]
set canvasPath .c
pack [canvas $canvasPath -height 600 -width 1150 -background black]
bind . <Escape> { exit 0 }
wm protocol . WM_DELETE_WINDOW { exit 0 }
set Xmargin 50 ; set Ymargin 50
prepareEnvironment $canvasPath $Xmargin $Ymargin white cw ;# „cw” or „ccw”
rollIt $canvasPath yes 20 ;# „yes”=twinkle, frames each 20 ms