Introduction to Functions

Core Concept: Mathematical functions create visual patterns. Use trigonometric functions and wave generation, integrate time with SwiftUI, synchronize animation, and understand phase/frequency. Challenge: Breathing animation with SwiftUI spring timing.

The Hidden Pattern: Everything is a Wave

You've mastered spatial patterns with coordinates (Chapter 2) and color manipulation (Chapter 3). But here's what no tutorial tells you: every visual effect in computer graphics ultimately comes from just a handful of mathematical functions. Master these, and you'll see them everywhere - in water simulations, audio visualizers, organic textures, even UI animations.

But here's the deeper truth: these aren't arbitrary functions. They represent fundamental patterns in nature and mathematics.

Concept Introduction: The Universe Runs on Waves

Look at these phenomena:

  • Ocean waves
  • Sound vibrations
  • Light oscillations
  • Heartbeats
  • Planetary orbits

What do they share? Periodic motion. Something that repeats, oscillates, cycles. The mathematics that describes a pendulum also describes how colors pulse in your shader.

The functions we'll learn aren't just "useful for graphics" - they're the building blocks of pattern itself.

Mathematical Foundation: From Circles to Waves

Why Sine Creates Waves

The sine function seems magical until you understand its origin:

Imagine a point rotating around a circle.
Track only its vertical position over time.
That trace creates a sine wave.

This is why:

  • sin(0) = 0 (starting at the right, vertical position is 0)
  • sin(π/2) = 1 (top of circle, maximum vertical position)
  • sin(π) = 0 (left side, back to center height)
  • sin(3π/2) = -1 (bottom, minimum vertical position)
  • sin(2π) = 0 (full rotation, back to start)

In shaders:

float wave = sin(angle);  // -1 to 1

But angle comes from position:

float angle = uv.x * frequency;  // Convert position to angle
float wave = sin(angle);

The Deep Meaning of Frequency

When you write sin(x * 5.0), that 5.0 isn't just "making more waves." You're saying: "In the space where x goes from 0 to 1, complete 5 full rotations around the circle."

Higher frequency = more rotations = more waves.

Fract: The Space Duplicator

The fract() function is misunderstood. It doesn't just "repeat patterns" - it creates infinite copies of coordinate space.

fract(3.7) = 0.7

But conceptually:

Original space: 0 -------- 1 -------- 2 -------- 3 -------- 4
After fract:    0 -------- 1, 0 ----- 1, 0 ----- 1, 0 ----- 1
                [Copy 1]      [Copy 2]   [Copy 3]   [Copy 4]

When you do:

float2 cell = fract(uv * 10.0);

You're creating 10×10 copies of your UV space. Each pixel thinks it's in a 0-1 box, unaware of the other 99 copies.

Step Functions: Digital Reality

step(edge, x) represents a fundamental question: "Have we crossed the threshold?"

In nature:

  • Water freezes at 0°C (step function)
  • Neurons fire when voltage exceeds threshold (step function)
  • Light switches toggle (step function)

In shaders:

float mask = step(0.5, uv.x);  // Left half: 0, Right half: 1

Smoothstep: Nature Abhors Sharp Edges

Real world transitions are rarely instant. smoothstep adds a transition zone:

// Hermite interpolation - smooth acceleration and deceleration
float t = clamp((x - a) / (b - a), 0.0, 1.0);
return t * t * (3.0 - 2.0 * t);

This specific curve (3t² - 2t³) has special properties:

  • Starts slowly (derivative = 0 at t=0)
  • Ends slowly (derivative = 0 at t=1)
  • Smooth throughout (continuous second derivative)

Visual Intuition

Let's see these functions as transformations of space:

Linear Space

Input:  0.0 ---- 0.5 ---- 1.0
Output: 0.0 ---- 0.5 ---- 1.0

After sin()

Input:  0.0 ---- 0.5π ---- π ---- 1.5π ---- 2π
Output: 0.0 ---- 1.0 ----- 0.0 --- -1.0 ---- 0.0

