Last active
December 30, 2024 20:58
-
-
Save Koshimizu-Takehito/406cc24623cf09e805bf60805817a2d5 to your computer and use it in GitHub Desktop.
長方形アニメーション
This file contains hidden or 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
import SwiftUI | |
struct ContentView: View { | |
@StateObject private var viewModel = RectsViewModel() | |
var body: some View { | |
GeometryReader { geometry in | |
TimelineView(.animation) { timeline in | |
Canvas {context, size in | |
let rects = viewModel.rects | |
for rect in rects { | |
rect.update(size: size) | |
let path = Path { path in | |
let rectSize = CGSize( | |
width: rect.currentWidth, | |
height: rect.currentHeight | |
) | |
let rectOrigin = CGPoint( | |
x: rect.currentX - rect.currentWidth / 2, | |
y: rect.currentY - rect.currentHeight / 2 | |
) | |
path.addRect(CGRect(origin: rectOrigin, size: rectSize)) | |
} | |
context.fill(path, with: .color(rect.currentColor)) | |
} | |
} | |
.background(Color(red: 0.137, green: 0.137, blue: 0.137)) | |
.ignoresSafeArea() | |
.onChange(of: timeline.date) { _, _ in | |
viewModel.updateRects() | |
} | |
} | |
.onAppear { | |
viewModel.setup(size: geometry.size) | |
} | |
} | |
} | |
} | |
class RectsViewModel: ObservableObject { | |
@Published var rects: [RectModel] = [] | |
let colors: [Color] = [ | |
Color(red: 0.937, green: 0.176, blue: 0.337), // #ef2d56 | |
Color(red: 0.051, green: 0.376, blue: 0.612), // #0D609C | |
Color(red: 0.929, green: 0.490, blue: 0.227), // #ed7d3a | |
Color(red: 0.941, green: 0.741, blue: 0.082), // #F0BD15 | |
Color(red: 0.549, green: 0.847, blue: 0.404), // #8cd867 | |
Color(red: 0.184, green: 0.749, blue: 0.443), // #2fbf71 | |
Color(red: 0.808, green: 0.878, blue: 0.863), // #cee0dc | |
Color(red: 0.725, green: 0.812, blue: 0.831), // #b9cfd4 | |
] | |
func setup(size: CGSize) { | |
var tempRects: [RectModel] = [] | |
let width = size.width | |
let height = size.height | |
for _ in 0..<5000 { | |
let x = .random(in: -0.1...1.1) * width | |
let y = .random(in: -0.1...1.1) * height | |
var w = width * .random(in: 0.02...0.3) | |
let h = width * .random(in: 0.02...0.3) | |
let type = Int.random(in: 0...1) | |
if type == 1 { | |
w = h | |
} | |
let newRect = RectModel( | |
x: x, | |
y: y, | |
w: w, | |
h: h, | |
color: colors.randomElement()!, | |
colors: colors, | |
canvasSize: size | |
) | |
if !tempRects.contains(where: { $0.checkCollision(with: newRect) }) { | |
tempRects.append(newRect) | |
} | |
} | |
rects = tempRects | |
} | |
func updateRects() { | |
for rect in rects { | |
rect.animate() | |
} | |
objectWillChange.send() | |
} | |
} | |
class RectModel: Identifiable { | |
let id = UUID() | |
let originX: Double | |
let originY: Double | |
let originWidth: Double | |
let originHeight: Double | |
var currentColor: Color // 現在の色 | |
var targetColor: Color // 目標の色 | |
var t: Double | |
let t1: Double = 120 | |
let t2: Double = 240 | |
let t3: Double = 480 | |
let t4: Double = 600 | |
let t5: Double = 720 | |
var progress1: Double = 0 | |
var progress2: Double = 0 | |
let shiftX: Double | |
let shiftY: Double | |
let voh: Bool | |
let colors: [Color] | |
// 現在の位置とサイズ | |
var currentX: Double | |
var currentY: Double | |
var currentWidth: Double | |
var currentHeight: Double | |
init( | |
x: Double, | |
y: Double, | |
w: Double, | |
h: Double, | |
color: Color, | |
colors: [Color], | |
canvasSize: CGSize | |
) { | |
self.originX = x | |
self.originY = y | |
self.originWidth = w | |
self.originHeight = h | |
self.currentX = x | |
self.currentY = y | |
self.currentWidth = w | |
self.currentHeight = h | |
self.currentColor = color | |
self.targetColor = color | |
self.colors = colors | |
self.t = -Double.random(in: 0..<100) | |
self.voh = Bool.random() | |
// 移動量をキャンバスサイズの10%に設定 | |
let maxShiftX = canvasSize.width * 0.1 | |
let maxShiftY = canvasSize.height * 0.1 | |
if voh { | |
self.shiftX = 0 | |
self.shiftY = Double.random(in: -maxShiftY...maxShiftY) | |
} else { | |
self.shiftX = Double.random(in: -maxShiftX...maxShiftX) | |
self.shiftY = 0 | |
} | |
} | |
func update(size: CGSize) { | |
// アニメーションの進行度合いに基づいて位置とサイズを更新 | |
let width = size.width | |
let xShift = shiftX * (1 - progress1) | |
let yShift = shiftY * (1 - progress1) | |
currentX = originX + xShift | |
currentY = originY + yShift | |
let minSize = width * 0.01 | |
currentWidth = lerp(a: minSize, b: originWidth, t: progress2) | |
currentHeight = lerp(a: minSize, b: originHeight, t: progress2) | |
} | |
func animate() { | |
// 色の補間 | |
currentColor = blendColor(from: currentColor, to: targetColor, fraction: 0.05) | |
if Int(t) % 80 == 0 { // 色の変更タイミングを調整 | |
targetColor = colors.randomElement()! // 新しい目標色を設定 | |
} | |
if t > 0 && t < t1 { | |
progress1 = easeInOutQuint(x: normalize(value: t, start: 0, end: t1 - 1)) | |
} else if t1 < t && t < t2 { | |
progress2 = easeInOutQuint(x: normalize(value: t, start: t1, end: t2 - 1)) | |
} else if t2 < t && t < t3 { | |
// 一時停止 | |
} else if t3 < t && t < t4 { | |
progress2 = easeInOutQuint(x: 1 - normalize(value: t, start: t3, end: t4 - 1)) | |
} else if t4 < t && t < t5 { | |
progress1 = easeInOutQuint(x: 1 - normalize(value: t, start: t4, end: t5 - 1)) | |
} | |
if t5 < t { | |
t = 0 | |
} | |
t += 1 // tの増加量はそのまま | |
} | |
func checkCollision(with other: RectModel) -> Bool { | |
return | |
!(originX - originWidth / 2 > other.originX + other.originWidth / 2 || originX + originWidth / 2 < other.originX - other.originWidth / 2 | |
|| originY - originHeight / 2 > other.originY + other.originHeight / 2 || originY + originHeight / 2 < other.originY - other.originHeight / 2) | |
} | |
// 色の補間 | |
private func blendColor(from: Color, to: Color, fraction: Double) -> Color { | |
let fromComponents = from.components() | |
let toComponents = to.components() | |
let r = fromComponents.red + (toComponents.red - fromComponents.red) * fraction | |
let g = fromComponents.green + (toComponents.green - fromComponents.green) * fraction | |
let b = fromComponents.blue + (toComponents.blue - fromComponents.blue) * fraction | |
let a = fromComponents.alpha + (toComponents.alpha - fromComponents.alpha) * fraction | |
return Color(red: r, green: g, blue: b, opacity: a) | |
} | |
} | |
// イージング関数と補間関数 | |
func easeInOutQuint(x: Double) -> Double { | |
return x < 0.5 ? 16 * pow(x, 5) : 1 - pow(-2 * x + 2, 5) / 2 | |
} | |
func normalize(value: Double, start: Double, end: Double) -> Double { | |
return (value - start) / (end - start) | |
} | |
func lerp(a: Double, b: Double, t: Double) -> Double { | |
return a + (b - a) * t | |
} | |
extension Color { | |
func components() -> (red: Double, green: Double, blue: Double, alpha: Double) { | |
var red: CGFloat = 0 | |
var green: CGFloat = 0 | |
var blue: CGFloat = 0 | |
var alpha: CGFloat = 0 | |
UIColor(self).getRed(&red, green: &green, blue: &blue, alpha: &alpha) | |
return (Double(red), Double(green), Double(blue), Double(alpha)) | |
} | |
} | |
#Preview { | |
ContentView() | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment