Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ A terminal-based presentation tool that creates beautiful presentations from mar
- Swipe left/right
- Slide up/down
- Flip effects
- Collapse and expand
- Fade in/out
- **Hot reload**: Live reloading of presentation files during editing by default
- **Customizable styling**: Configure borders, colors, and layouts via YAML front matter
- **Theme support**: Choose from built-in Glamour themes or load custom JSON theme files
Expand Down Expand Up @@ -130,6 +132,9 @@ This slide uses a custom JSON theme file
- `slideUp` - Slide slides up from bottom
- `slideDown` - Slide slides down from top
- `flip` - Flip transition effect
- `collapse` - Collapse transition effect
- `expand` - Expand transition effect
- `fade` - Fade transition effect

### Style Configuration

Expand Down Expand Up @@ -250,5 +255,5 @@ All contributions are welcome! If you're planning a significant change or you're
- ~~Allow choosing from any glamour themes~~ ✅ **Done!**
- ~~Support for custom JSON theme files~~ ✅ **Done!**
- Create grid-based slide layouts with transitions for each pane
- Add more transition effects
- ~~Add more transition effects~~ ✅ **Done!**
- Support image rendering in terminals (e.g., via the Kitty protocol)
131 changes: 131 additions & 0 deletions internal/tui/transitions/collapse.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package transitions

import (
"math"
"strings"
"time"

tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/harmonica"
charmansi "github.com/charmbracelet/x/ansi"
"github.com/muesli/reflow/truncate"

"github.com/museslabs/kyma/internal/skip"
)

type collapse struct {
width int
fps int
spring harmonica.Spring
progress float64
vel float64
animating bool
direction direction
}

func newCollapse(fps int) collapse {
const frequency = 7.0
const damping = 0.6

return collapse{
fps: fps,
spring: harmonica.NewSpring(harmonica.FPS(fps), frequency, damping),
}
}

func (t collapse) Start(width, _ int, direction direction) Transition {
t.width = width
t.animating = true
t.progress = 0
t.vel = 0
t.direction = direction
return t
}

func (t collapse) Animating() bool {
return t.animating
}

func (t collapse) Update() (Transition, tea.Cmd) {
targetProgress := 1.0

t.progress, t.vel = t.spring.Update(t.progress, t.vel, targetProgress)

if t.progress >= 0.99 {
t.animating = false
t.progress = 1.0
return t, nil
}

return t, Animate(time.Duration(t.fps))
}

func (t collapse) View(prev, next string) string {
var s strings.Builder

// Calculate how much should collapse from edges toward center
collapseWidth := int(math.Round((1.0 - t.progress) * float64(t.width) / 2))
centerStart := t.width/2 - collapseWidth
centerEnd := t.width/2 + collapseWidth

prevLines := strings.Split(prev, "\n")
nextLines := strings.Split(next, "\n")

// Ensure slides are equal height
maxLines := max(len(nextLines), len(prevLines))

for i := range maxLines {
var prevLine, nextLine string

if i < len(prevLines) {
prevLine = prevLines[i]
}
if i < len(nextLines) {
nextLine = nextLines[i]
}

var line string
if collapseWidth <= 0 {
// Animation complete, show next content
line = truncate.String(nextLine, uint(t.width))
} else {
// Build the line with collapse effect from edges toward center
// Center portion: show prev content that's collapsing
var center string
if centerEnd > centerStart {
truncatedPrev := truncate.String(prevLine, uint(centerEnd))
center = skip.String(truncatedPrev, uint(centerStart))
}

// Left and right parts: show next content appearing from edges
leftNext := truncate.String(nextLine, uint(centerStart))

rightNext := ""
if centerEnd < t.width {
rightNext = charmansi.TruncateLeft(nextLine, centerEnd, "")
}

line = leftNext + center + rightNext
line = truncate.String(line, uint(t.width))
}

s.WriteString(line)
if i < maxLines-1 {
s.WriteString("\n")
}
}

return s.String()
}

func (t collapse) Name() string {
return "collapse"
}

func (t collapse) Opposite() Transition {
return newExpand(t.fps)
}

func (t collapse) Direction() direction {
return t.direction
}
131 changes: 131 additions & 0 deletions internal/tui/transitions/expand.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package transitions

import (
"math"
"strings"
"time"

tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/harmonica"
charmansi "github.com/charmbracelet/x/ansi"
"github.com/muesli/reflow/truncate"

"github.com/museslabs/kyma/internal/skip"
)

type expand struct {
width int
fps int
spring harmonica.Spring
progress float64
vel float64
animating bool
direction direction
}

func newExpand(fps int) expand {
const frequency = 7.0
const damping = 0.6

return expand{
fps: fps,
spring: harmonica.NewSpring(harmonica.FPS(fps), frequency, damping),
}
}

func (t expand) Start(width, _ int, direction direction) Transition {
t.width = width
t.animating = true
t.progress = 0
t.vel = 0
t.direction = direction
return t
}

func (t expand) Animating() bool {
return t.animating
}

func (t expand) Update() (Transition, tea.Cmd) {
targetProgress := 1.0

t.progress, t.vel = t.spring.Update(t.progress, t.vel, targetProgress)

if t.progress >= 0.99 {
t.animating = false
t.progress = 1.0
return t, nil
}

return t, Animate(time.Duration(t.fps))
}

func (t expand) View(prev, next string) string {
var s strings.Builder

// Calculate how much should expand from center outward
expandWidth := int(math.Round(t.progress * float64(t.width) / 2))
centerStart := t.width/2 - expandWidth
centerEnd := t.width/2 + expandWidth

prevLines := strings.Split(prev, "\n")
nextLines := strings.Split(next, "\n")

// Ensure slides are equal height
maxLines := max(len(nextLines), len(prevLines))

for i := range maxLines {
var prevLine, nextLine string

if i < len(prevLines) {
prevLine = prevLines[i]
}
if i < len(nextLines) {
nextLine = nextLines[i]
}

var line string
if expandWidth >= t.width/2 {
// Animation complete, show next content
line = truncate.String(nextLine, uint(t.width))
} else {
// Build the line with expand effect from center outward
// Left and right parts: show prev content
leftPrev := truncate.String(prevLine, uint(centerStart))

rightPrev := ""
if centerEnd < t.width {
rightPrev = charmansi.TruncateLeft(prevLine, centerEnd, "")
}

// Center portion: show next content expanding from center
var center string
if centerEnd > centerStart {
truncatedNext := truncate.String(nextLine, uint(centerEnd))
center = skip.String(truncatedNext, uint(centerStart))
}

line = leftPrev + center + rightPrev
line = truncate.String(line, uint(t.width))
}

s.WriteString(line)
if i < maxLines-1 {
s.WriteString("\n")
}
}

return s.String()
}

func (t expand) Name() string {
return "expand"
}

func (t expand) Opposite() Transition {
return newCollapse(t.fps)
}

func (t expand) Direction() direction {
return t.direction
}
Loading