Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
@@ -0,0 +1,73 @@
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 = "PremiumDiamondComponentMetalResources",
srcs = glob([
"MetalResources/**/*.*",
]),
visibility = ["//visibility:public"],
)
plist_fragment(
name = "PremiumDiamondComponentBundleInfoPlist",
extension = "plist",
template =
"""
<key>CFBundleIdentifier</key>
<string>org.telegram.PremiumDiamondComponent</string>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleName</key>
<string>StoryPeerList</string>
"""
)
apple_resource_bundle(
name = "PremiumDiamondComponentBundle",
infoplists = [
":PremiumDiamondComponentBundleInfoPlist",
],
resources = [
":PremiumDiamondComponentMetalResources",
],
)
swift_library(
name = "PremiumDiamondComponent",
module_name = "PremiumDiamondComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
data = [
":PremiumDiamondComponentBundle",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/MetalEngine",
"//submodules/ComponentFlow",
"//submodules/AccountContext",
"//submodules/AppBundle",
"//submodules/GZip",
"//submodules/LegacyComponents",
"//submodules/TelegramPresentationData",
"//submodules/Components/MultilineTextComponent:MultilineTextComponent",
"//submodules/TelegramUI/Components/Premium/PremiumStarComponent",
"//submodules/TelegramUI/Components/Utils/AnimatableProperty",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,271 @@
#include <metal_stdlib>
using namespace metal;
#define EPS 1e-4
#define EPS2 1e-4
#define NEAR 1.0
#define FAR 10.0
#define NEAR2 0.02
#define ITER 96
#define ITER2 48
#define RI1 2.40
#define RI2 2.44
#define PI 3.14159265359
float3 hsv(float h, float s, float v) {
float3 k = float3(1.0, 2.0 / 3.0, 1.0 / 3.0);
float3 p = abs(fract(h + k.xyz) * 6.0 - 3.0);
return v * mix(float3(1.0), clamp(p - 1.0, 0.0, 1.0), s);
}
float2x2 rot(float a) {
float s = sin(a), c = cos(a);
return float2x2(c, s, -s, c);
}
float sdTable(float3 p) {
float2 d = abs(float2(length(p.xz), (p.y + 0.159) * 1.650)) - float2(1.0);
return min(max(d.x, d.y), 0.0) + length(max(d, 0.0));
}
float sdCut(float3 p, float a, float h) {
p.y *= a;
p.y -= (abs(p.x) + abs(p.z)) * h;
p = abs(p);
return (p.x + p.y + p.z - 1.0) * 0.5;
}
constant float2x2 ROT4 = float2x2(0.70710678, 0.70710678, -0.70710678, 0.70710678);
constant float2x2 ROT3 = float2x2(0.92387953, 0.38268343, -0.38268343, 0.92387953);
constant float2x2 ROT2 = float2x2(0.38268343, 0.92387953, -0.92387953, 0.38268343);
constant float2x2 ROT1 = float2x2(0.19509032, 0.98078528, -0.98078528, 0.19509032);
float map(float3 p, float time, float3 cameraRotation) {
p.y *= 0.72;
p.yz = p.yz;
p.xz = rot(time * 0.45) * p.xz;
float d = sdTable(p);
float3 q = p * 0.3000;
q.y += 0.0808;
q.xz = ROT2 * q.xz;
q.xz = abs(q.xz);
q.xz = ROT4 * q.xz;
q.xz = abs(q.xz);
q.xz = ROT2 * q.xz;
d = max(d, sdCut(q, 3.700, 0.0000));
q = p * 0.691;
q.xz = abs(q.xz);
q.xz = ROT4 * q.xz;
q.xz = abs(q.xz);
q.xz = ROT2 * q.xz;
d = max(d, sdCut(q, 1.868, 0.1744));
q *= 1.022;
q.y -= 0.034;
q.xz = ROT1 * q.xz;
d = max(d, sdCut(q, 1.650, 0.1000));
q.xz = ROT3 * q.xz;
d = max(d, sdCut(q, 1.650, 0.1000));
return d;
}
float3 normal(float3 p, float time, float3 cameraRotation) {
float2 e = float2(EPS, 0);
return normalize(float3(
map(p + e.xyy, time, cameraRotation) - map(p - e.xyy, time, cameraRotation),
map(p + e.yxy, time, cameraRotation) - map(p - e.yxy, time, cameraRotation),
map(p + e.yyx, time, cameraRotation) - map(p - e.yyx, time, cameraRotation)
));
}
float trace(float3 ro, float3 rd, thread float3 &p, thread float3 &n, float time, float3 cameraRotation) {
float t = NEAR, d;
for (int i = 0; i < ITER; i++) {
p = ro + rd * t;
d = map(p, time, cameraRotation);
if (abs(d) < EPS || t > FAR) break;
t += step(d, 1.0) * d * 0.5 + d * 0.5;
}
n = normal(p, time, cameraRotation);
return min(t, FAR);
}
float trace2(float3 ro, float3 rd, thread float3 &p, thread float3 &n, float time, float3 cameraRotation) {
float t = NEAR2, d;
for (int i = 0; i < ITER2; i++) {
p = ro + rd * t;
d = -map(p, time, cameraRotation);
if (abs(d) < EPS2 || d < EPS2) break;
t += d;
}
n = -normal(p, time, cameraRotation);
return t;
}
float schlickFresnel(float ri, float co) {
float r = (1.0 - ri) / (1.0 + ri);
r = r * r;
return r + (1.0 - r) * pow(1.0 - co, 5.0);
}
float3 lightPath(float3 p, float3 rd, float ri, float time, float3 cameraRotation) {
float3 n;
float3 r0 = -rd;
trace2(p, rd, p, n, time, cameraRotation);
rd = reflect(rd, n);
float3 r1 = refract(rd, n, ri);
r1 = length(r1) < EPS ? r0 : r1;
trace2(p, rd, p, n, time, cameraRotation);
rd = reflect(rd, n);
float3 r2 = refract(rd, n, ri);
r2 = length(r2) < EPS ? r1 : r2;
trace2(p, rd, p, n, time, cameraRotation);
float3 r3 = refract(rd, n, ri);
return length(r3) < EPS ? r2 : r3;
}
float3 material(float3 p, float3 rd, float3 n, texturecube<float> cubemap, float time, float3 cameraRotation) {
float3 l0 = reflect(rd, n);
float co = max(0.0, dot(-rd, n));
float f1 = schlickFresnel(RI1, co);
float3 l1 = lightPath(p, refract(rd, n, 1.0 / RI1), RI1, time, cameraRotation);
float f2 = schlickFresnel(RI2, co);
float3 l2 = lightPath(p, refract(rd, n, 1.0 / RI2), RI2, time, cameraRotation);
float a = 0.0;
float3 dc = float3(0.0);
float3 r = cubemap.sample(sampler(mag_filter::linear, min_filter::linear), l0).rgb;
for (int i = 0; i < 10; i++) {
float3 l = normalize(mix(l1, l2, a));
float f = mix(f1, f2, a);
dc += cubemap.sample(sampler(mag_filter::linear, min_filter::linear), l).rgb * hsv(a + 0.9, 1.0, 1.0) * (1.0 - f) + r * f;
a += 0.1;
}
dc *= 0.19;
return dc;
}
kernel void compute_main(texture2d<float, access::write> outputTexture [[texture(0)]],
texturecube<float> cubemap [[texture(1)]],
constant float &iTime [[buffer(0)]],
constant float2 &iResolution [[buffer(1)]],
constant float3 &cameraRotation [[buffer(2)]],
uint2 gid [[thread_position_in_grid]]) {
if (gid.x >= uint(iResolution.x) || gid.y >= uint(iResolution.y)) {
return;
}
float2 fragCoord = float2(gid.x, gid.y);
float2 uv = (fragCoord - 0.5 * iResolution) / iResolution.y;
float3 ro = float3(0.0, 0.0, -4.0);
float3 rd = normalize(float3(uv, 1.1));
float2x2 ry = rot(cameraRotation.y); // Yaw
ro.yz = ry * ro.yz;
rd.yz = ry * rd.yz;
float2x2 rx = rot(cameraRotation.x); // Pitch
ro.xz = rx * ro.xz;
rd.xz = rx * rd.xz;
float2x2 rz = rot(0.0); // cameraRotation.z); // Roll
ro.xy = rz * ro.xy;
rd.xy = rz * rd.xy;
float3 p, n;
float t = trace(ro, rd, p, n, iTime, cameraRotation);
float3 c = float3(0.0);
float w = 0.0;
if (t > 9.0) {
c = float3(1.0, 0.0, 0.0);
//c = cubemap.sample(sampler(mag_filter::linear, min_filter::linear), rd).rgb;
} else {
c = material(p, rd, n, cubemap, iTime, cameraRotation);
w = smoothstep(1.60, 1.61, length(c));
}
outputTexture.write(float4(c, w), gid);
}
#define POST_ITER 36.0
#define RADIUS 0.05
struct QuadVertexOut {
float4 position [[position]];
float2 uv;
};
constant static float2 quadVertices[6] = {
float2(0.0, 0.0),
float2(1.0, 0.0),
float2(0.0, 1.0),
float2(1.0, 0.0),
float2(0.0, 1.0),
float2(1.0, 1.0)
};
vertex QuadVertexOut post_vertex_main(
constant float4 &rect [[ buffer(0) ]],
uint vid [[ vertex_id ]]
) {
float2 quadVertex = quadVertices[vid];
QuadVertexOut out;
out.position = float4(rect.x + quadVertex.x * rect.z, rect.y + quadVertex.y * rect.w, 0.0, 1.0);
out.position.x = -1.0 + out.position.x * 2.0;
out.position.y = -1.0 + out.position.y * 2.0;
out.uv = quadVertex;
return out;
}
fragment float4 post_fragment_main(QuadVertexOut in [[stage_in]],
constant float &iTime [[buffer(0)]],
constant float2 &iResolution [[buffer(1)]],
texture2d<float> inputTexture [[texture(0)]]) {
constexpr sampler textureSampler(mag_filter::linear, min_filter::linear, address::clamp_to_edge);
float2 uv = in.uv;
float2 m = float2(1.0, iResolution.x / iResolution.y);
float4 co = inputTexture.sample(textureSampler, uv);
float4 c = co;
float a = sin(iTime * 0.1) * 6.283;
float v = 0.0;
float b = 1.0 / POST_ITER;
for (int j = 0; j < 6; j++) {
float r = RADIUS / POST_ITER;
float2 d = float2(cos(a), sin(a)) * m;
for (int i = 0; i < int(POST_ITER); i++) {
float4 sample = inputTexture.sample(textureSampler, uv + d * r * RADIUS);
v += sample.w * (1.0 - r);
r += b;
}
a += 1.047;
}
v *= 0.01;
c += float4(v, v, v, 0.0);
c.w = 1.0;
if (co.r == 1.0 && co.g == 0.0 && co.b == 0.0) {
c.w = 0.0;
} else {
c.w = 1.0;
}
return c;
}
@@ -0,0 +1,393 @@
import Foundation
import Display
import Metal
import MetalKit
import MetalEngine
import ComponentFlow
import TelegramPresentationData
import AnimatableProperty
import SwiftSignalKit
private var metalLibraryValue: MTLLibrary?
func metalLibrary(device: MTLDevice) -> MTLLibrary? {
if let metalLibraryValue {
return metalLibraryValue
}
let mainBundle = Bundle(for: DiamondLayer.self)
guard let path = mainBundle.path(forResource: "PremiumDiamondComponentBundle", ofType: "bundle") else {
return nil
}
guard let bundle = Bundle(path: path) else {
return nil
}
guard let library = try? device.makeDefaultLibrary(bundle: bundle) else {
return nil
}
metalLibraryValue = library
return library
}
final class DiamondLayer: MetalEngineSubjectLayer, MetalEngineSubject {
var internalData: MetalEngineSubjectInternalData?
private final class RenderState: RenderToLayerState {
let pipelineState: MTLRenderPipelineState
required init?(device: MTLDevice) {
guard let library = metalLibrary(device: device) else {
return nil
}
guard let vertexFunction = library.makeFunction(name: "post_vertex_main"),
let fragmentFunction = library.makeFunction(name: "post_fragment_main") else {
return nil
}
let pipelineDescriptor = MTLRenderPipelineDescriptor()
pipelineDescriptor.vertexFunction = vertexFunction
pipelineDescriptor.fragmentFunction = fragmentFunction
pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
pipelineDescriptor.colorAttachments[0].isBlendingEnabled = true
pipelineDescriptor.colorAttachments[0].rgbBlendOperation = .add
pipelineDescriptor.colorAttachments[0].alphaBlendOperation = .add
pipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = .sourceAlpha
pipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor = .sourceAlpha
pipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = .oneMinusSourceAlpha
pipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = .oneMinusSourceAlpha
guard let pipelineState = try? device.makeRenderPipelineState(descriptor: pipelineDescriptor) else {
return nil
}
self.pipelineState = pipelineState
}
}
final class DiamondState: ComputeState {
let computePipelineState: MTLComputePipelineState
let cubemapTexture: MTLTexture?
required init?(device: MTLDevice) {
guard let library = metalLibrary(device: device) else {
return nil
}
guard let functionComputeMain = library.makeFunction(name: "compute_main") else {
return nil
}
guard let computePipelineState = try? device.makeComputePipelineState(function: functionComputeMain) else {
return nil
}
self.computePipelineState = computePipelineState
self.cubemapTexture = loadCubemap(device: device)
}
}
private var offscreenTexture: PooledTexture?
private var rotationX = AnimatableProperty<CGFloat>(value: -15.0 * .pi / 180.0)
private var rotationY = AnimatableProperty<CGFloat>(value: 0.0)
private var rotationZ = AnimatableProperty<CGFloat>(value: 0.0 * .pi / 180.0)
private var time = AnimatableProperty<CGFloat>(value: 0.0)
private var startTime = CFAbsoluteTimeGetCurrent()
private var interactionStartTme: Double?
private var displayLinkSubscription: SharedDisplayLinkDriver.Link?
private var hasActiveAnimations: Bool = false
private var isExploding = false
private var currentRenderSize: CGSize = .zero
override init() {
super.init()
self.isOpaque = false
self.didEnterHierarchy = { [weak self] in
guard let self else {
return
}
self.displayLinkSubscription = SharedDisplayLinkDriver.shared.add { [weak self] _ in
guard let self else {
return
}
self.updateAnimations()
self.setNeedsUpdate()
}
}
self.didExitHierarchy = { [weak self] in
guard let self else {
return
}
self.displayLinkSubscription = nil
}
}
override init(layer: Any) {
super.init(layer: layer)
if let layer = layer as? DiamondLayer {
self.rotationX = layer.rotationX
self.rotationY = layer.rotationY
self.rotationZ = layer.rotationZ
self.time = layer.time
self.startTime = layer.startTime
self.currentRenderSize = layer.currentRenderSize
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc func handlePan(_ gesture: UIPanGestureRecognizer) {
switch gesture.state {
case .began:
self.interactionStartTme = CFAbsoluteTimeGetCurrent()
case .changed:
let translation = gesture.translation(in: gesture.view)
let yawPan = -Float(translation.x) * Float.pi / 180.0
func rubberBandingOffset(offset: CGFloat, bandingStart: CGFloat) -> CGFloat {
let bandedOffset = offset - bandingStart
let range: CGFloat = 75.0
let coefficient: CGFloat = 0.4
return bandingStart + (1.0 - (1.0 / ((bandedOffset * coefficient / range) + 1.0))) * range
}
var pitchTranslation = rubberBandingOffset(offset: abs(translation.y), bandingStart: 0.0)
if translation.y < 0.0 {
pitchTranslation *= -1.0
}
let pitchPan = Float(pitchTranslation) * Float.pi / 180.0
self.rotationX.update(value: CGFloat(yawPan), transition: .immediate)
self.rotationY.update(value: CGFloat(pitchPan), transition: .immediate)
case .ended:
let velocity = gesture.velocity(in: gesture.view)
if let interactionStartTme = self.interactionStartTme {
let delta = CFAbsoluteTimeGetCurrent() - interactionStartTme
self.startTime += delta
self.interactionStartTme = nil
}
//
// var smallAngle = false
// let previousYaw = Float(self.rotationX.presentationValue)
// if (previousYaw < .pi / 2 && previousYaw > -.pi / 2) && abs(velocity.x) < 200 {
// smallAngle = true
// }
playAppearanceAnimation(velocity: velocity.x, smallAngle: true, explode: false) //, smallAngle: smallAngle, explode: !smallAngle && abs(velocity.x) > 600)
default:
break
}
self.setNeedsUpdate()
}
func playAppearanceAnimation(velocity: CGFloat?, smallAngle: Bool, explode: Bool) {
if explode {
self.isExploding = true
self.time.update(value: 8.0, transition: .spring(duration: 2.0))
Queue.mainQueue().after(1.2) {
if self.isExploding {
self.isExploding = false
self.startTime = CFAbsoluteTimeGetCurrent() - 8.0
}
}
} else if smallAngle {
let transition = ComponentTransition.easeInOut(duration: 0.3)
self.rotationX.update(value: 0.0, transition: transition)
self.rotationY.update(value: 0.0, transition: transition)
}
}
private func updateAnimations() {
let properties = [
self.rotationX,
self.rotationY,
self.rotationZ
]
let timestamp = CACurrentMediaTime()
var hasAnimations = false
for property in properties {
if property.tick(timestamp: timestamp) {
hasAnimations = true
}
}
let currentTime = CFAbsoluteTimeGetCurrent()
if self.time.tick(timestamp: timestamp) {
hasAnimations = true
}
self.hasActiveAnimations = hasAnimations
if !self.isExploding && self.interactionStartTme == nil {
let elapsedTime = currentTime - self.startTime
self.time.update(value: CGFloat(elapsedTime), transition: .immediate)
}
}
func update(context: MetalEngineSubjectContext) {
if self.bounds.isEmpty {
return
}
let drawableSize = CGSize(width: self.bounds.width * UIScreen.main.scale, height: self.bounds.height * UIScreen.main.scale)
let offscreenTextureSpec = TextureSpec(width: Int(drawableSize.width), height: Int(drawableSize.height), pixelFormat: .rgba8UnsignedNormalized)
if self.offscreenTexture == nil || self.offscreenTexture?.spec != offscreenTextureSpec {
self.offscreenTexture = MetalEngine.shared.pooledTexture(spec: offscreenTextureSpec)
}
guard let offscreenTexture = self.offscreenTexture?.get(context: context) else {
return
}
let diamondTexture = context.compute(state: DiamondState.self, inputs: offscreenTexture.placeholer, commands: { commandBuffer, computeState, offscreenTexture -> MTLTexture? in
guard let offscreenTexture, let cubemapTexture = computeState.cubemapTexture else {
return nil
}
do {
guard let computeEncoder = commandBuffer.makeComputeCommandEncoder() else {
return nil
}
let threadgroupSize = MTLSize(width: 16, height: 16, depth: 1)
let threadgroupCount = MTLSize(width: (offscreenTextureSpec.width + threadgroupSize.width - 1) / threadgroupSize.width, height: (offscreenTextureSpec.height + threadgroupSize.height - 1) / threadgroupSize.height, depth: 1)
var iTime = Float(self.time.presentationValue)
var iResolution = simd_float2(
Float(drawableSize.width),
Float(drawableSize.height)
)
var cameraRotation = SIMD3<Float>(
Float(180.0 * .pi / 180.0 + self.rotationX.presentationValue),
Float(18.0 * .pi / 180.0 + self.rotationY.presentationValue),
Float(0.0)
)
computeEncoder.setComputePipelineState(computeState.computePipelineState)
computeEncoder.setBytes(&iTime, length: MemoryLayout<Float>.size, index: 0)
computeEncoder.setBytes(&iResolution, length: MemoryLayout<simd_float2>.size, index: 1)
computeEncoder.setBytes(&cameraRotation, length: MemoryLayout<simd_float3>.size, index: 2)
computeEncoder.setTexture(offscreenTexture, index: 0)
computeEncoder.setTexture(cubemapTexture, index: 1)
computeEncoder.dispatchThreadgroups(threadgroupCount, threadsPerThreadgroup: threadgroupSize)
computeEncoder.endEncoding()
}
return offscreenTexture
})
context.renderToLayer(spec: RenderLayerSpec(size: RenderSize(width: Int(drawableSize.width), height: Int(drawableSize.height))), state: RenderState.self, layer: self, inputs: diamondTexture, commands: { encoder, placement, diamondTexture in
guard let diamondTexture else {
return
}
let effectiveRect = placement.effectiveRect
var iTime = Float(self.time.presentationValue)
var rect = SIMD4<Float>(Float(effectiveRect.minX), Float(effectiveRect.minY), Float(effectiveRect.width), Float(effectiveRect.height))
encoder.setVertexBytes(&rect, length: 4 * 4, index: 0)
var iResolution = simd_float2(
Float(drawableSize.width),
Float(drawableSize.height)
)
encoder.setFragmentBytes(&iTime, length: MemoryLayout<Float>.size, index: 0)
encoder.setFragmentBytes(&iResolution, length: MemoryLayout<simd_float2>.size, index: 1)
encoder.setFragmentTexture(diamondTexture, index: 0)
encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6)
})
}
}
private func loadCubemap(device: MTLDevice) -> MTLTexture? {
let faceNames = ["right", "left", "top", "bottom", "front", "back"].map { "\($0).png" }
guard let firstImage = UIImage(named: faceNames[0]) else {
return nil
}
let width = Int(firstImage.size.width)
let height = Int(firstImage.size.height)
let textureDescriptor = MTLTextureDescriptor.textureCubeDescriptor(
pixelFormat: .rgba8Unorm,
size: width,
mipmapped: true
)
textureDescriptor.usage = [.shaderRead]
guard let cubemapTexture = device.makeTexture(descriptor: textureDescriptor) else {
return nil
}
for (index, faceName) in faceNames.enumerated() {
guard let image = UIImage(named: faceName),
let cgImage = image.cgImage else {
return nil
}
let colorSpace = CGColorSpaceCreateDeviceRGB()
let bytesPerPixel = 4
let bytesPerRow = width * bytesPerPixel
let bitsPerComponent = 8
var pixelData = [UInt8](repeating: 0, count: width * height * bytesPerPixel)
guard let context = CGContext(
data: &pixelData,
width: width,
height: height,
bitsPerComponent: bitsPerComponent,
bytesPerRow: bytesPerRow,
space: colorSpace,
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
) else {
return nil
}
context.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height))
let region = MTLRegion(origin: MTLOrigin(x: 0, y: 0, z: 0), size: MTLSize(width: width, height: height, depth: 1))
cubemapTexture.replace(
region: region,
mipmapLevel: 0,
slice: index,
withBytes: pixelData,
bytesPerRow: bytesPerRow,
bytesPerImage: 0
)
}
if textureDescriptor.mipmapLevelCount > 1 {
let commandQueue = device.makeCommandQueue()
let commandBuffer = commandQueue?.makeCommandBuffer()
let blitEncoder = commandBuffer?.makeBlitCommandEncoder()
blitEncoder?.generateMipmaps(for: cubemapTexture)
blitEncoder?.endEncoding()
commandBuffer?.commit()
commandBuffer?.waitUntilCompleted()
}
return cubemapTexture
}
@@ -0,0 +1,273 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import SwiftSignalKit
import SceneKit
import GZip
import AppBundle
import LegacyComponents
import PremiumStarComponent
import TelegramPresentationData
private let sceneVersion: Int = 5
private func deg2rad(_ number: Float) -> Float {
return number * .pi / 180
}
private func rad2deg(_ number: Float) -> Float {
return number * 180.0 / .pi
}
public final class PremiumDiamondComponent: Component {
let theme: PresentationTheme
public init(theme: PresentationTheme) {
self.theme = theme
}
public static func ==(lhs: PremiumDiamondComponent, rhs: PremiumDiamondComponent) -> Bool {
return lhs.theme === rhs.theme
}
public final class View: UIView, SCNSceneRendererDelegate, ComponentTaggedView {
public final class Tag {
public init() {
}
}
public func matches(tag: Any) -> Bool {
if let _ = tag as? Tag {
return true
}
return false
}
private var _ready = Promise<Bool>()
public var ready: Signal<Bool, NoError> {
return self._ready.get()
}
private let sceneView: SCNView
private let diamondLayer: DiamondLayer
private var timer: SwiftSignalKit.Timer?
private var component: PremiumDiamondComponent?
override init(frame: CGRect) {
self.sceneView = SCNView(frame: CGRect(origin: .zero, size: CGSize(width: 64.0, height: 64.0)))
self.sceneView.backgroundColor = .clear
self.sceneView.transform = CGAffineTransform(scaleX: 0.5, y: 0.5)
self.sceneView.isUserInteractionEnabled = false
self.sceneView.preferredFramesPerSecond = 60
self.sceneView.isJitteringEnabled = true
self.diamondLayer = DiamondLayer()
super.init(frame: frame)
self.addSubview(self.sceneView)
self.layer.addSublayer(self.diamondLayer)
self.setup()
let panGestureRecoginzer = UIPanGestureRecognizer(target: self.diamondLayer, action: #selector(self.diamondLayer.handlePan(_:)))
self.addGestureRecognizer(panGestureRecoginzer)
self.disablesInteractiveModalDismiss = true
self.disablesInteractiveTransitionGestureRecognizer = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.timer?.invalidate()
}
private func setup() {
guard let scene = loadCompressedScene(name: "gift2", version: sceneVersion) else {
return
}
self.sceneView.scene = scene
self.sceneView.delegate = self
let names: [String] = [
"particles_left",
"particles_right",
"particles_left_bottom",
"particles_right_bottom",
"particles_center"
]
let particleColor = UIColor(rgb: 0x428df4) //0x3b9bff)
for name in names {
if let node = scene.rootNode.childNode(withName: name, recursively: false), let particleSystem = node.particleSystems?.first {
particleSystem.particleIntensity = min(1.0, 2.0 * particleSystem.particleIntensity)
particleSystem.particleIntensityVariation = 0.05
particleSystem.particleColor = particleColor
particleSystem.particleColorVariation = SCNVector4Make(0.0, 0.0, 0.1, 0.0)
if let propertyControllers = particleSystem.propertyControllers, let sizeController = propertyControllers[.size], let colorController = propertyControllers[.color] {
let animation = CAKeyframeAnimation()
if let existing = colorController.animation as? CAKeyframeAnimation {
animation.keyTimes = existing.keyTimes
animation.values = existing.values?.compactMap { ($0 as? UIColor)?.alpha } ?? []
} else {
animation.values = [ 0.0, 1.0, 1.0, 0.0 ]
}
let opacityController = SCNParticlePropertyController(animation: animation)
particleSystem.propertyControllers = [
.size: sizeController,
.opacity: opacityController
]
}
}
}
//let _ = self.sceneView.snapshot()
}
private var didSetReady = false
public func renderer(_ renderer: SCNSceneRenderer, didRenderScene scene: SCNScene, atTime time: TimeInterval) {
if !self.didSetReady {
self.didSetReady = true
Queue.mainQueue().justDispatch {
self._ready.set(.single(true))
self.onReady()
}
}
}
private func onReady() {
self.setupScaleAnimation()
self.playAppearanceAnimation(mirror: true, explode: true)
}
private func setupScaleAnimation() {
let animation = CABasicAnimation(keyPath: "transform.scale")
animation.duration = 2.0
animation.fromValue = 0.9
animation.toValue = 1.0
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
animation.autoreverses = true
animation.repeatCount = .infinity
self.diamondLayer.add(animation, forKey: "scale")
}
private func playAppearanceAnimation(velocity: CGFloat? = nil, smallAngle: Bool = false, mirror: Bool = false, explode: Bool = false) {
guard let scene = self.sceneView.scene else {
return
}
if explode, let node = scene.rootNode.childNode(withName: "swirl", recursively: false), let particlesLeft = scene.rootNode.childNode(withName: "particles_left", recursively: false), let particlesRight = scene.rootNode.childNode(withName: "particles_right", recursively: false), let particlesBottomLeft = scene.rootNode.childNode(withName: "particles_left_bottom", recursively: false), let particlesBottomRight = scene.rootNode.childNode(withName: "particles_right_bottom", recursively: false) {
if let leftParticleSystem = particlesLeft.particleSystems?.first, let rightParticleSystem = particlesRight.particleSystems?.first, let leftBottomParticleSystem = particlesBottomLeft.particleSystems?.first, let rightBottomParticleSystem = particlesBottomRight.particleSystems?.first {
leftParticleSystem.speedFactor = 2.0
leftParticleSystem.particleVelocity = 1.6
leftParticleSystem.birthRate = 60.0
leftParticleSystem.particleLifeSpan = 4.0
rightParticleSystem.speedFactor = 2.0
rightParticleSystem.particleVelocity = 1.6
rightParticleSystem.birthRate = 60.0
rightParticleSystem.particleLifeSpan = 4.0
leftBottomParticleSystem.particleVelocity = 1.6
leftBottomParticleSystem.birthRate = 24.0
leftBottomParticleSystem.particleLifeSpan = 7.0
rightBottomParticleSystem.particleVelocity = 1.6
rightBottomParticleSystem.birthRate = 24.0
rightBottomParticleSystem.particleLifeSpan = 7.0
node.physicsField?.isActive = true
Queue.mainQueue().after(1.0) {
node.physicsField?.isActive = false
leftParticleSystem.birthRate = 15.0
leftParticleSystem.particleVelocity = 1.0
leftParticleSystem.particleLifeSpan = 3.0
rightParticleSystem.birthRate = 15.0
rightParticleSystem.particleVelocity = 1.0
rightParticleSystem.particleLifeSpan = 3.0
leftBottomParticleSystem.particleVelocity = 1.0
leftBottomParticleSystem.birthRate = 10.0
leftBottomParticleSystem.particleLifeSpan = 5.0
rightBottomParticleSystem.particleVelocity = 1.0
rightBottomParticleSystem.birthRate = 10.0
rightBottomParticleSystem.particleLifeSpan = 5.0
let leftAnimation = POPBasicAnimation()
leftAnimation.property = (POPAnimatableProperty.property(withName: "speedFactor", initializer: { property in
property?.readBlock = { particleSystem, values in
values?.pointee = (particleSystem as! SCNParticleSystem).speedFactor
}
property?.writeBlock = { particleSystem, values in
(particleSystem as! SCNParticleSystem).speedFactor = values!.pointee
}
property?.threshold = 0.01
}) as! POPAnimatableProperty)
leftAnimation.fromValue = 1.2 as NSNumber
leftAnimation.toValue = 0.85 as NSNumber
leftAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
leftAnimation.duration = 0.5
leftParticleSystem.pop_add(leftAnimation, forKey: "speedFactor")
let rightAnimation = POPBasicAnimation()
rightAnimation.property = (POPAnimatableProperty.property(withName: "speedFactor", initializer: { property in
property?.readBlock = { particleSystem, values in
values?.pointee = (particleSystem as! SCNParticleSystem).speedFactor
}
property?.writeBlock = { particleSystem, values in
(particleSystem as! SCNParticleSystem).speedFactor = values!.pointee
}
property?.threshold = 0.01
}) as! POPAnimatableProperty)
rightAnimation.fromValue = 1.2 as NSNumber
rightAnimation.toValue = 0.85 as NSNumber
rightAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
rightAnimation.duration = 0.5
rightParticleSystem.pop_add(rightAnimation, forKey: "speedFactor")
}
}
self.diamondLayer.playAppearanceAnimation(velocity:nil, smallAngle: false, explode: true)
}
}
func update(component: PremiumDiamondComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
self.component = component
self.sceneView.backgroundColor = component.theme.list.blocksBackgroundColor
self.sceneView.bounds = CGRect(origin: .zero, size: CGSize(width: availableSize.width * 2.0, height: availableSize.height * 2.0))
self.sceneView.center = CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0)
self.diamondLayer.bounds = CGRect(origin: .zero, size: CGSize(width: availableSize.height, height: availableSize.height))
self.diamondLayer.position = CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0 - 8.0)
return availableSize
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}