HuggingChat-Mac/Animations/FluidGradient/FluidGradientView.swift (121 lines of code) (raw):
//
// FluidGradientView.swift
// FluidGradientView
//
// Created by Oskar Groth on 2021-12-23.
//
import SwiftUI
import Combine
#if os(OSX)
import AppKit
public typealias SystemColor = NSColor
public typealias SystemView = NSView
#else
import UIKit
public typealias SystemColor = UIColor
public typealias SystemView = UIView
#endif
/// A system view that presents an animated gradient with ``CoreAnimation``
public class FluidGradientView: SystemView {
var speed: CGFloat
let baseLayer = ResizableLayer()
let highlightLayer = ResizableLayer()
var cancellables = Set<AnyCancellable>()
weak var delegate: FluidGradientDelegate?
init(blobs: [Color] = [],
highlights: [Color] = [],
speed: CGFloat = 1.0) {
self.speed = speed
super.init(frame: .zero)
if let compositingFilter = CIFilter(name: "CIOverlayBlendMode") {
highlightLayer.compositingFilter = compositingFilter
}
#if os(OSX)
layer = ResizableLayer()
wantsLayer = true
postsFrameChangedNotifications = true
layer?.delegate = self
baseLayer.delegate = self
highlightLayer.delegate = self
self.layer?.addSublayer(baseLayer)
self.layer?.addSublayer(highlightLayer)
#else
self.layer.addSublayer(baseLayer)
self.layer.addSublayer(highlightLayer)
#endif
create(blobs, layer: baseLayer)
create(highlights, layer: highlightLayer)
DispatchQueue.main.async {
self.update(speed: speed)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
/// Create blobs and add to specified layer
public func create(_ colors: [Color], layer: CALayer) {
// Remove blobs at the end if colors are removed
let count = layer.sublayers?.count ?? 0
let removeCount = count - colors.count
if removeCount > 0 {
layer.sublayers?.removeLast(removeCount)
}
for (index, color) in colors.enumerated() {
if index < count {
if let existing = layer.sublayers?[index] as? BlobLayer {
existing.set(color: color)
}
} else {
layer.addSublayer(BlobLayer(color: color))
}
}
}
/// Update sublayers and set speed and blur levels
public func update(speed: CGFloat) {
cancellables.removeAll()
self.speed = speed
guard speed > 0 else { return }
let layers = (baseLayer.sublayers ?? []) + (highlightLayer.sublayers ?? [])
for layer in layers {
if let layer = layer as? BlobLayer {
Timer.publish(every: .random(in: 0.8/speed...1.2/speed),
on: .main,
in: .common)
.autoconnect()
.sink { _ in
#if os(OSX)
let visible = self.window?.occlusionState.contains(.visible)
guard visible == true else { return }
#endif
layer.animate(speed: speed)
}
.store(in: &cancellables)
}
}
}
/// Compute and update new blur value
private func updateBlur() {
delegate?.updateBlur(min(frame.width, frame.height))
}
/// Functional methods
#if os(OSX)
public override func viewDidMoveToWindow() {
super.viewDidMoveToWindow()
let scale = window?.backingScaleFactor ?? 2
layer?.contentsScale = scale
baseLayer.contentsScale = scale
highlightLayer.contentsScale = scale
updateBlur()
}
public override func resize(withOldSuperviewSize oldSize: NSSize) {
updateBlur()
}
#else
public override func layoutSubviews() {
layer.frame = self.bounds
baseLayer.frame = self.bounds
highlightLayer.frame = self.bounds
updateBlur()
}
#endif
}
protocol FluidGradientDelegate: AnyObject {
func updateBlur(_ value: CGFloat)
}
#if os(OSX)
extension FluidGradientView: CALayerDelegate, NSViewLayerContentScaleDelegate {
public func layer(_ layer: CALayer,
shouldInheritContentsScale newScale: CGFloat,
from window: NSWindow) -> Bool {
return true
}
}
#endif