mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-05 05:51:42 +00:00
Add effect experiment
This commit is contained in:
parent
8ec570d6f4
commit
a4175c44ca
@ -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",
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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,
|
||||
|
||||
68
submodules/TelegramUI/Components/FullScreenEffectView/BUILD
Normal file
68
submodules/TelegramUI/Components/FullScreenEffectView/BUILD
Normal 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",
|
||||
],
|
||||
)
|
||||
@ -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 ¢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<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);
|
||||
}
|
||||
@ -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<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()
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user