Import Coucou 0.1.9 (Louis-CFM/coucou@83708fe), Mac app only

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
maksarsanjeev
2026-10-06 19:34:32 +03:00
commit 53b4f60da1
158 changed files with 29442 additions and 0 deletions
+331
View File
@@ -0,0 +1,331 @@
// RenderOutfits.swift — standalone planche renderer for Mochi outfits
// Compile + run via: bash scripts/render-outfits.sh
// Output: /tmp/coucou-outfits.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)
}
}
// MARK: - Cell view — one outfit at one pose
struct MochiCell: View {
let outfit: Outfit
let yaw: CGFloat
let pitch: CGFloat
let tilt: CGFloat
let phys: (dx: CGFloat, dy: CGFloat)
let cellSize: CGFloat
var roll: CGFloat = 0
var presence: CGFloat = 1
var body: some View {
Canvas { context, sz in
let W = sz.width, H = sz.height
// scale=0.62, R = W * 0.62 * 0.3
let R = W * 0.62 * 0.3
let rx = R * 1.14
let ry = R * 0.88
let cx = W / 2
let cy = H / 2 + R * 0.06
let sx: CGFloat = 1, sy: CGFloat = 1
let morph: CGFloat = 0
let mH = MochiH(R: R, yaw: yaw, pitch: pitch, physDx: phys.dx, physDy: phys.dy, roll: roll)
// 1. Behind-body outfit
drawOutfitBehindStatic(
context: context, outfit: outfit, H: mH,
cx: cx, cy: cy, tilt: tilt, sx: sx, sy: sy,
roll: roll, morph: morph, isMini: false, presence: presence
)
// 2. Body (replicate BotEngine.drawBody for idle / pumpkin state)
var bCtx = context
bCtx.translateBy(x: cx, y: cy)
if tilt != 0 { bCtx.rotate(by: .radians(tilt)) }
bCtx.scaleBy(x: sx, y: sy)
let body = mochiOutfitPath(rx, ry)
// Base gradient (pumpkin-aware)
let pumpkin = outfit == .pumpkin
let cTop: Color = pumpkin ? Color(hex: "#FFA94D") : Color(red: 0.929, green: 0.929, blue: 0.937)
let cBot: Color = pumpkin ? Color(hex: "#E8590C") : Color(red: 0.769, green: 0.773, blue: 0.792)
bCtx.fill(body, with: .linearGradient(
Gradient(colors: [cTop, cBot]),
startPoint: CGPoint(x: rx * 0.7, y: -ry * 0.85),
endPoint: CGPoint(x: -rx * 0.8, y: ry * 0.9)
))
// Shadow rim
bCtx.fill(body, with: .radialGradient(
Gradient(stops: [
.init(color: .clear, location: 0.6),
.init(color: Color.black.opacity(0.2), location: 1)
]),
center: .zero, startRadius: R * 0.15, endRadius: R * 1.25
))
// Top-left highlight
bCtx.fill(body, with: .radialGradient(
Gradient(stops: [
.init(color: Color.white.opacity(0.55), location: 0),
.init(color: .clear, location: 1)
]),
center: CGPoint(x: rx * 0.34, y: -ry * 0.46),
startRadius: 0, endRadius: R * 0.42
))
// 3. Eyes (clipped to body)
var eyeBase = bCtx
eyeBase.clip(to: body)
let ink = Color(red: 0.102, green: 0.082, blue: 0.071)
for f in mEyeFrames(mH) {
guard f.visible else { continue }
var eCtx = eyeBase
eCtx.translateBy(x: f.x, y: f.y)
eCtx.scaleBy(x: f.fx, y: f.fy)
let hh = max(f.h, f.w * 0.3)
var pill = Path()
pill.addRoundedRect(
in: CGRect(x: -f.w / 2, y: -hh / 2, width: f.w, height: hh),
cornerSize: CGSize(width: min(f.w / 2, hh / 2), height: min(f.w / 2, hh / 2))
)
eCtx.fill(pill, with: .color(ink))
}
// 4. Front outfit
drawOutfitFrontStatic(
context: context, outfit: outfit, H: mH,
cx: cx, cy: cy, tilt: tilt, sx: sx, sy: sy,
roll: roll, morph: morph, isMini: false, presence: presence
)
}
.frame(width: cellSize, height: cellSize)
}
}
// MARK: - Grid view
// Outfit order matching sheet.html
private let targetOutfits: [Outfit] = [
.none, .beanie, .santaHat, .partyHat, .crown, .witchHat,
.sunglasses, .roundGlasses, .scarf, .pumpkin, .bow,
]
private struct PoseSpec {
let label: String
let yaw: CGFloat
let pitch: CGFloat
let tilt: CGFloat
let phys: (dx: CGFloat, dy: CGFloat)
let size: CGFloat
}
// Columns matching sheet.html cols array + small pill-preview column (W=190, scale=0.62; small=64)
private let poses: [PoseSpec] = [
PoseSpec(label: "L", yaw: -0.5, pitch: 0, tilt: 0, phys: (0.6, 0), size: 190),
PoseSpec(label: "front", yaw: 0, pitch: 0, tilt: 0, phys: (0, 0), size: 190),
PoseSpec(label: "R", yaw: 0.5, pitch: 0, tilt: 0, phys: (-0.6, 0), size: 190),
PoseSpec(label: "up", yaw: 0.15, pitch: 0.4, tilt: 0, phys: (0, 0.4), size: 190),
PoseSpec(label: "down", yaw: -0.2, pitch: -0.5, tilt: 0, phys: (0, -0.4), size: 190),
PoseSpec(label: "tilt", yaw: 0.3, pitch: -0.25, tilt: 0.12, phys: (-0.3, 0), size: 190),
PoseSpec(label: "mini", yaw: 0, pitch: 0, tilt: 0, phys: (0, 0), size: 64),
]
private let labelW: CGFloat = 90
private let gap: CGFloat = 6
struct OutfitGrid: View {
var body: some View {
VStack(alignment: .leading, spacing: gap) {
// Column headers
HStack(spacing: gap) {
Spacer().frame(width: labelW)
ForEach(poses.indices, id: \.self) { i in
Text(poses[i].label)
.font(.system(size: 9, design: .monospaced))
.foregroundColor(.gray)
.frame(width: poses[i].size, alignment: .center)
}
}
// One row per outfit
ForEach(targetOutfits.indices, id: \.self) { oi in
let outfit = targetOutfits[oi]
HStack(spacing: gap) {
Text(outfit.rawValue)
.font(.system(size: 10))
.foregroundColor(.white)
.frame(width: labelW, alignment: .trailing)
ForEach(poses.indices, id: \.self) { pi in
let p = poses[pi]
MochiCell(outfit: outfit, yaw: p.yaw, pitch: p.pitch,
tilt: p.tilt, phys: p.phys, cellSize: p.size)
.background(pi == poses.count - 1
? Color.black
: Color(red: 0.083, green: 0.090, blue: 0.106))
.clipShape(RoundedRectangle(cornerRadius: pi == poses.count - 1 ? 0 : 8))
}
}
}
}
.padding(12)
.background(Color(red: 0.043, green: 0.047, blue: 0.055))
}
}
// MARK: - Roll planche
private let rollOutfits: [Outfit] = [
.none, .beanie, .santaHat, .witchHat, .crown,
.sunglasses, .roundGlasses, .scarf, .bow, .pumpkin, .bunnyEars
]
private let rollValues: [CGFloat] = [0, CGFloat.pi/3, CGFloat.pi*2/3, CGFloat.pi, CGFloat.pi*4/3, CGFloat.pi*5/3]
struct RollGrid: View {
var body: some View {
VStack(alignment: .leading, spacing: gap) {
// Column headers
HStack(spacing: gap) {
Spacer().frame(width: labelW)
ForEach(rollValues.indices, id: \.self) { i in
Text(String(format: "%.2fπ", rollValues[i] / .pi))
.font(.system(size: 9, design: .monospaced))
.foregroundColor(.gray)
.frame(width: 120, alignment: .center)
}
}
ForEach(rollOutfits.indices, id: \.self) { oi in
let outfit = rollOutfits[oi]
HStack(spacing: gap) {
Text(outfit.rawValue)
.font(.system(size: 10))
.foregroundColor(.white)
.frame(width: labelW, alignment: .trailing)
ForEach(rollValues.indices, id: \.self) { ri in
MochiCell(outfit: outfit, yaw: 0, pitch: 0, tilt: 0, phys: (0, 0),
cellSize: 120, roll: rollValues[ri])
.background(Color(red: 0.083, green: 0.090, blue: 0.106))
.clipShape(RoundedRectangle(cornerRadius: 8))
}
}
}
}
.padding(12)
.background(Color(red: 0.043, green: 0.047, blue: 0.055))
}
}
// MARK: - Transition planche
private let transitionOutfits: [Outfit] = [.witchHat, .santaHat, .bunnyEars, .scarf]
private let presenceValues: [CGFloat] = [0, 0.3, 0.6, 1]
struct TransitionGrid: View {
var body: some View {
VStack(alignment: .leading, spacing: gap) {
// Column headers
HStack(spacing: gap) {
Spacer().frame(width: labelW)
ForEach(presenceValues.indices, id: \.self) { i in
Text(String(format: "p=%.1f", presenceValues[i]))
.font(.system(size: 9, design: .monospaced))
.foregroundColor(.gray)
.frame(width: 120, alignment: .center)
}
}
ForEach(transitionOutfits.indices, id: \.self) { oi in
let outfit = transitionOutfits[oi]
HStack(spacing: gap) {
Text(outfit.rawValue)
.font(.system(size: 10))
.foregroundColor(.white)
.frame(width: labelW, alignment: .trailing)
ForEach(presenceValues.indices, id: \.self) { pi in
MochiCell(outfit: outfit, yaw: 0, pitch: 0, tilt: 0, phys: (0, 0),
cellSize: 120, presence: presenceValues[pi])
.background(Color(red: 0.083, green: 0.090, blue: 0.106))
.clipShape(RoundedRectangle(cornerRadius: 8))
}
}
}
}
.padding(12)
.background(Color(red: 0.043, green: 0.047, blue: 0.055))
}
}
// MARK: - Helper to render and save a view
@MainActor
private func renderAndSave<V: View>(_ view: V, path: String) {
let renderer = ImageRenderer(content: view)
renderer.scale = 2.0
guard let nsImage = renderer.nsImage else {
print("Error: ImageRenderer returned nil for \(path)")
return
}
guard let tiff = nsImage.tiffRepresentation,
let rep = NSBitmapImageRep(data: tiff),
let png = rep.representation(using: .png, properties: [:])
else {
print("Error: PNG conversion failed for \(path)")
return
}
do {
try png.write(to: URL(fileURLWithPath: path))
print("✓ \(path) — \(Int(nsImage.size.width))×\(Int(nsImage.size.height)) @ 2×")
} catch {
print("Error writing \(path): \(error)")
}
}
// MARK: - Entry point
@main
struct RenderOutfits {
static func main() {
MainActor.assumeIsolated {
renderAndSave(OutfitGrid(), path: "/tmp/coucou-outfits.png")
renderAndSave(RollGrid(), path: "/tmp/coucou-roll.png")
renderAndSave(TransitionGrid(), path: "/tmp/coucou-transition.png")
}
}
}