♨️

SwiftUIで紐を表現する

2022/08/10に公開
2

概要

  • Twitterで惚れ惚れするようなリアルな紐の表現を見かけたので、見様見真似で実装してみました。

前提

  • 厳密な物理法則に従うものではなく紐っぽい動きを表現するような実装です。
  • また高校一年生レベルの物理(等加速度直線運動・フックの法則等)を使います。

GitHub

参考

Twitterでの情報収集まとめ
  • TimelineViewを使う。
  • SpriteKitは使っていない。
  • Springを考慮している。
  • 実装のイメージがつきやすい。
  • quadratic Bezier curve、つまり二次ベジェ曲線で表現している。
  • 実装のヒント。
  • 中点の決め方でコードを参考にした。
// https://codesandbox.io/s/spring-rope-physics-pjk51h?file=/src/App.tsx:1751-1772
const distance = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
const decline = Math.max(0, 500 - distance);
const midpointX = (x1 + x2) / 2;
const midpointY = (y1 + y2) / 2 + decline;
  • Springの参考に。
  • Springの参考に。

紐の形のPathを作成

概要

image

GitHub

実装

Path { path in
    path.move(to: pointP0)
    path.addQuadCurve(to: pointP2,
                      control: pointP1)
    path.addLine(to: pointP2)
}
  • ここでP0, P2はドラッグ操作等で動かせるものとし、この2点を使ってP1の座標を算出したいです。
  • x座標はP0P2の中点で良さそうですが、y座標は紐の弛みを表現するために少しだけ工夫します。
  • 厳密に計算できるのかもしれませんが、今回はそれらしい表現のできる値となるようにしています。
private let ropeLength: CGFloat = 400  // (1)
@State var pointP0: CGPoint = .init(x: 100, y: 100)
@State var pointP2: CGPoint = .init(x: 300, y: 100)

// P1: 制御点(Control Point)
var pointP1: CGPoint {
    let distance = sqrt(
        pow(pointP2.x - pointP0.x, 2) + pow(pointP2.y - pointP0.y, 2)
    )
    let decline = max(0, 400 - distance / 2)  // (2)
    
    return .init(x: (pointP0.x + pointP2.x) / 2,
                 y: (pointP0.y + pointP2.y) / 2 + decline)  // (3)
}
  • 下記のような処理をしています。
    • (1): 紐の長さを400とし、あとの処理でP0とP2の距離がそれより短い場合に紐をたるませるようにします。
    • (2): 中点からどれだけ下げるのかを計算します。紐の長さ - (P0-P2間の距離 / 2)としています。
    • (3): 最終的にP1はP0-P2の中点から(2)の値を下げたものとなります。
  • これで紐のPathが決まったので、実際にP0, P2を動かして確認してみます。
  • 紐らしい形のパスの表現ができてますね。

Aug-10-2022 19-30-58

ばねのアニメーション(減衰なし)

概要

  • 紐の形はいい感じになりましたが、このままでは無機質な感じです。
  • リアルな表現のためにばねのアニメーション(Spring Animation)を加えます。
    • Springという単語はSwiftのアニメーションのメソッド(例: spring(response:dampingFraction:blendDuration:))などで馴染みがあるかもしれませんが、そのままの意味で"ばね"のようなアニメーションのことですね。
    • 今回はこれを手計算で実装していきます。

GitHub

実装

  • まず青い点を基準点として、そこからすこし離れた赤い点が単振動する動きを実装します。
    • 青い点から赤い点にむかってバネが伸びているイメージですね。

Aug-07-2022 19-41-24

  • 赤い点の計算と描画方法に関しては以下の通りとします。
    • (1): Timerをつかいフレームレート毎に制御点の座標の計算を行う。
    • (2): 上記の結果をTimelineViewを使い、同じフレームレートの間隔でビューを更新する。
  • 点の動きの計算ができるよう一定間隔で描画を行いたい、という理由でこのようにしています。
ZStack {
    // (2)
    TimelineView(.periodic(from: Date(), by: pointsManager.frameRate)) { context in
        // Views...
    }
}
.onAppear {
    pointsManager.startTimer()  // (1)
}
.onDisappear {
    pointsManager.stopTimer()
}
以下数式がでてくるのでアレルギーがなければどうぞ!
  • 今回の説明はこの記事の通りです。
  • まずフレームレート毎の等加速度直線運動を考えます。
    • 実際は単振動なので加速度は常に変化するのですが(元記事では明言がないですが)フレームレート毎の短い時間で近似的に等加速度直線運動で考えます。(間違っていたらご指摘ください)
  • フックの法則により、基準点よりxだけ離れたときにかかる力Fs(Fs:= F_spring)は以下の通り。
