// Copyright 2019 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package draw // braille_circle.go contains code that draws circles on a braille canvas. import ( "fmt" "image" "github.com/mum4k/termdash/cell" "github.com/mum4k/termdash/private/canvas/braille" "github.com/mum4k/termdash/private/numbers/trig" ) // BrailleCircleOption is used to provide options to BrailleCircle. type BrailleCircleOption interface { // set sets the provided option. set(*brailleCircleOptions) } // brailleCircleOptions stores the provided options. type brailleCircleOptions struct { cellOpts []cell.Option filled bool pixelChange braillePixelChange arcOnly bool startDegree int endDegree int } // newBrailleCircleOptions returns a new brailleCircleOptions instance. func newBrailleCircleOptions() *brailleCircleOptions { return &brailleCircleOptions{ pixelChange: braillePixelChangeSet, } } // validate validates the provided options. func (opts *brailleCircleOptions) validate() error { if !opts.arcOnly { return nil } if opts.startDegree == opts.endDegree { return fmt.Errorf("invalid degree range, start %d and end %d cannot be equal", opts.startDegree, opts.endDegree) } return nil } // brailleCircleOption implements BrailleCircleOption. type brailleCircleOption func(*brailleCircleOptions) // set implements BrailleCircleOption.set. func (o brailleCircleOption) set(opts *brailleCircleOptions) { o(opts) } // BrailleCircleCellOpts sets options on the cells that contain the circle. // Cell options on a braille canvas can only be set on the entire cell, not per // pixel. func BrailleCircleCellOpts(cOpts ...cell.Option) BrailleCircleOption { return brailleCircleOption(func(opts *brailleCircleOptions) { opts.cellOpts = cOpts }) } // BrailleCircleFilled indicates that the drawn circle should be filled. func BrailleCircleFilled() BrailleCircleOption { return brailleCircleOption(func(opts *brailleCircleOptions) { opts.filled = true }) } // BrailleCircleArcOnly indicates that only a portion of the circle should be drawn. // The arc will be between the two provided angles in degrees. // Each angle must be in range 0 <= angle <= 360. Start and end must not be equal. // The zero angle is on the X axis, angles grow counter-clockwise. func BrailleCircleArcOnly(startDegree, endDegree int) BrailleCircleOption { return brailleCircleOption(func(opts *brailleCircleOptions) { opts.arcOnly = true opts.startDegree = startDegree opts.endDegree = endDegree }) } // BrailleCircleClearPixels changes the behavior of BrailleCircle, so that it // clears the pixels belonging to the circle instead of setting them. // Useful in order to "erase" a circle from the canvas as opposed to drawing one. func BrailleCircleClearPixels() BrailleCircleOption { return brailleCircleOption(func(opts *brailleCircleOptions) { opts.pixelChange = braillePixelChangeClear }) } // BrailleCircle draws an approximated circle with the specified mid point and radius. // The mid point must be a valid pixel within the canvas. // All the points that form the circle must fit into the canvas. // The smallest valid radius is two. func BrailleCircle(bc *braille.Canvas, mid image.Point, radius int, opts ...BrailleCircleOption) error { if ar := bc.Area(); !mid.In(ar) { return fmt.Errorf("unable to draw circle with mid point %v which is outside of the braille canvas area %v", mid, ar) } if min := 2; radius < min { return fmt.Errorf("unable to draw circle with radius %d, must be in range %d <= radius", radius, min) } opt := newBrailleCircleOptions() for _, o := range opts { o.set(opt) } if err := opt.validate(); err != nil { return err } points := circlePoints(mid, radius) if opt.arcOnly { f, err := trig.FilterByAngle(points, mid, opt.startDegree, opt.endDegree) if err != nil { return err } points = f if opt.filled && (opt.startDegree != 0 || opt.endDegree != 360) { points = append(points, openingPoints(mid, radius, opt)...) } } if err := drawPoints(bc, points, opt); err != nil { return fmt.Errorf("failed to draw circle with mid:%v, radius:%d, start:%d degrees, end:%d degrees: %v", mid, radius, opt.startDegree, opt.endDegree, err) } if opt.filled { return fillCircle(bc, points, mid, radius, opt) } return nil } // drawPoints draws the points onto the canvas. func drawPoints(bc *braille.Canvas, points []image.Point, opt *brailleCircleOptions) error { for _, p := range points { switch opt.pixelChange { case braillePixelChangeSet: if err := bc.SetPixel(p, opt.cellOpts...); err != nil { return fmt.Errorf("SetPixel => %v", err) } case braillePixelChangeClear: if err := bc.ClearPixel(p, opt.cellOpts...); err != nil { return fmt.Errorf("ClearPixel => %v", err) } } } return nil } // fillCircle fills a circle that consists of the provided point and has the // mid point and radius. func fillCircle(bc *braille.Canvas, points []image.Point, mid image.Point, radius int, opt *brailleCircleOptions) error { lineOpts := []BrailleLineOption{ BrailleLineCellOpts(opt.cellOpts...), } fillOpts := []BrailleFillOption{ BrailleFillCellOpts(opt.cellOpts...), } if opt.pixelChange == braillePixelChangeClear { lineOpts = append(lineOpts, BrailleLineClearPixels()) fillOpts = append(fillOpts, BrailleFillClearPixels()) } // Determine a fill point that should be inside of the circle sector. midA, err := trig.RangeMid(opt.startDegree, opt.endDegree) if err != nil { return err } fp := trig.CirclePointAtAngle(midA, mid, radius-1) // Ensure the fill point falls inside the circle. // If drawing a partial circle, it must also fall within points belonging // to the opening. // This might not be true if drawing a partial circle and the arc is very // small. shape := points if opt.arcOnly { startP := trig.CirclePointAtAngle(opt.startDegree, mid, radius-1) endP := trig.CirclePointAtAngle(opt.endDegree, mid, radius-1) shape = append(shape, startP, endP) } if trig.PointIsIn(fp, shape) { if err := BrailleFill(bc, fp, points, fillOpts...); err != nil { return err } if err := BrailleLine(bc, mid, fp, lineOpts...); err != nil { return err } } return nil } // openingPoints returns points on the lines from the mid point to the circle // opening when drawing an incomplete circle. func openingPoints(mid image.Point, radius int, opt *brailleCircleOptions) []image.Point { var points []image.Point startP := trig.CirclePointAtAngle(opt.startDegree, mid, radius) endP := trig.CirclePointAtAngle(opt.endDegree, mid, radius) points = append(points, brailleLinePoints(mid, startP)...) points = append(points, brailleLinePoints(mid, endP)...) return points } // circlePoints returns a list of points that represent a circle with // the specified mid point and radius. func circlePoints(mid image.Point, radius int) []image.Point { var points []image.Point // Bresenham algorithm. // https://en.wikipedia.org/wiki/Midpoint_circle_algorithm x := radius y := 0 dx := 1 dy := 1 diff := dx - (radius << 1) // Cheap multiplication by two. for x >= y { points = append( points, image.Point{mid.X + x, mid.Y + y}, image.Point{mid.X + y, mid.Y + x}, image.Point{mid.X - y, mid.Y + x}, image.Point{mid.X - x, mid.Y + y}, image.Point{mid.X - x, mid.Y - y}, image.Point{mid.X - y, mid.Y - x}, image.Point{mid.X + y, mid.Y - x}, image.Point{mid.X + x, mid.Y - y}, ) if diff <= 0 { y++ diff += dy dy += 2 } if diff > 0 { x-- dx += 2 diff += dx - (radius << 1) } } return points }