After fract()

Input:  0.0 ---- 1.0 ---- 2.0 ---- 3.0
Output: 0.0 ---- 1.0, 0.0 - 1.0, 0.0 - 1.0

After pow(x, 2)

Input:  0.0 -- 0.5 -- 1.0
Output: 0.0 - 0.25 - 1.0

Your First Deep Pattern

Let's build understanding step by step:

[[ stitchable ]] half4 interferencePattern(float2 position, half4 color, float2 size) {
    float2 uv = position / size;
    
    // Two waves at different angles
    float wave1 = sin(uv.x * 10.0);
    float wave2 = sin(uv.y * 10.0);
    
    // Multiply: where both are positive or both negative = bright
    // Where they disagree = dark
    float interference = wave1 * wave2;
    
    // This creates a checkerboard of positive/negative values
    // Map to 0-1 for visibility
    float brightness = interference * 0.5 + 0.5;
    
    return half4(brightness, brightness, brightness, 1.0);
}

Building Complex Effects

Understanding Phase

[[ stitchable ]] half4 travelingWave(float2 position, half4 color, float2 size, float time) {
    float2 uv = position / size;
    
    // Phase shift moves the wave
    float phase = time * 2.0;
    float wave = sin(uv.x * 10.0 - phase);
    
    // Visualize
    float brightness = wave * 0.5 + 0.5;
    return half4(brightness, brightness, brightness, 1.0);
}

The -phase shifts where the wave starts. As time increases, the wave appears to move right.

Radial Patterns: From Linear to Polar

[[ stitchable ]] half4 radialPulse(float2 position, half4 color, float2 size, float time) {
    float2 uv = position / size;
    float2 center = uv - 0.5;
    
    // Distance creates circular coordinate
    float radius = length(center);
    
    // Apply sine to radius instead of x/y
    float wave = sin(radius * 30.0 - time * 3.0);
    
    // Smooth edges
    float mask = 1.0 - smoothstep(0.4, 0.5, radius);
    
    float brightness = wave * 0.5 + 0.5;
    return half4(brightness * mask, brightness * mask, brightness * mask, 1.0);
}

The Power of Power

[[ stitchable ]] half4 falloffComparison(float2 position, half4 color, float2 size) {
    float2 uv = position / size;
    
    // Three different falloff curves
    float linear = uv.x;
    float quadratic = pow(uv.x, 2.0);
    float sqrt = pow(uv.x, 0.5);
    
    // Show each in R, G, B channels
    return half4(linear, quadratic, sqrt, 1.0);
}

Common Pitfalls

Pitfall 1: Fighting the Coordinate System

// WRONG - Trying to create vertical waves with horizontal coordinate
float wave = sin(uv.x * 10.0);  // Always horizontal!

// CORRECT - Match coordinate to desired direction
float wave = sin(uv.y * 10.0);  // Vertical waves

Pitfall 2: Misunderstanding Frequency Limits

The Nyquist theorem applies to shaders too. If your frequency is too high:

// TOO HIGH - Creates aliasing
float wave = sin(uv.x * 1000.0);  

// CORRECT - Respect pixel density
float wave = sin(uv.x * 10.0);

Pitfall 3: Phase vs Frequency Confusion

// Frequency: How many waves fit in the space
sin(uv.x * frequency)

// Wavelength: How wide each wave is (1/frequency)

// Phase: Where the wave starts
sin(uv.x * frequency + phase)

// Amplitude: How tall the wave is
sin(uv.x * frequency) * amplitude

Advanced Pattern Building

Combining Octaves (Preview of Noise)

[[ stitchable ]] half4 complexWave(float2 position, half4 color, float2 size) {
    float2 uv = position / size;
    
    float wave = 0.0;
    float amplitude = 1.0;
    float frequency = 1.0;
    
    // Add multiple octaves
    for(int i = 0; i < 4; i++) {
        wave += sin(uv.x * frequency * 10.0) * amplitude;
        frequency *= 2.0;  // Double frequency
        amplitude *= 0.5;  // Halve amplitude
    }
    
    // Normalize
    wave = wave * 0.5 + 0.5;
    
    return half4(wave, wave, wave, 1.0);
}

