Skip to content

Commit 2d63db8

Browse files
committed
feat: add Arc, Line, and Polygon drawing primitives
This commit introduces three new drawing primitives to the render module: - `render.Arc`: Draws an arc defined by center, radius, and angles. - `render.Line`: Draws a line between two points. - `render.Polygon`: Draws a filled polygon from a list of vertices. These widgets are exposed to Starlark and integrated into the layout system. Documentation and an example script (`examples/draw/draw.star`) are included. The runtime generator was updated to support these new types.
1 parent c522d71 commit 2d63db8

9 files changed

Lines changed: 926 additions & 0 deletions

File tree

docs/widgets.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,20 @@ render.Animation(
4646
```
4747
![](img/widget_Animation_0.gif)
4848

49+
## Arc
50+
Arc draws an arc. The arc is centered at (x, y).
51+
52+
#### Attributes
53+
| Name | Type | Description | Required |
54+
| --- | --- | --- | --- |
55+
| `x` | `float / int` | The x-coordinate of the center of the arc. | **Y** |
56+
| `y` | `float / int` | The y-coordinate of the center of the arc. | **Y** |
57+
| `radius` | `float / int` | The radius of the arc. | **Y** |
58+
| `start_angle` | `float / int` | The starting angle of the arc, in radians. | **Y** |
59+
| `end_angle` | `float / int` | The ending angle of the arc, in radians. | **Y** |
60+
| `color` | `color` | The color of the arc. | **Y** |
61+
| `width` | `float / int` | The width of the arc. | **Y** |
62+
4963
## Box
5064
A Box is a rectangular widget that can hold a child widget.
5165

@@ -197,6 +211,19 @@ the `delay` attribute.
197211
| `delay` | `int` | (Read-only) Frame delay in ms, for animated GIFs | N |
198212
| `hold_frames` | `int` | Number of render frames to hold each animation frame, default is 1. | N |
199213

214+
## Line
215+
Line draws a line from (x1, y1) to (x2, y2).
216+
217+
#### Attributes
218+
| Name | Type | Description | Required |
219+
| --- | --- | --- | --- |
220+
| `x1` | `float / int` | The x-coordinate of the starting point. | **Y** |
221+
| `y1` | `float / int` | The y-coordinate of the starting point. | **Y** |
222+
| `x2` | `float / int` | The x-coordinate of the ending point. | **Y** |
223+
| `y2` | `float / int` | The y-coordinate of the ending point. | **Y** |
224+
| `color` | `color` | The color of the line. | **Y** |
225+
| `width` | `float / int` | The width of the line. | **Y** |
226+
200227
## Marquee
201228
Marquee scrolls its child horizontally or vertically.
202229

@@ -324,6 +351,15 @@ render.Plot(
324351
```
325352
![](img/widget_Plot_0.gif)
326353

354+
## Polygon
355+
Polygon draws a polygon.
356+
357+
#### Attributes
358+
| Name | Type | Description | Required |
359+
| --- | --- | --- | --- |
360+
| `vertices` | `[(float, float)]` | A list of (x, y) tuples representing the vertices of the polygon. | **Y** |
361+
| `color` | `color` | The color of the polygon. | **Y** |
362+
327363
## Root
328364
Every Widget tree has a Root.
329365

examples/draw/draw.star

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"""
2+
An example app that demonstrates the drawing primitives.
3+
"""
4+
5+
load("math.star", "math")
6+
load("render.star", "render")
7+
8+
def main():
9+
return render.Root(
10+
child = render.Stack(
11+
children = [
12+
render.Box(
13+
width = 64,
14+
height = 32,
15+
color = "#111",
16+
),
17+
render.Line(
18+
x1 = 0,
19+
y1 = 0,
20+
x2 = 63,
21+
y2 = 31,
22+
width = 1,
23+
color = "#fff",
24+
),
25+
render.Padding(
26+
pad = (10, 5, 0, 0),
27+
child = render.Polygon(
28+
vertices = [(0, 0), (44, 0), (44, 10), (0, 10)],
29+
color = "#f0f",
30+
),
31+
),
32+
render.Padding(
33+
pad = (22, 6, 0, 0), # Position arc roughly at center
34+
child = render.Arc(
35+
x = 10, # Center relative to widget
36+
y = 10,
37+
radius = 10,
38+
start_angle = 0,
39+
end_angle = math.pi * 1.5,
40+
width = 3,
41+
color = "#0ff",
42+
),
43+
),
44+
],
45+
),
46+
)

render/arc.go

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
package render
2+
3+
import (
4+
"image"
5+
"image/color"
6+
"math"
7+
8+
"github.com/tronbyt/gg"
9+
)
10+
11+
// Arc draws an arc. The arc is centered at (x, y).
12+
//
13+
// DOC(X): The x-coordinate of the center of the arc.
14+
// DOC(Y): The y-coordinate of the center of the arc.
15+
// DOC(Radius): The radius of the arc.
16+
// DOC(StartAngle): The starting angle of the arc, in radians.
17+
// DOC(EndAngle): The ending angle of the arc, in radians.
18+
// DOC(Color): The color of the arc.
19+
// DOC(Width): The width of the arc.
20+
type Arc struct {
21+
Widget
22+
X float64 `starlark:"x,required"`
23+
Y float64 `starlark:"y,required"`
24+
Radius float64 `starlark:"radius,required"`
25+
StartAngle float64 `starlark:"start_angle,required"`
26+
EndAngle float64 `starlark:"end_angle,required"`
27+
Color color.Color `starlark:"color,required"`
28+
Width float64 `starlark:"width,required"`
29+
}
30+
31+
func (a Arc) getBounds() (float64, float64, float64, float64) {
32+
// Start with endpoints
33+
x1 := a.X + a.Radius*math.Cos(a.StartAngle)
34+
y1 := a.Y + a.Radius*math.Sin(a.StartAngle)
35+
x2 := a.X + a.Radius*math.Cos(a.EndAngle)
36+
y2 := a.Y + a.Radius*math.Sin(a.EndAngle)
37+
38+
minX := math.Min(x1, x2)
39+
maxX := math.Max(x1, x2)
40+
minY := math.Min(y1, y2)
41+
maxY := math.Max(y1, y2)
42+
43+
// Check cardinal points (0, 90, 180, 270 degrees)
44+
// We need to normalize angles to [0, 2*pi)
45+
start := a.StartAngle
46+
end := a.EndAngle
47+
48+
// If start > end, we are crossing 0 (e.g. 350 to 10 degrees)
49+
// But gg uses "draw from start to end". If start > end, it generally draws clockwise or "the long way"?
50+
// Wait, gg documentation says: "Angles are specified in radians and go clockwise."
51+
// Actually, standard math is counter-clockwise.
52+
// Let's assume standard behavior: from Start to End.
53+
// If Start < End, it's simple interval [Start, End].
54+
// If Start > End, it's [Start, 2*pi] U [0, End]. (Crossing 0).
55+
56+
// Normalize angles to 0-2pi for comparison
57+
normalize := func(angle float64) float64 {
58+
angle = math.Mod(angle, 2*math.Pi)
59+
if angle < 0 {
60+
angle += 2 * math.Pi
61+
}
62+
return angle
63+
}
64+
65+
normStart := normalize(start)
66+
normEnd := normalize(end)
67+
68+
// If the original sweep was meant to be > 2pi (full circle), or specific winding,
69+
// checking just normalized values might be ambiguous.
70+
// But for bounding box, we just need to know if the cardinal directions are covered.
71+
72+
// We check each cardinal direction: 0, pi/2, pi, 3pi/2
73+
cardinals := []float64{0, math.Pi / 2, math.Pi, 3 * math.Pi / 2}
74+
75+
for _, angle := range cardinals {
76+
inArc := false
77+
if normStart <= normEnd {
78+
// Normal range
79+
if angle >= normStart && angle <= normEnd {
80+
inArc = true
81+
}
82+
} else {
83+
// Crossing 0
84+
if angle >= normStart || angle <= normEnd {
85+
inArc = true
86+
}
87+
}
88+
89+
if inArc {
90+
x := a.X + a.Radius*math.Cos(angle)
91+
y := a.Y + a.Radius*math.Sin(angle)
92+
93+
if x < minX {
94+
minX = x
95+
}
96+
if x > maxX {
97+
maxX = x
98+
}
99+
if y < minY {
100+
minY = y
101+
}
102+
if y > maxY {
103+
maxY = y
104+
}
105+
}
106+
}
107+
108+
// Expand by half width (stroke width)
109+
halfWidth := a.Width / 2.0
110+
minX -= halfWidth
111+
maxX += halfWidth
112+
minY -= halfWidth
113+
maxY += halfWidth
114+
115+
return minX, maxX, minY, maxY
116+
}
117+
118+
func (a Arc) PaintBounds(bounds image.Rectangle, frameIdx int) image.Rectangle {
119+
minX, maxX, minY, maxY := a.getBounds()
120+
return image.Rect(
121+
0,
122+
0,
123+
int(math.Ceil(maxX-minX)),
124+
int(math.Ceil(maxY-minY)),
125+
)
126+
}
127+
128+
func (a Arc) Paint(dc *gg.Context, bounds image.Rectangle, frameIdx int) {
129+
minX, _, minY, _ := a.getBounds()
130+
131+
dc.Push()
132+
dc.Translate(-minX, -minY)
133+
dc.SetColor(a.Color)
134+
dc.SetLineWidth(a.Width)
135+
dc.DrawArc(a.X, a.Y, a.Radius, a.StartAngle, a.EndAngle)
136+
dc.Stroke()
137+
dc.Pop()
138+
}
139+
140+
func (a Arc) FrameCount(bounds image.Rectangle) int {
141+
return 1
142+
}

render/line.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package render
2+
3+
import (
4+
"image"
5+
"image/color"
6+
"math"
7+
8+
"github.com/tronbyt/gg"
9+
)
10+
11+
// Line draws a line from (x1, y1) to (x2, y2).
12+
//
13+
// DOC(X1): The x-coordinate of the starting point.
14+
// DOC(Y1): The y-coordinate of the starting point.
15+
// DOC(X2): The x-coordinate of the ending point.
16+
// DOC(Y2): The y-coordinate of the ending point.
17+
// DOC(Color): The color of the line.
18+
// DOC(Width): The width of the line.
19+
type Line struct {
20+
Widget
21+
X1 float64 `starlark:"x1,required"`
22+
Y1 float64 `starlark:"y1,required"`
23+
X2 float64 `starlark:"x2,required"`
24+
Y2 float64 `starlark:"y2,required"`
25+
Color color.Color `starlark:"color,required"`
26+
Width float64 `starlark:"width,required"`
27+
}
28+
29+
func (l Line) getBounds() (float64, float64, float64, float64) {
30+
minX := math.Min(l.X1, l.X2)
31+
maxX := math.Max(l.X1, l.X2)
32+
minY := math.Min(l.Y1, l.Y2)
33+
maxY := math.Max(l.Y1, l.Y2)
34+
35+
halfWidth := l.Width / 2.0
36+
37+
// Ensure the bounds encompass the stroke width
38+
minX -= halfWidth
39+
maxX += halfWidth
40+
minY -= halfWidth
41+
maxY += halfWidth
42+
43+
return minX, maxX, minY, maxY
44+
}
45+
46+
func (l Line) PaintBounds(bounds image.Rectangle, frameIdx int) image.Rectangle {
47+
minX, maxX, minY, maxY := l.getBounds()
48+
return image.Rect(0, 0, int(math.Ceil(maxX-minX)), int(math.Ceil(maxY-minY)))
49+
}
50+
51+
func (l Line) Paint(dc *gg.Context, bounds image.Rectangle, frameIdx int) {
52+
minX, _, minY, _ := l.getBounds()
53+
54+
dc.Push()
55+
dc.Translate(-minX, -minY)
56+
dc.SetColor(l.Color)
57+
dc.SetLineWidth(l.Width)
58+
dc.DrawLine(l.X1, l.Y1, l.X2, l.Y2)
59+
dc.Stroke()
60+
dc.Pop()
61+
}
62+
63+
func (l Line) FrameCount(bounds image.Rectangle) int {
64+
return 1
65+
}

render/polygon.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package render
2+
3+
import (
4+
"image"
5+
"image/color"
6+
"math"
7+
8+
"github.com/tronbyt/gg"
9+
)
10+
11+
type Point struct {
12+
X, Y float64
13+
}
14+
15+
// Polygon draws a polygon.
16+
//
17+
// DOC(Vertices): A list of (x, y) tuples representing the vertices of the polygon.
18+
// DOC(Color): The color of the polygon.
19+
type Polygon struct {
20+
Widget
21+
Vertices []Point `starlark:"vertices,required"`
22+
Color color.Color `starlark:"color,required"`
23+
}
24+
25+
func (p Polygon) getBounds() (minX, maxX, minY, maxY float64) {
26+
minX, minY = math.Inf(1), math.Inf(1)
27+
maxX, maxY = math.Inf(-1), math.Inf(-1)
28+
29+
for _, pt := range p.Vertices {
30+
if pt.X < minX {
31+
minX = pt.X
32+
}
33+
if pt.X > maxX {
34+
maxX = pt.X
35+
}
36+
if pt.Y < minY {
37+
minY = pt.Y
38+
}
39+
if pt.Y > maxY {
40+
maxY = pt.Y
41+
}
42+
}
43+
return
44+
}
45+
46+
func (p Polygon) PaintBounds(bounds image.Rectangle, frameIdx int) image.Rectangle {
47+
minX, maxX, minY, maxY := p.getBounds()
48+
49+
if math.IsInf(minX, 0) {
50+
return image.Rect(0, 0, 0, 0)
51+
}
52+
53+
return image.Rect(0, 0, int(math.Ceil(maxX-minX)), int(math.Ceil(maxY-minY)))
54+
}
55+
56+
func (p Polygon) Paint(dc *gg.Context, bounds image.Rectangle, frameIdx int) {
57+
if len(p.Vertices) == 0 {
58+
return
59+
}
60+
61+
minX, _, minY, _ := p.getBounds()
62+
63+
dc.Push()
64+
dc.Translate(-minX, -minY)
65+
dc.SetColor(p.Color)
66+
67+
for i, pt := range p.Vertices {
68+
if i == 0 {
69+
dc.MoveTo(pt.X, pt.Y)
70+
} else {
71+
dc.LineTo(pt.X, pt.Y)
72+
}
73+
}
74+
75+
dc.ClosePath()
76+
dc.Fill()
77+
dc.Pop()
78+
}
79+
80+
func (p Polygon) FrameCount(bounds image.Rectangle) int {
81+
return 1
82+
}

0 commit comments

Comments
 (0)