Files
waveform-draw/draw_12_rects_and_send.py
2025-12-13 00:42:20 +00:00

206 lines
8.4 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
draw_12_slanted_top_bottom.py
* 800×600 ARGB framebuffer (A,R,G,B each 1byte)
* 12 rectangles, aspect1:12, 5px gap, 100px empty margin at top & bottom
* each rectangle has an individual tilt angle (degrees) stored in
ROT_ANGLES_DEG[12]
* the top and bottom edges are *slanted* while the left and righthand
edges remain perfectly vertical
* outline colour = opaque white, thickness = LINE_THICKNESS (default=3px)
* the raw buffer (with a 4byte length prefix) is streamed to a TCP server
listening on localhost:12345
"""
import math
import socket
import sys
# ----------------------------------------------------------------------
# 1⃣ Framebuffer configuration
# ----------------------------------------------------------------------
FB_WIDTH = 800 # horizontal pixels
FB_HEIGHT = 600 # vertical pixels
BPP = 4 # bytes per pixel (A,R,G,B)
# ----------------------------------------------------------------------
# 2⃣ Margins & rectangle geometry (before slant)
# ----------------------------------------------------------------------
MARGIN_TOP = 100 # empty band at the very top
MARGIN_BOTTOM = 150 # empty band at the very bottom
RECT_HEIGHT = FB_HEIGHT - MARGIN_TOP - MARGIN_BOTTOM # usable height = 400px
RECT_WIDTH = RECT_HEIGHT // 12 # ≈33px (aspect1:12)
SPACING = 10 # gap between rectangles
TOTAL_RECT_W = 12 * RECT_WIDTH + 11 * SPACING
X0_START = (FB_WIDTH - TOTAL_RECT_W) // 2 # centre the whole strip
# ----------------------------------------------------------------------
# 3⃣ Outline thickness (you may change it)
# ----------------------------------------------------------------------
LINE_THICKNESS = 5 # ≥1 pixel
# ----------------------------------------------------------------------
# 4⃣ Perrectangle tilt angles (degrees)
# ----------------------------------------------------------------------
# You can generate these programmatically, read them from a file, etc.
ROT_ANGLES_DEG = [
0, 5, 10, 15, 20, 25,
30, 35, 40, 45, 50, 85
]
assert len(ROT_ANGLES_DEG) == 12, "Exactly 12 angles are required."
# ----------------------------------------------------------------------
# Helper write a single pixel (boundschecked)
# ----------------------------------------------------------------------
def set_pixel(buf: bytearray, x: int, y: int, color: tuple) -> None:
"""Write an ARGB pixel at (x, y). `color` = (A,R,G,B)."""
if 0 <= x < FB_WIDTH and 0 <= y < FB_HEIGHT:
off = (y * FB_WIDTH + x) * BPP
buf[off:off + 4] = bytes(color) # A,R,G,B order
# ----------------------------------------------------------------------
# Helper draw a thick line (Bresenham + square brush)
# ----------------------------------------------------------------------
def draw_thick_line(buf: bytearray,
x0: int, y0: int,
x1: int, y1: int,
color: tuple,
thickness: int) -> None:
"""Draw a line from (x0,y0) to (x1,y1) using a square brush of `thickness`."""
dx = abs(x1 - x0)
dy = -abs(y1 - y0)
sx = 1 if x0 < x1 else -1
sy = 1 if y0 < y1 else -1
err = dx + dy # error term
while True:
# paint a filled square centred on the current pixel
for ty in range(-thickness // 2, thickness // 2 + 1):
for tx in range(-thickness // 2, thickness // 2 + 1):
set_pixel(buf, x0 + tx, y0 + ty, color)
if x0 == x1 and y0 == y1:
break
e2 = 2 * err
if e2 >= dy:
err += dy
x0 += sx
if e2 <= dx:
err += dx
y0 += sy
# ----------------------------------------------------------------------
# 5⃣ Build the framebuffer (background + white slantedtop/bottom rectangles)
# ----------------------------------------------------------------------
def build_framebuffer() -> bytearray:
"""Create the ARGB buffer and draw the 12 rectangles with slanted top/bottom."""
# ----- background: opaque black -----
fb = bytearray(FB_WIDTH * FB_HEIGHT * BPP)
bg = (255, 0, 0, 0) # A,R,G,B = opaque black
fb[:] = bg * (FB_WIDTH * FB_HEIGHT)
WHITE = (255, 255, 255, 255) # colour of the outlines
half_w = RECT_WIDTH // 2 # half the (unslanted) width
y_top = MARGIN_TOP
y_bottom = FB_HEIGHT - MARGIN_BOTTOM - 1 # inclusive bottom row
for idx in range(12):
# --------------------------------------------------------------
# 1⃣ Unslanted left edge of the rectangle (including spacing)
# --------------------------------------------------------------
orig_x0 = X0_START + idx * (RECT_WIDTH + SPACING)
# --------------------------------------------------------------
# 2⃣ Centre X coordinate (kept fixed while slanting)
# --------------------------------------------------------------
cx = orig_x0 + half_w
# --------------------------------------------------------------
# 3⃣ Angle for this rectangle
# --------------------------------------------------------------
angle_rad = math.radians(ROT_ANGLES_DEG[idx])
sin_a = math.sin(angle_rad)
# --------------------------------------------------------------
# 4⃣ Vertical offset applied to the *ends* of the top/bottom lines
# (the spec says: sin(angle) * RECT_WITH / 2)
# --------------------------------------------------------------
offset = sin_a * half_w # because RECT_WITH/2 == half_w
# --------------------------------------------------------------
# 5⃣ Coordinates of the four line endpoints
# --------------------------------------------------------------
# top edge
x0_top = cx - int(math.cos(angle_rad) * half_w)
y0_top = int(round(y_top + offset))
x1_top = cx + int(math.cos(angle_rad) * half_w)
y1_top = int(round(y_top - offset))
# bottom edge
x0_bot = cx - int(math.cos(angle_rad) * half_w)
y0_bot = int(round(y_bottom - offset))
x1_bot = cx + int(math.cos(angle_rad) * half_w)
y1_bot = int(round(y_bottom + offset))
# --------------------------------------------------------------
# 6⃣ Draw the four sides
# --------------------------------------------------------------
# top (slanted)
draw_thick_line(fb, x0_top, y0_top, x1_top, y1_top,
WHITE, LINE_THICKNESS)
# bottom (slanted)
draw_thick_line(fb, x0_bot, y0_bot, x1_bot, y1_bot,
WHITE, LINE_THICKNESS)
# left vertical side from the *topleft* endpoint down to the
# *bottomleft* endpoint
draw_thick_line(fb, x0_top, y0_top, x0_bot, y0_bot,
WHITE, LINE_THICKNESS)
# right vertical side from the *topright* endpoint down to the
# *bottomright* endpoint
draw_thick_line(fb, x1_top, y1_top, x1_bot, y1_bot,
WHITE, LINE_THICKNESS)
return fb
# ----------------------------------------------------------------------
# 6⃣ Send the framebuffer to the TCP server (localhost:12345)
# ----------------------------------------------------------------------
def send_framebuffer(buf: bytearray,
host: str = "127.0.0.1",
port: int = 12345) -> None:
"""Open a TCP socket, connect, and stream the whole framebuffer."""
try:
with socket.create_connection((host, port), timeout=5) as sock:
# optional 4byte length prefix many simple protocols expect it
length_prefix = len(buf).to_bytes(4, byteorder="big")
sock.sendall(length_prefix + buf)
print(f"✔ Sent {len(buf)} bytes to {host}:{port}")
except Exception as exc:
print(f"❌ Could not send framebuffer: {exc}", file=sys.stderr)
# ----------------------------------------------------------------------
# 7⃣ Main entry point
# ----------------------------------------------------------------------
def main() -> None:
fb = build_framebuffer()
send_framebuffer(fb)
if __name__ == "__main__":
main()