Add effect experiment

This commit is contained in:
Ali 2023-05-15 15:03:55 +04:00
parent 8ec570d6f4
commit a4175c44ca
6 changed files with 439 additions and 1 deletions

View File

@ -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",

View File

@ -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) {

View File

@ -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,

View File

@ -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 =
"""
<key>CFBundleIdentifier</key>
<string>org.telegram.FullScreenEffectView</string>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleName</key>
<string>FullScreenEffectView</string>
"""
)
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",
],
)

View File

@ -0,0 +1,158 @@
#include <metal_stdlib>
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 &center [[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<half> 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);
}

View File

@ -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(&center, length: MemoryLayout<simd_uint2>.size, index: 0)
renderEncoder.setVertexBytes(&gridResolution, length: MemoryLayout<simd_uint2>.size, index: 1)
renderEncoder.setVertexBytes(&resolution, length: MemoryLayout<simd_uint2>.size, index: 2)
renderEncoder.setVertexBytes(&time, length: MemoryLayout<Float>.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()
}
}