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 }