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 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:
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
Canvas::GLYPHS); █ ▀ ▐ ◢ ◣ are reversed codes (GLYPHS_REVERSED). Canvas.glyph?(ch) tells whether the set can show a character.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.╭╮╰╯ 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.
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:
# 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.
# 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:
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:
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.
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.
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.)
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.
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.
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 |
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.
An executable of the gem (bundle exec art-preview, or bin/art-preview in a rubbs checkout) for checking a drawing while drawing it:
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.