v0.1.1 — structural renderer with safe area and 40x24 grid baseline

This commit is contained in:
Stu Leak 2025-11-03 10:29:05 -05:00
parent 4dab3c8015
commit fa33334823
9 changed files with 132 additions and 48 deletions

Binary file not shown.

Binary file not shown.

BIN
assets/fonts/Modeseven.ttf Executable file

Binary file not shown.

16
config/config.json Normal file
View File

@ -0,0 +1,16 @@
{
"Mode": "Broadcaster",
"ScreenWidth": 800,
"ScreenHeight": 600,
"Font": {
"Name": "Modeseven",
"Path": "assets/fonts/Modeseven.ttf",
"Size": 18
},
"ShowGrid": false,
"Colours": {
"Header": "blue",
"Footer": "red",
"Background": "black"
}
}

23
main.py
View File

@ -1,22 +1,23 @@
"""
Telefact minimal runner
- Opens a 4:3 800×600 window
- Renders header (blue band) and footer (red band)
- No formatter yet; thats next.
"""
import tkinter as tk
from src.config_manager import ConfigManager
from src.telefact_renderer import TelefactRenderer
from src.core.telefact_frame import TelefactFrame
def main():
config = ConfigManager().config
root = tk.Tk()
root.title("Telefact — Broadcaster Prototype")
root.title(f"Telefact — Broadcaster ({config['Mode']})")
renderer = TelefactRenderer(
root,
width=config["ScreenWidth"],
height=config["ScreenHeight"],
show_grid=config.get("ShowGrid", False),
font_path=config["Font"]["Path"]
)
renderer = TelefactRenderer(root, width=800, height=600, show_grid=False)
frame = TelefactFrame()
# put a couple test glyphs so you can confirm alignment quickly
for i, ch in enumerate("TELEFACT"):
frame.set_cell(2 + i, 2, ch, "yellow")
for i, ch in enumerate("BROADCASTER BASE"):

Binary file not shown.

22
src/config_manager.py Normal file
View File

@ -0,0 +1,22 @@
import json
import os
class ConfigManager:
def __init__(self, config_path: str = "config/config.json"):
self.config_path = config_path
self.config = self._load()
def _load(self):
if not os.path.exists(self.config_path):
raise FileNotFoundError(f"Config file not found: {self.config_path}")
with open(self.config_path, "r", encoding="utf-8") as f:
return json.load(f)
def get(self, key, default=None):
"""Return a top-level config key, e.g. 'Font' or 'Mode'."""
return self.config.get(key, default)
def save(self):
"""Write any changes back to disk."""
with open(self.config_path, "w", encoding="utf-8") as f:
json.dump(self.config, f, indent=2)

View File

