The Eyeball Class

Composition: Building Complex Objects

Composition: The "Has-A" Relationship

While the Disk class taught us about creating simple objects, the Eyeball class introduces a powerful OOP concept called Composition.

Instead of being just one shape, an Eyeball is composed of multiple other objects. An Eyeball HAS-A base (a Disk), HAS-A pupil (another Disk), and even HAS catch-lights (tiny Disks)! This allows us to build complex things by assembling simpler building blocks.

👀 Eyeball Class Overview

The Eyeball class manages the base, pupil, and their interactions (like looking at things). It handles the logic for keeping the pupil inside the eye!

Attributes:
Attribute Type Description
base Disk The white part of the eye
pupil Disk The colorful center part
direction float Angle the eye is looking (radians)
pupil_radial_location float Distance of pupil from the center
is_locked boolean If true, the pupil stops moving
Key Methods:
  • look_at(target_x, target_y) - Rotates pupil to face a point
  • calculate_pupil_center() - Math magic to position the pupil based on angle/distance
  • render() - Draws the whole assembly (base first, then pupil)
  • handle_click() - Detects clicks to lock/unlock the eye

📜 Complete Eyeball Class Code

Importing & Initializing Components

Notice line 1: `from Disk3 import *`. This allows us to use the `Disk` class we defined earlier. In `__init__`, we don't just store numbers; we create new `Disk` objects and store them in `self.base` and `self.pupil`.

1from Disk3 import *
2import math
3
4class Eyeball:
5    """
6    Represents an eyeball with a circular base and a movable pupil.
7    Uses HSB color mode for all colors.
8    Stage 3: Enhanced to support looking in a direction
9    Attributes:
10        base: a Disk for the whole eye
11        pupil: a Disk for the pupil
12        light: a Disk for a catch-light
13        ...
19    """
20    
21    def __init__(self, center, radius, b_border, b_colors, p_percent, p_border, p_colors):
22        print("...init Eyeball...")
23        self.base = Disk(center, radius, b_border, b_colors)
24        self.pupil = Disk(center, p_percent*radius, p_border, p_colors)
25        
26        # Creating catch-lights (reflections)
27        light_radius1 = pow(p_percent, 2) * radius;
28        light_center1 = (center[0] + 1.2*self.pupil.radius * cos(.75*math.pi), center[1] - .6*self.pupil.radius * sin(.75*math.pi))
29        self.light1 = Disk(light_center1, light_radius1, b_border, b_colors)
            
36        self.pupil_radial_location = 0 # initially at center of eye
37        self.direction = 0
38        self.is_locked = False
            
45        self.allow_wonky = True   
46    #end init
Math & Logic (Encapsulation)

The `Eyeball` class handles complex math (trigonometry!) to ensure the pupil stays inside the eye. By hiding this complexity inside methods like `calculate_pupil_center` and `set_pupil_radial_location`, other parts of our program don't need to know trigonometry to move an eye. We just tell it where to look, and the class handles how.

Note the `constrain()` function: It's a handy Processing tool to keep a number between a minimum and maximum value.

59    def set_pupil_radial_location(self, distance):
66        # Calculate maximum distance pupil can move while staying inside
67        max_distance = self.base.radius - self.pupil.radius - self.pupil.thickness/2.0
68        
69        # Constrain the distance
70        # eureka! constrain is a great built-in function
71        self.pupil_radial_location = constrain(distance, 0, max_distance)
72        self.pupil.set_center(self.calculate_pupil_center())
73    #end set_pupil_distance
74    
75    def calculate_pupil_center(self):
81        # Convert polar coordinates to Cartesian
82        pupil_x = self.base.center[0] + self.pupil_radial_location * cos(self.direction)
83        pupil_y = self.base.center[1] - self.pupil_radial_location * sin(self.direction)
84        
85        # Adjust catch lights...
90        
91        return (pupil_x, pupil_y) #a tuple
92    #end get_pupil_center
Behaviors: Looking & Clicking

These methods give the eyeball personality! `look_at` uses `atan2` (arctangent) to find the angle between the eye and a target (like the mouse). `look_in_direction` is new for Stage 3 - it aligns the eyes with the velocity vector so ghosts watch where they are going!

127    def look_at(self, target_x, target_y):
135        # Don't move if locked...
136        if not self.allow_wonky:
137            if self.is_locked or not self.is_point_in_eye(mouseX, mouseY):
138                return
144        
145        dx = target_x - self.base.center[0]
146        dy = self.base.center[1] - target_y
147        
148        angle = atan2(dy, dx)  # Returns angle in radians
149        self.set_direction(angle)
150        
151        # Calculate distance to target
152        distance = dist(self.base.center[0], self.base.center[1], target_x, target_y)
153        self.set_pupil_radial_location(distance)
154    #end look_at
156    def look_in_direction(self, velocity_x, velocity_y):
166        if self.is_locked:
167            return
168        
169        # Calculate angle from velocity components
170        angle = atan2(-velocity_y, velocity_x)
171        self.set_direction(angle)
175        # Set pupil distance to a fixed amount (about 60% of max)
176        max_distance = self.base.radius - self.pupil.radius - self.pupil.thickness/2.0
177        self.set_pupil_radial_location(0.6 * max_distance)
178    #end look_in_direction
Nested Rendering

When we call `Eyeball.render()`, it doesn't draw ellipses directly. It asks its components (`base.render()`, `pupil.render()`) to draw themselves. This is the power of composition: delegating tasks to the sub-objects.

180    def render(self):
185        # Draw eyeball base
186        self.base.render()
191        # If locked, slightly darken the pupil as a visual indicator
192        if self.is_locked:
193            # use locked color
194            self.pupil.set_stroke_color(self.pupil_lock_color)
195        else:
196            self.pupil.set_stroke_color(self.pupil_orig_color)
197        
198        self.pupil.render()
199        self.light1.render()
200        self.light2.render()
201        
202    #end display
💡 Making an Eyeball

Creating an Eyeball object requires passing parameters for both the base and the pupil:

# Setup colors
white = color(0, 0, 100)
black = color(0, 0, 0)
blue = color(240, 80, 80)

# Parameters: Center, radius, border_thickness, (bg, stroke), 
#             pupil_percent_size, pupil_border, (pupil_bg, pupil_stroke)
eye = Eyeball(
    (250, 250),   # center (x,y)
    40,           # radius
    2,            # border width
    (white, black),# base colors
    0.5,          # pupil is 50% of the eye size
    1,            # pupil border width
    (black, blue) # pupil colors
)

# In the draw loop:
eye.look_at(mouseX, mouseY)
eye.render()
🎃 Coding Challenges
  1. Create a "Cyclops" sketch with one giant eyeball in the center of the screen properly tracking the mouse.
  2. Modify the `is_locked` logic so the eye turns red when locked.
  3. Add a "blink" method. (Hint: You might need to change the base `Disk` height to 0 temporarily, or draw a "lid" shape over it).
  4. Add functionality for "dilating" the pupil (changing `p_percent`) based on how close the mouse is.

Explore More Classes

Continue learning about OOP with our other classes:

Disk Class Ghost Class Driver Code