Icon: add ear holes on voice wake
This commit is contained in:
@@ -192,7 +192,8 @@ private struct CritterStatusLabel: View {
|
|||||||
blink: self.blinkAmount,
|
blink: self.blinkAmount,
|
||||||
legWiggle: max(self.legWiggle, self.isWorking ? 0.6 : 0),
|
legWiggle: max(self.legWiggle, self.isWorking ? 0.6 : 0),
|
||||||
earWiggle: self.earWiggle,
|
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)
|
.frame(width: 18, height: 16)
|
||||||
.rotationEffect(.degrees(self.wiggleAngle), anchor: .center)
|
.rotationEffect(.degrees(self.wiggleAngle), anchor: .center)
|
||||||
.offset(x: self.wiggleOffset)
|
.offset(x: self.wiggleOffset)
|
||||||
@@ -342,7 +343,8 @@ enum CritterIconRenderer {
|
|||||||
blink: CGFloat,
|
blink: CGFloat,
|
||||||
legWiggle: CGFloat = 0,
|
legWiggle: CGFloat = 0,
|
||||||
earWiggle: CGFloat = 0,
|
earWiggle: CGFloat = 0,
|
||||||
earScale: CGFloat = 1) -> NSImage
|
earScale: CGFloat = 1,
|
||||||
|
earHoles: Bool = false) -> NSImage
|
||||||
{
|
{
|
||||||
let image = NSImage(size: size)
|
let image = NSImage(size: size)
|
||||||
image.lockFocus()
|
image.lockFocus()
|
||||||
@@ -362,6 +364,16 @@ enum CritterIconRenderer {
|
|||||||
let earW = w * 0.22
|
let earW = w * 0.22
|
||||||
let earH = bodyH * 0.66 * earScale * (1 - 0.08 * abs(earWiggle))
|
let earH = bodyH * 0.66 * earScale * (1 - 0.08 * abs(earWiggle))
|
||||||
let earCorner = earW * 0.24
|
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 legW = w * 0.11
|
||||||
let legH = h * 0.26
|
let legH = h * 0.26
|
||||||
@@ -385,20 +397,12 @@ enum CritterIconRenderer {
|
|||||||
cornerHeight: bodyCorner,
|
cornerHeight: bodyCorner,
|
||||||
transform: nil))
|
transform: nil))
|
||||||
ctx.addPath(CGPath(
|
ctx.addPath(CGPath(
|
||||||
roundedRect: CGRect(
|
roundedRect: leftEarRect,
|
||||||
x: bodyX - earW * 0.55 + earWiggle,
|
|
||||||
y: bodyY + bodyH * 0.08 + earWiggle * 0.4,
|
|
||||||
width: earW,
|
|
||||||
height: earH),
|
|
||||||
cornerWidth: earCorner,
|
cornerWidth: earCorner,
|
||||||
cornerHeight: earCorner,
|
cornerHeight: earCorner,
|
||||||
transform: nil))
|
transform: nil))
|
||||||
ctx.addPath(CGPath(
|
ctx.addPath(CGPath(
|
||||||
roundedRect: CGRect(
|
roundedRect: rightEarRect,
|
||||||
x: bodyX + bodyW - earW * 0.45 - earWiggle,
|
|
||||||
y: bodyY + bodyH * 0.08 - earWiggle * 0.4,
|
|
||||||
width: earW,
|
|
||||||
height: earH),
|
|
||||||
cornerWidth: earCorner,
|
cornerWidth: earCorner,
|
||||||
cornerHeight: earCorner,
|
cornerHeight: earCorner,
|
||||||
transform: nil))
|
transform: nil))
|
||||||
@@ -416,6 +420,33 @@ enum CritterIconRenderer {
|
|||||||
let leftCenter = CGPoint(x: w / 2 - eyeOffset, y: eyeY)
|
let leftCenter = CGPoint(x: w / 2 - eyeOffset, y: eyeY)
|
||||||
let rightCenter = 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()
|
let left = CGMutablePath()
|
||||||
left.move(to: CGPoint(x: leftCenter.x - eyeW / 2, y: leftCenter.y - eyeH))
|
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))
|
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).
|
- **Idle:** Normal icon animation (blink, occasional wiggle).
|
||||||
- **Paused:** Status item uses `appearsDisabled`; no motion.
|
- **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.
|
- **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
|
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.
|
- 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
|
Shapes & sizes
|
||||||
- Base icon drawn in `CritterIconRenderer.makeIcon(blink:legWiggle:earWiggle:earScale:)`.
|
- Base icon drawn in `CritterIconRenderer.makeIcon(blink:legWiggle:earWiggle:earScale:earHoles:)`.
|
||||||
- Ear scale defaults to `1.0`; voice boost sets `earScale=1.9` without changing overall frame (18×16pt template image).
|
- 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.
|
- Scurry uses leg wiggle up to ~1.0 with a small horizontal jiggle; it’s additive to any existing idle wiggle.
|
||||||
|
|
||||||
Behavioral notes
|
Behavioral notes
|
||||||
|
|||||||
Reference in New Issue
Block a user