In my previous post on implementing Bézier curves on the Vectrex, I mentioned a piece of hardware I was waiting for before I could pause my series dedicated to this great vintage gaming console. The device I was referring to is a lightpen. A lightpen is a pointing device in the shape of a pen that allows the user to interact with the screen the same way as we do today with a touch screen. At the cost of extra code to handle such an input device, application developers can offer a new experience to their users. On the Vectrex, using the lightpen, one could, for example, sketch or compose music without using the joystick. This was an uncommon feature in the ’80s. If I recall well, only Thomson shipped its TOx home computers in standard with such a device.

User-Friendly

My lightpen arrived today in the mail so that I could re-work my Bézier curve demo program for Vextrex32. This first demo displayed a single Bézier curve and moved programmatically pre-selected anchor and control points to affect in real-time the shape of the curve. With today’s rework, I wanted to allow the user to select and move any points on the screen with the lightpen. However, nothing should prevent me to do the same using the joypad. To introduce a single change at the time, I started re-writing the code to do exactly that. First, we move a newly added cursor with the joystick close to a point of interest. Then, by pressing the third button of the joypad, we select or unselect the point. A selected point is moved alongside the cursor, and the affected Bézier curve is updated in real-time. Like any modern vector illustration software! The new code works as expected and provides excellent control to the user. To change dynamically the number of points used to compute each Bézier curve we use the first two buttons (instead of moving the joystick up or down as initially).

IMG_4849

No Driver, No BIOS

If you watch the videos where I use the lightpen and the Art Master application to draw my preferred bunny, you may see how poor the precision of the lightpen is. Undeniably, points and line segments don’t cut muster. In contrast, using the lightpen with my new Bézier program should offer the best of both worlds, right? Well, not really. Why? Because the Vectrex32 cannot handle the lightpen! Initially, I tried to inject machine language instructions that could be executed by the 6809, so I can retrieve the position pointed by the lightpen via the dual-port memory. My assumption was that there must be a BIOS routine to do so. After all, several cartridges use the lightpen. I also contacted the Vectrex32 designer, just to check if I missed something obvious. Nay, nay and thrice Nay! And there is a good reason why. Native Vectrex applications are driving the lightpen directly. For example, when the lightpen detects light on the screen, it triggers an interrupt. This interrupt is used by the application to identify the line on which the lightpen sees the light. Then, to detect where on that line the pen has seen the light, the programs seem to use a complicated time measurement. Cumbersome, and likely impossible to handle from the Vectrex32, or to fit such complex code in the 1KB of the Vectrex.

What Next?

Nonetheless, the new version of the code proves that the Vectrex is precise enough to draw interactively complex shapes using Bézier curves. Unfortunately, the lightpen debacle is a throwback. Now, if a future version of the Vectrex32 adds support for the lightpen, I will undoubtedly update the code so that it can drive the cursor with the pen instead of the joystick. This change is trivial, as you can see, looking at the source code. What next? I will soon mod my GCE’s Vectrex hardware to fix an annoying design flaw that cripples the early versions of the console. Stay tuned, and enjoy!

' bezier2.bas: draws a Bezier curve, use joystick up & down top change draw step.

' Define display configuration.
scale = 80
intensity = 70
frame_rate = 100

' Define various UI configuration.
anchor_size = 4
control_size = 1
instructions = { _
   {-50, 100, "INSTRUCTIONS"}, _ 
   {-80, 90, "PRESS BUTTON 1 TO +1 BEZIER STEPS."}, _ 
   {-80, 80, "PRESS BUTTON 2 TO -1 BEZIER STEPS."}, _ 
   {-80, 70, "MIN STEP IS 1 - MAX STEP IS 20."}, _
   {-80, 60, "USE JOYSTICK TO MOVE CURSOR."}, _
   {-80, 50, "PRESS BUTTON 3 TO (UN)SELECT."}, _
   {-80, 40, "PRESS BUTTON 4 TO EXIT."} _
}

