termdash/draw/braille_line.go

205 lines
5.6 KiB
Go

// Copyright 2018 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_line.go contains code that draws lines on a braille canvas.
import (
"fmt"
"image"
"github.com/mum4k/termdash/canvas/braille"
"github.com/mum4k/termdash/cell"
"github.com/mum4k/termdash/numbers"
)
// braillePixelChange represents an action on a pixel on the braille canvas.
type braillePixelChange int
// String implements fmt.Stringer()
func (bpc braillePixelChange) String() string {
if n, ok := braillePixelChangeNames[bpc]; ok {
return n
}
return "braillePixelChangeUnknown"
}
// braillePixelChangeNames maps braillePixelChange values to human readable names.
var braillePixelChangeNames = map[braillePixelChange]string{
braillePixelChangeSet: "braillePixelChangeSet",
braillePixelChangeClear: "braillePixelChangeClear",
}
const (
braillePixelChangeUnknown braillePixelChange = iota
braillePixelChangeSet
braillePixelChangeClear
)
// BrailleLineOption is used to provide options to BrailleLine().
type BrailleLineOption interface {
// set sets the provided option.
set(*brailleLineOptions)
}
// brailleLineOptions stores the provided options.
type brailleLineOptions struct {
cellOpts []cell.Option
pixelChange braillePixelChange
}
// newBrailleLineOptions returns a new brailleLineOptions instance.
func newBrailleLineOptions() *brailleLineOptions {
return &brailleLineOptions{
pixelChange: braillePixelChangeSet,
}
}
// brailleLineOption implements BrailleLineOption.
type brailleLineOption func(*brailleLineOptions)
// set implements BrailleLineOption.set.
func (o brailleLineOption) set(opts *brailleLineOptions) {
o(opts)
}
// BrailleLineCellOpts sets options on the cells that contain the line.
// Cell options on a braille canvas can only be set on the entire cell, not per
// pixel.
func BrailleLineCellOpts(cOpts ...cell.Option) BrailleLineOption {
return brailleLineOption(func(opts *brailleLineOptions) {
opts.cellOpts = cOpts
})
}
// BrailleLineClearPixels changes the behavior of BrailleLine, so that it
// clears the pixels belonging to the line instead of setting them.
// Useful in order to "erase" a line from the canvas as opposed to drawing one.
func BrailleLineClearPixels() BrailleLineOption {
return brailleLineOption(func(opts *brailleLineOptions) {
opts.pixelChange = braillePixelChangeClear
})
}
// BrailleLine draws an approximated line segment on the braille canvas between
// the two provided points.
// Both start and end must be valid points within the canvas. Start and end can
// be the same point in which case only one pixel will be set on the braille
// canvas.
// The start or end coordinates must not be negative.
func BrailleLine(bc *braille.Canvas, start, end image.Point, opts ...BrailleLineOption) error {
if start.X < 0 || start.Y < 0 {
return fmt.Errorf("the start coordinates cannot be negative, got: %v", start)
}
if end.X < 0 || end.Y < 0 {
return fmt.Errorf("the end coordinates cannot be negative, got: %v", end)
}
opt := newBrailleLineOptions()
for _, o := range opts {
o.set(opt)
}
points := brailleLinePoints(start, end)
for _, p := range points {
switch opt.pixelChange {
case braillePixelChangeSet:
if err := bc.SetPixel(p, opt.cellOpts...); err != nil {
return fmt.Errorf("bc.SetPixel(%v) => %v", p, err)
}
case braillePixelChangeClear:
if err := bc.ClearPixel(p, opt.cellOpts...); err != nil {
return fmt.Errorf("bc.ClearPixel(%v) => %v", p, err)
}
}
}
return nil
}
// brailleLinePoints returns the points to set when drawing the line.
func brailleLinePoints(start, end image.Point) []image.Point {
// Implements Bresenham's line algorithm.
// https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm
vertProj := numbers.Abs(end.Y - start.Y)
horizProj := numbers.Abs(end.X - start.X)
if vertProj < horizProj {
if start.X > end.X {
return lineLow(end.X, end.Y, start.X, start.Y)
}
return lineLow(start.X, start.Y, end.X, end.Y)
}
if start.Y > end.Y {
return lineHigh(end.X, end.Y, start.X, start.Y)
}
return lineHigh(start.X, start.Y, end.X, end.Y)
}
// lineLow returns points that create a line whose horizontal projection
// (end.X - start.X) is longer than its vertical projection
// (end.Y - start.Y).
func lineLow(x0, y0, x1, y1 int) []image.Point {
deltaX := x1 - x0
deltaY := y1 - y0
stepY := 1
if deltaY < 0 {
stepY = -1
deltaY = -deltaY
}
var res []image.Point
diff := 2*deltaY - deltaX
y := y0
for x := x0; x <= x1; x++ {
res = append(res, image.Point{x, y})
if diff > 0 {
y += stepY
diff -= 2 * deltaX
}
diff += 2 * deltaY
}
return res
}
// lineHigh returns points that createa line whose vertical projection
// (end.Y - start.Y) is longer than its horizontal projection
// (end.X - start.X).
func lineHigh(x0, y0, x1, y1 int) []image.Point {
deltaX := x1 - x0
deltaY := y1 - y0
stepX := 1
if deltaX < 0 {
stepX = -1
deltaX = -deltaX
}
var res []image.Point
diff := 2*deltaX - deltaY
x := x0
for y := y0; y <= y1; y++ {
res = append(res, image.Point{x, y})
if diff > 0 {
x += stepX
diff -= 2 * deltaY
}
diff += 2 * deltaX
}
return res
}