Use gravity to generate Cartoon animation frames

I wrote a cartoon animation program around 1982 in C. The Honolulu Academy of Arts asked me to demo the program in their Focus on Computer Graphics.

The program allows the user to draw lines of a cartoon frame using the mouse. The user then saves that frame and draws another. To animate, the program draws the line segments of the 1st frame, then calculates and draws the “in between frames” that morph the 1st frame to the 2nd frame.

The new version below makes the Demo mode automatically generates cartoon frames of pairs of closed loop “objects” using gravity. It’s interesting to watch the shapes as they morph into other shapes.

I wrote the original on my $2100 IBM PC with 256K Ram and dual 360k floppy disk drives, running MSDOS, using the 100$ DeSmet C compiler.

I remember setting the machine up to use a RamDisk to minimize the swapping to Floppy disk when compiling and linking. I bought my first 23 Megabyte PC hard disk soon after.

The color graphics adapter on my machine had a display buffer: write a byte to address 0xb800 and it shows on the top left corner of the screen.

For animation to appear smooth, the code had to be fast, so I wrote the code in assembly code. The 8088 CPU ran at 4.77 Megahertz. Here’s the code to draw a single pixel from my cartoon program:

wdot(x,y) {

#asm

    mov    dx,[bp+6]

    mov    cx,[bp+4]

    sal    cx,1

;cx has column 0-639, dx has row 0-199

    mov    al,28h

    push   dx

    and    dl,0feh

    mul    dl

    pop    dx

    test   dl,1

    jz     r4

    add    ax,2000h

r4: mov    si,ax        ;address of first byte of row

    mov    ax,0b800h

    mov    es,ax

    mov    dx,cx

    mov    cx,703h

    and    ch,dl        ;address of pel within byte

    shr    dx,cl

    add    si,dx        ;address of requested byte

    mov    cl,ch        ;bit #

    mov    al,colfl_

    shr    al,cl        ;put a 1 in the right bit location

    xor    es:[si],al       

#

}

Similarly, I had to implement write my own line drawing based on Bresenham’s line algortihm

I wrote an RS232 Serial port decoder and mouse driver in assembly code to interface with my VisiOn mouse. Good thing my C Compiler supported inline assembly code (VS C++ still does)

I had to write my own pull-down menu code and decide various menu behaviors, like whether the menu disappears when the user moved off its area, whether the item was selected with a mouse down or mouse up, etc.

Microsoft Windows, coming a decade later made it easier for programmers to do such programming tasks. Mouse, Printer and Graphics drivers isolated the programmer from the myriad of devices and their quirks. Rather than having to buy every possible mouse/printer and testing my code against them, I could write to a common API.

I wanted to demo my Cartoon program and found there were a few problems with my post from 7 years ago.

The code in the original post is double spaced Visual Basic code, and white space is significant to VB, so some of the lines had syntax errors because of the spacing.

