This is page 2 of 2. Use http://codebase.md/Zebbeni/ansizalizer?page={x} to view the full context. # Directory Structure ``` ├── .gitignore ├── ansizalizer ├── app │ ├── adapt │ │ └── generate.go │ ├── export.go │ ├── item.go │ ├── model.go │ ├── process │ │ ├── ascii.go │ │ ├── custom.go │ │ ├── image.go │ │ ├── renderer.go │ │ └── unicode.go │ ├── resize.go │ ├── update.go │ └── view.go ├── assets │ └── palettes │ ├── android-screenshot-editor.hex │ ├── cascade-gb.hex │ ├── dull-aquatic.hex │ ├── florescence.hex │ ├── gb-blue-steel.hex │ ├── hama-beads-tub.hex │ ├── kiwami64-v1.hex │ └── yes.hex ├── controls │ ├── browser │ │ ├── item.go │ │ ├── model.go │ │ └── update.go │ ├── export │ │ ├── destination │ │ │ ├── model.go │ │ │ ├── update.go │ │ │ └── view.go │ │ ├── model.go │ │ ├── source │ │ │ ├── model.go │ │ │ ├── update.go │ │ │ └── view.go │ │ ├── update.go │ │ └── view.go │ ├── menu │ │ └── model.go │ ├── model.go │ ├── settings │ │ ├── advanced │ │ │ ├── dithering │ │ │ │ ├── list.go │ │ │ │ ├── model.go │ │ │ │ ├── update.go │ │ │ │ └── view.go │ │ │ ├── model.go │ │ │ ├── sampling │ │ │ │ ├── const.go │ │ │ │ ├── item.go │ │ │ │ ├── model.go │ │ │ │ └── update.go │ │ │ ├── update.go │ │ │ └── view.go │ │ ├── characters │ │ │ ├── init.go │ │ │ ├── model.go │ │ │ ├── tabs.go │ │ │ ├── update.go │ │ │ └── view.go │ │ ├── colors │ │ │ ├── model.go │ │ │ ├── update.go │ │ │ └── view.go │ │ ├── item.go │ │ ├── model.go │ │ ├── palettes │ │ │ ├── adaptive │ │ │ │ ├── init.go │ │ │ │ ├── model.go │ │ │ │ ├── update.go │ │ │ │ └── view.go │ │ │ ├── loader │ │ │ │ ├── item.go │ │ │ │ ├── model.go │ │ │ │ ├── values.go │ │ │ │ └── view.go │ │ │ ├── lospec │ │ │ │ ├── init.go │ │ │ │ ├── list.go │ │ │ │ ├── model.go │ │ │ │ ├── update.go │ │ │ │ └── view.go │ │ │ ├── matrix.go │ │ │ ├── model.go │ │ │ ├── update.go │ │ │ └── view.go │ │ ├── size │ │ │ ├── init.go │ │ │ ├── model.go │ │ │ ├── update.go │ │ │ └── view.go │ │ ├── state.go │ │ ├── update.go │ │ └── view.go │ ├── update.go │ └── view.go ├── display │ └── model.go ├── env │ ├── os_darwin.go │ ├── os_linux.go │ └── os_windows.go ├── event │ ├── command.go │ └── keymap.go ├── global │ └── file.go ├── go.mod ├── go.sum ├── images │ └── characters │ ├── char_001.png │ ├── char_002.png │ ├── char_003.png │ ├── char_004.png │ ├── char_005.png │ ├── char_006.png │ ├── char_007.png │ ├── char_008.png │ ├── char_009.png │ ├── char_010.png │ ├── char_011.png │ ├── char_012.png │ ├── char_013.png │ ├── char_014.png │ ├── char_015.png │ ├── char_016.png │ ├── char_017.png │ ├── char_018.png │ ├── char_019.png │ ├── char_020.png │ ├── char_021.png │ ├── char_022.png │ ├── char_023.png │ ├── char_024.png │ ├── char_025.png │ ├── char_026.png │ ├── char_027.png │ └── char_028.png ├── LICENSE.md ├── main.go ├── palette │ ├── model.go │ └── view.go ├── README.md ├── style │ ├── box.go │ └── color.go ├── test_images │ ├── dock.png │ ├── mermaid.png │ ├── mona_lisa.jpg │ ├── planet.png │ ├── robots.png │ ├── sewer.png │ └── throne.png └── viewer ├── model.go └── update.go ``` # Files -------------------------------------------------------------------------------- /app/update.go: -------------------------------------------------------------------------------- ```go package app import ( "bufio" "encoding/json" "fmt" "io" "net/http" "os" "strings" "github.com/atotto/clipboard" tea "github.com/charmbracelet/bubbletea" "github.com/Zebbeni/ansizalizer/app/adapt" "github.com/Zebbeni/ansizalizer/app/process" "github.com/Zebbeni/ansizalizer/event" ) func (m Model) handleStartRenderToViewCmd() (Model, tea.Cmd) { m.viewer.WaitingOnRender = true return m, m.processRenderToViewCmd } func (m Model) handleFinishRenderToViewMsg(msg event.FinishRenderToViewMsg) (Model, tea.Cmd) { // cut out early if the finished render is for a previously selected image if msg.FilePath != m.controls.FileBrowser.ActiveFile { return m, nil } var cmd tea.Cmd m.viewer, cmd = m.viewer.Update(msg) return m, cmd } func (m Model) processRenderToViewCmd() tea.Msg { imgString := process.RenderImageFile(m.controls.Settings, m.controls.FileBrowser.ActiveFile) colorsString := "true color" if m.controls.Settings.Colors.IsLimited() { palette := m.controls.Settings.Colors.GetCurrentPalette() colorsString = palette.Title() } return event.FinishRenderToViewMsg{FilePath: m.controls.FileBrowser.ActiveFile, ImgString: imgString, ColorsString: colorsString} } func (m Model) handleStartExportMsg(msg event.StartExportMsg) (Model, tea.Cmd) { if m.waitingOnExport { return m, nil } var exportQueue []exportJob var err error // build export queue if msg.IsDir { exportQueue, err = buildExportQueue(msg.SourcePath, msg.DestinationPath, msg.UseSubDirs) if err != nil { return m, event.BuildDisplayCmd(fmt.Sprintf("error exporting: %s", err)) } } else { exportQueue = []exportJob{ { sourcePath: msg.SourcePath, destinationPath: msg.DestinationPath, }, } } m.exportIndex = 0 m.exportQueue = exportQueue m.waitingOnExport = true return m, tea.Batch(event.StartRenderToExportCmd, event.BuildDisplayCmd(fmt.Sprintf("%d jobs queued", len(exportQueue)))) } func (m Model) handleRenderToExportMsg() (Model, tea.Cmd) { currentJob := m.exportQueue[m.exportIndex] // render image imgString := process.RenderImageFile(m.controls.Settings, currentJob.sourcePath) // save file file, err := os.Create(currentJob.destinationPath) if err != nil { return m, event.BuildDisplayCmd("error creating save file") } w := bufio.NewWriter(file) _, err = w.WriteString(imgString) if err != nil { return m, event.BuildDisplayCmd("error writing to save file") } m.exportIndex += 1 displayMsg := fmt.Sprintf("%d/%d exports completed", m.exportIndex, len(m.exportQueue)) displayCmd := event.BuildDisplayCmd(displayMsg) if m.exportIndex >= len(m.exportQueue) { m.waitingOnExport = false return m, displayCmd } return m, tea.Batch(event.StartRenderToExportCmd, displayCmd) } func (m Model) startExportingDir(msg event.StartExportMsg) (Model, tea.Cmd) { return m, event.BuildDisplayCmd(fmt.Sprintf("exporting %s", msg.SourcePath)) } func (m Model) startExportingFile(msg event.StartExportMsg) (Model, tea.Cmd) { return m, event.BuildDisplayCmd(fmt.Sprintf("exporting %s", msg.SourcePath)) } func (m Model) handleStartAdaptingMsg() (Model, tea.Cmd) { filename := m.controls.FileBrowser.ActiveFilename() message := fmt.Sprintf("generating palette from %s...", filename) return m, tea.Batch(event.BuildDisplayCmd(message), m.processAdaptingCmd) } func (m Model) handleFinishAdaptingMsg(msg event.FinishAdaptingMsg) (Model, tea.Cmd) { m.controls.Settings.Colors.PaletteControls.Adapter = m.controls.Settings.Colors.PaletteControls.Adapter.SetPalette(msg.Colors, msg.Name) return m, tea.Batch(event.StartRenderToViewCmd, event.BuildDisplayCmd("rendering...")) } type Foo struct { Bar string } func (m Model) handleLospecRequestMsg(msg event.LospecRequestMsg) (Model, tea.Cmd) { // make url request r, err := http.Get(msg.URL) if err != nil { return m, event.BuildDisplayCmd("error making lospec request") } defer r.Body.Close() body, err := io.ReadAll(r.Body) if err != nil { return m, event.BuildDisplayCmd("error reading lospec response") } // parse json and populate LospecResponseMsg data := new(event.LospecData) err = json.Unmarshal(body, &data) if err != nil { return m, event.BuildDisplayCmd("error decoding lospec request") } // build Data Cmd return m, event.BuildLospecResponseCmd(event.LospecResponseMsg{ ID: msg.ID, Page: msg.Page, Data: *data, }) } func (m Model) handleLospecResponseMsg(msg event.LospecResponseMsg) (Model, tea.Cmd) { var cmd tea.Cmd m.controls.Settings.Colors.PaletteControls.Lospec, cmd = m.controls.Settings.Colors.PaletteControls.Lospec.Update(msg) return m, cmd } func (m Model) processAdaptingCmd() tea.Msg { colors, name := adapt.GeneratePalette(m.controls.Settings.Colors.PaletteControls.Adapter, m.controls.FileBrowser.ActiveFile) return event.FinishAdaptingMsg{ Name: name, Colors: colors, } } func (m Model) handleControlsUpdate(msg tea.Msg) (Model, tea.Cmd) { var cmd tea.Cmd m.controls, cmd = m.controls.Update(msg) return m, cmd } func (m Model) handleDisplayMsg(msg tea.Msg) (Model, tea.Cmd) { var cmd tea.Cmd m.display, cmd = m.display.Update(msg) return m, cmd } func (m Model) handleCopy() (Model, tea.Cmd) { if err := clipboard.WriteAll(m.viewer.View()); err != nil { return m, event.BuildDisplayCmd("Error copying to clipboard") // we should have a place in the UI where we display errors or processing messages, // and package our desired event to the user in a specific command } filename := m.controls.FileBrowser.ActiveFilename() name := strings.Split(filename, ".")[0] // strip extension return m, event.BuildDisplayCmd(fmt.Sprintf("copied %s to clipboard", name)) } func (m Model) handleSave() (Model, tea.Cmd) { name := strings.Split(m.controls.FileBrowser.ActiveFilename(), ".")[0] filename := fmt.Sprintf("%s.ansi", name) file, err := os.Create(filename) if err != nil { return m, event.BuildDisplayCmd("error creating save file") } w := bufio.NewWriter(file) _, err = w.WriteString(m.viewer.View()) if err != nil { return m, event.BuildDisplayCmd("error writing to save file") } return m, event.BuildDisplayCmd(fmt.Sprintf("saved to %s", filename)) } ``` -------------------------------------------------------------------------------- /app/process/renderer.go: -------------------------------------------------------------------------------- ```go package process import ( "image/color" "math" "github.com/lucasb-eyer/go-colorful" "github.com/Zebbeni/ansizalizer/controls/settings" "github.com/Zebbeni/ansizalizer/controls/settings/characters" ) type Renderer struct { Settings settings.Model shadeLightBlockFuncs map[rune]blockFunc shadeMedBlockFuncs map[rune]blockFunc shadeHeavyBlockFuncs map[rune]blockFunc quarterBlockFuncs map[rune]blockFunc halfBlockFuncs map[rune]blockFunc fullBlockFuncs map[rune]blockFunc } func New(s settings.Model) Renderer { m := Renderer{ Settings: s, } m.fullBlockFuncs = m.createFullBlockFuncs() m.halfBlockFuncs = m.createHalfBlockFuncs() m.quarterBlockFuncs = m.createQuarterBlockFuncs() m.shadeLightBlockFuncs = m.createShadeLightFuncs() m.shadeMedBlockFuncs = m.createShadeMedFuncs() m.shadeHeavyBlockFuncs = m.createShadeHeavyFuncs() return m } type blockFunc func(r1, r2, r3, r4 colorful.Color) (colorful.Color, colorful.Color, float64) func (m Renderer) createQuarterBlockFuncs() map[rune]blockFunc { return map[rune]blockFunc{ '▀': m.calcTop, '▐': m.calcRight, '▞': m.calcDiagonal, '▖': m.calcBotLeft, '▘': m.calcTopLeft, '▝': m.calcTopRight, '▗': m.calcBotRight, } } func (m Renderer) createHalfBlockFuncs() map[rune]blockFunc { return map[rune]blockFunc{ '▀': m.calcTop, } } func (m Renderer) createFullBlockFuncs() map[rune]blockFunc { return map[rune]blockFunc{ '█': m.calcFull, } } func (m Renderer) createShadeLightFuncs() map[rune]blockFunc { return map[rune]blockFunc{ '░': m.calcHeavy, } } func (m Renderer) createShadeMedFuncs() map[rune]blockFunc { return map[rune]blockFunc{ '▒': m.calcHeavy, } } func (m Renderer) createShadeHeavyFuncs() map[rune]blockFunc { return map[rune]blockFunc{ '▓': m.calcHeavy, } } func (m Renderer) getLightDarkPaletted(light, dark colorful.Color) (colorful.Color, colorful.Color) { _, _, p := m.Settings.Colors.GetSelected() colors := p.Colors() index := colors.Index(dark) paletteDark := colors.Convert(dark) palette := make([]color.Color, len(colors)) copy(palette, colors) paletteMinusDarkest := append(colors[:index], colors[index+1:]...) paletteLight := paletteMinusDarkest.Convert(light) light, _ = colorful.MakeColor(paletteLight) dark, _ = colorful.MakeColor(paletteDark) // swap light / dark if light is darker than dark lightBlackDist := light.DistanceLuv(black) darkBlackDist := dark.DistanceLuv(black) if darkBlackDist > lightBlackDist { temp := light light = dark dark = temp } return light, dark } func (m Renderer) getDarkestPaletted() colorful.Color { if !m.Settings.Colors.IsLimited() { return black } _, _, p := m.Settings.Colors.GetSelected() colors := p.Colors() darkest := colors.Convert(black) darkestConverted, _ := colorful.MakeColor(darkest) return darkestConverted } func (m Renderer) calcLight(r1, r2, r3, r4 colorful.Color) (colorful.Color, colorful.Color, float64) { if _, _, fgBg, _ := m.Settings.Characters.Selected(); fgBg == characters.OneColor { avg, dist := m.avgCol(r1, r2, r3, r4) return avg, colorful.Color{}, math.Min(1.0, math.Abs(dist-1)) } else { _, dark := lightDark(r1, r2, r3, r4) avg := m.avgColTrue(r1, r2, r3, r4) if m.Settings.Colors.IsLimited() { avg, dark = m.getLightDarkPaletted(avg, dark) } dist := avg.DistanceLuv(black) return avg, dark, math.Min(1.0, math.Abs(dist)) } } func (m Renderer) calcMed(r1, r2, r3, r4 colorful.Color) (colorful.Color, colorful.Color, float64) { if _, _, fgBg, _ := m.Settings.Characters.Selected(); fgBg == characters.OneColor { avg, dist := m.avgCol(r1, r2, r3, r4) return avg, colorful.Color{}, math.Min(1.0, math.Abs(dist-1)) } else { _, dark := lightDark(r1, r2, r3, r4) avg := m.avgColTrue(r1, r2, r3, r4) if m.Settings.Colors.IsLimited() { avg, dark = m.getLightDarkPaletted(avg, dark) } dist := avg.DistanceLuv(black) return avg, dark, math.Min(1.0, math.Abs(dist-0.5)) } } func (m Renderer) calcHeavy(r1, r2, r3, r4 colorful.Color) (colorful.Color, colorful.Color, float64) { if _, _, fgBg, _ := m.Settings.Characters.Selected(); fgBg == characters.OneColor { avg, dist := m.avgCol(r1, r2, r3, r4) return avg, colorful.Color{}, math.Min(1.0, math.Abs(dist-1)) } else { _, dark := lightDark(r1, r2, r3, r4) avg := m.avgColTrue(r1, r2, r3, r4) if m.Settings.Colors.IsLimited() { avg, dark = m.getLightDarkPaletted(avg, dark) } dist := avg.DistanceLuv(black) return avg, dark, math.Min(1.0, math.Abs(dist-1)) } } func (m Renderer) calcFull(r1, r2, r3, r4 colorful.Color) (colorful.Color, colorful.Color, float64) { if _, _, fgBg, _ := m.Settings.Characters.Selected(); fgBg == characters.OneColor { avg, _ := m.avgCol(r1, r2, r3, r4) return avg, colorful.Color{}, 1.0 } else { _, dark := lightDark(r1, r2, r3, r4) avg := m.avgColTrue(r1, r2, r3, r4) if m.Settings.Colors.IsLimited() { avg, dark = m.getLightDarkPaletted(avg, dark) } dist := avg.DistanceLuv(black) return avg, dark, math.Min(1.0, math.Abs(dist-1)) } } func (m Renderer) calcTop(r1, r2, r3, r4 colorful.Color) (colorful.Color, colorful.Color, float64) { if r1.R == 0 && r1.G == 0 && r1.B == 0 && (r3.R != 0 || r3.G != 0 || r3.B != 0) { r1.R = r1.G } fg, fDist := m.avgCol(r1, r2) bg, bDist := m.avgCol(r3, r4) return fg, bg, fDist + bDist } func (m Renderer) calcRight(r1, r2, r3, r4 colorful.Color) (colorful.Color, colorful.Color, float64) { fg, fDist := m.avgCol(r2, r4) bg, bDist := m.avgCol(r1, r3) return fg, bg, fDist + bDist } func (m Renderer) calcDiagonal(r1, r2, r3, r4 colorful.Color) (colorful.Color, colorful.Color, float64) { fg, fDist := m.avgCol(r2, r3) bg, bDist := m.avgCol(r1, r4) return fg, bg, fDist + bDist } func (m Renderer) calcBotLeft(r1, r2, r3, r4 colorful.Color) (colorful.Color, colorful.Color, float64) { fg, fDist := m.avgCol(r3) bg, bDist := m.avgCol(r1, r2, r4) return fg, bg, fDist + bDist } func (m Renderer) calcTopLeft(r1, r2, r3, r4 colorful.Color) (colorful.Color, colorful.Color, float64) { fg, fDist := m.avgCol(r1) bg, bDist := m.avgCol(r2, r3, r4) return fg, bg, fDist + bDist } func (m Renderer) calcTopRight(r1, r2, r3, r4 colorful.Color) (colorful.Color, colorful.Color, float64) { fg, fDist := m.avgCol(r2) bg, bDist := m.avgCol(r1, r3, r4) return fg, bg, fDist + bDist } func (m Renderer) calcBotRight(r1, r2, r3, r4 colorful.Color) (colorful.Color, colorful.Color, float64) { fg, fDist := m.avgCol(r4) bg, bDist := m.avgCol(r1, r2, r3) return fg, bg, fDist + bDist } ```