diff --git a/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftCompositionComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftCompositionComponent.swift index 8d6260f685..78fe8a36ad 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftCompositionComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftCompositionComponent.swift @@ -382,9 +382,10 @@ public final class GiftCompositionComponent: Component { container.frame.origin.x = floor((availableSize.width - visualSize.width) / 2.0) container.layer.animatePosition( - from: CGPoint(x: -containerWidth - visualSize.width * 0.5 + containerWidth / 2.0 - self.spacingX - 70.0, y: container.frame.center.y), + from: CGPoint(x: -containerWidth - visualSize.width * 0.5 + containerWidth / 2.0 - self.spacingX - 120.0, y: container.frame.center.y), to: CGPoint(x: container.frame.center.x, y: container.frame.center.y), duration: self.maxAnimDuration, + delay: 0.05, timingFunction: kCAMediaTimingFunctionSpring, completion: { [weak self] _ in guard let self, let container = self.decelContainer else { @@ -396,8 +397,7 @@ public final class GiftCompositionComponent: Component { ) self.spinState = .decelerating - self.spinLink?.invalidate() - self.spinLink = nil + self.ensureDisplayLink() } private func handleDecelArrived(container: UIView, iconSize: CGSize, visualSize: CGSize) { @@ -506,14 +506,91 @@ public final class GiftCompositionComponent: Component { self.componentState?.updated(transition: .easeInOut(duration: 0.25)) self.component?.requestUpdate(.easeInOut(duration: 0.25)) } + self.applyEdge3DHorizontal() case .decelerating: - break + self.applyEdge3DHorizontal() case .idle, .settled: self.spinLink?.invalidate() self.spinLink = nil } } + private let minScaleAtEdgeX: CGFloat = 0.7 + private let yawAtEdgeDegrees: CGFloat = 25.0 + private let edgeFalloffX: CGFloat = 0.25 + + @inline(__always) + private func smoothstep01(_ x: CGFloat) -> CGFloat { + let t = max(0.0, min(1.0, x)) + return t * t * (3.0 - 2.0 * t) + } + + @inline(__always) + private func liveMidX(in container: UIView, of view: UIView) -> CGFloat { + if let pres = view.layer.presentation() { + let p = container.layer.convert(pres.position, from: view.layer.superlayer) + return p.x + } + return view.center.x + } + + @inline(__always) + private func midXInsideAnimatedContainer(in selfView: UIView, container: UIView, hostView: UIView) -> CGFloat { + let contPres = container.layer.presentation() ?? container.layer + let hostPres = hostView.layer.presentation() ?? hostView.layer + + let hostOffsetFromContainerCenter = hostPres.position.x - container.bounds.midX + return contPres.position.x + hostOffsetFromContainerCenter + } + + private func edge3DTransformFor(midX: CGFloat, containerWidth: CGFloat, baseScale: CGFloat = 1.0) -> CATransform3D { + guard containerWidth > 0 else { + return CATransform3DMakeScale(baseScale, baseScale, 1.0) + } + let d = abs((midX - containerWidth * 0.5) / (containerWidth * 0.5)) + + let uRaw = (d - edgeFalloffX) / (1.0 - edgeFalloffX) + let u = smoothstep01(max(0.0, min(1.0, uRaw))) + + let scale = (1.0 - u) * baseScale + (minScaleAtEdgeX * baseScale) * u + let yawSign: CGFloat = (midX < containerWidth * 0.5) ? 1.0 : -1.0 + let yawRadians = (yawAtEdgeDegrees * .pi / 180.0) * u * yawSign + + var t = CATransform3DIdentity + t = CATransform3DRotate(t, yawRadians, 0.0, 1.0, 0.0) + t = CATransform3DScale(t, scale, scale, 1.0) + return t + } + + private func applyEdge3DHorizontal() { + let containerWidth = self.bounds.width + guard containerWidth > 0.0 else { return } + + CATransaction.begin() + CATransaction.setDisableActions(true) + + for w in self.activeWrappers { + guard w.superview === self else { continue } + let midX = liveMidX(in: self, of: w) + if let host = w.subviews.first { + let baseScale = max(0.01, w.bounds.width / max(host.bounds.width, 0.01)) + host.layer.transform = edge3DTransformFor(midX: midX, containerWidth: containerWidth, baseScale: baseScale) + } + } + + for hostView in self.decelItemHosts { + guard let container = self.decelContainer, hostView.superview === container && hostView !== self.decelItemHosts.first else { + continue + } + let midX = midXInsideAnimatedContainer(in: self, container: container, hostView: hostView) + if let host = hostView.subviews.first { + let baseScale = max(0.01, hostView.bounds.width / max(host.bounds.width, 0.01)) + host.layer.transform = edge3DTransformFor(midX: midX, containerWidth: containerWidth, baseScale: baseScale) + } + } + + CATransaction.commit() + } public func update(component: GiftCompositionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { let previousComponent = self.component diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index 16cda97b45..4567e1caad 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -1549,15 +1549,15 @@ private final class GiftViewSheetContent: CombinedComponent { } self.updated(transition: .spring(duration: 0.4)) - Queue.mainQueue().after(1.5) { + Queue.mainQueue().after(1.2) { self.revealedAttributes.insert(.backdrop) self.updated(transition: .immediate) - Queue.mainQueue().after(1.0) { + Queue.mainQueue().after(0.7) { self.revealedAttributes.insert(.pattern) self.updated(transition: .immediate) - Queue.mainQueue().after(1.0) { + Queue.mainQueue().after(0.7) { self.revealedAttributes.insert(.model) self.updated(transition: .immediate) @@ -2648,31 +2648,43 @@ private final class GiftViewSheetContent: CombinedComponent { AnyComponentWithIdentity(id: "label", component: AnyComponent(Text(text: "Collectible #", font: textFont, color: .white, tintColor: textColor))) ] + let numberFont = Font.with(size: 13.0, traits: .monospacedNumbers) let spinningItems: [AnyComponentWithIdentity] = [ - AnyComponentWithIdentity(id: "0", component: AnyComponent(Text(text: "0", font: textFont, color: textColor))), - AnyComponentWithIdentity(id: "1", component: AnyComponent(Text(text: "1", font: textFont, color: textColor))), - AnyComponentWithIdentity(id: "2", component: AnyComponent(Text(text: "2", font: textFont, color: textColor))), - AnyComponentWithIdentity(id: "3", component: AnyComponent(Text(text: "3", font: textFont, color: textColor))), - AnyComponentWithIdentity(id: "4", component: AnyComponent(Text(text: "4", font: textFont, color: textColor))), - AnyComponentWithIdentity(id: "5", component: AnyComponent(Text(text: "5", font: textFont, color: textColor))), - AnyComponentWithIdentity(id: "6", component: AnyComponent(Text(text: "6", font: textFont, color: textColor))), - AnyComponentWithIdentity(id: "7", component: AnyComponent(Text(text: "7", font: textFont, color: textColor))), - AnyComponentWithIdentity(id: "8", component: AnyComponent(Text(text: "8", font: textFont, color: textColor))), - AnyComponentWithIdentity(id: "9", component: AnyComponent(Text(text: "9", font: textFont, color: textColor))) + AnyComponentWithIdentity(id: "0", component: AnyComponent(Text(text: "0", font: numberFont, color: textColor))), + AnyComponentWithIdentity(id: "1", component: AnyComponent(Text(text: "1", font: numberFont, color: textColor))), + AnyComponentWithIdentity(id: "2", component: AnyComponent(Text(text: "2", font: numberFont, color: textColor))), + AnyComponentWithIdentity(id: "3", component: AnyComponent(Text(text: "3", font: numberFont, color: textColor))), + AnyComponentWithIdentity(id: "4", component: AnyComponent(Text(text: "4", font: numberFont, color: textColor))), + AnyComponentWithIdentity(id: "5", component: AnyComponent(Text(text: "5", font: numberFont, color: textColor))), + AnyComponentWithIdentity(id: "6", component: AnyComponent(Text(text: "6", font: numberFont, color: textColor))), + AnyComponentWithIdentity(id: "7", component: AnyComponent(Text(text: "7", font: numberFont, color: textColor))), + AnyComponentWithIdentity(id: "8", component: AnyComponent(Text(text: "8", font: numberFont, color: textColor))), + AnyComponentWithIdentity(id: "9", component: AnyComponent(Text(text: "9", font: numberFont, color: textColor))) ] if let numberValue = uniqueGift?.number { - let numberString = "\(numberValue)" + let numberString = formatCollectibleNumber(numberValue, dateTimeFormat: environment.dateTimeFormat) var i = 0 + var index = 0 for c in numberString { - items.append(AnyComponentWithIdentity(id: "c\(i)", component: AnyComponent(SlotsComponent( - item: AnyComponent(Text(text: String(c), font: textFont, color: .white)), - items: spinningItems, - isAnimating: i > state.revealedNumberDigits, - tintColor: textColor, - verticalOffset: -1.0 - UIScreenPixel, - motionBlur: false, - size: CGSize(width: 8.0, height: 14.0)))) - ) + let s = String(c) + if s == "\u{00A0}" { + items.append(AnyComponentWithIdentity(id: "c\(i)", component: AnyComponent(Text(text: s, font: textFont, color: .white, tintColor: textColor))) + ) + } else if [".", ","].contains(s) { + items.append(AnyComponentWithIdentity(id: "c\(i)", component: AnyComponent(Text(text: s, font: numberFont, color: .white, tintColor: textColor))) + ) + } else { + items.append(AnyComponentWithIdentity(id: "c\(i)", component: AnyComponent(SlotsComponent( + item: AnyComponent(Text(text: String(c), font: numberFont, color: .white)), + items: spinningItems, + isAnimating: index > state.revealedNumberDigits, + tintColor: textColor, + verticalOffset: -1.0 - UIScreenPixel, + motionBlur: false, + size: CGSize(width: 8.0, height: 14.0)))) + ) + index += 1 + } i += 1 } } diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/SlotsComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/SlotsComponent.swift index 3908766bb5..93d348a966 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/SlotsComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/SlotsComponent.swift @@ -340,9 +340,7 @@ final class SlotsComponent: Component { self.spawnRandomSlot(availableSize: availableSize) } case .decelerating: - let t = clamp01(self.decelTotalSteps > 1 - ? Double(self.decelStepIndex) / Double(self.decelTotalSteps - 1) - : 1.0) + let t = clamp01(self.decelTotalSteps > 1 ? Double(self.decelStepIndex) / Double(self.decelTotalSteps - 1) : 1.0) if let last = self.lastSpawnTime, now - last >= self.currentInterval { if !self.decelQueue.isEmpty {