' Bn functions are respectively first, second, third and fourth 
' Bernstein derivations to compute the quadratic B-Spline.
function B1(t) 
   return ((t) * (t) * (t)) 
endfunction

function B2(t) 
   return (3 * (t) * (t) * (1 - (t)))
endfunction

function B3(t)
   return (3 * (t) * (1 - (t)) * (1 - (t)))
endfunction

function B4(t)
   return ((1 - (t)) * (1 - (t)) * (1 - (t)))
endfunction

sub draw_square(x, y, s)
   move = MoveSprite(x, y)
   square = LinesSprite( _
      { _
         { MoveTo, s, s }, _
         { DrawTo, -s, s}, _
         { DrawTo, -s, -s }, _
         { DrawTo, s, -s }, _
         { DrawTo, s, s } _
      } _
   )
   call ReturnToOriginSprite()
endsub

sub draw_cursor(x, y, s)
   call ScaleSprite(scale)
   move = MoveSprite(x, y)
   l = s/2
   square = LinesSprite( _
      { _
         { MoveTo, -l, 0}, _
         { DrawTo, l, 0 }, _
         { MoveTo, 0, -l }, _
         { DrawTo, 0, l } _
      } _
   )
   call ReturnToOriginSprite()
endsub

sub draw_cursor_selected(x, y, s)
   call ScaleSprite(scale)
   move = MoveSprite(x, y)
   l = s/2
   square = LinesSprite( _
      { _
         { MoveTo, -l, -l }, _
         { DrawTo, l, l}, _
         { MoveTo, l, -l }, _
         { DrawTo, -l, l } _
      } _
   )
   call ReturnToOriginSprite()
endsub

sub draw_segment(x1, y1, x2, y2)
   segment = LinesSprite( _
      { _
         { MoveTo, x1, y1 }, _
         { DrawTo + $F0, x2, y2} _
      } _
   )
   call ReturnToOriginSprite()
endsub

sub draw_bezier_curve(x1, y1, x2, y2, x3, y3, x4, y4, s)
   if s > 0.0 then 
      call ScaleSprite(scale)
      call ReturnToOriginSprite()
      call draw_square(x1, y1, anchor_size)
      call draw_square(x2, y2, control_size)
      call draw_segment(x1, y1, x2, y2)
      call draw_square(x3, y3, control_size)
      call draw_square(x4, y4, anchor_size)
      call draw_segment(x3, y3, x4, y4)
      i = 0.0
      lastx = x4
      lasty = y4
      d = 1.0 / s;
      curve = {{ MoveTo, x1, y1 }}
      repeat
         x = Int(x1 * B1(i) + x2 * B2(i) + x3 * B3(i) + x4 * B4(i))
         y = Int(y1 * B1(i) + y2 * B2(i) + y3 * B3(i) + y4 * B4(i))
         move = {{ MoveTo, lastx, lasty}, { DrawTo, x, y }}
         curve = AppendArrays(curve, move)
         i = i + d
         lastx = x
         lasty = y
      until i >= 1.0
      move = {{ DrawTo, x1, y1 }}
      curve = AppendArrays(curve, move)
      bezier = LinesSprite(curve)
   endif
endsub

' Default configuration.
s = 8
cx = 0
cy = 0
sd = 1
sil = 1
sul = 20
sel = 0
idc = 0
idp = 0
controls = {{0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0}}
dx = 2
dy = 2

points = { _
   {-80, 0}, {-40, 40}, {40, 40}, {80, 0}, _
   {80, 0}, {120, -40}, {120, -80}, {80, -120}, _
   {80, -120}, {40, -160}, {-40, -160}, {-80, -120}, _
   {-80, -120}, {-120, -80}, {-120, -40}, {-80, 0} _
}

