From a4175c44ca381f36c6f38cb4d263df2de8fecd6e Mon Sep 17 00:00:00 2001 From: Ali <> Date: Mon, 15 May 2023 15:03:55 +0400 Subject: [PATCH] Add effect experiment --- submodules/ChatListUI/BUILD | 1 + .../Sources/ChatListController.swift | 24 ++- submodules/TelegramUI/BUILD | 1 + .../Components/FullScreenEffectView/BUILD | 68 +++++++ .../MetalResources/RippleEffect.metal | 158 +++++++++++++++ .../Sources/RippleEffectView.swift | 188 ++++++++++++++++++ 6 files changed, 439 insertions(+), 1 deletion(-) create mode 100644 submodules/TelegramUI/Components/FullScreenEffectView/BUILD create mode 100644 submodules/TelegramUI/Components/FullScreenEffectView/MetalResources/RippleEffect.metal create mode 100644 submodules/TelegramUI/Components/FullScreenEffectView/Sources/RippleEffectView.swift diff --git a/submodules/ChatListUI/BUILD b/submodules/ChatListUI/BUILD index 171957e021..a255ad726a 100644 --- a/submodules/ChatListUI/BUILD +++ b/submodules/ChatListUI/BUILD @@ -96,6 +96,7 @@ swift_library( "//submodules/TelegramUI/Components/Stories/StoryContainerScreen", "//submodules/TelegramUI/Components/Stories/StoryContentComponent", "//submodules/TelegramUI/Components/Stories/StoryPeerListComponent", + "//submodules/TelegramUI/Components/FullScreenEffectView", ], visibility = [ "//visibility:public", diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index bb88d23660..09fc926612 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -47,6 +47,7 @@ import InviteLinksUI import ChatFolderLinkPreviewScreen import StoryContainerScreen import StoryContentComponent +import FullScreenEffectView private func fixListNodeScrolling(_ listNode: ListView, searchNode: NavigationBarSearchContentNode) -> Bool { if listNode.scroller.isDragging { @@ -249,6 +250,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController private var storyListHeight: CGFloat + private var fullScreenEffectView: RippleEffectView? + public override func updateNavigationCustomData(_ data: Any?, progress: CGFloat, transition: ContainedViewLayoutTransition) { if self.isNodeLoaded { self.chatListDisplayNode.effectiveContainerNode.updateSelectedChatLocation(data: data as? ChatLocation, progress: progress, transition: transition) @@ -2141,7 +2144,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController if itemSet.peerId == self.context.account.peerId { continue } - if itemSet.items.contains(where: { !$0.isSeen }) { + if itemSet.items.contains(where: { $0.id > itemSet.maxReadId }) { peersWithNewStories.insert(itemSet.peerId) } } @@ -2498,6 +2501,25 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController searchContentNode.updateListVisibleContentOffset(.known(0.0)) self.chatListDisplayNode.scrollToTop() } + + #if DEBUG && false + var fullScreenEffectView: RippleEffectView? + if let current = self.fullScreenEffectView { + fullScreenEffectView = current + self.view.window?.addSubview(current) + current.sourceView = self.view + } else { + if let value = RippleEffectView(test: false) { + fullScreenEffectView = value + self.fullScreenEffectView = value + self.view.window?.addSubview(value) + value.sourceView = self.view + } + } + if let fullScreenEffectView { + fullScreenEffectView.frame = CGRect(origin: CGPoint(), size: layout.size) + } + #endif } private func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 41072eafc0..69371f0336 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -367,6 +367,7 @@ swift_library( "//submodules/TelegramUI/Components/ChatTextInputMediaRecordingButton", "//submodules/TelegramUI/Components/ChatSendButtonRadialStatusNode", "//submodules/TelegramUI/Components/LegacyInstantVideoController", + "//submodules/TelegramUI/Components/FullScreenEffectView", ] + select({ "@build_bazel_rules_apple//apple:ios_armv7": [], "@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets, diff --git a/submodules/TelegramUI/Components/FullScreenEffectView/BUILD b/submodules/TelegramUI/Components/FullScreenEffectView/BUILD new file mode 100644 index 0000000000..aff39e9fd4 --- /dev/null +++ b/submodules/TelegramUI/Components/FullScreenEffectView/BUILD @@ -0,0 +1,68 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") +load( + "@build_bazel_rules_apple//apple:resources.bzl", + "apple_resource_bundle", + "apple_resource_group", +) +load("//build-system/bazel-utils:plist_fragment.bzl", + "plist_fragment", +) + +filegroup( + name = "FullScreenEffectViewMetalResources", + srcs = glob([ + "MetalResources/**/*.*", + ]), + visibility = ["//visibility:public"], +) + +plist_fragment( + name = "FullScreenEffectViewBundleInfoPlist", + extension = "plist", + template = + """ + CFBundleIdentifier + org.telegram.FullScreenEffectView + CFBundleDevelopmentRegion + en + CFBundleName + FullScreenEffectView + """ +) + +apple_resource_bundle( + name = "FullScreenEffectViewBundle", + infoplists = [ + ":FullScreenEffectViewBundleInfoPlist", + ], + resources = [ + ":FullScreenEffectViewMetalResources", + ], +) + +filegroup( + name = "FullScreenEffectViewResources", + srcs = glob([ + "Resources/**/*", + ], exclude = ["Resources/**/.*"]), + visibility = ["//visibility:public"], +) + +swift_library( + name = "FullScreenEffectView", + module_name = "FullScreenEffectView", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + ], + data = [ + ":FullScreenEffectViewBundle", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/FullScreenEffectView/MetalResources/RippleEffect.metal b/submodules/TelegramUI/Components/FullScreenEffectView/MetalResources/RippleEffect.metal new file mode 100644 index 0000000000..625566a593 --- /dev/null +++ b/submodules/TelegramUI/Components/FullScreenEffectView/MetalResources/RippleEffect.metal @@ -0,0 +1,158 @@ +#include + +using namespace metal; + +typedef struct { + packed_float2 position; +} Vertex; + +typedef struct { + float4 position [[position]]; + float2 texCoord [[user(texture_coord)]]; + float visibilityFraction; +} RasterizerData; + +constant float2 vertices[6] = { + float2(1, -1), + float2(-1, -1), + float2(-1, 1), + float2(1, -1), + float2(-1, 1), + float2(1, 1) +}; + +float doubleStep(float value, float lowerBound, float upperBound) { + return step(lowerBound, value) * (1.0 - step(upperBound, value)); +} + +float fieldFunction(float2 center, float2 position, float2 dimensions, float time) { + float maxDimension = max(dimensions.x, dimensions.y); + + float currentDistance = time * maxDimension; + float waveWidth = 100.0f * 3.0f; + + float d = distance(center, position); + + float stepFactor = doubleStep(d, currentDistance, currentDistance + waveWidth); + float value = abs(sin((-currentDistance + d) * M_PI_F / (waveWidth))); + + return value * stepFactor * 1.0f; +} + +float linearDecay(float parameter, float maxParameter) { + float decay = clamp(1.0 - parameter / maxParameter, 0.0, 1.0); + return decay; +} + +vertex RasterizerData rippleVertex +( + uint vid [[ vertex_id ]], + device const uint2 ¢er [[buffer(0)]], + device const uint2 &gridResolution [[buffer(1)]], + device const uint2 &resolution [[buffer(2)]], + device const float &time [[buffer(3)]] +) { + uint triangleIndex = vid / 6; + uint vertexIndex = vid % 6; + float2 in = vertices[vertexIndex]; + in.x = (in.x + 1.0) * 0.5; + in.y = (in.y + 1.0) * 0.5; + + float2 dimensions = float2(resolution.x, resolution.y); + + float2 gridStep = float2(1.0 / (float)(gridResolution.x), 1.0 / (float)(gridResolution.y)); + uint2 positionInGrid = uint2(triangleIndex % gridResolution.x, triangleIndex / gridResolution.x); + + float2 position = float2( + float(positionInGrid.x) * gridStep.x + in.x * gridStep.x, + float(positionInGrid.y) * gridStep.y + in.y * gridStep.y + ); + float2 texCoord = float2(position.x, 1.0 - position.y); + + float zPosition = fieldFunction(float2(center), float2(position.x * dimensions.x, (1.0 - position.y) * dimensions.y), dimensions, time); + zPosition *= 0.5f; + + float leftEdgeDistance = abs(position.x); + float rightEdgeDistance = abs(1.0 - position.x); + float topEdgeDistance = abs(position.y); + float bottomEdgeDistance = abs(1.0 - position.y); + float minEdgeDistance = min(leftEdgeDistance, rightEdgeDistance); + minEdgeDistance = min(minEdgeDistance, topEdgeDistance); + minEdgeDistance = min(minEdgeDistance, bottomEdgeDistance); + float edgeNorm = 0.1f; + float edgeDistance = min(minEdgeDistance / edgeNorm, 1.0); + zPosition *= edgeDistance; + + zPosition *= max(0.0, min(1.0, linearDecay(time, 0.7))); + if (zPosition <= 0.1) { + //zPosition = 0.0; + } + + float3 camPosition = float3(0.0, 0.0f, 1.0f); + float3 camTarget = float3(0.0, 0.0, 0.0); + float3 forwardVector = normalize(camPosition - camTarget); + float3 rightVector = normalize(cross(float3(0.0, 1.0, 0.0), forwardVector)); + float3 upVector = normalize(cross(forwardVector, rightVector)); + + float translationX = dot(camPosition, rightVector); + float translationY = dot(camPosition, upVector); + float translationZ = dot(camPosition, forwardVector); + + float4x4 viewTransform = float4x4( + rightVector.x, upVector.x, forwardVector.x, 0.0, + rightVector.y, upVector.y, forwardVector.y, 0.0, + rightVector.z, upVector.z, forwardVector.z, 0.0, + -translationX, -translationY, -translationZ, 1.0 + ); + + float4x4 projectionTransform = float4x4( + 1.0, 0.0, 0.0, 0.0, + 0.0, 1.0, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, + 0.0, 0.0, -1.0 / 500.0, 1.0 + ); + + float4x4 mvp = projectionTransform * viewTransform; + + float zNorm = 0.1; + + float4 transformedPosition = float4(float2(-1.0 + position.x * 2.0, -1.0 + position.y * 2.0), -zPosition * zNorm, 1.0) * mvp; + transformedPosition.x /= transformedPosition.w; + transformedPosition.y /= transformedPosition.w; + transformedPosition.z /= transformedPosition.w; + + position.x = transformedPosition.x; + position.y = transformedPosition.y; + + RasterizerData out; + out.position = vector_float4(0.0, 0.0, 0.0, 1.0); + out.position.x = transformedPosition.x; + out.position.y = transformedPosition.y; + out.position.z = transformedPosition.z + zNorm; + + out.visibilityFraction = zPosition == 0.0 ? 0.0 : 1.0; + + out.texCoord = texCoord; + + return out; +} + +fragment half4 rippleFragment( + RasterizerData in[[stage_in]], + texture2d texture[[ texture(0) ]] +) { + constexpr sampler textureSampler(min_filter::linear, mag_filter::linear, mip_filter::linear, address::clamp_to_edge); + + float2 texCoord = in.texCoord; + float4 rgb = float4(texture.sample(textureSampler, texCoord)); + + float4 out = float4(rgb.xyz, 1.0); + + out.a = 1.0 - step(in.visibilityFraction, 0.5); + + out.r *= out.a; + out.g *= out.a; + out.b *= out.a; + + return half4(out); +} diff --git a/submodules/TelegramUI/Components/FullScreenEffectView/Sources/RippleEffectView.swift b/submodules/TelegramUI/Components/FullScreenEffectView/Sources/RippleEffectView.swift new file mode 100644 index 0000000000..10d867aed9 --- /dev/null +++ b/submodules/TelegramUI/Components/FullScreenEffectView/Sources/RippleEffectView.swift @@ -0,0 +1,188 @@ +import Foundation +import Metal +import MetalKit +import simd + +public final class RippleEffectView: MTKView { + private let textureLoader: MTKTextureLoader + private let commandQueue: MTLCommandQueue + private let drawPassthroughPipelineState: MTLRenderPipelineState + private var texture: MTLTexture? + + private var viewportDimensions = CGSize(width: 1, height: 1) + + private var time: Float = 0.0 + + private var lastUpdateTimestamp: Double? + + public weak var sourceView: UIView? { + didSet { + self.updateImageFromSourceView() + } + } + + public init?(test: Bool) { + let mainBundle = Bundle(for: RippleEffectView.self) + + guard let path = mainBundle.path(forResource: "FullScreenEffectViewBundle", ofType: "bundle") else { + return nil + } + guard let bundle = Bundle(path: path) else { + return nil + } + + guard let device = MTLCreateSystemDefaultDevice() else { + return nil + } + + guard let defaultLibrary = try? device.makeDefaultLibrary(bundle: bundle) else { + return nil + } + + guard let commandQueue = device.makeCommandQueue() else { + return nil + } + self.commandQueue = commandQueue + + guard let loadedVertexProgram = defaultLibrary.makeFunction(name: "rippleVertex") else { + return nil + } + + guard let loadedFragmentProgram = defaultLibrary.makeFunction(name: "rippleFragment") else { + return nil + } + + self.textureLoader = MTKTextureLoader(device: device) + + let pipelineStateDescriptor = MTLRenderPipelineDescriptor() + pipelineStateDescriptor.vertexFunction = loadedVertexProgram + pipelineStateDescriptor.fragmentFunction = loadedFragmentProgram + pipelineStateDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm + pipelineStateDescriptor.colorAttachments[0].isBlendingEnabled = true + pipelineStateDescriptor.colorAttachments[0].rgbBlendOperation = .add + pipelineStateDescriptor.colorAttachments[0].alphaBlendOperation = .add + pipelineStateDescriptor.colorAttachments[0].sourceRGBBlendFactor = .sourceAlpha + pipelineStateDescriptor.colorAttachments[0].sourceAlphaBlendFactor = .sourceAlpha + pipelineStateDescriptor.colorAttachments[0].destinationRGBBlendFactor = .oneMinusSourceAlpha + pipelineStateDescriptor.colorAttachments[0].destinationAlphaBlendFactor = .oneMinusSourceAlpha + + self.drawPassthroughPipelineState = try! device.makeRenderPipelineState(descriptor: pipelineStateDescriptor) + + super.init(frame: CGRect(), device: device) + + self.isOpaque = false + self.backgroundColor = nil + + self.framebufferOnly = true + + self.isPaused = false + } + + public func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) { + self.viewportDimensions = size + } + + required public init(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + override public func draw(_ rect: CGRect) { + self.redraw(drawable: self.currentDrawable!) + } + + private func updateImageFromSourceView() { + guard let sourceView = self.sourceView else { + return + } + + let unscaledSize = sourceView.bounds.size + + UIGraphicsBeginImageContextWithOptions(sourceView.bounds.size, true, 0.0) + let context = UIGraphicsGetCurrentContext()! + UIGraphicsPushContext(context) + + var unhideSelf = false + if self.isDescendant(of: sourceView) { + self.isHidden = true + unhideSelf = true + } + + sourceView.drawHierarchy(in: CGRect(origin: CGPoint(), size: unscaledSize), afterScreenUpdates: false) + + if unhideSelf { + self.isHidden = false + } + + UIGraphicsPopContext() + let image = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + + if let image { + self.updateImage(image: image) + } + + self.lastUpdateTimestamp = CACurrentMediaTime() + } + + private func updateImage(image: UIImage) { + guard let cgImage = image.cgImage else { + return + } + self.texture = try? self.textureLoader.newTexture(cgImage: cgImage) + } + + private func redraw(drawable: MTLDrawable) { + if let lastUpdateTimestamp = self.lastUpdateTimestamp { + if lastUpdateTimestamp + 1.0 < CACurrentMediaTime() { + self.updateImageFromSourceView() + } + } else { + self.updateImageFromSourceView() + } + + guard let commandBuffer = self.commandQueue.makeCommandBuffer() else { + return + } + + let renderPassDescriptor = self.currentRenderPassDescriptor! + renderPassDescriptor.colorAttachments[0].loadAction = .clear + renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0, green: 0, blue: 0, alpha: 0.0) + + guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else { + return + } + + let viewportDimensions = CGSize(width: self.bounds.size.width * self.contentScaleFactor, height: self.bounds.size.height * self.contentScaleFactor) + + renderEncoder.setRenderPipelineState(self.drawPassthroughPipelineState) + + let gridSize = 1000 + var time = self.time.truncatingRemainder(dividingBy: 0.7) + //time = 0.6 + self.time += (1.0 / 60.0) * 0.1 + + var gridResolution = simd_uint2(UInt32(gridSize), UInt32(gridSize)) + var resolution = simd_uint2(UInt32(viewportDimensions.width), UInt32(viewportDimensions.height)) + + var center = simd_uint2(200, 200); + + if let texture = self.texture { + renderEncoder.setVertexBytes(¢er, length: MemoryLayout.size, index: 0) + renderEncoder.setVertexBytes(&gridResolution, length: MemoryLayout.size, index: 1) + renderEncoder.setVertexBytes(&resolution, length: MemoryLayout.size, index: 2) + renderEncoder.setVertexBytes(&time, length: MemoryLayout.size, index: 3) + + renderEncoder.setFragmentTexture(texture, index: 0) + + renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6 * gridSize * gridSize, instanceCount: 1) + } + + renderEncoder.endEncoding() + + commandBuffer.present(drawable) + commandBuffer.commit() + } +}