diff --git a/testdata/TestFilledCircles_Large.png b/testdata/TestFilledCircles_Large.png new file mode 100644 index 0000000..7aa7555 Binary files /dev/null and b/testdata/TestFilledCircles_Large.png differ diff --git a/testdata/TestFilledCircles_OffBottomRight.png b/testdata/TestFilledCircles_OffBottomRight.png new file mode 100644 index 0000000..4e66727 Binary files /dev/null and b/testdata/TestFilledCircles_OffBottomRight.png differ diff --git a/testdata/TestFilledCircles_OffTopLeft.png b/testdata/TestFilledCircles_OffTopLeft.png new file mode 100644 index 0000000..6b5ec7c Binary files /dev/null and b/testdata/TestFilledCircles_OffTopLeft.png differ diff --git a/testdata/TestFilledCircles_Radius0.png b/testdata/TestFilledCircles_Radius0.png new file mode 100644 index 0000000..c258f95 Binary files /dev/null and b/testdata/TestFilledCircles_Radius0.png differ diff --git a/testdata/TestFilledCircles_Radius1.png b/testdata/TestFilledCircles_Radius1.png new file mode 100644 index 0000000..06bc2a0 Binary files /dev/null and b/testdata/TestFilledCircles_Radius1.png differ diff --git a/testdata/TestFilledCircles_Small.png b/testdata/TestFilledCircles_Small.png new file mode 100644 index 0000000..ddacb33 Binary files /dev/null and b/testdata/TestFilledCircles_Small.png differ diff --git a/testdata/TestFilledRectangles_FullyOff.png b/testdata/TestFilledRectangles_FullyOff.png new file mode 100644 index 0000000..0a8e3f8 Binary files /dev/null and b/testdata/TestFilledRectangles_FullyOff.png differ diff --git a/testdata/TestFilledRectangles_HorizLine.png b/testdata/TestFilledRectangles_HorizLine.png new file mode 100644 index 0000000..542b2db Binary files /dev/null and b/testdata/TestFilledRectangles_HorizLine.png differ diff --git a/testdata/TestFilledRectangles_Inside.png b/testdata/TestFilledRectangles_Inside.png new file mode 100644 index 0000000..9485e95 Binary files /dev/null and b/testdata/TestFilledRectangles_Inside.png differ diff --git a/testdata/TestFilledRectangles_LargerThanDisplay.png b/testdata/TestFilledRectangles_LargerThanDisplay.png new file mode 100644 index 0000000..2ef4c71 Binary files /dev/null and b/testdata/TestFilledRectangles_LargerThanDisplay.png differ diff --git a/testdata/TestFilledRectangles_OffBottomRight.png b/testdata/TestFilledRectangles_OffBottomRight.png new file mode 100644 index 0000000..3a6ce84 Binary files /dev/null and b/testdata/TestFilledRectangles_OffBottomRight.png differ diff --git a/testdata/TestFilledRectangles_OffTopLeft.png b/testdata/TestFilledRectangles_OffTopLeft.png new file mode 100644 index 0000000..050430c Binary files /dev/null and b/testdata/TestFilledRectangles_OffTopLeft.png differ diff --git a/testdata/TestFilledRectangles_OnePixel.png b/testdata/TestFilledRectangles_OnePixel.png new file mode 100644 index 0000000..5a1d897 Binary files /dev/null and b/testdata/TestFilledRectangles_OnePixel.png differ diff --git a/testdata/TestFilledRectangles_VertLine.png b/testdata/TestFilledRectangles_VertLine.png new file mode 100644 index 0000000..55d5c54 Binary files /dev/null and b/testdata/TestFilledRectangles_VertLine.png differ diff --git a/testdata/TestRectangles_Inside.png b/testdata/TestRectangles_Inside.png new file mode 100644 index 0000000..c7e7b2e Binary files /dev/null and b/testdata/TestRectangles_Inside.png differ diff --git a/testdata/TestRectangles_OffBottomRight.png b/testdata/TestRectangles_OffBottomRight.png new file mode 100644 index 0000000..9703eb6 Binary files /dev/null and b/testdata/TestRectangles_OffBottomRight.png differ diff --git a/testdata/TestRectangles_OffTopLeft.png b/testdata/TestRectangles_OffTopLeft.png new file mode 100644 index 0000000..546350f Binary files /dev/null and b/testdata/TestRectangles_OffTopLeft.png differ diff --git a/testdata/TestTriangles_OffBottomRight.png b/testdata/TestTriangles_OffBottomRight.png new file mode 100644 index 0000000..a6fee16 Binary files /dev/null and b/testdata/TestTriangles_OffBottomRight.png differ diff --git a/testdata/TestTriangles_OffBottomRight_Filled.png b/testdata/TestTriangles_OffBottomRight_Filled.png new file mode 100644 index 0000000..b6e6675 Binary files /dev/null and b/testdata/TestTriangles_OffBottomRight_Filled.png differ diff --git a/testdata/TestTriangles_OffLeft.png b/testdata/TestTriangles_OffLeft.png new file mode 100644 index 0000000..ba270fc Binary files /dev/null and b/testdata/TestTriangles_OffLeft.png differ diff --git a/testdata/TestTriangles_OffLeft_Filled.png b/testdata/TestTriangles_OffLeft_Filled.png new file mode 100644 index 0000000..ea566aa Binary files /dev/null and b/testdata/TestTriangles_OffLeft_Filled.png differ diff --git a/testdata/TestTriangles_Wide.png b/testdata/TestTriangles_Wide.png new file mode 100644 index 0000000..86fe94d Binary files /dev/null and b/testdata/TestTriangles_Wide.png differ diff --git a/testdata/TestTriangles_Wide_Filled.png b/testdata/TestTriangles_Wide_Filled.png new file mode 100644 index 0000000..f49855f Binary files /dev/null and b/testdata/TestTriangles_Wide_Filled.png differ diff --git a/tinydraw.go b/tinydraw.go index e1f1e06..5e9c04b 100644 --- a/tinydraw.go +++ b/tinydraw.go @@ -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 { @@ -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 } diff --git a/tinydraw_test.go b/tinydraw_test.go index 161265f..ab4939c 100644 --- a/tinydraw_test.go +++ b/tinydraw_test.go @@ -1,6 +1,7 @@ package tinydraw import ( + "errors" "fmt" "image" "image/color" @@ -10,6 +11,8 @@ import ( "path/filepath" "testing" "time" + + "tinygo.org/x/drivers" ) // Set to true to generate missing reference images for new tests @@ -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) @@ -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, @@ -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) } @@ -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{ @@ -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) + } } } }