/
donut.go
194 lines (165 loc) · 4.8 KB
/
donut.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
//
// donut.go
//
// An implementation in Go of Andy Sloan's donut.c
//
// Original:
// http://www.a1k0n.net/2011/07/20/donut-math.html
//
// To run:
// $ go mod init github.com/GaryBoone/GoDonut
// $ go mod tidy
// $ go run .
//
// Author: gary.boone@gmail.com
// History: 20121012 • initial version
// 20121013 • upload to GitHub
// 20240322 • update run directions for Go 1.122.1
package main
import (
"fmt"
"math"
"os"
"syscall"
"time"
"unsafe"
)
const frame_delay = 33 // ie, 30 fps
const theta_spacing = 0.07
const phi_spacing = 0.02
const R1 = 1.0
const R2 = 2.0
const K2 = 5.0
type Screen struct {
dim int
data [][]byte
}
func newZBuffer(d int) *[][]float64 {
b := make([][]float64, d)
for i := range b {
b[i] = make([]float64, d)
}
return &b
}
func newScreen(d int) *Screen {
b := make([][]byte, d)
for i := range b {
b[i] = make([]byte, d)
}
return &Screen{d, b}
}
func (screen Screen) render(time time.Time) {
fmt.Printf("\x1b[H") // bring cursor to "home" location
for j := 0; j < screen.dim; j++ {
fmt.Printf("%s\n", screen.data[j])
}
}
func (screen *Screen) clear() {
for i, _ := range screen.data {
for j, _ := range screen.data[i] {
screen.data[i][j] = ' '
}
}
}
func (screen *Screen) computeFrame(A, B, K1 float64) {
// precompute sines and cosines of A and B
cosA := math.Cos(A)
sinA := math.Sin(A)
cosB := math.Cos(B)
sinB := math.Sin(B)
screen.clear()
zbuffer := newZBuffer(screen.dim)
// theta goes around the cross-sectional circle of a torus
for theta := 0.0; theta < 2.0*math.Pi; theta += theta_spacing {
// precompute sines and cosines of theta
costheta := math.Cos(theta)
sintheta := math.Sin(theta)
// phi goes around the center of revolution of a torus
for phi := 0.0; phi < 2.0*math.Pi; phi += phi_spacing {
// precompute sines and cosines of phi
cosphi := math.Cos(phi)
sinphi := math.Sin(phi)
// the x,y coordinate of the circle, before revolving (factored out of the above equations)
circlex := R2 + R1*costheta
circley := R1 * sintheta
// final 3D (x,y,z) coordinate after rotations, directly from our math above
x := circlex*(cosB*cosphi+sinA*sinB*sinphi) - circley*cosA*sinB
y := circlex*(sinB*cosphi-sinA*cosB*sinphi) + circley*cosA*cosB
z := K2 + cosA*circlex*sinphi + circley*sinA
ooz := 1 / z // "one over z"
// x and y projection. note that y is negated here, because y goes up in
// 3D space but down on 2D displays.
xp := int(float64(screen.dim)/2.0 + K1*ooz*x)
yp := int(float64(screen.dim)/2.0 - K1*ooz*y)
// calculate luminance. ugly, but correct.
L := cosphi*costheta*sinB - cosA*costheta*sinphi - sinA*sintheta +
cosB*(cosA*sintheta-costheta*sinA*sinphi)
// L ranges from -sqrt(2) to +sqrt(2). If it's < 0, the surface is
// pointing away from us, so we won't bother trying to plot it.
if L > 0 {
// test against the z-buffer. larger 1/z means the pixel is closer to
// the viewer than what's already plotted.
if ooz > (*zbuffer)[yp][xp] {
(*zbuffer)[yp][xp] = ooz
luminance_index := int(L * 8.0) // this brings L into the range 0..11 (8*sqrt(2) = 11.3)
// now we lookup the character corresponding to the luminance and plot it in our output:
screen.data[yp][xp] = ".,-~:;=!*#$@"[luminance_index]
}
}
}
}
}
// return the min of two uint16 and convert to int
func min(x, y uint16) int {
if x < y {
return int(x)
}
return int(y)
}
type winsize struct {
Row uint16
Col uint16
Xpixel uint16
Ypixel uint16
}
// adapted from:
// https://www.darkcoding.net/software/pretty-command-line-console-output-on-unix-in-python-and-go-lang/
func GetWinsize() (*winsize, error) {
ws := new(winsize)
r1, _, errno := syscall.Syscall(syscall.SYS_IOCTL,
uintptr(syscall.Stdin),
uintptr(syscall.TIOCGWINSZ),
uintptr(unsafe.Pointer(ws)),
)
if int(r1) == -1 {
return nil, os.NewSyscallError("GetWinsize", errno)
}
return ws, nil
}
func animate(screen *Screen) {
// Calculate K1 based on screen size: the maximum x-distance occurs roughly at
// the edge of the torus, which is at x=R1+R2, z=0. we want that to be
// displaced 3/8ths of the width of the screen, which is 3/4th of the way from
// the center to the side of the screen.
// screen_width*3/8 = K1*(R1+R2)/(K2+0)
// screen_width*K2*3/(8*(R1+R2)) = K1
A, B, K1 := 1.0, 1.0, float64(screen.dim)*K2*3.0/(8.0*(R1+R2))
fmt.Println("\033[2J\033[;H") // clear the screen
c := time.Tick(frame_delay * time.Millisecond) // create timer channel
for now := range c {
A += 0.07
B += 0.03
screen.computeFrame(A, B, K1)
screen.render(now)
}
}
func main() {
ws, err := GetWinsize()
if err != nil {
fmt.Println("Error: Unable to read terminal size.")
os.Exit(0)
}
dim := min(ws.Row, ws.Col)
screen := newScreen(dim)
animate(screen)
}