Challenges

Challenge 1: Wave Mastery

Create waveVisualizer that shows:

  • A sine wave
  • The same wave with double frequency
  • The same wave with half amplitude
  • All three visible simultaneously (use R, G, B channels)

Challenge 2: Fract Exploration

Create gridExplorer that:

  • Uses fract() to create a 5×5 grid
  • Colors each cell differently based on its grid position
  • Hint: floor(uv * 5.0) gives you cell coordinates

Challenge 3: Smoothstep Animation

Create breathingCircle that:

  • Shows a circle that grows and shrinks
  • Uses smoothstep for soft edges
  • Uses sin(time) to control size
  • Bonus: Make it pulse with a heartbeat rhythm

Challenge 4: Function Composition

Create moire that:

  • Combines rotated coordinate systems
  • Uses sin() on both
  • Creates interference patterns
  • Should look like silk fabric patterns

Challenge 5: Distortion Introduction

Create ripple using distortionEffect:

[[ stitchable ]] float2 ripple(float2 position, float2 size, float time) {
    float2 uv = position / size;
    float2 center = uv - 0.5;
    
    float dist = length(center);
    float wave = sin(dist * 20.0 - time * 3.0) * 0.02;
    
    // Return new position (this is distortionEffect!)
    return position + normalize(center) * wave * size.x;
}

Want the Challenges Solutions?

Get the full Xcode project with solutions to all challenges, bonus examples, and clean, runnable code.

Get the Full Project →

Validation Questions

Before proceeding to Chapter 5:

  1. Why does sin() create smooth waves instead of sharp triangles?

Because sine comes from circular motion - tracking the vertical position of a point rotating around a circle. This creates smooth acceleration and deceleration, not linear motion.

  1. What happens when you multiply two sine waves together?

You get interference patterns. Where both waves are positive or both negative, you get bright areas. Where they have opposite signs, you get dark areas, creating a checkerboard-like pattern.

  1. How does fract(uv * 5) create 5 copies of space?

fract() returns only the fractional part (0.0-1.0), so multiplying by 5 creates coordinates from 0-5, but fract() wraps each integer interval back to 0-1, creating 5 copies of the original 0-1 coordinate space.

  1. Why use smoothstep(0.4, 0.6, x) instead of step(0.5, x)?

step() creates a hard binary transition (0 or 1) which causes aliasing and jagged edges. smoothstep() creates a smooth transition zone between 0.4 and 0.6, eliminating visual artifacts.

  1. What's the relationship between frequency and wavelength?

They're inversely related: wavelength = 1/frequency. Higher frequency means more waves in the same space, so each individual wave is shorter (smaller wavelength).

  1. How would you create a wave that speeds up over time?

Use accelerating phase: sin(uv.x * frequency - time * time * acceleration). The phase advances faster and faster over time, making the wave move at increasing speed. Alternative: increase frequency over time with sin(uv.x * (baseFrequency + time * rate)) for more waves over time.

Debugging Deep Patterns

// Visualize function ranges
return half4(
    sin(uv.x * 5.0) * 0.5 + 0.5,        // Red: sine
    fract(uv.x * 5.0),                   // Green: fract
    smoothstep(0.4, 0.6, uv.x),          // Blue: smoothstep
    1.0
);

Further Exploration

  • Fourier Analysis: How any pattern can be built from sines
  • Signal Processing: Why these functions matter beyond graphics
  • Natural Patterns: Reaction-diffusion, fluid dynamics, crystal growth
  • Distortion Effects: The path to truly dynamic shaders

Next Chapter Preview: You've learned that functions create patterns. Chapter 5 will show you how to use mathematical tricks to generate complex, infinitely detailed designs from simple rules. We'll explore how fract(), mod(), and coordinate transformation can create anything from celtic knots to fractal landscapes.