Swiftgram/submodules/PremiumUI/Sources/GiftAvatarComponent.swift
Ilya Laktyushin aa46e81a49 Various fixes
2022-07-10 20:35:05 +02:00

296 lines
12 KiB
Swift

import Foundation
import UIKit
import Display
import ComponentFlow
import SwiftSignalKit
import SceneKit
import GZip
import AppBundle
import LegacyComponents
import AvatarNode
import AccountContext
import TelegramCore
private let sceneVersion: Int = 3
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() -> UIImage {
return generateImage(CGSize(width: 256, height: 256), rotatedContext: { size, context in
let colorsArray: [CGColor] = [
UIColor(rgb: 0x0079ff).cgColor,
UIColor(rgb: 0x6a93ff).cgColor,
UIColor(rgb: 0x9172fe).cgColor,
UIColor(rgb: 0xe46acd).cgColor,
]
var locations: [CGFloat] = [0.0, 0.25, 0.5, 0.75, 1.0]
let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colorsArray as CFArray, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: size.height), options: CGGradientDrawingOptions())
})!
}
class GiftAvatarComponent: Component {
let context: AccountContext
let peer: EnginePeer?
let isVisible: Bool
let hasIdleAnimations: Bool
init(context: AccountContext, peer: EnginePeer?, isVisible: Bool, hasIdleAnimations: Bool) {
self.context = context
self.peer = peer
self.isVisible = isVisible
self.hasIdleAnimations = hasIdleAnimations
}
static func ==(lhs: GiftAvatarComponent, rhs: GiftAvatarComponent) -> Bool {
return lhs.peer == rhs.peer && lhs.isVisible == rhs.isVisible && lhs.hasIdleAnimations == rhs.hasIdleAnimations
}
final class View: UIView, SCNSceneRendererDelegate, ComponentTaggedView {
final class Tag {
}
func matches(tag: Any) -> Bool {
if let _ = tag as? Tag {
return true
}
return false
}
private var _ready = Promise<Bool>()
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 previousInteractionTimestamp: Double = 0.0
private var timer: SwiftSignalKit.Timer?
private var hasIdleAnimations = false
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)
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) {
self.playAppearanceAnimation(velocity: nil, mirror: false, explode: true)
}
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()
if #available(iOS 11.0, *) {
node.removeAnimation(forKey: "rotate", blendOutDuration: 0.1)
node.removeAnimation(forKey: "tapRotate", blendOutDuration: 0.1)
} else {
node.removeAllAnimations()
}
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 url = getAppBundle().url(forResource: "gift", withExtension: "scn"), let scene = try? SCNScene(url: url, options: nil) else {
return
}
self.sceneView.scene = scene
self.sceneView.delegate = self
let _ = self.sceneView.snapshot()
}
private var didSetReady = false
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 particles = scene.rootNode.childNode(withName: "particles", recursively: false) {
if let particleSystem = particles.particleSystems?.first {
particleSystem.particleColorVariation = SCNVector4(0.15, 0.2, 0.15, 0.3)
particleSystem.speedFactor = 2.0
particleSystem.particleVelocity = 2.2
particleSystem.birthRate = 4.0
particleSystem.particleLifeSpan = 2.0
node.physicsField?.isActive = true
Queue.mainQueue().after(1.0) {
node.physicsField?.isActive = false
particles.particleSystems?.first?.birthRate = 1.2
particleSystem.particleVelocity = 1.0
particleSystem.particleLifeSpan = 4.0
let animation = POPBasicAnimation()
animation.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)
animation.fromValue = 2.0 as NSNumber
animation.toValue = 1.0 as NSNumber
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
animation.duration = 0.5
particleSystem.pop_add(animation, forKey: "speedFactor")
}
}
}
}
func update(component: GiftAvatarComponent, availableSize: CGSize, transition: Transition) -> 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)
}
self.hasIdleAnimations = component.hasIdleAnimations
let avatarSize = CGSize(width: 100.0, height: 100.0)
if let peer = component.peer {
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: 63.0), size: avatarSize)
return availableSize
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, transition: transition)
}
}