@ -1,10 +1,13 @@
"""
Telefact Renderer (Tkinter)
- Renders a TelefactFrame to a Canvas.
- Renders a 40×24 Telefact grid within a safe-area margin.
- No header/footer bands; for layout calibration only.
- Pure Python (stdlib only).
"""
import os
import tkinter as tk
from tkinter import font as tkfont
from typing import Dict
from src.core.telefact_frame import TelefactFrame
@ -19,74 +22,116 @@ PALETTE: Dict[str, str] = {
"white": "#FFFFFF",
}
class TelefactRenderer:
def __init__(self, root: tk.Tk, width: int = 800, height: int = 600, show_grid: bool = False):
def __init__(
self,
root: tk.Tk,
width: int = 880,
height: int = 660,
show_grid: bool = True,
colors: dict | None = None,
font_path: str | None = None,
font_size: int = 18,
):
self.root = root
self.width = width
self.height = height
self.show_grid = show_grid
# 40×24 grid for Telefact pages
# Safe area for 40×24 grid (simulate CRT overscan)
self.cols, self.rows = 40, 24
self.cell_w = max(1, width // self.cols)
self.cell_h = max(1, height // self.rows)
self.w = self.cell_w * self.cols
self.h = self.cell_h * self.rows
self.margin_x = 24
self.margin_y = 24
self.inner_w = width - self.margin_x * 2
self.inner_h = height - self.margin_y * 2
self.cell_w = self.inner_w // self.cols
self.cell_h = self.inner_h // self.rows
self.w = self.inner_w
self.h = self.inner_h
# Colours
self.colors = {
"Background": "black",
"Grid": "#303030"
}
if colors:
self.colors.update(colors)
# Canvas covers the whole window
self.canvas = tk.Canvas(
root, width=self.w, height=self.h,
highlightthickness=0, bg=PALETTE["black"]
root,
width=self.width,
height=self.height,
highlightthickness=0,
bg=PALETTE.get(self.colors["Background"], self.colors["Background"]),
)
self.canvas.pack()
# simple monospace font; will swap for Teletext font later
self.font = ("Courier New", max(12, self.cell_h - 9), "bold")
# Font setup
self.font = self._load_font(font_path, font_size)
# ----- low-level drawing helpers -----
# ----------------------------------------------------------------------
def _load_font(self, font_path: str | None, font_size: int) -> tuple:
if font_path and os.path.exists(font_path):
try:
font_name = os.path.splitext(os.path.basename(font_path))[0]
try:
self.root.tk.call(
"font", "create", font_name, "-family", font_name, "-size", font_size
)
except tk.TclError:
pass
print(f"[info] Loaded Telefact font: {font_name}")
return (font_name, font_size)
except Exception as e:
print(f"[warn] Could not load custom font ({e}). Falling back to Courier New.")
elif font_path:
print(f"[warn] Font path not found: {font_path}. Using Courier New.")
return ("Courier New", font_size, "bold")
# ----------------------------------------------------------------------
def _gx(self, col: int) -> int:
return col * self.cell_w
return self.margin_x + col * self.cell_w
def _gy(self, row: int) -> int:
return row * self.cell_h
def _fill_row(self, row: int, color: str) -> None:
self.canvas.create_rectangle(
self._gx(0), self._gy(row), self._gx(self.cols), self._gy(row + 1),
fill=PALETTE.get(color, color), width=0
)
return self.margin_y + row * self.cell_h
def _draw_char(self, col: int, row: int, char: str, color: str) -> None:
"""Draw a single glyph."""
x = self._gx(col) + 2
y = self._gy(row) + self.cell_h // 2
self.canvas.create_text(
x, y, anchor="w", text=char,
font=self.font, fill=PALETTE.get(color, color)
x, y, anchor="w", text=char, font=self.font, fill=PALETTE.get(color, color)
)
def _draw_grid(self) -> None:
"""Debug overlay for exact cell spacing."""
for c in range(self.cols + 1):
x = self._gx(c)
self.canvas.create_line(x, 0, x, self.h, fill="#202020")
self.canvas.create_line(
x, self.margin_y, x, self.margin_y + self.h, fill=self.colors["Grid"]
)
for r in range(self.rows + 1):
y = self._gy(r)
self.canvas.create_line(0, y, self.w, y, fill="#202020")
self.canvas.create_line(
self.margin_x, y, self.margin_x + self.w, y, fill=self.colors["Grid"]
)
# ----- public API -----
# ----------------------------------------------------------------------
def render(self, frame: TelefactFrame) -> None:
"""Paint the whole frame to the canvas."""
"""Render just the text grid; no header/footer bars."""
self.canvas.delete("all")
# background
self.canvas.create_rectangle(0, 0, self.w, self.h,
fill=PALETTE["black"], width=0)
# Fill total background
self.canvas.create_rectangle(
0, 0, self.width, self.height,
fill=PALETTE.get(self.colors["Background"], self.colors["Background"]),
width=0,
)
# header/footer colour bands
for r in frame.header_region():
self._fill_row(r, "blue")
for r in frame.footer_region():
self._fill_row(r, "red")
# draw text cells
# Draw Teletext cells
for row in range(frame.rows):
for col in range(frame.cols):
ch, fg = frame.grid[row][col]