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:
@@ -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") }
|
||||
}
|
||||
Executable
+10
@@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
# Render BroV in every state → /tmp/brov-states.png
|
||||
set -e
|
||||
cd "$(dirname "$0")/.."
|
||||
K=NotchBuddy/Sources/CoucouKit
|
||||
swiftc -parse-as-library -target arm64-apple-macosx15.0 \
|
||||
$K/IslandScreenGeometry.swift $K/IslandTypes.swift $K/MochiWardrobe.swift \
|
||||
$K/BotEngine.swift $K/MochiOutfitDrawing.swift scripts/RenderBroV.swift \
|
||||
-framework AppKit -framework SwiftUI -o /tmp/brov-render
|
||||
/tmp/brov-render "$@"
|
||||
Reference in New Issue
Block a user