|  | 
|  | 1 | +package transitions | 
|  | 2 | + | 
|  | 3 | +import ( | 
|  | 4 | +	"math" | 
|  | 5 | +	"math/rand" | 
|  | 6 | +	"strings" | 
|  | 7 | +	"time" | 
|  | 8 | + | 
|  | 9 | +	tea "github.com/charmbracelet/bubbletea" | 
|  | 10 | +	"github.com/charmbracelet/harmonica" | 
|  | 11 | +	charmansi "github.com/charmbracelet/x/ansi" | 
|  | 12 | +	"github.com/muesli/reflow/truncate" | 
|  | 13 | + | 
|  | 14 | +	"github.com/museslabs/kyma/internal/skip" | 
|  | 15 | +) | 
|  | 16 | + | 
|  | 17 | +type tileGrid struct { | 
|  | 18 | +	tileSize   int | 
|  | 19 | +	gridWidth  int | 
|  | 20 | +	gridHeight int | 
|  | 21 | +	tileOrder  []int | 
|  | 22 | +} | 
|  | 23 | + | 
|  | 24 | +func newTileGrid(width, height, tileSize int) tileGrid { | 
|  | 25 | +	gridWidth := (width + tileSize - 1) / tileSize | 
|  | 26 | +	gridHeight := (height + tileSize - 1) / tileSize | 
|  | 27 | + | 
|  | 28 | +	totalTiles := gridWidth * gridHeight | 
|  | 29 | +	tileOrder := make([]int, totalTiles) | 
|  | 30 | +	for i := range totalTiles { | 
|  | 31 | +		tileOrder[i] = i | 
|  | 32 | +	} | 
|  | 33 | + | 
|  | 34 | +	r := rand.New(rand.NewSource(42)) | 
|  | 35 | +	r.Shuffle(len(tileOrder), func(i, j int) { | 
|  | 36 | +		tileOrder[i], tileOrder[j] = tileOrder[j], tileOrder[i] | 
|  | 37 | +	}) | 
|  | 38 | + | 
|  | 39 | +	return tileGrid{ | 
|  | 40 | +		tileSize:   tileSize, | 
|  | 41 | +		gridWidth:  gridWidth, | 
|  | 42 | +		gridHeight: gridHeight, | 
|  | 43 | +		tileOrder:  tileOrder, | 
|  | 44 | +	} | 
|  | 45 | +} | 
|  | 46 | + | 
|  | 47 | +func (g tileGrid) revealedTiles(progress float64) map[int]bool { | 
|  | 48 | +	totalTiles := len(g.tileOrder) | 
|  | 49 | +	revealedCount := int(math.Round(progress * float64(totalTiles))) | 
|  | 50 | + | 
|  | 51 | +	revealedSet := make(map[int]bool) | 
|  | 52 | +	for i := range revealedCount { | 
|  | 53 | +		if i < len(g.tileOrder) { | 
|  | 54 | +			revealedSet[g.tileOrder[i]] = true | 
|  | 55 | +		} | 
|  | 56 | +	} | 
|  | 57 | +	return revealedSet | 
|  | 58 | +} | 
|  | 59 | + | 
|  | 60 | +func (g tileGrid) tileIndex(x, y int) int { | 
|  | 61 | +	tileX := x / g.tileSize | 
|  | 62 | +	tileY := y / g.tileSize | 
|  | 63 | +	return tileY*g.gridWidth + tileX | 
|  | 64 | +} | 
|  | 65 | + | 
|  | 66 | +type fade struct { | 
|  | 67 | +	width     int | 
|  | 68 | +	height    int | 
|  | 69 | +	fps       int | 
|  | 70 | +	spring    harmonica.Spring | 
|  | 71 | +	progress  float64 | 
|  | 72 | +	vel       float64 | 
|  | 73 | +	animating bool | 
|  | 74 | +	direction direction | 
|  | 75 | +	grid      tileGrid | 
|  | 76 | +} | 
|  | 77 | + | 
|  | 78 | +func newFade(fps int) fade { | 
|  | 79 | +	const frequency = 15 | 
|  | 80 | +	const damping = 0.65 | 
|  | 81 | + | 
|  | 82 | +	return fade{ | 
|  | 83 | +		fps:    fps, | 
|  | 84 | +		spring: harmonica.NewSpring(harmonica.FPS(fps), frequency, damping), | 
|  | 85 | +	} | 
|  | 86 | +} | 
|  | 87 | + | 
|  | 88 | +func (t fade) Start(width, height int, direction direction) Transition { | 
|  | 89 | +	t.width = width | 
|  | 90 | +	t.height = height | 
|  | 91 | +	t.animating = true | 
|  | 92 | +	t.progress = 0 | 
|  | 93 | +	t.vel = 0 | 
|  | 94 | +	t.direction = direction | 
|  | 95 | +	t.grid = newTileGrid(width, height, 2) | 
|  | 96 | + | 
|  | 97 | +	return t | 
|  | 98 | +} | 
|  | 99 | + | 
|  | 100 | +func (t fade) Animating() bool { | 
|  | 101 | +	return t.animating | 
|  | 102 | +} | 
|  | 103 | + | 
|  | 104 | +func (t fade) Update() (Transition, tea.Cmd) { | 
|  | 105 | +	targetProgress := 1.0 | 
|  | 106 | + | 
|  | 107 | +	t.progress, t.vel = t.spring.Update(t.progress, t.vel, targetProgress) | 
|  | 108 | + | 
|  | 109 | +	if t.progress >= 0.99 { | 
|  | 110 | +		t.animating = false | 
|  | 111 | +		t.progress = 1.0 | 
|  | 112 | +		return t, nil | 
|  | 113 | +	} | 
|  | 114 | + | 
|  | 115 | +	return t, Animate(time.Duration(t.fps)) | 
|  | 116 | +} | 
|  | 117 | + | 
|  | 118 | +func (t fade) View(prev, next string) string { | 
|  | 119 | +	var s strings.Builder | 
|  | 120 | + | 
|  | 121 | +	prevLines := strings.Split(prev, "\n") | 
|  | 122 | +	nextLines := strings.Split(next, "\n") | 
|  | 123 | + | 
|  | 124 | +	// Ensure slides are equal height | 
|  | 125 | +	maxLines := max(len(nextLines), len(prevLines)) | 
|  | 126 | + | 
|  | 127 | +	// Get revealed tiles from grid | 
|  | 128 | +	revealedSet := t.grid.revealedTiles(t.progress) | 
|  | 129 | +	allRevealed := len(revealedSet) >= len(t.grid.tileOrder) | 
|  | 130 | + | 
|  | 131 | +	for lineIdx := range maxLines { | 
|  | 132 | +		var prevLine, nextLine string | 
|  | 133 | + | 
|  | 134 | +		if lineIdx < len(prevLines) { | 
|  | 135 | +			prevLine = prevLines[lineIdx] | 
|  | 136 | +		} | 
|  | 137 | +		if lineIdx < len(nextLines) { | 
|  | 138 | +			nextLine = nextLines[lineIdx] | 
|  | 139 | +		} | 
|  | 140 | + | 
|  | 141 | +		var line string | 
|  | 142 | +		if allRevealed { | 
|  | 143 | +			line = truncate.String(nextLine, uint(t.width)) | 
|  | 144 | +		} else { | 
|  | 145 | +			line = t.buildFadeLine(prevLine, nextLine, revealedSet, lineIdx) | 
|  | 146 | +		} | 
|  | 147 | + | 
|  | 148 | +		s.WriteString(line) | 
|  | 149 | +		if lineIdx < maxLines-1 { | 
|  | 150 | +			s.WriteString("\n") | 
|  | 151 | +		} | 
|  | 152 | +	} | 
|  | 153 | + | 
|  | 154 | +	return s.String() | 
|  | 155 | +} | 
|  | 156 | + | 
|  | 157 | +func (t fade) buildFadeLine(prevLine, nextLine string, revealedSet map[int]bool, lineIdx int) string { | 
|  | 158 | +	var result strings.Builder | 
|  | 159 | + | 
|  | 160 | +	for tileX := range t.grid.gridWidth { | 
|  | 161 | +		startPos := tileX * t.grid.tileSize | 
|  | 162 | +		endPos := min(startPos+t.grid.tileSize, t.width) | 
|  | 163 | +		tileWidth := endPos - startPos | 
|  | 164 | + | 
|  | 165 | +		tileIndex := t.grid.tileIndex(startPos, lineIdx) | 
|  | 166 | + | 
|  | 167 | +		// Choose source line based on tile state | 
|  | 168 | +		sourceLine := prevLine | 
|  | 169 | +		if revealedSet[tileIndex] { | 
|  | 170 | +			sourceLine = nextLine | 
|  | 171 | +		} | 
|  | 172 | + | 
|  | 173 | +		// Extract tile segment | 
|  | 174 | +		segment := t.extractTileSegment(sourceLine, startPos, tileWidth) | 
|  | 175 | +		result.WriteString(segment) | 
|  | 176 | +	} | 
|  | 177 | + | 
|  | 178 | +	finalLine := result.String() | 
|  | 179 | +	if charmansi.StringWidth(finalLine) > t.width { | 
|  | 180 | +		finalLine = truncate.String(finalLine, uint(t.width)) | 
|  | 181 | +	} | 
|  | 182 | + | 
|  | 183 | +	return finalLine | 
|  | 184 | +} | 
|  | 185 | + | 
|  | 186 | +func (t fade) extractTileSegment(line string, startPos, tileWidth int) string { | 
|  | 187 | +	if startPos == 0 { | 
|  | 188 | +		return truncate.String(line, uint(tileWidth)) | 
|  | 189 | +	} | 
|  | 190 | + | 
|  | 191 | +	skipped := skip.String(line, uint(startPos)) | 
|  | 192 | +	return truncate.String(skipped, uint(tileWidth)) | 
|  | 193 | +} | 
|  | 194 | + | 
|  | 195 | +func (t fade) Name() string { | 
|  | 196 | +	return "fade" | 
|  | 197 | +} | 
|  | 198 | + | 
|  | 199 | +func (t fade) Opposite() Transition { | 
|  | 200 | +	return newFade(t.fps) | 
|  | 201 | +} | 
|  | 202 | + | 
|  | 203 | +func (t fade) Direction() direction { | 
|  | 204 | +	return t.direction | 
|  | 205 | +} | 
0 commit comments