EN /HU | Login

Art and graphics screens

The pieces of a picture-led board, such as the TTG Clubhouse look of the Teletype BBS: drawings kept as readable YAML and drawn in 256-colour ANSI on the modern face and as raw PETSCII on the Commodore 64 face; a cursor menu on the C64 picture; widgets whose hotspots show the board's life; and the time of day and the season for choosing a picture's layers. Available since 0.30.0; every class is in the API reference.

Piece What it does
BBS::Petscii::Canvas The C64 screen as 40×25 cells in the uppercase/graphics set, out as raw PETSCII: a whole frame, then patches
BBS::Art A YAML drawing: chars, colours, reverse, backgrounds, hotspots, layers; out to ANSI, a FrameBuffer or a Canvas
BBS::Petscii::CanvasMenu BBS::Selector's keys and items, drawn on a Canvas
BBS::Widgets::Scene An Art as a modern-face widget, its hotspots filled from data
BBS::Season Dawn, day, dusk or night by the sun; snow, pumpkin, fireworks, bunting
art-preview Checks a drawing and shows it: ANSI, PETSCII bytes, a C64 screenshot

The C64 canvas

The C64 has two character sets. The lowercase set, which session.petscii! and BBS::Petscii::Screen speak, has letters of both cases but no graphics. The uppercase set has the card suits, balls, rounded corners, diagonals and quarter blocks, so a picture is drawn there. BBS::Petscii::Canvas holds the screen as cells and writes raw PETSCII, which goes out with session.write_raw, past the lowercase encoder:

RUBY
C = BBS::Petscii::Canvas
canvas = C.new
canvas.text(0, 1, 'TTG CLUBHOUSE', :yellow)
canvas.text(2, 1, '╭──╮', :light_blue)
canvas.text(3, 1, '█▀▀█', :orange)                 # reversed glyphs work too
session.write_raw(canvas.frame)                    # the whole screen

before = canvas.dup
canvas.text(2, 2, '●', :red)
session.write_raw(canvas.update(before))           # a patch: only the cell that changed
  • Cells are drawn from Unicode: letters (shown as capitals), digits and signs as they are, the graphics by what they look like (Canvas::GLYPHS); █ ▀ ▐ ◢ ◣ are reversed codes (GLYPHS_REVERSED). Canvas.glyph?(ch) tells whether the set can show a character.
  • Colours are the codes (Canvas::YELLOW), names (:light_blue, 'dark grey') or, since 1.0.0, colours of the palette: Canvas.color(YE) == Canvas::YELLOW, so the same BBS::Petscii::Colors constants serve a Screen and a canvas.
  • Since 1.0.0 there is one PETSCII glyph table: the rounded corners ╭╮╰╯ are Petscii::ROUNDED_CORNERS in this uppercase set, and the quarter blocks ▖▗▘▝▚ are drawn too; the lowercase set of a Screen shows the corners square.
  • frame sends the screen once, each row cut after its last visible cell; span(row, col, width) one stretch of a row; patch(positions) scattered cells, the cursor stepping to the nearest; update(before) the cheaper of a patch and a frame. On a slow line, aim for about 1.2 KB a screen and under 200 bytes an animation step.
  • feed(bytes) plays PETSCII onto a canvas as the C64's screen editor prints it: in a test, what the caller sees after your bytes (canvas.changes(seen) is empty when they agree).

The canvas came from TTG Gate BBS; its bytes are the same as the gate's own class gave.

Drawings as YAML

A drawing is text, written and reviewed like code. chars is the picture, colors a palette letter a cell (. = no colour), reverse optional Rs, background optional (cp437 only: the second colour of a half block), hotspots the cells the board fills from its data, layers other files stacked on it. Every row is exactly size[0] wide, trailing spaces too. Write the grids with |2: without the indentation indicator YAML takes the indentation from the first row, and a row that starts with spaces breaks the block.

A PETSCII drawing for the C64 face:

YAML
# art/house.yml
size: [20, 6]
charset: petscii-upper     # only glyphs the uppercase set shows
palette:                   # C64 colour names, 256-colour indexes or '#rrggbb'
  y: yellow
  r: red
  b: brown
  g: dark grey
  w: white
chars: |2
       ◢████◣    ●    
      ◢██████◣        
      │ ▒  ▒ │        
      │ ▒  █ │  TTG   
  ▁▁▁▁│▁▁▁▁█▁│▁▁▁▁▁▁▁ 

colors: |2
  .....rrrrrr....y....
  ....rrrrrrrr........
  ....b.y..y.b........
  ....b.y..b.b..www...
  ggggbggggbgbggggggg.
  ....................
hotspots:
  window_1: [6, 2]         # [col, row], 0-based
  window_2: [9, 2]
  sign: [14, 3, 3, 1]      # [col, row, width, height]
layers: [snow]             # art/snow.yml

A layer: spaces are transparent, at places it, its palette and hotspots add to the drawing's.

YAML
# art/snow.yml: a layer; spaces are transparent, 'at' places it
size: [10, 1]
at: [4, 0]
palette:
  w: white
chars: |2
   ▄▄▄▄▄▄   
colors: |2
  .wwwwww...

A cp437 drawing for the modern face, with backgrounds:

YAML
size: [24, 4]
charset: cp437
palette: { y: yellow, o: 208, n: '#203060', w: white }
chars: |2
  ╔══════════════════════╗
  ║ ▄▀▀▄ TTG CLUBHOUSE ☼ ║
  ║ ▀▄▄▀  open tonight   ║
  ╚══════════════════════╝
