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 } 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) return e } 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] }) } func (e *LogoEngine) Update(now time.Time) { if e.LastUpdate.IsZero() { e.LastUpdate = now } dt := now.Sub(e.LastUpdate).Seconds() if dt > 0.1 { dt = 0.1 } e.LastUpdate = now e.PhaseTime += dt switch e.Phase { case logoPhaseAssemble: spawnRate := float64(len(e.Nodes)) / 2.0 // Заполнение за ~2 секунды 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 := make([]*logoParticle, 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) 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)) } func (e *LogoEngine) Render(now time.Time) string { grid := make([][]string, e.Height) for y := 0; y < e.Height; y++ { grid[y] = make([]string, e.Width) for x := 0; x < e.Width; x++ { 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) 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() 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) grid[y][x] = style.Render(char) } } } var sb strings.Builder for y := 0; y < e.Height; y++ { sb.WriteString(strings.Join(grid[y], "")) if y < e.Height-1 { sb.WriteString("\n") } } return sb.String() } // 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 }