PowerPointで円を弾ませてみる

[PptClash]:http://chemiphys.hateblo.jp/entry/2017/01/14/130717 "Powerpoint VBAで当たり判定を考える。"

> [Powerpoint VBAで当たり判定を考える。][PptClash]



の記事に触発されて、適当に円が跳ねるものを作ってみた。

それっぽく動いているだけで、法則とか適当なのはご勘弁を。

![1484451441SIcrKEUMxhtsTZ21484451379.gif](https://qiita-image-store.s3.amazonaws.com/0/142842/29ae9d49-bb53-da72-c455-c7f77545a1e5.gif)


# 接触判定

今回接触判定に使用しているのはMSOffice 2010以降から搭載されている「図形の結合」機能である（2010ではリボンに表示されていないが、機能自体は存在している）。

Office 2013以降なら
（図形を選択） 描画ツール > 書式 > 図形の挿入
に存在する。
![Menu.PNG](https://qiita-image-store.s3.amazonaws.com/0/142842/8b7b6176-9d87-16ef-ec7e-cfe7c4a485ed.png)

より詳細な部分は[先頭の記事][PptClash]を参照。

# コード

PowerPoint2010以降で動作するはずです（2013 32bit/2016 64bitで動作確認）。

PowerPointで、標準モジュールとクラスモジュールを挿入し、以下のコードを貼り付ける。
クラスモジュールの名前は`Ball`とすること。

`BallBoundSample`プロシージャを起動すると上記の画像のような動作を行うはずである。

## 標準モジュール

```vbnet:標準モジュール
Option Explicit

Private Declare PtrSafe Sub _
    Sleep Lib "kernel32" ( _
    Optional ByVal dwMilliseconds As Long = 1)

Public Type OfficeXY
    X As Single
    Y As Single
End Type

Sub BallBoundSample()
    '作業場所の指定
        Dim TgtPres As PowerPoint.Presentation
        Set TgtPres = PowerPoint.ActivePresentation
        
        TgtPres.SlideShowSettings.ShowType = ppShowTypeWindow2
        
        Dim sldShowWin As PowerPoint.SlideShowWindow
        If TgtPres.Windows.Count = 0 Then
            Set sldShowWin = TgtPres.SlideShowWindow
            sldShowWin.Activate
        Else
            Set sldShowWin = TgtPres.SlideShowSettings.Run
        End If
        
        Dim tgtSld As Slide
        Set tgtSld = sldShowWin.View.Slide
    
    '形状の準備
        Dim tgtGround As PowerPoint.Shape
        Set tgtGround = MakeGround(tgtSld)
        
        Dim myBall As Ball
        Set myBall = MakeBall(tgtSld)
    
    '動かしてみる
        Do
            'スライド外になったら抜ける
            If Not InSlideArea(myBall.BaseShape, TgtPres) Then Exit Do
            Call myBall.NextStep(0.2)
            Call myBall.Clash(tgtGround)
            Call Sleep
            DoEvents    '描写・途中で止めたい場合用
        Loop
    
    '後始末
        'Stop
        tgtSld.Shapes.Range.Delete
    
End Sub

'適当な形状を作るだけ
Private Function MakeGround(ByVal iTgtSld As Slide) As Shape
    Dim shps As PowerPoint.Shapes
    Set shps = iTgtSld.Shapes
    
    Dim oShp As Shape
    
    '位置や角度の設定は適当
    With iTgtSld.Parent.SlideMaster
        Set oShp = shps.AddShape(msoShapeRectangle, -.Width, .Height / 2, .Width * 3, 1)
    End With    'iTgtSld.Parent.SlideMaster
    Call oShp.IncrementRotation(15)
    Set MakeGround = oShp
End Function


Private Function MakeBall(ByVal iTgtSld As Slide) As Ball
    Const Size! = 50!
    
    Dim shpTop As Single: shpTop = 0!
    Dim shpLft As Single: shpLft = 0!
    Dim shpWid As Single: shpWid = Size
    Dim shpHit As Single: shpHit = Size
    
    Dim shps As PowerPoint.Shapes
    Set shps = iTgtSld.Shapes
    
    Dim tmpBall As PowerPoint.Shape
    Set tmpBall = shps.AddShape(msoShapeOval, shpLft, shpTop, shpWid, shpHit)
    Set MakeBall = GetBallClass(tmpBall)
End Function

Private Function GetBallClass(ByVal BaseShape As Shape) As Ball
    Dim tmpCls As Ball
    Set tmpCls = New Ball
    Call tmpCls.Init(BaseShape)
    Set GetBallClass = tmpCls
End Function

Private Function InSlideArea(ByVal ChkShp As Shape, ByVal TgtPres As Presentation) As Boolean
    With TgtPres.SlideMaster
        If ChkShp.Top + ChkShp.Height < 0 Then GoTo NotInSlideArea
        If ChkShp.Left > .Width Then GoTo NotInSlideArea
        
        If ChkShp.Left + ChkShp.Width < 0 Then GoTo NotInSlideArea
        If ChkShp.Top > .Height Then GoTo NotInSlideArea
    End With
    Let InSlideArea = True
Exit Function
NotInSlideArea:
    Let InSlideArea = False
Exit Function
End Function
```

## クラスモジュール：Ball

```vbnet:Ball.cls
Option Explicit

Private clsShp As PowerPoint.Shape
Private clsVelocity As OfficeXY
Private clsAcceleration As OfficeXY

'反発率を再現しようとしたもの
Private Const Ratio! = 0.9!

Public Property Get BaseShape() As PowerPoint.Shape
    Set BaseShape = clsShp
End Property

Public Property Get Velocity() As OfficeXY
    Let Velocity = clsVelocity
End Property

Public Property Get Acceleration() As OfficeXY
    Let Acceleration = clsAcceleration
End Property

Private Sub Class_Initialize()
    clsVelocity.X = 0!
    clsVelocity.Y = 0!
    clsAcceleration.X = 0!
    clsAcceleration.Y = 9.81!
End Sub

Friend Sub Init(ByVal BaseShape As PowerPoint.Shape)
    Set clsShp = BaseShape
End Sub

'時間を進める
    '現在位置の更新と速度の再定義
Public Sub NextStep(Optional ByVal Step As Single = 1)
    With clsShp
        .Left = VBA.CSng(.Left + (clsVelocity.X * Step))
        .Top = VBA.CSng(.Top + (clsVelocity.Y * Step))
    End With    'clsShp
    
    With clsVelocity
        .X = VBA.CSng((.X + clsAcceleration.X * Step))
        .Y = VBA.CSng((.Y + clsAcceleration.Y * Step))
    End With    'clsVelocity
    
    '画面描写
    clsShp.TextFrame2.TextRange.Text = " "
End Sub

'不完全なロジック
    '大きくめり込んだとき、貫通したとき復帰不可
    '分解能を上げる（NextStepの引数を小さくする）ことで仮対応
Public Function Clash(ByVal iTgtShp As PowerPoint.Shape) As Boolean
    Dim sldShps As PowerPoint.Shapes
    Set sldShps = clsShp.Parent.Shapes
    
    Dim currentShpCnt As Long
    currentShpCnt = sldShps.Count
    
    
    Dim shpIndxs(1) As Long
    shpIndxs(0) = clsShp.ZOrderPosition
    shpIndxs(1) = iTgtShp.ZOrderPosition
    
    Dim dupShpRng As PowerPoint.ShapeRange
    Set dupShpRng = sldShps.Range(shpIndxs).Duplicate
    
    
    Dim dupOvalIndex As Long
    dupOvalIndex = IIf(shpIndxs(0) < shpIndxs(1), 1, 2)
    
    Dim currentCoords As OfficeXY
    With dupShpRng.Item(dupOvalIndex)
        currentCoords.X = .Left + (.Width / 2)
        currentCoords.Y = .Top + (.Height / 2)
    End With    'dupShpRng.Item(1)
    
    
    '形状が交差していれば、形状作成。交差していなければ形状消失。
    Call dupShpRng.MergeShapes(msoMergeIntersect)
    If currentShpCnt + 1 <> sldShps.Count Then Exit Function
    Clash = True
    
    
    Dim intersectShp As PowerPoint.Shape
    Set intersectShp = sldShps.Item(currentShpCnt + 1)
    
    Dim normalVector As OfficeXY
    normalVector.X = currentCoords.X - (intersectShp.Left + (intersectShp.Width / 2))
    normalVector.Y = currentCoords.Y - (intersectShp.Top + (intersectShp.Height / 2))
    
    Dim normalVec As Double
    normalVec = VectorNorm(normalVector)
    
    
    Dim basVector As Double
    basVector = VectorNorm(clsVelocity)
    
    With clsVelocity
        .X = VBA.CSng(basVector * (normalVector.X / normalVec) * Ratio)
        .Y = VBA.CSng(basVector * (normalVector.Y / normalVec) * Ratio)
    End With
    
    Call intersectShp.Delete
End Function

Private Function VectorNorm(BaseVector As OfficeXY) As Double
    Let VectorNorm = VBA.Sqr(BaseVector.X * BaseVector.X + BaseVector.Y * BaseVector.Y)
End Function
```

# その他

## 2017/01/15追記
[先頭の記事][PptClash]の人の記事が追加されていました。
> [線に沿う玉の運動を描く④](http://chemiphys.hateblo.jp/entry/2017/01/15/114501 "線に沿う玉の運動を描く④")