termdash/private/segdisp/sixteen/attributes.go

286 lines
8.2 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 sixteen
// attributes.go calculates attributes needed when determining placement of
// segments.
import (
"fmt"
"image"
"math"
"github.com/mum4k/termdash/private/numbers"
"github.com/mum4k/termdash/private/segdisp"
"github.com/mum4k/termdash/private/segdisp/segment"
)
// hvSegType maps horizontal and vertical segments to their type.
var hvSegType = map[Segment]segment.Type{
A1: segment.Horizontal,
A2: segment.Horizontal,
B: segment.Vertical,
C: segment.Vertical,
D1: segment.Horizontal,
D2: segment.Horizontal,
E: segment.Vertical,
F: segment.Vertical,
G1: segment.Horizontal,
G2: segment.Horizontal,
J: segment.Vertical,
M: segment.Vertical,
}
// diaSegType maps diagonal segments to their type.
var diaSegType = map[Segment]segment.DiagonalType{
H: segment.LeftToRight,
K: segment.RightToLeft,
N: segment.RightToLeft,
L: segment.LeftToRight,
}
// Attributes contains attributes needed to draw the segment display.
// Refer to doc/segment_placement.svg for a visual aid and explanation of the
// usage of the square roots.
type Attributes struct {
// segSize is the width of a vertical or height of a horizontal segment.
segSize int
// diaGap is the shortest distance between slopes on two neighboring
// perpendicular segments.
diaGap float64
// segPeakDist is the distance between the peak of the slope on a segment
// and the point where the slope ends.
segPeakDist float64
// diaLeg is the leg of a square whose hypotenuse is the diaGap.
diaLeg float64
// peakToPeak is a horizontal or vertical distance between peaks of two
// segments.
peakToPeak int
// shortLen is length of the shorter segment, e.g. D1.
shortLen int
// longLen is length of the longer segment, e.g. F.
longLen int
// horizLeftX is the X coordinate where the area of the segment horizontally
// on the left starts, i.e. X coordinate of F and E.
horizLeftX int
// horizMidX is the X coordinate where the area of the segment horizontally in
// the middle starts, i.e. X coordinate of J and M.
horizMidX int
// horizRightX is the X coordinate where the area of the segment horizontally
// on the right starts, i.e. X coordinate of B and C.
horizRightX int
// vertCenY is the Y coordinate where the area of the segment vertically
// in the center starts, i.e. Y coordinate of G1 and G2.
vertCenY int
// VertBotY is the Y coordinate where the area of the segment vertically
// at the bottom starts, i.e. Y coordinate of D1 and D2.
VertBotY int
}
// NewAttributes calculates attributes needed to place the segments for the
// provided pixel area.
func NewAttributes(bcAr image.Rectangle) *Attributes {
segSize := segdisp.SegmentSize(bcAr)
// diaPerc is the size of the diaGap in percentage of the segment's size.
const diaPerc = 40
// Ensure there is at least one pixel diagonally between segments so they
// don't visually blend.
_, dg := numbers.MinMaxInts([]int{
int(float64(segSize) * diaPerc / 100),
1,
})
diaGap := float64(dg)
segLeg := float64(segSize) / math.Sqrt2
segPeakDist := segLeg / math.Sqrt2
diaLeg := diaGap / math.Sqrt2
peakToPeak := diaLeg * 2
if segSize == 2 {
// Display that has segment size of two looks more balanced with peak
// distance of two.
peakToPeak = 2
}
if peakToPeak > 3 && int(peakToPeak)%2 == 0 {
// Prefer odd distances to create centered look.
peakToPeak++
}
twoSegHypo := 2*segLeg + diaGap
twoSegLeg := twoSegHypo / math.Sqrt2
edgeSegGap := twoSegLeg - segPeakDist
spaces := int(math.Round(2*edgeSegGap + peakToPeak))
shortLen := (bcAr.Dx()-spaces)/2 - 1
longLen := (bcAr.Dy()-spaces)/2 - 1
ptp := int(math.Round(peakToPeak))
horizLeftX := int(math.Round(edgeSegGap))
// Refer to doc/segment_placement.svg.
// Diagram labeled "A mid point".
offset := int(math.Round(diaLeg - segPeakDist))
horizMidX := horizLeftX + shortLen + offset
horizRightX := horizLeftX + shortLen + ptp + shortLen + offset
vertCenY := horizLeftX + longLen + offset
vertBotY := horizLeftX + longLen + ptp + longLen + offset
return &Attributes{
segSize: segSize,
diaGap: diaGap,
segPeakDist: segPeakDist,
diaLeg: diaLeg,
peakToPeak: ptp,
shortLen: shortLen,
longLen: longLen,
horizLeftX: horizLeftX,
horizMidX: horizMidX,
horizRightX: horizRightX,
vertCenY: vertCenY,
VertBotY: vertBotY,
}
}
// hvSegArea returns the area for the specified horizontal or vertical segment.
func (a *Attributes) hvSegArea(s Segment) image.Rectangle {
var (
start image.Point
length int
)
switch s {
case A1:
start = image.Point{a.horizLeftX, 0}
length = a.shortLen
case A2:
a1 := a.hvSegArea(A1)
start = image.Point{a1.Max.X + a.peakToPeak, 0}
length = a.shortLen
case F:
start = image.Point{0, a.horizLeftX}
length = a.longLen
case J:
start = image.Point{a.horizMidX, a.horizLeftX}
length = a.longLen
case B:
start = image.Point{a.horizRightX, a.horizLeftX}
length = a.longLen
case G1:
start = image.Point{a.horizLeftX, a.vertCenY}
length = a.shortLen
case G2:
g1 := a.hvSegArea(G1)
start = image.Point{g1.Max.X + a.peakToPeak, a.vertCenY}
length = a.shortLen
case E:
f := a.hvSegArea(F)
start = image.Point{0, f.Max.Y + a.peakToPeak}
length = a.longLen
case M:
j := a.hvSegArea(J)
start = image.Point{a.horizMidX, j.Max.Y + a.peakToPeak}
length = a.longLen
case C:
b := a.hvSegArea(B)
start = image.Point{a.horizRightX, b.Max.Y + a.peakToPeak}
length = a.longLen
case D1:
start = image.Point{a.horizLeftX, a.VertBotY}
length = a.shortLen
case D2:
d1 := a.hvSegArea(D1)
start = image.Point{d1.Max.X + a.peakToPeak, a.VertBotY}
length = a.shortLen
default:
panic(fmt.Sprintf("cannot determine area for unknown horizontal or vertical segment %v(%d)", s, s))
}
return a.hvArFromStart(start, s, length)
}
// hvArFromStart given start coordinates of a segment, its length and its type,
// determines its area.
func (a *Attributes) hvArFromStart(start image.Point, s Segment, length int) image.Rectangle {
st := hvSegType[s]
switch st {
case segment.Horizontal:
return image.Rect(start.X, start.Y, start.X+length, start.Y+a.segSize)
case segment.Vertical:
return image.Rect(start.X, start.Y, start.X+a.segSize, start.Y+length)
default:
panic(fmt.Sprintf("cannot create area for segment of unknown type %v(%d)", st, st))
}
}
// diaSegArea returns the area for the specified diagonal segment.
func (a *Attributes) diaSegArea(s Segment) image.Rectangle {
switch s {
case H:
return a.diaBetween(A1, F, J, G1)
case K:
return a.diaBetween(A2, B, J, G2)
case N:
return a.diaBetween(G1, M, E, D1)
case L:
return a.diaBetween(G2, M, C, D2)
default:
panic(fmt.Sprintf("cannot determine area for unknown diagonal segment %v(%d)", s, s))
}
}
// diaBetween given four segments (two horizontal and two vertical) returns the
// area between them for a diagonal segment.
func (a *Attributes) diaBetween(top, left, right, bottom Segment) image.Rectangle {
topAr := a.hvSegArea(top)
leftAr := a.hvSegArea(left)
rightAr := a.hvSegArea(right)
bottomAr := a.hvSegArea(bottom)
// hvToDiaGapPerc is the size of gap between horizontal or vertical segment
// and the diagonal segment between them in percentage of the diaGap.
const hvToDiaGapPerc = 30
hvToDiaGap := a.diaGap * hvToDiaGapPerc / 100
startX := int(math.Round(float64(topAr.Min.X) + a.segPeakDist - a.diaLeg + hvToDiaGap))
startY := int(math.Round(float64(leftAr.Min.Y) + a.segPeakDist - a.diaLeg + hvToDiaGap))
endX := int(math.Round(float64(bottomAr.Max.X) - a.segPeakDist + a.diaLeg - hvToDiaGap))
endY := int(math.Round(float64(rightAr.Max.Y) - a.segPeakDist + a.diaLeg - hvToDiaGap))
return image.Rect(startX, startY, endX, endY)
}