mirror of
https://github.com/mum4k/termdash.git
synced 2025-04-25 13:48:50 +08:00
474 lines
13 KiB
Go
474 lines
13 KiB
Go
// 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 segment provides functions that draw a single segment.
|
|
package segment
|
|
|
|
import (
|
|
"fmt"
|
|
"image"
|
|
|
|
"github.com/mum4k/termdash/cell"
|
|
"github.com/mum4k/termdash/private/canvas/braille"
|
|
"github.com/mum4k/termdash/private/draw"
|
|
)
|
|
|
|
// Type identifies the type of the segment that is drawn.
|
|
type Type int
|
|
|
|
// String implements fmt.Stringer()
|
|
func (st Type) String() string {
|
|
if n, ok := segmentTypeNames[st]; ok {
|
|
return n
|
|
}
|
|
return "TypeUnknown"
|
|
}
|
|
|
|
// segmentTypeNames maps Type values to human readable names.
|
|
var segmentTypeNames = map[Type]string{
|
|
Horizontal: "Horizontal",
|
|
Vertical: "Vertical",
|
|
}
|
|
|
|
const (
|
|
segmentTypeUnknown Type = iota
|
|
|
|
// Horizontal is a horizontal segment.
|
|
Horizontal
|
|
// Vertical is a vertical segment.
|
|
Vertical
|
|
|
|
segmentTypeMax // Used for validation.
|
|
)
|
|
|
|
// Option is used to provide options.
|
|
type Option interface {
|
|
// set sets the provided option.
|
|
set(*options)
|
|
}
|
|
|
|
// options stores the provided options.
|
|
type options struct {
|
|
cellOpts []cell.Option
|
|
skipSlopesLTE int
|
|
reverseSlopes bool
|
|
}
|
|
|
|
// option implements Option.
|
|
type option func(*options)
|
|
|
|
// set implements Option.set.
|
|
func (o option) set(opts *options) {
|
|
o(opts)
|
|
}
|
|
|
|
// CellOpts sets options on the cells that contain the segment.
|
|
// Cell options on a braille canvas can only be set on the entire cell, not per
|
|
// pixel.
|
|
func CellOpts(cOpts ...cell.Option) Option {
|
|
return option(func(opts *options) {
|
|
opts.cellOpts = cOpts
|
|
})
|
|
}
|
|
|
|
// SkipSlopesLTE if provided instructs HV to not create slopes at the ends of a
|
|
// segment if the height of the horizontal or the width of the vertical segment
|
|
// is less or equal to the provided value.
|
|
func SkipSlopesLTE(v int) Option {
|
|
return option(func(opts *options) {
|
|
opts.skipSlopesLTE = v
|
|
})
|
|
}
|
|
|
|
// ReverseSlopes if provided reverses the order in which slopes are drawn.
|
|
// This only has a visible effect when the horizontal segment has height of two
|
|
// or the vertical segment has width of two.
|
|
// Without this option segments with height / width of two look like this:
|
|
// x - |
|
|
// x --- ||
|
|
// x |
|
|
// x
|
|
// x With this option:
|
|
// x
|
|
// x --- |
|
|
// x - ||
|
|
// x |
|
|
func ReverseSlopes() Option {
|
|
return option(func(opts *options) {
|
|
opts.reverseSlopes = true
|
|
})
|
|
}
|
|
|
|
// validArea validates the provided area.
|
|
func validArea(ar image.Rectangle) error {
|
|
if ar.Min.X < 0 || ar.Min.Y < 0 {
|
|
return fmt.Errorf("the start coordinates cannot be negative, got: %v", ar)
|
|
}
|
|
if ar.Max.X < 0 || ar.Max.Y < 0 {
|
|
return fmt.Errorf("the end coordinates cannot be negative, got: %v", ar)
|
|
}
|
|
if ar.Dx() < 1 || ar.Dy() < 1 {
|
|
return fmt.Errorf("the area for the segment must be at least 1x1 pixels, got %vx%v in area:%v", ar.Dx(), ar.Dy(), ar)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// HV draws a horizontal or a vertical display segment, filling the provided area.
|
|
// The segment will have slopes on both of its ends.
|
|
func HV(bc *braille.Canvas, ar image.Rectangle, st Type, opts ...Option) error {
|
|
if err := validArea(ar); err != nil {
|
|
return err
|
|
}
|
|
|
|
opt := &options{}
|
|
for _, o := range opts {
|
|
o.set(opt)
|
|
}
|
|
|
|
var nextLine nextHVLineFn
|
|
var lines int
|
|
switch st {
|
|
case Horizontal:
|
|
lines = ar.Dy()
|
|
nextLine = nextHorizLine
|
|
|
|
case Vertical:
|
|
lines = ar.Dx()
|
|
nextLine = nextVertLine
|
|
|
|
default:
|
|
return fmt.Errorf("unsupported segment type %v(%d)", st, st)
|
|
}
|
|
|
|
for i := 0; i < lines; i++ {
|
|
start, end := nextLine(i, ar, opt)
|
|
if err := draw.BrailleLine(bc, start, end, draw.BrailleLineCellOpts(opt.cellOpts...)); err != nil {
|
|
return err
|
|
}
|
|
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// nextHVLineFn is a function that determines the start and end points of a line
|
|
// number num in a horizontal or a vertical segment.
|
|
type nextHVLineFn func(num int, ar image.Rectangle, opt *options) (image.Point, image.Point)
|
|
|
|
// nextHorizLine determines the start and end point of individual lines in a
|
|
// horizontal segment.
|
|
func nextHorizLine(num int, ar image.Rectangle, opt *options) (image.Point, image.Point) {
|
|
// Start and end points of the full row without adjustments for slopes.
|
|
start := image.Point{ar.Min.X, ar.Min.Y + num}
|
|
end := image.Point{ar.Max.X - 1, ar.Min.Y + num}
|
|
|
|
height := ar.Dy()
|
|
width := ar.Dx()
|
|
if height <= opt.skipSlopesLTE || height < 2 || width < 3 {
|
|
// No slopes under these dimensions as we don't have the resolution.
|
|
return start, end
|
|
}
|
|
|
|
// Don't adjust rows that fall exactly in the middle of the segment height.
|
|
// E.g when height divides oddly, we want the middle row to take the full
|
|
// width:
|
|
// --
|
|
// ----
|
|
// --
|
|
//
|
|
// And when the height divides oddly, we want the two middle rows to take
|
|
// the full width:
|
|
// --
|
|
// ----
|
|
// ----
|
|
// --
|
|
// We only do this for segments that are at least three rows tall.
|
|
// For smaller segments we still want this behavior:
|
|
// --
|
|
// ----
|
|
halfHeight := height / 2
|
|
if height > 2 {
|
|
if num == halfHeight || (height%2 == 0 && num == halfHeight-1) {
|
|
return start, end
|
|
}
|
|
}
|
|
if height == 2 && opt.reverseSlopes {
|
|
return adjustHoriz(start, end, width, num)
|
|
}
|
|
|
|
if num < halfHeight {
|
|
adjust := halfHeight - num
|
|
if height%2 == 0 && height > 2 {
|
|
// On evenly divided height, we need one less adjustment on every
|
|
// row above the half, since two rows are taking the full width
|
|
// as shown above.
|
|
adjust--
|
|
}
|
|
return adjustHoriz(start, end, width, adjust)
|
|
}
|
|
adjust := num - halfHeight
|
|
return adjustHoriz(start, end, width, adjust)
|
|
}
|
|
|
|
// nextVertLine determines the start and end point of individual lines in a
|
|
// vertical segment.
|
|
func nextVertLine(num int, ar image.Rectangle, opt *options) (image.Point, image.Point) {
|
|
// Start and end points of the full column without adjustments for slopes.
|
|
start := image.Point{ar.Min.X + num, ar.Min.Y}
|
|
end := image.Point{ar.Min.X + num, ar.Max.Y - 1}
|
|
|
|
height := ar.Dy()
|
|
width := ar.Dx()
|
|
if width <= opt.skipSlopesLTE || height < 3 || width < 2 {
|
|
// No slopes under these dimensions as we don't have the resolution.
|
|
return start, end
|
|
}
|
|
|
|
// Don't adjust lines that fall exactly in the middle of the segment height.
|
|
// E.g when width divides oddly, we want the middle line to take the full
|
|
// height:
|
|
// |
|
|
// |||
|
|
// |||
|
|
// |
|
|
//
|
|
// And when the width divides oddly, we want the two middle columns to take
|
|
// the full height:
|
|
// ||
|
|
// ||||
|
|
// ||||
|
|
// ||
|
|
//
|
|
// We only do this for segments that are at least three columns wide.
|
|
// For smaller segments we still want this behavior:
|
|
// |
|
|
// ||
|
|
// ||
|
|
// |
|
|
halfWidth := width / 2
|
|
if width > 2 {
|
|
if num == halfWidth || (width%2 == 0 && num == halfWidth-1) {
|
|
return start, end
|
|
}
|
|
}
|
|
if width == 2 && opt.reverseSlopes {
|
|
return adjustVert(start, end, width, num)
|
|
}
|
|
|
|
if num < halfWidth {
|
|
adjust := halfWidth - num
|
|
if width%2 == 0 && width > 2 {
|
|
// On evenly divided width, we need one less adjustment on every
|
|
// column above the half, since two lines are taking the full
|
|
// height as shown above.
|
|
adjust--
|
|
}
|
|
return adjustVert(start, end, height, adjust)
|
|
}
|
|
adjust := num - halfWidth
|
|
return adjustVert(start, end, height, adjust)
|
|
}
|
|
|
|
// adjustHoriz given start and end points that identify a horizontal line,
|
|
// returns points that are adjusted towards each other on the line by the
|
|
// specified amount.
|
|
// I.e. the start is moved to the right and the end is moved to the left.
|
|
// The points won't be allowed to cross each other.
|
|
// The segWidth is the full width of the segment we are drawing.
|
|
func adjustHoriz(start, end image.Point, segWidth int, adjust int) (image.Point, image.Point) {
|
|
ns := start.Add(image.Point{adjust, 0})
|
|
ne := end.Sub(image.Point{adjust, 0})
|
|
|
|
if ns.X <= ne.X {
|
|
return ns, ne
|
|
}
|
|
|
|
halfWidth := segWidth / 2
|
|
if segWidth%2 == 0 {
|
|
// The width of the segment divides evenly, place start and end next to each other.
|
|
// E.g: 0 1 2 3 4 5
|
|
// - - ns ne - -
|
|
ns = image.Point{halfWidth - 1, start.Y}
|
|
ne = image.Point{halfWidth, end.Y}
|
|
} else {
|
|
// The width of the segment divides oddly, place both start and end on the mid point.
|
|
// E.g: 0 1 2 3 4
|
|
// - - nsne - -
|
|
ns = image.Point{halfWidth, start.Y}
|
|
ne = ns
|
|
}
|
|
return ns, ne
|
|
}
|
|
|
|
// adjustVert given start and end points that identify a vertical line,
|
|
// returns points that are adjusted towards each other on the line by the
|
|
// specified amount.
|
|
// I.e. the start is moved down and the end is moved up.
|
|
// The points won't be allowed to cross each other.
|
|
// The segHeight is the full height of the segment we are drawing.
|
|
func adjustVert(start, end image.Point, segHeight int, adjust int) (image.Point, image.Point) {
|
|
adjStart, adjEnd := adjustHoriz(swapCoord(start), swapCoord(end), segHeight, adjust)
|
|
return swapCoord(adjStart), swapCoord(adjEnd)
|
|
}
|
|
|
|
// swapCoord returns a point with its X and Y coordinates swapped.
|
|
func swapCoord(p image.Point) image.Point {
|
|
return image.Point{p.Y, p.X}
|
|
}
|
|
|
|
// DiagonalType determines the type of diagonal segment.
|
|
type DiagonalType int
|
|
|
|
// String implements fmt.Stringer()
|
|
func (dt DiagonalType) String() string {
|
|
if n, ok := diagonalTypeNames[dt]; ok {
|
|
return n
|
|
}
|
|
return "DiagonalTypeUnknown"
|
|
}
|
|
|
|
// diagonalTypeNames maps DiagonalType values to human readable names.
|
|
var diagonalTypeNames = map[DiagonalType]string{
|
|
LeftToRight: "LeftToRight",
|
|
RightToLeft: "RightToLeft",
|
|
}
|
|
|
|
const (
|
|
diagonalTypeUnknown DiagonalType = iota
|
|
// LeftToRight is a diagonal segment from top left to bottom right.
|
|
LeftToRight
|
|
// RightToLeft is a diagonal segment from top right to bottom left.
|
|
RightToLeft
|
|
|
|
diagonalTypeMax // Used for validation.
|
|
)
|
|
|
|
// nextDiagLineFn is a function that determines the start and end points of a line
|
|
// number num in a diagonal segment.
|
|
// Points start and end define the first diagonal exactly in the middle.
|
|
// Points prevStart and prevEnd define line num-1.
|
|
type nextDiagLineFn func(num int, start, end, prevStart, prevEnd image.Point) (image.Point, image.Point)
|
|
|
|
// DiagonalOption is used to provide options.
|
|
type DiagonalOption interface {
|
|
// set sets the provided option.
|
|
set(*diagonalOptions)
|
|
}
|
|
|
|
// diagonalOptions stores the provided diagonal options.
|
|
type diagonalOptions struct {
|
|
cellOpts []cell.Option
|
|
}
|
|
|
|
// diagonalOption implements DiagonalOption.
|
|
type diagonalOption func(*diagonalOptions)
|
|
|
|
// set implements DiagonalOption.set.
|
|
func (o diagonalOption) set(opts *diagonalOptions) {
|
|
o(opts)
|
|
}
|
|
|
|
// DiagonalCellOpts sets options on the cells that contain the diagonal
|
|
// segment.
|
|
// Cell options on a braille canvas can only be set on the entire cell, not per
|
|
// pixel.
|
|
func DiagonalCellOpts(cOpts ...cell.Option) DiagonalOption {
|
|
return diagonalOption(func(opts *diagonalOptions) {
|
|
opts.cellOpts = cOpts
|
|
})
|
|
}
|
|
|
|
// Diagonal draws a diagonal segment of the specified width filling the area.
|
|
func Diagonal(bc *braille.Canvas, ar image.Rectangle, width int, dt DiagonalType, opts ...DiagonalOption) error {
|
|
if err := validArea(ar); err != nil {
|
|
return err
|
|
}
|
|
if min := 1; width < min {
|
|
return fmt.Errorf("invalid width %d, must be width >= %d", width, min)
|
|
}
|
|
opt := &diagonalOptions{}
|
|
for _, o := range opts {
|
|
o.set(opt)
|
|
}
|
|
|
|
var start, end image.Point
|
|
var nextFn nextDiagLineFn
|
|
switch dt {
|
|
case LeftToRight:
|
|
start = ar.Min
|
|
end = image.Point{ar.Max.X - 1, ar.Max.Y - 1}
|
|
nextFn = nextLRLine
|
|
|
|
case RightToLeft:
|
|
start = image.Point{ar.Max.X - 1, ar.Min.Y}
|
|
end = image.Point{ar.Min.X, ar.Max.Y - 1}
|
|
nextFn = nextRLLine
|
|
|
|
default:
|
|
return fmt.Errorf("unsupported diagonal type %v(%d)", dt, dt)
|
|
}
|
|
|
|
if err := draw.BrailleLine(bc, start, end, draw.BrailleLineCellOpts(opt.cellOpts...)); err != nil {
|
|
return err
|
|
}
|
|
|
|
ns := start
|
|
ne := end
|
|
for i := 1; i < width; i++ {
|
|
ns, ne = nextFn(i, start, end, ns, ne)
|
|
|
|
if !ns.In(ar) || !ne.In(ar) {
|
|
return fmt.Errorf("cannot draw diagonal segment of width %d in area %v, the area isn't large enough for line %v-%v", width, ar, ns, ne)
|
|
}
|
|
|
|
if err := draw.BrailleLine(bc, ns, ne, draw.BrailleLineCellOpts(opt.cellOpts...)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// nextLRLine is a function that determines the start and end points of the
|
|
// next line of a left-to-right diagonal segment.
|
|
func nextLRLine(num int, start, end, prevStart, prevEnd image.Point) (image.Point, image.Point) {
|
|
dist := num / 2
|
|
if num%2 != 0 {
|
|
// Every odd line is placed above the mid diagonal.
|
|
ns := image.Point{start.X + dist + 1, start.Y}
|
|
ne := image.Point{end.X, end.Y - dist - 1}
|
|
return ns, ne
|
|
}
|
|
|
|
// Every even line is placed under the mid diagonal.
|
|
ns := image.Point{start.X, start.Y + dist}
|
|
ne := image.Point{end.X - dist, end.Y}
|
|
return ns, ne
|
|
}
|
|
|
|
// nextRLLine is a function that determines the start and end points of the
|
|
// next line of a right-to-left diagonal segment.
|
|
func nextRLLine(num int, start, end, prevStart, prevEnd image.Point) (image.Point, image.Point) {
|
|
dist := num / 2
|
|
if num%2 != 0 {
|
|
// Every odd line is placed above the mid diagonal.
|
|
ns := image.Point{start.X - dist - 1, start.Y}
|
|
ne := image.Point{end.X, end.Y - dist - 1}
|
|
return ns, ne
|
|
}
|
|
|
|
// Every even line is placed under the mid diagonal.
|
|
ns := image.Point{start.X, start.Y + dist}
|
|
ne := image.Point{end.X + dist, end.Y}
|
|
return ns, ne
|
|
}
|