boot.go (2869B)
1 package tui 2 3 import ( 4 "time" 5 6 tea "github.com/charmbracelet/bubbletea" 7 ) 8 9 const ( 10 bootChars = "\u2593\u2592\u2591\u2588\u2584\u2580\u25a0\u25a1\u25aa\u25ab\u25cf\u25cb\u25c6\u25c7" 11 bootStartDelay = 100 * time.Millisecond 12 bootScrambleDur = 400 * time.Millisecond 13 bootScrambleTick = 30 * time.Millisecond 14 bootHoldDur = 450 * time.Millisecond 15 bootFadeDur = 300 * time.Millisecond 16 bootTitleRevealEnd = bootStartDelay + bootScrambleDur 17 bootSubRevealEnd = bootTitleRevealEnd + bootScrambleDur 18 bootHoldEnd = bootSubRevealEnd + bootHoldDur 19 bootAnimationTotal = bootHoldEnd + bootFadeDur 20 ) 21 22 var bootRuneSet = []rune(bootChars) 23 24 type bootTickMsg time.Time 25 26 func bootTickCmd() tea.Cmd { 27 return tea.Tick(bootScrambleTick, func(t time.Time) tea.Msg { 28 return bootTickMsg(t) 29 }) 30 } 31 32 func (m *portfolioModel) syncBootFrame(now time.Time) { 33 if !m.bootActive { 34 return 35 } 36 37 elapsed := now.Sub(m.bootStartedAt) 38 if elapsed < 0 { 39 elapsed = 0 40 } 41 42 switch { 43 case elapsed < bootStartDelay: 44 m.bootFadeProgress = 0 45 m.bootTitleFrame = m.scrambleBootText(m.bootTitle) 46 m.bootSubtitleFrame = m.scrambleBootText(m.bootSubtitle) 47 case elapsed < bootTitleRevealEnd: 48 m.bootFadeProgress = 0 49 p := float64(elapsed-bootStartDelay) / float64(bootScrambleDur) 50 m.bootTitleFrame = m.revealBootText(m.bootTitle, p) 51 m.bootSubtitleFrame = m.scrambleBootText(m.bootSubtitle) 52 case elapsed < bootSubRevealEnd: 53 m.bootFadeProgress = 0 54 p := float64(elapsed-bootTitleRevealEnd) / float64(bootScrambleDur) 55 m.bootTitleFrame = m.bootTitle 56 m.bootSubtitleFrame = m.revealBootText(m.bootSubtitle, p) 57 case elapsed < bootHoldEnd: 58 m.bootFadeProgress = 0 59 m.bootTitleFrame = m.bootTitle 60 m.bootSubtitleFrame = m.bootSubtitle 61 case elapsed < bootAnimationTotal: 62 m.bootFadeProgress = float64(elapsed-bootHoldEnd) / float64(bootFadeDur) 63 m.bootTitleFrame = m.bootTitle 64 m.bootSubtitleFrame = m.bootSubtitle 65 default: 66 m.bootFadeProgress = 1 67 m.bootTitleFrame = m.bootTitle 68 m.bootSubtitleFrame = m.bootSubtitle 69 m.bootActive = false 70 } 71 } 72 73 func (m *portfolioModel) scrambleBootText(target string) string { 74 length := len([]rune(target)) 75 if length == 0 { 76 return "" 77 } 78 out := make([]rune, length) 79 for i := 0; i < length; i++ { 80 out[i] = bootRuneSet[m.bootRand.Intn(len(bootRuneSet))] 81 } 82 return string(out) 83 } 84 85 func (m *portfolioModel) revealBootText(target string, progress float64) string { 86 r := []rune(target) 87 if len(r) == 0 { 88 return "" 89 } 90 91 if progress < 0 { 92 progress = 0 93 } else if progress > 1 { 94 progress = 1 95 } 96 97 revealCount := int(progress * float64(len(r))) 98 if revealCount > len(r) { 99 revealCount = len(r) 100 } 101 102 out := make([]rune, 0, len(r)) 103 out = append(out, r[:revealCount]...) 104 for i := revealCount; i < len(r); i++ { 105 out = append(out, bootRuneSet[m.bootRand.Intn(len(bootRuneSet))]) 106 } 107 108 return string(out) 109 }