Skip to content

Commit

Permalink
feat: Set LoopOffset (#113)
Browse files Browse the repository at this point in the history
* feat: Set LoopOffset

* feat: Set LoopOffset (%)

* chore: refactor

* chore: update example and fmt

* feat: support only offset percentage

* chore: remove unnecessary export
  • Loading branch information
griimick authored Nov 1, 2022
1 parent 48d6b02 commit 0d38b18
Show file tree
Hide file tree
Showing 10 changed files with 140 additions and 1 deletion.
10 changes: 10 additions & 0 deletions command.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ var Settings = map[string]CommandFunc{
"Theme": ExecuteSetTheme,
"TypingSpeed": ExecuteSetTypingSpeed,
"Width": ExecuteSetWidth,
"LoopOffset": ExecuteLoopOffset,
}

// ExecuteSet applies the settings on the running vhs specified by the
Expand Down Expand Up @@ -315,6 +316,15 @@ func ExecuteSetPlaybackSpeed(c Command, v *VHS) {
v.Options.Video.PlaybackSpeed = playbackSpeed
}

// ExecuteLoopOffset applies the loop offset option on the vhs.
func ExecuteLoopOffset(c Command, v *VHS) {
loopOffset, err := strconv.ParseFloat(strings.TrimRight(c.Args, "%"), bitSize)
if err != nil {
return
}
v.Options.LoopOffset = loopOffset
}

func getTheme(s string) (Theme, error) {
if strings.TrimSpace(s) == "" {
return DefaultTheme, nil
Expand Down
2 changes: 2 additions & 0 deletions examples/fixtures/all.tape
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ Set Padding 50
Set Framerate 60
Set PlaybackSpeed 2
Set TypingSpeed .1
Set LoopOffset 60.4
Set LoopOffset 20.99%

# Sleep:
Sleep 1
Expand Down
3 changes: 3 additions & 0 deletions examples/settings/set-loop-offset.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions examples/settings/set-loop-offset.tape
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Output examples/settings/set-loop-offset.mp4
# Output examples/settings/set-loop-offset.webm
# Output examples/settings/frames/

Output examples/settings/set-loop-offset.gif

Set LoopOffset 50.55%
# Set LoopOffset 50.55

Type "Betty bought a butter but the bit of butter was bitter."
Sleep 1s
Type " So Betty bought another bit of butter to make the bitter bit of butter, a better bit of butter."
Sleep 2s

7 changes: 7 additions & 0 deletions lexer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,13 @@ func TestLexTapeFile(t *testing.T) {
{SET, "Set"},
{TYPING_SPEED, "TypingSpeed"},
{NUMBER, ".1"},
{SET, "Set"},
{LOOP_OFFSET, "LoopOffset"},
{NUMBER, "60.4"},
{SET, "Set"},
{LOOP_OFFSET, "LoopOffset"},
{NUMBER, "20.99"},
{PERCENT, "%"},
{COMMENT, " Sleep:"},
{SLEEP, "Sleep"},
{NUMBER, "1"},
Expand Down
9 changes: 9 additions & 0 deletions parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,15 @@ func (p *Parser) parseSet() Command {
p.nextToken()

switch p.cur.Type {
case LOOP_OFFSET:
cmd.Args = p.peek.Literal
p.nextToken()
// Allow LoopOffset without '%'
// Set LoopOffset 20
cmd.Args += "%"
if p.peek.Type == PERCENT {
p.nextToken()
}
case TYPING_SPEED:
cmd.Args = p.peek.Literal
p.nextToken()
Expand Down
2 changes: 2 additions & 0 deletions parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ func TestParseTapeFile(t *testing.T) {
{Type: SET, Options: "Framerate", Args: "60"},
{Type: SET, Options: "PlaybackSpeed", Args: "2"},
{Type: SET, Options: "TypingSpeed", Args: ".1s"},
{Type: SET, Options: "LoopOffset", Args: "60.4%"},
{Type: SET, Options: "LoopOffset", Args: "20.99%"},
{Type: SLEEP, Options: "", Args: "1s"},
{Type: SLEEP, Options: "", Args: "500ms"},
{Type: SLEEP, Options: "", Args: ".5s"},
Expand Down
4 changes: 3 additions & 1 deletion token.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ const (
TYPING_SPEED = "TYPING_SPEED"
PADDING = "PADDING"
THEME = "THEME"
LOOP_OFFSET = "LOOP_OFFSET"
)

var keywords = map[string]TokenType{
Expand Down Expand Up @@ -99,14 +100,15 @@ var keywords = map[string]TokenType{
"Padding": PADDING,
"Theme": THEME,
"Width": WIDTH,
"LoopOffset": LOOP_OFFSET,
}

// IsSetting returns whether a token is a setting.
func IsSetting(t TokenType) bool {
switch t {
case FONT_FAMILY, FONT_SIZE, LETTER_SPACING, LINE_HEIGHT,
FRAMERATE, TYPING_SPEED, THEME, PLAYBACK_SPEED,
HEIGHT, WIDTH, PADDING:
HEIGHT, WIDTH, PADDING, LOOP_OFFSET:
return true
default:
return false
Expand Down
81 changes: 81 additions & 0 deletions vhs.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"context"
"fmt"
"math"
"os"
"os/exec"
"path/filepath"
Expand All @@ -25,6 +26,7 @@ type VHS struct {
mutex *sync.Mutex
recording bool
tty *exec.Cmd
totalFrames int
close func() error
}

Expand All @@ -39,6 +41,7 @@ type Options struct {
Theme Theme
Test TestOptions
Video VideoOptions
LoopOffset float64
}

const (
Expand Down Expand Up @@ -137,6 +140,9 @@ func (vhs *VHS) Cleanup() {
vhs.browser.MustClose()
_ = vhs.tty.Process.Kill()

// Apply Loop Offset by modifying frame sequence
vhs.ApplyLoopOffset()

// Generate the video(s) with the frames.
var cmds []*exec.Cmd
cmds = append(cmds, MakeGIF(vhs.Options.Video))
Expand All @@ -159,6 +165,79 @@ func (vhs *VHS) Cleanup() {
}
}

// Apply Loop Offset by modifying frame sequence
func (vhs *VHS) ApplyLoopOffset() {
loopOffsetPercentage := vhs.Options.LoopOffset

// Calculate # of frames to offset from LoopOffset percentage
loopOffsetFrames := int(math.Ceil(loopOffsetPercentage / 100.0 * float64(vhs.totalFrames)))

// Take care of overflow and keep track of exact offsetPercentage
loopOffsetFrames = loopOffsetFrames % vhs.totalFrames
loopOffsetPercentage = float64(loopOffsetFrames) / float64(vhs.totalFrames) * 100

// No operation if nothing to offset
if loopOffsetFrames <= 0 {
return
}

// Move all frames in [offsetStart, offsetEnd] to end of frame sequence
offsetStart := vhs.Options.Video.StartingFrame
offsetEnd := loopOffsetFrames

// New starting frame will be the next frame after offsetEnd
vhs.Options.Video.StartingFrame = offsetEnd + 1
fmt.Printf(
"Applying LoopOffset %d/%d frames (%.2f%%)\n",
loopOffsetFrames, vhs.totalFrames, loopOffsetPercentage,
)

// Rename all text and cursor frame files in the range concurrently
errCh := make(chan error)
doneCh := make(chan bool)
var wg sync.WaitGroup

for counter := offsetStart; counter <= offsetEnd; counter++ {
wg.Add(1)
go func(frameNum int) {
defer wg.Done()
offsetFrameNum := frameNum + vhs.totalFrames
if err := os.Rename(
filepath.Join(vhs.Options.Video.Input, fmt.Sprintf(cursorFrameFormat, frameNum)),
filepath.Join(vhs.Options.Video.Input, fmt.Sprintf(cursorFrameFormat, offsetFrameNum)),
); err != nil {
errCh <- fmt.Errorf("error applying offset to cursor frame: %w", err)
}
}(counter)

wg.Add(1)
go func(frameNum int) {
defer wg.Done()
offsetFrameNum := frameNum + vhs.totalFrames
if err := os.Rename(
filepath.Join(vhs.Options.Video.Input, fmt.Sprintf(textFrameFormat, frameNum)),
filepath.Join(vhs.Options.Video.Input, fmt.Sprintf(textFrameFormat, offsetFrameNum)),
); err != nil {
errCh <- fmt.Errorf("error applying offset to text frame: %w", err)
}
}(counter)
}

go func() {
wg.Wait()
close(doneCh)
}()

select {
case <-doneCh:
break
case err := <-errCh:
// Bail out in case of an error while renaming
fmt.Println(err)
os.Exit(1)
}
}

const quality = 0.92

// Record begins the goroutine which captures images from the xterm.js canvases.
Expand All @@ -173,6 +252,8 @@ func (vhs *VHS) Record(ctx context.Context) <-chan error {
select {
case <-ctx.Done():
close(ch)
// Save total # of frames for offset calculation
vhs.totalFrames = counter
return

default:
Expand Down
9 changes: 9 additions & 0 deletions video.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ type VideoOptions struct {
Height int
Padding int
BackgroundColor string
StartingFrame int
}

const defaultFramerate = 50
Expand All @@ -57,6 +58,7 @@ const defaultMaxColors = 256
const defaultPadding = 72
const defaultPlaybackSpeed = 1.0
const defaultWidth = 1200
const defaultStartingFrame = 1

// DefaultVideoOptions is the set of default options for converting frames
// to a GIF, which are used if they are not overridden.
Expand All @@ -72,6 +74,7 @@ func DefaultVideoOptions() VideoOptions {
Padding: defaultPadding,
PlaybackSpeed: defaultPlaybackSpeed,
BackgroundColor: DefaultTheme.Background,
StartingFrame: defaultStartingFrame,
}
}

Expand All @@ -87,8 +90,10 @@ func MakeGIF(opts VideoOptions) *exec.Cmd {
return exec.Command(
"ffmpeg", "-y",
"-r", fmt.Sprint(opts.Framerate),
"-start_number", fmt.Sprint(opts.StartingFrame),
"-i", filepath.Join(opts.Input, textFrameFormat),
"-r", fmt.Sprint(opts.Framerate),
"-start_number", fmt.Sprint(opts.StartingFrame),
"-i", filepath.Join(opts.Input, cursorFrameFormat),
"-filter_complex",
fmt.Sprintf(`[0][1]overlay[merged];[merged]scale=%d:%d:force_original_aspect_ratio=1[scaled];[scaled]fps=%d,setpts=PTS/%f[speed];[speed]pad=%d:%d:(ow-iw)/2:(oh-ih)/2:%s[padded];[padded]fillborders=left=%d:right=%d:top=%d:bottom=%d:mode=fixed:color=%s[bordered];[bordered]split[a][b];[a]palettegen=max_colors=256[p];[b][p]paletteuse[out]`,
Expand Down Expand Up @@ -116,8 +121,10 @@ func MakeWebM(opts VideoOptions) *exec.Cmd {
return exec.Command(
"ffmpeg", "-y",
"-r", fmt.Sprint(opts.Framerate),
"-start_number", fmt.Sprint(opts.StartingFrame),
"-i", filepath.Join(opts.Input, textFrameFormat),
"-r", fmt.Sprint(opts.Framerate),
"-start_number", fmt.Sprint(opts.StartingFrame),
"-i", filepath.Join(opts.Input, cursorFrameFormat),
"-filter_complex",
fmt.Sprintf(`[0][1]overlay,scale=%d:%d:force_original_aspect_ratio=1,fps=%d,setpts=PTS/%f,pad=%d:%d:(ow-iw)/2:(oh-ih)/2:%s,fillborders=left=%d:right=%d:top=%d:bottom=%d:mode=fixed:color=%s`,
Expand Down Expand Up @@ -148,8 +155,10 @@ func MakeMP4(opts VideoOptions) *exec.Cmd {
return exec.Command(
"ffmpeg", "-y",
"-r", fmt.Sprint(opts.Framerate),
"-start_number", fmt.Sprint(opts.StartingFrame),
"-i", filepath.Join(opts.Input, textFrameFormat),
"-r", fmt.Sprint(opts.Framerate),
"-start_number", fmt.Sprint(opts.StartingFrame),
"-i", filepath.Join(opts.Input, cursorFrameFormat),
"-filter_complex",
fmt.Sprintf(`[0][1]overlay,scale=%d:%d:force_original_aspect_ratio=1,fps=%d,setpts=PTS/%f,pad=%d:%d:(ow-iw)/2:(oh-ih)/2:%s,fillborders=left=%d:right=%d:top=%d:bottom=%d:mode=fixed:color=%s`,
Expand Down

0 comments on commit 0d38b18

Please sign in to comment.