Icon: add ear holes on voice wake
This commit is contained in:
@@ -192,7 +192,8 @@ private struct CritterStatusLabel: View {
|
||||
blink: self.blinkAmount,
|
||||
legWiggle: max(self.legWiggle, self.isWorking ? 0.6 : 0),
|
||||
earWiggle: self.earWiggle,
|
||||
earScale: self.earBoostActive ? 1.9 : 1.0))
|
||||
earScale: self.earBoostActive ? 1.9 : 1.0,
|
||||
earHoles: self.earBoostActive))
|
||||
.frame(width: 18, height: 16)
|
||||
.rotationEffect(.degrees(self.wiggleAngle), anchor: .center)
|
||||
.offset(x: self.wiggleOffset)
|
||||
@@ -342,7 +343,8 @@ enum CritterIconRenderer {
|
||||
blink: CGFloat,
|
||||
legWiggle: CGFloat = 0,
|
||||
earWiggle: CGFloat = 0,
|
||||
earScale: CGFloat = 1) -> NSImage
|
||||
earScale: CGFloat = 1,
|
||||
earHoles: Bool = false) -> NSImage
|
||||
{
|
||||
let image = NSImage(size: size)
|
||||
image.lockFocus()
|
||||
@@ -362,6 +364,16 @@ enum CritterIconRenderer {
|
||||
let earW = w * 0.22
|
||||
let earH = bodyH * 0.66 * earScale * (1 - 0.08 * abs(earWiggle))
|
||||
let earCorner = earW * 0.24
|
||||
let leftEarRect = CGRect(
|
||||
x: bodyX - earW * 0.55 + earWiggle,
|
||||
y: bodyY + bodyH * 0.08 + earWiggle * 0.4,
|
||||
width: earW,
|
||||
height: earH)
|
||||
let rightEarRect = CGRect(
|
||||
x: bodyX + bodyW - earW * 0.45 - earWiggle,
|
||||
y: bodyY + bodyH * 0.08 - earWiggle * 0.4,
|
||||
width: earW,
|
||||
height: earH)
|
||||
|
||||
let legW = w * 0.11
|
||||
let legH = h * 0.26
|
||||
@@ -385,20 +397,12 @@ enum CritterIconRenderer {
|
||||
cornerHeight: bodyCorner,
|
||||
transform: nil))
|
||||
ctx.addPath(CGPath(
|
||||
roundedRect: CGRect(
|
||||
x: bodyX - earW * 0.55 + earWiggle,
|
||||
y: bodyY + bodyH * 0.08 + earWiggle * 0.4,
|
||||
width: earW,
|
||||
height: earH),
|
||||
roundedRect: leftEarRect,
|
||||
cornerWidth: earCorner,
|
||||
cornerHeight: earCorner,
|
||||
transform: nil))
|
||||
ctx.addPath(CGPath(
|
||||
roundedRect: CGRect(
|
||||
x: bodyX + bodyW - earW * 0.45 - earWiggle,
|
||||
y: bodyY + bodyH * 0.08 - earWiggle * 0.4,
|
||||
width: earW,
|
||||
height: earH),
|
||||
roundedRect: rightEarRect,
|
||||
cornerWidth: earCorner,
|
||||
cornerHeight: earCorner,
|
||||
transform: nil))
|
||||
@@ -416,6 +420,33 @@ enum CritterIconRenderer {
|
||||
let leftCenter = CGPoint(x: w / 2 - eyeOffset, y: eyeY)
|
||||
let rightCenter = CGPoint(x: w / 2 + eyeOffset, y: eyeY)
|
||||
|
||||
if earHoles || earScale > 1.05 {
|
||||
let holeW = earW * 0.6
|
||||
let holeH = earH * 0.46
|
||||
let holeCorner = holeW * 0.34
|
||||
let leftHoleRect = CGRect(
|
||||
x: leftEarRect.midX - holeW / 2,
|
||||
y: leftEarRect.midY - holeH / 2 + earH * 0.04,
|
||||
width: holeW,
|
||||
height: holeH)
|
||||
let rightHoleRect = CGRect(
|
||||
x: rightEarRect.midX - holeW / 2,
|
||||
y: rightEarRect.midY - holeH / 2 + earH * 0.04,
|
||||
width: holeW,
|
||||
height: holeH)
|
||||
|
||||
ctx.addPath(CGPath(
|
||||
roundedRect: leftHoleRect,
|
||||
cornerWidth: holeCorner,
|
||||
cornerHeight: holeCorner,
|
||||
transform: nil))
|
||||
ctx.addPath(CGPath(
|
||||
roundedRect: rightHoleRect,
|
||||
cornerWidth: holeCorner,
|
||||
cornerHeight: holeCorner,
|
||||
transform: nil))
|
||||
}
|
||||
|
||||
let left = CGMutablePath()
|
||||
left.move(to: CGPoint(x: leftCenter.x - eyeW / 2, y: leftCenter.y - eyeH))
|
||||
left.addLine(to: CGPoint(x: leftCenter.x + eyeW / 2, y: leftCenter.y))
|
||||
|
||||
@@ -4,7 +4,7 @@ Author: steipete · Updated: 2025-12-06 · Scope: macOS app (`apps/macos`)
|
||||
|
||||
- **Idle:** Normal icon animation (blink, occasional wiggle).
|
||||
- **Paused:** Status item uses `appearsDisabled`; no motion.
|
||||
- **Voice trigger (big ears):** Voice wake detector calls `AppState.triggerVoiceEars()` → `earBoostActive=true` for ~5s. Ears scale up (1.9x) then auto-reset. Only fired from the in-app voice pipeline.
|
||||
- **Voice trigger (big ears):** Voice wake detector calls `AppState.triggerVoiceEars()` → `earBoostActive=true` for ~5s. Ears scale up (1.9x), get circular ear holes for readability, then auto-reset. Only fired from the in-app voice pipeline.
|
||||
- **Working (agent running):** `AppState.isWorking=true` drives a “tail/leg scurry” micro-motion: faster leg wiggle and slight offset while work is in-flight. Currently toggled around WebChat agent runs; add the same toggle around other long tasks when you wire them.
|
||||
|
||||
Wiring points
|
||||
@@ -12,8 +12,8 @@ Wiring points
|
||||
- Agent activity: set `AppStateStore.shared.setWorking(true/false)` around work spans (already done in WebChat agent call). Keep spans short and reset in `defer` blocks to avoid stuck animations.
|
||||
|
||||
Shapes & sizes
|
||||
- Base icon drawn in `CritterIconRenderer.makeIcon(blink:legWiggle:earWiggle:earScale:)`.
|
||||
- Ear scale defaults to `1.0`; voice boost sets `earScale=1.9` without changing overall frame (18×16pt template image).
|
||||
- Base icon drawn in `CritterIconRenderer.makeIcon(blink:legWiggle:earWiggle:earScale:earHoles:)`.
|
||||
- Ear scale defaults to `1.0`; voice boost sets `earScale=1.9` and toggles `earHoles=true` without changing overall frame (18×16pt template image).
|
||||
- Scurry uses leg wiggle up to ~1.0 with a small horizontal jiggle; it’s additive to any existing idle wiggle.
|
||||
|
||||
Behavioral notes
|
||||
|
||||
Reference in New Issue
Block a user