175 lines
4.1 KiB
Go
175 lines
4.1 KiB
Go
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
|
||
}
|