Initial commit

main
diff 2021-05-28 22:58:08 -05:00
commit 534eab9fac
11 changed files with 822 additions and 0 deletions

1
.gitignore vendored 100644
View File

@ -0,0 +1 @@
atmosphere

131
forecast.go 100644
View File

@ -0,0 +1,131 @@
package main
import (
"os/exec"
"strings"
)
// Forecast is a representation of the current state of the weather.
type Forecast struct {
raw string
time TimeOfDay
cloudiness Cloudiness
raininess Raininess
visibility Visibility
windiness Windiness
}
// NewForecast parses the output of ~iajrz's climate program.
// TODO?: boolean randomize option to generate completely random weather as a demo mode?
func NewForecast() (Forecast, error) {
out, err := exec.Command("/home/iajrz/climate").Output()
if err != nil {
return Forecast{}, err
}
rawWeather := string(out)
return Forecast{
raw: rawWeather,
time: TimeOfDay(findSubstring(rawWeather, timeStrings)),
cloudiness: Cloudiness(findSubstring(rawWeather, cloudStrings)),
raininess: Raininess(findSubstring(rawWeather, rainStrings)),
visibility: Visibility(findSubstring(rawWeather, visibilityStrings)),
windiness: Windiness(findSubstring(rawWeather, windStrings)),
}, nil
}
func (f Forecast) String() string {
return f.raw
}
func findSubstring(s string, substrings []string) int {
for i := range substrings {
if strings.Contains(s, substrings[i]) {
return i
}
}
return 0
}
type TimeOfDay int
const (
EarlyMorning TimeOfDay = iota
Morning
Afternoon
Night
)
var timeStrings = []string{
"early morning",
"morning",
"afternoon",
"night",
}
type Cloudiness int
const (
ClearSky Cloudiness = iota
AlmostClear
PartlyCloudy
MostlyCloudy
Cloudy
)
var cloudStrings = []string{
"clear",
"almost clear",
"partly cloudy",
"mostly cloudy",
"cloudy",
}
type Raininess int
const (
NoRain Raininess = iota
Drizzle
LightShower
Shower
HeavyShower
)
var rainStrings = []string{
"no rain",
"a drizzle",
"a light shower",
"a shower",
"a heavy shower",
}
type Visibility int
const (
NoFog Visibility = iota
Haze
Mist
Fog
HeavyFog
)
var visibilityStrings = []string{
"visibility",
"There's haze",
"There's mist",
"There's fog",
"There's heavy fog",
}
type Windiness int
const (
NoWind Windiness = iota
Breeze
StiffWind
)
var windStrings = []string{
"no breeze",
"light breeze",
"stiff wind",
}

202
main.go 100644
View File

