7d76986bc0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
151 lines
6.4 KiB
Swift
151 lines
6.4 KiB
Swift
// 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") }
|
|
}
|