diff --git a/_examples/text_rotation/main.go b/_examples/text_rotation/main.go index 76bb2b0..53ca040 100644 --- a/_examples/text_rotation/main.go +++ b/_examples/text_rotation/main.go @@ -32,11 +32,17 @@ func drawChart(res http.ResponseWriter, req *http.Request) { tbc := tb.Corners().Rotate(45) - chart.Draw.BoxCorners(r, tbc, chart.Style{ + chart.Draw.Box2d(r, tbc, chart.Style{ StrokeColor: drawing.ColorRed, StrokeWidth: 2, }) + tbc2 := tbc.Shift(tbc.Height(), 0) + chart.Draw.Box2d(r, tbc2, chart.Style{ + StrokeColor: drawing.ColorGreen, + StrokeWidth: 2, + }) + tbcb := tbc.Box() chart.Draw.Box(r, tbcb, chart.Style{ StrokeColor: drawing.ColorBlue, diff --git a/box.go b/box.go index c59ab69..c6c0baa 100644 --- a/box.go +++ b/box.go @@ -2,7 +2,6 @@ package chart import ( "fmt" - "math" util "github.com/wcharczuk/go-chart/util" ) @@ -166,12 +165,12 @@ func (b Box) Shift(x, y int) Box { } // Corners returns the box as a set of corners. -func (b Box) Corners() BoxCorners { - return BoxCorners{ - TopLeft: Point{b.Left, b.Top}, - TopRight: Point{b.Right, b.Top}, - BottomRight: Point{b.Right, b.Bottom}, - BottomLeft: Point{b.Left, b.Bottom}, +func (b Box) Corners() Box2d { + return Box2d{ + TopLeft: Point{float64(b.Left), float64(b.Top)}, + TopRight: Point{float64(b.Right), float64(b.Top)}, + BottomRight: Point{float64(b.Right), float64(b.Bottom)}, + BottomLeft: Point{float64(b.Left), float64(b.Bottom)}, } } @@ -255,99 +254,3 @@ func (b Box) OuterConstrain(bounds, other Box) Box { } return newBox } - -// BoxCorners is a box with independent corners. -type BoxCorners struct { - TopLeft, TopRight, BottomRight, BottomLeft Point -} - -// Box return the BoxCorners as a regular box. -func (bc BoxCorners) Box() Box { - return Box{ - Top: util.Math.MinInt(bc.TopLeft.Y, bc.TopRight.Y), - Left: util.Math.MinInt(bc.TopLeft.X, bc.BottomLeft.X), - Right: util.Math.MaxInt(bc.TopRight.X, bc.BottomRight.X), - Bottom: util.Math.MaxInt(bc.BottomLeft.Y, bc.BottomRight.Y), - } -} - -// Width returns the width -func (bc BoxCorners) Width() int { - minLeft := util.Math.MinInt(bc.TopLeft.X, bc.BottomLeft.X) - maxRight := util.Math.MaxInt(bc.TopRight.X, bc.BottomRight.X) - return maxRight - minLeft -} - -// Height returns the height -func (bc BoxCorners) Height() int { - minTop := util.Math.MinInt(bc.TopLeft.Y, bc.TopRight.Y) - maxBottom := util.Math.MaxInt(bc.BottomLeft.Y, bc.BottomRight.Y) - return maxBottom - minTop -} - -// Center returns the center of the box -func (bc BoxCorners) Center() (x, y int) { - - left := util.Math.MeanInt(bc.TopLeft.X, bc.BottomLeft.X) - right := util.Math.MeanInt(bc.TopRight.X, bc.BottomRight.X) - x = ((right - left) >> 1) + left - - top := util.Math.MeanInt(bc.TopLeft.Y, bc.TopRight.Y) - bottom := util.Math.MeanInt(bc.BottomLeft.Y, bc.BottomRight.Y) - y = ((bottom - top) >> 1) + top - - return -} - -// Rotate rotates the box. -func (bc BoxCorners) Rotate(thetaDegrees float64) BoxCorners { - cx, cy := bc.Center() - - thetaRadians := util.Math.DegreesToRadians(thetaDegrees) - - tlx, tly := util.Math.RotateCoordinate(cx, cy, bc.TopLeft.X, bc.TopLeft.Y, thetaRadians) - trx, try := util.Math.RotateCoordinate(cx, cy, bc.TopRight.X, bc.TopRight.Y, thetaRadians) - brx, bry := util.Math.RotateCoordinate(cx, cy, bc.BottomRight.X, bc.BottomRight.Y, thetaRadians) - blx, bly := util.Math.RotateCoordinate(cx, cy, bc.BottomLeft.X, bc.BottomLeft.Y, thetaRadians) - - return BoxCorners{ - TopLeft: Point{tlx, tly}, - TopRight: Point{trx, try}, - BottomRight: Point{brx, bry}, - BottomLeft: Point{blx, bly}, - } -} - -// Equals returns if the box equals another box. -func (bc BoxCorners) Equals(other BoxCorners) bool { - return bc.TopLeft.Equals(other.TopLeft) && - bc.TopRight.Equals(other.TopRight) && - bc.BottomRight.Equals(other.BottomRight) && - bc.BottomLeft.Equals(other.BottomLeft) -} - -func (bc BoxCorners) String() string { - return fmt.Sprintf("BoxC{%s,%s,%s,%s}", bc.TopLeft.String(), bc.TopRight.String(), bc.BottomRight.String(), bc.BottomLeft.String()) -} - -// Point is an X,Y pair -type Point struct { - X, Y int -} - -// DistanceTo calculates the distance to another point. -func (p Point) DistanceTo(other Point) float64 { - dx := math.Pow(float64(p.X-other.X), 2) - dy := math.Pow(float64(p.Y-other.Y), 2) - return math.Pow(dx+dy, 0.5) -} - -// Equals returns if a point equals another point. -func (p Point) Equals(other Point) bool { - return p.X == other.X && p.Y == other.Y -} - -// String returns a string representation of the point. -func (p Point) String() string { - return fmt.Sprintf("P{%d,%d}", p.X, p.Y) -} diff --git a/box_2d.go b/box_2d.go new file mode 100644 index 0000000..8648422 --- /dev/null +++ b/box_2d.go @@ -0,0 +1,181 @@ +package chart + +import ( + "fmt" + "math" + + util "github.com/wcharczuk/go-chart/util" +) + +// Box2d is a box with (4) independent corners. +// It is used when dealing with ~rotated~ boxes. +type Box2d struct { + TopLeft, TopRight, BottomRight, BottomLeft Point +} + +// Points returns the constituent points of the box. +func (bc Box2d) Points() []Point { + return []Point{ + bc.TopRight, + bc.BottomRight, + bc.BottomLeft, + bc.TopLeft, + } +} + +// Box return the Box2d as a regular box. +func (bc Box2d) Box() Box { + return Box{ + Top: int(math.Min(bc.TopLeft.Y, bc.TopRight.Y)), + Left: int(math.Min(bc.TopLeft.X, bc.BottomLeft.X)), + Right: int(math.Max(bc.TopRight.X, bc.BottomRight.X)), + Bottom: int(math.Max(bc.BottomLeft.Y, bc.BottomRight.Y)), + } +} + +// Width returns the width +func (bc Box2d) Width() float64 { + minLeft := math.Min(bc.TopLeft.X, bc.BottomLeft.X) + maxRight := math.Max(bc.TopRight.X, bc.BottomRight.X) + return maxRight - minLeft +} + +// Height returns the height +func (bc Box2d) Height() float64 { + minTop := math.Min(bc.TopLeft.Y, bc.TopRight.Y) + maxBottom := math.Max(bc.BottomLeft.Y, bc.BottomRight.Y) + return maxBottom - minTop +} + +// Center returns the center of the box +func (bc Box2d) Center() (x, y float64) { + left := util.Math.Mean(bc.TopLeft.X, bc.BottomLeft.X) + right := util.Math.Mean(bc.TopRight.X, bc.BottomRight.X) + x = ((right - left) / 2.0) + left + + top := util.Math.Mean(bc.TopLeft.Y, bc.TopRight.Y) + bottom := util.Math.Mean(bc.BottomLeft.Y, bc.BottomRight.Y) + y = ((bottom - top) / 2.0) + top + + return +} + +// Rotate rotates the box. +func (bc Box2d) Rotate(thetaDegrees float64) Box2d { + cx, cy := bc.Center() + + thetaRadians := util.Math.DegreesToRadians(thetaDegrees) + + tlx, tly := util.Math.RotateCoordinate(int(cx), int(cy), int(bc.TopLeft.X), int(bc.TopLeft.Y), thetaRadians) + trx, try := util.Math.RotateCoordinate(int(cx), int(cy), int(bc.TopRight.X), int(bc.TopRight.Y), thetaRadians) + brx, bry := util.Math.RotateCoordinate(int(cx), int(cy), int(bc.BottomRight.X), int(bc.BottomRight.Y), thetaRadians) + blx, bly := util.Math.RotateCoordinate(int(cx), int(cy), int(bc.BottomLeft.X), int(bc.BottomLeft.Y), thetaRadians) + + return Box2d{ + TopLeft: Point{float64(tlx), float64(tly)}, + TopRight: Point{float64(trx), float64(try)}, + BottomRight: Point{float64(brx), float64(bry)}, + BottomLeft: Point{float64(blx), float64(bly)}, + } +} + +// Shift shifts a box by a given x and y value. +func (bc Box2d) Shift(x, y float64) Box2d { + return Box2d{ + TopLeft: bc.TopLeft.Shift(x, y), + TopRight: bc.TopRight.Shift(x, y), + BottomRight: bc.BottomRight.Shift(x, y), + BottomLeft: bc.BottomLeft.Shift(x, y), + } +} + +// Equals returns if the box equals another box. +func (bc Box2d) Equals(other Box2d) bool { + return bc.TopLeft.Equals(other.TopLeft) && + bc.TopRight.Equals(other.TopRight) && + bc.BottomRight.Equals(other.BottomRight) && + bc.BottomLeft.Equals(other.BottomLeft) +} + +// Overlaps returns if two boxes overlap. +func (bc Box2d) Overlaps(other Box2d) bool { + for _, polygon := range []Box2d{bc, other} { + points := polygon.Points() + for i1 := 0; i1 < len(points); i1++ { + i2 := (i1 + 1) % len(points) + + p1 := polygon.Points()[i1] + p2 := polygon.Points()[i2] + + normal := Point{X: p2.Y - p1.Y, Y: p1.X - p2.X} + + minA := math.MaxFloat64 + maxA := -math.MaxFloat64 + + for _, p := range bc.Points() { + projected := normal.X*p.X + normal.Y*p.Y + + if projected < minA { + minA = projected + } + if projected > maxA { + maxA = projected + } + } + + minB := math.MaxFloat64 + maxB := -math.MaxFloat64 + + for _, p := range other.Points() { + projected := normal.X*p.X + normal.Y*p.Y + + if projected < minB { + minB = projected + } + if projected > maxB { + maxB = projected + } + } + + if maxA < minB || maxB < minA { + return false + } + } + } + + return true +} + +func (bc Box2d) String() string { + return fmt.Sprintf("BoxC{%s,%s,%s,%s}", bc.TopLeft.String(), bc.TopRight.String(), bc.BottomRight.String(), bc.BottomLeft.String()) +} + +// Point is an X,Y pair +type Point struct { + X, Y float64 +} + +// Shift shifts a point. +func (p Point) Shift(x, y float64) Point { + return Point{ + X: p.X + x, + Y: p.Y + y, + } +} + +// DistanceTo calculates the distance to another point. +func (p Point) DistanceTo(other Point) float64 { + dx := math.Pow(float64(p.X-other.X), 2) + dy := math.Pow(float64(p.Y-other.Y), 2) + return math.Pow(dx+dy, 0.5) +} + +// Equals returns if a point equals another point. +func (p Point) Equals(other Point) bool { + return p.X == other.X && p.Y == other.Y +} + +// String returns a string representation of the point. +func (p Point) String() string { + return fmt.Sprintf("P{%.2f,%.2f}", p.X, p.Y) +} diff --git a/box_2d_test.go b/box_2d_test.go new file mode 100644 index 0000000..07a01dc --- /dev/null +++ b/box_2d_test.go @@ -0,0 +1,66 @@ +package chart + +import ( + "fmt" + "testing" + + assert "github.com/blendlabs/go-assert" +) + +func TestBox2dCenter(t *testing.T) { + assert := assert.New(t) + + bc := Box2d{ + TopLeft: Point{5, 5}, + TopRight: Point{15, 5}, + BottomRight: Point{15, 15}, + BottomLeft: Point{5, 15}, + } + + cx, cy := bc.Center() + assert.Equal(10, cx) + assert.Equal(10, cy) +} + +func TestBox2dRotate(t *testing.T) { + assert := assert.New(t) + + bc := Box2d{ + TopLeft: Point{5, 5}, + TopRight: Point{15, 5}, + BottomRight: Point{15, 15}, + BottomLeft: Point{5, 15}, + } + + rotated := bc.Rotate(45) + assert.True(rotated.TopLeft.Equals(Point{10, 3}), rotated.String()) +} + +func TestBox2dOverlaps(t *testing.T) { + assert := assert.New(t) + + bc := Box2d{ + TopLeft: Point{5, 5}, + TopRight: Point{15, 5}, + BottomRight: Point{15, 15}, + BottomLeft: Point{5, 15}, + } + + // shift meaningfully the full width of bc right. + bc2 := bc.Shift(bc.Width()+1, 0) + assert.False(bc.Overlaps(bc2), fmt.Sprintf("%v\n\t\tshould not overlap\n\t%v", bc, bc2)) + + // shift meaningfully the full height of bc down. + bc3 := bc.Shift(0, bc.Height()+1) + assert.False(bc.Overlaps(bc3), fmt.Sprintf("%v\n\t\tshould not overlap\n\t%v", bc, bc3)) + + bc4 := bc.Shift(5, 0) + assert.True(bc.Overlaps(bc4)) + + bc5 := bc.Shift(0, 5) + assert.True(bc.Overlaps(bc5)) + + bcr := bc.Rotate(45) + bcr2 := bc.Rotate(45).Shift(bc.Height(), 0) + assert.False(bcr.Overlaps(bcr2), fmt.Sprintf("%v\n\t\tshould not overlap\n\t%v", bcr, bcr2)) +} diff --git a/box_test.go b/box_test.go index 3f3fa02..4c0b18a 100644 --- a/box_test.go +++ b/box_test.go @@ -157,32 +157,3 @@ func TestBoxCenter(t *testing.T) { assert.Equal(15, cx) assert.Equal(20, cy) } - -func TestBoxCornersCenter(t *testing.T) { - assert := assert.New(t) - - bc := BoxCorners{ - TopLeft: Point{5, 5}, - TopRight: Point{15, 5}, - BottomRight: Point{15, 15}, - BottomLeft: Point{5, 15}, - } - - cx, cy := bc.Center() - assert.Equal(10, cx) - assert.Equal(10, cy) -} - -func TestBoxCornersRotate(t *testing.T) { - assert := assert.New(t) - - bc := BoxCorners{ - TopLeft: Point{5, 5}, - TopRight: Point{15, 5}, - BottomRight: Point{15, 15}, - BottomLeft: Point{5, 15}, - } - - rotated := bc.Rotate(45) - assert.True(rotated.TopLeft.Equals(Point{10, 3}), rotated.String()) -} diff --git a/draw.go b/draw.go index ed17404..612adb3 100644 --- a/draw.go +++ b/draw.go @@ -314,17 +314,17 @@ func (d draw) Box(r Renderer, b Box, s Style) { } func (d draw) BoxRotated(r Renderer, b Box, thetaDegrees float64, s Style) { - d.BoxCorners(r, b.Corners().Rotate(thetaDegrees), s) + d.Box2d(r, b.Corners().Rotate(thetaDegrees), s) } -func (d draw) BoxCorners(r Renderer, bc BoxCorners, s Style) { +func (d draw) Box2d(r Renderer, bc Box2d, s Style) { s.GetFillAndStrokeOptions().WriteToRenderer(r) defer r.ResetStyle() - r.MoveTo(bc.TopLeft.X, bc.TopLeft.Y) - r.LineTo(bc.TopRight.X, bc.TopRight.Y) - r.LineTo(bc.BottomRight.X, bc.BottomRight.Y) - r.LineTo(bc.BottomLeft.X, bc.BottomLeft.Y) + r.MoveTo(int(bc.TopLeft.X), int(bc.TopLeft.Y)) + r.LineTo(int(bc.TopRight.X), int(bc.TopRight.Y)) + r.LineTo(int(bc.BottomRight.X), int(bc.BottomRight.Y)) + r.LineTo(int(bc.BottomLeft.X), int(bc.BottomLeft.Y)) r.Close() r.FillStroke() }