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)
- }
-}