// // MetalAnimationView.swift // YabaiPro // // Created by Jake Shore // Copyright © 2024 Jake Shore. All rights reserved. // import SwiftUI import MetalKit struct MetalAnimationView: View { let animationType: AnimationType let windowInfo: AnimationWindowInfo? @Binding var time: Double enum AnimationType: String { case liquidBorder case flowingParticles case rippleEffect case morphingShape } var body: some View { ZStack { // Try Metal first if available if MetalAnimationEngine.shared.isMetalAvailable { MetalView(animationType: animationType, windowInfo: windowInfo, time: $time) .frame(maxWidth: .infinity, maxHeight: .infinity) .drawingGroup() // Enable GPU rendering } // Canvas fallback (always rendered, but hidden if Metal works) CanvasFallbackView(animationType: animationTypeFromString(animationType.rawValue) ?? .liquidBorder, windowInfo: windowInfo, time: $time) .frame(maxWidth: .infinity, maxHeight: .infinity) .opacity(MetalAnimationEngine.shared.isMetalAvailable ? 0.0 : 1.0) } .allowsHitTesting(false) } private func animationTypeFromString(_ string: String) -> CanvasFallbackView.AnimationType? { switch string { case "liquidBorder": return .liquidBorder case "flowingParticles": return .flowingParticles case "rippleEffect": return .rippleEffect case "morphingShape": return .morphingShape default: return nil } } } // MARK: - Metal View (NSViewRepresentable) struct MetalView: NSViewRepresentable { let animationType: MetalAnimationView.AnimationType let windowInfo: AnimationWindowInfo? @Binding var time: Double func makeNSView(context: Context) -> MTKView { let metalView = MTKView() let device = MetalAnimationEngine.shared.metalDevice print("🎯 MetalView device: \(device != nil ? "✅ Available" : "❌ Nil")") metalView.device = device metalView.clearColor = MTLClearColor(red: 0, green: 0, blue: 0, alpha: 0) metalView.enableSetNeedsDisplay = true metalView.isPaused = false metalView.preferredFramesPerSecond = 60 // Configure for transparency metalView.clearColor = MTLClearColor(red: 0, green: 0, blue: 0, alpha: 0) metalView.colorPixelFormat = .bgra8Unorm // Disable depth/stencil metalView.depthStencilPixelFormat = .invalid context.coordinator.metalView = metalView context.coordinator.animationType = animationType context.coordinator.windowInfo = windowInfo // Start rendering loop metalView.delegate = context.coordinator return metalView } func updateNSView(_ nsView: MTKView, context: Context) { context.coordinator.animationType = animationType context.coordinator.windowInfo = windowInfo // Trigger redraw nsView.setNeedsDisplay(nsView.bounds) } func makeCoordinator() -> Coordinator { Coordinator(time: $time) } class Coordinator: NSObject, MTKViewDelegate { var metalView: MTKView? var animationType: MetalAnimationView.AnimationType = .liquidBorder var windowInfo: AnimationWindowInfo? @Binding var time: Double private let engine = MetalAnimationEngine.shared init(time: Binding) { self._time = time } func draw(in view: MTKView) { print("🎨 MetalAnimationView draw called for animation: \(animationType)") guard let drawable = view.currentDrawable else { print("❌ MetalAnimationView: No drawable available") return } // Check if Metal is ready guard engine.isMetalAvailable else { print("❌ MetalAnimationView: Metal not available") return } guard engine.areShadersCompiled else { print("❌ MetalAnimationView: Shaders not compiled") // Clear to a visible color to indicate shader failure let renderPass = MTLRenderPassDescriptor() renderPass.colorAttachments[0].texture = drawable.texture renderPass.colorAttachments[0].loadAction = .clear renderPass.colorAttachments[0].clearColor = MTLClearColor(red: 1.0, green: 1.0, blue: 0.0, alpha: 1.0) // Yellow background renderPass.colorAttachments[0].storeAction = .store if let commandBuffer = engine.metalDevice?.makeCommandQueue()?.makeCommandBuffer(), let encoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPass) { encoder.endEncoding() commandBuffer.commit() drawable.present() } return } // Update animation time let currentTime = Float(time) // Get current drawable texture let texture = drawable.texture // Render based on animation type switch animationType { case .liquidBorder: renderLiquidBorder(to: texture, time: currentTime, in: view) case .flowingParticles: renderFlowingParticles(to: texture, time: currentTime, in: view) case .rippleEffect: renderRippleEffect(to: texture, time: currentTime, in: view) case .morphingShape: renderMorphingShape(to: texture, time: currentTime, in: view) } // Present drawable drawable.present() } func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) { // Handle resize if needed } private func renderLiquidBorder(to texture: MTLTexture, time: Float, in view: MTKView) { let isActive = windowInfo?.isFocused ?? false let bounds = view.bounds // Default blue color with some transparency let color = SIMD4(0.3, 0.6, 1.0, 0.4) engine.renderLiquidBorder(to: texture, time: time, bounds: bounds, isActive: isActive, color: color) } private func renderFlowingParticles(to texture: MTLTexture, time: Float, in view: MTKView) { // Generate particles for this frame let particles = generateParticles(for: view.bounds.size, time: time, count: 15) engine.renderParticles(to: texture, particles: particles, time: time, viewportSize: view.bounds.size) } private func renderRippleEffect(to texture: MTLTexture, time: Float, in view: MTKView) { guard let focusPoint = windowInfo?.focusPoint else { return } let ripple = RippleUniforms( center: SIMD2(Float(focusPoint.x), Float(focusPoint.y)), radius: Float(time * 100.0), // Expanding radius strength: max(0.0, 1.0 - time * 0.5), // Fading strength color: SIMD4(0.5, 0.8, 1.0, 0.6) ) engine.renderRipples(to: texture, ripples: [ripple], time: time, viewportSize: view.bounds.size) } private func renderMorphingShape(to texture: MTLTexture, time: Float, in view: MTKView) { // This would require additional shader work - for now, fall back to liquid border renderLiquidBorder(to: texture, time: time, in: view) } private func generateParticles(for size: CGSize, time: Float, count: Int) -> [ParticleUniforms] { var particles: [ParticleUniforms] = [] for i in 0..(x, y), velocity: SIMD2( cos(time + t * 6.28) * 20, sin(time + t * 6.28) * 20 ), color: SIMD4( 0.3 + sin(time + t) * 0.2, // Red 0.6 + cos(time + t) * 0.2, // Green 1.0, // Blue 0.6 // Alpha ), size: 4.0 + sin(time * 2 + t * 6.28) * 2.0, life: 1.0 ) particles.append(particle) } return particles } } } // MARK: - Shared Types // AnimationWindowInfo is defined in AnimationTypes.swift // MARK: - Preview Provider struct MetalAnimationView_Previews: PreviewProvider { static var previews: some View { ZStack { Color.gray.opacity(0.2) MetalAnimationView( animationType: .liquidBorder, windowInfo: AnimationWindowInfo(id: 1, isFocused: true, focusPoint: nil, morphProgress: 0.5), time: .constant(0.0) ) .frame(width: 200, height: 150) .border(Color.red, width: 1) } .frame(width: 300, height: 200) .previewDisplayName("Liquid Border Animation") } }