@ -0,0 +1,202 @@
package main
import (
tea "github.com/charmbracelet/bubbletea"
"github.com/muesli/reflow/wordwrap"
"github.com/muesli/termenv"
"strings"
"time"
)
func main() {
app := tea.NewProgram(model{startTime: time.Now()})
if err := app.Start(); err != nil {
panic(err)
}
}
// Queues the initial loading of the forecast and
func (m model) Init() tea.Cmd {
return tea.Batch(updateForecast, renderOften)
}
var updateForecast = func() tea.Msg {
forecast, err := NewForecast()
if err != nil {
return err
}
return forecast
}
var updateForecastOften = tea.Tick(20*time.Second, func(t time.Time) tea.Msg {
return updateForecast()
})
var makeSceneMsg = func(f Forecast) func() tea.Msg {
return func() tea.Msg {
scene, err := NewScene("scene1", f)
if err != nil {
return err
}
return scene
}
}
type fadeOut float32
type fadeIn float32
// Used to update the progress of a fade out transition.
var tickFadeOut = tea.Every(time.Millisecond*100, func(t time.Time) tea.Msg {
return fadeOut(0.05)
})
// Used to update the progress of a fade in transition.
var tickFadeIn = tea.Every(time.Millisecond*100, func(t time.Time) tea.Msg {
return fadeIn(0.05)
})
type renderTick struct{}
// Used to update the screen at least once per second.
var renderOften = tea.Tick(time.Second, func(t time.Time) tea.Msg {
return renderTick{}
})
// Update updates the internal state of the model and queues any events that
// need to be scheduled.
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
cmds := []tea.Cmd{}
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c", "ctrl+d", "q":
return m, tea.Quit
case "esc":
// TODO: Menu for options. Maybe 100% randomized weather?
}
case tea.WindowSizeMsg:
m.width = msg.Width
m.height = msg.Height
case Forecast:
// Re-queue the forecast polling.
cmds = append(cmds, updateForecastOften)
// No action required if the forecast hasn't changed.
if msg == m.Forecast {
break
}
// If this isn't the initial startup forecast, we need to fade out the scene.
if m.Forecast != (Forecast{}) {
m.fading = true
cmds = append(cmds, tickFadeOut)
}
// In all cases, we need to load a scene with the forecast.
m.Forecast = msg
cmds = append(cmds, makeSceneMsg(m.Forecast))
case Scene:
if m.Scene.foreground == nil {
m.Scene = msg
} else {
m.nextScene = msg
}
case fadeOut:
m.fadeProgress += float32(msg)
if m.fadeProgress >= 1.0 {
m.fadeProgress = 1.0
// To unfade, we need a Scene ready to rock. If we don't have one, stall.
if m.nextScene.foreground == nil {
cmds = append(cmds, tickFadeOut)
break
}
m.Scene = m.nextScene
m.nextScene = Scene{}
cmds = append(cmds, tickFadeIn)
break
}
cmds = append(cmds, tickFadeOut)
case fadeIn:
m.fadeProgress -= float32(msg)
if m.fadeProgress <= 0.0 {
m.fadeProgress = 0.0
m.fading = false
break
}
cmds = append(cmds, tickFadeIn)
case error:
m.err = msg
case renderTick:
cmds = append(cmds, renderOften)
}
return m, tea.Batch(cmds...)
}
// View lays out the screen and queries the Scene for its contents.
// It also handles transitions when changing from Scene to Scene.
func (m model) View() string {
if m.err != nil {
return m.err.Error()
}
weather := wordwrap.String(m.Forecast.String(), m.width)
m.height -= strings.Count(weather, "\n")
weather = strings.TrimSpace(weather) // Trim trailing newline
output := ""
lastStyle := Style{}
profile := termenv.EnvColorProfile()
// Center scene in terminal
xOff := (m.Scene.Width - m.width) / 2
yOff := (m.Scene.Height - m.height) / 2
for y := yOff; y < yOff+m.height; y++ {
for x := xOff; x < xOff+m.width; x++ {
char, style := m.Scene.GetCell(x, y, int(time.Since(m.startTime).Seconds()))
style = style.Convert(profile)
// For wipe transitions, replace character with blank space.
if float32((x-xOff)/2+y-yOff) < m.fadeProgress*float32(m.width/2+m.height) {
char = ' '
style = Style{}
}
// Only output formatting escape codes if the style's changed since the last cell.
if lastStyle.String() != style.String() {
output += style.String()
lastStyle = style
}
output += string(char)
}
output += "\n"
}
output += termenv.CSI + termenv.ResetSeq + "m"
output += weather
return output
}
type model struct {
Forecast
Scene
err error
nextScene Scene
fading bool
fadeProgress float32
width int
height int
startTime time.Time
}

271
scene.go 100644
View File

@ -0,0 +1,271 @@
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
}

View File

@ -0,0 +1,31 @@
---___ | __------''''
---__ ,.---., __---
-__ ;/-'''-\; --'
:|' `|:
:|. ,|; --__ ___
__--- ;\-___-/; ''-- --___
'' , `'---'` -----____
___---'' / ''
'' ' \
____-- , \
'' /
/ \
__-- ' \
\
/
/
/
'

31
scene1/depth.txt 100644
View File

@ -0,0 +1,31 @@
9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999
9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999
9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999899
9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999988999999999999988888
9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999889999999999999999999999999999999998888888888899999888888888888
9999999999999999999999999999999999998999999999999999999999999999999999999999999998888888888889999999999999999999999999888888888888888889998888888888888
9999999999999999999999999999999999888889999988888888999999988889999998889998888888888888888888888888899999888889999888888888888888888888888888888888888
9999999999999999999999999999999998888888888888888888888988888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888
9999999999999999999999999999998888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888555555555888
9999999999999999999999999999988888888888888888888888888888888888888888888888888888888888888888888888855555555555888888888855555555555555555555555555555
9999999999999999999999999988888888888888888888888888888888888888888888888888888888888888888888555555555555555555555535555555555555555555555555555555555
9999999999999998889999999888888888888888888888888888888888888888888888888888888888888888885555555555555555555555555355555555555555555555555555555555555
9999999999999988888899998888888888888888888888888888888888888888888888888888888888888885555555555555555555555555553555555555555555555555555555555555555
9999988889999888888888888888888888888888888888888888888888888888888888888888555558888833333333333333333333553333335555555555555555555555555555555555555
9888888888888888888888888888888888888888888888888888888888888888888888855555555555555333333333333333333333355555533555555555555555555555555555555555555
8888888888888888888888888888888888888555555558888888888888555555555555555555555555533333333333333333333333335555535355555555555555533333333333333333333
8888888888888888888888555588885555555555555555555555555555555555555555555555555555333333333333333333333333344444434434444444333333333333333333333333333
8888888888888888855555555555555555555555555555555555555555555555555555555555555444433333333333333333333333344444434444444433333333333333333333333333333
8888888855555555555555555555555555555555555555555555544444444444444444444444444444433333333333333333333333344433333333333333333333333333333333333333333
5555555555555555555555555555555555555555555544444444444444443333333333333333333333333333333333333333333333333333333333333333333333333111111111111111111
5555555555555555555555555555555555544444444444444444444444333333333333333333333333333333333333333333333333333333333333331111111111111111111111111111111
5555555555555555544444444444444444444444444444444444443333333333333333333333333333333333333333333333333333333311111111111111111111111111111111111111111
4444444444444444444444444444444444444444444444444333333333333333333333333333333333333333333333333333333331111111111111111111111111111111111111111111111
3333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333111111111111111111111111111111111111111111111111
3333333333333333333333333333333333333333333333333333333333333333333333333333333333332222222222222221111111111111111111111111111111111111111111111111111
2222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222111111111111111111111111111111111111111111111111111111111111
2222222222222222222222222222222222221212222222222222222222222222222222222211111111111111111111111111111111111111111111111111111111111111111111111111111
1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111