Fs = -k*x

x: 基準点からどれだけ離れているか。
k: バネ定数。
F = m*a

m: 質量。軽いほど単振動の動きが早くなります。
a: 加速度。速度の変化する度合いですね。
  • 今回はF = Fsなので、加速度aは下記の通りです。
m*a = -k*x
-> a = -k*x/m
  • よって現在の状態からt秒後の速度v2は以下の通り。
v2 = v1 + a*t
  • またt秒後の位置p2は以下となります。
    • (正直ここの計算があまり理解できていないです。v2はt秒間中に変化するよなあと思うのですが、わかる方がいれば教えていただけると助かります…!)
p2 = p1 + v2*t
  • 以上をSwiftで表すとこのようになり、前述のような単振動が表現できます。
let offsetX = point.x - standardPoint.x  // 基準点からどれだけはなれているか
let fSpringX = spring.k * offsetX  // フックの法則
let ax = fSpringX / point.mass  // 加速度
point.vx = point.vx + ax * frameRate  // v2: frameRate秒後の点の速度
point.x = point.x + point.vx * frameRate  // p2: frameRate秒後の点の位置

ばねのアニメーション(減衰あり)

概要

  • あとは実際のばねが徐々に止まっていくのを表現するため、上記の式に減衰(Damping)を加えると、以下のような動きを表現できます。

Aug-10-2022 19-20-56

GitHub

実装

以下数式がでてくるのでアレルギーがなければどうぞ!
  • 減衰の力Fd(:= F_damping)は以下のように表せます。
Fd = -d * v

d: 減衰定数。値が大きいほど減衰の度合いが大きいです。
v: 物体の速度
  • よって加速度aは以下のように表せます。
F = Fs + Fd
またF = maなので
a = (Fs + Fd) / m
  • 以上を前と同様にSwiftで表現すると以下のようになります。
let offsetX = point.x - standardPoint.x  // 基準点からどれだけはなれているか
let fSpringX = spring.k * offsetX  // フックの法則
let fDampingX = spring.d * point.vx  // 減衰
let ax = (fSpringX + fDampingX) / point.mass  // 加速度
point.vx = point.vx + ax * frameRate  // v2: frameRate秒後の点の速度
point.x = point.x + point.vx * frameRate  // p2: frameRate秒後の点の位置

紐の実装

概要

  • 先程のばねのアニメーションを紐の形のPathに対して適用すると、いい感じの紐の表現ができるようになります。

Aug-10-2022 19-40-58

GitHub

実装

  • P1を基準とした点をcontrolPointと定義します。
  • 紐のパスはP0, controlPoint, P2で描画し、controlPointP1を中心にばねのアニメーションを行います。
  • 具体的にはばねのアニメーション(減衰あり)の内容をy軸にも拡張し、これをcontrolPointに適用します。
let offsetX = controlPoint.x - pointP1.x
let offsetY = controlPoint.y - pointP1.y
let fSpringX = spring.k * offsetX
let fSpringY = spring.k * offsetY
let fDampingX = spring.d * controlPoint.vx
let fDampingY = spring.d * controlPoint.vy
let ax = (fSpringX + fDampingX) / controlPoint.mass
let ay = (fSpringY + fDampingY) / controlPoint.mass

controlPoint.vx = controlPoint.vx + ax * frameRate
controlPoint.vy = controlPoint.vy + ay * frameRate
controlPoint.x = controlPoint.x + controlPoint.vx * frameRate
controlPoint.y = controlPoint.y + controlPoint.vy * frameRate

Tweetの実装

概要

  • これまで説明した紐の実装を使って、最初のツイートのような表現を実装しています。

GitHub

実装

  • 紐の実装以外は特別な所は少ないと思うので、コードを参照くださいませ。

紐の始点・終点をニコちゃんマークにあわせる実装

  • ここのうまい方法が思いつかず、状態管理とDragGestureでゴリゴリに実装しています。
    • いい方法があればコメントください!
  • 方針としては一つのView、今回でいうとZStackに全てのViewを配置しDragGestureで諸々の処理を行っています。

Discussion