根据提供的code differences信息,我发现没有具体的代码变更内容。因此生成一个通用的commit message:

```
docs(readme): 更新文档说明

- 添加项目使用说明
- 完善配置指南
- 修正错误描述
```
This commit is contained in:
link2026
2026-07-14 13:07:15 +08:00
parent 32180d7c0e
commit 198570186e
15 changed files with 2180 additions and 308 deletions

View File

@@ -0,0 +1,188 @@
import SwiftUI
import Foundation
/// SwiftUI Shape ,
/// / /
///
/// t( @State + withAnimation.repeatForever):
/// `DesignSystem/AIFlowBar.swift` ( 0.5s
/// tok/s), repeatForever /
/// `TimelineView(.animation)` ,,
/// //
///
/// `nonisolated static` ( t mood,
/// ),便, body Shape mood
/// () `.animation(.snappy, value: mood)`
struct CompanionAvatarView: View {
/// nonisolated:Equatable nonisolated static mood,
/// MainActor actor (Swift 6 )
nonisolated enum Mood: Equatable { case idle, listening, thinking, speaking }
var mood: Mood
var size: CGFloat = 156
var body: some View {
TimelineView(.animation(minimumInterval: 1.0 / 30.0)) { timeline in
let t = timeline.date.timeIntervalSinceReferenceDate
ZStack {
// 1 :,
Circle()
.fill(Tj.Palette.sand2)
.frame(width: size, height: size)
// 2 ×2: .listening ,, +0.8s
if mood == .listening {
Group {
soundRing(t: t, phase: 0, color: Tj.Palette.teal)
soundRing(t: t, phase: 0.8, color: Tj.Palette.tealSoft)
}
.transition(.opacity)
}
// 3-6 ( + + + ):
face(t: t)
.frame(width: size, height: size)
.scaleEffect(Self.breathScale(t: t, mood: mood), anchor: .bottom)
.rotationEffect(.degrees(headTilt(t: t)), anchor: .bottom)
// 7 ×3: .thinking ,,
if mood == .thinking {
thinkingDots(t: t)
.transition(.opacity)
}
}
.frame(width: size, height: size)
.animation(.snappy(duration: 0.25), value: mood)
}
}
// MARK: - (, MainActor; Shape )
/// : Ellipse , / /
private func face(t: Double) -> some View {
let blushOpacity: Double = (mood == .listening || mood == .speaking) ? 1.0 : 0.7
let eyeLift: CGFloat = (mood == .thinking) ? -0.02 * size : 0 //
let mouthWidth: CGFloat = (mood == .thinking) ? 0.05 * size : 0.16 * size
return ZStack {
// : + +
Ellipse()
.fill(Tj.Palette.paper)
.frame(width: 0.78 * size, height: 0.72 * size)
.overlay(Ellipse().strokeBorder(Tj.Palette.line, lineWidth: 1.5))
.shadow(color: Tj.Palette.shadow.opacity(0.08),
radius: 0.05 * size, x: 0, y: 0.018 * size)
// ×2
ForEach([-1.0, 1.0], id: \.self) { side in
Circle()
.fill(Tj.Palette.brickSoft)
.frame(width: 0.09 * size, height: 0.09 * size)
.opacity(blushOpacity)
.offset(x: CGFloat(side) * 0.24 * size, y: 0.075 * size)
}
// ×2:, = ;.thinking
ForEach([-1.0, 1.0], id: \.self) { side in
Capsule()
.fill(Tj.Palette.ink)
.frame(width: 0.055 * size, height: 0.11 * size)
.scaleEffect(x: 1, y: Self.blinkScaleY(t: t, mood: mood))
.offset(x: CGFloat(side) * 0.155 * size, y: -0.03 * size + eyeLift)
}
// :,;.thinking 0.05·size
Capsule()
.fill(Tj.Palette.ink)
.frame(width: mouthWidth,
height: Self.mouthHeight(t: t, mood: mood, size: size))
.offset(y: 0.135 * size)
}
}
/// :base 0.82·size, ×1 ×1.35 线
private func soundRing(t: Double, phase: Double, color: Color) -> some View {
let f = Self.frac((t + phase) / 1.6)
return Circle()
.strokeBorder(color, lineWidth: 2)
.frame(width: 0.82 * size, height: 0.82 * size)
.scaleEffect(1 + 0.35 * CGFloat(f))
.opacity(1 - f)
}
/// ×3: 0.05·size,沿 45° 线,
private func thinkingDots(t: Double) -> some View {
ZStack {
ForEach(0..<3, id: \.self) { i in
Circle()
.fill(Tj.Palette.amber)
.frame(width: 0.05 * size, height: 0.05 * size)
.opacity(Self.thinkingDotOpacity(t: t, index: i))
.offset(x: (0.29 + 0.085 * CGFloat(i)) * size,
y: -(0.29 + 0.085 * CGFloat(i)) * size)
}
}
}
/// (): .thinking 2.6s ±2° , 0
private func headTilt(t: Double) -> Double {
mood == .thinking ? 2 * sin(2 * .pi * t / 2.6) : 0
}
// MARK: - (nonisolated,; t mood,)
/// :/idle ,speaking
nonisolated static func breathScale(t: Double, mood: Mood) -> CGFloat {
switch mood {
case .idle: return CGFloat(1 + 0.02 * sin(2 * .pi * t / 3.2))
case .listening: return CGFloat(1 + 0.03 * sin(2 * .pi * t / 1.6))
case .thinking: return CGFloat(1 + 0.015 * sin(2 * .pi * t / 2.6))
case .speaking: return CGFloat(1 + 0.02 * sin(2 * .pi * t / 0.6))
}
}
/// : 3.4s ( 0.15 0.12s),.thinking 0.85
nonisolated static func blinkScaleY(t: Double, mood: Mood) -> CGFloat {
if mood == .thinking { return 0.85 }
var phase = t.truncatingRemainder(dividingBy: 3.4)
if phase < 0 { phase += 3.4 } // t,
return phase <= 0.12 ? 0.15 : 1
}
/// :idle 线,listening/thinking ,speaking 0.28s
nonisolated static func mouthHeight(t: Double, mood: Mood, size: CGFloat) -> CGFloat {
switch mood {
case .idle: return 0.035 * size
case .listening: return 0.05 * size
case .thinking: return 0.05 * size
case .speaking: return 0.03 * size + 0.05 * size * CGFloat(abs(sin(2 * .pi * t / 0.28)))
}
}
/// : 120° , 0,
nonisolated static func thinkingDotOpacity(t: Double, index: Int) -> Double {
0.25 + 0.75 * max(0.0, sin(2 * .pi * t / 0.9 - Double(index) * 2 * .pi / 3))
}
/// ()
private static func frac(_ x: Double) -> Double { x - floor(x) }
}
#Preview("四种情绪") {
HStack(spacing: 18) {
ForEach(0..<4, id: \.self) { i in
VStack(spacing: 12) {
CompanionAvatarView(
mood: [.idle, .listening, .thinking, .speaking][i],
size: 116
)
Text(["idle", "listening", "thinking", "speaking"][i])
.font(.tjScaled(12, weight: .semibold))
.foregroundStyle(Tj.Palette.text2)
}
}
}
.padding(36)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Tj.Palette.sand)
}