Import Coucou 0.1.9 (Louis-CFM/coucou@83708fe), Mac app only
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
import SwiftUI
|
||||
|
||||
/// Mochi in a fixed pose: one frame of BotEngine, no animation, no timer.
|
||||
/// For surfaces that can't animate (widgets, lists on the iPhone). The Mac
|
||||
/// island keeps its animated BotCanvasView / MiniBotCanvasView.
|
||||
struct MochiStill: View {
|
||||
var state: BotState = .idle
|
||||
/// Overrides the state's eyes (e.g. .closed for a sleeping agent).
|
||||
var eye: EyeShape? = nil
|
||||
/// Body color; white by default, like the mockups.
|
||||
var bodyHex: String = "#FFFFFF"
|
||||
var showBadge: Bool = true
|
||||
/// Where he looks and how his eyes are, for a still that isn't always the same.
|
||||
var pose: MochiPose = .neutral
|
||||
|
||||
var body: some View {
|
||||
Canvas { context, size in
|
||||
let engine = BotEngine()
|
||||
engine.isMini = true
|
||||
engine.bodyColor = cgColorFromHex(bodyHex)
|
||||
engine.state = state
|
||||
engine.cfg = BotStates[state]!
|
||||
engine.yaw = pose.yaw
|
||||
engine.pitch = pose.pitch
|
||||
engine.tilt = pose.tilt
|
||||
engine.open = pose.open
|
||||
if let eye = eye ?? pose.eye {
|
||||
engine.permanentEye = eye
|
||||
engine.eyeOverride = eye
|
||||
engine.eyeOverrideUntil = .greatestFiniteMagnitude
|
||||
}
|
||||
engine.draw(context: context, size: size)
|
||||
if showBadge, let badge = engine.cfg.badge {
|
||||
engine.badge = badge
|
||||
engine.badgeS = 1
|
||||
engine.drawHandsAndExtras(context: context, size: size)
|
||||
}
|
||||
}
|
||||
.aspectRatio(1, contentMode: .fit)
|
||||
}
|
||||
}
|
||||
|
||||
/// One frame of the little life the Mac's mini Mochi have: looking around,
|
||||
/// blinking, a happy squint. Widgets can't animate, so each refresh of the
|
||||
/// timeline picks a new pose per Mochi; the system cross-fades between them.
|
||||
struct MochiPose: Equatable, Sendable {
|
||||
var yaw: CGFloat = 0
|
||||
var pitch: CGFloat = 0
|
||||
var tilt: CGFloat = 0
|
||||
var open: CGFloat = 1
|
||||
var eye: EyeShape? = nil
|
||||
|
||||
static let neutral = MochiPose()
|
||||
|
||||
static let idlePoses: [MochiPose] = [
|
||||
MochiPose(),
|
||||
MochiPose(yaw: -0.45),
|
||||
MochiPose(yaw: 0.45),
|
||||
MochiPose(yaw: -0.3, pitch: -0.25),
|
||||
MochiPose(yaw: 0.35, pitch: 0.2, tilt: 0.08),
|
||||
MochiPose(pitch: -0.3, tilt: -0.06),
|
||||
MochiPose(eye: .closed), // blink
|
||||
MochiPose(yaw: 0.2, eye: .happy),
|
||||
MochiPose(yaw: -0.5, tilt: -0.1),
|
||||
MochiPose(yaw: 0.15, pitch: 0.25),
|
||||
]
|
||||
|
||||
/// A pose that differs per Mochi and per tick, the same every time for a
|
||||
/// given pair (so a widget redraw doesn't jump).
|
||||
static func idle(id: String, tick: Int) -> MochiPose {
|
||||
var hash: UInt64 = 5381
|
||||
for byte in id.utf8 { hash = (hash &* 33) &+ UInt64(byte) }
|
||||
hash = (hash &* 31) &+ UInt64(truncatingIfNeeded: tick &* 7919)
|
||||
hash ^= hash >> 13
|
||||
return idlePoses[Int(hash % UInt64(idlePoses.count))]
|
||||
}
|
||||
}
|
||||
|
||||
extension Color {
|
||||
/// Background for a white Mochi on an agent's color. Very light colors
|
||||
/// (VS Code's is near-white) would hide him, so they get a dark gray tile.
|
||||
static func mochiTile(hex: String) -> Color {
|
||||
guard let c = cgColorFromHex(hex), let comps = c.components, comps.count >= 3 else {
|
||||
return Color(hex: hex)
|
||||
}
|
||||
let luminance = 0.2126 * comps[0] + 0.7152 * comps[1] + 0.0722 * comps[2]
|
||||
return luminance > 0.7 ? Color(white: 0.32) : Color(hex: hex)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user