Lesson: vectors7.nim

← Back to all lessons

Run Code Download Code

Source Code

# ****************************************************************************************
#
#   raylib [vectors] lesson 7 - Graphing Quadratic Functions
#
#   This lesson demonstrates:
#   - The difference between a quadratic FUNCTION and a quadratic EQUATION.
#   - How to plot a mathematical function on the screen.
#   - Mapping coordinates from a "graph space" to "screen space".
#   - Calculating and visualizing the roots (solutions) of a quadratic equation.
#
# ****************************************************************************************

import raylib
import raymath
import math
import strformat

const
  screenWidth = 800
  screenHeight = 600 # A bit taller for the graph.

# --- Graphing Helper Types and Procs ---
type
  GraphSpace = object
    origin: Vector2   # Screen coordinates of the graph's (0,0) point
    scale: Vector2    # Pixels per unit for x and y

# Function to convert a point from graph space to screen space
proc toScreenSpace(p: Vector2, graph: GraphSpace): Vector2 =
  # X: Start at the origin's screen X and add the scaled graph X.
  result.x = graph.origin.x + p.x * graph.scale.x 
  # Y: Start at the origin's screen Y and SUBTRACT the scaled graph Y.
  # This is because in math, positive Y goes up, but in screen coordinates, positive Y goes down.
  result.y = graph.origin.y - p.y * graph.scale.y

# The quadratic function itself: y = ax² + bx + c
proc quadraticFunc(x: float32, a, b, c: float32): float32 =
  return a * x * x + b * x + c

