Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added testdata/TestFilledCircles_Large.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added testdata/TestFilledCircles_OffBottomRight.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added testdata/TestFilledCircles_OffTopLeft.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added testdata/TestFilledCircles_Radius0.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added testdata/TestFilledCircles_Radius1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added testdata/TestFilledCircles_Small.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added testdata/TestFilledRectangles_FullyOff.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added testdata/TestFilledRectangles_HorizLine.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added testdata/TestFilledRectangles_Inside.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added testdata/TestFilledRectangles_LargerThanDisplay.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added testdata/TestFilledRectangles_OffBottomRight.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added testdata/TestFilledRectangles_OffTopLeft.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added testdata/TestFilledRectangles_OnePixel.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added testdata/TestFilledRectangles_VertLine.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added testdata/TestRectangles_Inside.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added testdata/TestRectangles_OffBottomRight.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added testdata/TestRectangles_OffTopLeft.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added testdata/TestTriangles_OffBottomRight.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added testdata/TestTriangles_OffBottomRight_Filled.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added testdata/TestTriangles_OffLeft.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added testdata/TestTriangles_OffLeft_Filled.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added testdata/TestTriangles_Wide.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added testdata/TestTriangles_Wide_Filled.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
72 changes: 57 additions & 15 deletions tinydraw.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,66 @@ import (

var errEmptyRectangle = errors.New("tinydraw: empty rectangle")

// rectangleFiller is a display with a fast FillRectangle, like most drivers
type rectangleFiller interface {
FillRectangle(x int16, y int16, width int16, height int16, color color.RGBA) error
}

// fillRectangle draws the part of a filled rectangle that is on the display
func fillRectangle(display drivers.Displayer, x int16, y int16, w int16, h int16, color color.RGBA) {
filler, ok := display.(rectangleFiller)
if !ok {
for i := x; i < x+w; i++ {
for j := y; j < y+h; j++ {
display.SetPixel(i, j, color)
}
}
return
}
// FillRectangle rejects rectangles outside the display: clip them
sx, sy := display.Size()
if x < 0 {
w += x
x = 0
}
if y < 0 {
h += y
y = 0
}
if x+w > sx {
w = sx - x
}
if y+h > sy {
h = sy - y
}
if w <= 0 || h <= 0 {
return
}
filler.FillRectangle(x, y, w, h, color)
}

// hLine draws a horizontal line between x0 and x1
func hLine(display drivers.Displayer, x0 int16, x1 int16, y int16, color color.RGBA) {
if x0 > x1 {
x0, x1 = x1, x0
}
fillRectangle(display, x0, y, x1-x0+1, 1, color)
}

// vLine draws a vertical line between y0 and y1
func vLine(display drivers.Displayer, x int16, y0 int16, y1 int16, color color.RGBA) {
if y0 > y1 {
y0, y1 = y1, y0
}
fillRectangle(display, x, y0, 1, y1-y0+1, color)
}

// Line draws a line between two points
func Line(display drivers.Displayer, x0 int16, y0 int16, x1 int16, y1 int16, color color.RGBA) {
if x0 == x1 {
if y0 > y1 {
y0, y1 = y1, y0
}
for ; y0 <= y1; y0++ {
display.SetPixel(x0, y0, color)
}
vLine(display, x0, y0, y1, color)
} else if y0 == y1 {
if x0 > x1 {
x0, x1 = x1, x0
}
for ; x0 <= x1; x0++ {
display.SetPixel(x0, y0, color)
}
hLine(display, x0, x1, y0, color)
} else { // Bresenham
dx := x1 - x0
if dx < 0 {
Expand Down Expand Up @@ -82,9 +126,7 @@ func FilledRectangle(display drivers.Displayer, x int16, y int16, w int16, h int
if w <= 0 || h <= 0 {
return errEmptyRectangle
}
for i := x; i < x+w; i++ {
Line(display, i, y, i, y+h-1, color)
}
fillRectangle(display, x, y, w, h, color)
return nil
}

Expand Down
177 changes: 150 additions & 27 deletions tinydraw_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package tinydraw

import (
"errors"
"fmt"
"image"
"image/color"
Expand All @@ -10,6 +11,8 @@ import (
"path/filepath"
"testing"
"time"

"tinygo.org/x/drivers"
)

// Set to true to generate missing reference images for new tests
Expand Down Expand Up @@ -101,11 +104,45 @@ func (d *testDisplay) Display() error {
return nil
}

// fillerDisplay is a testDisplay with FillRectangle, as strict as the drivers
type fillerDisplay struct {
testDisplay
}

func (d *fillerDisplay) FillRectangle(x, y, width, height int16, c color.RGBA) error {
w, h := d.Size()
if x < 0 || y < 0 || width <= 0 || height <= 0 || x+width > w || y+height > h {
return errors.New("rectangle coordinates outside display area")
}
for i := x; i < x+width; i++ {
for j := y; j < y+height; j++ {
d.SetPixel(i, j, c)
}
}
return nil
}

type displayUnderTest struct {
name string
display drivers.Displayer
image testDisplay
}

// newTestDisplays returns a display without and a display with FillRectangle
func newTestDisplays() []displayUnderTest {
plain := newTestDisplay()
filler := &fillerDisplay{newTestDisplay()}
return []displayUnderTest{
{"SetPixel", &plain, plain},
{"FillRectangle", filler, filler.testDisplay},
}
}

func TestFilledTriangleClockMinuteHand(t *testing.T) {

black := color.RGBA{0, 0, 0, 255}
unfilledDisplay := newTestDisplay()
filledDisplay := newTestDisplay()
filledDisplays := newTestDisplays()

x := int16(160)
y := int16(120)
Expand All @@ -130,13 +167,14 @@ func TestFilledTriangleClockMinuteHand(t *testing.T) {
x2 := x + int16(minuteRadius*mx)
y2 := y - int16(minuteRadius*my)

FilledTriangle(

&filledDisplay,
x0, y0,
x1, y1,
x2, y2,
black)
for _, d := range filledDisplays {
FilledTriangle(
d.display,
x0, y0,
x1, y1,
x2, y2,
black)
}

Triangle(
&unfilledDisplay,
Expand All @@ -148,12 +186,14 @@ func TestFilledTriangleClockMinuteHand(t *testing.T) {
testTime = testTime.Add(1 * time.Minute)
}

err := compareWithReference("TestFilledTriangleClockMinuteHand", filledDisplay)
if err != nil {
t.Errorf("Fail %v", err)
for _, d := range filledDisplays {
err := compareWithReference("TestFilledTriangleClockMinuteHand", d.image)
if err != nil {
t.Errorf("Fail on %v display: %v", d.name, err)
}
}

err = compareWithReference("TestTriangleClockMinuteHand", unfilledDisplay)
err := compareWithReference("TestTriangleClockMinuteHand", unfilledDisplay)
if err != nil {
t.Errorf("Fail %v", err)
}
Expand All @@ -179,9 +219,12 @@ func TestTriangles(t *testing.T) {
{"HorizLine", 80, 200, 160, 200, 213, 200}, //Horizontal line
{"VertLine", 80, 80, 80, 120, 80, 200}, //Vertical line

{"ClockHand1", 160, 115, 160, 125, 255, 120}, //Previously rendered too Wide
{"ClockHand2", 160, 116, 160, 124, 254, 129}, //Previusly fill extendended to the left outside of triangle
{"ClockHand3", 165, 120, 155, 120, 160, 215}, //Too Wide
{"ClockHand1", 160, 115, 160, 125, 255, 120}, //Previously rendered too Wide
{"ClockHand2", 160, 116, 160, 124, 254, 129}, //Previusly fill extendended to the left outside of triangle
{"ClockHand3", 165, 120, 155, 120, 160, 215}, //Too Wide
{"OffLeft", -40, 20, 100, 60, 30, 200}, //Partly outside the display
{"OffBottomRight", 250, 180, 360, 200, 200, 280}, //Partly outside the display
{"Wide", 20, 100, 300, 120, 160, 140}, //Wider than high
}

colors := []color.RGBA{
Expand Down Expand Up @@ -210,19 +253,99 @@ func TestTriangles(t *testing.T) {
t.Errorf("Fail %v", err)
}

filledDisplay := newTestDisplay()
for _, d := range newTestDisplays() {
//Test all permutations of coordinates
FilledTriangle(d.display, test.x0, test.y0, test.x1, test.y1, test.x2, test.y2, colors[0])
FilledTriangle(d.display, test.x0, test.y0, test.x2, test.y2, test.x1, test.y1, colors[1])
FilledTriangle(d.display, test.x1, test.y1, test.x0, test.y0, test.x2, test.y2, colors[2])
FilledTriangle(d.display, test.x1, test.y1, test.x2, test.y2, test.x0, test.y0, colors[3])
FilledTriangle(d.display, test.x2, test.y2, test.x0, test.y0, test.x1, test.y1, colors[4])
FilledTriangle(d.display, test.x2, test.y2, test.x1, test.y1, test.x0, test.y0, colors[5])

//Test all permutations of coordinates
FilledTriangle(&filledDisplay, test.x0, test.y0, test.x1, test.y1, test.x2, test.y2, colors[0])
FilledTriangle(&filledDisplay, test.x0, test.y0, test.x2, test.y2, test.x1, test.y1, colors[1])
FilledTriangle(&filledDisplay, test.x1, test.y1, test.x0, test.y0, test.x2, test.y2, colors[2])
FilledTriangle(&filledDisplay, test.x1, test.y1, test.x2, test.y2, test.x0, test.y0, colors[3])
FilledTriangle(&filledDisplay, test.x2, test.y2, test.x0, test.y0, test.x1, test.y1, colors[4])
FilledTriangle(&filledDisplay, test.x2, test.y2, test.x1, test.y1, test.x0, test.y0, colors[5])

err = compareWithReference(fmt.Sprintf("TestTriangles_%v_Filled", test.name), filledDisplay)
if err != nil {
t.Errorf("Fail %v", err)
err = compareWithReference(fmt.Sprintf("TestTriangles_%v_Filled", test.name), d.image)
if err != nil {
t.Errorf("Fail on %v display: %v", d.name, err)
}
}
}
}

type rectangleTest struct {
name string
x int16
y int16
w int16
h int16
}

func TestFilledRectangles(t *testing.T) {
tests := []rectangleTest{
{"Inside", 40, 30, 200, 150},
{"OnePixel", 160, 120, 1, 1},
{"HorizLine", 20, 100, 280, 1},
{"VertLine", 100, 20, 1, 200},
{"OffTopLeft", -20, -30, 100, 80},
{"OffBottomRight", 260, 200, 100, 80},
{"LargerThanDisplay", -10, -10, 340, 260},
{"FullyOff", 400, 300, 20, 20},
}

for _, test := range tests {
for _, d := range newTestDisplays() {
FilledRectangle(d.display, test.x, test.y, test.w, test.h, color.RGBA{255, 0, 0, 255})

err := compareWithReference(fmt.Sprintf("TestFilledRectangles_%v", test.name), d.image)
if err != nil {
t.Errorf("Fail on %v display: %v", d.name, err)
}
}
}
}

type circleTest struct {
name string
x int16
y int16
r int16
}

func TestFilledCircles(t *testing.T) {
tests := []circleTest{
{"Large", 160, 120, 100},
{"Small", 160, 120, 5},
{"Radius1", 160, 120, 1},
{"Radius0", 160, 120, 0},
{"OffTopLeft", 10, 20, 60},
{"OffBottomRight", 300, 230, 60},
}

for _, test := range tests {
for _, d := range newTestDisplays() {
FilledCircle(d.display, test.x, test.y, test.r, color.RGBA{0, 0, 255, 255})

err := compareWithReference(fmt.Sprintf("TestFilledCircles_%v", test.name), d.image)
if err != nil {
t.Errorf("Fail on %v display: %v", d.name, err)
}
}
}
}

func TestRectangles(t *testing.T) {
tests := []rectangleTest{
{"Inside", 40, 30, 200, 150},
{"OffTopLeft", -20, -30, 100, 80},
{"OffBottomRight", 260, 200, 100, 80},
}

for _, test := range tests {
for _, d := range newTestDisplays() {
Rectangle(d.display, test.x, test.y, test.w, test.h, color.RGBA{0, 255, 0, 255})

err := compareWithReference(fmt.Sprintf("TestRectangles_%v", test.name), d.image)
if err != nil {
t.Errorf("Fail on %v display: %v", d.name, err)
}
}
}
}