272 lines
6.3 KiB
Go
272 lines
6.3 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"github.com/muesli/termenv"
|
|
"github.com/ojrac/opensimplex-go"
|
|
"io"
|
|
"os"
|
|
"path"
|
|
"unicode"
|
|
)
|
|
|
|
// Style represents a pair of colors, foreground and background.
|
|
type Style struct {
|
|
fg termenv.Color
|
|
bg termenv.Color
|
|
}
|
|
|
|
// Convert is a wrapper for termenv.Profile.Convert, it converts the foreground
|
|
// and background colors of a Style to be within a given terminal's Profile.
|
|
func (s Style) Convert(profile termenv.Profile) Style {
|
|
return Style{
|
|
profile.Convert(s.fg),
|
|
profile.Convert(s.bg),
|
|
}
|
|
}
|
|
|
|
// String fills the Stringer interface and returns an ANSI escape code
|
|
// formatted to produce text in the specified Style.
|
|
func (s Style) String() string {
|
|
if s.fg == nil {
|
|
s.fg = termenv.NoColor{}
|
|
}
|
|
if s.bg == nil {
|
|
s.bg = termenv.NoColor{}
|
|
}
|
|
// TODO: Looks like the background color is overriding the foreground color. What's the proper way to do this?
|
|
return fmt.Sprintf("%s%s;%sm", termenv.CSI, s.fg.Sequence(false), s.bg.Sequence(true))
|
|
}
|
|
|
|
// Scene holds all the necessary information to make a dynamic, weather-y ASCII
|
|
// landscape.
|
|
type Scene struct {
|
|
foreground [][]rune
|
|
background [][]rune
|
|
windground [][]rune
|
|
depth [][]rune
|
|
forecast Forecast
|
|
|
|
generator opensimplex.Noise
|
|
|
|
Width int
|
|
Height int
|
|
}
|
|
|
|
const (
|
|
fgPath = "foreground.txt"
|
|
depthPath = "depth.txt"
|
|
windPath = "wind.txt"
|
|
|
|
earlyPath = "early.txt"
|
|
morningPath = "morning.txt"
|
|
afternoonPath = "afternoon.txt"
|
|
nightPath = "night.txt"
|
|
)
|
|
|
|
var timeToPath = []string{
|
|
earlyPath,
|
|
morningPath,
|
|
afternoonPath,
|
|
nightPath,
|
|
}
|
|
|
|
// NewScene accepts a path to a folder containing foreground, windground,
|
|
// depth, and background files and loads them from disk as needed to generate
|
|
// imagery for the given forecast.
|
|
func NewScene(scenePath string, forecast Forecast) (Scene, error) {
|
|
var s Scene
|
|
var err error
|
|
|
|
s.forecast = forecast
|
|
s.generator = opensimplex.NewNormalized(0)
|
|
|
|
s.foreground, err = readRunesFromFile(path.Join(scenePath, fgPath))
|
|
if err != nil {
|
|
return s, err
|
|
}
|
|
s.depth, err = readRunesFromFile(path.Join(scenePath, depthPath))
|
|
if err != nil {
|
|
return s, err
|
|
}
|
|
|
|
s.windground, err = readRunesFromFile(path.Join(scenePath, windPath))
|
|
if err != nil {
|
|
return s, err
|
|
}
|
|
|
|
bgPath := timeToPath[forecast.time]
|
|
s.background, err = readRunesFromFile(path.Join(scenePath, bgPath))
|
|
if err != nil {
|
|
return s, err
|
|
}
|
|
|
|
s.normalize()
|
|
|
|
/*m.depthData = make([][]uint8, 0)
|
|
for i := range depthRunes {
|
|
m.depthData = append(m.depthData, make([]uint8, 0))
|
|
for j := range depthRunes[i] {
|
|
m.depthData[i] = append(m.depthData[i], uint8(depthRunes[i][j])-48)
|
|
}
|
|
}*/
|
|
return s, nil
|
|
}
|
|
|
|
// normalize adjusts the foreground, windground, depth map, and background to
|
|
// have matching widths and heights. It adjusts by cropping to the shortest
|
|
// number of lines between the four and adjusts each line to the shortest of
|
|
// any lines in any of the four maps.
|
|
func (s *Scene) normalize() {
|
|
scenes := [...][][]rune{s.foreground, s.windground, s.depth, s.background}
|
|
|
|
s.Height = 999999999
|
|
for i := range scenes {
|
|
if len(scenes[i]) < s.Height {
|
|
s.Height = len(scenes[i])
|
|
}
|
|
}
|
|
|
|
for i := range scenes {
|
|
scenes[i] = scenes[i][:s.Height]
|
|
}
|
|
|
|
s.Width = 999999999
|
|
for j := 0; j < s.Height; j++ {
|
|
for i := range scenes {
|
|
if len(scenes[i][j]) < s.Width {
|
|
s.Width = len(scenes[i][j])
|
|
}
|
|
}
|
|
|
|
for i := range scenes {
|
|
scenes[i][j] = scenes[i][j][:s.Width]
|
|
}
|
|
}
|
|
|
|
// TODO: Change this to a map? Map can be iterated over and still referenced by name.
|
|
s.foreground = scenes[0]
|
|
s.windground = scenes[1]
|
|
s.depth = scenes[2]
|
|
s.background = scenes[3]
|
|
}
|
|
|
|
func (s Scene) GetCell(x, y, time int) (rune, Style) {
|
|
fx := float64(x)
|
|
fy := float64(y)
|
|
ftime := float64(time)
|
|
fcloud := float64(s.forecast.cloudiness)
|
|
fwind := float64(s.forecast.windiness)
|
|
frain := float64(s.forecast.raininess)
|
|
|
|
char := ' '
|
|
style := Style{
|
|
termenv.ANSI256Color(255),
|
|
termenv.ANSI256Color(232),
|
|
}
|
|
|
|
// Out of bounds
|
|
if y >= s.Height || y < 0 {
|
|
return char, Style{}
|
|
}
|
|
if x >= s.Width || x < 0 {
|
|
return char, Style{}
|
|
}
|
|
|
|
depth := uint8(s.depth[y][x] - 48)
|
|
|
|
// Char selection
|
|
char = s.foreground[y][x]
|
|
// Pull from wind map if the current cell is windswept.
|
|
if fwind > 0.0 && s.generator.Eval3(fx/40+fwind/8*ftime*2, fy/20, ftime/5+fwind/10*ftime/2) > 0.6 {
|
|
char = s.windground[y][x]
|
|
}
|
|
// Depth 9 is considered "transparent," so use the char from the background.
|
|
if depth == 9 {
|
|
char = s.background[y][x]
|
|
}
|
|
if frain > 0.0 && 0.1+frain/20 > s.generator.Eval3(fx+ftime*fwind*3, fy-ftime*5, ftime/15) {
|
|
char = '|'
|
|
if fwind > 1.0 {
|
|
char = '/'
|
|
}
|
|
}
|
|
|
|
// Style selection
|
|
// Calculate fog
|
|
fog := (depth - 1) + uint8(s.forecast.visibility-1)*2
|
|
if s.forecast.visibility == 0 {
|
|
fog = (depth + uint8(s.forecast.visibility)) / 2
|
|
}
|
|
// Don't show fog in the sky during the night.
|
|
if depth == 9 && (s.forecast.time == Night || s.forecast.time == EarlyMorning) {
|
|
fog = 0
|
|
}
|
|
|
|
// Add clouds
|
|
cloudiness := 0
|
|
if depth > 8 && fcloud/5 > s.generator.Eval3(fx/50+ftime/24+ftime*(fwind/8), fy/12, 1000+ftime/80) {
|
|
cloudiness += 10
|
|
}
|
|
if depth > 6 && fcloud/7 > s.generator.Eval3(fx/20+ftime/14+ftime*(fwind/8), fy/6, 0+ftime/80) {
|
|
cloudiness += 10
|
|
}
|
|
|
|
// At night, fog obscures distant objects with darkness.
|
|
if s.forecast.time == Night || s.forecast.time == EarlyMorning {
|
|
style.fg = termenv.ANSI256Color(255 - fog - fog/2)
|
|
} else {
|
|
// Merge fog with clouds during daytime.
|
|
cloudiness += int(fog)
|
|
}
|
|
|
|
if cloudiness > 23 {
|
|
cloudiness = 23
|
|
}
|
|
|
|
if cloudiness > 0 {
|
|
style.bg = termenv.ANSI256Color(232 + cloudiness)
|
|
} else {
|
|
style.bg = termenv.ANSI256Color(232 + fog)
|
|
}
|
|
return char, style
|
|
}
|
|
|
|
// readRunesFromFile reads the file at the given path and reads it character by
|
|
// character, line by line into a slice of slices of runes.
|
|
func readRunesFromFile(filepath string) ([][]rune, error) {
|
|
file, err := os.Open(filepath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
br := bufio.NewReader(file)
|
|
img := make([][]rune, 0)
|
|
img = append(img, make([]rune, 0))
|
|
|
|
for {
|
|
r, s, err := br.ReadRune()
|
|
if err != nil {
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
// Invalid unicode characters are skipped.
|
|
if r == unicode.ReplacementChar && s == 1 {
|
|
continue
|
|
}
|
|
|
|
// Start a new slice on newline, otherwise append character to current slice.
|
|
if r == '\n' {
|
|
img = append(img, make([]rune, 0))
|
|
} else {
|
|
img[len(img)-1] = append(img[len(img)-1], r)
|
|
}
|
|
}
|
|
|
|
return img, nil
|
|
}
|