proc main =
  initWindow(screenWidth, screenHeight, "raylib [vectors] lesson 7 - Quadratic Functions")
  setTargetFPS(60)

  # LESSON 1: DEFINING THE GRAPH SPACE
  # We need to map our mathematical coordinates to screen coordinates.
  let graph = GraphSpace(
    origin: Vector2(x: screenWidth / 2.0, y: screenHeight / 2.0 + 50),
    scale: Vector2(x: 30.0, y: 30.0) # 30 pixels per 1 unit
  )

  # LESSON 2: PRE-DEFINED FUNCTIONS
  # We'll create a list of functions to cycle through. Each is a tuple of (a, b, c).
  let functions = [
    (a: 0.5, b: -1.0, c: -4.0),   # Two real roots
    (a: 1.0, b: -6.0, c: 9.0),    # One real root
    (a: 1.0, b: 2.0, c: 5.0),     # No real roots
    (a: -0.5, b: 1.0, c: 6.0)     # Downward-facing parabola
  ]
  var currentFuncIndex = 0

  # Main game loop
  # --------------------------------------------------------------------------------------
  while not windowShouldClose():
    # Update
    # ----------------------------------------------------------------------------------
    if isKeyPressed(Space):
      # Use the modulo operator to cycle through the list of functions, wrapping
      # around to the beginning when we reach the end.
      currentFuncIndex = (currentFuncIndex + 1) mod functions.len

    # LESSON 3: THE CURRENT FUNCTION AND EQUATION
    # Get the current coefficients and calculate the roots on every frame.
    let (a, b, c) = functions[currentFuncIndex]

    # This is the implementation of the quadratic formula to find the roots:
    # x = [-b ± sqrt(b² - 4ac)] / 2a
    # The "discriminant" is the part under the square root. Its value tells us
    # how many real roots the equation has.
    let discriminant = b*b - 4*a*c
    var roots: seq[float32] = @[]
    if discriminant > 0:
      let sqrtDiscriminant = sqrt(discriminant)
      roots.add((-b + sqrtDiscriminant) / (2*a))
      roots.add((-b - sqrtDiscriminant) / (2*a))
    elif discriminant == 0:
      roots.add(-b / (2*a))

    # Draw
    # ------------------------------------------------------------------------------------
    beginDrawing()
    clearBackground(RayWhite)

    # --- Draw Graph Axes and Grid ---
    let xAxisStart = toScreenSpace(Vector2(x: -20, y: 0), graph)
    let xAxisEnd = toScreenSpace(Vector2(x: 20, y: 0), graph)
    drawLine(xAxisStart, xAxisEnd, 2.0, LightGray)

    let yAxisStart = toScreenSpace(Vector2(x: 0, y: -20), graph)
    let yAxisEnd = toScreenSpace(Vector2(x: 0, y: 20), graph)
    drawLine(yAxisStart, yAxisEnd, 2.0, LightGray)

    # --- Draw the Parabola ---
    # We draw the function by calculating many points and connecting them with lines.
    let steps = 200 # More steps = smoother curve
    let graphWidth = screenWidth.float / graph.scale.x
    for i in 0 ..< steps:
      # Calculate the start point (p1) of a tiny line segment.
      # We map the loop counter `i` to an x-coordinate in our graph's space.
      let x1_graph = -graphWidth/2 + (i.float / steps.float) * graphWidth
      let y1_graph = quadraticFunc(x1_graph, a, b, c)
      let p1_screen = toScreenSpace(Vector2(x: x1_graph, y: y1_graph), graph)

      # Calculate the end point (p2) of the segment for the next step.
      let x2_graph = -graphWidth/2 + ((i+1).float / steps.float) * graphWidth
      let y2_graph = quadraticFunc(x2_graph, a, b, c)
      let p2_screen = toScreenSpace(Vector2(x: x2_graph, y: y2_graph), graph)

      drawLine(p1_screen, p2_screen, 2.0, Maroon)

    # --- Draw the Roots ---
    # The roots are the solutions to the equation, where y=0.
    for root in roots:
      let rootPosGraph = Vector2(x: root, y: 0)
      let rootPosScreen = toScreenSpace(rootPosGraph, graph)
      drawCircle(rootPosScreen, 8.0, colorAlpha(Blue, 0.7))
      drawCircleLines(rootPosScreen, 8.0, Blue)

      # Draw the numerical value of the root on the graph.
      let rootValueText = fmt"{root:.2f}"
      # Measure the text so we can center it under the circle.
      let textWidth = measureText(rootValueText, 15)
      let textPos = Vector2(x: rootPosScreen.x - textWidth / 2, y: rootPosScreen.y + 12)
      drawText(rootValueText, textPos.x.int32, textPos.y.int32, 15, Blue)

    # --- Draw Explanations ---
    drawText("Quadratic Visualizer", 20, 20, 40, DarkGray)

    let funcText = fmt"Function: y = {a}x² + {b}x + {c}"
    drawText(funcText, 20, 80, 20, Black)

    let eqText = fmt"Equation: {a}x² + {b}x + {c} = 0"
    drawText(eqText, 20, 110, 20, Black)

    drawText("The function (red curve) describes the relationship between x and y.",
             20, 150, 20, Gray)
    drawText("Press [Space] to cycle through different functions.",
             20, screenHeight - 40, 20, LightGray)
    drawText("The equation's solutions (blue circles) are the 'roots' -",
             20, 180, 20, Gray)
    drawText("the x-values where y is 0.", 20, 200, 20, Gray)

    var rootText = "Roots: "
    var discriminantText = fmt"Discriminant (b² - 4ac) = {discriminant:.2f}"
    var discriminantMeaning: string

    if roots.len == 0:
      rootText &= "None (parabola does not cross the x-axis)"
      discriminantMeaning = "Discriminant is negative: No real roots"
    elif roots.len == 1:
      rootText &= fmt"{roots[0]:.2f}"
      discriminantMeaning = "Discriminant is zero: One real root"
    else:
      for i, root in roots:
        rootText &= fmt"{root:.2f}"
        if i < roots.len - 1: rootText &= ", "
      discriminantMeaning = "Discriminant is positive: Two real roots"
    
    drawText(rootText, 20, 240, 20, Blue)

    drawText(discriminantText, 20, 270, 20, DarkGreen)
    drawText(discriminantMeaning, 20, 290, 20, DarkGreen)

    endDrawing()
    # ------------------------------------------------------------------------------------
  # De-Initialization
  # --------------------------------------------------------------------------------------
  closeWindow()
  # --------------------------------------------------------------------------------------

main()

#[
This lesson shows the clear difference:
- The FUNCTION `y = ax² + bx + c` is the entire red curve. 
  It's a "map" of all possible points.
- The EQUATION `ax² + bx + c = 0` asks a specific question: 
  "Where does that curve cross the line y=0 (the x-axis)?".
- The blue circles are the answers to that question.
]#