Skip to content

Commit 05c9dcc

Browse files
committed
feat(apps): match the mascot design to the openclaw.ai hero
Aligns the shared OpenClawMascotView with the website hero styling: per-part bounding-box gradients (SVG objectBoundingBox semantics; the claws were nearly flat-colored before), antenna stroke width 2, eye glow r=2, float depth -8% of size, and a light-scheme palette from the site theme variables. The float now offsets the whole canvas like the site floats the hero container, so the antennae no longer clip at the float peak. macOS adopts the hero glow treatment: the About pane mascot gets the coral silhouette drop-shadow with the teal hover glow and 1.1 hover scale, and the onboarding hero replaces its hand-rolled breathing glow circle with the same silhouette glow.
1 parent 07bf384 commit 05c9dcc

4 files changed

Lines changed: 100 additions & 74 deletions

File tree

apps/macos/Sources/OpenClaw/AboutSettings.swift

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import SwiftUI
33

44
struct AboutSettings: View {
55
weak var updater: UpdaterProviding?
6+
@Environment(\.colorScheme) private var colorScheme
67
@State private var iconHover = false
78
@AppStorage("autoUpdateEnabled") private var autoCheckEnabled = true
89
@State private var didLoadUpdaterState = false
@@ -14,10 +15,16 @@ struct AboutSettings: View {
1415
NSWorkspace.shared.open(url)
1516
}
1617
} label: {
18+
// Hero treatment from openclaw.ai: coral silhouette glow at 10% of
19+
// size, teal glow at 15% plus scale 1.1 on hover.
1720
OpenClawMascotView()
1821
.frame(width: 160, height: 160)
19-
.shadow(color: self.iconHover ? .accentColor.opacity(0.25) : .clear, radius: 10)
20-
.scaleEffect(self.iconHover ? 1.05 : 1.0)
22+
.shadow(
23+
color: OpenClawMascotView.heroGlowColor(
24+
for: self.colorScheme,
25+
hovering: self.iconHover),
26+
radius: self.iconHover ? 24 : 16)
27+
.scaleEffect(self.iconHover ? 1.1 : 1.0)
2128
}
2229
.buttonStyle(.plain)
2330
.accessibilityLabel("OpenClaw on GitHub")

