---
title: "Stop motion editing with stopmotion: the laser dinosaur"
vignette: >
  %\VignetteIndexEntry{Stop motion editing with stopmotion: the laser dinosaur}
  %\VignetteEngine{quarto::html}
  %\VignetteEncoding{UTF-8}
format:
  html:
    toc: true
knitr:
  opts_chunk:
    collapse: true
    warning: false
    comment: "#>"
    fig.align: center
    eval: false
---

## Credit

The dinosaur animation used throughout this vignette was created by
**\@looksrawr** and is released under the
[CC0 1.0 Universal Public Domain Dedication](https://creativecommons.org/publicdomain/zero/1.0/).

* * *

## Overview

**stopmotion** is a pipeline-friendly toolkit for assembling and editing stop
motion animations from sequences of still images.  Its functions fall into
three families:

| Family | Functions |
|---|---|
| Load | `read()` |
| Restructure | `duplicate()`, `splice()` |
| Transform | `rotate()`, `wiggle()`, `flip()`, `flop()`, `blur()`, `scale()`, `crop()`, `trim()`, `border()`, `background()`, `centre()` |
| Display | `montage()`, `preview()` |

All functions accept an optional `frames` argument so that any operation can
target a precise subset of frames — the feature that sets **stopmotion** apart
from plain **magick** pipelines.

After each operation **stopmotion** prints a message listing the updated frame
sequence.  These messages are shown in interactive sessions and suppressed
automatically during document rendering.  To control verbosity explicitly, use
`stopmotion_verbosity()` or set the option directly:

```{r options}
#| eval: true
#| include: false
library(magick)
library(stopmotion)

stopmotion_verbosity(FALSE) # silence for the rest of the session
options(stopmotion.verbose = FALSE) # equivalent
```
Or edit your `.Rprofile` to save the option with `usethis::edit_r_profile()`

This vignette walks through a complete editing session using the bundled
`extdata/` frames: a ten-frame cartoon dinosaur whose eyes shoot a laser ray.

* * *

## 1  Loading a GIF

The bundled `extdata/` directory contains the ten frames of the dinosaur
animation as individual PNG files — exactly the kind of image sequence that
**stopmotion** is designed to work with.  `read()` loads them in lexicographic
order and returns a `magick-image` object accepted by every **stopmotion**
function.

```{r load}
#| eval: true
dino_dir <- system.file("extdata", package = "stopmotion")
dino <- read(dir = dino_dir)
dino |> preview(fps = 2)
```

```{r frame-count}
#| eval: true
cat("Number of frames:", length(dino), "\n")
image_info(dino)[, c("width", "height", "filesize")]
```

Ten frames, 480 × 480 pixels.

* * *

## 2  Inspecting frames

`montage` is a quick way to display all frames side-by-side.

```{r montage}
#| eval: true
#| fig-width: 7
#| fig-height: 2.5
montage(dino, tile = "10x1", geometry = "64x64+2+2")
```

Scanning left to right you can read the story:

* **Frames 1–4** — the dinosaur stands still, then opens its eyes.
* **Frames 5–6** — eyes glow and start charging up.
* **Frames 7–9** — the laser fires.
* **Frame 10**   — the beam fades and the dinosaur looks pleased.

* * *

## 3  Hand-held shake with `wiggle()`

`wiggle()` inserts two slightly rotated copies after each selected frame —
one tilted `+degrees`, one `−degrees` — creating the organic wobble typical
of real stop motion.  We apply it to the quieter frames at the start so the
calm contrast with the explosive laser section.

```{r wiggle}
#| eval: true
dino_w <- wiggle(dino, degrees = 2, frames = 1:3)
cat("Total frames after wiggle():", length(dino_w), "\n")
```

* * *
## 4  Building anticipation with `duplicate()`

The charging phase (frames 5–6) flashes by too quickly.  `duplicate()` with
`style = "looped"` inserts a copy of those frames immediately after the
originals, making the build-up feel more deliberate.

```{r dup-frames}
#| eval: true
dino2 <- duplicate(dino, frames = 5:6, style = "looped")
cat("Frames after duplicate():", length(dino2), "\n")
```

The `looped` style repeats the selected range in order, so the sequence
becomes …5, 6, 5, 6… — a natural charging pulse.

* * *

## 5  Adding drama with `border()`

A vivid red border on the laser frames (now frames 7–11 after the insertion)
signals "danger" to the viewer.

```{r border}
#| eval: true
dino3 <- border(dino2, color = "red", geometry = "8x8", frames = 7:11)
```

* * *

## 6  Motion blur on the laser beam with `blur()`

The three peak laser frames (frames 8–10) benefit from a subtle blur that
conveys raw energy.

```{r blur}
#| eval: true
dino4 <- blur(dino3, radius = 3, sigma = 1.5, frames = 8:10)
```

* * *

## 7  Putting it together: the full pipeline

The edits above can be chained into a single pipe for clarity.

```{r pipeline}
#| eval: true
read(dir = system.file("extdata", package = "stopmotion")) |>
  wiggle(degrees = 2, frames = 1:3) |>             # hand-held shake
  duplicate(frames = 5:6, style = "looped") |>     # hold the charge
  border(color = "red", geometry = "8x8",
          frames = 7:11) |>                        # danger border
  blur(radius = 3, sigma = 1.5, frames = 8:10) |>  # energy blur
  preview(fps = 2)
```

* * *

## 8  Exporting the result

`magick::image_write_gif()` writes the edited sequence back to a GIF file.
The `delay` argument controls playback speed (seconds per frame).

```{r export}
out <- tempfile(fileext = ".gif")
image_write_gif(dino_final, path = out, delay = 1 / 8)
message("Saved to: ", out)
```

* * *

## 9  The celebratory somersault: `flip()`, `flop()`, and `rotate()`

Once the laser fades, the dinosaur celebrates with a somersault.  This example
chains three pure geometric transforms — each applied to a precise subset of
frames — to choreograph a full forward flip.

| Step | Function | Visual effect |
|---|---|---|
| Mirror left–right | `flop()` | Dino faces left for the run-up |
| Lean forward 90° | `rotate()` | Start of the forward flip |
| Upside-down apex | `flip()` | Top-to-bottom mirror at the peak |
| Lean back 270° | `rotate()` | Completing the circle |
| Loop it twice | `duplicate()` | Two full somersaults |

```{r somersault-flop}
#| eval: true
# Frames 1–2: mirror horizontally so the dino faces left (run-up)
dino_s <- flop(dino, frames = 1:2)
```

```{r somersault-rotate1}
#| eval: true
# Frame 3: rotate 90° — leaning forward into the jump
dino_s <- rotate(dino_s, degrees = 90, frames = 3L)
```

```{r somersault-flip}
#| eval: true
# Frame 4: flip vertically — upside-down at the apex of the somersault
dino_s <- flip(dino_s, frames = 4L)
```

```{r somersault-rotate2}
#| eval: true
# Frame 5: rotate 270° — coming back around to land upright
dino_s <- rotate(dino_s, degrees = 270, frames = 5L)
```

```{r somersault-loop}
#| eval: true
# Duplicate the spin frames so the dino does two full somersaults
dino_s <- duplicate(dino_s, frames = 1:5, style = "looped")
cat("Frames after duplication:", length(dino_s), "\n")
```

The five steps collapse into a single pipe:

```{r somersault-pipeline}
#| eval: true
dino_somersault <- dino |>
  flop(frames = 1:2)               |>   # run-up: face left
  rotate(degrees = 90,  frames = 3L) |> # lean into the jump
  flip(frames = 4L)                |>   # upside-down apex
  rotate(degrees = 270, frames = 5L) |> # complete the circle
  duplicate(frames = 1:5, style = "looped") # loop it twice

montage(dino_somersault[1:10], tile = "10x1", geometry = "64x64+2+2")
```

```{r somersault-preview}
#| eval: true
dino_somersault |> preview(fps = 2)
```

* * *

## 10  Other useful operations

### 10.1  Dropping unwanted frames with `splice()`

`splice()` inserts new frames after a given position.  Combined with standard
**magick** subsetting you can also remove frames:

```{r splice}
# Insert a custom "RAWR!" title card after frame 4
title_card <- image_blank(480, 480, color = "black") |>
  image_annotate("RAWR!", size = 80, color = "red", gravity = "Center")

dino_with_title <- splice(dino, insert = title_card, after = 4L)
```

### 10.2  Scaling down for the web

`scale()` accepts any **magick** geometry string.

```{r scale}
dino_small <- scale(dino, geometry = "50%")
```

### 10.3  Cropping to the face

```{r crop}
# Keep a 200×200 window centred on the head (adjust offsets to taste)
dino_face <- crop(dino, geometry = "200x200+140+60")
```

### 10.4  Aligning frames with `centre()`

When frames drift slightly between photos — a common artefact of hand-held
stop motion — `centre()` performs a full affine warp (translation, rotation,
and scaling simultaneously) so the subject stays locked in place across the
whole animation.  It needs exactly **two landmarks per frame**: consistent
anatomical anchors such as the left and right eye.  The `reference` frame
defines the target position; every other frame is warped to match it.

#### Collecting landmarks interactively

The most practical way to record landmarks is `locator()` from base R.
Click the two anchors on each frame in turn (always in the same order),
and build up the data frame row by row.  `locator()` returns `y` measured
from the **bottom** edge of the plot, which is the convention `centre()`
expects:

```{r centre-locator}
# Run once per editing session — requires an interactive graphics device.
# Display each frame, click the two landmarks, store the coordinates.
pts_list <- lapply(seq_along(dino), function(i) {
  plot(as.raster(dino[i])) # display frame i
  message("Frame ", i, ": click LEFT eye then RIGHT eye")
  p <- locator(2L) # two clicks; y is from the bottom edge
  data.frame(frame = i, x = p$x, y = p$y)
})
pts <- do.call(rbind, pts_list)
```

#### A worked example with artificial drift

The bundled dino is a clean digital sprite with no accidental drift, so the
example below **introduces** known drift first via a pure-translation affine
warp, then corrects it — making the fix unambiguous.

Frame 2 is shifted +5 px right / +3 px down; frame 3 is shifted −4 px left /
+2 px down (all in ImageMagick's top-edge coordinate system used by
`image_distort`).  The landmark table is in the bottom-edge convention that
`centre()` expects.

```{r centre}
#| eval: true
# Introduce known translational drift.  Two widely-spaced control-point pairs
# both encoding the same displacement define a pure translation.
# Coordinates are in ImageMagick top-edge convention for image_distort.
dino_d <- c(
  dino[1],
  magick::image_distort(dino[2], "Affine",        # +5 right, +3 down
    c(100, 100, 105, 103,  380, 380, 385, 383)),
  magick::image_distort(dino[3], "Affine",        # −4 left, +2 down
    c(100, 100,  96, 102,  380, 380, 376, 382)),
  dino[4:10]
)

# Eye positions in the drifted sequence — y from the bottom edge (locator convention).
# Frame 1 reference (unchanged):       left (212, 271), right (272, 270).
# Frame 2 shifted (+5 right, +3 down): left (217, 268), right (277, 267).
# Frame 3 shifted (−4 left,  +2 down): left (208, 269), right (268, 268).
pts <- data.frame(
  frame = c(1L, 1L, 2L, 2L, 3L, 3L),
  x     = c(212, 272, 217, 277, 208, 268),
  y     = c(271, 270, 268, 267, 269, 268)
)

# Correct only the drifted frames; leave 4–10 untouched.
dino_stabilised <- centre(dino_d, points = pts, reference = 1L, frames = 2:3)
```

Compare the original drifted sequence with the stabilised one:

```{r centre-compare}
#| eval: true
montage(dino_d[1:3],          tile = "3x1", geometry = "128x128+2+2")
montage(dino_stabilised[1:3], tile = "3x1", geometry = "128x128+2+2")
```

* * *

## Summary

| Step | Function | Key argument |
|---|---|---|
| Hold charging frames | `duplicate()` | `style = "looped"` |
| Red danger border | `border()` | `color`, `geometry` |
| Energy motion blur | `blur()` | `radius`, `sigma` |
| Hand-held shake | `wiggle()` | `degrees` |
| Insert title card | `splice()` | `insert`, `after` |
| Resize for web | `scale()` | `geometry` |
| Crop to face | `crop()` | `geometry` |
| Stabilise frames | `centre()` | `points`, `reference` |
| Mirror left–right | `flop()` | `frames` |
| Mirror top–bottom | `flip()` | `frames` |
| Rotate by angle | `rotate()` | `degrees` |

All of the above accept a `frames` argument to restrict the operation to any
subset of frames, giving you frame-precise control over your animation.
