Rainbow Trail
An interactive drawing component that creates beautiful rainbow trails with variable width and smooth animations
Preview
Code
1import SwiftUI23struct RainbowTrailView: View {4 @State private var trails: [TrailSegment] = []5 @State private var currentPath: [CGPoint] = []6 @State private var isDrawing = false7 @State private var lastPoint: CGPoint = .zero8 @State private var lastTime: Date = Date()9 10 var body: some View {11 ZStack {12 // Drawing Canvas (Full Screen)13 ZStack {14 // Trail segments15 ForEach(trails.indices, id: \.self) { index in16 TrailSegmentView(segment: trails[index])17 }18 19 // Current drawing path20 if !currentPath.isEmpty {21 VariableWidthPath(points: currentPath)22 .fill(23 LinearGradient(24 colors: [.red, .orange, .yellow, .green, .blue, .purple],25 startPoint: .leading,26 endPoint: .trailing27 )28 )29 }30 }31 .frame(maxWidth: .infinity, maxHeight: .infinity)32 .contentShape(Rectangle()) // Make the entire area tappable33 .gesture(34 DragGesture(minimumDistance: 0)35 .onChanged { value in36 let currentTime = Date()37 let timeDelta = currentTime.timeIntervalSince(lastTime)38 39 if !isDrawing {40 isDrawing = true41 currentPath = [value.location]42 lastPoint = value.location43 lastTime = currentTime44 } else {45 // Calculate velocity46 let distance = sqrt(pow(value.location.x - lastPoint.x, 2) + pow(value.location.y - lastPoint.y, 2))47 let velocity = timeDelta > 0 ? distance / CGFloat(timeDelta) : 048 49 // Add interpolated points based on velocity50 let minDistance: CGFloat = max(2, 20 - velocity * 0.5) // Faster = smaller distance = more circles51 let interpolatedPoints = interpolatePoints(from: lastPoint, to: value.location, minDistance: minDistance)52 53 currentPath.append(contentsOf: interpolatedPoints)54 lastPoint = value.location55 lastTime = currentTime56 }57 }58 .onEnded { _ in59 if !currentPath.isEmpty {60 // Create a new trail segment61 let newSegment = TrailSegment(62 path: currentPath,63 createdAt: Date(),64 opacity: 1.065 )66 trails.append(newSegment)67 68 // Start fading animation69 withAnimation(.linear(duration: 3.0)) {70 if let index = trails.firstIndex(where: { $0.id == newSegment.id }) {71 trails[index].opacity = 0.072 }73 }74 75 // Remove the segment after animation completes76 DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {77 trails.removeAll { $0.id == newSegment.id }78 }79 }80 81 currentPath = []82 isDrawing = false83 }84 )85 86 }87 .background(Color(.systemGroupedBackground))88 }89 90 private func interpolatePoints(from start: CGPoint, to end: CGPoint, minDistance: CGFloat) -> [CGPoint] {91 let distance = sqrt(pow(end.x - start.x, 2) + pow(end.y - start.y, 2))92 93 if distance < minDistance {94 return []95 }96 97 let numberOfPoints = Int(distance / minDistance)98 var points: [CGPoint] = []99 100 for i in 1...numberOfPoints {101 let ratio = CGFloat(i) / CGFloat(numberOfPoints)102 let x = start.x + (end.x - start.x) * ratio103 let y = start.y + (end.y - start.y) * ratio104 points.append(CGPoint(x: x, y: y))105 }106 107 return points108 }109}110111struct TrailSegment: Identifiable {112 let id = UUID()113 let path: [CGPoint]114 let createdAt: Date115 var opacity: Double116}117118struct TrailSegmentView: View {119 let segment: TrailSegment120 121 var body: some View {122 VariableWidthPath(points: segment.path)123 .fill(124 LinearGradient(125 colors: [.red, .orange, .yellow, .green, .blue, .purple],126 startPoint: .leading,127 endPoint: .trailing128 )129 )130 .opacity(segment.opacity)131 }132}133134struct VariableWidthPath: Shape {135 let points: [CGPoint]136 137 func path(in rect: CGRect) -> Path {138 var path = Path()139 140 guard points.count >= 2 else { return path }141 142 // Create variable width stroke by drawing multiple circles along the path143 for i in 0..<points.count {144 let point = points[i]145 let progress = Double(i) / Double(points.count - 1)146 147 // Calculate width based on position (thinner at start and end)148 let baseWidth: CGFloat = 30 // Much thicker base width149 let widthVariation = sin(progress * .pi) * 0.6 + 0.4 // 0.4 to 1.0 multiplier150 let width = baseWidth * widthVariation151 152 // Add circle at this point153 let circleRect = CGRect(154 x: point.x - width/2,155 y: point.y - width/2,156 width: width,157 height: width158 )159 path.addEllipse(in: circleRect)160 }161 162 return path163 }164}165166#Preview {167 RainbowTrailView()168}