The Quotes, Apostrophes and hyphens were munged into the Word Smart Quote (here I attempt to use Word Quotes around a quote char “"”). The Apostrophe is the beginning of a comment in VB. (Here I attempt to use Word Smart Apostrophes around a normal apostrophe: ‘'’

Also while cleaning up the code for this post, I thought I’d add some new features.

1. Wrap around: after drawing the N-1 to Nth frame, draw the frames between the Nth frame to the first frame so the animations wrap around

2. I’ve added color, changing, so line segments are assigned a changing color: the Nth segment in each frame will be the same color. This allows more easily seeing how segments morph from one frame to the next.

3. I’ve added thickness, so if you’re using a stylus, the thickness will be proportional to the stylus pressure. Even without a stylus, the demo has a mode that increases the thickness with the line segment number

4. A slider allows the user to control the number of frames generated in between.

5. Demo: I’ve made the demo mode automatically generate cartoon frames. By default, it generates 20 frames of 2 objects each, with each object composed of 1000 lines

a. The 2 objects each are drawn randomly, starting from an initial random point and an initial random velocity.

b. The 4 walls each exert a force on the line inversely proportional to the square of the distance to the wall, just as gravity does (inverse square law). This force causes an acceleration which is perturbed randomly in the cartoon frame.

c. The final line segment of each object is a line connecting the last point with the first point, thus forming a closed curve. For these 2 objects, the colors are blue and pink.

d. If you hit the Left Control button and click Demo, the demo will vary the line thickness from thinnest to thickest per object.

Try running the code below

1. Using a mouse, your finger or a stylus, draw a shape with multiple line segments. The Status shows the # of line segments. Try several hundred segments.

2. Hit New Frame

3. Draw another one of roughly the same # of line segments.

4. Hit Play

5. Try moving the slider (or using the mouse wheel) to change the number of generated animation frames.

6. Try drawing the letters of the alphabet in each frame.

See also:

Why was the original IBM PC 4.77 Megahertz?

My toys over the years

 

image

 

< Code>

 Imports System.Windows.Markup
Imports System.Windows.Media
Imports System.Windows.Threading
' Start Visual Studio.
' File-> New->Project->Visual Basic->Windows->WPF
' Replace MainWindow.xaml.vb with this code
' see also https://blogs.msdn.microsoft.com/calvin_hsia/2009/01/29/cartoon-animation-program/

Class MainWindow
  Friend WithEvents btnNewFrame As Button
  Friend WithEvents btnErase As Button
  Friend WithEvents btnPlay As Button
  Friend WithEvents btnDemo As Button
  Friend WithEvents btnReset As Button
  Friend WithEvents sldBetween As Slider
  Friend txtStatus As TextBlock
  Private _AnimControl As AnimControl
  Sub Load() Handles MyBase.Initialized
    WindowState = WindowState.Maximized
    Title = "Cartoon Animation by Calvin Hsia 1982"
    Dim xaml =
<DockPanel
  xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml" LastChildFill="True">
  <StackPanel Background="Transparent" Orientation="Vertical" DockPanel.Dock="Top">
    <TextBlock>Draw To create lines For a cartoon frame. Add a New frame, hit play. 
                        Rebirth Of Calvin's cartoon program circa 1982
                </TextBlock>
  </StackPanel>
  <Border DockPanel.Dock="Top" Height="25">
    <StackPanel Orientation="Horizontal">
      <Button Name="btnNewFrame"
        ToolTip="Add current drawing to cartoon, so you can create a new one">_New Frame</Button>
      <Button Name="btnErase"
        ToolTip="erase current frame">_Erase</Button>
      <Button Name="btnPlay"
        ToolTip="Animate the current frames or stop animation" Width="40">_Play</Button>
      <Button Name="btnDemo"
        ToolTip="Automatically generate frames with curves. Left Ctrl-Click to vary thickness">_Demo</Button>
      <Button Name="btnReset"
        ToolTip="Erase all frames">_Reset</Button>
      <Slider Name="sldBetween" Width="200"
        ToolTip="# Frames to generate in between"
        Maximum="1000" Minimum="0" Value="500"/>
      <TextBlock Name="txtStatus"/>
    </StackPanel>
  </Border>
  <UserControl Name="MyCtrl"/>
</DockPanel>

    Dim dPanel = CType(XamlReader.Load(xaml.CreateReader), DockPanel)
    Dim MyCtrl = CType(dPanel.FindName("MyCtrl"), UserControl)
    _AnimControl = New AnimControl(Me)
    MyCtrl.Content = _AnimControl
    btnPlay = CType(dPanel.FindName("btnPlay"), Button)
    btnNewFrame = CType(dPanel.FindName("btnNewFrame"), Button)
    btnPlay = CType(dPanel.FindName("btnPlay"), Button)
    btnDemo = CType(dPanel.FindName("btnDemo"), Button)
    btnErase = CType(dPanel.FindName("btnErase"), Button)
    btnReset = CType(dPanel.FindName("btnReset"), Button)
    txtStatus = CType(dPanel.FindName("txtStatus"), TextBlock)
    sldBetween = CType(dPanel.FindName("sldBetween"), Slider)
    Me.Content = dPanel
  End Sub
  Sub winloaded() Handles Me.Loaded
    _AnimControl.Demo(varyThickness:=True)
    btnPlay_Click()
  End Sub
  Sub btnNewFrame_Click() Handles btnNewFrame.Click
    _AnimControl.NewFrame()
    RefreshStatus()
  End Sub
  Sub btnPlay_Click() Handles btnPlay.Click
    If _AnimControl._timer.IsEnabled Then
      StopPlay()
    Else
      btnNewFrame_Click()   ' save any currently drawn changes first
      btnPlay.Content = "Sto_p"
      _AnimControl.Play()
    End If
  End Sub
  Sub StopPlay()
    If _AnimControl._timer.IsEnabled Then
      btnPlay.Content = "_Play"
      _AnimControl._timer.IsEnabled = False
    End If
  End Sub
  Sub btnDemo_Click() Handles btnDemo.Click
    StopPlay()
    Dim varyThickness = Keyboard.IsKeyDown(Key.LeftCtrl)
    _AnimControl.Demo(varyThickness)
    btnPlay_Click()
  End Sub
  Sub btnErase_Click() Handles btnErase.Click
    _AnimControl.EraseBtn()
  End Sub
  Sub btnReset_Click() Handles btnReset.Click
    StopPlay()
    _AnimControl.Reset()
  End Sub

  Friend Sub RefreshStatus()
    Me.txtStatus.Text = $"Between={CInt(sldBetween.Value),4:n0}  Frame count={_AnimControl._UserFrameList.Count} #Lines={_AnimControl._CurLineList.Count} CurFrame={_AnimControl._ndxUserFrame}"
  End Sub
End Class

Public Class AnimControl
  Inherits FrameworkElement
  Friend WithEvents _timer As New DispatcherTimer
  Friend _rand = New Random()
  Private _MainWindow As MainWindow
  Friend _ndxUserFrame As Integer   ' index into user created frames. 
  Private _ndxBetween As Integer  ' from 0 to nBetween
  Private _ptCurrent As Point?
  Private _ptOld As Point?
  Private _fPenDown As Boolean
  Private _PenModeDrag As Boolean = True ' either create line segs, or continuous drag to create multiple segs
  ' lines to draw for current image: could be while composing, or playing.
  Friend _CurLineList As New List(Of cFrameLine)
  Public Const ThicknessDefault = 10
  Public Const ThicknessMax = 50
  Public Const InBetweenDefault = 100

  'Frames stored by user
  Friend _UserFrameList As New List(Of cCartoonFrame)
  Sub New(w As MainWindow)
    _MainWindow = w
  End Sub
  Sub Reset()
    Me._MainWindow.sldBetween.Value = InBetweenDefault
    Me._UserFrameList.Clear() ' erase all user data
    EraseBtn()
  End Sub
  Sub EraseBtn() ' erase current frame
    _CurLineList.Clear()
    Me._ptOld = Nothing
    Me._fPenDown = False
    Me.InvalidateVisual()
  End Sub
  Sub Demo(varyThickness As Boolean)
    Reset()
    Dim nFrames = 20
    Dim numObjects = 2
    Dim numSegsPerObject = 1000
    Dim fForce = Me.ActualHeight / 5
    Dim pow = 2
    Dim wallBound = 15
    For nFrame = 0 To nFrames - 1
      Dim nInitSpeed = 4
      For nObject = 0 To numObjects - 1
        Dim pos0 As New Point With {
            .X = _rand.Next(Me.ActualWidth),
            .Y = _rand.Next(Me.ActualHeight)
        }
        Dim startPos = pos0
        Dim vel As New Vector With {
            .X = _rand.NextDouble * nInitSpeed,
            .Y = _rand.NextDouble * nInitSpeed
        }
        Dim thickness = AnimControl.ThicknessDefault
        For nSeg = 0 To numSegsPerObject - 2
          Dim pos1 = Point.Add(pos0, vel)
          If varyThickness Then
            thickness = 1 + AnimControl.ThicknessMax * nSeg / numSegsPerObject
          End If
          Me._CurLineList.Add(New cFrameLine(pos0, pos1, thickness))
          ' all 4 walls exert a force proportional to inverse square of distance
          Dim dWest = Math.Max(pos1.X, wallBound) ' displacement 
          Dim accWest = fForce / dWest ^ pow
          Dim dEast = Math.Max(Me.ActualWidth - pos1.X, wallBound)
          Dim accEast = -fForce / dEast ^ pow
          Dim dNorth = Math.Max(pos1.Y, wallBound)
          Dim accNorth = fForce / dNorth ^ pow
          Dim dSouth = Math.Max(Me.ActualHeight - pos1.Y, wallBound)
          Dim accSouth = -fForce / dSouth ^ pow
          Dim accEastWest = (accEast + accWest) * _rand.NextDouble * 20
          Dim accNorthSouth = (accNorth + accSouth) * _rand.NextDouble * 20
          Dim accel = New Vector(accEastWest, accNorthSouth)
          vel = Vector.Add(vel, accel)
          pos0 = pos1 ' next line segment starts at end of cur seg
        Next
        ' now add a final line segment that closes the curve
        Me._CurLineList.Add(New cFrameLine(pos0, startPos))
      Next
      Me._UserFrameList.Add(New cCartoonFrame(Me._CurLineList))
      Me._CurLineList.Clear() ' reset for next frame
    Next
  End Sub
  Sub NewFrame()
    If _CurLineList.Count > 0 Then
      Dim curFrame = New cCartoonFrame(_CurLineList)
      _UserFrameList.Add(curFrame)
      EraseBtn()
    End If
  End Sub

  Friend Sub Play()
    If _UserFrameList.Count < 2 Then
      MsgBox("Need at least 2 frames to animate")
      Return
    End If
    If _timer.IsEnabled Then ' if we're already playing, stop
      _timer.IsEnabled = False
    Else
      _timer.Interval = New TimeSpan(0, 0, 0, 0, 50) ' days,hrs,mins,secs,msecs
      _timer.IsEnabled = True
    End If
    Me._fPenDown = False
    Me._ndxUserFrame = 0
    Me._ndxBetween = 1 ' 1st is drawn now, next by timer tick
    Me._CurLineList.Clear()
    Me._CurLineList.AddRange(Me._UserFrameList(0)._Lines) 'get the 1st frame
    Me.InvalidateVisual() ' show it
  End Sub
  Sub tmr_tick() Handles _timer.Tick ' let's do the animating
    If _ndxUserFrame = Me._UserFrameList.Count Then ' restart at last frame
      Me._ndxUserFrame = 0
      Me._ndxBetween = 0
    End If
    Me._CurLineList.Clear()
    Dim frmLeft = Me._UserFrameList(Me._ndxUserFrame) ' the frame on the left
    Dim ndxFrmRight = Me._ndxUserFrame + 1
    If ndxFrmRight = Me._UserFrameList.Count Then
      ndxFrmRight = 0 ' wrap around to first frame
    End If
    Dim frmRight = Me._UserFrameList(ndxFrmRight)
    Dim nBetween = CInt(_MainWindow.sldBetween.Value) + 1
    Dim fnInterpolate = Function(lVal, rVal) lVal + Me._ndxBetween * (rVal - lVal) / nBetween
    Dim nLinesToDraw = Math.Max(frmLeft._Lines.Count, frmRight._Lines.Count) - 1
    Dim pt0, pt1 As Point

    For ndx = 0 To nLinesToDraw ' calc the lines to draw
      Dim lineLeft = frmLeft._Lines(Math.Min(ndx, frmLeft._Lines.Count - 1))
      Dim lineRight = frmRight._Lines(Math.Min(ndx, frmRight._Lines.Count - 1))

      Dim thickness = fnInterpolate(lineLeft.thickness, lineRight.thickness)
      pt0.X = fnInterpolate(lineLeft.pt0.X, lineRight.pt0.X)
      pt0.Y = fnInterpolate(lineLeft.pt0.Y, lineRight.pt0.Y)
      pt1.X = fnInterpolate(lineLeft.pt1.X, lineRight.pt1.X)
      pt1.Y = fnInterpolate(lineLeft.pt1.Y, lineRight.pt1.Y)

      Dim newLine = New cFrameLine(pt0, pt1, thickness)
      Me._CurLineList.Add(newLine)
    Next
    If Me._ndxBetween >= nBetween Then ' we've reached the right
      Me._ndxUserFrame += 1 ' advance to next user frame 
      Me._ndxBetween = 0
    End If
    Me._ndxBetween += 1 ' advance to next frame
    Me.InvalidateVisual()
  End Sub
  Protected Overrides Sub OnRender(drawingContext As DrawingContext)
    drawingContext.DrawRectangle(
        Brushes.AliceBlue,
        New Pen(Brushes.Purple, 1),
        New Rect(0, 0, Me.RenderSize.Width, Me.RenderSize.Height))
    Dim colorVal = &H0
    Dim colorDelta = IIf(_timer.IsEnabled, Me._CurLineList.Count, 50)
    For Each lin In Me._CurLineList ' draw the lines in the current frame
      Dim c = Color.FromRgb((colorVal / 256) And &HFF,
                            (colorVal / 256 / 256) And &HFF,
                            colorVal And &HFF)
      Dim brush = New SolidColorBrush(c)

      Dim oPen = New Pen(brush, lin.thickness)
      colorVal = (colorVal + 10 * colorDelta) Mod &HFFFFFF
      drawingContext.DrawLine(oPen, lin.pt0, lin.pt1)
    Next
    If Me._fPenDown Then
      If Me._ptOld.HasValue Then
        drawingContext.DrawLine(
            New Pen(Brushes.Black, 2),
            Me._ptOld,
            Me._ptCurrent)
      End If
    End If
    _MainWindow.RefreshStatus()
  End Sub
  Protected Overrides Sub OnMouseDown(e As MouseButtonEventArgs)
    If e.RightButton = MouseButtonState.Pressed Then
      Me._PenModeDrag = Not Me._PenModeDrag ' toggle modes on right click
    Else
      If Me._PenModeDrag Then
        Me._ptOld = e.GetPosition(Me)
      Else
        If e.RightButton = MouseButtonState.Pressed Then
          Me._fPenDown = False
          Me._ptOld = Nothing
        Else
          Me._fPenDown = True
          Me._ptCurrent = e.GetPosition(Me) ' get cur pos rel to self
          If Not Me._ptOld.HasValue Then
            Me._ptOld = Me._ptCurrent ' same
          End If
          Me.InvalidateVisual()
        End If
      End If
    End If
  End Sub

  Protected Overrides Sub OnMouseMove(e As MouseEventArgs)
    If Me._PenModeDrag Then
      If e.LeftButton = MouseButtonState.Pressed Then
        If Me._ptOld.HasValue Then
          Me._ptCurrent = e.GetPosition(Me)
          Dim thickNesss = AnimControl.ThicknessDefault
          If e.StylusDevice IsNot Nothing Then
            Dim pts = e.StylusDevice.GetStylusPoints(e.StylusDevice.Target)
            Dim avgPressure = Aggregate pt In pts Into Average(pt.PressureFactor)
            thickNesss = AnimControl.ThicknessMax * avgPressure
          End If
          Dim newFrameLine = New cFrameLine(Me._ptOld, Me._ptCurrent, thickNesss)
          Me._CurLineList.Add(newFrameLine)
          Me._ptOld = Me._ptCurrent
          Me.InvalidateVisual()
        End If
      End If
    Else
      If Me._fPenDown Then
        If Me._ptOld.HasValue Then
          Me._ptCurrent = e.GetPosition(Me)
        End If
        Me.InvalidateVisual()
      End If
    End If
  End Sub
  Protected Overrides Sub OnMouseUp(e As MouseButtonEventArgs)
    If Me._fPenDown Then
      Me._ptCurrent = e.GetPosition(Me) ' get cur pos rel to self
      Dim newFrameLine = New cFrameLine(Me._ptOld, Me._ptCurrent)
      Me._CurLineList.Add(newFrameLine)
      Me._ptOld = Me._ptCurrent
      Me._fPenDown = False
      Me.InvalidateVisual()
    End If
  End Sub
  Protected Overrides Sub OnMouseWheel(e As MouseWheelEventArgs)
    If Me._timer.IsEnabled Then ' only when playing
      Me._MainWindow.sldBetween.Value += If(e.Delta > 0, 10, -10)
    End If
  End Sub

  <DebuggerDisplay("{ToString()}")>
  Public Class cCartoonFrame ' User created cartoon frame
    Public ReadOnly _Lines As New List(Of cFrameLine) ' a frame is a list of lines
    ' # of frames to gen between real user frames
    Sub New(lst As List(Of cFrameLine))
      _Lines.AddRange(lst)
    End Sub
    Public Overrides Function ToString() As String
      Return String.Format("LineCount = {0}", _Lines.Count)
    End Function
  End Class
  'A line to be animated. Could belong to a real or gen'd user frame, or while user is actively drawing
  <DebuggerDisplay("{ToString()}")>
  Public Class cFrameLine ' User created cartoon line. It's just 2 points.
    Friend ReadOnly pt0 As Point
    Friend ReadOnly pt1 As Point
    Friend ReadOnly thickness As Double
    Sub New(pt0 As Point, pt1 As Point, Optional thickness As Double = AnimControl.ThicknessDefault)
      Me.pt0 = pt0
      Me.pt1 = pt1
      Me.thickness = thickness
    End Sub
    Public Overrides Function ToString() As String
      Return String.Format("Line = ({0:n0}, {1:n0}) ({2:n0}, {3:n0}) {4:n3}", pt0.X, pt0.Y, pt1.X, pt1.Y, (pt1 - pt0).Length)
    End Function
  End Class
End Class

</Code>