31
scene1/early.txt 100644
View File

@ -0,0 +1,31 @@
. *
` * . `
* ` .: TWA. *
* . . / 0 Vm *
, . 'B .
* * * |' D
` '| O P
\\
. ' `=____-'

View File

@ -0,0 +1,31 @@
.
__ ./ \.
__ ___.--'' \ _.--'\/
. ___,.-" '-, _--'' \ / /
./ \. /''\_.-. _-". .-. __.--'" '--.____ ___.. _-' '-/
/ \_.-' \-. ,-" \_-""" \-" "---' \--"
__/ ' ,----.___
/ _._________ _______..------'' ''-
.-' _.--''' ''--/-''''
.__ / _-'` /
/ '- / .-' __ /
__.. / '--" _.--. /-----""----,-,_____ -----O '
_.-' '--' __.-' `-__//-\___-' .' _ '. \ |\ '
' _,-.,___ ____,,,,..../ ,-'|+|,- .' |+| '-,-" | \
____ ,.----" \___,----''`` -,-- ---| | | \ .=
..--' '--/ . - | == == | _ _ | |
.--.___.' __ | === | |+| |+| | | '
-'-.___/ ' _ - _ _ __ ___ __|__|||___|_____________|___ _|_ _
' .- - '
' -' - '
' .
_ . - - " ' && '
' _..= - '
' _.' - '
' ' * * ' ' '
*|*|* @&& , &&
' &&&@& ' '
' '

31
scene1/morning.txt 100644
View File

@ -0,0 +1,31 @@
'--__ | __------
--__ ,.---., __---
-__ ;/-'''-\; --'
:|' `|:
:|. ,|; --__ ___
;\-___-/; ''-- ____
, `'---'` '----_____
/ -------____
' \ ____
, \ '''--------
/
/ \
/ \
' \

31
scene1/night.txt 100644
View File

@ -0,0 +1,31 @@
. *
` * . `
* .: TWA. ` *
/ 0 Vm * . . *
, . 'B * .
|' D * * *
'| O P
\\
. `=____-' '

31
scene1/wind.txt 100644
View File

@ -0,0 +1,31 @@
.
__ ./ \.
__ ___.--'' \ _.--'\/
. ___,.-" '-, _--'' \ / /
./ \. /''\_.-. _-". .-. __.--'" '--.____ ___.. _-' '-/
/ \_.-' \-. ,-" \_-""" \-" "---' \--"
__/ ' ,----.___
/ _._________ _______..------'' ''-
.-' _.--''' ''--/-''''
.__ / _-'` /
/ '- / .-' __ /
__.. / '--" _.--. /-----""----,-,_____ -----O `
_.-' '--' __.-' `-__//-\___-' .' _ '. \ |\ `
' _,-.,___ ____,,,,..../ ,-'|+|,- .' |+| '-,-" | \
____ ,.----" \___,----''`` -,-- ---| | | \ .=
..--' '--/ . - | == == | _ _ | |
.--.___.' __ | === | |+| |+| | | `
-'-.___/ ` _ - _ _ __ ___ __|__|||___|_____________|___ _|_ _
` .- - '
` -' - '
' .
_ . - - " ` && '
' _..= - `
' _.` - '
` ` + + ` ` `
+|+|+ @&& , &&
' &&&@& ` `
` `