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,27 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "PremiumCoinComponent",
module_name = "PremiumCoinComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/ComponentFlow",
"//submodules/AccountContext",
"//submodules/AppBundle",
"//submodules/GZip",
"//submodules/LegacyComponents",
"//submodules/Components/MultilineTextComponent:MultilineTextComponent",
"//submodules/TelegramUI/Components/Premium/PremiumStarComponent",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,548 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import SwiftSignalKit
import SceneKit
import GZip
import AppBundle
import LegacyComponents
import PremiumStarComponent
private let sceneVersion: Int = 4
private func deg2rad(_ number: Float) -> Float {
return number * .pi / 180
}
private func rad2deg(_ number: Float) -> Float {
return number * 180.0 / .pi
}
public final class PremiumCoinComponent: Component {
public enum Mode {
case business
case affiliate
}
public let mode: Mode
public let isIntro: Bool
public let isVisible: Bool
public let hasIdleAnimations: Bool
public init(
mode: Mode,
isIntro: Bool,
isVisible: Bool,
hasIdleAnimations: Bool
) {
self.mode = mode
self.isIntro = isIntro
self.isVisible = isVisible
self.hasIdleAnimations = hasIdleAnimations
}
public static func ==(lhs: PremiumCoinComponent, rhs: PremiumCoinComponent) -> Bool {
return lhs.mode == rhs.mode && lhs.isIntro == rhs.isIntro && lhs.isVisible == rhs.isVisible && lhs.hasIdleAnimations == rhs.hasIdleAnimations
}
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()
}
weak var animateFrom: UIView?
weak var containerView: UIView?
private let sceneView: SCNView
private var previousInteractionTimestamp: Double = 0.0
private var timer: SwiftSignalKit.Timer?
private var hasIdleAnimations = false
private let isIntro: Bool
init(frame: CGRect, isIntro: Bool) {
self.isIntro = isIntro
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
super.init(frame: frame)
self.addSubview(self.sceneView)
self.setup()
let panGestureRecoginzer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:)))
self.addGestureRecognizer(panGestureRecoginzer)
let tapGestureRecoginzer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:)))
self.addGestureRecognizer(tapGestureRecoginzer)
self.disablesInteractiveModalDismiss = true
self.disablesInteractiveTransitionGestureRecognizer = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.timer?.invalidate()
}
private let hapticFeedback = HapticFeedback()
private var delayTapsTill: Double?
@objc private func handleTap(_ gesture: UITapGestureRecognizer) {
guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else {
return
}
let currentTime = CACurrentMediaTime()
self.previousInteractionTimestamp = currentTime
if let delayTapsTill = self.delayTapsTill, currentTime < delayTapsTill {
return
}
var left: Bool?
var top: Bool?
if let view = gesture.view {
let point = gesture.location(in: view)
let horizontalDistanceFromCenter = abs(point.x - view.frame.size.width / 2.0)
if horizontalDistanceFromCenter > 60.0 {
return
}
let verticalDistanceFromCenter = abs(point.y - view.frame.size.height / 2.0)
if horizontalDistanceFromCenter > 20.0 {
left = point.x < view.frame.width / 2.0
}
if verticalDistanceFromCenter > 20.0 {
top = point.y < view.frame.height / 2.0
}
}
if node.animationKeys.contains("tapRotate"), let left = left {
self.playAppearanceAnimation(velocity: nil, mirror: left, explode: true)
self.hapticFeedback.impact(.medium)
return
}
let initial = node.eulerAngles
var yaw: CGFloat = 0.0
var pitch: CGFloat = 0.0
if let left = left {
yaw = left ? -0.6 : 0.6
}
if let top = top {
pitch = top ? -0.3 : 0.3
}
let target = SCNVector3(pitch, yaw, 0.0)
let animation = CABasicAnimation(keyPath: "eulerAngles")
animation.fromValue = NSValue(scnVector3: initial)
animation.toValue = NSValue(scnVector3: target)
animation.duration = 0.25
animation.timingFunction = CAMediaTimingFunction(name: .easeOut)
animation.fillMode = .forwards
node.addAnimation(animation, forKey: "tapRotate")
node.eulerAngles = target
Queue.mainQueue().after(0.25) {
node.eulerAngles = initial
let springAnimation = CASpringAnimation(keyPath: "eulerAngles")
springAnimation.fromValue = NSValue(scnVector3: target)
springAnimation.toValue = NSValue(scnVector3: SCNVector3(x: 0.0, y: 0.0, z: 0.0))
springAnimation.mass = 1.0
springAnimation.stiffness = 21.0
springAnimation.damping = 5.8
springAnimation.duration = springAnimation.settlingDuration * 0.8
node.addAnimation(springAnimation, forKey: "tapRotate")
}
self.hapticFeedback.tap()
}
private var previousYaw: Float = 0.0
@objc private func handlePan(_ gesture: UIPanGestureRecognizer) {
guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else {
return
}
self.previousInteractionTimestamp = CACurrentMediaTime()
let keys = [
"rotate",
"tapRotate"
]
for key in keys {
node.removeAnimation(forKey: key)
}
switch gesture.state {
case .began:
self.previousYaw = 0.0
case .changed:
let translation = gesture.translation(in: gesture.view)
let yawPan = deg2rad(Float(translation.x))
func rubberBandingOffset(offset: CGFloat, bandingStart: CGFloat) -> CGFloat {
let bandedOffset = offset - bandingStart
let range: CGFloat = 60.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 = deg2rad(Float(pitchTranslation))
self.previousYaw = yawPan
node.eulerAngles = SCNVector3(pitchPan, yawPan, 0.0)
case .ended:
let velocity = gesture.velocity(in: gesture.view)
var smallAngle = false
if (self.previousYaw < .pi / 2 && self.previousYaw > -.pi / 2) && abs(velocity.x) < 200 {
smallAngle = true
}
self.playAppearanceAnimation(velocity: velocity.x, smallAngle: smallAngle, explode: !smallAngle && abs(velocity.x) > 600)
node.eulerAngles = SCNVector3(0.0, 0.0, 0.0)
default:
break
}
}
private func setup() {
guard let scene = loadCompressedScene(name: "coin", version: sceneVersion) else {
return
}
self.sceneView.scene = scene
self.sceneView.delegate = self
let _ = self.sceneView.snapshot()
// self.didSetReady = true
// self._ready.set(.single(true))
// self.onReady()
}
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 maybeAnimateIn() {
guard let scene = self.sceneView.scene, let _ = scene.rootNode.childNode(withName: "star", recursively: false), let animateFrom = self.animateFrom, var containerView = self.containerView else {
return
}
containerView = containerView.subviews[2].subviews[1]
let initialPosition = self.sceneView.center
let targetPosition = self.sceneView.superview!.convert(self.sceneView.center, to: containerView)
let sourcePosition = animateFrom.superview!.convert(animateFrom.center, to: containerView).offsetBy(dx: 0.0, dy: -20.0)
containerView.addSubview(self.sceneView)
self.sceneView.center = targetPosition
animateFrom.alpha = 0.0
self.sceneView.layer.animateScale(from: 0.05, to: 0.5, duration: 1.0, timingFunction: kCAMediaTimingFunctionSpring)
self.sceneView.layer.animatePosition(from: sourcePosition, to: targetPosition, duration: 1.0, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in
self.addSubview(self.sceneView)
self.sceneView.center = initialPosition
})
Queue.mainQueue().after(0.4, {
animateFrom.alpha = 1.0
})
self.animateFrom = nil
self.containerView = nil
}
private func onReady() {
self.setupScaleAnimation()
self.setupGradientAnimation()
self.setupShineAnimation()
self.maybeAnimateIn()
self.playAppearanceAnimation(explode: true)
self.previousInteractionTimestamp = CACurrentMediaTime()
self.timer = SwiftSignalKit.Timer(timeout: 1.0, repeat: true, completion: { [weak self] in
if let strongSelf = self, strongSelf.hasIdleAnimations {
let currentTimestamp = CACurrentMediaTime()
if currentTimestamp > strongSelf.previousInteractionTimestamp + 5.0 {
strongSelf.playAppearanceAnimation()
}
}
}, queue: Queue.mainQueue())
self.timer?.start()
}
private func setupScaleAnimation() {
guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else {
return
}
let fromScale: Float = 0.9
let toScale: Float = 1.0
let animation = CABasicAnimation(keyPath: "scale")
animation.duration = 2.0
animation.fromValue = NSValue(scnVector3: SCNVector3(x: fromScale, y: fromScale, z: fromScale))
animation.toValue = NSValue(scnVector3: SCNVector3(x: toScale, y: toScale, z: toScale))
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
animation.autoreverses = true
animation.repeatCount = .infinity
node.addAnimation(animation, forKey: "scale")
}
private func setupGradientAnimation() {
guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else {
return
}
for node in node.childNodes {
guard let initial = node.geometry?.materials.first?.diffuse.contentsTransform else {
return
}
let animation = CABasicAnimation(keyPath: "contentsTransform")
animation.duration = 4.5
animation.fromValue = NSValue(scnMatrix4: initial)
animation.toValue = NSValue(scnMatrix4: SCNMatrix4Translate(initial, -0.35, 0.35, 0))
animation.timingFunction = CAMediaTimingFunction(name: .linear)
animation.autoreverses = true
animation.repeatCount = .infinity
node.geometry?.materials.first?.diffuse.addAnimation(animation, forKey: "gradient")
}
}
private func setupShineAnimation() {
guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else {
return
}
for node in node.childNodes {
guard let initial = node.geometry?.materials.first?.emission.contentsTransform else {
return
}
if let material = node.geometry?.materials.first {
if ["Business", "Affiliate", "Plane"].contains(node.name) {
material.metalness.intensity = 0.1
} else {
material.metalness.intensity = 0.3
}
}
let animation = CABasicAnimation(keyPath: "contentsTransform")
animation.fillMode = .forwards
animation.fromValue = NSValue(scnMatrix4: initial)
animation.toValue = NSValue(scnMatrix4: SCNMatrix4Translate(initial, -1.6, 0.0, 0.0))
animation.timingFunction = CAMediaTimingFunction(name: .easeOut)
animation.beginTime = 1.1
animation.duration = 0.9
let group = CAAnimationGroup()
group.animations = [animation]
group.beginTime = 1.0
group.duration = 4.0
group.repeatCount = .infinity
node.geometry?.materials.first?.emission.addAnimation(group, forKey: "shimmer")
}
}
private func playAppearanceAnimation(velocity: CGFloat? = nil, smallAngle: Bool = false, mirror: Bool = false, explode: Bool = false) {
guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else {
return
}
let currentTime = CACurrentMediaTime()
self.previousInteractionTimestamp = currentTime
self.delayTapsTill = currentTime + 0.85
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")
}
}
}
var from = node.presentation.eulerAngles
if abs(from.y - .pi * 2.0) < 0.001 {
from.y = 0.0
}
node.removeAnimation(forKey: "tapRotate")
var toValue: Float = smallAngle ? 0.0 : .pi * 2.0
if let velocity = velocity, !smallAngle && abs(velocity) > 200 && velocity < 0.0 {
toValue *= -1
}
if mirror {
toValue *= -1
}
let to = SCNVector3(x: 0.0, y: toValue, z: 0.0)
let distance = rad2deg(to.y - from.y)
guard !distance.isZero else {
return
}
let springAnimation = CASpringAnimation(keyPath: "eulerAngles")
springAnimation.fromValue = NSValue(scnVector3: from)
springAnimation.toValue = NSValue(scnVector3: to)
springAnimation.mass = 1.0
springAnimation.stiffness = 21.0
springAnimation.damping = 5.8
springAnimation.duration = springAnimation.settlingDuration * 0.75
springAnimation.initialVelocity = velocity.flatMap { abs($0 / CGFloat(distance)) } ?? 1.7
springAnimation.completion = { [weak node] finished in
if finished {
node?.eulerAngles = SCNVector3(x: 0.0, y: 0.0, z: 0.0)
}
}
node.addAnimation(springAnimation, forKey: "rotate")
}
func update(component: PremiumCoinComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
self.sceneView.bounds = CGRect(origin: .zero, size: CGSize(width: availableSize.width * 2.0, height: availableSize.height * 2.0))
if self.sceneView.superview == self {
self.sceneView.center = CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0)
}
let visibleLogo: String
switch component.mode {
case .business:
visibleLogo = "Business"
case .affiliate:
visibleLogo = "Affiliate"
}
if let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) {
for node in node.childNodes {
if ["Business", "Affiliate"].contains(node.name) {
node.isHidden = node.name != visibleLogo
}
}
}
self.hasIdleAnimations = component.hasIdleAnimations
return availableSize
}
}
public func makeView() -> View {
return View(frame: CGRect(), isIntro: self.isIntro)
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}
@@ -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)
}
}
@@ -0,0 +1,29 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "PremiumStarComponent",
module_name = "PremiumStarComponent",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/ComponentFlow",
"//submodules/AccountContext",
"//submodules/AppBundle",
"//submodules/GZip",
"//submodules/LegacyComponents",
"//submodules/AvatarNode",
"//submodules/PhotoResources",
"//submodules/TelegramUI/Components/Chat/MergedAvatarsNode",
"//submodules/Components/MultilineTextComponent:MultilineTextComponent",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,464 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import SwiftSignalKit
import SceneKit
import GZip
import AppBundle
import LegacyComponents
import AvatarNode
import AccountContext
import TelegramCore
import MergedAvatarsNode
import MultilineTextComponent
import TelegramPresentationData
import PhotoResources
private let sceneVersion: Int = 1
public final class GiftAvatarComponent: Component {
let context: AccountContext
let theme: PresentationTheme
let peers: [EnginePeer]
let photo: TelegramMediaWebFile?
let isVisible: Bool
let hasIdleAnimations: Bool
let hasScaleAnimation: Bool
let avatarSize: CGFloat
let color: UIColor?
let offset: CGFloat?
let hasLargeParticles: Bool
let action: (() -> Void)?
public init(
context: AccountContext,
theme: PresentationTheme,
peers: [EnginePeer],
photo: TelegramMediaWebFile? = nil,
isVisible: Bool,
hasIdleAnimations: Bool,
hasScaleAnimation: Bool = true,
avatarSize: CGFloat = 100.0,
color: UIColor? = nil,
offset: CGFloat? = nil,
hasLargeParticles: Bool = false,
action: (() -> Void)? = nil
) {
self.context = context
self.theme = theme
self.peers = peers
self.photo = photo
self.isVisible = isVisible
self.hasIdleAnimations = hasIdleAnimations
self.hasScaleAnimation = hasScaleAnimation
self.avatarSize = avatarSize
self.color = color
self.offset = offset
self.hasLargeParticles = hasLargeParticles
self.action = action
}
public static func ==(lhs: GiftAvatarComponent, rhs: GiftAvatarComponent) -> Bool {
return lhs.peers == rhs.peers && lhs.photo == rhs.photo && lhs.theme === rhs.theme && lhs.isVisible == rhs.isVisible && lhs.hasIdleAnimations == rhs.hasIdleAnimations && lhs.hasScaleAnimation == rhs.hasScaleAnimation && lhs.avatarSize == rhs.avatarSize && lhs.offset == rhs.offset && lhs.hasLargeParticles == rhs.hasLargeParticles
}
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 component: GiftAvatarComponent?
private var _ready = Promise<Bool>()
public var ready: Signal<Bool, NoError> {
return self._ready.get()
}
weak var animateFrom: UIView?
weak var containerView: UIView?
var animationColor: UIColor?
private let sceneView: SCNView
private let avatarNode: ImageNode
private var mergedAvatarsNode: MergedAvatarsNode?
private var imageNode: TransformImageNode?
private var iconBackgroundView: UIImageView?
private var iconView: UIImageView?
private let badgeBackground = ComponentView<Empty>()
private let badge = ComponentView<Empty>()
private var previousInteractionTimestamp: Double = 0.0
private var timer: SwiftSignalKit.Timer?
private var hasIdleAnimations = false
private let fetchDisposable = MetaDisposable()
public 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.avatarNode = ImageNode()
self.avatarNode.displaysAsynchronously = false
super.init(frame: frame)
self.addSubview(self.sceneView)
self.addSubview(self.avatarNode.view)
let tapGestureRecoginzer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:)))
self.addGestureRecognizer(tapGestureRecoginzer)
self.disablesInteractiveModalDismiss = true
self.disablesInteractiveTransitionGestureRecognizer = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.timer?.invalidate()
self.fetchDisposable.dispose()
}
private let hapticFeedback = HapticFeedback()
private var delayTapsTill: Double?
@objc private func handleTap(_ gesture: UITapGestureRecognizer) {
if let action = self.component?.action {
action()
} else {
self.playAppearanceAnimation(velocity: nil, mirror: false, explode: true)
}
}
private var didSetup = false
private func setup() {
guard let scene = loadCompressedScene(name: "gift2", version: sceneVersion), !self.didSetup else {
return
}
self.didSetup = true
self.sceneView.scene = scene
self.sceneView.delegate = self
if let color = self.component?.color {
let names: [String] = [
"particles_left",
"particles_right",
"particles_left_bottom",
"particles_right_bottom",
"particles_center"
]
let starNames: [String] = [
"coins_left",
"coins_right"
]
let particleColor = color
for name in starNames {
if let node = scene.rootNode.childNode(withName: name, recursively: false), let particleSystem = node.particleSystems?.first {
particleSystem.particleIntensity = 1.0
particleSystem.particleIntensityVariation = 0.05
particleSystem.particleColor = particleColor
particleSystem.particleColorVariation = SCNVector4Make(0.07, 0.0, 0.1, 0.0)
node.isHidden = false
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
]
}
}
}
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.1, 0.0, 0.12, 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
]
}
}
}
self.didSetReady = true
self._ready.set(.single(true))
self.onReady()
} else {
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.playAppearanceAnimation(explode: true)
self.previousInteractionTimestamp = CACurrentMediaTime()
self.timer = SwiftSignalKit.Timer(timeout: 1.0, repeat: true, completion: { [weak self] in
if let strongSelf = self, strongSelf.hasIdleAnimations {
let currentTimestamp = CACurrentMediaTime()
if currentTimestamp > strongSelf.previousInteractionTimestamp + 5.0 {
strongSelf.playAppearanceAnimation()
}
}
}, queue: Queue.mainQueue())
self.timer?.start()
}
private func playAppearanceAnimation(velocity: CGFloat? = nil, smallAngle: Bool = false, mirror: Bool = false, explode: Bool = false) {
guard let scene = self.sceneView.scene else {
return
}
let currentTime = CACurrentMediaTime()
self.previousInteractionTimestamp = currentTime
self.delayTapsTill = currentTime + 0.85
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.speedFactor = 2.0
leftBottomParticleSystem.particleVelocity = 1.6
leftBottomParticleSystem.birthRate = 24.0
leftBottomParticleSystem.particleLifeSpan = 7.0
// rightBottomParticleSystem.speedFactor = 2.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 = 12.0
leftParticleSystem.particleVelocity = 1.2
leftParticleSystem.particleLifeSpan = 3.0
rightParticleSystem.birthRate = 12.0
rightParticleSystem.particleVelocity = 1.2
rightParticleSystem.particleLifeSpan = 3.0
leftBottomParticleSystem.particleVelocity = 1.2
leftBottomParticleSystem.birthRate = 7.0
leftBottomParticleSystem.particleLifeSpan = 5.0
rightBottomParticleSystem.particleVelocity = 1.2
rightBottomParticleSystem.birthRate = 7.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")
}
}
}
}
func update(component: GiftAvatarComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
self.component = component
self.setup()
self.sceneView.bounds = CGRect(origin: .zero, size: CGSize(width: availableSize.width * 2.0, height: availableSize.height * 2.0))
if self.sceneView.superview == self {
self.sceneView.center = CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0 + (component.offset ?? 0.0))
}
self.hasIdleAnimations = component.hasIdleAnimations
if let _ = component.color {
self.sceneView.backgroundColor = component.theme.list.blocksBackgroundColor
}
if let photo = component.photo {
let imageNode: TransformImageNode
if let current = self.imageNode {
imageNode = current
} else {
imageNode = TransformImageNode()
imageNode.contentAnimations = [.firstUpdate, .subsequentUpdates]
self.addSubview(imageNode.view)
self.imageNode = imageNode
imageNode.setSignal(chatWebFileImage(account: component.context.account, file: photo))
self.fetchDisposable.set(chatMessageWebFileInteractiveFetched(account: component.context.account, userLocation: .other, image: photo).startStrict())
}
let imageSize = CGSize(width: component.avatarSize, height: component.avatarSize)
imageNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - imageSize.width) / 2.0), y: 113.0 - imageSize.height / 2.0), size: imageSize)
imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: imageSize.width / 2.0), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: component.theme.list.mediaPlaceholderColor))()
self.avatarNode.isHidden = true
} else if component.peers.count > 1 {
let avatarSize = CGSize(width: 60.0, height: 60.0)
let mergedAvatarsNode: MergedAvatarsNode
if let current = self.mergedAvatarsNode {
mergedAvatarsNode = current
} else {
mergedAvatarsNode = MergedAvatarsNode()
mergedAvatarsNode.isUserInteractionEnabled = false
self.addSubview(mergedAvatarsNode.view)
self.mergedAvatarsNode = mergedAvatarsNode
}
mergedAvatarsNode.update(context: component.context, peers: Array(component.peers.map { $0._asPeer() }.prefix(3)), synchronousLoad: false, imageSize: avatarSize.width, imageSpacing: 30.0, borderWidth: 2.0, avatarFontSize: 26.0)
let avatarsSize = CGSize(width: avatarSize.width + 30.0 * CGFloat(min(3, component.peers.count) - 1), height: avatarSize.height)
mergedAvatarsNode.updateLayout(size: avatarsSize)
mergedAvatarsNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - avatarsSize.width) / 2.0), y: 113.0 - avatarSize.height / 2.0), size: avatarsSize)
self.avatarNode.isHidden = true
} else {
self.mergedAvatarsNode?.view.removeFromSuperview()
self.mergedAvatarsNode = nil
self.avatarNode.isHidden = false
let avatarSize = CGSize(width: component.avatarSize, height: component.avatarSize)
if let peer = component.peers.first {
self.avatarNode.setSignal(peerAvatarCompleteImage(account: component.context.account, peer: peer, size: avatarSize, font: avatarPlaceholderFont(size: 43.0), fullSize: true))
}
self.avatarNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - avatarSize.width) / 2.0), y: 113.0 - avatarSize.height / 2.0), size: avatarSize)
}
if component.peers.count > 3 {
let badgeTextSize = self.badge.update(
transition: .immediate,
component: AnyComponent(
MultilineTextComponent(
text: .plain(NSAttributedString(string: "+\(component.peers.count - 3)", font: Font.with(size: 10.0, design: .round, weight: .semibold), textColor: component.theme.list.itemCheckColors.foregroundColor))
)
),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
let lineWidth = 1.0 + UIScreenPixel
let badgeSize = CGSize(width: max(17.0, badgeTextSize.width + 7.0) + lineWidth * 2.0, height: 17.0 + lineWidth * 2.0)
let _ = self.badgeBackground.update(
transition: .immediate,
component: AnyComponent(
RoundedRectangle(color: component.theme.list.itemCheckColors.fillColor, cornerRadius: badgeSize.height / 2.0, stroke: lineWidth, strokeColor: component.theme.list.blocksBackgroundColor)
),
environment: {},
containerSize: badgeSize
)
if let badgeTextView = self.badge.view, let badgeBackgroundView = self.badgeBackground.view {
if badgeBackgroundView.superview == nil {
self.addSubview(badgeBackgroundView)
self.addSubview(badgeTextView)
}
let avatarsSize = CGSize(width: 120.0, height: 60.0)
let backgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width + avatarsSize.width) / 2.0) - 19.0 - lineWidth, y: 113.0 + avatarsSize.height / 2.0 - 15.0 - lineWidth), size: badgeSize)
badgeBackgroundView.frame = backgroundFrame
badgeTextView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels(backgroundFrame.midX - badgeTextSize.width / 2.0), y: floorToScreenPixels(backgroundFrame.midY - badgeTextSize.height / 2.0) - UIScreenPixel), size: badgeTextSize)
}
} else {
self.badge.view?.removeFromSuperview()
self.badgeBackground.view?.removeFromSuperview()
}
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)
}
}
@@ -0,0 +1,929 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import SwiftSignalKit
import SceneKit
import GZip
import AppBundle
import LegacyComponents
import TelegramPresentationData
private let sceneVersion: Int = 7
private func deg2rad(_ number: Float) -> Float {
return number * .pi / 180
}
private func rad2deg(_ number: Float) -> Float {
return number * 180.0 / .pi
}
private func generateParticlesTexture() -> UIImage {
return UIImage()
}
private func generateFlecksTexture() -> UIImage {
return UIImage()
}
private func generateShineTexture() -> UIImage {
return UIImage()
}
private func generateDiffuseTexture(colors: [UIColor]) -> UIImage {
return generateImage(CGSize(width: 256, height: 256), rotatedContext: { size, context in
let colorsArray: [CGColor] = colors.map { $0.cgColor }
var locations: [CGFloat] = []
for i in 0 ..< colors.count {
let t = CGFloat(i) / CGFloat(colors.count - 1)
locations.append(t)
}
let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colorsArray as CFArray, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: size.height), end: CGPoint(x: size.width, y: 0.0), options: CGGradientDrawingOptions())
})!
}
public func loadCompressedScene(name: String, version: Int) -> SCNScene? {
let resourceUrl: URL
if let url = getAppBundle().url(forResource: name, withExtension: "scn") {
resourceUrl = url
} else {
let fileName = "\(name)_\(version).scn"
let tmpUrl = URL(fileURLWithPath: NSTemporaryDirectory() + fileName)
if !FileManager.default.fileExists(atPath: tmpUrl.path) {
guard let url = getAppBundle().url(forResource: name, withExtension: ""),
let compressedData = try? Data(contentsOf: url),
let decompressedData = TGGUnzipData(compressedData, 8 * 1024 * 1024) else {
return nil
}
try? decompressedData.write(to: tmpUrl)
}
resourceUrl = tmpUrl
}
guard let scene = try? SCNScene(url: resourceUrl, options: nil) else {
return nil
}
return scene
}
public final class PremiumStarComponent: Component {
let theme: PresentationTheme
let isIntro: Bool
let isVisible: Bool
let hasIdleAnimations: Bool
let colors: [UIColor]?
let particleColor: UIColor?
let backgroundColor: UIColor?
public init(
theme: PresentationTheme,
isIntro: Bool,
isVisible: Bool,
hasIdleAnimations: Bool,
colors: [UIColor]? = nil,
particleColor: UIColor? = nil,
backgroundColor: UIColor? = nil
) {
self.theme = theme
self.isIntro = isIntro
self.isVisible = isVisible
self.hasIdleAnimations = hasIdleAnimations
self.colors = colors
self.particleColor = particleColor
self.backgroundColor = backgroundColor
}
public static func ==(lhs: PremiumStarComponent, rhs: PremiumStarComponent) -> Bool {
return lhs.theme === rhs.theme && lhs.isIntro == rhs.isIntro && lhs.isVisible == rhs.isVisible && lhs.hasIdleAnimations == rhs.hasIdleAnimations && lhs.colors == rhs.colors && lhs.particleColor == rhs.particleColor && lhs.backgroundColor == rhs.backgroundColor
}
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 component: PremiumStarComponent?
private var _ready = Promise<Bool>()
public var ready: Signal<Bool, NoError> {
return self._ready.get()
}
public weak var animateFrom: UIView?
public weak var containerView: UIView?
public var animationColor: UIColor?
private let sceneView: SCNView
private var previousInteractionTimestamp: Double = 0.0
private var timer: SwiftSignalKit.Timer?
private var hasIdleAnimations = false
private let isIntro: Bool
init(frame: CGRect, isIntro: Bool) {
self.isIntro = isIntro
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
super.init(frame: frame)
self.addSubview(self.sceneView)
let panGestureRecoginzer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:)))
self.addGestureRecognizer(panGestureRecoginzer)
let tapGestureRecoginzer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:)))
self.addGestureRecognizer(tapGestureRecoginzer)
self.disablesInteractiveModalDismiss = true
self.disablesInteractiveTransitionGestureRecognizer = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.timer?.invalidate()
}
private let hapticFeedback = HapticFeedback()
private var delayTapsTill: Double?
@objc private func handleTap(_ gesture: UITapGestureRecognizer) {
guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else {
return
}
let currentTime = CACurrentMediaTime()
self.previousInteractionTimestamp = currentTime
if let delayTapsTill = self.delayTapsTill, currentTime < delayTapsTill {
return
}
var left: Bool?
var top: Bool?
if let view = gesture.view {
let point = gesture.location(in: view)
let horizontalDistanceFromCenter = abs(point.x - view.frame.size.width / 2.0)
if horizontalDistanceFromCenter > 60.0 {
return
}
let verticalDistanceFromCenter = abs(point.y - view.frame.size.height / 2.0)
if horizontalDistanceFromCenter > 20.0 {
left = point.x < view.frame.width / 2.0
}
if verticalDistanceFromCenter > 20.0 {
top = point.y < view.frame.height / 2.0
}
}
if node.animationKeys.contains("tapRotate"), let left = left {
self.playAppearanceAnimation(velocity: nil, mirror: left, explode: true)
self.hapticFeedback.impact(.medium)
return
}
let initial = node.eulerAngles
var yaw: CGFloat = 0.0
var pitch: CGFloat = 0.0
if let left = left {
yaw = left ? -0.6 : 0.6
}
if let top = top {
pitch = top ? -0.3 : 0.3
}
let target = SCNVector3(pitch, yaw, 0.0)
let animation = CABasicAnimation(keyPath: "eulerAngles")
animation.fromValue = NSValue(scnVector3: initial)
animation.toValue = NSValue(scnVector3: target)
animation.duration = 0.25
animation.timingFunction = CAMediaTimingFunction(name: .easeOut)
animation.fillMode = .forwards
node.addAnimation(animation, forKey: "tapRotate")
node.eulerAngles = target
Queue.mainQueue().after(0.25) {
node.eulerAngles = initial
let springAnimation = CASpringAnimation(keyPath: "eulerAngles")
springAnimation.fromValue = NSValue(scnVector3: target)
springAnimation.toValue = NSValue(scnVector3: SCNVector3(x: 0.0, y: 0.0, z: 0.0))
springAnimation.mass = 1.0
springAnimation.stiffness = 21.0
springAnimation.damping = 5.8
springAnimation.duration = springAnimation.settlingDuration * 0.8
node.addAnimation(springAnimation, forKey: "tapRotate")
}
self.hapticFeedback.tap()
}
private var previousYaw: Float = 0.0
@objc private func handlePan(_ gesture: UIPanGestureRecognizer) {
guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else {
return
}
self.previousInteractionTimestamp = CACurrentMediaTime()
let keys = [
"rotate",
"tapRotate"
]
for key in keys {
node.removeAnimation(forKey: key)
}
switch gesture.state {
case .began:
self.previousYaw = 0.0
case .changed:
let translation = gesture.translation(in: gesture.view)
let yawPan = deg2rad(Float(translation.x))
func rubberBandingOffset(offset: CGFloat, bandingStart: CGFloat) -> CGFloat {
let bandedOffset = offset - bandingStart
let range: CGFloat = 60.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 = deg2rad(Float(pitchTranslation))
self.previousYaw = yawPan
node.eulerAngles = SCNVector3(pitchPan, yawPan, 0.0)
case .ended:
let velocity = gesture.velocity(in: gesture.view)
var smallAngle = false
if (self.previousYaw < .pi / 2 && self.previousYaw > -.pi / 2) && abs(velocity.x) < 200 {
smallAngle = true
}
self.playAppearanceAnimation(velocity: velocity.x, smallAngle: smallAngle, explode: !smallAngle && abs(velocity.x) > 600)
node.eulerAngles = SCNVector3(0.0, 0.0, 0.0)
default:
break
}
}
private func updateColors(animated: Bool = false) {
guard let component = self.component, let colors = component.colors, let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else {
return
}
if animated {
UIView.animate(withDuration: 0.25, animations: {
node.geometry?.materials.first?.diffuse.contents = generateDiffuseTexture(colors: colors)
})
} else {
node.geometry?.materials.first?.diffuse.contents = generateDiffuseTexture(colors: colors)
}
let names: [String] = [
"particles_left",
"particles_right",
"particles_left_bottom",
"particles_right_bottom",
"particles_center"
]
let starNames: [String] = [
"coins_left",
"coins_right"
]
if let particleColor = component.particleColor {
for name in starNames {
if let node = scene.rootNode.childNode(withName: name, recursively: false), let particleSystem = node.particleSystems?.first {
if animated {
particleSystem.warmupDuration = 0.0
}
particleSystem.particleIntensity = 1.0
particleSystem.particleIntensityVariation = 0.05
particleSystem.particleColor = particleColor
particleSystem.particleColorVariation = SCNVector4Make(0.07, 0.0, 0.1, 0.0)
node.isHidden = false
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
]
}
}
}
} else {
if animated {
for name in starNames {
if let node = scene.rootNode.childNode(withName: name, recursively: false) {
node.isHidden = true
}
}
}
}
for name in names {
if let node = scene.rootNode.childNode(withName: name, recursively: false), let particleSystem = node.particleSystems?.first {
if let particleColor = component.particleColor {
particleSystem.particleIntensity = min(1.0, 2.0 * particleSystem.particleIntensity)
particleSystem.particleIntensityVariation = 0.05
particleSystem.particleColor = particleColor
particleSystem.particleColorVariation = SCNVector4Make(0.1, 0.0, 0.12, 0.0)
} else {
particleSystem.particleColorVariation = SCNVector4Make(0.12, 0.03, 0.035, 0.0)
if animated {
particleSystem.particleColor = UIColor(rgb: 0xaa69ea)
}
}
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
]
}
}
}
}
private var didSetup = false
private func setup() {
guard !self.didSetup, let scene = loadCompressedScene(name: "star2", version: sceneVersion) else {
return
}
self.didSetup = true
self.sceneView.scene = scene
self.sceneView.delegate = self
self.updateColors()
if self.animateFrom != nil {
let _ = self.sceneView.snapshot()
} else {
self.didSetReady = true
self._ready.set(.single(true))
self.onReady()
}
}
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 maybeAnimateIn() {
guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false), let animateFrom = self.animateFrom, var containerView = self.containerView else {
return
}
containerView = containerView.subviews[2].subviews[1]
if let animationColor = self.animationColor {
let newNode = node.clone()
newNode.geometry = node.geometry?.copy() as? SCNGeometry
let colorMaterial = SCNMaterial()
colorMaterial.diffuse.contents = animationColor
colorMaterial.lightingModel = SCNMaterial.LightingModel.blinn
newNode.geometry?.materials = [colorMaterial]
node.addChildNode(newNode)
newNode.scale = SCNVector3(1.03, 1.03, 1.03)
newNode.geometry?.materials.first?.diffuse.contents = animationColor
let animation = CABasicAnimation(keyPath: "opacity")
animation.beginTime = CACurrentMediaTime() + 0.1
animation.duration = 0.7
animation.fromValue = 1.0
animation.toValue = 0.0
animation.fillMode = .forwards
animation.isRemovedOnCompletion = false
animation.completion = { [weak newNode] _ in
newNode?.removeFromParentNode()
}
newNode.addAnimation(animation, forKey: "opacity")
}
let initialPosition = self.sceneView.center
let targetPosition = self.sceneView.superview!.convert(self.sceneView.center, to: containerView)
let sourcePosition = animateFrom.superview!.convert(animateFrom.center, to: containerView).offsetBy(dx: 0.0, dy: -20.0)
containerView.addSubview(self.sceneView)
self.sceneView.center = targetPosition
animateFrom.alpha = 0.0
self.sceneView.layer.animateScale(from: 0.05, to: 0.5, duration: 1.0, timingFunction: kCAMediaTimingFunctionSpring)
self.sceneView.layer.animatePosition(from: sourcePosition, to: targetPosition, duration: 1.0, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in
self.addSubview(self.sceneView)
self.sceneView.center = initialPosition
})
Queue.mainQueue().after(0.4, {
animateFrom.alpha = 1.0
})
self.animateFrom = nil
self.containerView = nil
}
private func onReady() {
self.setupScaleAnimation()
self.setupGradientAnimation()
self.setupShineAnimation()
self.maybeAnimateIn()
self.playAppearanceAnimation(explode: true)
self.previousInteractionTimestamp = CACurrentMediaTime()
self.timer = SwiftSignalKit.Timer(timeout: 1.0, repeat: true, completion: { [weak self] in
if let strongSelf = self, strongSelf.hasIdleAnimations {
let currentTimestamp = CACurrentMediaTime()
if currentTimestamp > strongSelf.previousInteractionTimestamp + 5.0 {
strongSelf.playAppearanceAnimation()
}
}
}, queue: Queue.mainQueue())
self.timer?.start()
}
private func setupScaleAnimation() {
guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else {
return
}
let fromScale: Float = self.isIntro ? 0.1 : 0.08
let toScale: Float = self.isIntro ? 0.115 : 0.092
let animation = CABasicAnimation(keyPath: "scale")
animation.duration = 2.0
animation.fromValue = NSValue(scnVector3: SCNVector3(x: fromScale, y: fromScale, z: fromScale))
animation.toValue = NSValue(scnVector3: SCNVector3(x: toScale, y: toScale, z: toScale))
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
animation.autoreverses = true
animation.repeatCount = .infinity
node.addAnimation(animation, forKey: "scale")
}
private func setupGradientAnimation() {
guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else {
return
}
guard let initial = node.geometry?.materials.first?.diffuse.contentsTransform else {
return
}
let animation = CABasicAnimation(keyPath: "contentsTransform")
animation.duration = 4.5
animation.fromValue = NSValue(scnMatrix4: initial)
animation.toValue = NSValue(scnMatrix4: SCNMatrix4Translate(initial, -0.35, 0.35, 0))
animation.timingFunction = CAMediaTimingFunction(name: .linear)
animation.autoreverses = true
animation.repeatCount = .infinity
node.geometry?.materials.first?.diffuse.addAnimation(animation, forKey: "gradient")
}
private func setupShineAnimation() {
guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else {
return
}
guard let initial = node.geometry?.materials.first?.emission.contentsTransform else {
return
}
let animation = CABasicAnimation(keyPath: "contentsTransform")
animation.fillMode = .forwards
animation.fromValue = NSValue(scnMatrix4: initial)
animation.toValue = NSValue(scnMatrix4: SCNMatrix4Translate(initial, -1.6, 0.0, 0.0))
animation.timingFunction = CAMediaTimingFunction(name: .easeOut)
animation.beginTime = 1.1
animation.duration = 0.9
let group = CAAnimationGroup()
group.animations = [animation]
group.beginTime = 1.0
group.duration = 4.0
group.repeatCount = .infinity
node.geometry?.materials.first?.emission.addAnimation(group, forKey: "shimmer")
}
private func playAppearanceAnimation(
velocity: CGFloat? = nil,
smallAngle: Bool = false,
mirror: Bool = false,
explode: Bool = false,
force: Bool = false
) {
guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else {
return
}
let currentTime = CACurrentMediaTime()
self.previousInteractionTimestamp = currentTime
self.delayTapsTill = currentTime + 0.85
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")
}
}
}
var from = node.presentation.eulerAngles
if abs(from.y) - .pi * 2.0 < 0.05 {
from.y = 0.0
}
node.removeAnimation(forKey: "tapRotate")
var toValue: Float = smallAngle ? 0.0 : .pi * 2.0
if let velocity = velocity, !smallAngle && abs(velocity) > 200 && velocity < 0.0 {
toValue *= -1
}
if mirror {
toValue *= -1
}
let to = SCNVector3(x: 0.0, y: toValue, z: 0.0)
let distance = rad2deg(to.y - from.y)
guard !distance.isZero else {
return
}
let springAnimation = CASpringAnimation(keyPath: "eulerAngles")
springAnimation.fromValue = NSValue(scnVector3: from)
springAnimation.toValue = NSValue(scnVector3: to)
springAnimation.mass = 1.0
springAnimation.stiffness = 21.0
springAnimation.damping = 5.8
springAnimation.duration = springAnimation.settlingDuration * 0.75
springAnimation.initialVelocity = velocity.flatMap { abs($0 / CGFloat(distance)) } ?? 1.7
springAnimation.completion = { [weak node] finished in
if finished {
node?.eulerAngles = SCNVector3(x: 0.0, y: 0.0, z: 0.0)
}
}
node.addAnimation(springAnimation, forKey: "rotate")
}
func update(component: PremiumStarComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
let previousComponent = self.component
self.component = component
self.setup()
if let previousComponent, component.colors != previousComponent.colors {
self.updateColors(animated: true)
self.playAppearanceAnimation(velocity: nil, mirror: component.colors?.contains(UIColor(rgb: 0xe57d02)) == true, explode: true, force: true)
}
if let backgroundColor = component.backgroundColor {
self.sceneView.backgroundColor = backgroundColor
} else {
self.sceneView.backgroundColor = .clear
}
self.sceneView.bounds = CGRect(origin: .zero, size: CGSize(width: availableSize.width * 2.0, height: availableSize.height * 2.0))
if self.sceneView.superview == self {
self.sceneView.center = CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0)
}
self.hasIdleAnimations = component.hasIdleAnimations
return availableSize
}
}
public func makeView() -> View {
return View(frame: CGRect(), isIntro: self.isIntro)
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}
public final class StandalonePremiumStarComponent: Component {
let theme: PresentationTheme
let colors: [UIColor]?
public init(
theme: PresentationTheme,
colors: [UIColor]? = nil
) {
self.theme = theme
self.colors = colors
}
public static func ==(lhs: StandalonePremiumStarComponent, rhs: StandalonePremiumStarComponent) -> Bool {
return lhs.theme === rhs.theme && lhs.colors == rhs.colors
}
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 component: StandalonePremiumStarComponent?
private var _ready = Promise<Bool>()
public var ready: Signal<Bool, NoError> {
return self._ready.get()
}
private let sceneView: SCNView
private var timer: SwiftSignalKit.Timer?
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
super.init(frame: frame)
self.addSubview(self.sceneView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.timer?.invalidate()
}
private var didSetup = false
private func setup() {
guard !self.didSetup, let scene = loadCompressedScene(name: "star2", version: sceneVersion) else {
return
}
self.didSetup = true
self.sceneView.scene = scene
self.sceneView.delegate = self
if let component = self.component, let node = scene.rootNode.childNode(withName: "star", recursively: false), let colors =
component.colors {
node.geometry?.materials.first?.diffuse.contents = generateDiffuseTexture(colors: colors)
}
for node in scene.rootNode.childNodes {
if let name = node.name, name.hasPrefix("particles") {
node.isHidden = true
}
}
self.didSetReady = true
self._ready.set(.single(true))
self.onReady()
}
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.setupGradientAnimation()
self.playAppearanceAnimation(mirror: true)
}
private func setupScaleAnimation() {
guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else {
return
}
let fromScale: Float = 0.1
let toScale: Float = 0.092
let animation = CABasicAnimation(keyPath: "scale")
animation.duration = 2.0
animation.fromValue = NSValue(scnVector3: SCNVector3(x: fromScale, y: fromScale, z: fromScale))
animation.toValue = NSValue(scnVector3: SCNVector3(x: toScale, y: toScale, z: toScale))
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
animation.autoreverses = true
animation.repeatCount = .infinity
node.addAnimation(animation, forKey: "scale")
}
private func setupGradientAnimation() {
guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else {
return
}
guard let initial = node.geometry?.materials.first?.diffuse.contentsTransform else {
return
}
let animation = CABasicAnimation(keyPath: "contentsTransform")
animation.duration = 4.5
animation.fromValue = NSValue(scnMatrix4: initial)
animation.toValue = NSValue(scnMatrix4: SCNMatrix4Translate(initial, -0.35, 0.35, 0))
animation.timingFunction = CAMediaTimingFunction(name: .linear)
animation.autoreverses = true
animation.repeatCount = .infinity
node.geometry?.materials.first?.diffuse.addAnimation(animation, forKey: "gradient")
}
private func playAppearanceAnimation(velocity: CGFloat? = nil, smallAngle: Bool = false, mirror: Bool = false) {
guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else {
return
}
var from = node.presentation.eulerAngles
if abs(from.y - .pi * 2.0) < 0.001 {
from.y = 0.0
}
node.removeAnimation(forKey: "tapRotate")
var toValue: Float = smallAngle ? 0.0 : .pi * 2.0
if let velocity = velocity, !smallAngle && abs(velocity) > 200 && velocity < 0.0 {
toValue *= -1
}
if mirror {
toValue *= -1
}
let to = SCNVector3(x: 0.0, y: toValue, z: 0.0)
let distance = rad2deg(to.y - from.y)
guard !distance.isZero else {
return
}
let animation = CABasicAnimation(keyPath: "eulerAngles")
animation.fromValue = NSValue(scnVector3: from)
animation.toValue = NSValue(scnVector3: to)
animation.duration = 0.4 * UIView.animationDurationFactor()
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
animation.completion = { [weak node] finished in
if finished {
node?.eulerAngles = SCNVector3(x: 0.0, y: 0.0, z: 0.0)
}
}
node.addAnimation(animation, forKey: "rotate")
}
func update(component: StandalonePremiumStarComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
self.component = component
self.setup()
self.sceneView.bounds = CGRect(origin: .zero, size: CGSize(width: availableSize.width * 2.0, height: availableSize.height * 2.0))
if self.sceneView.superview == self {
self.sceneView.center = CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.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)
}
}