// 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(_ 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") } }