diff --git a/Example/ColorfulApp.xcodeproj/project.pbxproj b/Example/ColorfulApp.xcodeproj/project.pbxproj index 1b6709b..741949a 100644 --- a/Example/ColorfulApp.xcodeproj/project.pbxproj +++ b/Example/ColorfulApp.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 500E7C462C6F5397001D7AA1 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 500E7C452C6F5397001D7AA1 /* main.swift */; }; 507C2F092C36FB1000BCB5FA /* ChessboardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 507C2F082C36FB1000BCB5FA /* ChessboardView.swift */; }; 50C663882B19B04800AF7053 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C663872B19B04800AF7053 /* App.swift */; }; 50C6638A2B19B04800AF7053 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C663892B19B04800AF7053 /* ContentView.swift */; }; @@ -15,6 +16,7 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 500E7C452C6F5397001D7AA1 /* main.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; 507C2F082C36FB1000BCB5FA /* ChessboardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChessboardView.swift; sourceTree = ""; }; 50C663842B19B04800AF7053 /* ColorfulApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ColorfulApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; 50C663872B19B04800AF7053 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = ""; }; @@ -55,6 +57,7 @@ 50C663862B19B04800AF7053 /* ColorfulApp */ = { isa = PBXGroup; children = ( + 500E7C452C6F5397001D7AA1 /* main.swift */, 50C663872B19B04800AF7053 /* App.swift */, 50C663892B19B04800AF7053 /* ContentView.swift */, 507C2F082C36FB1000BCB5FA /* ChessboardView.swift */, @@ -145,6 +148,7 @@ files = ( 50C6638A2B19B04800AF7053 /* ContentView.swift in Sources */, 507C2F092C36FB1000BCB5FA /* ChessboardView.swift in Sources */, + 500E7C462C6F5397001D7AA1 /* main.swift in Sources */, 50C663882B19B04800AF7053 /* App.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Example/ColorfulApp/App.swift b/Example/ColorfulApp/App.swift index 57099d6..93ba2db 100644 --- a/Example/ColorfulApp/App.swift +++ b/Example/ColorfulApp/App.swift @@ -8,7 +8,6 @@ import ColorfulX import SwiftUI -@main struct App: SwiftUI.App { var body: some Scene { WindowGroup { diff --git a/Example/ColorfulApp/main.swift b/Example/ColorfulApp/main.swift new file mode 100644 index 0000000..a330400 --- /dev/null +++ b/Example/ColorfulApp/main.swift @@ -0,0 +1,40 @@ +// +// main.swift +// +// +// Created by 秋星桥 on 2024/8/16. +// + +import ColorfulX +import Foundation + +App.main() + +/* + + let threadTestTarget = AnimatedMulticolorGradientView() + let colors = ColorfulPreset.appleIntelligence.colors.map { ColorElement($0) } + threadTestTarget.setColors(colors.map { .init($0) }) + + DispatchQueue.main.async { + Thread { + while true { + DispatchQueue.main.asyncAndWait { + threadTestTarget.layoutSublayers(of: threadTestTarget.layer) + } + } + }.start() + Thread { + while true { + let colors = ColorfulPreset.allCases + .randomElement()! + .colors + .map { ColorElement($0) } + threadTestTarget.setColors(colors.map { .init($0) }) + } + }.start() + } + + CFRunLoopRun() + + */ diff --git a/Package.resolved b/Package.resolved index c9b579e..2989bce 100644 --- a/Package.resolved +++ b/Package.resolved @@ -9,6 +9,15 @@ "version" : "1.0.3" } }, + { + "identity" : "msdisplaylink", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Lakr233/MSDisplayLink.git", + "state" : { + "revision" : "08ad21d886e2e3f990ccbc6813eb6cdad8c427cd", + "version" : "1.0.0" + } + }, { "identity" : "springinterpolation", "kind" : "remoteSourceControl", diff --git a/Sources/ColorfulX/AnimatedMulticolorGradientView+Speckle.swift b/Sources/ColorfulX/AnimatedMulticolorGradientView+Speckle.swift index ab1ac65..b63b192 100644 --- a/Sources/ColorfulX/AnimatedMulticolorGradientView+Speckle.swift +++ b/Sources/ColorfulX/AnimatedMulticolorGradientView+Speckle.swift @@ -40,3 +40,35 @@ public extension AnimatedMulticolorGradientView { } } } + +extension AnimatedMulticolorGradientView.Speckle { + var colorVector: ColorVector { + let progress = transitionProgress.context.currentPos + return previousColor.lerp(to: targetColor, percent: progress) + } +} + +public extension AnimatedMulticolorGradientView { + func setColors(_ colors: [ColorVector], interpolationEnabled: Bool = true, repeatToFillColorSlots: Bool = true) { + var colors = colors + if colors.isEmpty { colors.append(.init(v: .zero, space: .rgb)) } + colors = colors.map { $0.color(in: .lab) } + + let endingIndex = repeatToFillColorSlots ? Uniforms.COLOR_SLOT : min(colors.count, Uniforms.COLOR_SLOT) + guard endingIndex > 0 else { return } + + alteringSpeckles { speckles in + for idx in 0 ..< endingIndex { + var read = speckles[idx] + let color: ColorVector = colors[idx % colors.count] + guard read.targetColor != color else { continue } + let interpolationEnabled = interpolationEnabled && read.enabled + read.enabled = true + read.targetColor = color + read.previousColor = interpolationEnabled ? read.colorVector : color + read.transitionProgress.setCurrent(interpolationEnabled ? 0 : 1, 0) + speckles[idx] = read + } + } + } +} diff --git a/Sources/ColorfulX/AnimatedMulticolorGradientView+Update.swift b/Sources/ColorfulX/AnimatedMulticolorGradientView+Update.swift index 3bd36f6..335fc42 100644 --- a/Sources/ColorfulX/AnimatedMulticolorGradientView+Update.swift +++ b/Sources/ColorfulX/AnimatedMulticolorGradientView+Update.swift @@ -17,51 +17,55 @@ extension AnimatedMulticolorGradientView { func initializeRenderParameters() { var rand = randomLocationPair() - for idx in 0 ..< colorElements.count { + for idx in 0 ..< speckles.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)) + alteringSpeckles { speckles in + speckles[idx].position.setCurrent(.init(x: rand.x, y: rand.y)) + rand = randomLocationPair() + speckles[idx].position.setTarget(.init(x: rand.x, y: rand.y)) + } } } func updateRenderParameters(deltaTime: Double) { - // clear the flag defer { renderInputWasModified = false } 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) + var points: [MulticolorGradientView.Parameters.ColorStop]? + alteringSpeckles { speckles in + for idx in 0 ..< speckles.count where speckles[idx].enabled { + if speckles[idx].transitionProgress.context.currentPos < 1 { + speckles[idx].transitionProgress.update(withDeltaTime: deltaTime * transitionSpeed) + } + if moveDelta > 0 { + speckles[idx].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)) + let pos_x = speckles[idx].position.x.context.currentPos + let tar_x = speckles[idx].position.x.context.targetPos + let pos_y = speckles[idx].position.y.context.currentPos + let tar_y = speckles[idx].position.y.context.targetPos + if abs(pos_x - tar_x) < 0.125 || abs(pos_y - tar_y) < 0.125 { + let rand = randomLocationPair() + speckles[idx].position.setTarget(.init(x: rand.x, y: rand.y)) + } } } - } - parameters = .init( - points: colorElements + points = speckles .filter(\.enabled) .map { .init( - color: computeSpeckleColor($0), + color: $0.colorVector, position: .init( x: $0.position.x.context.currentPos, y: $0.position.y.context.currentPos ) - ) }, + ) } + } + guard let points else { return } + + parameters = .init( + points: points, bias: bias, noise: noise ) diff --git a/Sources/ColorfulX/AnimatedMulticolorGradientView.swift b/Sources/ColorfulX/AnimatedMulticolorGradientView.swift index eaa8ce7..97dbbed 100644 --- a/Sources/ColorfulX/AnimatedMulticolorGradientView.swift +++ b/Sources/ColorfulX/AnimatedMulticolorGradientView.swift @@ -28,10 +28,12 @@ open class AnimatedMulticolorGradientView: MulticolorGradientView { didSet { lastRenderParametersUpdate = obtainCurrentTimestamp() } } - public internal(set) var colorElements: [Speckle] { + public private(set) var speckles: [Speckle] { didSet { renderInputWasModified = true } } + private let specklesAccessLock = NSLock() + public var speed: Double = 1.0 { didSet { renderInputWasModified = true } } @@ -55,32 +57,11 @@ open class AnimatedMulticolorGradientView: MulticolorGradientView { // MARK: - FUNCTION override public init() { - colorElements = .init(repeating: .init(position: SPRING_ENGINE), count: Uniforms.COLOR_SLOT) + speckles = .init(repeating: .init(position: SPRING_ENGINE), count: Uniforms.COLOR_SLOT) super.init() initializeRenderParameters() } - public func setColors(_ colors: [ColorVector], interpolationEnabled: Bool = true, repeatToFillColorSlots: Bool = true) { - var colors = colors - if colors.isEmpty { colors.append(.init(v: .zero, space: .rgb)) } - colors = colors.map { $0.color(in: .lab) } - - let endingIndex = repeatToFillColorSlots ? Uniforms.COLOR_SLOT : min(colors.count, Uniforms.COLOR_SLOT) - guard endingIndex > 0 else { return } - for idx in 0 ..< endingIndex { - var read = colorElements[idx] - let color: ColorVector = colors[idx % colors.count] - guard read.targetColor != color else { continue } - let interpolationEnabled = interpolationEnabled && read.enabled - let currentColor = computeSpeckleColor(read) - read.enabled = true - read.targetColor = color - read.previousColor = interpolationEnabled ? currentColor : color - read.transitionProgress.setCurrent(interpolationEnabled ? 0 : 1, 0) - colorElements[idx] = read - } - } - // MARK: - GETTER @inline(__always) @@ -123,7 +104,7 @@ open class AnimatedMulticolorGradientView: MulticolorGradientView { @inline(__always) func isColorTransitionCompleted() -> Bool { - colorElements + speckles .filter(\.enabled) .allSatisfy { $0.transitionProgress.context.currentPos >= 1 } } @@ -139,16 +120,9 @@ open class AnimatedMulticolorGradientView: MulticolorGradientView { // MARK: - RENDER LIFE CYCLE - private let updateParametersLock = NSLock() - override public func layoutSublayers(of layer: CALayer) { super.layoutSublayers(of: layer) - - updateParametersLock.lock() - renderInputWasModified = true updateRenderParameters(deltaTime: deltaTimeForRenderParametersUpdate()) - updateParametersLock.unlock() - renderIfNeeded() } @@ -159,17 +133,13 @@ open class AnimatedMulticolorGradientView: MulticolorGradientView { override func vsync() { guard shouldRenderNextFrameWithinSynchornization() else { return } - - updateParametersLock.lock() updateRenderParameters(deltaTime: deltaTimeForRenderParametersUpdate()) - updateParametersLock.unlock() - - // sine the render parameters were updated, we call super.vsync to render super.vsync() } - func computeSpeckleColor(_ speckle: Speckle) -> ColorVector { - let progress = speckle.transitionProgress.context.currentPos - return speckle.previousColor.lerp(to: speckle.targetColor, percent: progress) + func alteringSpeckles(_ callback: (inout [Speckle]) -> Void) { + specklesAccessLock.lock() + callback(&speckles) + specklesAccessLock.unlock() } } diff --git a/Sources/ColorfulX/MulticolorGradientView.swift b/Sources/ColorfulX/MulticolorGradientView.swift index 763ddc2..92142ae 100644 --- a/Sources/ColorfulX/MulticolorGradientView.swift +++ b/Sources/ColorfulX/MulticolorGradientView.swift @@ -135,9 +135,17 @@ open class MulticolorGradientView: MetalView { commandBuffer.commit() commandBuffer.waitUntilScheduled() - drawable.present() - currentDrawable = drawable - commandBuffer.waitUntilCompleted() + if Thread.isMainThread { + drawable.present() + currentDrawable = drawable + commandBuffer.waitUntilCompleted() + } else { + DispatchQueue.main.asyncAndWait { + drawable.present() + currentDrawable = drawable + commandBuffer.waitUntilCompleted() + } + } } }