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