--- title: "Playing and Exporting Coordinate Animations" output: rmarkdown::html_vignette: vignette: > %\VignetteIndexEntry{Playing and Exporting Coordinate Animations} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include=FALSE} knitr::opts_chunk$set(collapse = TRUE, comment = "#>", out.width = "100%") library(ivue) have.rgl <- nzchar(system.file(package = "rgl")) have.magick <- requireNamespace("magick", quietly = TRUE) have.grip <- requireNamespace("grip", quietly = TRUE) ``` For a task index, see [Finding your way around ivue](function-guide.html); for small reproducible inputs, see [Example data and recipes](example-data.html). `animate.frames()` turns recorded coordinates into a browser player. `write.animation.gif()` exports the same retained frames to a GIF for a README, presentation, or website. Playback requires `rgl`; GIF export additionally requires `magick`: ```{r installation, eval=FALSE} install.packages(c("rgl", "magick")) ``` **On this page:** [Watch a Sierpinski triangle unfold](#watch-a-sierpinski-triangle-unfold) · [Supply your own frames](#supply-your-own-frames) · [Animate a changing 3D surface](#animate-a-changing-3d-surface) · [Export interactive HTML](#export-interactive-html) · [Export a GIF](#export-a-gif) · [Control size and preserve diagnostic meaning](#control-size-and-preserve-diagnostic-meaning) ## Watch a Sierpinski triangle unfold Generate a level-4 Sierpinski triangle graph, then record its two-dimensional layout with `grip::trace.grip()`. The optional `grip` package supplies both the graph constructor and the layout algorithm; install it with `install.packages("grip")` to run this example. ```{r triangle-trace, eval=have.grip} edges <- grip::edges.sierpinski.triangle(level = 4) tr <- grip::trace.grip( edges, n = max(edges), dim = 2, preset = "carpet", seed = 1, trace = "round", trace.every = 1 ) length(tr$frames) dim(tr$frames[[1]]) utils::packageVersion("grip") ``` The trace contains coordinates after successive layout rounds, including rows for vertices that have not yet been introduced. Select up to 24 evenly spaced frames, including the first and last, to keep the player compact. Retain their metadata and original frame numbers for the labels: ```{r triangle-frame-selection, eval=have.grip} frame.index <- unique(as.integer(round(seq( 1, length(tr$frames), length.out = min(24L, length(tr$frames)) )))) triangle.frames <- tr$frames[frame.index] triangle.meta <- tr$meta[frame.index, , drop = FALSE] head(triangle.meta[, c("phase", "active_vertices")]) triangle.labels <- paste0("Frame ", frame.index, " | ", triangle.meta$phase, " | ", triangle.meta$active_vertices, " vertices") ``` No coordinates are aligned to a target shape, rotated, or normalized. The seed fixes the random input to the installed GRIP implementation; the resulting trace can change between package versions, so its version is printed above. ```{r triangle-player, eval=have.grip && have.rgl} triangle.player <- animate.frames( triangle.frames, edges = edges, labels = triangle.labels, fps = 5, col = "#C24E25", point.size = 4, edge.col = "#314E6ECC", edge.width = 1, background.color = "#FAF7F0", height = 450 ) triangle.player ``` ```{r triangle-unavailable, echo=FALSE, results='asis'} if (!have.grip) cat("The GRIP example code is shown but not evaluated in this build because `grip` is unavailable. The small triangle and saddle examples below do not require it.\n\n") ``` Press **Play** to start; the button becomes **Pause**. Drag the slider to inspect a particular frame. With the slider focused, use the arrow keys to move one frame at a time. **Slower** and **Faster** change playback speed, **Reverse** changes direction, and **Reset** returns to the beginning. Drag inside the scene to rotate it, including during playback. The default 2D camera looks straight down onto the xy plane. The recorded frames all have the same number of rows. A vertex that has not yet been introduced has an entirely missing (`NA` or `NaN`) row. Its point and all incident edges stay hidden until its coordinates are finite. Rows remain associated with the same vertices throughout playback; they must not be reordered. ## Supply your own frames Frames are a list of numeric matrices with two or three columns. Every matrix must have identical dimensions. If row names are supplied, they must be unique and identical across frames. A row must be completely finite or completely missing (`NA` or `NaN`); partial missing coordinates and infinity are errors. GRIP records its inactive vertices using `NaN` rows. Here a triangle gains its third vertex and then expands. The second edge appears only when its missing endpoint is introduced. ```{r small-example, eval=have.rgl} X <- rbind(a = c(0, 0), b = c(1, 0), c = c(0.5, 0.9)) first <- X first[3, ] <- NA small <- animate.frames( list(first, X, X * 1.4), edges = rbind(c(1, 2), c(2, 3), c(3, 1)), labels = c("Two vertices", "Triangle", "Expanded triangle"), fps = 1, loop = FALSE, point.size = 9, height = 280 ) small ``` Vertices may disappear as well as appear. An entirely empty frame is allowed, provided at least one frame contains a finite point. The stricter finite-coordinate requirement of ordinary `plot3D` functions is unchanged. ## Animate a changing 3D surface ![Three static views show a plane, halfway deformation, and a saddle, using the same framing. Blue and red colors stay fixed and describe final saddle height, even in the flat frame.](figures/saddle-animation.png) The poster shows three of the recorded states below with a common spatial scale. All players start paused, including for readers who prefer reduced motion. Use the labeled frame slider with the arrow keys to inspect one frame at a time without starting playback. Any sequence satisfying the frame contract can be played. This example explicitly constructs a sequence from a plane to a saddle; it is an illustrative deformation, not the output of a layout optimizer. The xy positions and vertex identities are fixed while the height changes. A 7-by-7 grid and 17 recorded amplitudes keep this installed example compact; the later paraboloid-to-saddle example uses 33 stages. ```{r saddle-frames} side <- 7L grid <- expand.grid(x = seq(-1, 1, length.out = side), y = seq(-1, 1, length.out = side)) amplitudes <- seq(0, 1.2, length.out = 17) saddle.frames <- lapply(amplitudes, function(a) { cbind(x = grid$x, y = grid$y, z = a * (grid$x^2 - grid$y^2)) }) ids <- matrix(seq_len(nrow(grid)), side, side) saddle.edges <- rbind( cbind(as.vector(ids[-side, ]), as.vector(ids[-1, ])), cbind(as.vector(ids[, -side]), as.vector(ids[, -1])) ) final.heights <- saddle.frames[[length(saddle.frames)]][, "z"] height.scale <- color.scale.cont(final.heights, center = 0, palette = c("#2455A4", "#ECE6C2", "#B83232")) height.mapping <- map.colors(final.heights, height.scale) point.colors <- height.mapping$colors ``` ```{r saddle-player, eval=have.rgl} saddle.player <- animate.frames( saddle.frames, edges = saddle.edges, labels = sprintf("Saddle amplitude = %.2f", amplitudes), mapping = height.mapping, legend.title = "Final saddle height", caption = "Color: final saddle height; positions: current frame.", description = "Plane to saddle, with fixed colors for final saddle height.", point.size = 6, edge.col = "#314E6E99", camera = camera.zup(elevation = 25, turn = -130), fps = 8, height = 450 ) saddle.player ``` Colors describe each vertex's **final saddle height** and remain fixed across frames. This makes identity easy to follow; they do not encode instantaneous height. Frame-dependent colors and animated mesh faces are outside this initial API. `edges` draws a wire grid without filling its faces. ### From a paraboloid through a flat grid to a saddle Reuse the same grid, edges, and colors, but change the height formula to pass through three shapes. Let `t` run from -1 to 1 and set \[ z(t) = 1.2\bigl(|t|x^2 - t y^2\bigr). \] At `t = -1` this is the upward-opening paraboloid `z = 1.2 * (x^2 + y^2)`. At `t = 0` all heights are zero, and at `t = 1` the surface is the saddle `z = 1.2 * (x^2 - y^2)`. An odd number of equally spaced frames places the flat grid exactly at the slider's midpoint. ```{r surface-morph-frames} stages <- seq(-1, 1, length.out = 33) surface.frames <- lapply(stages, function(t) { cbind(x = grid$x, y = grid$y, z = 1.2 * (abs(t) * grid$x^2 - t * grid$y^2)) }) surface.labels <- sprintf("%s | t = %.2f", ifelse(stages < 0, "Paraboloid", ifelse(stages == 0, "Flat grid", "Saddle")), stages) ``` ```{r surface-morph-player, eval=have.rgl} surface.player <- animate.frames( surface.frames, edges = saddle.edges, labels = surface.labels, mapping = height.mapping, legend.title = "Final saddle height", caption = "Color: final saddle height; positions: current frame.", description = "Paraboloid through a plane to a saddle, with fixed colors for final saddle height.", point.size = 6, edge.col = "#314E6E99", camera = camera.zup(elevation = 25, turn = -130), fps = 8, height = 450 ) surface.player ``` Move the slider to the center to inspect the flat grid (frame 17 of 33). Use **Pause** to hold a view and **Reverse** to change the playback direction. Colors still represent final saddle height, not the height in the current frame. ## Export interactive HTML The returned object is an ordinary htmlwidget. Saving it preserves the player, caption, readable color legend, and camera controls and does not require Shiny or a running R session for playback. No file is saved merely by constructing a player. ```{r html-export, eval=FALSE} htmlwidgets::saveWidget(triangle.player, "triangle-playback.html", selfcontained = TRUE) htmlwidgets::saveWidget(saddle.player, "saddle-playback.html", selfcontained = TRUE) ``` Self-contained HTML requires Pandoc and bundles widget assets into one file. Use `selfcontained = FALSE` to save the HTML alongside a dependency folder; keep that folder with the HTML when sharing it. ## Export a GIF Use `annotations = TRUE` to carry the fixed color legend and plain-text caption into the GIF. The raster renderer reserves space beside and below the scene; it does not capture arbitrary HTML. Text wraps at a readable size, and an export that cannot fit its annotations asks for larger dimensions. GIF export uses the frames retained in the player, including inactive vertices and edges. This build checks the small three-frame triangle; the larger trace can be exported with the same function in your own session. The example writes only temporary output and removes it after checking the file. No GIF asset is embedded in the installed guide. ```{r gif-export, eval=have.rgl && have.magick} local({ gif.path <- tempfile(fileext = ".gif") on.exit(unlink(gif.path)) write.animation.gif(small, gif.path, fps = 1, width = 240, height = 240, final.hold = 0) stopifnot(file.exists(gif.path)) }) ``` ```{r keep-gif, eval=FALSE} write.animation.gif(saddle.player, "saddle.gif", fps = 8, width = 720, height = 560, final.hold = 2, annotations = TRUE, loop = TRUE, overwrite = FALSE) ``` The GIF has the widget's **initial camera orientation**, not a rotation made later in the browser. Set `camera` explicitly when constructing the player to choose the exported view. Export requires an orthographic camera (`fov = 0`), which is the animation default. It renders a diagram from the recorded coordinates using R graphics and `magick`; it is not a screenshot of the WebGL scene. Smaller `camera$zoom` values enlarge the scene in both outputs. Use matching dimensions and annotation layouts when comparing GIF spatial scale; legend and caption space reduces the scene area. Point appearance can differ slightly. Edges are painted before points, so complex 3D intersections do not have WebGL depth-buffer semantics. The last frame has `final.hold` additional seconds per loop. GIF frame delays are rounded to centiseconds. Existing files are protected unless you set `overwrite = TRUE`. ## Control size and preserve diagnostic meaning Playback changes only which recorded frame is displayed. It does not interpolate missing frames, align to a target, or recenter each frame. All original frames determine one fixed viewing box, so translation, contraction, and expansion remain visible. Rotating or zooming the camera changes the view, not the stored coordinates. By default, at most 100 evenly spaced frames are retained, including the first and last. If this limit is exceeded, a message reports the subsampling. For a diagnostic inspection, choose frames explicitly or retain all of them: ```{r frame-selection, eval=have.grip && have.rgl} inspect.index <- unique(as.integer(round(seq( 1, length(triangle.frames), length.out = min(5L, length(triangle.frames)) )))) selected <- animate.frames(triangle.frames, edges, frame.index = inspect.index, labels = triangle.labels, fps = 2) attr(selected, "ivue.animation")$frame.index ``` ```{r all-frames, eval=FALSE} all.frames <- animate.frames(my.frames, edges = my.edges, max.frames = NULL) ``` Every retained frame has equal duration. A subsampled animation therefore shows the order of the solve, not elapsed computation time. Widget size grows with both frame count and graph size; large traces benefit from an explicit selection. The exported GIF uses that same selection. The same interface accepts three-dimensional traces and frames recorded by other solvers. Neither `animate.frames()` nor GIF export calls `grip` or computes a layout.