A focused braille graphics rendering library for the Maze Wars TUI game.
In scope for v1:
- Core braille canvas with pixel-level control
- Line drawing (Bresenham algorithm)
- Rectangle drawing (outline and filled)
- Circle drawing (Bresenham midpoint algorithm) for eyeball sprites
- Optional per-cell ANSI color support
Out of scope (handled by game layer):
- Text rendering and fonts (HUD handled elsewhere)
- Animation framework
- Input handling
- 3D math/projection (game provides 2D coordinates)
stipple/
├── canvas/
│ ├── braille.go # Braille encoding constants and helpers
│ ├── canvas.go # Canvas struct and core methods
│ ├── color.go # ANSI color type and escape codes
│ └── options.go # Functional options pattern
├── draw/
│ ├── circle.go # Bresenham midpoint circle algorithm
│ ├── line.go # Bresenham line algorithm
│ └── rectangle.go # Rectangle outline and fill
├── internal/
│ └── term/
│ └── size.go # Terminal size detection (optional utility)
└── examples/
├── demo/
│ └── main.go # Visual demo (grows with each version)
└── maze/
└── main.go # Maze Wars-style rendering example
Two complementary approaches for visual feedback during development:
A growing example program that exercises new features as they're added:
go run ./examples/demo/v0.1.0: Draw individual pixels, show braille encoding v0.2.0: Add line demonstrations (horizontal, vertical, diagonal) v0.3.0: Add rectangle demonstrations (outline and filled) v0.4.0: Add circle demonstrations (outline and filled) v0.5.0+: Add color demonstrations
Tests can print rendered output when run with -visual:
go test ./... -v -args -visualImplementation in test files:
var visualFlag = flag.Bool("visual", false, "print visual output")
func TestSomething(t *testing.T) {
c := canvas.New(40, 20)
// ... draw something ...
if *visualFlag {
fmt.Println("\n=== TestSomething ===")
fmt.Println(c.Frame())
}
// ... assertions ...
}Add flag.Parse() in TestMain for each test package:
func TestMain(m *testing.M) {
flag.Parse()
os.Exit(m.Run())
}Establish the core canvas with braille encoding. A user can set individual pixels and render to a string.
package canvas
// BrailleOffset is the Unicode code point for the empty braille pattern.
const BrailleOffset = 0x2800
// pixelMap maps (row, column) within a 4x2 cell to the braille dot bit.
// Rows 0-3, Columns 0-1.
var pixelMap = [4][2]rune{
{0x01, 0x08}, // row 0: dots 1, 4
{0x02, 0x10}, // row 1: dots 2, 5
{0x04, 0x20}, // row 2: dots 3, 6
{0x40, 0x80}, // row 3: dots 7, 8
}Core struct and methods:
package canvas
type Canvas struct {
width int // pixel width
height int // pixel height
cells [][]rune // braille character grid [row][col]
invertY bool // Y-axis direction
}
func New(width, height int, options ...Option) *Canvas
func (c *Canvas) Set(x, y float64)
func (c *Canvas) Unset(x, y float64)
func (c *Canvas) Toggle(x, y float64)
func (c *Canvas) Get(x, y float64) bool
func (c *Canvas) Clear()
func (c *Canvas) Frame() string
func (c *Canvas) Width() int
func (c *Canvas) Height() int
func (c *Canvas) Rows() int // height / 4 (terminal rows)
func (c *Canvas) Cols() int // width / 2 (terminal columns)Implementation notes:
cellsis allocated as[Rows()][Cols()]rune, each initialized toBrailleOffset- Pixel coordinates use
float64for sub-pixel precision; convert tointinternally - Out-of-bounds coordinates are silently ignored (no error, no panic)
Frame()joins rows with newlines, each row joins cells into a string
package canvas
type Option func(*Canvas)
func WithInvertedY() Option {
return func(c *Canvas) {
c.invertY = true
}
}- Test that each pixel position maps to the correct braille pattern
- Test all 8 dot positions individually
- Test full cell (all dots set) equals
\u28FF
TestNew: verify dimensions and initial state (all cells areBrailleOffset)TestSetGet: set a pixel, verify Get returns trueTestUnset: set then unset, verify Get returns falseTestToggle: toggle twice returns to original stateTestClear: set pixels, clear, verify all Get return falseTestFrame: set specific pixels, verify exact braille outputTestOutOfBounds: setting out-of-bounds coordinates does not panicTestInvertedY: verify Y-axis inversion works correctly
-
canvas/braille.gowith constants and pixel map -
canvas/canvas.gowith Canvas struct and all core methods -
canvas/options.gowithWithInvertedY -
canvas/braille_test.go -
canvas/canvas_test.gowith-visualflag support -
examples/demo/main.goshowing pixel operations -
go.modinitialized
# Run tests
go test ./canvas/...
# Visual verification
go test ./canvas/... -v -args -visual
go run ./examples/demo/Add Bresenham's line algorithm for drawing straight lines between two points.
package draw
import "github.com/cboone/stipple/canvas"
// Line draws a line from (startX, startY) to (endX, endY) using Bresenham's algorithm.
func Line(c *canvas.Canvas, startX, startY, endX, endY float64)Implementation notes:
- Use integer Bresenham algorithm (no floating point in inner loop)
- Convert float64 coordinates to int at function entry
- Handle all octants (steep/shallow, positive/negative slopes)
- Set each pixel along the line using
c.Set()
TestLineHorizontal: verify pixels along a horizontal lineTestLineVertical: verify pixels along a vertical lineTestLineDiagonalPositive: 45-degree line with positive slopeTestLineDiagonalNegative: 45-degree line with negative slopeTestLineSymmetry:Line(a, b, c, d)produces same pixels asLine(c, d, a, b)TestLineSinglePoint: start equals end draws one pixelTestLineShallowSlope: slope < 1TestLineSteepSlope: slope > 1
-
draw/line.gowith Bresenham implementation -
draw/line_test.gowith-visualflag support - Update
examples/demo/main.gowith line demonstrations
go test ./draw/... -v -args -visual
go run ./examples/demo/Add rectangle drawing (outline and filled) for rendering wall faces.
package draw
import "github.com/cboone/stipple/canvas"
// Rectangle draws a rectangle outline from (x, y) with the given width and height.
func Rectangle(c *canvas.Canvas, x, y, width, height float64)
// RectangleFilled draws a filled rectangle from (x, y) with the given width and height.
func RectangleFilled(c *canvas.Canvas, x, y, width, height float64)Implementation notes:
RectanglecallsLinefour times for the edgesRectangleFillediterates row by row, setting all pixels in each row- Width and height of 0 or negative draw nothing
- Coordinates can be negative (partially off-canvas rectangles are clipped)
TestRectangle: verify outline pixels are set, interior is notTestRectangleFilled: verify all interior pixels are setTestRectangleZeroSize: zero width or height draws nothingTestRectanglePartiallyOffCanvas: rectangle extending beyond bounds is clipped
-
draw/rectangle.go -
draw/rectangle_test.gowith-visualflag support - Update
examples/demo/main.gowith rectangle demonstrations
go test ./draw/... -v -args -visual
go run ./examples/demo/Add Bresenham's midpoint circle algorithm for drawing circles (eyeball sprites).
package draw
import "github.com/cboone/stipple/canvas"
// Circle draws a circle outline centered at (centerX, centerY) with the given radius.
func Circle(c *canvas.Canvas, centerX, centerY, radius float64)
// CircleFilled draws a filled circle centered at (centerX, centerY) with the given radius.
func CircleFilled(c *canvas.Canvas, centerX, centerY, radius float64)Implementation notes:
- Use Bresenham's midpoint circle algorithm (integer arithmetic in inner loop)
- Convert float64 coordinates to int at function entry
- Leverage 8-way symmetry: compute one octant, reflect to all 8
Circlesets outline pixels onlyCircleFilleddraws horizontal lines between symmetric points for each y level- Radius of 0 draws a single pixel at center
- Negative radius draws nothing
TestCircleSymmetry: verify 8-way symmetry (circle looks round, not skewed)TestCircleRadius0: radius 0 draws single pixelTestCircleRadius1: verify small circle pixelsTestCircleFilled: verify interior pixels are setTestCircleFilledNoGaps: verify no gaps in filled circle (scan each row)TestCircleOutlineOnly: verify outline circle has empty interior
-
draw/circle.gowith Bresenham midpoint implementation -
draw/circle_test.gowith-visualflag support - Update
examples/demo/main.gowith circle demonstrations
go test ./draw/... -v -args -visual
go run ./examples/demo/Add optional per-cell ANSI color support for visual distinction.
package canvas
type Color uint8
const (
ColorDefault Color = iota
ColorBlack
ColorRed
ColorGreen
ColorYellow
ColorBlue
ColorMagenta
ColorCyan
ColorWhite
)
// ANSI returns the ANSI escape sequence for this color.
func (c Color) ANSI() string
// ANSIReset returns the reset escape sequence.
func ANSIReset() stringAdd color grid and methods:
type Canvas struct {
// ... existing fields
colors [][]Color // per-cell color, nil if colors disabled
colorEnabled bool
}
// SetColor sets a pixel and its cell color.
func (c *Canvas) SetColor(x, y float64, color Color)
// Frame() updated to include ANSI escape codes when colors are enabledAdd color option:
func WithColor() Option {
return func(c *Canvas) {
c.colorEnabled = true
c.colors = make([][]Color, c.Rows())
for i := range c.colors {
c.colors[i] = make([]Color, c.Cols())
}
}
}TestColorANSI: verify each color produces correct escape sequenceTestSetColor: set colored pixel, verify Frame contains escape codesTestColorDisabled: withoutWithColor(), SetColor still sets pixel but no escape codesTestColorReset: verify colors reset between cells
-
canvas/color.go - Updated
canvas/canvas.gowith color support - Updated
canvas/options.gowithWithColor() -
canvas/color_test.gowith-visualflag support - Update
examples/demo/main.gowith color demonstrations
go test ./canvas/... -v -args -visual
go run ./examples/demo/Extend draw functions to accept optional color parameters.
Add color variant:
// LineWithColor draws a colored line.
func LineWithColor(c *canvas.Canvas, startX, startY, endX, endY float64, color canvas.Color)Add color variants:
// RectangleWithColor draws a colored rectangle outline.
func RectangleWithColor(c *canvas.Canvas, x, y, width, height float64, color canvas.Color)
// RectangleFilledWithColor draws a colored filled rectangle.
func RectangleFilledWithColor(c *canvas.Canvas, x, y, width, height float64, color canvas.Color)Add color variants:
// CircleWithColor draws a colored circle outline.
func CircleWithColor(c *canvas.Canvas, centerX, centerY, radius float64, color canvas.Color)
// CircleFilledWithColor draws a colored filled circle.
func CircleFilledWithColor(c *canvas.Canvas, centerX, centerY, radius float64, color canvas.Color)TestLineWithColor: verify colored line outputTestRectangleWithColor: verify colored rectangle outputTestCircleWithColor: verify colored circle output
- Updated
draw/line.gowith color variant - Updated
draw/rectangle.gowith color variants - Updated
draw/circle.gowith color variants - Updated tests with
-visualflag support - Update
examples/demo/main.gowith colored shape demonstrations
go test ./... -v -args -visual
go run ./examples/demo/Create a Maze Wars-style rendering example and documentation.
Demonstrate:
- Creating a canvas sized to terminal
- Drawing a simple first-person corridor view with rectangles
- Using colors to distinguish wall segments at different depths
- Rendering an "eyeball" sprite using circles (outline and filled)
Document:
- Installation
- Quick start example
- API overview
- Link to examples
-
examples/maze/main.go -
README.mdwith documentation
go run ./examples/maze/Visual inspection of output.
Final polish, golden tests, and release preparation.
-
Golden Output Test
- Create
testdata/directory with expected output files - Add golden test that renders a known scene and compares to expected output
- Create
-
Code Review
- Ensure all public types and functions have doc comments
- Verify error handling is consistent (silent ignore for out-of-bounds)
- Check for any panics on edge cases
-
CI Setup
- Add GitHub Actions workflow for
go test ./... - Test on Go 1.21 and latest
- Add GitHub Actions workflow for
-
Release
- Tag v1.0.0
- Ensure
go.modmodule path is correct for import
-
canvas/golden_test.gowith golden output tests -
testdata/*.goldenfiles -
.github/workflows/test.yml - All doc comments complete
- v1.0.0 tag
go test ./...All tests pass, including golden tests.
| Version | Focus | Key Deliverables |
|---|---|---|
| v0.1.0 | Braille canvas foundation | Canvas, Set/Get/Frame, options |
| v0.2.0 | Line drawing | Bresenham line algorithm |
| v0.3.0 | Rectangle drawing | Outline and filled rectangles |
| v0.4.0 | Circle drawing | Bresenham midpoint circle algorithm |
| v0.5.0 | Color support (canvas) | ANSI colors, per-cell coloring |
| v0.6.0 | Colored draw functions | Color variants for all draw functions |
| v0.7.0 | Example and docs | Maze example, README |
| v1.0.0 | Release polish | Golden tests, CI, release |
- Use
float64for all public API coordinates - Allows sub-pixel precision for smooth rendering
- Convert to
intinternally for pixel operations
float64coordinates are converted to integer pixel coordinates usingmath.Floor.New(width, height)expects pixel units;Rows()isheight / 4,Cols()iswidth / 2.- Out-of-bounds pixels are silently ignored; partially off-canvas shapes are clipped.
- Color is per-braille-cell; when multiple pixels in the same cell use different colors, the last write wins.
Frame()emits ANSI color codes only when color is enabled and resets after each cell.
- Out-of-bounds coordinates are silently ignored
- No panics, no errors returned
- Simplifies calling code, enables partial rendering of off-canvas shapes
- Optional via
WithColor()to avoid allocation when not needed - 8 basic ANSI colors (sufficient for Maze Wars)
- Per-cell coloring (not per-pixel, matches braille character granularity)
- Zero external dependencies in core packages
- Standard library only (
strings,math)
Unicode Braille patterns (U+2800 to U+28FF) encode a 2x4 dot grid:
Dot positions: Bit values:
0 3 0x01 0x08
1 4 0x02 0x10
2 5 0x04 0x20
6 7 0x40 0x80
Each terminal cell represents 2 pixels wide by 4 pixels tall, providing 8x resolution improvement over standard characters.
Conversion formulas:
- Cell column = pixel_x / 2
- Cell row = pixel_y / 4
- Dot column = pixel_x % 2
- Dot row = pixel_y % 4
- Braille char = BrailleOffset | pixelMap[dot_row][dot_column]