v0.1.1 — structural renderer with safe area and 40x24 grid baseline
This commit is contained in:
parent
4dab3c8015
commit
fa33334823
BIN
assets/fonts/EuropeanTeletext.ttf
Normal file
BIN
assets/fonts/EuropeanTeletext.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/EuropeanTeletextNuevo.ttf
Normal file
BIN
assets/fonts/EuropeanTeletextNuevo.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Modeseven.ttf
Executable file
BIN
assets/fonts/Modeseven.ttf
Executable file
Binary file not shown.
16
config/config.json
Normal file
16
config/config.json
Normal 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
23
main.py
|
|
@ -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; that’s next.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
|
from src.config_manager import ConfigManager
|
||||||
from src.telefact_renderer import TelefactRenderer
|
from src.telefact_renderer import TelefactRenderer
|
||||||
from src.core.telefact_frame import TelefactFrame
|
from src.core.telefact_frame import TelefactFrame
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
config = ConfigManager().config
|
||||||
|
|
||||||
root = tk.Tk()
|
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()
|
frame = TelefactFrame()
|
||||||
|
|
||||||
# put a couple test glyphs so you can confirm alignment quickly
|
|
||||||
for i, ch in enumerate("TELEFACT"):
|
for i, ch in enumerate("TELEFACT"):
|
||||||
frame.set_cell(2 + i, 2, ch, "yellow")
|
frame.set_cell(2 + i, 2, ch, "yellow")
|
||||||
for i, ch in enumerate("BROADCASTER BASE"):
|
for i, ch in enumerate("BROADCASTER BASE"):
|
||||||
|
|
|
||||||
BIN
src/__pycache__/config_manager.cpython-313.pyc
Normal file
BIN
src/__pycache__/config_manager.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
22
src/config_manager.py
Normal file
22
src/config_manager.py
Normal 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)
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
"""
|
"""
|
||||||
Telefact Renderer (Tkinter)
|
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).
|
- Pure Python (stdlib only).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
|
from tkinter import font as tkfont
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
from src.core.telefact_frame import TelefactFrame
|
from src.core.telefact_frame import TelefactFrame
|
||||||
|
|
||||||
|
|
@ -12,81 +15,123 @@ PALETTE: Dict[str, str] = {
|
||||||
"black": "#000000",
|
"black": "#000000",
|
||||||
"blue": "#0000FF",
|
"blue": "#0000FF",
|
||||||
"red": "#FF0000",
|
"red": "#FF0000",
|
||||||
"magenta":"#FF00FF",
|
"magenta": "#FF00FF",
|
||||||
"green": "#00FF00",
|
"green": "#00FF00",
|
||||||
"cyan": "#00FFFF",
|
"cyan": "#00FFFF",
|
||||||
"yellow": "#FFFF00",
|
"yellow": "#FFFF00",
|
||||||
"white": "#FFFFFF",
|
"white": "#FFFFFF",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class TelefactRenderer:
|
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.root = root
|
||||||
self.width = width
|
self.width = width
|
||||||
self.height = height
|
self.height = height
|
||||||
self.show_grid = show_grid
|
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.cols, self.rows = 40, 24
|
||||||
self.cell_w = max(1, width // self.cols)
|
self.margin_x = 24
|
||||||
self.cell_h = max(1, height // self.rows)
|
self.margin_y = 24
|
||||||
self.w = self.cell_w * self.cols
|
self.inner_w = width - self.margin_x * 2
|
||||||
self.h = self.cell_h * self.rows
|
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(
|
self.canvas = tk.Canvas(
|
||||||
root, width=self.w, height=self.h,
|
root,
|
||||||
highlightthickness=0, bg=PALETTE["black"]
|
width=self.width,
|
||||||
|
height=self.height,
|
||||||
|
highlightthickness=0,
|
||||||
|
bg=PALETTE.get(self.colors["Background"], self.colors["Background"]),
|
||||||
)
|
)
|
||||||
self.canvas.pack()
|
self.canvas.pack()
|
||||||
|
|
||||||
# simple monospace font; will swap for Teletext font later
|
# Font setup
|
||||||
self.font = ("Courier New", max(12, self.cell_h - 9), "bold")
|
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:
|
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:
|
def _gy(self, row: int) -> int:
|
||||||
return row * self.cell_h
|
return self.margin_y + 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
|
|
||||||
)
|
|
||||||
|
|
||||||
def _draw_char(self, col: int, row: int, char: str, color: str) -> None:
|
def _draw_char(self, col: int, row: int, char: str, color: str) -> None:
|
||||||
|
"""Draw a single glyph."""
|
||||||
x = self._gx(col) + 2
|
x = self._gx(col) + 2
|
||||||
y = self._gy(row) + self.cell_h // 2
|
y = self._gy(row) + self.cell_h // 2
|
||||||
self.canvas.create_text(
|
self.canvas.create_text(
|
||||||
x, y, anchor="w", text=char,
|
x, y, anchor="w", text=char, font=self.font, fill=PALETTE.get(color, color)
|
||||||
font=self.font, fill=PALETTE.get(color, color)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def _draw_grid(self) -> None:
|
def _draw_grid(self) -> None:
|
||||||
|
"""Debug overlay for exact cell spacing."""
|
||||||
for c in range(self.cols + 1):
|
for c in range(self.cols + 1):
|
||||||
x = self._gx(c)
|
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):
|
for r in range(self.rows + 1):
|
||||||
y = self._gy(r)
|
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:
|
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")
|
self.canvas.delete("all")
|
||||||
|
|
||||||
# background
|
# Fill total background
|
||||||
self.canvas.create_rectangle(0, 0, self.w, self.h,
|
self.canvas.create_rectangle(
|
||||||
fill=PALETTE["black"], width=0)
|
0, 0, self.width, self.height,
|
||||||
|
fill=PALETTE.get(self.colors["Background"], self.colors["Background"]),
|
||||||
|
width=0,
|
||||||
|
)
|
||||||
|
|
||||||
# header/footer colour bands
|
# Draw Teletext cells
|
||||||
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
|
|
||||||
for row in range(frame.rows):
|
for row in range(frame.rows):
|
||||||
for col in range(frame.cols):
|
for col in range(frame.cols):
|
||||||
ch, fg = frame.grid[row][col]
|
ch, fg = frame.grid[row][col]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user