Archived
wip: Nearlight redesign implementation
This commit is contained in:
1 parent
c76afbd11e
commit
fa21c81787
9 files changed
+904
-273
No files matched your search
@@ -0,0 +1,122 @@
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
/// The Nearlight signature element: an upright, softly lit aperture.
|
||||
///
|
||||
/// Two capsules in a ZStack — no assets, shaders, blur, or particles. The
|
||||
/// inner capsule carries a vertical Rosewood gradient; its geometry encodes
|
||||
/// conversation state and audio level. Noninteractive and accessibility
|
||||
/// hidden: the companion status text carries meaning.
|
||||
struct LightwellView: View {
|
||||
/// Lightwell presentation state, mapped from session/conversation models.
|
||||
enum Phase: Equatable {
|
||||
case idle
|
||||
case connecting
|
||||
case waiting
|
||||
case listening(level: Double)
|
||||
case thinking
|
||||
case speaking(level: Double)
|
||||
case tearingDown
|
||||
case failed
|
||||
/// Input never drives the light (muted or Type mode).
|
||||
case readyNoInput
|
||||
}
|
||||
|
||||
let phase: Phase
|
||||
/// Compact (24×36) in the conversation status strip; full (64×96) on welcome.
|
||||
var compact: Bool = false
|
||||
@Environment(\.accessibilityReduceMotion) private var reduceMotion
|
||||
/// Pending-state pulse driver, 0 → 1 → 0 over the 2.4s cycle.
|
||||
@State private var pulse: Double = 0
|
||||
|
||||
/// Frame dimensions never animate; inner dimensions multiply by 0.375
|
||||
/// in the compact frame.
|
||||
private var frameSize: CGSize {
|
||||
compact ? CGSize(width: 24, height: 36) : CGSize(width: 64, height: 96)
|
||||
}
|
||||
private var baseInner: CGSize {
|
||||
let base = CGSize(width: 16, height: 40)
|
||||
return compact
|
||||
? CGSize(width: base.width * 0.375, height: base.height * 0.375)
|
||||
: base
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
let inner = innerSize
|
||||
ZStack {
|
||||
Capsule()
|
||||
.fill(Nearlight.surface)
|
||||
Capsule()
|
||||
.fill(
|
||||
LinearGradient(
|
||||
stops: [
|
||||
.init(color: Nearlight.rosewood.opacity(0.35), location: 0),
|
||||
.init(color: Nearlight.rosewood.opacity(1), location: 0.5),
|
||||
.init(color: Nearlight.rosewood.opacity(0.55), location: 1),
|
||||
],
|
||||
startPoint: .top, endPoint: .bottom
|
||||
)
|
||||
)
|
||||
.frame(width: inner.width, height: inner.height)
|
||||
.opacity(overallOpacity)
|
||||
.animation(reduceMotion ? nil : Nearlight.quick, value: overallOpacity)
|
||||
.animation(reduceMotion ? nil : Nearlight.settle, value: innerSize)
|
||||
}
|
||||
.frame(width: frameSize.width, height: frameSize.height)
|
||||
.overlay(
|
||||
Capsule().strokeBorder(Nearlight.quiet, lineWidth: 1)
|
||||
)
|
||||
.accessibilityHidden(true)
|
||||
.task(id: pulseActive) {
|
||||
guard pulseActive else {
|
||||
pulse = 0
|
||||
return
|
||||
}
|
||||
while !Task.isCancelled {
|
||||
withAnimation(reduceMotion ? nil : Nearlight.cycle) { pulse = pulse == 0 ? 1 : 0 }
|
||||
try? await Task.sleep(for: .seconds(2.4))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the pulse loop should run for this phase.
|
||||
private var pulseActive: Bool {
|
||||
switch phase {
|
||||
case .connecting, .waiting, .thinking: return !reduceMotion
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
|
||||
/// Inner capsule geometry per spec table. `a` is the smoothed level.
|
||||
private var innerSize: CGSize {
|
||||
let base = baseInner
|
||||
switch phase {
|
||||
case .listening(let a):
|
||||
let level = min(max(a, 0), 1)
|
||||
return CGSize(width: base.width + (compact ? 3 : 8) * level,
|
||||
height: base.height + (compact ? 6 : 16) * level)
|
||||
case .speaking(let a):
|
||||
let level = min(max(a, 0), 1)
|
||||
return CGSize(width: base.width + (compact ? 4.5 : 12) * level,
|
||||
height: base.height + (compact ? 9 : 24) * level)
|
||||
case .waiting, .thinking:
|
||||
// Pulse drives opacity; height steps up one band.
|
||||
return CGSize(width: base.width, height: base.height + (compact ? 3 : 8))
|
||||
case .connecting, .idle, .failed, .tearingDown, .readyNoInput:
|
||||
return base
|
||||
}
|
||||
}
|
||||
|
||||
private var overallOpacity: Double {
|
||||
switch phase {
|
||||
case .connecting, .waiting:
|
||||
return 0.45 + 0.45 * pulse
|
||||
case .thinking:
|
||||
return 0.65 + 0.35 * pulse
|
||||
case .idle, .failed, .tearingDown:
|
||||
return 0.45
|
||||
case .listening, .speaking, .readyNoInput:
|
||||
return 1
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user