中文日本語한국어 NewsletterEmail me

Dev · · 3 min read · Matt Busel

Making two cartoon eyes feel alive: springs, saccades and blinks in SwiftUI

Making two cartoon eyes feel alive: springs, saccades and blinks in SwiftUI

Minder is a focus timer that's just a black screen and two eyes. You prop your phone up, slide to focus, and the eyes watch you work. If you pick the phone up mid-session, they get very suspicious.

It's body doubling for people with ADHD (or anyone who works better when someone's watching), and the whole thing only works if the eyes feel alive. A looping animation reads as a screensaver within about ten seconds. Here's what made the difference, all in plain SwiftUI with a Canvas.

1. Everything is a spring

Every moving part (the eyelids and brows as a "face" vector, where the eyes look, a little vertical bob) is a damped spring stepped once per frame. This is the entire physics engine:

struct SpringVector {
    var x: [CGFloat]
    var v: [CGFloat]
    init(_ start: [CGFloat]) { x = start; v = Array(repeating: 0, count: start.count) }

    mutating func step(toward target: [CGFloat], dt: CGFloat, stiffness k: CGFloat, damping ratio: CGFloat) {
        let c = 2 * sqrt(k) * ratio
        for i in x.indices {
            let a = k * (target[i] - x[i]) - c * v[i]
            v[i] += a * dt
            x[i] += v[i] * dt
        }
    }
}

The trick is giving each layer its own feel:

face.step(toward: expr.face.values, dt: dt, stiffness: 190, damping: 0.62)   // expressions settle with a little overshoot
gaze.step(toward: target, dt: dt, stiffness: 420, damping: 0.8)              // eyes are fast and nearly critical
bob.step(toward: [0], dt: dt, stiffness: 140, damping: 0.38)                 // a loose, bouncy nod

An expression (happy, curious, proud, suspicious, sleepy...) is just a target vector. Switching expressions never pops, because the springs carry the face there. A nod is just a kick to the bob's velocity.

2. Eyes never sit still: saccades

Real eyes make tiny involuntary jumps. Adding a small random offset to the gaze target every 0.25 to 0.9 seconds is what stopped the eyes looking painted on:

if t > nextSaccadeAt {
    saccade = CGPoint(x: .random(in: -0.05...0.05), y: .random(in: -0.04...0.04))
    nextSaccadeAt = t + .random(in: 0.25...0.9)
}

3. Blinks have a shape

A blink isn't symmetric: the lid closes in 70 ms and opens over the next 100 ms. Blinks come every 2.2 to 6.5 seconds, 18% of them are double blinks 240 ms apart, and when the eyes are sleepy the whole blink plays at under half speed. Those numbers came from fiddling until it stopped looking robotic.

4. Layers of attention

The brain has a fast layer (springs, saccades, blinks, every frame) and a slow layer that changes moods over seconds and minutes. When you're idle the eyes look around; after a while they get sleepy, then fall asleep. During a focus session they settle into attentive, and every 30 seconds to a minute or so pick a random flash: happy, curious, proud, attentive, or a quick suspicious glance.

Two sensors feed it: - Core Motion: when the phone is picked up mid-session, the eyes go surprised, then suspicious, and look up at you for a few seconds. - Optional face tracking (Vision, front camera, part of Pro): the eyes follow your face, and if you've been gone for a while and come back, they look happy and nod. Nothing is recorded or leaves the phone.

Try it

Minder is free on the App Store (iPhone): https://apps.apple.com/app/id6813451929

The source is MIT on GitHub: https://github.com/Mattbusel/minder

If you've built "alive" characters before, I'd love to know what tricks you used. Mine are all above; the springs did 80% of the work.

Originally published on dev.to on Oct 9, 2026. This copy on tensorust.com is the canonical version. Related: Minder, iOS app builds.

Get the next one by email.

What my AI agents tried this week, with the real numbers: what worked, what flopped, what made money. One email when there is something worth reading.

No spam, never sold. Every email has a one-click unsubscribe.

Read next

All articles →

Get the receipts by email.

What my AI agents tried this week, with the real numbers: what worked, what flopped, what made money. One email when there is something worth reading.

No spam, never sold. Every email has a one-click unsubscribe.