Ztorrent/ui/logo.go

535 lines
13 KiB
Go

package ui
import (
"fmt"
"math"
"math/rand"
"strings"
"time"
"github.com/charmbracelet/lipgloss"
)
// ── Стили и цвета логотипа ───────────────────────────────────────────────────
var (
logoColorGray = lipgloss.Color("#444444")
logoColorRed = lipgloss.Color("#ff4444")
logoColorYellow = lipgloss.Color("#ffff44")
logoColorGreen = lipgloss.Color("#44ff44")
logoColorWhite = lipgloss.Color("#ffffff")
)
var (
logoStyleIdle = lipgloss.NewStyle().Foreground(logoColorGray)
logoStyleDownloading = lipgloss.NewStyle().Foreground(logoColorYellow)
logoStyleVerifying = lipgloss.NewStyle().Foreground(logoColorYellow)
logoStyleCompleted = lipgloss.NewStyle().Foreground(logoColorGreen)
)
// ── Состояние узла ───────────────────────────────────────────────────────────
type logoNodeState int
const (
logoStateIdle logoNodeState = iota
logoStateDownloading
logoStateVerifying
logoStateCompleted
logoStateFading
logoStateOutline
)
type logoNode struct {
ID int
X, Y float64
State logoNodeState
StateTime time.Time
TargetDuration time.Duration
IsOutline bool
}
func NewLogoNode(id int, x, y float64) *logoNode {
return &logoNode{
ID: id,
X: x,
Y: y,
State: logoStateIdle,
StateTime: time.Now(),
IsOutline: false,
}
}
func (n *logoNode) setState(state logoNodeState, now time.Time) {
n.State = state
n.StateTime = now
switch state {
case logoStateVerifying:
n.TargetDuration = 3 * time.Second
case logoStateCompleted:
n.TargetDuration = 1 * time.Second
case logoStateFading:
n.TargetDuration = 1 * time.Second
default:
n.TargetDuration = 0
}
}
func (n *logoNode) Render(now time.Time) (string, lipgloss.Style) {
char := "•"
style := logoStyleIdle
switch n.State {
case logoStateIdle:
style = logoStyleIdle
case logoStateDownloading:
style = logoStyleDownloading
case logoStateVerifying:
// Плавный переход от желтого (255, 255, 68) к зеленому (68, 255, 68)
t := float64(now.Sub(n.StateTime)) / float64(n.TargetDuration)
if t > 1.0 {
t = 1.0
}
r := int(255.0 - (255.0-68.0)*t)
g := 255
b := 68
hex := fmt.Sprintf("#%02x%02x%02x", r, g, b)
style = lipgloss.NewStyle().Foreground(lipgloss.Color(hex))
case logoStateCompleted:
style = logoStyleCompleted
case logoStateFading:
t := float64(now.Sub(n.StateTime)) / float64(n.TargetDuration)
if t > 1.0 {
t = 1.0
}
r := 68
g := int(255.0 - (255.0-68.0)*t)
b := 68
hex := fmt.Sprintf("#%02x%02x%02x", r, g, b)
style = lipgloss.NewStyle().Foreground(lipgloss.Color(hex))
case logoStateOutline:
style = lipgloss.NewStyle().Foreground(logoColorWhite)
}
return char, style
}
// ── Частицы ──────────────────────────────────────────────────────────────────
type logoParticleType int
const (
logoTypeDownload logoParticleType = iota
logoTypeUpload
)
type logoParticle struct {
Type logoParticleType
X, Y float64
StartX float64
StartY float64
TargetX float64
TargetY float64
Speed float64
Progress float64 // 0.0 до 1.0
Angle float64 // Для дрифта
Active bool
TargetID int // ID целевого узла (для частиц загрузки)
}
func NewLogoParticle(pType logoParticleType, startX, startY, targetX, targetY float64, targetID int) *logoParticle {
var speed float64
if pType == logoTypeDownload {
speed = 0.5 + rand.Float64()*0.5
} else {
speed = 0.25 + rand.Float64()*0.25
}
return &logoParticle{
Type: pType,
X: startX,
Y: startY,
StartX: startX,
StartY: startY,
TargetX: targetX,
TargetY: targetY,
Speed: speed,
Progress: 0.0,
Angle: rand.Float64() * math.Pi * 2,
Active: true,
TargetID: targetID,
}
}
func (p *logoParticle) Update(dt float64) {
if !p.Active {
return
}
p.Progress += p.Speed * dt
if p.Progress >= 1.0 {
p.Progress = 1.0
p.Active = false
p.X = p.TargetX
p.Y = p.TargetY
return
}
var eased float64
if p.Type == logoTypeDownload {
t := p.Progress
eased = 1.0 - math.Pow(1.0-t, 3.0)
} else {
t := p.Progress
eased = math.Pow(t, 2.0)
}
baseX := p.StartX + (p.TargetX-p.StartX)*eased
baseY := p.StartY + (p.TargetY-p.StartY)*eased
p.X = baseX
p.Y = baseY
}
func (p *logoParticle) Render() (string, lipgloss.Style) {
char := "·"
if p.Type == logoTypeUpload {
r := int(170.0 - (170.0-51.0)*p.Progress)
g := int(255.0 - (255.0-51.0)*p.Progress)
b := int(170.0 - (170.0-51.0)*p.Progress)
hex := fmt.Sprintf("#%02x%02x%02x", r, g, b)
return char, lipgloss.NewStyle().Foreground(lipgloss.Color(hex))
}
return char, lipgloss.NewStyle().Foreground(logoColorRed)
}
// ── Движок логотипа ──────────────────────────────────────────────────────────
type logoEnginePhase int
const (
logoPhaseAssemble logoEnginePhase = iota
logoPhaseVerify
logoPhaseDisperse
logoPhaseWait
)
type LogoEngine struct {
Nodes []*logoNode
Particles []*logoParticle
Width int
Height int
LastUpdate time.Time
Phase logoEnginePhase
PhaseTime float64
TargetNodeQueue []int
SpawnAccumulator float64
Grid [][]string
// Pre-baked frames
frames []string
frameIndex int
}
func (e *LogoEngine) ensureGrid(w, h int) {
if len(e.Grid) < h {
newGrid := make([][]string, h)
copy(newGrid, e.Grid)
e.Grid = newGrid
}
for y := 0; y < h; y++ {
if len(e.Grid[y]) < w {
newRow := make([]string, w)
copy(newRow, e.Grid[y])
e.Grid[y] = newRow
}
}
}
func NewLogoEngine(width, height int, offsetX, offsetY float64) *LogoEngine {
e := &LogoEngine{
Nodes: generateZNodes(offsetX, offsetY),
Particles: make([]*logoParticle, 0),
Width: width,
Height: height,
LastUpdate: time.Now(),
}
e.startAssemblePhase(e.LastUpdate)
e.bakeFrames()
return e
}
const logoBakeFrames = 180 // ~18 секунд полного цикла при 10 FPS
func (e *LogoEngine) bakeFrames() {
now := time.Unix(0, 0)
e.frames = make([]string, logoBakeFrames)
for f := 0; f < logoBakeFrames; f++ {
e.simulateStep(now, 0.1)
now = now.Add(100 * time.Millisecond)
e.frames[f] = e.renderNow(now)
}
}
func (e *LogoEngine) simulateStep(now time.Time, dt float64) {
e.PhaseTime += dt
switch e.Phase {
case logoPhaseAssemble:
spawnRate := float64(len(e.Nodes)) / 2.0
e.SpawnAccumulator += spawnRate * dt
spawnCount := int(e.SpawnAccumulator)
e.SpawnAccumulator -= float64(spawnCount)
for i := 0; i < spawnCount && len(e.TargetNodeQueue) > 0; i++ {
targetID := e.TargetNodeQueue[0]
e.TargetNodeQueue = e.TargetNodeQueue[1:]
e.spawnDownloadParticleTo(e.Nodes[targetID])
}
activeDL := 0
for _, p := range e.Particles {
if p.Type == logoTypeDownload && p.Active {
activeDL++
}
}
if len(e.TargetNodeQueue) == 0 && activeDL == 0 {
e.Phase = logoPhaseVerify
e.PhaseTime = 0
for _, n := range e.Nodes {
if !n.IsOutline {
n.setState(logoStateVerifying, now)
}
}
}
case logoPhaseVerify:
if e.PhaseTime >= 3.0 {
e.Phase = logoPhaseDisperse
e.PhaseTime = 0
for _, n := range e.Nodes {
if !n.IsOutline {
n.setState(logoStateCompleted, now)
e.spawnUploadParticle(n)
}
}
}
case logoPhaseDisperse:
if e.PhaseTime >= 1.0 {
for _, n := range e.Nodes {
if !n.IsOutline {
n.setState(logoStateFading, now)
}
}
e.Phase = logoPhaseWait
e.PhaseTime = 0
}
case logoPhaseWait:
if e.PhaseTime >= 2.0 {
e.startAssemblePhase(now)
}
}
activeParticles := e.Particles[:0]
for _, p := range e.Particles {
p.Update(dt)
if !p.Active && p.Type == logoTypeDownload {
if p.TargetID >= 0 && p.TargetID < len(e.Nodes) {
node := e.Nodes[p.TargetID]
if node.State == logoStateIdle {
node.setState(logoStateDownloading, now)
}
}
}
if p.Active {
activeParticles = append(activeParticles, p)
}
}
e.Particles = activeParticles
}
func (e *LogoEngine) renderNow(now time.Time) string {
e.ensureGrid(e.Width, e.Height)
for y := 0; y < e.Height; y++ {
for x := 0; x < e.Width; x++ {
e.Grid[y][x] = " "
}
}
for _, n := range e.Nodes {
if !n.IsOutline {
x, y := int(n.X+0.5), int(n.Y+0.5)
if x >= 0 && x < e.Width && y >= 0 && y < e.Height {
char, style := n.Render(now)
e.Grid[y][x] = style.Render(char)
}
}
}
for _, p := range e.Particles {
x, y := int(p.X+0.5), int(p.Y+0.5)
if x >= 0 && x < e.Width && y >= 0 && y < e.Height {
char, style := p.Render()
e.Grid[y][x] = style.Render(char)
}
}
for _, n := range e.Nodes {
if n.IsOutline {
x, y := int(n.X+0.5), int(n.Y+0.5)
if x >= 0 && x < e.Width && y >= 0 && y < e.Height {
char, style := n.Render(now)
e.Grid[y][x] = style.Render(char)
}
}
}
var sb strings.Builder
sb.Grow(e.Height * e.Width * 5)
for y := 0; y < e.Height; y++ {
sb.WriteString(strings.Join(e.Grid[y][:e.Width], ""))
if y < e.Height-1 {
sb.WriteString("\n")
}
}
return sb.String()
}
func (e *LogoEngine) startAssemblePhase(now time.Time) {
e.Phase = logoPhaseAssemble
e.PhaseTime = 0
e.SpawnAccumulator = 0
e.TargetNodeQueue = nil
for _, n := range e.Nodes {
if !n.IsOutline {
n.setState(logoStateIdle, now)
e.TargetNodeQueue = append(e.TargetNodeQueue, n.ID)
}
}
rand.Shuffle(len(e.TargetNodeQueue), func(i, j int) {
e.TargetNodeQueue[i], e.TargetNodeQueue[j] = e.TargetNodeQueue[j], e.TargetNodeQueue[i]
})
}
// Update переключает кадр — нулевой CPU.
func (e *LogoEngine) Update(now time.Time) {
if len(e.frames) > 0 {
e.frameIndex = (e.frameIndex + 1) % len(e.frames)
}
}
func (e *LogoEngine) spawnDownloadParticleTo(target *logoNode) {
cx := float64(e.Width) / 2.0
cy := float64(e.Height) / 2.0
radius := 18.0 + rand.Float64()*5.0
angle := rand.Float64() * math.Pi * 2
startX := cx + math.Cos(angle)*radius
startY := cy + math.Sin(angle)*radius
e.Particles = append(e.Particles, NewLogoParticle(logoTypeDownload, startX, startY, target.X, target.Y, target.ID))
}
func (e *LogoEngine) spawnUploadParticle(n *logoNode) {
var targetX, targetY float64
angle := rand.Float64() * math.Pi * 2
dist := 8.0 + rand.Float64()*4.0
targetX = n.X + math.Cos(angle)*dist
targetY = n.Y + math.Sin(angle)*dist
e.Particles = append(e.Particles, NewLogoParticle(logoTypeUpload, n.X, n.Y, targetX, targetY, -1))
}
// Render возвращает pre-baked кадр — нулевой CPU.
func (e *LogoEngine) Render(now time.Time) string {
if len(e.frames) == 0 {
return ""
}
return e.frames[e.frameIndex]
}
// generateZNodes генерирует координаты для Z-образного логотипа.
func generateZNodes(offsetX, offsetY float64) []*logoNode {
var nodes []*logoNode
id := 0
startX, endX := 0.0, 20.0
startY, endY := 0.0, 10.0
addNode := func(x, y float64) {
nodes = append(nodes, NewLogoNode(id, x+offsetX, y+offsetY))
id++
}
thicknessY := 2.0
thicknessX := 3.0
// Верхняя полоса
for y := startY; y <= startY+thicknessY; y += 1.0 {
for x := startX; x <= endX; x += 0.7 {
addNode(x, y)
}
}
// Диагональ
steps := 50
for i := 0; i <= steps; i++ {
progress := float64(i) / float64(steps)
baseX := endX - progress*(endX-startX)
baseY := startY + progress*(endY-startY)
for ox := -thicknessX; ox <= thicknessX; ox += 0.7 {
addNode(baseX+ox, baseY)
}
}
// Нижняя полоса
for y := endY - thicknessY; y <= endY; y += 1.0 {
for x := startX; x <= endX; x += 0.7 {
addNode(x, y)
}
}
seen := make(map[string]bool)
var uniqueNodes []*logoNode
for _, n := range nodes {
cx, cy := int(n.X+0.5), int(n.Y+0.5)
key := fmt.Sprintf("%d,%d", cx, cy)
if !seen[key] {
seen[key] = true
uniqueNodes = append(uniqueNodes, n)
}
}
nodes = uniqueNodes
for idx, n := range nodes {
n.ID = idx
}
grid := make(map[string]bool)
for _, n := range nodes {
cx, cy := int(n.X+0.5), int(n.Y+0.5)
grid[fmt.Sprintf("%d,%d", cx, cy)] = true
}
for _, n := range nodes {
cx, cy := int(n.X+0.5), int(n.Y+0.5)
isEdge := false
for dy := -1; dy <= 1; dy++ {
for dx := -1; dx <= 1; dx++ {
if dx == 0 && dy == 0 {
continue
}
neighborKey := fmt.Sprintf("%d,%d", cx+dx, cy+dy)
if !grid[neighborKey] {
isEdge = true
break
}
}
if isEdge {
break
}
}
if isEdge {
n.IsOutline = true
n.State = logoStateOutline
}
}
return nodes
}