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).
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
First congrats on your Vectrex, I really do enjoy the series!
Regarding lightpen handling: shouldn’t it be possible to have just a basic interrupt routine for the lightpen in the cartridge ROM and store coordinates in RAM, to be fetched later by the code in the Vectrex32 in some kind of event loop?
Norbert
Thank you, Norbert. Your suggestion sounds interesting. What I do not know is how the Vectrex32’s current ROM is written – the part that executes the code injected into the dual-port memory to draw the ”sprites”. But, if it can handle the interrupt generated by the lightpen, your idea should work. On the PIC side, we could pool the data at each frame, the way it does for the controllers. That would be precise enough for the type of applications the pen is used.
How much I’d like to have a Vectrex (just for Scramble), I really don’t know too much about it and nothing about Vectrex32. So, please apologize any crude misconceptions. – Is this 1K of RAM anything that is facing the Vectrex as a cartridge ROM or is there any way to define a set of calls to be sent from the Vectrex32, which may be abused to inject some code? (Eg, if there is the equivalent of an FN statement to define some machine code, this may be interesting. Or something like the facility of BBC BASIC to include machine code directly in the BASIC code. But then, we probably don’t know the address this code is running at, which is, on the other hand, a must for pointing any interrupt vectors at this…)
Anyway, a cool application!
I hear you, Norbert. To my discharge, not everything in collecting is rational 🙂 Regarding your comment/question, it is the latter. There is a 2KB dual-ported memory in the Vectrex32 cartridge that is used by the PIC to inject, at runtime, “code” into, based upon the program you wrote on the PC and uploaded to the cartridge. This “code” a.k.a. sprites are essentially graphic paths (a list of vectors to display if you prefer) will be translated later into calls to graphics routines – but sprites can also be raw machine language to be executes as-is – executes by the Vectrex’s 6809. This translation and execution is what I call an ”engine”. It translates and executes the “code” from the 2KB of shared memory. The 1KB traditional RAM of the Vectrex can be used by the translated code. It is pretty cool and works fine, without any change done to the Vectrex. To link this to your idea regarding the lightpen handling, the interrupt routine should live and execute in the ”engine”. What I really like in thsi, is that it is possible to use the Vectrex gaming console as a computer! A capability that was never released – possibly never developed – but mentioned by the manufacturer.