mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-06 03:33:41 +02:00
Improve ripple effect
This commit is contained in:
parent
37a744a26c
commit
761701caa1
9 changed files with 232 additions and 844 deletions
|
|
@ -14,12 +14,10 @@ class UITests: XCTestCase {
|
|||
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
||||
}
|
||||
|
||||
func testExample() throws {
|
||||
// UI tests must launch the application that they test.
|
||||
func testLogin() throws {
|
||||
let app = XCUIApplication()
|
||||
app.launchArguments.append("--ui-test")
|
||||
app.launch()
|
||||
|
||||
// Use recording to get started writing UI tests.
|
||||
// Use XCTAssert and related functions to verify your tests produce the correct results.
|
||||
XCTAssert(app.buttons["Continue"].waitForExistence(timeout: 5.0))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -94,7 +94,6 @@ swift_library(
|
|||
"//submodules/TelegramUI/Components/ActionPanelComponent",
|
||||
"//submodules/TelegramUI/Components/Stories/StoryContainerScreen",
|
||||
"//submodules/TelegramUI/Components/Stories/StoryPeerListComponent",
|
||||
"//submodules/TelegramUI/Components/FullScreenEffectView",
|
||||
"//submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent",
|
||||
"//submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen",
|
||||
"//submodules/TelegramUI/Components/Settings/ArchiveInfoScreen",
|
||||
|
|
|
|||
|
|
@ -46,7 +46,6 @@ import ChatListTitleView
|
|||
import InviteLinksUI
|
||||
import ChatFolderLinkPreviewScreen
|
||||
import StoryContainerScreen
|
||||
import FullScreenEffectView
|
||||
import PeerInfoStoryGridScreen
|
||||
import ArchiveInfoScreen
|
||||
import BirthdayPickerScreen
|
||||
|
|
@ -220,8 +219,6 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||
|
||||
var currentTooltipUpdateTimer: Foundation.Timer?
|
||||
|
||||
private var fullScreenEffectView: RippleEffectView?
|
||||
|
||||
let globalControlPanelsContext: GlobalControlPanelsContext
|
||||
private(set) var globalControlPanelsContextState: GlobalControlPanelsContext.State?
|
||||
private var globalControlPanelsContextStateDisposable: Disposable?
|
||||
|
|
@ -2807,11 +2804,6 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||
|
||||
self.chatListDisplayNode.clearHighlightAnimated(true)
|
||||
|
||||
if let fullScreenEffectView = self.fullScreenEffectView {
|
||||
self.fullScreenEffectView = nil
|
||||
fullScreenEffectView.removeFromSuperview()
|
||||
}
|
||||
|
||||
self.sharedOpenStoryProgressDisposable.set(nil)
|
||||
|
||||
if let storyPeerListView = self.chatListHeaderView()?.storyPeerListView() {
|
||||
|
|
@ -3441,37 +3433,6 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||
return nil
|
||||
}
|
||||
|
||||
public func animateStoryUploadRipple() {
|
||||
if let componentView = self.chatListHeaderView() {
|
||||
if let (transitionView, _) = componentView.storyPeerListView()?.transitionViewForItem(peerId: self.context.account.peerId) {
|
||||
let localRect = transitionView.convert(transitionView.bounds, to: self.view)
|
||||
self.animateRipple(centerLocation: localRect.center)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func animateRipple(centerLocation: CGPoint) {
|
||||
if let fullScreenEffectView = self.fullScreenEffectView {
|
||||
self.fullScreenEffectView = nil
|
||||
fullScreenEffectView.removeFromSuperview()
|
||||
}
|
||||
|
||||
if let value = RippleEffectView(centerLocation: centerLocation, completion: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if let fullScreenEffectView = self.fullScreenEffectView {
|
||||
self.fullScreenEffectView = nil
|
||||
fullScreenEffectView.removeFromSuperview()
|
||||
}
|
||||
}) {
|
||||
self.fullScreenEffectView = value
|
||||
value.sourceView = self.view
|
||||
self.view.addSubview(value)
|
||||
value.frame = CGRect(origin: CGPoint(), size: self.view.bounds.size)
|
||||
}
|
||||
}
|
||||
|
||||
private(set) var storyUploadProgress: [PeerId: Float] = [:]
|
||||
private func updateStoryUploadProgress(_ progress: [PeerId: Float]) {
|
||||
self.storyUploadProgress = progress.mapValues {
|
||||
|
|
|
|||
|
|
@ -320,7 +320,6 @@ swift_library(
|
|||
"//submodules/TelegramUI/Components/ChatTextInputMediaRecordingButton",
|
||||
"//submodules/TelegramUI/Components/ChatSendButtonRadialStatusNode",
|
||||
"//submodules/TelegramUI/Components/LegacyInstantVideoController",
|
||||
"//submodules/TelegramUI/Components/FullScreenEffectView",
|
||||
"//submodules/TelegramUI/Components/ShareWithPeersScreen",
|
||||
"//submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode",
|
||||
"//submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen",
|
||||
|
|
|
|||
|
|
@ -1,69 +0,0 @@
|
|||
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/**/*",
|
||||
], allow_empty=True, exclude = ["Resources/**/.*"]),
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
swift_library(
|
||||
name = "FullScreenEffectView",
|
||||
module_name = "FullScreenEffectView",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/ComponentFlow",
|
||||
],
|
||||
data = [
|
||||
":FullScreenEffectViewBundle",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
|
|
@ -1,159 +0,0 @@
|
|||
#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, float contentScale, float2 position, float2 dimensions, float time) {
|
||||
float maxDimension = max(dimensions.x, dimensions.y);
|
||||
|
||||
float currentDistance = time * maxDimension;
|
||||
float waveWidth = 100.0f * contentScale;
|
||||
|
||||
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)]],
|
||||
device const float &contentScale [[buffer(4)]]
|
||||
) {
|
||||
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), contentScale, 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)));
|
||||
|
||||
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.r = 0.0;
|
||||
out.g = 0.0;
|
||||
out.b = 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);
|
||||
}
|
||||
|
|
@ -1,212 +0,0 @@
|
|||
import Foundation
|
||||
import Metal
|
||||
import MetalKit
|
||||
import simd
|
||||
import ComponentFlow
|
||||
|
||||
public final class RippleEffectView: MTKView {
|
||||
private let centerLocation: CGPoint
|
||||
private let completion: () -> Void
|
||||
|
||||
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 startTime: Double?
|
||||
|
||||
private var lastUpdateTimestamp: Double?
|
||||
|
||||
public weak var sourceView: UIView? {
|
||||
didSet {
|
||||
self.updateImageFromSourceView()
|
||||
}
|
||||
}
|
||||
|
||||
public init?(centerLocation: CGPoint, completion: @escaping () -> Void) {
|
||||
self.centerLocation = centerLocation
|
||||
self.completion = completion
|
||||
|
||||
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
|
||||
|
||||
self.isUserInteractionEnabled = 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()
|
||||
}*/
|
||||
|
||||
let relativeTime: Double
|
||||
let timestamp = CACurrentMediaTime()
|
||||
if let startTime = self.startTime {
|
||||
relativeTime = (timestamp - startTime) * (1.0 / UIView.animationDurationFactor)
|
||||
} else {
|
||||
self.startTime = timestamp
|
||||
relativeTime = 0.0
|
||||
}
|
||||
|
||||
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: Float = Float(min(relativeTime, 0.7))
|
||||
|
||||
var gridResolution = simd_uint2(UInt32(gridSize), UInt32(gridSize))
|
||||
var resolution = simd_uint2(UInt32(viewportDimensions.width), UInt32(viewportDimensions.height))
|
||||
|
||||
var center = simd_uint2(UInt32(self.centerLocation.x * self.contentScaleFactor), UInt32(self.centerLocation.y * self.contentScaleFactor));
|
||||
|
||||
if let texture = self.texture {
|
||||
var contentScale: Float = Float(self.contentScaleFactor)
|
||||
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.setVertexBytes(&contentScale, length: MemoryLayout<Float>.size, index: 4)
|
||||
|
||||
renderEncoder.setFragmentTexture(texture, index: 0)
|
||||
|
||||
renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6 * gridSize * gridSize, instanceCount: 1)
|
||||
}
|
||||
|
||||
renderEncoder.endEncoding()
|
||||
|
||||
commandBuffer.present(drawable)
|
||||
commandBuffer.commit()
|
||||
|
||||
if relativeTime >= 0.7 {
|
||||
//self.startTime = nil
|
||||
self.isPaused = true
|
||||
self.completion()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -13,7 +13,7 @@ swift_library(
|
|||
"//submodules/Display",
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/TelegramUI/Components/SpaceWarpView/STCMeshView",
|
||||
"//submodules/TelegramUI/Components/MeshTransform",
|
||||
"//submodules/UIKitRuntimeUtils",
|
||||
],
|
||||
visibility = [
|
||||
|
|
|
|||
|
|
@ -3,8 +3,82 @@ import UIKit
|
|||
import Display
|
||||
import AsyncDisplayKit
|
||||
import ComponentFlow
|
||||
import STCMeshView
|
||||
import UIKitRuntimeUtils
|
||||
import MeshTransform
|
||||
|
||||
private let backdropLayerClass: NSObject? = {
|
||||
let name = ("CA" as NSString).appendingFormat("BackdropLayer")
|
||||
if let cls = NSClassFromString(name as String) as AnyObject as? NSObject {
|
||||
return cls
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
|
||||
@inline(__always)
|
||||
private func getMethod<T>(object: NSObject, selector: String) -> T? {
|
||||
guard let method = object.method(for: NSSelectorFromString(selector)) else {
|
||||
return nil
|
||||
}
|
||||
return unsafeBitCast(method, to: T.self)
|
||||
}
|
||||
|
||||
private var cachedBackdropLayerAllocMethod: (@convention(c) (AnyObject, Selector) -> NSObject?, Selector)?
|
||||
private func invokeBackdropLayerCreateMethod() -> NSObject? {
|
||||
guard let backdropLayerClass = backdropLayerClass else {
|
||||
return nil
|
||||
}
|
||||
if let cachedBackdropLayerAllocMethod {
|
||||
return cachedBackdropLayerAllocMethod.0(backdropLayerClass, cachedBackdropLayerAllocMethod.1)
|
||||
} else {
|
||||
let method: (@convention(c) (AnyObject, Selector) -> NSObject?)? = getMethod(object: backdropLayerClass, selector: "alloc")
|
||||
if let method {
|
||||
let selector = NSSelectorFromString("alloc")
|
||||
cachedBackdropLayerAllocMethod = (method, selector)
|
||||
return method(backdropLayerClass, selector)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var cachedBackdropLayerInitMethod: (@convention(c) (NSObject, Selector) -> NSObject?, Selector)?
|
||||
private func invokeBackdropLayerInitMethod(object: NSObject) -> NSObject? {
|
||||
if let cachedBackdropLayerInitMethod {
|
||||
return cachedBackdropLayerInitMethod.0(object, cachedBackdropLayerInitMethod.1)
|
||||
} else {
|
||||
let method: (@convention(c) (AnyObject, Selector) -> NSObject?)? = getMethod(object: object, selector: "init")
|
||||
if let method {
|
||||
let selector = NSSelectorFromString("init")
|
||||
cachedBackdropLayerInitMethod = (method, selector)
|
||||
return method(object, selector)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func createBackdropLayer() -> CALayer? {
|
||||
return invokeBackdropLayerCreateMethod().flatMap(invokeBackdropLayerInitMethod) as? CALayer
|
||||
}
|
||||
|
||||
private var cachedBackdropLayerSetScaleMethod: (@convention(c) (NSObject, Selector, Double) -> Void, Selector)?
|
||||
private func invokeBackdropLayerSetScaleMethod(object: NSObject, scale: Double) {
|
||||
if let cachedBackdropLayerSetScaleMethod {
|
||||
cachedBackdropLayerSetScaleMethod.0(object, cachedBackdropLayerSetScaleMethod.1, scale)
|
||||
} else {
|
||||
let method: (@convention(c) (AnyObject, Selector, Double) -> Void)? = getMethod(object: object, selector: "setScale:")
|
||||
if let method {
|
||||
let selector = NSSelectorFromString("setScale:")
|
||||
cachedBackdropLayerSetScaleMethod = (method, selector)
|
||||
method(object, selector, scale)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class BackdropLayerDelegate: NSObject, CALayerDelegate {
|
||||
func action(for layer: CALayer, forKey event: String) -> CAAction? {
|
||||
return nullAction
|
||||
}
|
||||
}
|
||||
|
||||
private extension CGPoint {
|
||||
static func -(lhs: CGPoint, rhs: CGPoint) -> CGPoint {
|
||||
|
|
@ -90,110 +164,6 @@ private func rippleOffset(
|
|||
return n * (-rippleAmount)
|
||||
}
|
||||
|
||||
func transformToFitQuad2(frame: CGRect, topLeft tl: CGPoint, topRight tr: CGPoint, bottomLeft bl: CGPoint, bottomRight br: CGPoint) -> (frame: CGRect, transform: CATransform3D) {
|
||||
let frameTopLeft = frame.origin
|
||||
|
||||
let transform = rectToQuad(
|
||||
rect: CGRect(origin: CGPoint(), size: frame.size),
|
||||
quadTL: CGPoint(x: tl.x - frameTopLeft.x, y: tl.y - frameTopLeft.y),
|
||||
quadTR: CGPoint(x: tr.x - frameTopLeft.x, y: tr.y - frameTopLeft.y),
|
||||
quadBL: CGPoint(x: bl.x - frameTopLeft.x, y: bl.y - frameTopLeft.y),
|
||||
quadBR: CGPoint(x: br.x - frameTopLeft.x, y: br.y - frameTopLeft.y)
|
||||
)
|
||||
|
||||
let anchorPoint = frame.origin
|
||||
let anchorOffset = CGPoint(x: anchorPoint.x - frame.origin.x, y: anchorPoint.y - frame.origin.y)
|
||||
let transPos = CATransform3DMakeTranslation(anchorOffset.x, anchorOffset.y, 0)
|
||||
let transNeg = CATransform3DMakeTranslation(-anchorOffset.x, -anchorOffset.y, 0)
|
||||
let fullTransform = CATransform3DConcat(CATransform3DConcat(transPos, transform), transNeg)
|
||||
|
||||
return (frame, fullTransform)
|
||||
}
|
||||
|
||||
func transformToFitQuad(frame: CGRect, topLeft tl: CGPoint, topRight tr: CGPoint, bottomLeft bl: CGPoint, bottomRight br: CGPoint) -> (frame: CGRect, transform: CATransform3D) {
|
||||
let boundingBox = boundingBox(forQuadWithTR: tr, tl: tl, bl: bl, br: br)
|
||||
|
||||
let frameTopLeft = boundingBox.origin
|
||||
let transform = rectToQuad(
|
||||
rect: CGRect(origin: CGPoint(), size: frame.size),
|
||||
quadTL: CGPoint(x: tl.x - frameTopLeft.x, y: tl.y - frameTopLeft.y),
|
||||
quadTR: CGPoint(x: tr.x - frameTopLeft.x, y: tr.y - frameTopLeft.y),
|
||||
quadBL: CGPoint(x: bl.x - frameTopLeft.x, y: bl.y - frameTopLeft.y),
|
||||
quadBR: CGPoint(x: br.x - frameTopLeft.x, y: br.y - frameTopLeft.y)
|
||||
)
|
||||
|
||||
// To account for anchor point, we must translate, transform, translate
|
||||
let anchorPoint = frame.center
|
||||
let anchorOffset = CGPoint(x: anchorPoint.x - boundingBox.origin.x, y: anchorPoint.y - boundingBox.origin.y)
|
||||
let transPos = CATransform3DMakeTranslation(anchorOffset.x, anchorOffset.y, 0)
|
||||
let transNeg = CATransform3DMakeTranslation(-anchorOffset.x, -anchorOffset.y, 0)
|
||||
let fullTransform = CATransform3DConcat(CATransform3DConcat(transPos, transform), transNeg)
|
||||
|
||||
// Now we set our transform
|
||||
return (boundingBox, fullTransform)
|
||||
}
|
||||
|
||||
private func boundingBox(forQuadWithTR tr: CGPoint, tl: CGPoint, bl: CGPoint, br: CGPoint) -> CGRect {
|
||||
var boundingBox = CGRect.zero
|
||||
|
||||
let xmin = min(min(min(tr.x, tl.x), bl.x), br.x)
|
||||
let ymin = min(min(min(tr.y, tl.y), bl.y), br.y)
|
||||
let xmax = max(max(max(tr.x, tl.x), bl.x), br.x)
|
||||
let ymax = max(max(max(tr.y, tl.y), bl.y), br.y)
|
||||
|
||||
boundingBox.origin.x = xmin
|
||||
boundingBox.origin.y = ymin
|
||||
boundingBox.size.width = xmax - xmin
|
||||
boundingBox.size.height = ymax - ymin
|
||||
|
||||
return boundingBox
|
||||
}
|
||||
|
||||
func rectToQuad(rect: CGRect, quadTL topLeft: CGPoint, quadTR topRight: CGPoint, quadBL bottomLeft: CGPoint, quadBR bottomRight: CGPoint) -> CATransform3D {
|
||||
return rectToQuad(rect: rect, quadTLX: topLeft.x, quadTLY: topLeft.y, quadTRX: topRight.x, quadTRY: topRight.y, quadBLX: bottomLeft.x, quadBLY: bottomLeft.y, quadBRX: bottomRight.x, quadBRY: bottomRight.y)
|
||||
}
|
||||
|
||||
private func rectToQuad(rect: CGRect, quadTLX x1a: CGFloat, quadTLY y1a: CGFloat, quadTRX x2a: CGFloat, quadTRY y2a: CGFloat, quadBLX x3a: CGFloat, quadBLY y3a: CGFloat, quadBRX x4a: CGFloat, quadBRY y4a: CGFloat) -> CATransform3D {
|
||||
let X = rect.origin.x
|
||||
let Y = rect.origin.y
|
||||
let W = rect.size.width
|
||||
let H = rect.size.height
|
||||
|
||||
let y21 = y2a - y1a
|
||||
let y32 = y3a - y2a
|
||||
let y43 = y4a - y3a
|
||||
let y14 = y1a - y4a
|
||||
let y31 = y3a - y1a
|
||||
let y42 = y4a - y2a
|
||||
|
||||
let a = -H * (x2a * x3a * y14 + x2a * x4a * y31 - x1a * x4a * y32 + x1a * x3a * y42)
|
||||
let b = W * (x2a * x3a * y14 + x3a * x4a * y21 + x1a * x4a * y32 + x1a * x2a * y43)
|
||||
let c = H * X * (x2a * x3a * y14 + x2a * x4a * y31 - x1a * x4a * y32 + x1a * x3a * y42) - H * W * x1a * (x4a * y32 - x3a * y42 + x2a * y43) - W * Y * (x2a * x3a * y14 + x3a * x4a * y21 + x1a * x4a * y32 + x1a * x2a * y43)
|
||||
|
||||
let d = H * (-x4a * y21 * y3a + x2a * y1a * y43 - x1a * y2a * y43 - x3a * y1a * y4a + x3a * y2a * y4a)
|
||||
let e = W * (x4a * y2a * y31 - x3a * y1a * y42 - x2a * y31 * y4a + x1a * y3a * y42)
|
||||
let f = -(W * (x4a * (Y * y2a * y31 + H * y1a * y32) - x3a * (H + Y) * y1a * y42 + H * x2a * y1a * y43 + x2a * Y * (y1a - y3a) * y4a + x1a * Y * y3a * (-y2a + y4a)) - H * X * (x4a * y21 * y3a - x2a * y1a * y43 + x3a * (y1a - y2a) * y4a + x1a * y2a * (-y3a + y4a)))
|
||||
|
||||
let g = H * (x3a * y21 - x4a * y21 + (-x1a + x2a) * y43)
|
||||
let h = W * (-x2a * y31 + x4a * y31 + (x1a - x3a) * y42)
|
||||
var i = W * Y * (x2a * y31 - x4a * y31 - x1a * y42 + x3a * y42) + H * (X * (-(x3a * y21) + x4a * y21 + x1a * y43 - x2a * y43) + W * (-(x3a * y2a) + x4a * y2a + x2a * y3a - x4a * y3a - x2a * y4a + x3a * y4a))
|
||||
|
||||
let kEpsilon = 0.0001
|
||||
|
||||
if abs(i) < kEpsilon {
|
||||
i = kEpsilon * (i > 0 ? 1.0 : -1.0)
|
||||
}
|
||||
|
||||
let transform = CATransform3D(
|
||||
m11: a / i, m12: d / i, m13: 0, m14: g / i,
|
||||
m21: b / i, m22: e / i, m23: 0, m24: h / i,
|
||||
m31: 0, m32: 0, m33: 1, m34: 0,
|
||||
m41: c / i, m42: f / i, m43: 0, m44: 1.0
|
||||
)
|
||||
|
||||
return transform
|
||||
}
|
||||
|
||||
public protocol SpaceWarpNode: ASDisplayNode {
|
||||
var contentNode: ASDisplayNode { get }
|
||||
|
||||
|
|
@ -201,126 +171,6 @@ public protocol SpaceWarpNode: ASDisplayNode {
|
|||
func update(size: CGSize, cornerRadius: CGFloat, transition: ComponentTransition)
|
||||
}
|
||||
|
||||
private final class MaskGridLayer: SimpleLayer {
|
||||
private var itemLayers: [SimpleLayer] = []
|
||||
|
||||
private var resolution: (x: Int, y: Int)?
|
||||
|
||||
func updateGrid(size: CGSize, resolutionX: Int, resolutionY: Int, cornerRadius: CGFloat) {
|
||||
if let resolution = self.resolution, resolution.x == resolutionX, resolution.y == resolutionY {
|
||||
return
|
||||
}
|
||||
self.resolution = (resolutionX, resolutionY)
|
||||
|
||||
for itemLayer in self.itemLayers {
|
||||
itemLayer.removeFromSuperlayer()
|
||||
}
|
||||
self.itemLayers.removeAll()
|
||||
|
||||
let itemSize = CGSize(width: size.width / CGFloat(resolutionX), height: size.height / CGFloat(resolutionY))
|
||||
|
||||
let topLeftCorner = CGRect(origin: CGPoint(), size: CGSize(width: cornerRadius, height: cornerRadius))
|
||||
let topRightCorner = CGRect(origin: CGPoint(x: size.width - cornerRadius, y: 0.0), size: CGSize(width: cornerRadius, height: cornerRadius))
|
||||
let bottomLeftCorner = CGRect(origin: CGPoint(x: 0.0, y: size.height - cornerRadius), size: CGSize(width: cornerRadius, height: cornerRadius))
|
||||
let bottomRightCorner = CGRect(origin: CGPoint(x: size.width - cornerRadius, y: size.height - cornerRadius), size: CGSize(width: cornerRadius, height: cornerRadius))
|
||||
|
||||
var cornersImage: UIImage?
|
||||
if cornerRadius > 0.0 {
|
||||
cornersImage = generateImage(CGSize(width: cornerRadius * 2.0 + 200.0, height: cornerRadius * 2.0 + 200.0), rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.setFillColor(UIColor.black.cgColor)
|
||||
context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: cornerRadius).cgPath)
|
||||
context.fillPath()
|
||||
})
|
||||
}
|
||||
|
||||
for y in 0 ..< resolutionY {
|
||||
for x in 0 ..< resolutionX {
|
||||
let itemLayer = SimpleLayer()
|
||||
itemLayer.backgroundColor = UIColor.black.cgColor
|
||||
itemLayer.isOpaque = true
|
||||
itemLayer.opacity = 1.0
|
||||
itemLayer.anchorPoint = CGPoint()
|
||||
self.addSublayer(itemLayer)
|
||||
self.itemLayers.append(itemLayer)
|
||||
|
||||
if cornerRadius > 0.0, let cornersImage {
|
||||
let gridPosition = CGPoint(x: CGFloat(x) / CGFloat(resolutionX), y: CGFloat(y) / CGFloat(resolutionY))
|
||||
let sourceRect = CGRect(origin: CGPoint(x: gridPosition.x * (size.width), y: gridPosition.y * (size.height)), size: itemSize)
|
||||
if sourceRect.intersects(topLeftCorner) || sourceRect.intersects(topRightCorner) || sourceRect.intersects(bottomLeftCorner) || sourceRect.intersects(bottomRightCorner) {
|
||||
var clippedCornersRect = sourceRect
|
||||
if clippedCornersRect.maxX > cornersImage.size.width {
|
||||
clippedCornersRect.origin.x -= size.width - cornersImage.size.width
|
||||
}
|
||||
if clippedCornersRect.maxY > cornersImage.size.height {
|
||||
clippedCornersRect.origin.y -= size.height - cornersImage.size.height
|
||||
}
|
||||
|
||||
itemLayer.contents = cornersImage.cgImage
|
||||
itemLayer.contentsRect = CGRect(origin: CGPoint(x: clippedCornersRect.minX / cornersImage.size.width, y: clippedCornersRect.minY / cornersImage.size.height), size: CGSize(width: clippedCornersRect.width / cornersImage.size.width, height: clippedCornersRect.height / cornersImage.size.height))
|
||||
itemLayer.backgroundColor = nil
|
||||
itemLayer.isOpaque = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func update(positions: [CGPoint], bounds: [CGRect], transforms: [CATransform3D]) {
|
||||
for i in 0 ..< self.itemLayers.count {
|
||||
if i < positions.count && i < bounds.count && i < transforms.count {
|
||||
let itemLayer = self.itemLayers[i]
|
||||
itemLayer.position = positions[i]
|
||||
itemLayer.bounds = bounds[i]
|
||||
itemLayer.transform = transforms[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class PrivateContentLayerRestoreContext {
|
||||
final class Reference {
|
||||
weak var layer: CALayer?
|
||||
|
||||
init(layer: CALayer) {
|
||||
self.layer = layer
|
||||
}
|
||||
}
|
||||
|
||||
private static func collectPrivateContentLayers(layer: CALayer, into references: inout [Reference]) {
|
||||
if getLayerDisableScreenshots(layer) {
|
||||
references.append(Reference(layer: layer))
|
||||
}
|
||||
if let sublayers = layer.sublayers {
|
||||
for sublayer in sublayers {
|
||||
collectPrivateContentLayers(layer: sublayer, into: &references)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let references: [Reference]
|
||||
|
||||
init(rootLayer: CALayer) {
|
||||
var references: [Reference] = []
|
||||
PrivateContentLayerRestoreContext.collectPrivateContentLayers(layer: rootLayer, into: &references)
|
||||
self.references = references
|
||||
|
||||
for reference in self.references {
|
||||
if let layer = reference.layer {
|
||||
setLayerDisableScreenshots(layer, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func restore() {
|
||||
for reference in self.references {
|
||||
if let layer = reference.layer {
|
||||
setLayerDisableScreenshots(layer, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open class SpaceWarpNodeImpl: ASDisplayNode, SpaceWarpNode {
|
||||
private final class Shockwave {
|
||||
let startPoint: CGPoint
|
||||
|
|
@ -337,13 +187,11 @@ open class SpaceWarpNodeImpl: ASDisplayNode, SpaceWarpNode {
|
|||
|
||||
private let contentNodeSource: ASDisplayNode
|
||||
private let backgroundView: UIView
|
||||
private var currentCloneView: UIView?
|
||||
private var meshView: STCMeshView?
|
||||
|
||||
private var privateContentRestoreContext: PrivateContentLayerRestoreContext?
|
||||
private let backdropLayer: CALayer?
|
||||
private let cornerOverlayView: UIImageView
|
||||
private let backdropLayerDelegate: BackdropLayerDelegate
|
||||
|
||||
private var gradientLayer: SimpleGradientLayer?
|
||||
private var gradientMaskLayer: MaskGridLayer?
|
||||
|
||||
private var link: SharedDisplayLinkDriver.Link?
|
||||
|
||||
|
|
@ -351,30 +199,43 @@ open class SpaceWarpNodeImpl: ASDisplayNode, SpaceWarpNode {
|
|||
|
||||
private var resolution: (x: Int, y: Int)?
|
||||
private var layoutParams: (size: CGSize, cornerRadius: CGFloat)?
|
||||
private var cornerOverlayImageRadius: CGFloat?
|
||||
|
||||
private let cornerOverlayInset: CGFloat = 48.0
|
||||
|
||||
override public init() {
|
||||
self.backdropLayerDelegate = BackdropLayerDelegate()
|
||||
self.backdropLayer = createBackdropLayer()
|
||||
self.cornerOverlayView = UIImageView()
|
||||
|
||||
self.contentNodeSource = ASDisplayNode()
|
||||
self.contentNodeSource.layer.rasterizationScale = UIScreenScale
|
||||
|
||||
self.backgroundView = UIView()
|
||||
self.backgroundView.backgroundColor = .black
|
||||
self.backgroundView.isHidden = true
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.contentNodeSource)
|
||||
self.view.addSubview(self.backgroundView)
|
||||
self.view.insertSubview(self.backgroundView, belowSubview: self.contentNodeSource.view)
|
||||
|
||||
if let backdropLayer = self.backdropLayer {
|
||||
self.layer.addSublayer(backdropLayer)
|
||||
backdropLayer.delegate = self.backdropLayerDelegate
|
||||
backdropLayer.isHidden = true
|
||||
|
||||
invokeBackdropLayerSetScaleMethod(object: backdropLayer, scale: UIScreenScale)
|
||||
backdropLayer.rasterizationScale = UIScreenScale
|
||||
}
|
||||
|
||||
self.cornerOverlayView.isUserInteractionEnabled = false
|
||||
self.cornerOverlayView.isHidden = true
|
||||
self.cornerOverlayView.layer.rasterizationScale = UIScreenScale
|
||||
self.view.addSubview(self.cornerOverlayView)
|
||||
}
|
||||
|
||||
public static func supportsHierarchy(layer: CALayer) -> Bool {
|
||||
if getLayerDisableScreenshots(layer) {
|
||||
return false
|
||||
}
|
||||
if let sublayers = layer.sublayers {
|
||||
for sublayer in sublayers {
|
||||
if !supportsHierarchy(layer: sublayer) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
@ -415,17 +276,92 @@ open class SpaceWarpNodeImpl: ASDisplayNode, SpaceWarpNode {
|
|||
return
|
||||
}
|
||||
self.resolution = (resolutionX, resolutionY)
|
||||
}
|
||||
|
||||
private func makeRippleMeshTransform(size: CGSize, resolution: (x: Int, y: Int), params: RippleParams) -> MeshTransform.Value? {
|
||||
let vertexWidth = resolution.x + 1
|
||||
let vertexHeight = resolution.y + 1
|
||||
let vertexCount = vertexWidth * vertexHeight
|
||||
|
||||
if let meshView = self.meshView {
|
||||
self.meshView = nil
|
||||
meshView.removeFromSuperview()
|
||||
guard size.width > 0.0, size.height > 0.0, vertexCount > 0 else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let meshView = STCMeshView(frame: CGRect())
|
||||
self.meshView = meshView
|
||||
self.view.insertSubview(meshView, aboveSubview: self.backgroundView)
|
||||
func vertexIndex(_ x: Int, _ y: Int) -> Int {
|
||||
return y * vertexWidth + x
|
||||
}
|
||||
|
||||
meshView.instanceCount = resolutionX * resolutionY
|
||||
var positions = Array(repeating: CGPoint(), count: vertexCount)
|
||||
for y in 0 ..< vertexHeight {
|
||||
let normalizedY = CGFloat(y) / CGFloat(resolution.y)
|
||||
for x in 0 ..< vertexWidth {
|
||||
let normalizedX = CGFloat(x) / CGFloat(resolution.x)
|
||||
let initialPosition = CGPoint(x: normalizedX * size.width, y: normalizedY * size.height)
|
||||
|
||||
var displacedPosition = initialPosition
|
||||
for shockwave in self.shockwaves {
|
||||
displacedPosition = displacedPosition + rippleOffset(position: initialPosition, origin: shockwave.startPoint, time: shockwave.timeValue, params: params)
|
||||
}
|
||||
|
||||
positions[vertexIndex(x, y)] = displacedPosition
|
||||
}
|
||||
}
|
||||
|
||||
let mesh = MeshTransform()
|
||||
for y in 0 ..< vertexHeight {
|
||||
let normalizedY = CGFloat(y) / CGFloat(resolution.y)
|
||||
for x in 0 ..< vertexWidth {
|
||||
let normalizedX = CGFloat(x) / CGFloat(resolution.x)
|
||||
let source = CGPoint(x: normalizedX, y: normalizedY)
|
||||
let displacedPosition = positions[vertexIndex(x, y)]
|
||||
let destination = MeshTransform.Point3D(
|
||||
x: displacedPosition.x / size.width,
|
||||
y: displacedPosition.y / size.height,
|
||||
z: 0.0
|
||||
)
|
||||
mesh.add(MeshTransform.Vertex(from: source, to: destination))
|
||||
}
|
||||
}
|
||||
|
||||
for y in 0 ..< resolution.y {
|
||||
for x in 0 ..< resolution.x {
|
||||
let topLeft = UInt32(vertexIndex(x, y))
|
||||
let topRight = UInt32(vertexIndex(x + 1, y))
|
||||
let bottomRight = UInt32(vertexIndex(x + 1, y + 1))
|
||||
let bottomLeft = UInt32(vertexIndex(x, y + 1))
|
||||
|
||||
mesh.add(MeshTransform.Face(indices: (topLeft, topRight, bottomRight, bottomLeft), w: (0.0, 0.0, 0.0, 0.0)))
|
||||
}
|
||||
}
|
||||
|
||||
return mesh.makeValue()
|
||||
}
|
||||
|
||||
private func updateCornerOverlayImage(cornerRadius: CGFloat) {
|
||||
let cornerRadius = max(0.0, cornerRadius)
|
||||
if let currentRadius = self.cornerOverlayImageRadius, abs(currentRadius - cornerRadius) < 0.001 {
|
||||
return
|
||||
}
|
||||
self.cornerOverlayImageRadius = cornerRadius
|
||||
|
||||
let cornerExtent = max(1.0, ceil(cornerRadius + self.cornerOverlayInset))
|
||||
let imageSize = CGSize(width: cornerExtent * 2.0 + 1.0, height: cornerExtent * 2.0 + 1.0)
|
||||
|
||||
self.cornerOverlayView.image = generateImage(imageSize, opaque: false, rotatedContext: { size, context in
|
||||
let bounds = CGRect(origin: CGPoint(), size: size)
|
||||
context.clear(bounds)
|
||||
context.setFillColor(UIColor.black.cgColor)
|
||||
context.fill(bounds)
|
||||
|
||||
let innerRect = bounds.insetBy(dx: self.cornerOverlayInset, dy: self.cornerOverlayInset)
|
||||
context.setBlendMode(.clear)
|
||||
context.addPath(UIBezierPath(roundedRect: innerRect, cornerRadius: cornerRadius).cgPath)
|
||||
context.fillPath()
|
||||
context.setBlendMode(.normal)
|
||||
})?.resizableImage(
|
||||
withCapInsets: UIEdgeInsets(top: cornerExtent, left: cornerExtent, bottom: cornerExtent, right: cornerExtent),
|
||||
resizingMode: .stretch
|
||||
)
|
||||
}
|
||||
|
||||
public func update(size: CGSize, cornerRadius: CGFloat, transition: ComponentTransition) {
|
||||
|
|
@ -438,12 +374,7 @@ open class SpaceWarpNodeImpl: ASDisplayNode, SpaceWarpNode {
|
|||
|
||||
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
let params = RippleParams(amplitude: 10.0, frequency: 15.0, decay: 5.5, speed: 1400.0)
|
||||
|
||||
if let currentCloneView = self.currentCloneView {
|
||||
currentCloneView.removeFromSuperview()
|
||||
self.currentCloneView = nil
|
||||
}
|
||||
let params = RippleParams(amplitude: 15.0, frequency: 15.0, decay: 5.5, speed: 1400.0)
|
||||
|
||||
let maxEdge = (max(size.width, size.height) * 0.5) * 2.0
|
||||
let maxDistance = sqrt(maxEdge * maxEdge + maxEdge * maxEdge)
|
||||
|
|
@ -461,10 +392,14 @@ open class SpaceWarpNodeImpl: ASDisplayNode, SpaceWarpNode {
|
|||
link.invalidate()
|
||||
}
|
||||
|
||||
if let meshView = self.meshView {
|
||||
self.meshView = nil
|
||||
meshView.removeFromSuperview()
|
||||
if let backdropLayer = self.backdropLayer {
|
||||
backdropLayer.removeAnimation(forKey: "meshTransform")
|
||||
backdropLayer.setValue(nil, forKey: "meshTransform")
|
||||
backdropLayer.isHidden = true
|
||||
}
|
||||
self.cornerOverlayView.layer.removeAnimation(forKey: "meshTransform")
|
||||
self.cornerOverlayView.layer.setValue(nil, forKey: "meshTransform")
|
||||
self.cornerOverlayView.isHidden = true
|
||||
|
||||
self.resolution = nil
|
||||
self.backgroundView.isHidden = true
|
||||
|
|
@ -475,50 +410,42 @@ open class SpaceWarpNodeImpl: ASDisplayNode, SpaceWarpNode {
|
|||
self.gradientLayer = nil
|
||||
gradientLayer.removeFromSuperlayer()
|
||||
}
|
||||
if let gradientMaskLayer = self.gradientMaskLayer {
|
||||
self.gradientMaskLayer = nil
|
||||
gradientMaskLayer.removeFromSuperlayer()
|
||||
}
|
||||
|
||||
if let privateContentRestoreContext = self.privateContentRestoreContext {
|
||||
self.privateContentRestoreContext = nil
|
||||
privateContentRestoreContext.restore()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if self.privateContentRestoreContext == nil {
|
||||
self.privateContentRestoreContext = PrivateContentLayerRestoreContext(rootLayer: self.contentNodeSource.view.layer)
|
||||
}
|
||||
|
||||
self.backgroundView.isHidden = false
|
||||
self.contentNodeSource.clipsToBounds = true
|
||||
self.contentNodeSource.layer.cornerRadius = cornerRadius
|
||||
|
||||
let resolutionX = max(2, Int(size.width / 50.0))
|
||||
let resolutionY = max(2, Int(size.height / 50.0))
|
||||
if let backdropLayer = self.backdropLayer {
|
||||
backdropLayer.isHidden = false
|
||||
transition.setFrame(layer: backdropLayer, frame: CGRect(origin: CGPoint(), size: size))
|
||||
transition.setCornerRadius(layer: backdropLayer, cornerRadius: cornerRadius)
|
||||
|
||||
self.cornerOverlayView.isHidden = false
|
||||
self.updateCornerOverlayImage(cornerRadius: cornerRadius)
|
||||
transition.setFrame(
|
||||
view: self.cornerOverlayView,
|
||||
frame: CGRect(
|
||||
x: -self.cornerOverlayInset,
|
||||
y: -self.cornerOverlayInset,
|
||||
width: size.width + self.cornerOverlayInset * 2.0,
|
||||
height: size.height + self.cornerOverlayInset * 2.0
|
||||
)
|
||||
)
|
||||
} else {
|
||||
self.cornerOverlayView.isHidden = true
|
||||
}
|
||||
|
||||
let resolutionX = max(2, Int(size.width / 48.0))
|
||||
let resolutionY = max(2, Int(size.height / 48.0))
|
||||
self.updateGrid(resolutionX: resolutionX, resolutionY: resolutionY)
|
||||
guard let resolution = self.resolution, let meshView = self.meshView else {
|
||||
guard let resolution = self.resolution else {
|
||||
return
|
||||
}
|
||||
|
||||
if let cloneView = self.contentNodeSource.view.resizableSnapshotView(from: CGRect(origin: CGPoint(), size: size), afterScreenUpdates: false, withCapInsets: UIEdgeInsets()) {
|
||||
self.currentCloneView = cloneView
|
||||
meshView.contentView.addSubview(cloneView)
|
||||
}
|
||||
|
||||
meshView.frame = CGRect(origin: CGPoint(), size: size)
|
||||
|
||||
if let shockwave = self.shockwaves.first {
|
||||
let gradientMaskLayer: MaskGridLayer
|
||||
if let current = self.gradientMaskLayer {
|
||||
gradientMaskLayer = current
|
||||
} else {
|
||||
gradientMaskLayer = MaskGridLayer()
|
||||
self.gradientMaskLayer = gradientMaskLayer
|
||||
}
|
||||
|
||||
let gradientLayer: SimpleGradientLayer
|
||||
if let current = self.gradientLayer {
|
||||
gradientLayer = current
|
||||
|
|
@ -529,12 +456,9 @@ open class SpaceWarpNodeImpl: ASDisplayNode, SpaceWarpNode {
|
|||
|
||||
gradientLayer.type = .radial
|
||||
gradientLayer.colors = [UIColor(white: 1.0, alpha: 0.0).cgColor, UIColor(white: 1.0, alpha: 0.0).cgColor, UIColor(white: 1.0, alpha: 0.2).cgColor, UIColor(white: 1.0, alpha: 0.0).cgColor]
|
||||
|
||||
gradientLayer.mask = gradientMaskLayer
|
||||
}
|
||||
|
||||
gradientLayer.frame = CGRect(origin: CGPoint(), size: size)
|
||||
gradientMaskLayer.frame = CGRect(origin: CGPoint(), size: size)
|
||||
|
||||
gradientLayer.startPoint = CGPoint(x: shockwave.startPoint.x / size.width, y: shockwave.startPoint.y / size.height)
|
||||
|
||||
|
|
@ -564,73 +488,20 @@ open class SpaceWarpNodeImpl: ASDisplayNode, SpaceWarpNode {
|
|||
}
|
||||
}
|
||||
|
||||
let itemSize = CGSize(width: size.width / CGFloat(resolution.x), height: size.height / CGFloat(resolution.y))
|
||||
|
||||
var instanceBounds: [CGRect] = []
|
||||
var instancePositions: [CGPoint] = []
|
||||
var instanceTransforms: [CATransform3D] = []
|
||||
|
||||
for y in 0 ..< resolution.y {
|
||||
for x in 0 ..< resolution.x {
|
||||
let gridPosition = CGPoint(x: CGFloat(x) / CGFloat(resolution.x), y: CGFloat(y) / CGFloat(resolution.y))
|
||||
|
||||
let sourceRect = CGRect(origin: CGPoint(x: gridPosition.x * (size.width), y: gridPosition.y * (size.height)), size: itemSize)
|
||||
|
||||
let initialTopLeft = CGPoint(x: sourceRect.minX, y: sourceRect.minY)
|
||||
let initialTopRight = CGPoint(x: sourceRect.maxX, y: sourceRect.minY)
|
||||
let initialBottomLeft = CGPoint(x: sourceRect.minX, y: sourceRect.maxY)
|
||||
let initialBottomRight = CGPoint(x: sourceRect.maxX, y: sourceRect.maxY)
|
||||
|
||||
var topLeft = initialTopLeft
|
||||
var topRight = initialTopRight
|
||||
var bottomLeft = initialBottomLeft
|
||||
var bottomRight = initialBottomRight
|
||||
|
||||
for shockwave in self.shockwaves {
|
||||
topLeft = topLeft + rippleOffset(position: initialTopLeft, origin: shockwave.startPoint, time: shockwave.timeValue, params: params)
|
||||
topRight = topRight + rippleOffset(position: initialTopRight, origin: shockwave.startPoint, time: shockwave.timeValue, params: params)
|
||||
bottomLeft = bottomLeft + rippleOffset(position: initialBottomLeft, origin: shockwave.startPoint, time: shockwave.timeValue, params: params)
|
||||
bottomRight = bottomRight + rippleOffset(position: initialBottomRight, origin: shockwave.startPoint, time: shockwave.timeValue, params: params)
|
||||
if let backdropLayer = self.backdropLayer {
|
||||
if let meshTransform = self.makeRippleMeshTransform(size: size, resolution: resolution, params: params) {
|
||||
if !transition.animation.isImmediate {
|
||||
backdropLayer.removeAnimation(forKey: "meshTransform")
|
||||
self.cornerOverlayView.layer.removeAnimation(forKey: "meshTransform")
|
||||
}
|
||||
/*topLeft = transformCoordinate(position: topLeft, origin: startPoint, time: self.timeValue, params: params)
|
||||
topRight = transformCoordinate(position: topRight, origin: startPoint, time: self.timeValue, params: params)
|
||||
bottomLeft = transformCoordinate(position: bottomLeft, origin: startPoint, time: self.timeValue, params: params)
|
||||
bottomRight = transformCoordinate(position: bottomRight, origin: startPoint, time: self.timeValue, params: params)*/
|
||||
|
||||
let distanceTopLeft = length(topLeft - initialTopLeft)
|
||||
let distanceTopRight = length(topRight - initialTopRight)
|
||||
let distanceBottomLeft = length(bottomLeft - initialBottomLeft)
|
||||
let distanceBottomRight = length(bottomRight - initialBottomRight)
|
||||
var maxDistance = max(distanceTopLeft, distanceTopRight)
|
||||
maxDistance = max(maxDistance, distanceBottomLeft)
|
||||
maxDistance = max(maxDistance, distanceBottomRight)
|
||||
|
||||
var (frame, transform) = transformToFitQuad2(frame: sourceRect, topLeft: topLeft, topRight: topRight, bottomLeft: bottomLeft, bottomRight: bottomRight)
|
||||
|
||||
if maxDistance <= 0.005 {
|
||||
transform = CATransform3DIdentity
|
||||
}
|
||||
|
||||
instanceBounds.append(frame)
|
||||
instancePositions.append(frame.origin)
|
||||
|
||||
instanceTransforms.append(transform)
|
||||
backdropLayer.setValue(meshTransform, forKey: "meshTransform")
|
||||
self.cornerOverlayView.layer.setValue(meshTransform, forKey: "meshTransform")
|
||||
} else {
|
||||
backdropLayer.setValue(nil, forKey: "meshTransform")
|
||||
self.cornerOverlayView.layer.setValue(nil, forKey: "meshTransform")
|
||||
}
|
||||
}
|
||||
|
||||
instanceBounds.withUnsafeMutableBufferPointer { buffer in
|
||||
meshView.instanceBounds = buffer.baseAddress!
|
||||
}
|
||||
instancePositions.withUnsafeMutableBufferPointer { buffer in
|
||||
meshView.instancePositions = buffer.baseAddress!
|
||||
}
|
||||
instanceTransforms.withUnsafeMutableBufferPointer { buffer in
|
||||
meshView.instanceTransforms = buffer.baseAddress!
|
||||
}
|
||||
|
||||
if let gradientMaskLayer = self.gradientMaskLayer {
|
||||
gradientMaskLayer.updateGrid(size: size, resolutionX: resolutionX, resolutionY: resolutionY, cornerRadius: cornerRadius)
|
||||
gradientMaskLayer.update(positions: instancePositions, bounds: instanceBounds, transforms: instanceTransforms)
|
||||
} else {
|
||||
self.cornerOverlayView.layer.setValue(nil, forKey: "meshTransform")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue