vhs/draw.go
Carlos Alexandro Becker 710bb769af
fix: lint (#637)
2025-06-17 20:49:26 +02:00

416 lines
8.8 KiB
Go

package main
import (
"fmt"
"image"
"image/color"
"image/draw"
"image/png"
"math"
"os"
)
type circle struct {
p image.Point
r int
}
const (
white = 0xFF
black = 0x17
)
func (c *circle) ColorModel() color.Model {
return color.AlphaModel
}
func (c *circle) Bounds() image.Rectangle {
return image.Rect(
c.p.X-c.r,
c.p.Y-c.r,
c.p.X+c.r,
c.p.Y+c.r,
)
}
const (
halfPixel = 0.5
doublingFactor = 2
)
func double(i int) int { return i * doublingFactor }
func half(i int) int { return i / doublingFactor }
func (c *circle) At(x, y int) color.Color {
// Prepare points for circle calculations.
// We subtract 1 from the radius to leave space for
// antialiased pixels.
xx := float64(x-c.p.X) + halfPixel
yy := float64(y-c.p.Y) + halfPixel
rr := float64(c.r) - 1
// The distance from this pixel to the closest point
// in the circle.
dist := math.Sqrt(xx*xx+yy*yy) - rr
if dist < 0 {
// This pixel is inside the circle
return color.Alpha{white}
} else if dist <= 1 {
// This pixel is partly inside the circle
// and needs antialiasing
return color.Alpha{
uint8((1 - dist) * white),
}
}
// This pixel is outside the circle
// and should be fully transparent
return color.Alpha{0x00}
}
type rect struct {
pa image.Point
pb image.Point
}
func (r *rect) ColorModel() color.Model {
return color.AlphaModel
}
func (r *rect) Bounds() image.Rectangle {
return image.Rect(r.pa.X, r.pa.Y, r.pb.X, r.pb.Y)
}
func (r *rect) At(x, y int) color.Color {
if (x >= r.pa.X) &&
(x < r.pb.X) &&
(y >= r.pa.Y) &&
(y < r.pb.Y) {
return color.Alpha{white}
}
return color.Alpha{0x00}
}
type roundedrect struct {
pa image.Point
pb image.Point
radius int
}
func (r *roundedrect) ColorModel() color.Model {
return color.AlphaModel
}
func (r *roundedrect) Bounds() image.Rectangle {
return image.Rect(r.pa.X, r.pa.Y, r.pb.X, r.pb.Y)
}
func (r *roundedrect) At(x, y int) color.Color {
// Top-left corner
if (x >= r.pa.X) &&
(x < r.pa.X+r.radius) &&
(y >= r.pa.Y) &&
(y < r.pa.Y+r.radius) {
c := circle{
image.Point{
r.radius,
r.radius,
},
// Add one to corner radius so that
// fully-opaque pixels match the rectangle.
// The outermost pixels of a circle are
// always antialiased and thus transparent.
r.radius + 1,
}
return c.At(x, y)
}
// Top-right corner
if (x >= r.pb.X-r.radius) &&
(x < r.pb.X) &&
(y >= r.pa.Y) &&
(y < r.pa.Y+r.radius) {
c := circle{
image.Point{
r.pb.X - r.radius,
r.radius,
},
r.radius + 1,
}
return c.At(x, y)
}
// Bottom-left corner
if (x >= r.pa.X) &&
(x < r.pa.X+r.radius) &&
(y >= r.pb.Y-r.radius) &&
(y < r.pb.Y) {
c := circle{
image.Point{
r.radius,
r.pb.Y - r.radius,
},
r.radius + 1,
}
return c.At(x, y)
}
// Bottom-right corner
if (x >= r.pb.X-r.radius) &&
(x < r.pb.X) &&
(y >= r.pb.Y-r.radius) &&
(y < r.pb.Y) {
c := circle{
image.Point{
r.pb.X - r.radius,
r.pb.Y - r.radius,
},
r.radius + 1,
}
return c.At(x, y)
}
return color.Alpha{white}
}
// MakeBorderRadiusMask a mask to round a terminal's corners.
func MakeBorderRadiusMask(width, height, radius int, targetpng string) {
img := image.NewGray(
image.Rectangle{
image.Point{0, 0},
image.Point{width, height},
},
)
// Fill image with black
draw.DrawMask(
img, img.Bounds(), &image.Uniform{color.Gray{0x00}}, image.Point{0, 0},
&rect{image.Point{0, 0}, image.Point{width, height}},
image.Point{0, 0}, draw.Src,
)
// Put mask in white on top
draw.DrawMask(
img, img.Bounds(), &image.Uniform{color.Gray{white}}, image.Point{0, 0},
&roundedrect{image.Point{0, 0}, image.Point{width, height}, radius},
image.Point{0, 0}, draw.Over,
)
f, err := os.Create(targetpng)
if err != nil {
fmt.Println(ErrorStyle.Render("Could not draw Border Mask: unable to save file."))
} else {
err = png.Encode(f, img)
}
if err != nil {
fmt.Println(ErrorStyle.Render("Could not draw Border Mask: encoding failed."))
}
}
// MakeWindowBar a window bar and save it to a file.
func MakeWindowBar(termWidth, termHeight int, opts StyleOptions, file string) {
var err error
switch opts.WindowBar {
case "Colorful":
err = makeColorfulBar(termWidth, termHeight, false, opts, file)
case "ColorfulRight":
err = makeColorfulBar(termWidth, termHeight, true, opts, file)
case "Rings":
err = makeRingBar(termWidth, termHeight, false, opts, file)
case "RingsRight":
err = makeRingBar(termWidth, termHeight, true, opts, file)
}
if err != nil {
fmt.Println(ErrorStyle.Render("Couldn't draw Bar: encoding failed"))
}
}
const (
barToDotRatio = 6
barToDotBorderRatio = 5
)
func makeColorfulBar(termWidth int, termHeight int, isRight bool, opts StyleOptions, targetpng string) error {
// Radius of dots
dotRad := opts.WindowBarSize / barToDotRatio
dotDia := double(dotRad)
// Space between dots and edge
dotGap := half(opts.WindowBarSize - dotDia)
// Space between dot centers
dotSpace := dotDia + opts.WindowBarSize/barToDotRatio
// Dimensions of bar image
width := termWidth
height := termHeight + opts.WindowBarSize
img := image.NewRGBA(
image.Rectangle{
image.Point{0, 0},
image.Point{width, height},
},
)
bg, _ := parseHexColor(opts.WindowBarColor)
dotA := color.RGBA{white, 0x4F, 0x4D, white}
dotB := color.RGBA{0xFE, 0xBB, 0x00, white}
dotC := color.RGBA{0x00, 0xCC, 0x1D, white}
var pta, ptb, ptc image.Point
if isRight {
pta = image.Point{termWidth - (dotGap + dotRad), dotRad + dotGap}
ptb = image.Point{termWidth - (dotGap + dotRad + dotSpace), dotRad + dotGap}
ptc = image.Point{termWidth - (dotGap + dotRad + 2*dotSpace), dotRad + dotGap}
} else {
pta = image.Point{dotGap + dotRad, dotRad + dotGap}
ptb = image.Point{dotGap + dotRad + dotSpace, dotRad + dotGap}
ptc = image.Point{dotGap + dotRad + 2*dotSpace, dotRad + dotGap}
}
draw.DrawMask(
img, img.Bounds(), &image.Uniform{bg}, image.Point{0, 0},
&rect{image.Point{0, 0}, image.Point{width, height}},
image.Point{0, 0}, draw.Src,
)
draw.DrawMask(
img,
img.Bounds(),
&image.Uniform{dotA},
image.Point{0, 0},
&circle{pta, dotRad},
image.Point{0, 0},
draw.Over,
)
draw.DrawMask(
img,
img.Bounds(),
&image.Uniform{dotB},
image.Point{0, 0},
&circle{ptb, dotRad},
image.Point{0, 0},
draw.Over,
)
draw.DrawMask(
img,
img.Bounds(),
&image.Uniform{dotC},
image.Point{0, 0},
&circle{ptc, dotRad},
image.Point{0, 0},
draw.Over,
)
f, err := os.Create(targetpng)
if err != nil {
fmt.Println(ErrorStyle.Render("Couldn't draw colorful bar: unable to save file."))
} else {
err = png.Encode(f, img)
}
return err //nolint:wrapcheck
}
func makeRingBar(termWidth int, termHeight int, isRight bool, opts StyleOptions, targetpng string) error {
// Radius of dots
outerRad := opts.WindowBarSize / barToDotBorderRatio
outerDia := double(outerRad)
innerRad := double(outerDia) / barToDotBorderRatio
// Space between dots and edge
ringGap := half(opts.WindowBarSize - outerDia)
// Space between dot centers
ringSpace := outerDia + opts.WindowBarSize/barToDotRatio
// Dimensions of bar image
width := termWidth
height := termHeight + opts.WindowBarSize
img := image.NewRGBA(
image.Rectangle{
image.Point{0, 0},
image.Point{width, height},
},
)
bg, _ := parseHexColor(opts.WindowBarColor)
ring := color.RGBA{0x33, 0x33, 0x33, white}
draw.DrawMask(
img, img.Bounds(), &image.Uniform{bg}, image.Point{0, 0},
&rect{image.Point{0, 0}, image.Point{width, height}},
image.Point{0, 0}, draw.Src,
)
for i := 0; i <= 2; i++ {
var pt image.Point
if isRight {
pt = image.Point{
termWidth - (ringGap + outerRad + i*ringSpace),
outerRad + ringGap,
}
} else {
pt = image.Point{
ringGap + outerRad + i*ringSpace,
outerRad + ringGap,
}
}
draw.DrawMask(
img,
img.Bounds(),
&image.Uniform{ring},
image.Point{0, 0},
&circle{pt, outerRad},
image.Point{0, 0},
draw.Over,
)
draw.DrawMask(
img,
img.Bounds(),
&image.Uniform{bg},
image.Point{0, 0},
&circle{pt, innerRad},
image.Point{0, 0},
draw.Over,
)
}
f, err := os.Create(targetpng)
if err != nil {
fmt.Println(ErrorStyle.Render("Couldn't draw ring bar: unable to save file."))
} else {
err = png.Encode(f, img)
}
return err //nolint:wrapcheck
}
//nolint:mnd
func parseHexColor(s string) (c color.RGBA, err error) {
c.R, c.G, c.B, c.A = black, black, black, white
switch len(s) {
case 7:
_, err = fmt.Sscanf(s, "#%02x%02x%02x", &c.R, &c.G, &c.B)
case 6:
_, err = fmt.Sscanf(s, "%02x%02x%02x", &c.R, &c.G, &c.B)
case 4:
_, err = fmt.Sscanf(s, "#%1x%1x%1x", &c.R, &c.G, &c.B)
// Double the hex digits:
c.R *= 17
c.G *= 17
c.B *= 17
case 3:
_, err = fmt.Sscanf(s, "%1x%1x%1x", &c.R, &c.G, &c.B)
// Double the hex digits:
c.R *= 17
c.G *= 17
c.B *= 17
default:
err = fmt.Errorf("%s color of invalid length", s)
}
return
}