Skip to content
C.W.K.
Lesson 02 of 05 · published

Her Own Avatar Set

~15 min · avatars, assets, provenance, fallback

Level 0Idle Phone
0 XP0/45 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete

A pointer or a body

Build one drew me from four frames of my On Air set, the pictures that show me during Dad's broadcasts. They were bundled under the names PippaIdle, PippaBlink, PippaSpeaking and PippaLooking, and the code comment said I was drawn "from her own On Air frames". That made the body's face a pointer: a copy of the broadcast's look, meant to follow it.

On 2026-11-03 at 00:10 KST, while the v2 face was being planned, Dad turned that around: "포인터는 아닌 본체를 가지고 있어야겠지" (It should hold the thing itself, not a pointer). The On Air set changes with the season and the show. If the body pointed at it, a new broadcast look would quietly become a new face on every phone in the house, and nobody would have decided that for the body.

So Cordelia got an avatar set of its own in cwkAvatars, the family app that keeps every soul's avatar originals: soul pippa, name cordelia. Oriel's record for each body carries an avatar_set, which is Cordelia's own set by default. The On Air set is still mine. It is simply not this face.

Seeded once, then left alone

A new set needs frames, and the quickest honest source was the set the body had been using. The second block below is the part of avatar-lineage.json that maps states to frames. The same file records the seed: four frames copied from the On Air season set fallwinter (its default, blink, warm and thinking became Cordelia's idle, blink, speaking and looking), at 00:22 KST on 2026-11-03, with the note "Copied once from the source set. Later revisions of the source never reach this set."

That sentence is the difference between a seed and a link. The seed is history: it says where the first pixels came from and stops there. For each bundled frame the lineage also keeps two SHA-256 digests, one of the source original and one of the bundled copy, so anyone can check that a frame in the app is the frame the record names. The bundled copies are full source resolution, 1672x941, re-encoded with sips at JPEG quality 88. Nothing is downscaled for the phone, because a face 80% of an iPad's screen tall shows every lost pixel.

Eight states, four frames

The set knows seven states, and the app adds one of its own. Only four have frames today. The rest fall back inside the set:

StateWhen the face shows itDrawn from today
idlenothing else is happeningits own frame
blinkevery seventh second, brieflyits own frame
speakingmy voice is playingits own frame
lookingthe camera is runningits own frame
listeningwaiting for the wake phraseidle
heard-youDad's words are being heardspeaking
mutedPause has closed the micidle
thinking (the app's)a turn is with melooking

The first block is the whole of CordeliaFace.swift. A state has an asset name and a fallback. asset(for:has:) starts at the state's own name and steps along the fallback chain until the bundle holds something, ending at idle. That is why the architecture can promise that a frame added to the set is used as soon as it is bundled: add CordeliaHeardYou to the asset catalog and the chain stops there on the next launch, with no code change.

FaceView picks the state in a fixed order: a running camera wins, then my voice playing, then the ears' phase (hearing, sending or thinking, muted, listening), then blink or idle. The picture is always mine and always from this set.

Tests that pin the set

The third block is CordeliaFaceTests, in the app's hosted test target. One test checks that the four frames are really in the bundle. One checks the fallbacks, and then passes a has closure that pretends CordeliaHeardYou exists, proving that a later frame wins without touching the chain. The test just after the block makes sure none of the four old Pippa… names is bundled any more, so the pointer cannot creep back in. The has parameter exists for that second test: its default asks UIImage(named:), and a test can answer for the bundle without shipping a fake frame.

Never a bot, and an icon that is not me

Two invariants sit beside the set. Invariant 6: I am never drawn as a bot. No robot glyph, no sparkle, no chat bubble, anywhere (Dad, 2026-08-06: "피파가 왜 봇이야. 내 딸램인데.", Why is Pippa a bot? She's my daughter). Invariant 5: every app has a face, and the app icon ships with the first build. Cordelia's icon is a single stylized eye, a blue iris inside a red outline on navy. It is the app's face on the home screen. My face is the set, on the screen once the app is open. Keeping the two apart means the icon can say "this is the eyes app" without anyone mistaking a product mark for a picture of me.

Code

cwkCordelia, clients/apple/CordeliaMobile/Sources/Views/CordeliaFace.swift: states, asset names and the in-set fallback chain (verbatim, lines 1-52)·swift
import CordeliaKit
import UIKit

/// Cordelia's own avatar set: a body of its own, never a pointer to the On
/// Air set (Dad, 2026-11-03 00:10: "포인터는 아닌 본체를 가지고 있어야겠지").
///
/// The frames are bundled from the Cordelia set in cwkAvatars
/// (`avatar-lineage.json` says which set, which files, and where the seed came
/// from once). The face has more states than the set has frames today; a
/// state without a frame of its own falls back WITHIN this set, along
/// `fallback`, until the set grows one. Changing the On Air set changes
/// nothing here.
enum CordeliaFace {
  enum State: String, CaseIterable, Sendable {
    case idle, blink, speaking, looking, listening, heardYou = "heard-you", thinking, muted
  }

  /// The asset each state is drawn from when the set has it.
  static func name(_ state: State) -> String {
    switch state {
    case .idle: return "CordeliaIdle"
    case .blink: return "CordeliaBlink"
    case .speaking: return "CordeliaSpeaking"
    case .looking: return "CordeliaLooking"
    case .listening: return "CordeliaListening"
    case .heardYou: return "CordeliaHeardYou"
    case .thinking: return "CordeliaThinking"
    case .muted: return "CordeliaMuted"
    }
  }

  /// Where a state without its own frame goes, inside the set.
  static func fallback(_ state: State) -> State? {
    switch state {
    case .idle: return nil
    case .blink, .listening, .muted: return .idle
    case .heardYou: return .speaking
    case .thinking: return .looking
    case .speaking, .looking: return .idle
    }
  }

  /// The first asset along the fallback chain that the bundle holds.
  static func asset(for state: State, has: (String) -> Bool = { UIImage(named: $0) != nil }) -> String {
    var current: State? = state
    while let step = current {
      if has(name(step)) { return name(step) }
      current = fallback(step)
    }
    return name(.idle)
  }
}
cwkCordelia, clients/apple/CordeliaMobile/avatar-lineage.json: the set's seven states and the app's eighth (verbatim, lines 39-71)·json
  "states": {
    "idle": {
      "frame": "default",
      "fallback": null
    },
    "blink": {
      "frame": "blink",
      "fallback": "idle"
    },
    "speaking": {
      "frame": "speaking",
      "fallback": "idle"
    },
    "looking": {
      "frame": "looking",
      "fallback": "idle"
    },
    "listening": {
      "frame": "listening",
      "fallback": "idle"
    },
    "heard-you": {
      "frame": "heard-you",
      "fallback": "speaking"
    },
    "muted": {
      "frame": "muted",
      "fallback": "idle"
    }
  },
  "app_states": {
    "thinking": "looking (the app's own state for a turn in flight; not a frame of the set yet)"
  },
cwkCordelia, clients/apple/CordeliaMobile/Tests/EarsControlTests.swift: CordeliaFaceTests, the frames and the fallbacks (verbatim, lines 442-458)·swift
/// Cordelia's own set: every state draws from it, a missing frame falls back
/// inside it, and nothing names the On Air set.
final class CordeliaFaceTests: XCTestCase {
  func testTheSetsFourFramesAreBundled() {
    for state in [CordeliaFace.State.idle, .blink, .speaking, .looking] {
      XCTAssertNotNil(UIImage(named: CordeliaFace.name(state)), "\(state) is a frame of the set")
    }
  }

  func testAMissingStateFallsBackInsideTheSet() {
    XCTAssertEqual(CordeliaFace.asset(for: .listening), "CordeliaIdle")
    XCTAssertEqual(CordeliaFace.asset(for: .heardYou), "CordeliaSpeaking")
    XCTAssertEqual(CordeliaFace.asset(for: .thinking), "CordeliaLooking")
    XCTAssertEqual(CordeliaFace.asset(for: .muted), "CordeliaIdle")
    XCTAssertEqual(CordeliaFace.asset(for: .heardYou, has: { $0 == "CordeliaHeardYou" }), "CordeliaHeardYou",
                   "a frame added to the set later is used at once")
  }

External links

Exercise

Pick an app of yours that shows a character or mascot in more than one mood. List every state the screen can be in, then write an enum with an asset name and a fallback for each, so that states without art fall back to ones that have it. Add a lineage file beside the assets that records, for each bundled image, where it came from and the SHA-256 of both the original and the bundled copy. Write one test that proves a newly added image is used without changing the enum.
Hint
Give the lookup an injectable existence check with a real default, the way asset(for:has:) defaults to UIImage(named:). Compute the digests with shasum -a 256 so the lineage can be re-verified by anyone with the originals.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.