diff --git a/SnowfallApp.xcodeproj/project.pbxproj b/SnowfallApp.xcodeproj/project.pbxproj index 9a10b1b..a0fa0bc 100644 --- a/SnowfallApp.xcodeproj/project.pbxproj +++ b/SnowfallApp.xcodeproj/project.pbxproj @@ -272,6 +272,7 @@ CURRENT_PROJECT_VERSION = 1; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"SnowfallApp/Preview Content\""; + DEVELOPMENT_TEAM = 694AARYR2X; ENABLE_APP_SANDBOX = YES; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; @@ -317,6 +318,7 @@ CURRENT_PROJECT_VERSION = 1; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"SnowfallApp/Preview Content\""; + DEVELOPMENT_TEAM = 694AARYR2X; ENABLE_APP_SANDBOX = YES; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; diff --git a/SnowfallApp.xcodeproj/xcshareddata/xcschemes/SnowfallApp 1.xcscheme b/SnowfallApp.xcodeproj/xcshareddata/xcschemes/SnowfallApp 1.xcscheme new file mode 100644 index 0000000..94df417 --- /dev/null +++ b/SnowfallApp.xcodeproj/xcshareddata/xcschemes/SnowfallApp 1.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SnowfallApp/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/SnowfallApp/Assets.xcassets/AccentColor.colorset/Contents.json similarity index 100% rename from SnowfallApp/Resources/Assets.xcassets/AccentColor.colorset/Contents.json rename to SnowfallApp/Assets.xcassets/AccentColor.colorset/Contents.json diff --git a/SnowfallApp/Resources/Assets.xcassets/AppIcon.appiconset/1024-mac.png b/SnowfallApp/Assets.xcassets/AppIcon.appiconset/1024-mac.png similarity index 100% rename from SnowfallApp/Resources/Assets.xcassets/AppIcon.appiconset/1024-mac.png rename to SnowfallApp/Assets.xcassets/AppIcon.appiconset/1024-mac.png diff --git a/SnowfallApp/Resources/Assets.xcassets/AppIcon.appiconset/128-mac.png b/SnowfallApp/Assets.xcassets/AppIcon.appiconset/128-mac.png similarity index 100% rename from SnowfallApp/Resources/Assets.xcassets/AppIcon.appiconset/128-mac.png rename to SnowfallApp/Assets.xcassets/AppIcon.appiconset/128-mac.png diff --git a/SnowfallApp/Resources/Assets.xcassets/AppIcon.appiconset/16-mac.png b/SnowfallApp/Assets.xcassets/AppIcon.appiconset/16-mac.png similarity index 100% rename from SnowfallApp/Resources/Assets.xcassets/AppIcon.appiconset/16-mac.png rename to SnowfallApp/Assets.xcassets/AppIcon.appiconset/16-mac.png diff --git a/SnowfallApp/Resources/Assets.xcassets/AppIcon.appiconset/256-mac.png b/SnowfallApp/Assets.xcassets/AppIcon.appiconset/256-mac.png similarity index 100% rename from SnowfallApp/Resources/Assets.xcassets/AppIcon.appiconset/256-mac.png rename to SnowfallApp/Assets.xcassets/AppIcon.appiconset/256-mac.png diff --git a/SnowfallApp/Resources/Assets.xcassets/AppIcon.appiconset/32-mac.png b/SnowfallApp/Assets.xcassets/AppIcon.appiconset/32-mac.png similarity index 100% rename from SnowfallApp/Resources/Assets.xcassets/AppIcon.appiconset/32-mac.png rename to SnowfallApp/Assets.xcassets/AppIcon.appiconset/32-mac.png diff --git a/SnowfallApp/Resources/Assets.xcassets/AppIcon.appiconset/512-mac.png b/SnowfallApp/Assets.xcassets/AppIcon.appiconset/512-mac.png similarity index 100% rename from SnowfallApp/Resources/Assets.xcassets/AppIcon.appiconset/512-mac.png rename to SnowfallApp/Assets.xcassets/AppIcon.appiconset/512-mac.png diff --git a/SnowfallApp/Resources/Assets.xcassets/AppIcon.appiconset/64-mac.png b/SnowfallApp/Assets.xcassets/AppIcon.appiconset/64-mac.png similarity index 100% rename from SnowfallApp/Resources/Assets.xcassets/AppIcon.appiconset/64-mac.png rename to SnowfallApp/Assets.xcassets/AppIcon.appiconset/64-mac.png diff --git a/SnowfallApp/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/SnowfallApp/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from SnowfallApp/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json rename to SnowfallApp/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/SnowfallApp/Resources/Assets.xcassets/Contents.json b/SnowfallApp/Assets.xcassets/Contents.json similarity index 100% rename from SnowfallApp/Resources/Assets.xcassets/Contents.json rename to SnowfallApp/Assets.xcassets/Contents.json diff --git a/SnowfallApp/Sources/Common/Settings.swift b/SnowfallApp/Common/Settings.swift similarity index 61% rename from SnowfallApp/Sources/Common/Settings.swift rename to SnowfallApp/Common/Settings.swift index fb795c8..9247cde 100644 --- a/SnowfallApp/Sources/Common/Settings.swift +++ b/SnowfallApp/Common/Settings.swift @@ -12,8 +12,8 @@ enum SnowPreset: String, CaseIterable, Codable { case custom = "Свой" } -final class Settings: Codable { - static let shared = Settings() +class Settings: Codable { + static var shared = Settings() var currentPreset: SnowPreset = .comfort var isPaused: Bool = false @@ -26,46 +26,28 @@ final class Settings: Codable { var meltingSpeed: Float = 0.05 var windowInteraction: Bool = true - private init() { - load() - } + private init() {} - func applyPreset(_ preset: SnowPreset) { - currentPreset = preset - preset.apply(to: self) - save() + static func reset() { + UserDefaults.standard.dictionaryRepresentation().keys.forEach({ UserDefaults.standard.removeObject(forKey: $0) }) + load() } - func reset() { - UserDefaults.standard.dictionaryRepresentation().keys.forEach { - UserDefaults.standard.removeObject(forKey: $0) + static func save() { + if let data = try? JSONEncoder().encode(shared) { + UserDefaults().set(data, forKey: "settings") } - load() } - func save() { - guard let data = try? JSONEncoder().encode(self) else { return } - - UserDefaults.standard.set(data, forKey: "settings") + static func load() { + let settings = UserDefaults().data(forKey: "settings").flatMap({ try? JSONDecoder().decode(Settings.self, from: $0) }) + shared = settings ?? Settings() } - private func load() { - guard let data = UserDefaults.standard.data(forKey: "settings"), - let settings = try? JSONDecoder().decode(Settings.self, from: data) else { - applyPreset(.comfort) - return - } - - currentPreset = settings.currentPreset - isPaused = settings.isPaused - displayMode = settings.displayMode - pauseInFullscreen = settings.pauseInFullscreen - snowflakeSizeRange = settings.snowflakeSizeRange - maxSnowflakes = settings.maxSnowflakes - snowflakeSpeedRange = settings.snowflakeSpeedRange - windStrength = settings.windStrength - meltingSpeed = settings.meltingSpeed - windowInteraction = settings.windowInteraction + func applyPreset(_ preset: SnowPreset) { + self.currentPreset = preset + preset.apply(to: self) + Settings.save() } } diff --git a/SnowfallApp/Sources/Common/SnowRenderer.swift b/SnowfallApp/Common/SnowRenderer.swift similarity index 61% rename from SnowfallApp/Sources/Common/SnowRenderer.swift rename to SnowfallApp/Common/SnowRenderer.swift index 33997ab..0a0a2e8 100644 --- a/SnowfallApp/Sources/Common/SnowRenderer.swift +++ b/SnowfallApp/Common/SnowRenderer.swift @@ -12,22 +12,19 @@ struct SnowUniforms { var windStrength: Float var minSize: Float var maxSize: Float - var minSpeed: Float - var maxSpeed: Float var isWindowInteractionEnabled: Bool - var particleCount: Float + var _padding: (Bool, Bool, Bool) = (false, false, false) } -final class SnowRenderer: NSObject { +class SnowRenderer: NSObject, MTKViewDelegate { private var device: MTLDevice! private var commandQueue: MTLCommandQueue! private var particleBuffer: MTLBuffer! + private var renderPipelineState: MTLRenderPipelineState! private var computePipelineState: MTLComputePipelineState! - private var initComputePipelineState: MTLComputePipelineState! - private var particleCount: Int = 0 - private var globalRect: CGRect! - private var screenRect: CGRect! + + private var snowflakes: [Snowflake] = [] var mousePosition: simd_float2 = simd_float2(-1000, -1000) var screenSize: simd_float2 = .zero @@ -35,13 +32,12 @@ final class SnowRenderer: NSObject { private var cachedWindowRect: CGRect = .zero private var lastWindowCheckTime: TimeInterval = 0 private let windowCheckInterval: TimeInterval = 0.5 + private var lastDrawTime: CFTimeInterval = CACurrentMediaTime() - init(mtkView: MTKView, screenRect: CGRect, globalRect: CGRect) { + init(mtkView: MTKView, screenSize: CGSize) { super.init() self.device = mtkView.device - self.globalRect = globalRect - self.screenRect = screenRect self.commandQueue = device.makeCommandQueue() mtkView.colorPixelFormat = .bgra8Unorm mtkView.clearColor = MTLClearColor(red: 0, green: 0, blue: 0, alpha: 0) @@ -49,14 +45,13 @@ final class SnowRenderer: NSObject { createPipelineStates() - self.screenSize = SIMD2(Float(screenRect.size.width), Float(screenRect.size.height)) + self.screenSize = SIMD2(Float(screenSize.width), Float(screenSize.height)) generateSnowflakes() } private func createPipelineStates() { let library = device.makeDefaultLibrary() - // Render pipeline let vertexFunction = library?.makeFunction(name: "vertex_main") let fragmentFunction = library?.makeFunction(name: "fragment_main") @@ -78,7 +73,6 @@ final class SnowRenderer: NSObject { print("Render Pipeline Error: \(error)") } - // Update compute pipeline if let computeFunction = library?.makeFunction(name: "updateSnowflakes") { do { computePipelineState = try device.makeComputePipelineState(function: computeFunction) @@ -86,70 +80,13 @@ final class SnowRenderer: NSObject { print("Compute Pipeline Error: \(error)") } } - - // Init compute pipeline - if let initFunction = library?.makeFunction(name: "initializeSnowflakes") { - do { - initComputePipelineState = try device.makeComputePipelineState(function: initFunction) - } catch { - print("Init Compute Pipeline Error: \(error)") - } - } } - private func generateSnowflakes() { + func generateSnowflakes() { let maxSnowflakes = Settings.shared.maxSnowflakes - particleCount = maxSnowflakes - - let bufferSize = maxSnowflakes * MemoryLayout.stride - particleBuffer = device.makeBuffer(length: bufferSize, options: .storageModeShared) - - initializeParticlesOnGPU() - } - - private func initializeParticlesOnGPU() { - guard let commandBuffer = commandQueue.makeCommandBuffer(), - let initKernel = initComputePipelineState else { return } - - var uniforms = makeCurrentUniforms() - - let computeEncoder = commandBuffer.makeComputeCommandEncoder()! - computeEncoder.setComputePipelineState(initKernel) - computeEncoder.setBuffer(particleBuffer, offset: 0, index: 0) - computeEncoder.setBytes(&uniforms, length: MemoryLayout.stride, index: 1) - - let width = initKernel.threadExecutionWidth - let threadsPerGrid = MTLSize(width: particleCount, height: 1, depth: 1) - let threadsPerGroup = MTLSize(width: width, height: 1, depth: 1) - computeEncoder.dispatchThreads(threadsPerGrid, threadsPerThreadgroup: threadsPerGroup) - computeEncoder.endEncoding() - - commandBuffer.commit() - commandBuffer.waitUntilCompleted() - } - - private func makeCurrentUniforms() -> SnowUniforms { - let cachedWindowRectLocalOrigin = WindowInfo().cast(from: globalRect, to: screenRect, point: cachedWindowRect.origin) - return SnowUniforms( - screenSize: screenSize, - mousePosition: mousePosition, - windowRect: simd_float4( - // cast window origin from global to local - Float(cachedWindowRectLocalOrigin.x), - Float(cachedWindowRectLocalOrigin.y), - Float(cachedWindowRect.width), - Float(cachedWindowRect.height) - ), - time: Float(CACurrentMediaTime()), - deltaTime: Float(CACurrentMediaTime() - lastDrawTime), - windStrength: Settings.shared.windStrength, - minSize: Settings.shared.snowflakeSizeRange.lowerBound, - maxSize: Settings.shared.snowflakeSizeRange.upperBound, - minSpeed: Settings.shared.snowflakeSpeedRange.lowerBound, - maxSpeed: Settings.shared.snowflakeSpeedRange.upperBound, - isWindowInteractionEnabled: Settings.shared.windowInteraction, - particleCount: Float(particleCount) - ) + snowflakes = (0...stride + particleBuffer = device.makeBuffer(bytes: snowflakes, length: bufferSize, options: .storageModeShared) } private func updateActiveWindowRect() { @@ -163,31 +100,31 @@ final class SnowRenderer: NSObject { lastWindowCheckTime = currentTime } } -} - -extension SnowRenderer: MTKViewDelegate { - internal func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) { - self.screenSize = SIMD2(Float(size.width), Float(size.height)) - } - internal func draw(in view: MTKView) { + func draw(in view: MTKView) { guard let commandBuffer = commandQueue.makeCommandBuffer(), let drawable = view.currentDrawable, let renderPassDescriptor = view.currentRenderPassDescriptor, let computePipeline = computePipelineState else { return } - - if particleCount != Settings.shared.maxSnowflakes { - generateSnowflakes() - } + if snowflakes.count != Settings.shared.maxSnowflakes { generateSnowflakes() } updateActiveWindowRect() let currentTime = CACurrentMediaTime() let deltaTime = Float(currentTime - lastDrawTime) lastDrawTime = currentTime - var uniforms = makeCurrentUniforms() - uniforms.deltaTime = deltaTime + var uniforms = SnowUniforms( + screenSize: screenSize, + mousePosition: mousePosition, + windowRect: simd_float4(Float(cachedWindowRect.origin.x), Float(cachedWindowRect.origin.y), Float(cachedWindowRect.width), Float(cachedWindowRect.height)), + time: Float(currentTime), + deltaTime: deltaTime, + windStrength: Settings.shared.windStrength, + minSize: Settings.shared.snowflakeSizeRange.lowerBound, + maxSize: Settings.shared.snowflakeSizeRange.upperBound, + isWindowInteractionEnabled: Settings.shared.windowInteraction + ) // Compute let computeEncoder = commandBuffer.makeComputeCommandEncoder()! @@ -196,7 +133,7 @@ extension SnowRenderer: MTKViewDelegate { computeEncoder.setBytes(&uniforms, length: MemoryLayout.stride, index: 1) let width = computePipeline.threadExecutionWidth - let threadsPerGrid = MTLSize(width: particleCount, height: 1, depth: 1) + let threadsPerGrid = MTLSize(width: snowflakes.count, height: 1, depth: 1) let threadsPerGroup = MTLSize(width: width, height: 1, depth: 1) computeEncoder.dispatchThreads(threadsPerGrid, threadsPerThreadgroup: threadsPerGroup) computeEncoder.endEncoding() @@ -206,10 +143,14 @@ extension SnowRenderer: MTKViewDelegate { renderEncoder.setRenderPipelineState(renderPipelineState) renderEncoder.setVertexBuffer(particleBuffer, offset: 0, index: 0) renderEncoder.setVertexBytes(&uniforms, length: MemoryLayout.stride, index: 1) - renderEncoder.drawPrimitives(type: .point, vertexStart: 0, vertexCount: particleCount) + renderEncoder.drawPrimitives(type: .point, vertexStart: 0, vertexCount: snowflakes.count) renderEncoder.endEncoding() commandBuffer.present(drawable) commandBuffer.commit() } + + func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) { + self.screenSize = SIMD2(Float(size.width), Float(size.height)) + } } diff --git a/SnowfallApp/Common/Snowflake.swift b/SnowfallApp/Common/Snowflake.swift new file mode 100644 index 0000000..d2895b6 --- /dev/null +++ b/SnowfallApp/Common/Snowflake.swift @@ -0,0 +1,26 @@ +import SwiftUI +import simd + +struct Snowflake { + var position: simd_float2 + var velocity: simd_float2 + var color: simd_float4 + var size: Float + + init(for screenSize: simd_float2) { + let sizeVal = Float.random(in: Settings.shared.snowflakeSizeRange) + self.size = sizeVal + + self.position = simd_float2( + Float.random(in: 0...screenSize.x), + Float.random(in: 0...screenSize.y) + ) + + let speed = Float.random(in: Settings.shared.snowflakeSpeedRange) + self.velocity = simd_float2(0, speed) + + let normalizedSize = (sizeVal - Settings.shared.snowflakeSizeRange.lowerBound) / (Settings.shared.snowflakeSizeRange.upperBound - Settings.shared.snowflakeSizeRange.lowerBound) + let opacity = Swift.max(0.2, 0.5 + normalizedSize * 0.5) + self.color = simd_float4(1.0, 1.0, 1.0, opacity) + } +} diff --git a/SnowfallApp/Sources/Views/MenuBarSettings.swift b/SnowfallApp/Common/Views/MenuBarSettings.swift similarity index 74% rename from SnowfallApp/Sources/Views/MenuBarSettings.swift rename to SnowfallApp/Common/Views/MenuBarSettings.swift index 1bbc27b..e65ab6e 100644 --- a/SnowfallApp/Sources/Views/MenuBarSettings.swift +++ b/SnowfallApp/Common/Views/MenuBarSettings.swift @@ -2,6 +2,7 @@ import SwiftUI struct MenuBarSettings: View { @State private var selectedPreset: SnowPreset = Settings.shared.currentPreset + @State private var minSpeed: Float = Settings.shared.snowflakeSpeedRange.lowerBound @State private var maxSpeed: Float = Settings.shared.snowflakeSpeedRange.upperBound @State private var minSize: Float = Settings.shared.snowflakeSizeRange.lowerBound @@ -9,9 +10,10 @@ struct MenuBarSettings: View { @State private var maxSnowflakes: Float = Float(Settings.shared.maxSnowflakes) @State private var windowInteraction: Bool = Settings.shared.windowInteraction @State private var windStrength: Float = Settings.shared.windStrength * 100 - + var body: some View { VStack(alignment: .leading, spacing: 16) { + // --- PRESETS --- HStack { Text("Режим:") @@ -27,108 +29,79 @@ struct MenuBarSettings: View { .onChange(of: selectedPreset) { _, newPreset in if newPreset != .custom { applyPreset(newPreset) - } else { - Settings.shared.currentPreset = .custom - Settings.shared.save() } } } - + Divider() - + // --- SPEED --- VStack(alignment: .leading, spacing: 4) { HStack { Text("Скорость") Spacer() - Text(String(format: "%.1f - %.1f", minSpeed, maxSpeed)) - .monospacedDigit() - .foregroundStyle(.secondary) + Text(String(format: "%.1f - %.1f", minSpeed, maxSpeed)).monospacedDigit().foregroundStyle(.secondary) } - HStack { Text("Min").font(.caption).foregroundStyle(.secondary) - Slider(value: $minSpeed, in: 0.1...8.0) { _ in - switchToCustom() - } + Slider(value: $minSpeed, in: 0.1...8.0) { _ in switchToCustom() } } - HStack { Text("Max").font(.caption).foregroundStyle(.secondary) - Slider(value: $maxSpeed, in: 0.1...8.0) { _ in - switchToCustom() - } + Slider(value: $maxSpeed, in: 0.1...8.0) { _ in switchToCustom() } } } - + // --- SIZE --- VStack(alignment: .leading, spacing: 4) { HStack { Text("Размер") Spacer() - Text(String(format: "%.0f - %.0f", minSize, maxSize)) - .monospacedDigit() - .foregroundStyle(.secondary) + Text(String(format: "%.0f - %.0f", minSize, maxSize)).monospacedDigit().foregroundStyle(.secondary) } - HStack { Text("Min").font(.caption).foregroundStyle(.secondary) - Slider(value: $minSize, in: 1...25) { _ in - switchToCustom() - } + Slider(value: $minSize, in: 1...25) { _ in switchToCustom() } } - HStack { Text("Max").font(.caption).foregroundStyle(.secondary) - Slider(value: $maxSize, in: 1...25) { _ in - switchToCustom() - } + Slider(value: $maxSize, in: 1...25) { _ in switchToCustom() } } } - + // --- COUNT --- VStack(alignment: .leading, spacing: 4) { HStack { Text("Количество") Spacer() - Text("\(Int(maxSnowflakes))") - .monospacedDigit() - .foregroundStyle(.secondary) - } - - Slider(value: $maxSnowflakes, in: 100...10000) { _ in - switchToCustom() + Text("\(Int(maxSnowflakes))").monospacedDigit().foregroundStyle(.secondary) } + Slider(value: $maxSnowflakes, in: 100...10000) { _ in switchToCustom() } } - + // --- WIND --- VStack(alignment: .leading, spacing: 4) { HStack { Text("Сила ветра") Spacer() - Text("\(Int(windStrength))") - .monospacedDigit() - .foregroundStyle(.secondary) - } - - Slider(value: $windStrength, in: 0...500) { _ in - switchToCustom() + Text("\(Int(windStrength))").monospacedDigit().foregroundStyle(.secondary) } + Slider(value: $windStrength, in: 0...500) { _ in switchToCustom() } } - + Divider() - + Toggle("Взаимодействие с окнами", isOn: $windowInteraction) - + HStack { Button("Сбросить") { applyPreset(.comfort) } .buttonStyle(.plain) .foregroundStyle(.red) - + Spacer() - + Button("Выход") { NSApplication.shared.terminate(nil) } @@ -143,50 +116,48 @@ struct MenuBarSettings: View { .onChange(of: minSize) { _, _ in updateSettings() } .onChange(of: maxSize) { _, _ in updateSettings() } .onChange(of: maxSnowflakes) { _, _ in updateSettings() } - .onChange(of: windStrength) { _, _ in updateSettings() } .onChange(of: windowInteraction) { _, val in Settings.shared.windowInteraction = val - Settings.shared.save() + Settings.save() } + .onChange(of: windStrength) { _, _ in updateSettings() } .onAppear { loadValues() } } - - // MARK: - Helpers - + private func switchToCustom() { guard selectedPreset != .custom else { return } + selectedPreset = .custom Settings.shared.currentPreset = .custom - Settings.shared.save() + Settings.save() } - + private func applyPreset(_ preset: SnowPreset) { Settings.shared.applyPreset(preset) loadValues() } - + private func updateSettings() { if minSpeed > maxSpeed { maxSpeed = minSpeed } if minSize > maxSize { maxSize = minSize } - + Settings.shared.snowflakeSpeedRange = minSpeed...maxSpeed Settings.shared.snowflakeSizeRange = minSize...maxSize Settings.shared.maxSnowflakes = Int(maxSnowflakes) Settings.shared.windStrength = windStrength / 100 - Settings.shared.save() + Settings.save() } - + private func loadValues() { - let s = Settings.shared - selectedPreset = s.currentPreset - minSpeed = s.snowflakeSpeedRange.lowerBound - maxSpeed = s.snowflakeSpeedRange.upperBound - minSize = s.snowflakeSizeRange.lowerBound - maxSize = s.snowflakeSizeRange.upperBound - maxSnowflakes = Float(s.maxSnowflakes) - windowInteraction = s.windowInteraction - windStrength = s.windStrength * 100 + selectedPreset = Settings.shared.currentPreset + minSpeed = Settings.shared.snowflakeSpeedRange.lowerBound + maxSpeed = Settings.shared.snowflakeSpeedRange.upperBound + minSize = Settings.shared.snowflakeSizeRange.lowerBound + maxSize = Settings.shared.snowflakeSizeRange.upperBound + maxSnowflakes = Float(Settings.shared.maxSnowflakes) + windowInteraction = Settings.shared.windowInteraction + windStrength = Settings.shared.windStrength * 100 } } diff --git a/SnowfallApp/Sources/Views/MetalSnowViewController.swift b/SnowfallApp/Common/Views/MetalSnowViewController.swift similarity index 76% rename from SnowfallApp/Sources/Views/MetalSnowViewController.swift rename to SnowfallApp/Common/Views/MetalSnowViewController.swift index 86b1b09..46fbdca 100644 --- a/SnowfallApp/Sources/Views/MetalSnowViewController.swift +++ b/SnowfallApp/Common/Views/MetalSnowViewController.swift @@ -4,22 +4,20 @@ import MetalKit class MetalSnowViewController: NSViewController { private let mtkView = MTKView() private var renderer: SnowRenderer! - private let screenRect: CGRect - private let globalRect: CGRect + private let initialSize: CGSize - init(screenRect: CGRect, globalRect: CGRect) { - self.screenRect = screenRect - self.globalRect = globalRect + init(screenSize: CGSize) { + self.initialSize = screenSize super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func loadView() { - self.view = NSView(frame: CGRect(origin: .zero, size: screenRect.size)) + self.view = NSView(frame: CGRect(origin: .zero, size: initialSize)) self.view.wantsLayer = true self.view.layer?.backgroundColor = NSColor.clear.cgColor - + mtkView.frame = self.view.bounds mtkView.autoresizingMask = [.width, .height] mtkView.device = MTLCreateSystemDefaultDevice() @@ -32,8 +30,8 @@ class MetalSnowViewController: NSViewController { override func viewDidLoad() { super.viewDidLoad() - renderer = SnowRenderer(mtkView: mtkView, screenRect: screenRect, globalRect: globalRect) - renderer.mtkView(mtkView, drawableSizeWillChange: screenRect.size) + renderer = SnowRenderer(mtkView: mtkView, screenSize: initialSize) + renderer.mtkView(mtkView, drawableSizeWillChange: initialSize) mtkView.delegate = renderer let trackingArea = NSTrackingArea( diff --git a/SnowfallApp/Sources/Common/WindowInfo.swift b/SnowfallApp/Common/WindowInfo.swift similarity index 87% rename from SnowfallApp/Sources/Common/WindowInfo.swift rename to SnowfallApp/Common/WindowInfo.swift index e0693c4..234b29e 100644 --- a/SnowfallApp/Sources/Common/WindowInfo.swift +++ b/SnowfallApp/Common/WindowInfo.swift @@ -42,10 +42,4 @@ class WindowInfo { } return false } - - func cast(from global: CGRect, to local: CGRect, point: CGPoint) -> CGPoint { - let x = point.x - local.origin.x - let y = point.y - (global.height - (local.origin.y + local.height)) - return CGPoint(x: x, y: y) - } } diff --git a/SnowfallApp/Info.plist b/SnowfallApp/Info.plist index 62d843b..85732f5 100644 --- a/SnowfallApp/Info.plist +++ b/SnowfallApp/Info.plist @@ -8,5 +8,7 @@ NSRequiresAquaSystemAppearance + LSUIElement + diff --git a/SnowfallApp/Resources/Localizable.xcstrings b/SnowfallApp/Localizable.xcstrings similarity index 95% rename from SnowfallApp/Resources/Localizable.xcstrings rename to SnowfallApp/Localizable.xcstrings index 755c37e..2d65329 100644 --- a/SnowfallApp/Resources/Localizable.xcstrings +++ b/SnowfallApp/Localizable.xcstrings @@ -2,16 +2,16 @@ "sourceLanguage" : "en", "strings" : { "" : { - + "shouldTranslate" : false }, "%lld" : { - + "shouldTranslate" : false }, "Max" : { - + "shouldTranslate" : false }, "Min" : { - + "shouldTranslate" : false }, "Snowfall" : { "shouldTranslate" : false diff --git a/SnowfallApp/Sources/Common/SnowShader.metal b/SnowfallApp/SnowShader.metal similarity index 65% rename from SnowfallApp/Sources/Common/SnowShader.metal rename to SnowfallApp/SnowShader.metal index 1cfccdf..b5bba7e 100644 --- a/SnowfallApp/Sources/Common/SnowShader.metal +++ b/SnowfallApp/SnowShader.metal @@ -23,43 +23,17 @@ struct Uniforms { float windStrength; float minSize; float maxSize; - float minSpeed; - float maxSpeed; bool isWindowInteractionEnabled; - float particleCount; }; + float random(uint seed, float time) { return fract(sin(float(seed) * 12.9898 + time) * 43758.5453); } -kernel void initializeSnowflakes(device Snowflake *snowflakes [[buffer(0)]], - constant Uniforms &uniforms [[buffer(1)]], - uint id [[thread_position_in_grid]]) { - device Snowflake &flake = snowflakes[id]; - - float rndX = random(id, uniforms.time); - float rndY = random(id * 2, uniforms.time); - float widthSpread = uniforms.screenSize.x + 400.0; - - flake.position.x = (rndX * widthSpread) - 200.0; - flake.position.y = rndY * uniforms.screenSize.y; - - float sizeRange = uniforms.maxSize - uniforms.minSize; - flake.size = uniforms.minSize + random(id + 1, uniforms.time) * sizeRange; - - float speedRange = uniforms.maxSpeed - uniforms.minSpeed; - float speed = uniforms.minSpeed + random(id + 3, uniforms.time) * speedRange; - flake.velocity = float2(0, speed); - - float opacity = max(0.2, flake.size / uniforms.maxSize); - flake.color = float4(1.0, 1.0, 1.0, opacity); -} - kernel void updateSnowflakes(device Snowflake *snowflakes [[buffer(0)]], - constant Uniforms &uniforms [[buffer(1)]], - uint id [[thread_position_in_grid]]) { - if (float(id) >= uniforms.particleCount) return; + constant Uniforms &uniforms [[buffer(1)]], + uint id [[thread_position_in_grid]]) { device Snowflake &flake = snowflakes[id]; float timeFactor = uniforms.deltaTime * 60.0; @@ -76,6 +50,7 @@ kernel void updateSnowflakes(device Snowflake *snowflakes [[buffer(0)]], if (isOnWindow) { flake.position += flake.velocity * 0.1 * timeFactor; + float meltSpeed = 0.1 * timeFactor; flake.size -= meltSpeed; @@ -88,10 +63,12 @@ kernel void updateSnowflakes(device Snowflake *snowflakes [[buffer(0)]], float sizeRange = uniforms.maxSize - uniforms.minSize; flake.size = uniforms.minSize + random(id + 1, uniforms.time) * sizeRange; } + } else { flake.position += flake.velocity * timeFactor; flake.position.x += (uniforms.windStrength * (flake.size * 0.05)) * timeFactor; + // Мышь float2 mouseDir = flake.position - uniforms.mousePosition; float influenceRadius = 50.0; float dist = length(mouseDir); @@ -106,12 +83,12 @@ kernel void updateSnowflakes(device Snowflake *snowflakes [[buffer(0)]], float widthSpread = uniforms.screenSize.x + 400.0; flake.position.x = (rnd * widthSpread) - 200.0; } - - if (flake.position.x > uniforms.screenSize.x + 200.0) { - flake.position.x = -100.0; - } else if (flake.position.x < -200.0) { - flake.position.x = uniforms.screenSize.x + 100.0; - } + } + + if (flake.position.x > uniforms.screenSize.x + 200.0) { + flake.position.x = -100.0; + } else if (flake.position.x < -200.0) { + flake.position.x = uniforms.screenSize.x + 100.0; } } @@ -121,8 +98,8 @@ float2 convert_to_metal_coordinates(float2 point, float2 viewSize) { } vertex VertexOut vertex_main(const device Snowflake *snowflakes [[buffer(0)]], - constant Uniforms &uniforms [[buffer(1)]], - uint vertexID [[vertex_id]]) { + constant Uniforms &uniforms [[buffer(1)]], + uint vertexID [[vertex_id]]) { VertexOut out; float2 pos = convert_to_metal_coordinates(snowflakes[vertexID].position, uniforms.screenSize); out.position = float4(pos, 0, 1); @@ -132,12 +109,10 @@ vertex VertexOut vertex_main(const device Snowflake *snowflakes [[buffer(0)]], } fragment float4 fragment_main(VertexOut fragData [[stage_in]], - float2 pointCoord [[point_coord]]) { + float2 pointCoord [[point_coord]]) { float dist = length(pointCoord - 0.5); float delta = fwidth(dist); float alpha = 1.0 - smoothstep(0.45 - delta, 0.45 + delta, dist); if (alpha < 0.01) discard_fragment(); - - float finalAlpha = fragData.color.a * alpha; - return float4(fragData.color.rgb * finalAlpha, finalAlpha); + return float4(fragData.color.rgb, fragData.color.a * alpha); } diff --git a/SnowfallApp/SnowfallApp.swift b/SnowfallApp/SnowfallApp.swift new file mode 100644 index 0000000..9305322 --- /dev/null +++ b/SnowfallApp/SnowfallApp.swift @@ -0,0 +1,105 @@ +import SwiftUI + +@main +struct SnowfallApp: App { + @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate + + init() { + Settings.load() + } + + var body: some Scene { + MenuBarExtra("Snowfall", systemImage: "snowflake") { + MenuBarSettings() + } + .menuBarExtraStyle(.window) + } +} + +import Cocoa +import MetalKit + +class AppDelegate: NSObject, NSApplicationDelegate { + var snowWindows: [NSWindow] = [] + + func applicationDidFinishLaunching(_ notification: Notification) { + NSApp.setActivationPolicy(.accessory) + setupSnowWindows() + NotificationCenter.default.addObserver(self, selector: #selector(setupSnowWindows), name: NSApplication.didChangeScreenParametersNotification, object: nil) + NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(handleSpaceChange), name: NSWorkspace.activeSpaceDidChangeNotification, object: nil) + NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(handleSpaceChange), name: NSWorkspace.didActivateApplicationNotification, object: nil) + updateSnowVisibility() + } + + @objc private func setupSnowWindows() { + snowWindows.forEach { $0.close() } + snowWindows.removeAll() + + for screen in NSScreen.screens { + createSnowWindow(for: screen) + } + + updateSnowVisibility() + } + + private func createSnowWindow(for screen: NSScreen) { + let screenRect = screen.frame + + let window = NSWindow(contentRect: screenRect, styleMask: [.borderless], backing: .buffered, defer: false) + + let metalController = MetalSnowViewController(screenSize: screenRect.size) + window.contentViewController = metalController + + window.isOpaque = false + window.hasShadow = false + window.backgroundColor = .clear + window.level = .screenSaver + window.collectionBehavior = [.ignoresCycle, .transient, .stationary] + window.ignoresMouseEvents = true + window.isReleasedWhenClosed = false + window.setFrame(screenRect, display: true) + + window.orderFront(nil) + + snowWindows.append(window) + } + + @objc private func handleSpaceChange() { + updateSnowVisibility() + } + + private func updateSnowVisibility() { + let shouldHide = isAnyFullscreenWindowPresent() + for window in snowWindows { + if shouldHide { + window.orderOut(nil) + } else { + window.orderFront(nil) + } + } + } + + private func isAnyFullscreenWindowPresent() -> Bool { + let options: CGWindowListOption = [.optionOnScreenOnly, .excludeDesktopElements] + guard let windowList = CGWindowListCopyWindowInfo(options, kCGNullWindowID) as? [[String: Any]] else { return false } + + let screenSizes = NSScreen.screens.map { $0.frame.size } + let tolerance: CGFloat = 2.0 + + for entry in windowList { + guard let layer = entry[kCGWindowLayer as String] as? Int, layer == 0, + let bounds = entry[kCGWindowBounds as String] as? [String: Any], + let width = bounds["Width"] as? CGFloat, + let height = bounds["Height"] as? CGFloat else { continue } + + for screenSize in screenSizes { + let widthClose = abs(width - screenSize.width) <= tolerance + let heightClose = abs(height - screenSize.height) <= tolerance + if widthClose && heightClose { + return true + } + } + } + return false + } +} diff --git a/SnowfallApp/Sources/Common/Snowflake.swift b/SnowfallApp/Sources/Common/Snowflake.swift deleted file mode 100644 index 7c97cf3..0000000 --- a/SnowfallApp/Sources/Common/Snowflake.swift +++ /dev/null @@ -1,9 +0,0 @@ -import SwiftUI -import simd - -struct Snowflake { - var position: simd_float2 - var velocity: simd_float2 - var color: simd_float4 - var size: Float -} diff --git a/SnowfallApp/Sources/SnowfallApp.swift b/SnowfallApp/Sources/SnowfallApp.swift deleted file mode 100644 index 0b1fa83..0000000 --- a/SnowfallApp/Sources/SnowfallApp.swift +++ /dev/null @@ -1,68 +0,0 @@ -import SwiftUI - -@main -struct SnowfallApp: App { - @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate - - var body: some Scene { - MenuBarExtra("Snowfall", systemImage: "snowflake") { - MenuBarSettings() - } - .menuBarExtraStyle(.window) - } -} - -import Cocoa -import MetalKit - -class AppDelegate: NSObject, NSApplicationDelegate { - var snowWindows: [NSWindow] = [] - - func applicationDidFinishLaunching(_ notification: Notification) { - setupSnowWindows() - NotificationCenter.default.addObserver(self, selector: #selector(setupSnowWindows), name: NSApplication.didChangeScreenParametersNotification, object: nil) - } - - @objc private func setupSnowWindows() { - snowWindows.forEach { $0.close() } - snowWindows.removeAll() - - var maxX = CGFloat.leastNormalMagnitude - var maxY = CGFloat.leastNormalMagnitude - - for screen in NSScreen.screens { - let f = screen.frame - - maxX = max(maxX, f.maxX) - maxY = max(maxY, f.maxY) - } - - let globalRect = CGRect(x: 0, y: 0, width: maxX, height: maxY) - - for screen in NSScreen.screens { - createSnowWindow(for: screen, in: globalRect) - } - } - - private func createSnowWindow(for screen: NSScreen, in globalRect: CGRect) { - let screenRect = screen.frame - - let window = NSWindow(contentRect: screenRect, styleMask: [.borderless], backing: .buffered, defer: false) - - let metalController = MetalSnowViewController(screenRect: screenRect, globalRect: globalRect) - window.contentViewController = metalController - - window.isOpaque = false - window.hasShadow = false - window.backgroundColor = .clear - window.level = .screenSaver - window.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary, .ignoresCycle, .transient, .stationary] - window.ignoresMouseEvents = true - window.isReleasedWhenClosed = false - window.setFrame(screenRect, display: true) - - window.orderFront(nil) - - snowWindows.append(window) - } -}