termdash/area/area.go

155 lines
4.4 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 area provides functions working with image areas.
package area
import (
"fmt"
"image"
"github.com/mum4k/termdash/numbers"
)
// Size returns the size of the provided area.
func Size(area image.Rectangle) image.Point {
return image.Point{
area.Dx(),
area.Dy(),
}
}
// FromSize returns the corresponding area for the provided size.
func FromSize(size image.Point) (image.Rectangle, error) {
if size.X < 0 || size.Y < 0 {
return image.Rectangle{}, fmt.Errorf("cannot convert zero or negative size to an area, got: %+v", size)
}
return image.Rect(0, 0, size.X, size.Y), nil
}
// HSplit returns two new areas created by splitting the provided area at the
// specified percentage of its width. The percentage must be in the range
// 0 <= heightPerc <= 100.
// Can return zero size areas.
func HSplit(area image.Rectangle, heightPerc int) (top image.Rectangle, bottom image.Rectangle, err error) {
if min, max := 0, 100; heightPerc < min || heightPerc > max {
return image.ZR, image.ZR, fmt.Errorf("invalid heightPerc %d, must be in range %d <= heightPerc <= %d", heightPerc, min, max)
}
height := area.Dy() * heightPerc / 100
top = image.Rect(area.Min.X, area.Min.Y, area.Max.X, area.Min.Y+height)
if top.Dy() == 0 {
top = image.ZR
}
bottom = image.Rect(area.Min.X, area.Min.Y+height, area.Max.X, area.Max.Y)
if bottom.Dy() == 0 {
bottom = image.ZR
}
return top, bottom, nil
}
// VSplit returns two new areas created by splitting the provided area at the
// specified percentage of its width. The percentage must be in the range
// 0 <= widthPerc <= 100.
// Can return zero size areas.
func VSplit(area image.Rectangle, widthPerc int) (left image.Rectangle, right image.Rectangle, err error) {
if min, max := 0, 100; widthPerc < min || widthPerc > max {
return image.ZR, image.ZR, fmt.Errorf("invalid widthPerc %d, must be in range %d <= widthPerc <= %d", widthPerc, min, max)
}
width := area.Dx() * widthPerc / 100
left = image.Rect(area.Min.X, area.Min.Y, area.Min.X+width, area.Max.Y)
if left.Dx() == 0 {
left = image.ZR
}
right = image.Rect(area.Min.X+width, area.Min.Y, area.Max.X, area.Max.Y)
if right.Dx() == 0 {
right = image.ZR
}
return left, right, nil
}
// ExcludeBorder returns a new area created by subtracting a border around the
// provided area. Return the zero area if there isn't enough space to exclude
// the border.
func ExcludeBorder(area image.Rectangle) image.Rectangle {
// If the area dimensions are smaller than this, subtracting a point for the
// border on each of its sides results in a zero area.
const minDim = 2
if area.Dx() < minDim || area.Dy() < minDim {
return image.ZR
}
return image.Rect(
numbers.Abs(area.Min.X+1),
numbers.Abs(area.Min.Y+1),
numbers.Abs(area.Max.X-1),
numbers.Abs(area.Max.Y-1),
)
}
// findGCF finds the greatest common factor of two integers.
func findGCF(a, b int) int {
if a == 0 || b == 0 {
return 0
}
// https://en.wikipedia.org/wiki/Euclidean_algorithm
for {
rem := a % b
a = b
b = rem
if b == 0 {
break
}
}
return a
}
// simplifyRatio simplifies the given ratio.
func simplifyRatio(ratio image.Point) image.Point {
gcf := findGCF(ratio.X, ratio.Y)
if gcf == 0 {
return image.ZP
}
return image.Point{
X: ratio.X / gcf,
Y: ratio.Y / gcf,
}
}
// WithRatio returns the largest area that has the requested ratio but is
// either equal or smaller than the provided area. Returns zero area if the
// area or the ratio are zero, or if there is no such area.
func WithRatio(area image.Rectangle, ratio image.Point) image.Rectangle {
ratio = simplifyRatio(ratio)
if area == image.ZR || ratio == image.ZP {
return image.ZR
}
wFact := area.Dx() / ratio.X
hFact := area.Dy() / ratio.Y
var fact int
if wFact < hFact {
fact = wFact
} else {
fact = hFact
}
return image.Rect(
area.Min.X,
area.Min.Y,
ratio.X*fact+area.Min.X,
ratio.Y*fact+area.Min.Y,
)
}