Skip to content

A canvas you draw on in your own coordinates - #80

Merged
ralyodio merged 1 commit into
mainfrom
feat/canvas-shapes
Sep 8, 2026
Merged

A canvas you draw on in your own coordinates#80
ralyodio merged 1 commit into
mainfrom
feat/canvas-shapes

Conversation

@ralyodio

@ralyodio ralyodio commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

The third slice of #64, and the one the issue calls the most valuable.

BrailleCanvas works in pixels. The primitives are good, but the caller does every unit conversion, which means a drawing written for one panel size is wrong in the next — and there is no way to say "this line runs from (0, 0) to (10, 10) in my units" without doing the arithmetic by hand every frame.

ui.shapes({
  shapes: [
    { type: "rect", x: 1, y: 1, width: 4, height: 4 },
    { type: "circle", x: 7, y: 5, radius: 2 },
    { type: "polyline", points: [[0, 8], [3, 9], [6, 7], [9, 9]] },
  ],
  x: { min: 0, max: 10 },
  y: { min: 0, max: 10 },
});

Bounds per axis, five shapes placed inside them, and a colour per shape. The existing canvas is untouched: it is still there for anyone who wants the pixel grid.

Four decisions worth the words

Y goes up. A canvas is for drawing things that have their own geometry — a plot, a map, a diagram — and making the caller flip every y would be handing them back the conversion this exists to take away. There is a test asserting that y = max lands on the top row.

A radius is scaled, not projected. It is a distance rather than a position, and the two axes rarely scale alike in a terminal cell, so a circle is measured against the x span.

A rectangle is a corner and a size in the caller's own direction, so a positive height goes up, because their y does.

Each shape is blitted before the next is drawn. A shared canvas would let the last colour win everywhere two shapes overlap; a test renders two lines in two colours and counts them.

The C++ side needed primitives first

The C++ Braille class had only pixel, line and blit, so rect, fill_rect, circle, hline, vline and the span helper are new there — written against the reference rather than invented, which is why all five canvas fixtures matched on the first run.

Scope

Six ports, five fixtures, all additive. Not added to the widget gallery: the existing canvas is not in it either, because it is a graphics primitive rather than a widget, and plot is in the same position. This is one slice of #64; Shadow is still open there, which is why this closes nothing.

Verified

  • 331 TypeScript tests under both bun test and node --test, typecheck clean
  • 86 widget scenes matching the reference in Rust, Go, Python, Zig and C++ (was 81)
  • 11/11 ctest, the widget galleries, next build for the site

🤖 Generated with Claude Code

https://claude.ai/code/session_017Df2FNu5DhinMV2soRz3cy

The third slice of #64, and the one the issue calls the most valuable.

`BrailleCanvas` works in pixels. The primitives are good, but the caller does
every unit conversion, which means a drawing written for one panel size is
wrong in the next -- and there is no way to say "this line runs from (0, 0) to
(10, 10) in my units" without doing the arithmetic by hand every frame.

  ui.shapes({
    shapes: [
      { type: "rect", x: 1, y: 1, width: 4, height: 4 },
      { type: "circle", x: 7, y: 5, radius: 2 },
      { type: "polyline", points: [[0, 8], [3, 9], [6, 7], [9, 9]] },
    ],
    x: { min: 0, max: 10 },
    y: { min: 0, max: 10 },
  });

Bounds per axis, five shapes placed inside them, and a colour per shape. The
existing `canvas` is untouched: it is still there for anyone who wants the
pixel grid.

Four decisions worth the words:

Y goes up. A canvas is for drawing things that have their own geometry -- a
plot, a map, a diagram -- and making the caller flip every y would be handing
them back the conversion this exists to take away. There is a test asserting
that y = max lands on the top row.

A radius is scaled, not projected. It is a distance rather than a position,
and the two axes rarely scale alike in a terminal cell, so a circle is measured
against the x span.

A rectangle is a corner and a size in the caller's own direction, so a positive
height goes up, because their y does.

Each shape is blitted before the next is drawn. A shared canvas would let the
last colour win everywhere two shapes overlap; a test renders two lines in two
colours and counts them.

The C++ Braille class had only `pixel`, `line` and `blit`, so `rect`,
`fill_rect`, `circle`, `hline`, `vline` and the span helper are new there,
written against the reference rather than invented -- which is why all five
canvas fixtures matched on the first run.

Six ports, five fixtures, all additive. Not added to the widget gallery: the
existing canvas is not in it either, because it is a graphics primitive rather
than a widget, and `plot` is in the same position.

Verified: 331 TS tests under bun and node; 86 widget scenes matching the
reference in Rust, Go, Python, Zig and C++; 11/11 ctest; the galleries; the
site builds.

Part of #64

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Df2FNu5DhinMV2soRz3cy
@ralyodio
ralyodio merged commit 4c56aee into main Sep 8, 2026
18 checks passed
@ralyodio
ralyodio deleted the feat/canvas-shapes branch September 8, 2026 23:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant