6
\$\begingroup\$

I've been working on re-writing comfortablydrei's answer to this PPCG Question regarding creating a illusion animation in pure Excel VBA. So far, I've been able to rewrite the code to function in VBA, and to get the cycle time for the animation down substantially, but have run into an issue where I am GPU-limited on my current system (that is, the animation causes the GPU to pin @ 100% 3D usage)

To reduce the cycle time, I have tried

  • modifying how frequently the VBA.DoEvents call is made, and while this does reduce the time, it also breaks the animation severely
  • Adding Application.ScreenUpdating = False and "=True calls, and this appears not to work
  • reducing the code in the Animate Subroutine to be as simple as I can, with the result being the below

Is there anything that I have missed (general or case-specific tricks or tips) that can make this faster, or is this about as good as can be expected for animations made in Excel VBA?

I am pimarily concerned with the performance of the animate subroutine, and am otherwise only concerned with the readiblity of the other routines


Animate Subroutine

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
''
''  Private (looping) Animation Subroutine
''
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Private Sub animate()
    
    '' dim vars
    Dim iter    As Integer, _
        iPos    As Double, _
        iX      As Double, _
        iY      As Double
        
    '' Debug Vars
    #If DebugMode Then
        Dim t As Long
        Debug.Print "Animation Cycle Time (sec)"
    #End If
    
nextLoop:
    '' do events - allow for screen update, and event handling
    Call VBA.DoEvents
    
    '' Debug Timing
    #If DebugMode Then
        If rad > 0 And rad <= stp Then
           If t > 0 Then Debug.Print Timer - t
           Let t = Timer
        End If
    #End If
    
    '' loop over shapes
    For iter = 1 To numPnts
        
        '' calculate the new position of shape
        Let iPos = Pi * (iter - 1) / numPnts
        If rad + iPos < 0 Then GoTo nextIter
        Let iX = Sin(iPos)
        Let iY = Cos(iPos)
        
        '' set shapes location
        Let shp(iter).Left = plotCenterX - shpSize / 2 + (iX * plotScale / 2) * Cos(rad + iPos)
        Let shp(iter).Top = plotCenterY - shpSize / 2 + (iY * plotScale / 2) * Cos(rad + iPos)
        
nextIter:
    Next iter
    
   
    '' get decimal mod of the rad with respect to tau
    Let rad = rad + stp
    Let rad = Round(rad - Fix(rad / Tau) * Tau, 14)

    GoTo nextLoop
        
End Sub

Full Commented Code

'' Module Options
Option Compare Binary
Option Explicit
Option Base 1

'' Global Vars
Global app            As Excel.Application, _
       twb            As Excel.Workbook, _
       cht            As Excel.Chart, _
       ser            As Excel.Series, _
       shp()          As Excel.Shape, _
       dat()          As Double, _
       rad            As Double

'' Positioning Vars
Global plotCenterX    As Double, _
       plotCenterY    As Double, _
       plotScale      As Double

'' Naming Constants
Private Const chtName As String = "Pie Chart"
Private Const serName As String = "DataSheet"
Private Const numPnts As Integer = 8

'' Shape Constants
Private Const shpType As Long = MsoAutoShapeType.msoShapeRectangle
Private Const shpSize As Long = 15

'' Math Constants
Public Const Pi       As Double = 3.14159265358979
Public Const Tau      As Double = 6.28318530717959
Public Const stp      As Double = 0.03

'' Conditional Compilation Switch for Debug Mode -
''
'' When True, shows timing for a complete animation cycle
''   ( rad - 0 to rad = 2*pi )
#Const DebugMode = True