apps/macos/Sources/OpenClaw/OnboardingView+Layout.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import SwiftUI
44
extension OnboardingView {
55
var body: some View {
66
VStack(spacing: 0) {
7-
GlowingOpenClawIcon(size: 130, glowIntensity: 0.28)
7+
GlowingOpenClawIcon(size: 130)
88
.offset(y: 10)
99
.frame(height: 145)
1010

Lines changed: 9 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,22 @@
11
import OpenClawChatUI
22
import SwiftUI
33

4+
/// Onboarding hero mascot with the openclaw.ai hero treatment: the animated
5+
/// mascot plus its coral silhouette glow (drop-shadow at ~10% of size).
46
struct GlowingOpenClawIcon: View {
5-
@Environment(\.scenePhase) private var scenePhase
6-
@Environment(\.accessibilityReduceMotion) private var reduceMotion
7+
@Environment(\.colorScheme) private var colorScheme
78

89
let size: CGFloat
9-
let glowIntensity: Double
10-
let enableFloating: Bool
1110

12-
@State private var breathe = false
13-
14-
init(size: CGFloat = 148, glowIntensity: Double = 0.35, enableFloating: Bool = true) {
11+
init(size: CGFloat = 148) {
1512
self.size = size
16-
self.glowIntensity = glowIntensity
17-
self.enableFloating = enableFloating
1813
}
1914

2015
var body: some View {
21-
let glowBlurRadius: CGFloat = 18
22-
let glowCanvasSize: CGFloat = self.size + 56
23-
ZStack {
24-
Circle()
25-
.fill(
26-
LinearGradient(
27-
colors: [
28-
Color.accentColor.opacity(self.glowIntensity),
29-
Color.blue.opacity(self.glowIntensity * 0.6),
30-
],
31-
startPoint: .topLeading,
32-
endPoint: .bottomTrailing))
33-
.frame(width: glowCanvasSize, height: glowCanvasSize)
34-
.padding(glowBlurRadius)
35-
.blur(radius: glowBlurRadius)
36-
.scaleEffect(self.breathe ? 1.08 : 0.96)
37-
.opacity(0.84)
38-
39-
// Mascot animates itself (and goes still under reduce motion), so no breathe scale here.
40-
OpenClawMascotView()
41-
.frame(width: self.size, height: self.size)
42-
.shadow(color: .black.opacity(0.18), radius: 14, y: 6)
43-
}
44-
.frame(
45-
width: glowCanvasSize + (glowBlurRadius * 2),
46-
height: glowCanvasSize + (glowBlurRadius * 2))
47-
.onAppear { self.updateBreatheAnimation() }
48-
.onDisappear { self.breathe = false }
49-
.onChange(of: self.scenePhase) { _, _ in
50-
self.updateBreatheAnimation()
51-
}
52-
}
53-
54-
private func updateBreatheAnimation() {
55-
guard self.enableFloating, !self.reduceMotion, self.scenePhase == .active else {
56-
self.breathe = false
57-
return
58-
}
59-
guard !self.breathe else { return }
60-
withAnimation(Animation.easeInOut(duration: 3.6).repeatForever(autoreverses: true)) {
61-
self.breathe = true
62-
}
16+
OpenClawMascotView()
17+
.frame(width: self.size, height: self.size)
18+
.shadow(
19+
color: OpenClawMascotView.heroGlowColor(for: self.colorScheme),
20+
radius: self.size * 0.1)
6321
}
6422
}

apps/shared/OpenClawKit/Sources/OpenClawChatUI/OpenClawMascotView.swift

Lines changed: 81 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,66 @@ import SwiftUI
33
/// Animated OpenClaw mascot. Redraws the canonical 120x120 vector from
44
/// `ui/public/favicon.svg` so individual parts (claws, antennae, eyes) can
55
/// animate like the openclaw.ai hero mark; the bundled PNG asset cannot.
6+
/// Styling (palette, glow colors, float depth) follows the openclaw.ai hero
7+
/// (`src/pages/index.astro` + `Layout.astro` theme variables).
68
public struct OpenClawMascotView: View {
79
@Environment(\.accessibilityReduceMotion) private var reduceMotion
10+
@Environment(\.colorScheme) private var colorScheme
811

912
public init() {}
1013

1114
public var body: some View {
15+
let palette = OpenClawMascotPalette.forScheme(self.colorScheme)
1216
if self.reduceMotion {
13-
OpenClawMascotCanvas(pose: .still)
17+
OpenClawMascotCanvas(pose: .still, palette: palette)
1418
} else {
1519
TimelineView(.animation(minimumInterval: 1.0 / 30.0)) { timeline in
16-
OpenClawMascotCanvas(pose: .at(time: timeline.date.timeIntervalSinceReferenceDate))
20+
let pose = OpenClawMascotPose.at(time: timeline.date.timeIntervalSinceReferenceDate)
21+
// Float translates the whole canvas like the site floats the hero
22+
// container; drawing the offset inside the canvas would clip the
23+
// antennae (art starts at y~5 of 120) at the -9.6 float peak.
24+
GeometryReader { proxy in
25+
OpenClawMascotCanvas(pose: pose, palette: palette)
26+
.offset(y: pose.floatOffset * min(proxy.size.width, proxy.size.height) / 120)
27+
}
1728
}
1829
}
1930
}
31+
32+
/// openclaw.ai hero drop-shadow color (`--logo-glow` / `--logo-glow-hover`).
33+
/// Pair with a shadow radius of ~10% of the mascot size (15% while hovering)
34+
/// to match the site's `drop-shadow(0 0 20px)` on a 100px mark.
35+
public static func heroGlowColor(for colorScheme: ColorScheme, hovering: Bool = false) -> Color {
36+
switch (colorScheme, hovering) {
37+
case (.light, false): Color(red: 239 / 255, green: 75 / 255, blue: 88 / 255).opacity(0.2)
38+
case (.light, true): Color(red: 0, green: 143 / 255, blue: 135 / 255).opacity(0.35)
39+
case (_, false): Color(red: 1, green: 77 / 255, blue: 77 / 255).opacity(0.4)
40+
case (_, true): Color(red: 0, green: 229 / 255, blue: 204 / 255).opacity(0.6)
41+
}
42+
}
43+
}
44+
45+
/// Body/antenna colors from the openclaw.ai theme variables: `:root` (dark)
46+
/// and `html[data-theme='light']` in `Layout.astro`. Eye colors are fixed in
47+
/// the site markup and shared by both themes.
48+
struct OpenClawMascotPalette: Equatable {
49+
let gradientTop: Color
50+
let gradientBottom: Color
51+
let antenna: Color
52+
53+
static let dark = OpenClawMascotPalette(
54+
gradientTop: Color(red: 1, green: 77 / 255, blue: 77 / 255),
55+
gradientBottom: Color(red: 153 / 255, green: 27 / 255, blue: 27 / 255),
56+
antenna: Color(red: 1, green: 77 / 255, blue: 77 / 255))
57+
58+
static let light = OpenClawMascotPalette(
59+
gradientTop: Color(red: 255 / 255, green: 112 / 255, blue: 121 / 255),
60+
gradientBottom: Color(red: 234 / 255, green: 76 / 255, blue: 89 / 255),
61+
antenna: Color(red: 239 / 255, green: 75 / 255, blue: 88 / 255))
62+
63+
static func forScheme(_ colorScheme: ColorScheme) -> OpenClawMascotPalette {
64+
colorScheme == .light ? .light : .dark
65+
}
2066
}
2167

2268
/// Part transforms for one animation frame. Mirrors the openclaw.ai CSS
@@ -32,8 +78,9 @@ struct OpenClawMascotPose: Equatable {
3278
static let still = OpenClawMascotPose()
3379

3480
static func at(time: TimeInterval) -> OpenClawMascotPose {
81+
// Float depth matches the hero: -8px on a 100px mark = 8% of the 120 box.
3582
OpenClawMascotPose(
36-
floatOffset: -2.5 * (1 - cos(2 * .pi * self.cyclePhase(time, period: 4))),
83+
floatOffset: -4.8 * (1 - cos(2 * .pi * self.cyclePhase(time, period: 4))),
3784
antennaDegrees: -3 * sin(2 * .pi * self.cyclePhase(time, period: 2)),
3885
leftClawDegrees: self.clawSnapDegrees(phase: self.cyclePhase(time, period: 4)),
3986
rightClawDegrees: self.clawSnapDegrees(phase: self.cyclePhase(time - 0.2, period: 4)),
@@ -73,17 +120,16 @@ struct OpenClawMascotPose: Equatable {
73120

74121
private struct OpenClawMascotCanvas: View {
75122
let pose: OpenClawMascotPose
123+
let palette: OpenClawMascotPalette
76124

77125
var body: some View {
78126
Canvas { context, size in
79-
Self.draw(context: &context, size: size, pose: self.pose)
127+
Self.draw(context: &context, size: size, pose: self.pose, palette: self.palette)
80128
}
81129
.accessibilityHidden(true)
82130
}
83131

84132
// Geometry below is the favicon.svg path data in its native 120x120 space.
85-
private static let bodyColorTop = Color(red: 255 / 255, green: 77 / 255, blue: 77 / 255)
86-
private static let bodyColorBottom = Color(red: 153 / 255, green: 27 / 255, blue: 27 / 255)
87133
private static let eyeColor = Color(red: 5 / 255, green: 8 / 255, blue: 16 / 255)
88134
private static let eyeGlowColor = Color(red: 0, green: 229 / 255, blue: 204 / 255)
89135
// Rotation pivots: claws hinge on their body-facing edge, antennae on their own center.
@@ -144,44 +190,59 @@ private struct OpenClawMascotCanvas: View {
144190
return path
145191
}()
146192

147-
private static func draw(context: inout GraphicsContext, size: CGSize, pose: OpenClawMascotPose) {
193+
private static func draw(
194+
context: inout GraphicsContext,
195+
size: CGSize,
196+
pose: OpenClawMascotPose,
197+
palette: OpenClawMascotPalette)
198+
{
148199
let scale = min(size.width, size.height) / 120
149200
context.scaleBy(x: scale, y: scale)
150-
context.translateBy(x: 0, y: pose.floatOffset)
151201

152-
let bodyShading = GraphicsContext.Shading.linearGradient(
153-
Gradient(colors: [self.bodyColorTop, self.bodyColorBottom]),
154-
startPoint: .zero,
155-
endPoint: CGPoint(x: 120, y: 120))
156-
let antennaStroke = StrokeStyle(lineWidth: 3, lineCap: .round)
202+
// Site antennae: stroke-width 2, `--coral-bright`.
203+
let antennaStroke = StrokeStyle(lineWidth: 2, lineCap: .round)
157204

158205
// Same paint order as favicon.svg: body, claws, antennae, eyes.
159-
context.fill(self.bodyPath, with: bodyShading)
206+
context.fill(self.bodyPath, with: self.gradient(for: self.bodyPath, palette: palette))
160207
self.drawRotated(context: context, degrees: pose.leftClawDegrees, pivot: self.leftClawPivot) {
161-
$0.fill(self.leftClawPath, with: bodyShading)
208+
$0.fill(self.leftClawPath, with: self.gradient(for: self.leftClawPath, palette: palette))
162209
}
163210
self.drawRotated(context: context, degrees: pose.rightClawDegrees, pivot: self.rightClawPivot) {
164-
$0.fill(self.rightClawPath, with: bodyShading)
211+
$0.fill(self.rightClawPath, with: self.gradient(for: self.rightClawPath, palette: palette))
165212
}
166213
self.drawRotated(context: context, degrees: pose.antennaDegrees, pivot: self.leftAntennaPivot) {
167-
$0.stroke(self.leftAntennaPath, with: .color(self.bodyColorTop), style: antennaStroke)
214+
$0.stroke(self.leftAntennaPath, with: .color(palette.antenna), style: antennaStroke)
168215
}
169216
self.drawRotated(context: context, degrees: pose.antennaDegrees, pivot: self.rightAntennaPivot) {
170-
$0.stroke(self.rightAntennaPath, with: .color(self.bodyColorTop), style: antennaStroke)
217+
$0.stroke(self.rightAntennaPath, with: .color(palette.antenna), style: antennaStroke)
171218
}
172219

173220
context.fill(Path(ellipseIn: CGRect(x: 39, y: 29, width: 12, height: 12)), with: .color(self.eyeColor))
174221
context.fill(Path(ellipseIn: CGRect(x: 69, y: 29, width: 12, height: 12)), with: .color(self.eyeColor))
175222
var glowContext = context
176223
glowContext.opacity = pose.eyeGlowOpacity
177224
glowContext.fill(
178-
Path(ellipseIn: CGRect(x: 43.5, y: 31.5, width: 5, height: 5)),
225+
Path(ellipseIn: CGRect(x: 44, y: 32, width: 4, height: 4)),
179226
with: .color(self.eyeGlowColor))
180227
glowContext.fill(
181-
Path(ellipseIn: CGRect(x: 73.5, y: 31.5, width: 5, height: 5)),
228+
Path(ellipseIn: CGRect(x: 74, y: 32, width: 4, height: 4)),
182229
with: .color(self.eyeGlowColor))
183230
}
184231

232+
/// SVG gradients default to objectBoundingBox units, so the body and each
233+
/// claw span the full top-left -> bottom-right ramp across their own bounds;
234+
/// one canvas-wide gradient would leave the claws nearly flat-colored.
235+
private static func gradient(
236+
for path: Path,
237+
palette: OpenClawMascotPalette) -> GraphicsContext.Shading
238+
{
239+
let box = path.boundingRect
240+
return .linearGradient(
241+
Gradient(colors: [palette.gradientTop, palette.gradientBottom]),
242+
startPoint: box.origin,
243+
endPoint: CGPoint(x: box.maxX, y: box.maxY))
244+
}
245+
185246
private static func drawRotated(
186247
context: GraphicsContext,
187248
degrees: CGFloat,

0 commit comments

Comments
 (0)