colors: |2
  oooooooooooooooooooooooo
  oyyyyywwwwwwwwwwwwwwwyyo
  oyyyyyyyyyyyyyyyyyyyyyyo
  oooooooooooooooooooooooo
background: |2
  ........................
  .nnnnnnnnnnnnnnnnnnnnnn.
  .nnnnnnnnnnnnnnnnnnnnnn.
  ........................
hotspots:
  tonight: [8, 2, 13, 1]

Using them:

RUBY
house = BBS::Art.load('art/house.yml').with(*BBS::Season.now.layers)  # only the layers it lists
session.write(house.ansi)                        # modern: 256-colour ANSI lines
house.draw(frame, x: 1, y: 2)                    # … or into a BBS::FrameBuffer
session.write_raw(house.petscii)                 # C64: a whole frame
house.draw_canvas(canvas, row: 1, col: 10)       # … or onto a canvas
house.hotspot(:window_1)                         # => Art::Hotspot col=6 row=2 …

with stacks only the layers the file lists, so with(*season.layers) is safe for any drawing. A mistake raises BBS::Art::Error naming the file, the grid, the row and the column: a row of the wrong width, a letter not in the palette, a glyph the charset cannot show, a hotspot outside the drawing. Palette colours are C64 names, 256-colour indexes or #rrggbb; each is matched to the nearest colour of the other face, so a PETSCII drawing previews in ANSI and the reverse.

A cursor menu on the canvas

BBS::Petscii::CanvasMenu is a BBS::Selector that draws on a canvas: the same items, keys and options (CRSR, RETURN, an item's key, RUN/STOP or ← to go back, numbers, columns, pages, the about and hint rows), the lit item a reversed bar in its own colour. Each move goes out as a patch of the changed cells: about 140 bytes in a 16-item, two-column menu.

RUBY
S = BBS::Selector
items = [S.item('CORK BOARD', key: 'm', value: :boards, color: :light_blue, about: 'READ AND POST'),
         S.item('GAMES ROOM', key: 'r', value: :doors, color: :orange),
         S.item('LEAVE',      key: 'q', value: :quit)]
menu = BBS::Petscii::CanvasMenu.new(session, items, canvas: canvas, row: 12, col: 1, width: 36,
                                    columns: 2, hint: 'CRSR: CHOOSE  RETURN: OPEN', idle: 0.5) do
  smoke.step(canvas)                 # bytes for an animation step while the caller thinks
end
menu.paint
session.write_raw(canvas.frame)      # the picture with the menu on it
choice = menu.run(drawn: true)       # without drawn: it paints and sends its rows itself

With idle: seconds and a block, the block is called while the caller thinks and returns the bytes of an animation step; a key stops waiting at once.

The menu's colour roles are the Selector's, BBS::Selector::COLORS (text key bar heading about hint), turned into C64 codes on the canvas; colors: overrides some of them. (CanvasMenu::COLORS is gone in 1.0.0.)

Scenes on the modern face

BBS::Widgets::Scene is a drawing as a widget: the drawing, then each hotspot filled by its block at every render. A block returns a String (ANSI colours allowed, lines for a taller hotspot) or nil to leave the drawing; what it returns is cut to its hotspot.

RUBY
scene = BBS::Widgets::Scene.new(art: house, align: :center)
scene.fill(:window_1) { node_busy?(1) ? "\e[38;5;220m▓" : nil }
scene.fill(:sign)     { |spot| "\e[1;37m#{inside_count}".ljust(spot.width) }

On a terminal smaller than the drawing the scene is cut to its bounds; with crop: false it draws nothing, and fits? tells a dashboard whether to show the picture at all.

Time of day and season

BBS::Season.now(tz: 'Europe/Budapest', at: Time.now, jam: nil) gives the time of day by the sun (:dawn and :dusk are the 45 minutes either side of sunrise and sunset, worked out for latitude: and longitude:, Budapest by default), the season, and the layers in effect:

Layer When
:snow December to February
:pumpkin 24–31 October
:fireworks 31 December, and New Year's Day until 06:00
:bunting the days of jam:, a Range of Date
RUBY
s = BBS::Season.now(jam: Date.new(2027, 1, 8)..Date.new(2027, 1, 10))
s.time_of_day          # => :night
s.layers               # => [:snow]
art = BBS::Art.load("art/house_#{s.night? ? 'night' : 'day'}.yml").with(*s.layers)

It is pure: a test passes at:. EU time zones and fixed offsets ('+01:00') work without other gems; any other IANA name needs tzinfo.

art-preview

An executable of the gem (bundle exec art-preview, or bin/art-preview in a rubbs checkout) for checking a drawing while drawing it:

TXT
art-preview art/house.yml                   # ANSI here, then size, palette, hotspots, PETSCII bytes
art-preview art/house.yml -l snow -s        # a layer stacked, the hotspots marked
art-preview art/house.yml --hex             # the PETSCII frame as a hex dump
art-preview art/house.yml --petscii h.seq   # the raw frame, e.g. for a C64 terminal program
art-preview art/house.yml --png h.png       # a C64 screenshot of the frame (VICE's character ROM)

--png plays the PETSCII frame onto a canvas and draws it with the C64 character ROM that VICE installs (--chargen FILE or $C64_CHARGEN for another); the ROM is not part of the gem. For a check in the emulator itself, load the --petscii file into a C64 terminal program, or watch the board in VICE.