''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
''
''  Public Main Subroutine
''
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Public Sub Main()
    
    #If DebugMode Then
        Debug.Print
        Debug.Print "Initializing"
    #End If
    
    Dim iter          As Integer, _
        colors        As Variant, _
        pnt           As Excel.Point

    ''  initialize global variables, incl. chart and Data series
    Call pInit
    
    '' To avoid having to upload another module, the output of Scales.HuePalette(8) is hardcoded
    ' Let colors = scales.HuePalette(numPnts)
    Let colors = [{7173880,38603,44664,6798848,12893952,16754944,16743623,13394431}]
    
    
    
    ''  Format the chart as desired
    With ser
        Let .Name = "Spacers"
        Let .Values = "={" & Replace(Space(2 * numPnts - 1), " ", "1,") & "1}"
        With .Format
            Let .Line.ForeColor.RGB = rgbWhite
            Let .Line.Visible = True
            Let .Line.Weight = 1.5
    End With: End With

    Call VBA.DoEvents '' On some machines, the cht.HasTitle
                      '' fails to let if this is not included
    
    '' clean up the view
    Let cht.HasTitle = False
    Let cht.HasLegend = False
    
    Let rad = 0
    
    Dim iPos          As Double, _
        iX            As Double, _
        iY            As Double
    
    '' give each section a nice gradiant, because why not
    For iter = 1 To 2 * numPnts
        Set pnt = ser.Points(iter)
        With pnt.Format.Fill
            Call .TwoColorGradient(msoGradientVertical, 2)
            Let .GradientStops(1).Color.RGB = &HFEFEFE
            Let .GradientStops(2).Color.RGB = &HF2F2F2
            Let .GradientAngle = (360 / (2 * numPnts)) * (iter - 0.5) + 90
        End With
    Next iter
    
    
    '' place the shapes on the edges of the circle
    For iter = 1 To numPnts
        Let iPos = Pi * (iter - 1) / numPnts
        Let iX = Sin(iPos)
        Let iY = Cos(iPos)
        
        '' setup the shapes with a nice color
        With shp(iter)
            Let .Fill.ForeColor.RGB = colors(iter)
            Let .Line.ForeColor.RGB = rgbBlack
            Let .Line.Weight = 0.75
            
            Let .Left = plotCenterX - shpSize / 2 + iX * plotScale / 2
            Let .Top = plotCenterY - shpSize / 2 + iY * plotScale / 2
        End With
    Next iter
    
    
    Let rad = -Pi
    
    #If DebugMode Then
        Debug.Print "Initialization Complete. Beginnning Animation"
        Debug.Print
    #End If
    
    Call animate
    
End Sub


''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
''
''  Private (looping) Animation Subroutine
''
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Private Sub animate()
    
    '' dim vars
    Dim iter    As Integer, _
        iPos    As Double, _
        iX      As Double, _
        iY      As Double
        
    '' Debug Vars
    #If DebugMode Then
        Dim t As Long
        Debug.Print "Animation Cycle Time (sec)"
    #End If
    
nextLoop:
    '' do events - allow for screen update, and event handling
    Call VBA.DoEvents
    
    '' Debug Timing
    #If DebugMode Then
        If rad > 0 And rad <= stp Then
           If t > 0 Then Debug.Print Timer - t
           Let t = Timer
        End If
    #End If
    
    '' loop oveer shapes
    For iter = 1 To numPnts
        
        '' calculate the new position of shape
        Let iPos = Pi * (iter - 1) / numPnts
        If rad + iPos < 0 Then GoTo nextIter
        Let iX = Sin(iPos)
        Let iY = Cos(iPos)
        
        '' set shapes location
        Let shp(iter).Left = plotCenterX - shpSize / 2 + (iX * plotScale / 2) * Cos(rad + iPos)
        Let shp(iter).Top = plotCenterY - shpSize / 2 + (iY * plotScale / 2) * Cos(rad + iPos)
        
nextIter:
    Next iter
    
   
    '' get decimal mod of the rad with respect to tau
    Let rad = rad + stp
    Let rad = Round(rad - Fix(rad / Tau) * Tau, 14)

    GoTo nextLoop
        
End Sub