' Build curves.
dim curves[0, 8]
curve = {{points[1, 1], points[1, 2], points[2, 1], points[2, 2], points[3, 1], points[3, 2], points[4, 1], points[4, 2]}}
curves = AppendArrays(curves, curve)
curve = {{points[5, 1], points[5, 2], points[6, 1], points[6, 2], points[7, 1], points[7, 2], points[8, 1], points[8, 2]}}
curves = AppendArrays(curves, curve)
curve = {{points[9, 1], points[9, 2], points[10, 1], points[10, 2], points[11, 1], points[11, 2], points[12, 1], points[12, 2]}}
curves = AppendArrays(curves, curve)
curve = {{points[13, 1], points[13, 2], points[14, 1], points[14, 2], points[15, 1], points[15, 2], points[16, 1], points[16, 2]}}
curves = AppendArrays(curves, curve)
curves_count = ubound(curves)

function cursor_pointing_to(cx, cy, x, y, s)
   f = 0
   if _
      x-s <= cx and _
      cx <= x+s and _
      y-s <= cy and _
      cy <= y+s _
    then
       f = 1
    endif
    return f
endfunction

' Driver code to demonstrate use of draw_bezier_curve().
repeat

   ' Draw cursor & move selected point if nay.
   if sel then
      call draw_cursor_selected(cx, cy, anchor_size)
      curves[idc, (idp * 2) - 1] = cx
      curves[idc, (idp * 2)] = cy
   else
      call draw_cursor(cx, cy, anchor_size)
   endif

   ' Display instructions.
   textSize = {25, 4}
   call TextSizeSprite(textSize) 
   call TextListSprite(instructions)

   ' Display current steps.
   textSize = {40, 5}
   current_steps = { _
      { _
         -50, 130, _
         "STEPS: " + s + ". " _
         + controls[1, 1] + ", " _
         + controls[1, 2] + ", " _
         + idc + ", " _
         + idp _
      } _
   } 
   call TextSizeSprite(textSize) 
   call TextListSprite(current_steps)

   ' Setup display for the Bezier curve.
   call IntensitySprite(intensity)
   call SetFrameRate(frame_rate)
   call ScaleSprite(scale)

   ' Walk the curves array.
   for i = 1 to ubound(curves)
      call draw_bezier_curve( _
         curves[i, 1], _
         curves[i, 2], _
         curves[i, 3], _
         curves[i, 4], _
         curves[i, 5], _
         curves[i, 6], _
         curves[i, 7], _
         curves[i, 8], _
         s, _
      )
   next i

   ' Get user input
   controls = WaitForFrame( _
      JoystickDigital, _
      Controller1 + Controller2, _ 
      JoystickX + JoystickY _
   )

   ' Process user input (buttons first).
   if controls[1, 3] then
      s = s + sd
      if s > sul then 
         s = sul 
      endif
   elseif controls[1, 4] then
      s = s - sd
      if s < sil then 
         s = sil
      endif
   elseif controls[1, 5] then
      if sel then
         sel = 0
         idc = 0
         idp = 0
      else
         c = 1
         found = 0
         while found = 0 and c <= curves_count 
            p = 1
            while found = 0 and p <= 4 
               idx = cursor_pointing_to( _
                  cx, cy, _
                  curves[c, (p * 2) - 1], _
                  curves[c, (p * 2)], _
                  10 _
               )
               if idx then
                  found = 1
                  sel = 1
                  idc = c
                  idp = p
               endif
               p = p + 1
            endwhile
            c = c + 1
         endwhile
      endif 
   endif

   ' Move cursor using joystic input.
   if controls[1, 1] then
      if controls[1, 1] < 0 then
         cx = cx - dx
      else
         cx = cx + dx
      endif 
   endif
   if controls[1, 2] then
      if controls[1, 2] < 0 then
         cy = cy - dx
      else
         cy = cy + dx
      endif 
   endif

   call ClearScreen()
   call ReturnToOriginSprite()

until controls[1, 6] 
stop