Various improvements

This commit is contained in:
Ilya Laktyushin 2025-08-25 07:16:46 +04:00
parent ea2c1e24af
commit 1d20927877
3 changed files with 117 additions and 30 deletions

View File

@ -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<Empty>, transition: ComponentTransition) -> CGSize {
let previousComponent = self.component

View File

@ -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<Empty>] = [
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
}
}

View File

@ -340,9 +340,7 @@ final class SlotsComponent<ChildEnvironment: Equatable>: 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 {