''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
''
''  Private Initialization Subroutines
''
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

'' Main Initialization Sub, Calls all following Init Calls
Private Sub pInit()
    ReDim shp(1 To numPnts)
    ReDim dat(1 To numPnts, 0 To 1)
    Set app = Excel.Application
    Set twb = app.ThisWorkbook
    Let app.Calculation = xlCalculationManual
    Call pInitChart
    Call pInitSeries
    Call pInitShapes
    Call pInitPlotVars
End Sub

'' Initialixes the Pie Chart, cht
Private Sub pInitChart()
    If Not cht Is Nothing Then Exit Sub
    If KeyExists(app.Charts, chtName) Then Set cht = app.Charts(chtName): Exit Sub
    Set cht = app.Charts.Add
    Let cht.Name = chtName
    Let cht.ChartType = xlPie
End Sub

'' Initialixes Initializes the data series, ser, in cht
Private Sub pInitSeries()
    Dim i As Long
    With cht
        For i = .SeriesCollection.Count To 1 Step -1
            Call .SeriesCollection(i).Delete
        Next i
        Call .SeriesCollection.NewSeries
        Set ser = .SeriesCollection(1)
    End With
End Sub

'' adds array of shapes to the chart workbook object
Private Sub pInitShapes()
    Dim i As Long
    For i = cht.Shapes.Count To 1 Step -1
        Call cht.Shapes(i).Delete
    Next i
    For i = 1 To numPnts Step 1
        Set shp(i) = cht.Shapes.AddShape(shpType, shpSize * i, shpSize * i, shpSize, shpSize)
    Next i
End Sub

'' initializes the ploting vars
Private Sub pInitPlotVars()
    Let plotScale = cht.PlotArea.InsideHeight
    Debug.Assert plotScale = cht.PlotArea.InsideWidth '' if this fails the chart is not square
    Let plotCenterX = plotScale / 2 + cht.PlotArea.InsideLeft
    Let plotCenterY = plotScale / 2 + cht.PlotArea.InsideTop
End Sub


''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
''
''  Generic functions
''
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

''  Takes an object, and returns a bool which describes if `key` is a vaild key
''    for either the default method of the object or for the `item` method of
''    object
Public Function KeyExists(ByRef objCol As Object, ByVal key As String) As Boolean
    Dim var As Variant, _
        k   As Boolean
    On Error Resume Next
    With VBA.Err
        Let var = objCol(key):      Let k = k Or (.Number = 0): Call .Clear '' test let and default
        Set var = objCol(key):      Let k = k Or (.Number = 0): Call .Clear '' test set and default
        Let var = objCol.Item(key): Let k = k Or (.Number = 0): Call .Clear '' test let and item
        Set var = objCol.Item(key): Let k = k Or (.Number = 0): Call .Clear '' test set and item
    End With
    Let KeyExists = k
    On Error GoTo 0
End Function

Current Performance:

Win 11P, Excel 2021, R9-5950X, RTX 3070Ti 8GB \$ \approx1.75\text{ sec} \$ - Limited by VBA.DoEvents

Win 10P, Excel 2016, I7 8850H, Quadro P2000 4GB \$ \approx1.99\text{ sec} \$

Win 10P, i7 8850H, Intel Integrated Graphics \$\approx8.07\text{ sec}\$

\$\endgroup\$
1
  • \$\begingroup\$ If performance is key, then it wouldn't hurt to profile your code and determine exactly the source of slowness. Is it the DoEvents calls (in which case make this asynchronous using a winapi timer instead of DoEvents to avoid unnecessary processing)? Maybe it's the writing to the chart (use an algorithmic change to reduce the number of hits, try doing all your writing at once)? Perhaps there's some kind of caching on Excel's part that slows the animation over time. Or something else entirely. Profile the code using a high performance stopwatch to determine the source of bottlenecks. \$\endgroup\$
    – Greedo
    Mar 20, 2020 at 7:45

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.