-
-
Save fatbobman/5b9c22a575a0d4a284bf5db79a283c8b to your computer and use it in GitHub Desktop.
A demo of textRender
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
struct LineByLineDemo: View { | |
@State var show = true | |
var body: some View { | |
VStack { | |
if show { | |
let weekly = Text("Fatbobman's Swift Weekly") | |
.foregroundStyle(.pink).bold() | |
let swiftui = Text("SwiftUI") | |
.foregroundStyle(.green).bold() | |
Text("Get weekly handpicked \nupdates on \(swiftui) from\n \(weekly)!") | |
.font(.system(.title, design: .rounded, weight: .semibold)) | |
.transition(LineByLineTransition()) | |
} | |
Spacer() | |
GroupBox { | |
Toggle("Show", isOn: $show.animation()) | |
} | |
} | |
.frame(height: 200) | |
.multilineTextAlignment(.center) | |
.padding() | |
} | |
} | |
#Preview { | |
LineByLineDemo() | |
} | |
struct LineByLineEffect: TextRenderer { | |
var elapsedTime: TimeInterval // Time elapsed since the start of the animation | |
var elementDuration: TimeInterval // Duration of each element's animation | |
var totalDuration: TimeInterval // Total duration of the animation | |
var animatableData: Double { | |
get { elapsedTime } // Get the elapsed time | |
set { | |
elapsedTime = newValue // Set the elapsed time | |
} | |
} | |
init(elapsedTime: TimeInterval, elementDuration: Double = 0.5, totalDuration: TimeInterval) { | |
// Initialize with elapsed time, element duration, and total duration | |
self.elapsedTime = min(elapsedTime, totalDuration) // Ensure elapsed time does not exceed total duration | |
self.elementDuration = min(elementDuration, totalDuration) // Ensure element duration does not exceed total duration | |
self.totalDuration = totalDuration // Set the total duration | |
} | |
func draw(layout: Text.Layout, in context: inout GraphicsContext) { | |
// Draw the text layout in the graphics context | |
let delay = elementDelay(count: layout.count) // Calculate the delay between elements | |
for (i, line) in layout.enumerated() { | |
// Iterate over each line in the layout | |
let timeOffset = TimeInterval(i) * delay // Calculate the time offset for the current line | |
let elementTime = max(0, min(elapsedTime - timeOffset, elementDuration)) // Calculate the animation time for the current line | |
var copy = context // Create a copy of the graphics context | |
draw(line, at: elementTime, in: ©) // Draw the current line | |
} | |
} | |
var spring: Spring { | |
// Create a spring animation with snappy effect | |
.snappy(duration: elementDuration - 0.05, extraBounce: 0.4) | |
} | |
func draw( | |
_ line: Text.Layout.Line, | |
at time: TimeInterval, | |
in context: inout GraphicsContext | |
) { | |
// Draw a single line of text layout | |
let progress = time / elementDuration // Calculate the progress of the animation | |
let opacity = UnitCurve.easeIn.value(at: 1.4 * progress) // Calculate the opacity based on progress | |
let blurRadius = line.typographicBounds.rect.height / 16 * UnitCurve.easeIn.value(at: 1 - progress) // Calculate the blur radius based on progress | |
let translationY = spring.value(fromValue: -line.typographicBounds.descent, toValue: 0, initialVelocity: 0, time: time) // Calculate the y-axis translation | |
context.opacity = opacity // Set the context opacity | |
context.addFilter(.blur(radius: blurRadius)) // Add blur filter to the context | |
context.translateBy(x: 0, y: translationY) // Translate the context | |
context.draw(line, options: .disablesSubpixelQuantization) // Draw the line of text | |
} | |
/// Calculates how much time passes between the start of two consecutive | |
/// element animations. | |
/// | |
/// For example, if there's a total duration of 1 s and an element | |
/// duration of 0.5 s, the delay for two elements is 0.5 s. | |
/// The first element starts at 0 s, and the second element starts at 0.5 s | |
/// and finishes at 1 s. | |
/// | |
/// However, to animate three elements in the same duration, | |
/// the delay is 0.25 s, with the elements starting at 0.0 s, 0.25 s, | |
/// and 0.5 s, respectively. | |
func elementDelay(count: Int) -> TimeInterval { | |
let count = TimeInterval(count) // Convert element count to time interval | |
let remainingTime = totalDuration - count * elementDuration // Calculate the remaining time | |
let delay = max(remainingTime / (count + 1), (totalDuration - elementDuration) / count) // Calculate the delay between elements | |
return delay // Return the calculated delay | |
} | |
} | |
extension Text.Layout { | |
var flattenedRuns: some RandomAccessCollection<Text.Layout.Run> { | |
// Flatten the lines into runs | |
flatMap { line in | |
line | |
} | |
} | |
var flattenedRunSlices: some RandomAccessCollection<Text.Layout.RunSlice> { | |
// Flatten the runs into run slices | |
flattenedRuns.flatMap(\.self) | |
} | |
} | |
struct LineByLineTransition: Transition { | |
let duration: TimeInterval | |
init(duration: TimeInterval = 1.0) { | |
self.duration = duration | |
} | |
func body(content: Content, phase: TransitionPhase) -> some View { | |
let elapsedTime = phase.isIdentity ? duration : 0 | |
let renderer = LineByLineEffect( | |
elapsedTime: elapsedTime, | |
totalDuration: duration | |
) | |
content.transaction { t in | |
if !t.disablesAnimations { | |
t.animation = .linear(duration: duration) | |
} | |
} body: { view in | |
view.textRenderer(renderer) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment