BroV look: swamp-zombie unibrow character, new app and menu bar icons, BroV naming in UI

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
maksarsanjeev
2026-10-06 19:53:47 +03:00
parent 0f77ec8704
commit 7d76986bc0
22 changed files with 422 additions and 54 deletions
+150
View File
@@ -0,0 +1,150 @@
// RenderBroV.swift (pass any argument to render the app + menu bar icons instead) — renders the real BotEngine in every state to a PNG.
// Run: bash scripts/render-brov.sh → /tmp/brov-states.png
import Foundation
import SwiftUI
import AppKit
// MARK: - Stubs (substitutes for app-only types)
@MainActor
final class SoundEngine {
static let shared = SoundEngine()
var enabled: Bool = false
func play(_ name: String) {}
}
extension Notification.Name {
static let botDizzy = Notification.Name("notchBuddy.botDizzy")
static let botGreet = Notification.Name("notchBuddy.botGreet")
static let botBlink = Notification.Name("notchBuddy.botBlink")
static let botSetTgEs = Notification.Name("notchBuddy.botSetTgEs")
static let botGulp = Notification.Name("notchBuddy.botGulp")
static let botMorphTo = Notification.Name("notchBuddy.botMorphTo")
static let triggerEmote = Notification.Name("notchBuddy.triggerEmote")
static let triggerSlap = Notification.Name("notchBuddy.triggerSlap")
static let greetComplete = Notification.Name("notchBuddy.greetComplete")
static let greetingHover = Notification.Name("notchBuddy.greetingHover")
static let greetingInterrupt = Notification.Name("notchBuddy.greetingInterrupt")
static let islandAction = Notification.Name("notchBuddy.islandAction")
static let islandCollapse = Notification.Name("notchBuddy.islandCollapse")
static let hookReveal = Notification.Name("notchBuddy.hookReveal")
static let musicReveal = Notification.Name("notchBuddy.musicReveal")
static let openFullSettings = Notification.Name("notchBuddy.openFullSettings")
}
extension Color {
init(hex: String) {
let h = hex.trimmingCharacters(in: CharacterSet(charactersIn: "#"))
let val = UInt64(h, radix: 16) ?? 0
let r = Double((val >> 16) & 0xFF) / 255
let g = Double((val >> 8) & 0xFF) / 255
let b = Double( val & 0xFF) / 255
self.init(red: r, green: g, blue: b)
}
}
struct StateCell: View {
let engine: BotEngine
let label: String
let size: CGFloat
var body: some View {
VStack(spacing: 2) {
Canvas { ctx, sz in
engine.drawHandsBehind(context: ctx, size: sz)
engine.draw(context: ctx, size: sz)
engine.drawHandsAndExtras(context: ctx, size: sz)
}
.frame(width: size, height: size)
Text(label).font(.system(size: 10, design: .monospaced)).foregroundColor(.gray)
}
}
}
@main
struct RenderBroV {
@MainActor static func main() {
if CommandLine.arguments.count > 1 { renderIcons(); return }
var cells: [(BotEngine, String, CGFloat)] = []
for st in BotState.allCases {
let e = BotEngine()
e.setState(st, force: true)
for _ in 0..<90 { e.update(dt: 1.0 / 60) }
cells.append((e, st.rawValue, 170))
}
let small = BotEngine()
for _ in 0..<60 { small.update(dt: 1.0 / 60) }
cells.append((small, "notch-size", 48))
let view = VStack(alignment: .leading, spacing: 8) {
ForEach(0..<3, id: \.self) { row in
HStack(spacing: 8) {
ForEach(Array(cells.enumerated()).filter { $0.offset / 4 == row }, id: \.offset) { _, c in
StateCell(engine: c.0, label: c.1, size: c.2)
}
}
}
}
.padding(16)
.background(Color(red: 0.04, green: 0.04, blue: 0.05))
let r = ImageRenderer(content: view)
r.scale = 2
guard let img = r.nsImage, let tiff = img.tiffRepresentation,
let rep = NSBitmapImageRep(data: tiff), let png = rep.representation(using: .png, properties: [:]) else {
print("render failed"); return
}
try! png.write(to: URL(fileURLWithPath: "/tmp/brov-states.png"))
print("wrote /tmp/brov-states.png")
}
}
@MainActor
func savePNG<V: View>(_ view: V, scale: CGFloat, to path: String) {
let r = ImageRenderer(content: view)
r.scale = scale
r.isOpaque = false
guard let cg = r.cgImage else { print("render failed"); return }
let rep = NSBitmapImageRep(cgImage: cg)
try! rep.representation(using: .png, properties: [:])!.write(to: URL(fileURLWithPath: path))
print("wrote", path)
}
/// /tmp/brov-icon-1024.png (app icon) and /tmp/brov-menubar-{1,2,3}x.png (template unibrow).
@MainActor
func renderIcons() {
let e = BotEngine()
e.setState(.approval, force: true)
for _ in 0..<90 { e.update(dt: 1.0 / 60) }
let icon = ZStack {
RoundedRectangle(cornerRadius: 185, style: .continuous)
.fill(LinearGradient(colors: [Color(hex: "#2B1B2E"), Color(hex: "#0B0A10")], startPoint: .top, endPoint: .bottom))
.frame(width: 824, height: 824)
Canvas { ctx, sz in
e.draw(context: ctx, size: sz)
}
.frame(width: 900, height: 900)
.offset(y: -20)
}
.frame(width: 1024, height: 1024)
savePNG(icon, scale: 1, to: "/tmp/brov-icon-1024.png")
// Menu bar: a hairy unibrow over two eyes, black on clear (used as a template image).
let bar = Canvas { ctx, sz in
let w = sz.width, h = sz.height
var brow = Path()
brow.move(to: CGPoint(x: w * 0.06, y: h * 0.42))
brow.addQuadCurve(to: CGPoint(x: w * 0.5, y: h * 0.40), control: CGPoint(x: w * 0.28, y: h * 0.12))
brow.addQuadCurve(to: CGPoint(x: w * 0.94, y: h * 0.42), control: CGPoint(x: w * 0.72, y: h * 0.12))
ctx.stroke(brow, with: .color(.black), style: StrokeStyle(lineWidth: h * 0.2, lineCap: .round))
for i in 0..<9 {
let x = w * (0.12 + 0.76 * CGFloat(i) / 8)
var hair = Path()
hair.move(to: CGPoint(x: x, y: h * 0.28))
hair.addLine(to: CGPoint(x: x + w * 0.02, y: h * 0.10))
ctx.stroke(hair, with: .color(.black), style: StrokeStyle(lineWidth: h * 0.07, lineCap: .round))
}
ctx.fill(Path(ellipseIn: CGRect(x: w * 0.20, y: h * 0.56, width: w * 0.20, height: h * 0.34)), with: .color(.black))
ctx.fill(Path(ellipseIn: CGRect(x: w * 0.60, y: h * 0.60, width: w * 0.15, height: h * 0.26)), with: .color(.black))
}
.frame(width: 22, height: 16)
for s in [1, 2, 3] { savePNG(bar, scale: CGFloat(s), to: "/tmp/brov-menubar-\(s)x.png") }
}