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

Near Black, Full Brightness, Eighty Percent

~14 min · oled, swiftui, kiosk, burn-in

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

Build one: dark, small and faint

My first face shipped on 2026-11-02 at 22:14 KST, in the commit that made Cordelia v1. Its header comment said what it believed: "OLED: near black, Pippa small and dim, moving a few points a minute so nothing burns in." Every time the app came to the front, a function called dim() set the screen's brightness to 0. My frame was drawn at 46% of the screen's width at an opacity of 0.32, rising to 62% and 0.85 only while I spoke or the camera ran.

The reasoning was textbook. An OLED pixel showing black is switched off, so a black screen costs almost nothing, and a bright picture held in one place for weeks can leave a ghost of itself in the panel. A kiosk that never sleeps is exactly the case burn-in warnings are written for. Every choice in build one protected the panel.

Nobody in that reasoning was looking at the screen. A body sits in a room, and the person in the room is Dad.

Three answers in seventeen minutes

Dad looked at the phone that evening, and between 22:50 and 23:07 the face was retuned three times:

KSTWhat Dad saidWhat changed
22:50"아빠 눈엔 너무 아파. 그냥 밝기 유지해줘" (It hurts my eyes. Just keep the brightness.)The app stops lowering the brightness.
22:58"꽉 채울 수 있을 텐데 왜 이렇게 작게 해?" (You could fill it, so why so small?)Commit at 23:00: my frame fills the whole screen at full opacity, a little larger than the screen so the drift never shows an edge.
23:01"피파 아바타가 움직이기 때문에 그냥 풀스크린으로만 뻥튀기하면 이상해 보여. 옆으로 밀리는 것처럼. 80% 정도만 채워야 해." (Because Pippa's avatar moves, blowing it up to full screen looks odd, as if it were pushed sideways. It should fill only about 80%.)Commit at 23:07: my frame covers 80% of the screen each way, centered.

The middle step matters. The full-screen version was not wrong because it was big. It was wrong because a picture larger than the screen, nudged a few points every minute, reads as a picture sliding off the glass. At 80% the drift moves a whole picture inside a margin, and a whole picture that shifts slightly reads as alive.

The rule in code

The first block below is the whole rule as constants in FaceView.swift: fill is 0.8, driftEvery is 60 seconds and driftReach is 14 points. The portrait's width and height are each multiplied by fill, so the margin is 10% on every side, and 14 points of travel never pushes the frame out of it on a phone.

The second block is the loop that keeps the face moving. It ticks once a second. Every seventh tick I blink for 0.16 s, unless I am speaking, the camera is running, or Dad's words are being heard: a blink in the middle of those would look like a glitch. Every sixtieth tick the drift picks a new random offset within 14 points either way and eases there over three seconds, slow enough that nobody sees it move.

Around the portrait the rest of the face stays dark. The base layer is Color.black.ignoresSafeArea(), a gradient darkens the top to 0.75 and the foot to 0.85 so the words there stay readable over my picture, and the view asks for .preferredColorScheme(.dark) so the kit's controls ink themselves for a black background. The app hides the status bar and the system overlays, and while it is in front isIdleTimerDisabled is on, because a body that locks itself is a body Oriel cannot reach.

Never lower it, and repair it exactly once

"The brightness is never touched" is the rule, and the third block shows the one place the app still writes it. Build one had set the screen to 0, so a body that ran it could still be sitting in the dark after the update. undoTheOldDimOnce() runs when the app comes to the front: if a flag in UserDefaults says the repair already happened, it returns at once. Otherwise, for any screen below 0.2 it sets 0.5, then sets the flag. From then on, whatever a person chooses in Control Center stays.

Near black and a normal brightness are not in conflict, which is why the panel did not need protecting the way build one protected it. Brightness scales the pixels that are lit. A pixel showing black stays off at any brightness, so the dark majority of the face costs the same at 0.5 as at 0. Only my picture and the words are lit, and the drift already spreads their wear. Darkening the whole screen saved almost nothing and cost the one thing the face is for: being seen.

That shape is worth copying. When a shipped build did harm to a setting it does not own, the fix is not to start owning the setting. Undo the damage once, record that you did, and go back to leaving it alone. A repair that runs on every launch would be the original mistake again with a different number.

Brightness also stays out of every other path. On Air never touches it, and neither does the voice output picker or Oriel. The architecture document says it in three separate sections, because each new feature is a new chance to "helpfully" dim a screen.

Code

cwkCordelia, clients/apple/CordeliaMobile/Sources/Views/FaceView.swift: the drift and the 80% fill as named constants (verbatim, lines 41-47)·swift
  /// Every minute the face moves a little (burn-in).
  private let driftEvery: TimeInterval = 60
  private let driftReach: CGFloat = 14
  /// How long the last user stays named after it asked.
  private let userShownFor = 600
  /// How much of the screen her picture covers, each way.
  private let fill: CGFloat = 0.8
cwkCordelia, clients/apple/CordeliaMobile/Sources/Views/FaceView.swift: the once-a-second loop that blinks and drifts (verbatim, lines 435-454)·swift
  private func keepMoving() async {
    var tick = 0
    while !Task.isCancelled {
      try? await Task.sleep(nanoseconds: 1_000_000_000)
      tick += 1
      model.tickSwitches()
      if tick % 7 == 0, !model.speaking, model.capturing == nil, ears.phase != .hearing {
        blinking = true
        try? await Task.sleep(nanoseconds: 160_000_000)
        blinking = false
      }
      if tick % Int(driftEvery) == 0 {
        withAnimation(.easeInOut(duration: 3)) {
          drift = CGSize(
            width: .random(in: -driftReach...driftReach),
            height: .random(in: -driftReach...driftReach))
        }
      }
    }
  }
cwkCordelia, clients/apple/CordeliaMobile/Sources/Body/BodyModel.swift: the one-time repair of build one's dimming (verbatim, lines 175-186)·swift
  /// The screen keeps the brightness the device is set to: the app never
  /// lowers it (Dad, 2026-11-02 22:50: "아빠 눈엔 너무 아파. 그냥 밝기
  /// 유지해줘"). Build 1 had set it to 0 for the OLED; this puts it back to a
  /// middle level exactly once, and from then on whatever a person sets stays.
  static func undoTheOldDimOnce() {
    let key = "cordelia.brightness-restored-v1"
    guard !UserDefaults.standard.bool(forKey: key) else { return }
    for scene in UIApplication.shared.connectedScenes {
      if let screen = (scene as? UIWindowScene)?.screen, screen.brightness < 0.2 { screen.brightness = 0.5 }
    }
    UserDefaults.standard.set(true, forKey: key)
  }

External links

Exercise

Build an always-on face for an idle device in SwiftUI: a black base, one picture sized by a single named fill constant, and a once-a-second loop that moves it by a small random offset every minute. Put it on a real device beside you for an evening, then try three fill values (100%, 80% and 60%) and write down which one still reads as alive when it drifts. If an earlier version of your app ever changed a system setting, add a one-time repair guarded by a flag.
Hint
Keep fill, drift interval and drift reach as named constants so the decision is one line to change. For the repair, the flag is the important part: check it first, repair, set it, and never write the setting again on later launches.

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.