-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathvectors9.nim
More file actions
276 lines (239 loc) · 11.3 KB
/
vectors9.nim
File metadata and controls
276 lines (239 loc) · 11.3 KB
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
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
# ****************************************************************************************
#
# raylib [vectors] lesson 9 - The Unit Circle
#
# This lesson demonstrates:
# - The direct relationship between a point rotating on a circle and the
# sine and cosine functions.
# - How sin(x) corresponds to the Y-coordinate of the point.
# - How cos(x) corresponds to the X-coordinate of the point.
# - Animating a value over time to generate the graphs.
#
# ****************************************************************************************
import raylib, raymath
import math
import strformat
from deques import Deque, addLast, popFirst, len, `[]`
const
screenWidth = 1000
screenHeight = 600
maxWavePoints = 500
proc main =
initWindow(screenWidth, screenHeight, "raylib [vectors] lesson 9 - The Unit Circle")
setTargetFPS(60)
# --- Visualization Setup ---
let
circleCenter = Vector2(x: 800, y: screenHeight / 2)
circleRadius = 100.0
# Both waves will share a single origin point
let
graphOrigin = Vector2(x: 450, y: screenHeight / 2)
graphScale = Vector2(x: 50.0, y: 100.0) # x-scale, y-scale (amplitude)
# We use a Deque (Double-Ended Queue) to efficiently store the wave points.
# It lets us add to one end and remove from the other in constant time.
var
sinWavePoints: Deque[Vector2]
cosWavePoints: Deque[Vector2]
var
angle = 0.0'f32
prevAngle = 0.0'f32
isPaused = false
# We'll update this text less frequently to make it readable.
var
frameCounter = 0
continuousAngleText = fmt"Continuous Angle (for graph): {angle / PI:.2f}pi rad"
# Use `mod` from the math module for float modulo.
let wrappedAngleRad = math.mod(angle, TAU)
var wrappedAngleText =
fmt"Effective Angle (for circle): {wrappedAngleRad * 360.0 / TAU:.1f}°"
let font = getFontDefault()
# Main game loop
# --------------------------------------------------------------------------------------
while not windowShouldClose():
# Update
# ----------------------------------------------------------------------------------
if isKeyPressed(Space): # Manual pause/unpause has priority
isPaused = not isPaused
if not isPaused:
# When we manually unpause, immediately advance the angle
# to prevent the auto-pause from re-triggering on the same frame.
prevAngle = angle
angle += 0.03
elif not isPaused: # Only run animation logic if not manually paused
prevAngle = angle
angle += 0.03 # Increment the angle each frame to animate
# --- Auto-pause logic ---
# Check if we crossed a 45-degree (PI/4) boundary
let boundary = PI / 4.0
let prevStep = floor(prevAngle / boundary)
let currentStep = floor(angle / boundary)
if currentStep > prevStep:
# We crossed a boundary! Pause the animation.
isPaused = true
# Snap the angle to the boundary for perfect alignment
angle = currentStep * boundary
# Calculate current sin and cos values
let sinValue = sin(angle)
let cosValue = cos(angle)
# Add new points to our wave history
sinWavePoints.addLast(Vector2(x: angle, y: sinValue))
cosWavePoints.addLast(Vector2(x: angle, y: cosValue))
# Prune the history if it gets too long
if sinWavePoints.len() > maxWavePoints:
sinWavePoints.popFirst()
if cosWavePoints.len() > maxWavePoints:
cosWavePoints.popFirst()
# Calculate the position of the point on the unit circle
let pointOnCircle = Vector2(
x: circleCenter.x + cos(angle) * circleRadius,
y: circleCenter.y - sin(angle) * circleRadius # Y is inverted
)
# Update the angle text every 6 frames (10 times per second at 60 FPS)
frameCounter += 1
if frameCounter mod 6 == 0:
continuousAngleText = fmt"Continuous Angle (for graph): {angle / PI:.2f}pi rad"
# Use `mod` from math module for float modulo
let wrappedAngleRad = math.mod(angle, TAU)
wrappedAngleText =
fmt"Effective Angle (for circle): {wrappedAngleRad * 360.0 / TAU:.1f}°"
# Draw
# ------------------------------------------------------------------------------------
beginDrawing()
clearBackground(RayWhite)
# --- Draw UI and Explanations ---
drawText(font, "The Unit Circle and Trigonometry",
Vector2(x: 20, y: 20), 30.0, 1.0, DarkGray)
if isPaused:
drawText(font, "Press [Space] to resume",
Vector2(x: 20, y: screenHeight - 40), 20.0, 1.0, DarkGray)
# When paused, show the exact numerical values
# TAU is a constant equal to 2*PI, representing a full circle in radians.
# It's often clearer than writing 2*PI everywhere.
let angleDeg = math.mod(angle, TAU) * (360.0 / TAU)
let sinVal = sin(angle)
let cosVal = cos(angle)
# Helper to format the values intelligently
proc formatTrigValue(val: float32): string =
const epsilon = 1e-6
let roundedVal = round(val)
# Check if the value is very close to a whole number
if abs(val - roundedVal) < epsilon:
if roundedVal == 0.0: return "0" # Explicitly handle zero to avoid "-0"
return fmt"{roundedVal:.0f}"
else:
return fmt"{val:.3f}"
let sinValText = fmt"sin({angleDeg:.0f}°) = {formatTrigValue(sinVal)}"
let cosValText = fmt"cos({angleDeg:.0f}°) = {formatTrigValue(cosVal)}"
drawText(font, sinValText,
Vector2(x: graphOrigin.x - 200, y: graphOrigin.y + 100), 20.0, 1.0, Red)
drawText(font, cosValText,
Vector2(x: graphOrigin.x - 200, y: graphOrigin.y + 125), 20.0, 1.0, Blue)
else:
drawText(font, "Press [Space] to pause",
Vector2(x: 20, y: screenHeight - 40), 20.0, 1.0, LightGray)
drawText(font, wrappedAngleText,
Vector2(x: circleCenter.x - 320, y: circleCenter.y + circleRadius + 20),
20.0, 1.0, Black)
drawText(font, continuousAngleText,
Vector2(x: circleCenter.x - 320, y: circleCenter.y + circleRadius + 45),
20.0, 1.0, Gray)
# --- Draw Unit Circle Visualization (Left) ---
drawCircleLines(circleCenter, circleRadius, LightGray)
let xAxisStart = Vector2(x: circleCenter.x - circleRadius - 20, y: circleCenter.y)
let xAxisEnd = Vector2(x: circleCenter.x + circleRadius + 20, y: circleCenter.y)
drawLine(xAxisStart, xAxisEnd, LightGray) # X-axis
let yAxisStart = Vector2(x: circleCenter.x, y: circleCenter.y - circleRadius - 20)
let yAxisEnd = Vector2(x: circleCenter.x, y: circleCenter.y + circleRadius + 20)
drawLine(yAxisStart, yAxisEnd, LightGray) # Y-axis
# Draw the rotating radius line
drawLine(circleCenter, pointOnCircle, 2.0, Black)
drawCircle(pointOnCircle, 8.0, Black)
# --- Draw Combined Graph Visualization ---
drawText(font, "y = sin(angle)",
Vector2(x: graphOrigin.x - 200, y: graphOrigin.y - 150), 20.0, 1.0, Red)
drawText(font, "y = cos(angle)",
Vector2(x: graphOrigin.x - 200, y: graphOrigin.y - 125), 20.0, 1.0, Blue)
# Draw the -1, 0, and 1 horizontal lines
let yZero = graphOrigin.y
let yPlusOne = graphOrigin.y - 1 * graphScale.y
let yMinusOne = graphOrigin.y + 1 * graphScale.y
let lineStartX = graphOrigin.x - maxWavePoints
let lineEndX = graphOrigin.x + maxWavePoints
drawLine(Vector2(x: lineStartX, y: yZero), Vector2(x: lineEndX, y: yZero), LightGray)
drawLine(Vector2(x: lineStartX, y: yPlusOne), Vector2(x: lineEndX, y: yPlusOne),
colorAlpha(LightGray, 0.5))
drawLine(Vector2(x: lineStartX, y: yMinusOne), Vector2(x: lineEndX, y: yMinusOne),
colorAlpha(LightGray, 0.5))
# Add labels for the horizontal lines
let labelX = circleCenter.x + circleRadius + 20
drawText(font, "1", Vector2(x: labelX, y: yPlusOne - 10), 20.0, 1.0, Gray)
drawText(font, "0", Vector2(x: labelX, y: yZero - 10), 20.0, 1.0, Gray)
drawText(font, "-1", Vector2(x: labelX, y: yMinusOne - 10), 20.0, 1.0, Gray)
# Draw Sine Wave
for i in 0 ..< sinWavePoints.len() - 1:
# Draw the wave relative to the current angle, so it scrolls left.
let p1 = Vector2(
x: graphOrigin.x + (sinWavePoints[i].x - angle) * graphScale.x,
y: graphOrigin.y - sinWavePoints[i].y * graphScale.y)
let p2 = Vector2(
x: graphOrigin.x + (sinWavePoints[i+1].x - angle) * graphScale.x,
y: graphOrigin.y - sinWavePoints[i+1].y * graphScale.y)
drawLine(p1, p2, 2.0, Red)
# Draw Cosine Wave
for i in 0 ..< cosWavePoints.len() - 1:
let p1 = Vector2(
x: graphOrigin.x + (cosWavePoints[i].x - angle) * graphScale.x,
y: graphOrigin.y - cosWavePoints[i].y * graphScale.y)
let p2 = Vector2(
x: graphOrigin.x + (cosWavePoints[i+1].x - angle) * graphScale.x,
y: graphOrigin.y - cosWavePoints[i+1].y * graphScale.y)
drawLine(p1, p2, 2.0, Blue)
# --- Draw the Connecting Lines ---
# Line from circle's Y to the start of the sine wave
let sinStartPoint =
Vector2(x: graphOrigin.x, y: graphOrigin.y - sin(angle) * graphScale.y)
drawLine(Vector2(x: pointOnCircle.x, y: pointOnCircle.y),
Vector2(x: sinStartPoint.x, y: pointOnCircle.y), 2.0, colorAlpha(Red, 0.5))
drawLine(Vector2(x: sinStartPoint.x, y: pointOnCircle.y),
sinStartPoint, 2.0, colorAlpha(Red, 0.5))
drawCircle(sinStartPoint, 5.0, Red)
# Line from circle's X to the start of the cosine wave
let cosStartPoint =
Vector2(x: graphOrigin.x, y: graphOrigin.y - cos(angle) * graphScale.y)
drawLine(Vector2(x: pointOnCircle.x, y: pointOnCircle.y),
Vector2(x: pointOnCircle.x, y: cosStartPoint.y), 2.0, colorAlpha(Blue, 0.5))
drawLine(Vector2(x: pointOnCircle.x, y: cosStartPoint.y),
cosStartPoint, 2.0, colorAlpha(Blue, 0.5))
drawCircle(cosStartPoint, 5.0, Blue)
# --- Draw the triangle inside the circle and label the sides ---
let projectionPoint = Vector2(x: pointOnCircle.x, y: circleCenter.y)
# Draw the vertical "sin" side
drawLine(pointOnCircle, projectionPoint, 2.0, Red)
drawText(font, "sin",
Vector2(x: pointOnCircle.x + 10,
y: circleCenter.y + (pointOnCircle.y - circleCenter.y)/2),
20.0, 1.0, Red)
# Draw the horizontal "cos" side
drawLine(circleCenter, projectionPoint, 2.0, Blue)
drawText(font, "cos",
Vector2(x: circleCenter.x + (pointOnCircle.x - circleCenter.x)/2,
y: circleCenter.y + 10),
20.0, 1.0, Blue)
endDrawing()
# ------------------------------------------------------------------------------------
# De-Initialization
# --------------------------------------------------------------------------------------
closeWindow()
# --------------------------------------------------------------------------------------
main()
#[
Key takeaways from this lesson:
- Sine and Cosine are geometric functions. They directly describe the coordinates
of a point on a circle of radius 1 as it rotates.
- sin(angle) is the Y-coordinate.
- cos(angle) is the X-coordinate.
- The wave shape is what you get when you "unroll" the circle's motion over time.
- The `deques` module is very useful for managing a fixed-size list of historical
data, like the points of our wave, because adding to one end and removing from
the other is very fast.
]#