From 777230bb24423cc2d232d01c3b9b96a4ee754c7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A0=8D=E7=A0=8D?= Date: Mon, 12 Aug 2024 19:25:37 +0800 Subject: [PATCH] Reduce Drawing @ Speed = 0 --- ...nimatedMulticolorGradientView+Update.swift | 76 ++++++++++++ .../AnimatedMulticolorGradientView.swift | 113 +++++++----------- Sources/ColorfulX/ColorfulPreset.swift | 8 +- Sources/ColorfulX/MetalView.swift | 5 +- .../ColorfulX/MulticolorGradientView.swift | 5 +- 5 files changed, 126 insertions(+), 81 deletions(-) create mode 100644 Sources/ColorfulX/AnimatedMulticolorGradientView+Update.swift diff --git a/Sources/ColorfulX/AnimatedMulticolorGradientView+Update.swift b/Sources/ColorfulX/AnimatedMulticolorGradientView+Update.swift new file mode 100644 index 0000000..2bb56c0 --- /dev/null +++ b/Sources/ColorfulX/AnimatedMulticolorGradientView+Update.swift @@ -0,0 +1,76 @@ +// +// AnimatedMulticolorGradientView+Update.swift +// +// +// Created by 秋星桥 on 2024/8/12. +// + +import Foundation + +extension AnimatedMulticolorGradientView { + private func randomLocationPair() -> (x: Double, y: Double) { + ( + x: Double.random(in: 0 ... 1), + y: Double.random(in: 0 ... 1) + ) + } + + func initializeRenderParameters() { + var rand = randomLocationPair() + for idx in 0 ..< colorElements.count { + rand = randomLocationPair() + colorElements[idx].position.setCurrent(.init(x: rand.x, y: rand.y)) + rand = randomLocationPair() + colorElements[idx].position.setTarget(.init(x: rand.x, y: rand.y)) + } + } + + func updateRenderParameters() { + defer { needsUpdateRenderParameters = false } + + var deltaTime = -Date(timeIntervalSince1970: lastUpdate).timeIntervalSinceNow + lastUpdate = Date().timeIntervalSince1970 + guard deltaTime > 0 else { return } + + // when the app goes back from background, deltaTime could be very large + let maxDeltaAllowed = 1.0 / Double(frameLimit > 0 ? frameLimit : 30) + deltaTime = min(deltaTime, maxDeltaAllowed) + + let moveDelta = deltaTime * speed * 0.5 // just slow down + + for idx in 0 ..< colorElements.count where colorElements[idx].enabled { + var inplaceEdit = colorElements[idx] + defer { colorElements[idx] = inplaceEdit } + + if inplaceEdit.transitionProgress.context.currentPos < 1 { + inplaceEdit.transitionProgress.update(withDeltaTime: deltaTime * transitionSpeed) + } + if moveDelta > 0 { + inplaceEdit.position.update(withDeltaTime: moveDelta) + + let pos_x = inplaceEdit.position.x.context.currentPos + let tar_x = inplaceEdit.position.x.context.targetPos + let pos_y = inplaceEdit.position.y.context.currentPos + let tar_y = inplaceEdit.position.y.context.targetPos + if abs(pos_x - tar_x) < 0.125 || abs(pos_y - tar_y) < 0.125 { + let rand = randomLocationPair() + inplaceEdit.position.setTarget(.init(x: rand.x, y: rand.y)) + } + } + } + + parameters = .init( + points: colorElements + .filter(\.enabled) + .map { .init( + color: computeSpeckleColor($0), + position: .init( + x: $0.position.x.context.currentPos, + y: $0.position.y.context.currentPos + ) + ) }, + bias: bias, + noise: noise + ) + } +} diff --git a/Sources/ColorfulX/AnimatedMulticolorGradientView.swift b/Sources/ColorfulX/AnimatedMulticolorGradientView.swift index 5663d17..8896c5a 100644 --- a/Sources/ColorfulX/AnimatedMulticolorGradientView.swift +++ b/Sources/ColorfulX/AnimatedMulticolorGradientView.swift @@ -18,26 +18,38 @@ private let SPRING_ENGINE = SpringInterpolation2D(SPRING_CONFIG) open class AnimatedMulticolorGradientView: MulticolorGradientView { public var lastUpdate: Double = 0 public var lastRender: Double = 0 - public private(set) var colorElements: [Speckle] + public var needsUpdateRenderParameters: Bool = false - public var speed: Double = 1.0 - public var bias: Double = 0.01 - public var noise: Double = 0 - public var transitionSpeed: Double = 1 - public var frameLimit: Int = 0 + public internal(set) var colorElements: [Speckle] { + didSet { needsUpdateRenderParameters = true } + } + + public var speed: Double = 1.0 { + didSet { needsUpdateRenderParameters = true } + } + + public var bias: Double = 0.01 { + didSet { needsUpdateRenderParameters = true } + } + + public var noise: Double = 0 { + didSet { needsUpdateRenderParameters = true } + } + + public var transitionSpeed: Double = 1 { + didSet { needsUpdateRenderParameters = true } + } + + public var frameLimit: Int = 0 { + didSet { needsUpdateRenderParameters = true } + } override public init() { colorElements = .init(repeating: .init(position: SPRING_ENGINE), count: Uniforms.COLOR_SLOT) super.init() - var rand = randomLocationPair() - for idx in 0 ..< colorElements.count { - rand = randomLocationPair() - colorElements[idx].position.setCurrent(.init(x: rand.x, y: rand.y)) - rand = randomLocationPair() - colorElements[idx].position.setTarget(.init(x: rand.x, y: rand.y)) - } + initializeRenderParameters() #if canImport(UIKit) NotificationCenter.default.addObserver( @@ -49,24 +61,24 @@ open class AnimatedMulticolorGradientView: MulticolorGradientView { #endif } - @objc - func applicationWillEnterForeground(_: Notification) { - lastRender = .init() - } + #if canImport(UIKit) + @objc + func applicationWillEnterForeground(_: Notification) { + lastRender = .init() + needsUpdateRenderParameters = true + } + #endif deinit { #if canImport(UIKit) - NotificationCenter.default.removeObserver(self, name: UIApplication.willEnterForegroundNotification, object: nil) + NotificationCenter.default.removeObserver( + self, + name: UIApplication.willEnterForegroundNotification, + object: nil + ) #endif } - private func randomLocationPair() -> (x: Double, y: Double) { - ( - x: Double.random(in: 0 ... 1), - y: Double.random(in: 0 ... 1) - ) - } - public func setColors(_ colors: [ColorVector], interpolationEnabled: Bool = true) { var colors = colors if colors.isEmpty { colors.append(.init(v: .zero, space: .rgb)) } @@ -86,60 +98,16 @@ open class AnimatedMulticolorGradientView: MulticolorGradientView { } } - private func updateRenderParameters() { - var deltaTime = -Date(timeIntervalSince1970: lastUpdate).timeIntervalSinceNow - lastUpdate = Date().timeIntervalSince1970 - guard deltaTime > 0 else { return } - - // when the app goes back from background, deltaTime could be very large - let maxDeltaAllowed = 1.0 / Double(frameLimit > 0 ? frameLimit : 30) - deltaTime = min(deltaTime, maxDeltaAllowed) - - let moveDelta = deltaTime * speed * 0.5 // just slow down - - for idx in 0 ..< colorElements.count where colorElements[idx].enabled { - var inplaceEdit = colorElements[idx] - defer { colorElements[idx] = inplaceEdit } - - if inplaceEdit.transitionProgress.context.currentPos < 1 { - inplaceEdit.transitionProgress.update(withDeltaTime: deltaTime * transitionSpeed) - } - if moveDelta > 0 { - inplaceEdit.position.update(withDeltaTime: moveDelta) - - let pos_x = inplaceEdit.position.x.context.currentPos - let tar_x = inplaceEdit.position.x.context.targetPos - let pos_y = inplaceEdit.position.y.context.currentPos - let tar_y = inplaceEdit.position.y.context.targetPos - if abs(pos_x - tar_x) < 0.125 || abs(pos_y - tar_y) < 0.125 { - let rand = randomLocationPair() - inplaceEdit.position.setTarget(.init(x: rand.x, y: rand.y)) - } - } - } - - parameters = .init( - points: colorElements - .filter(\.enabled) - .map { .init( - color: computeSpeckleColor($0), - position: .init( - x: $0.position.x.context.currentPos, - y: $0.position.y.context.currentPos - ) - ) }, - bias: bias, - noise: noise - ) - } - override public func layoutSublayers(of layer: CALayer) { super.layoutSublayers(of: layer) + needsUpdateRenderParameters = true updateRenderParameters() super.vsync() } override func vsync() { + defer { super.vsync() } + guard needsUpdateRenderParameters || speed > 0 else { return } // when calling from vsync, MetalView is holding strong reference. DispatchQueue.main.asyncAndWait(execute: DispatchWorkItem { if self.frameLimit > 0 { @@ -149,7 +117,6 @@ open class AnimatedMulticolorGradientView: MulticolorGradientView { } self.updateRenderParameters() }) - super.vsync() } func computeSpeckleColor(_ speckle: Speckle) -> ColorVector { diff --git a/Sources/ColorfulX/ColorfulPreset.swift b/Sources/ColorfulX/ColorfulPreset.swift index dcfa399..4a7fc8d 100644 --- a/Sources/ColorfulX/ColorfulPreset.swift +++ b/Sources/ColorfulX/ColorfulPreset.swift @@ -51,10 +51,10 @@ public enum ColorfulPreset: String, CaseIterable { case .neon: return [make(22, 4, 74), make(240, 54, 248), make(79, 216, 248), make(74, 0, 217)] case .aurora: return [make(0, 209, 172), make(0, 150, 150), make(4, 76, 112), make(23, 38, 69)] case .appleIntelligence: return [ - make(239, 176, 76), make(233, 128, 86), make(234, 75, 107), - make(230, 97, 165), make(223, 138, 233), make(192, 160, 245), - make(100, 181, 245), make(126, 201, 238) - ].map { ColorElement($0) }.map { $0.withAlphaComponent(.random(in: 0.75 ... 1.0)) }.map { Color($0) } + make(239, 176, 76), make(233, 128, 86), make(234, 75, 107), + make(230, 97, 165), make(223, 138, 233), make(192, 160, 245), + make(100, 181, 245), make(126, 201, 238), + ].map { ColorElement($0) }.map { $0.withAlphaComponent(.random(in: 0.75 ... 1.0)) }.map { Color($0) } case .colorful: return [#colorLiteral(red: 0.9586862922, green: 0.660125792, blue: 0.8447988033, alpha: 1), #colorLiteral(red: 0.8714533448, green: 0.723166883, blue: 0.9342088699, alpha: 1), #colorLiteral(red: 0.7458761334, green: 0.7851135731, blue: 0.9899476171, alpha: 1), #colorLiteral(red: 0.4398113191, green: 0.8953480721, blue: 0.9796616435, alpha: 1), #colorLiteral(red: 0.3484552801, green: 0.933657825, blue: 0.9058339596, alpha: 1), #colorLiteral(red: 0.5567936897, green: 0.9780793786, blue: 0.6893508434, alpha: 1)].map { .init($0.withAlphaComponent(0.5)) } } } diff --git a/Sources/ColorfulX/MetalView.swift b/Sources/ColorfulX/MetalView.swift index ec9fb14..4068598 100644 --- a/Sources/ColorfulX/MetalView.swift +++ b/Sources/ColorfulX/MetalView.swift @@ -18,6 +18,8 @@ import MetalKit let metalLayer: CAMetalLayer let commandQueue: MTLCommandQueue + var scaleFactor: Double = 1.0 + private weak var mDisplayLink: CADisplayLink? private var hasParentWindow: Bool = false private var hasActiveScene: Bool = true @@ -114,7 +116,6 @@ import MetalKit // 15.79ms for a 1290x2796 image on iPhone 15 Pro Max // native scaleFactor will case a performance issue // so we downscale the image to 1x - let scaleFactor = 1.0 metalLayer.frame = bounds var width = bounds.width * scaleFactor var height = bounds.height * scaleFactor @@ -208,6 +209,7 @@ import MetalKit let commandQueue: MTLCommandQueue var displayLink: CVDisplayLink? + var scaleFactor: Double = 1.0 init() { guard let device = MTLCreateSystemDefaultDevice(), @@ -253,7 +255,6 @@ import MetalKit public func layoutSublayers(of layer: CALayer) { guard layer == metalLayer else { return } metalLayer.frame = bounds - let scaleFactor = 1.0 var width = bounds.width * scaleFactor var height = bounds.height * scaleFactor if width <= 0 { width = 1 } diff --git a/Sources/ColorfulX/MulticolorGradientView.swift b/Sources/ColorfulX/MulticolorGradientView.swift index eb215a9..279b970 100644 --- a/Sources/ColorfulX/MulticolorGradientView.swift +++ b/Sources/ColorfulX/MulticolorGradientView.swift @@ -44,13 +44,14 @@ open class MulticolorGradientView: MetalView { guard lock.try() else { return } defer { lock.unlock() } + guard needsRender else { return } + defer { needsRender = false } + guard let drawable = metalLayer.nextDrawable(), let commandBuffer = commandQueue.makeCommandBuffer(), let commandEncoder = commandBuffer.makeComputeCommandEncoder() else { return } - defer { needsRender = false } - var shaderPoints: [(simd_float2, simd_float4)] = Array( repeating: ( simd_float2(0.0, 0.0),