Replicating QB's box-drawing algorithm

Post your FreeBASIC source, examples, tips and tricks here. Please don’t post code without including an explanation.
Post Reply
counting_pine
Site Admin
Posts: 6323
Joined: Jul 05, 2005 17:32
Location: Manchester, Lancs

Replicating QB's box-drawing algorithm

Post by counting_pine »

When drawing a line or box in QB, an optional 'style' parameter can be used. This contains a 16-bit repeating pattern, that controls which pixels of the line are set.

A box is made up of up to four lines. With no style, it can only be drawn one way (just a solid border), but when there is a style, it is hard to predict which pixels are set, because it depends on both the direction and starting point of the lines when drawing the pattern.

QBasic has a specific system for drawing them, but there are a lot of factors to consider:
1. the order the lines are drawn
2. the direction they are drawn in
3. where the pattern starts for each line
4. how the lines are affected by clipping

After some experimenting, here are my findings:
1. The horizontal lines (y2 then y1) are drawn before the vertical lines (x2 then x1) - this is not quite the order you'd expect
2. The lines are drawn in the expected direction (horizontal: x1 to x2, vertical: y1 to y2)
3. After each line, the pattern's rotation is remembered - so if the first line is 10 pixels long, the pattern is rotated left by 10 for the second line. (This is the reason why it matters which order the lines are drawn in.)
4. Lines are clipped as well as they can be. Lines that go offscreen have their length cropped. For lines that are entirely offscreen, they are not drawn at all. This affects the pattern rotation.

There is an additional quirk with line clipping: if x2 is offscreen, then x1 and x2 are swapped. Similarly, if y2 is offscreen, then y1 and y2 are swapped. (No swapping seems to occur when x1 or y1 are offscreen.)
Swapping the positions has important implications for the order and the direction of the lines drawn above.

I've replicated the algorithm in this code:

Code: Select all

'$lang: "qb"
defint a-z

function rol (style as integer, n as integer)' as integer
  dim i as integer, ret as integer
  ret = style
  for i = 1 to (-n and 15)
    ret = ((ret and &HFFFE) \ 2 and &H7FFF) or (&H8000 and -(ret and 1))
  next i
  rol = ret
end function

function clip (n as integer, l as integer, u as integer)' as integer
  if n < l then
    clip = l
  elseif n > u then
    clip = u
  else
    clip = n
  end if
end function

sub hline (x1 as integer, x2 as integer, y as integer, c as integer, style as integer)
  dim x1t as integer, x2t as integer
  if y < 0 or y >= 200 then exit sub

  x1t = clip(x1, 0, 319): x2t = clip(x2, 0, 319)

  line (x1t, y)-(x2t, y), c, , style
  style = rol(style, abs(x2t - x1t) + 1)

end sub

sub vline (x as integer, y1 as integer, y2 as integer, c as integer, style as integer)
  dim y1t as integer, y2t as integer
  if x < 0 and x >= 320 then exit sub

  y1t = clip(y1, 0, 199): y2t = clip(y2, 0, 199)

  line (x, y1t)-(x, y2t), c, , style
  style = rol(style, abs(y2t - y1t) + 1)

end sub

sub box (x1 as integer, y1 as integer, x2 as integer, y2 as integer, c as integer, style as integer)
  line (x1, y1)-(x2, y2), c, B, style
end sub

sub box2 (x1 as integer, y1 as integer, x2 as integer, y2 as integer, c as integer, style as integer)
  dim x1t as integer, y1t as integer
  dim x2t as integer, y2t as integer
  
  x1t = x1: x2t = x2
  y1t = y1: y2t = y2

  if x2 < 0 or x2 >= 320 then swap x1t, x2t
  if y2 < 0 or y2 >= 200 then swap y1t, y2t

  hline x1t, x2t, y2t, c, style
  hline x1t, x2t, y1t, c, style

  vline x2t, y1t, y2t, c, style
  vline x1t, y1t, y2t, c, style
end sub


screen 13

const SBITS = &hb800
dim x1 as integer, x2 as integer, y1 as integer, y2 as integer
dim k as string
dim t as single
x1 = 20: y1 = 20
x2 = 80: y2 = 80

do
  locate 1: print "WASD, IJKL, Shift for *16 speed, H/V to flip"
  k = inkey$

  box x1, y1, x2, y2, 0, SBITS
  'box2 x1 + 160, y1, x2 + 160, y2, 0, SBITS
  box2 x1, y1, x2, y2, 0, (not SBITS)

  select case k
  case "a": x1 = x1 - 1: case "A": x1 = x1 - 16
  case "d": x1 = x1 + 1: case "D": x1 = x1 + 16
  case "w": y1 = y1 - 1: case "W": y1 = y1 - 16
  case "s": y1 = y1 + 1: case "S": y1 = y1 + 16

  case "j": x2 = x2 - 1: case "J": x2 = x2 - 16
  case "l": x2 = x2 + 1: case "L": x2 = x2 + 16
  case "i": y2 = y2 - 1: case "I": y2 = y2 - 16
  case "k": y2 = y2 + 1: case "K": y2 = y2 + 16
  case "h", "H": swap x1, x2
  case "v", "V": swap y1, y2
  case chr$(27): exit do
  end select

  box x1, y1, x2, y2, 14, SBITS
  'box2 x1 + 160, y1, x2 + 160, y2, 14, SBITS
  box2 x1, y1, x2, y2, 12, (not SBITS)
  
  if k = "" then sleep

loop
The code is designed to run in QBasic. It draws a styled box with the given coordinates, and overlays it with a box drawn using my method, using the inverse of the style. So the expected result is that the two boxes combine to produce a solid border.

The code also runs in FB. FB actually covers the default case well (top-left to bottom-right, no clipping), but quickly breaks if the box clips outside the screen, or the coordinates swap positions.

EDIT: adding a screenshot:
Image
When the yellow and red lines complement each other with no gaps, that means it's working as expected.
Post Reply