Ztorrent/ui/matrix.go

175 lines
4.1 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package ui
import (
"fmt"
"math/rand"
"strings"
"github.com/charmbracelet/lipgloss"
)
type matrixDrop struct {
X int
Y float64
Speed float64
Length int
CharType int // 0 = Z, 1 = V, 2 = O
}
// MatrixEngine рендерит кадры заранее и хранит их как 2D-сетки ячеек.
// RenderRegion вырезает любой прямоугольник напрямую — без ANSI-парсинга.
type MatrixEngine struct {
Width int
Height int
// frames[f] — плоский массив len=Width*Height, доступ: cell = frames[f][y*Width+x]
frames [][]string
frameIndex int
}
const matrixNumFrames = 120
func NewMatrixEngine(width, height int) *MatrixEngine {
e := &MatrixEngine{
Width: width,
Height: height,
}
e.bakeFrames()
return e
}
func (e *MatrixEngine) bakeFrames() {
drops := []*matrixDrop{}
step := func(dt float64) {
for _, d := range drops {
d.Y += d.Speed * dt
}
var active []*matrixDrop
for _, d := range drops {
if int(d.Y)-d.Length < e.Height {
active = append(active, d)
}
}
drops = active
targetDrops := e.Width / 6
if targetDrops < 1 {
targetDrops = 1
}
if len(drops) < targetDrops && rand.Float64() < 0.15 {
drops = append(drops, &matrixDrop{
X: rand.Intn(e.Width),
Y: float64(rand.Intn(5)) - 5.0,
Speed: 3.0 + rand.Float64()*4.0,
Length: 4 + rand.Intn(5),
CharType: rand.Intn(3),
})
}
}
renderGrid := func() []string {
grid := make([]string, e.Width*e.Height)
for i := range grid {
grid[i] = " "
}
for _, d := range drops {
headY := int(d.Y)
for i := 0; i < d.Length; i++ {
y := headY - i
x := d.X
if y >= 0 && y < e.Height && x >= 0 && x < e.Width {
opacity := 1.0 - float64(i)/float64(d.Length)
var r, g, b int
if i == 0 {
r, g, b = 255, 255, 255
} else if d.CharType == 0 {
g = int(255.0 * opacity)
} else if d.CharType == 1 {
r = int(255.0 * opacity)
} else {
r = int(255.0 * opacity)
g = int(255.0 * opacity)
}
char := "Z"
if d.CharType == 1 {
char = "V"
} else if d.CharType == 2 {
char = "O"
}
grid[y*e.Width+x] = getMatrixStyledChar(char, r, g, b, i == 0)
}
}
}
return grid
}
// Прогрев: заполняем экран каплями
for i := 0; i < 100; i++ {
step(0.1)
}
e.frames = make([][]string, matrixNumFrames)
for f := 0; f < matrixNumFrames; f++ {
// 5 шагов симуляции на кадр (100ms / 5 = 20ms шаг)
for s := 0; s < 5; s++ {
step(0.02)
}
e.frames[f] = renderGrid()
}
}
// Update переключает кадр — нулевой CPU.
func (e *MatrixEngine) Update() {
e.frameIndex = (e.frameIndex + 1) % len(e.frames)
}
// RenderRegion вырезает прямоугольник [rx,ry,rw,rh] из текущего кадра.
// Работает напрямую с ячейками — нет ANSI-парсинга, нет аллокаций.
func (e *MatrixEngine) RenderRegion(rx, ry, rw, rh int) string {
if len(e.frames) == 0 || rw <= 0 || rh <= 0 {
return strings.Repeat(" \n", rh)
}
frame := e.frames[e.frameIndex]
var sb strings.Builder
sb.Grow(rh * rw * 8)
for row := 0; row < rh; row++ {
y := ry + row
for col := 0; col < rw; col++ {
x := rx + col
if y >= 0 && y < e.Height && x >= 0 && x < e.Width {
sb.WriteString(frame[y*e.Width+x])
} else {
sb.WriteString(" ")
}
}
if row < rh-1 {
sb.WriteString("\n")
}
}
return sb.String()
}
// CurrentFrame — весь кадр целиком (для совместимости).
func (e *MatrixEngine) CurrentFrame() string {
return e.RenderRegion(0, 0, e.Width, e.Height)
}
var matrixStyleCache = make(map[string]string)
func getMatrixStyledChar(char string, r, g, b int, bold bool) string {
hex := fmt.Sprintf("#%02x%02x%02x", r, g, b)
key := char + hex
if bold {
key += "B"
}
if val, ok := matrixStyleCache[key]; ok {
return val
}
style := lipgloss.NewStyle().Foreground(lipgloss.Color(hex))
if bold {
style = style.Bold(true)
}
res := style.Render(char)
matrixStyleCache[key] = res
return res
}