Last active
March 26, 2023 22:08
-
-
Save HonmaMasaru/7a13f53b9ea839f63f7990638d7fc71f to your computer and use it in GitHub Desktop.
Shapeのアニメーション
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
import SwiftUI | |
import Combine | |
import PlaygroundSupport | |
/// アニメーション対応のシェイプ | |
struct AnimatableShape: Shape { | |
/// ポイントデータ | |
var points: AnimatableCGPointVector | |
/// アニメーション対象 | |
var animatableData: AnimatableCGPointVector { | |
get { points } | |
set { points = newValue } | |
} | |
/// パスの作成 | |
/// - Parameter rect: 領域 | |
/// - Returns: パス | |
func path(in rect: CGRect) -> Path { | |
.init { $0.addLines(points.values) } | |
} | |
} | |
/// 形状の切り替え | |
struct SwitchShapeView: View { | |
/// ポイントの数 | |
private let length: Int = 30 | |
/// 形状 | |
enum Mode { | |
case rect, circle | |
/// トグル | |
mutating func toggle() { | |
self = self == .rect ? .circle : .rect | |
} | |
} | |
var mode: Mode | |
/// ポイントデータ | |
@State var points: AnimatableCGPointVector = .zero | |
/// 色 | |
var fillColor: Color | |
/// 本体 | |
var body: some View { | |
GeometryReader { proxy in | |
AnimatableShape(points: points) | |
.fill(fillColor) | |
.onReceive(Just(mode)) { newValue in | |
withAnimation { | |
switch newValue { | |
case .rect: | |
points = .init(values: getRectPoints(proxy.size)) | |
case .circle: | |
points = .init(values: getCirclePoints(proxy.size)) | |
} | |
} | |
} | |
} | |
} | |
/// 矩形のポイントリストを取得 | |
/// - Parameter size: 矩形のサイズ | |
/// - Returns: ポイントリスト | |
private func getRectPoints(_ size: CGSize) -> [CGPoint] { | |
let radius = sqrt(size.width * size.height / .pi) // 半径 | |
let vRange = -(size.height / 2)...(size.height / 2) | |
let hRange = -(size.width / 2)...(size.width / 2) | |
return (0..<length).map { | |
let radian = 2 * .pi / CGFloat(length) * CGFloat($0) | |
let point = CGPoint(x: radius * cos(radian), y: radius * sin(radian)) | |
let x = point.x < 0 ? min(hRange.lowerBound, point.x) : max(hRange.upperBound, point.x) | |
let y = point.y < 0 ? max(vRange.lowerBound, point.y) : min(vRange.upperBound, point.y) | |
return .init(x: x + size.width / 2, y: y + size.height / 2) | |
} | |
} | |
/// 円のポイントリストを取得 | |
/// - Parameter size: 矩形のサイズ | |
/// - Returns: ポイントリスト | |
private func getCirclePoints(_ size: CGSize) -> [CGPoint] { | |
let radius = sqrt(size.width * size.height / .pi) // 半径 | |
return (0..<length).map { | |
let radian = 2 * .pi / CGFloat(length) * CGFloat($0) | |
var point = CGPoint(x: radius * cos(radian), y: radius * sin(radian)) // 座標 | |
point.x += size.width / 2 | |
point.y += size.height / 2 | |
return point | |
} | |
} | |
} | |
/// メイン | |
struct ContentView: View { | |
/// 形状 | |
@State private var mode: SwitchShapeView.Mode = .rect | |
/// 本体 | |
var body: some View { | |
ZStack { | |
SwitchShapeView(mode: mode, fillColor: .red) | |
.frame(width: 100, height: 50) | |
} | |
.frame(width: 300, height: 200) | |
Button("Toggle") { | |
mode.toggle() | |
} | |
} | |
} | |
PlaygroundPage.current.setLiveView(ContentView()) | |
// MARK: - | |
// https://gist.github.com/mecid/04ab91f45fec501e72e4d5fb02277f3f | |
struct AnimatableCGPointVector: VectorArithmetic { | |
static var zero = AnimatableCGPointVector(values: [.zero]) | |
static func - (lhs: AnimatableCGPointVector, rhs: AnimatableCGPointVector) -> AnimatableCGPointVector { | |
let values = zip(lhs.values, rhs.values) | |
.map { lhs, rhs in lhs.animatableData - rhs.animatableData } | |
.map { CGPoint(x: $0.first, y: $0.second) } | |
return AnimatableCGPointVector(values: values) | |
} | |
static func -= (lhs: inout AnimatableCGPointVector, rhs: AnimatableCGPointVector) { | |
for i in 0..<min(lhs.values.count, rhs.values.count) { | |
lhs.values[i].animatableData -= rhs.values[i].animatableData | |
} | |
} | |
static func + (lhs: AnimatableCGPointVector, rhs: AnimatableCGPointVector) -> AnimatableCGPointVector { | |
let values = zip(lhs.values, rhs.values) | |
.map { lhs, rhs in lhs.animatableData + rhs.animatableData } | |
.map { CGPoint(x: $0.first, y: $0.second) } | |
return AnimatableCGPointVector(values: values) | |
} | |
static func += (lhs: inout AnimatableCGPointVector, rhs: AnimatableCGPointVector) { | |
for i in 0..<min(lhs.values.count, rhs.values.count) { | |
lhs.values[i].animatableData += rhs.values[i].animatableData | |
} | |
} | |
var values: [CGPoint] | |
mutating func scale(by rhs: Double) { | |
for i in 0..<values.count { | |
values[i].animatableData.scale(by: rhs) | |
} | |
} | |
var magnitudeSquared: Double { | |
values | |
.map { $0.animatableData.magnitudeSquared } | |
.reduce(0.0, +) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment