Telegram-iOS/submodules/TelegramUI/Components/StreamingTextReveal/Sources/TextRevealController.swift
2026-05-20 00:34:25 +08:00

90 lines
3.9 KiB
Swift

import Foundation
public final class TextRevealController {
private static let velocityTau: Double = 0.12
private static let gapEwmaAlpha: Double = 0.4
private static let initialGap: Double = 0.5
private static let stallFloor: Double = 0.10
private static let finalizeTime: Double = 0.3
private static let frameDtCap: Double = 0.05
private static let initialInputRate: Double = 40.0
public private(set) var revealedCount: Double
private var velocity: Double = 0.0
private var avgInterArrival: Double = TextRevealController.initialGap
private var lastSampleTime: Double?
private var lastSampleLength: Int?
private var predictedNextArrivalTime: Double?
private var chunkCount: Int = 0
public private(set) var latestLength: Int
public private(set) var isFinalizing: Bool = false
private var lastFrameTime: Double?
public var durationMultiplier: Double
public init(initialRevealedCount: Int, initialLength: Int, durationMultiplier: Double = 1.0) {
self.revealedCount = Double(initialRevealedCount)
self.latestLength = initialLength
self.durationMultiplier = max(0.0001, durationMultiplier)
}
public var currentGlyphCount: Int {
return Int(self.revealedCount)
}
public func observeUpdate(latestLength: Int, at now: Double) {
if let lastLen = self.lastSampleLength {
if latestLength > lastLen {
if let lastTime = self.lastSampleTime {
let interArrival = max(now - lastTime, 0.001)
self.avgInterArrival = TextRevealController.gapEwmaAlpha * interArrival
+ (1.0 - TextRevealController.gapEwmaAlpha) * self.avgInterArrival
}
self.lastSampleTime = now
self.lastSampleLength = latestLength
self.predictedNextArrivalTime = now + self.avgInterArrival
self.chunkCount += 1
} else if latestLength < lastLen {
self.lastSampleLength = latestLength
}
} else {
self.lastSampleTime = now
self.lastSampleLength = latestLength
self.predictedNextArrivalTime = now + self.avgInterArrival
self.chunkCount += 1
}
self.latestLength = latestLength
if self.revealedCount > Double(latestLength) {
self.revealedCount = Double(latestLength)
}
}
public func finalize(finalLength: Int) {
self.latestLength = finalLength
self.isFinalizing = true
if self.revealedCount > Double(finalLength) {
self.revealedCount = Double(finalLength)
}
}
public func tick(now: Double) -> (revealedGlyphCount: Int, isComplete: Bool) {
let dt = min(now - (self.lastFrameTime ?? now), TextRevealController.frameDtCap)
let lag = max(0.0, Double(self.latestLength) - self.revealedCount)
let targetVelocity: Double
if self.isFinalizing {
targetVelocity = max(self.velocity, lag / TextRevealController.finalizeTime)
} else if self.chunkCount < 2 {
targetVelocity = lag > 0.0 ? TextRevealController.initialInputRate : 0.0
} else if let predNext = self.predictedNextArrivalTime {
let timeToNext = max(TextRevealController.stallFloor, predNext - now)
targetVelocity = lag / timeToNext
} else {
targetVelocity = lag > 0.0 ? TextRevealController.initialInputRate : 0.0
}
let smoothing = min(1.0, dt / TextRevealController.velocityTau)
self.velocity += (targetVelocity - self.velocity) * smoothing
self.revealedCount = min(Double(self.latestLength), self.revealedCount + self.velocity * dt / self.durationMultiplier)
self.lastFrameTime = now
let isComplete = self.isFinalizing && self.revealedCount >= Double(self.latestLength)
return (Int(self.revealedCount), isComplete)
}
}