Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Video support #139

Draft
wants to merge 8 commits into
base: dev
Choose a base branch
from
Draft
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,6 @@ tests/*/_extensions

# .css.map (can be generated manually from .scss file)
_extensions/closeread/closeread.css.map

# images generated for video demo
docs/gallery/demos/videos/image*.png
19 changes: 19 additions & 0 deletions _extensions/closeread/closeread.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

54 changes: 53 additions & 1 deletion _extensions/closeread/closeread.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ document.addEventListener("DOMContentLoaded", () => {
ojsProgressBlock?.define("crProgressBlock", 0);

if (ojsModule === undefined) {
console.error("Warning: Quarto OJS module not found")
console.warn("Warning: Quarto OJS module not found")
}

// expand hlz option into highlight and zoom-to
Expand All @@ -70,6 +70,13 @@ document.addEventListener("DOMContentLoaded", () => {
trigger.setAttribute('data-zoom-to', hlzValue);
trigger.setAttribute('data-highlight', hlzValue);
});

// initialise scrolly videos
let scrollyVideo = new ScrollyVideo({
scrollyVideoContainer: "cr-rayshader",
src: "rayshader.mp4",
trackScroll: false
})

// collect all sticky elements
const allStickies = Array.from(document.querySelectorAll(".sticky"));
Expand Down Expand Up @@ -103,6 +110,9 @@ document.addEventListener("DOMContentLoaded", () => {
function crTriggerStepProgress(trigger) {
ojsTriggerProgress?.define("crTriggerProgress", trigger.progress)
ojsDirection?.define("crDirection", trigger.direction)
scrollyVideo.setVideoPercentage(trigger.progress, {
transitionSpeed: 12, easing: t => +t // linear easing
})
}

function crProgressStepEnter(progressBlock) {
Expand Down Expand Up @@ -195,6 +205,7 @@ function updateStickies(allStickies, focusedStickyName, trigger) {
// apply additional effects
transformSticky(focusedSticky, trigger.element);
highlightSpans(focusedSticky, trigger.element);
controlVideo(focusedSticky, trigger.element);

if ( // scale-to-fill only takes effect if there are no other transforms
focusedSticky.classList.contains("scale-to-fill") &&
Expand Down Expand Up @@ -410,6 +421,47 @@ function scaleToFill(el, paddingX = 75, paddingY = 50) {
`matrix(${scale}, 0, 0, ${scale}, 0, ${centerDeltaY})`)
}

//==============//
// Videos //
//==============//
// Execute different methods on video elements such as play() and pause().
function controlVideo(focusedSticky, triggerEl) {

console.log("Controlling video ", focusedSticky, "from trigger", triggerEl)

// get any video methods
const videoAttributes = Array.from(triggerEl.attributes).filter(attr =>
/^data-.*-video$/.test(attr.name));

// exit function if there's no video method
if (videoAttributes.length == 0) {
return;
}

if (videoAttributes.length > 1) {
console.warn(`Multiple video method are called by a single trigger. Applying only the first one, ${videoAttributes[0].name}`)
}

// get video element
const videoEl = focusedSticky.querySelector("video");

// execute method on video
if (videoAttributes[0].value !== "false") {
const attributeName = videoAttributes[0].name;

// extract method from attribute name
const methodName = attributeName.replace(/^data-/, "").replace(/-video$/, "");

// check if the method exists on videoEl, then call it
if (typeof videoEl[methodName] === "function") {
videoEl[methodName]();
} else {
console.log(`Method ${methodName} does not exist for a video element.`);
}
}
}


/* getBooleanConfig: checks for a <meta> with named attribute `cr-[metaFlag]`
and returns true if its value is "true" or false otherwise */
function getBooleanConfig(metaFlag) {
Expand Down
6 changes: 6 additions & 0 deletions _extensions/closeread/closeread.lua
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,12 @@ quarto.doc.add_html_dependency({
scripts = {"closeread.js"}
})

quarto.doc.add_html_dependency({
name = "scrollyvideo",
version = "0.0.23",
scripts = {"scrollyvideo.js"}
})


--=============--
-- Run Filters --
Expand Down
20 changes: 20 additions & 0 deletions _extensions/closeread/closeread.scss
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,26 @@
}
}
}

.scale-to-cover,
.sticky:has(.scale-to-cover) {
width: 100%;
height: 100%;

> p {
margin: 0;
}

* {
width: 100%;
height: 100%;
}

:is(video, img, iframe),
&:is(video, img, iframe) {
object-fit: cover;
}
}
}
}
}
Expand Down
7 changes: 7 additions & 0 deletions _extensions/closeread/scrollyvideo.js

Large diffs are not rendered by default.

140 changes: 140 additions & 0 deletions docs/gallery/demos/videos/index.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
---
title: "Videos"
# image: "globe.png"
subtitle: "Use videos that loop or progress on user scroll."
format:
closeread-html:
resources: rayshader.mp4
cr-style:
narrative-border-radius: 0
narrative-background-color-overlay: "#d8e4f2aa"
narrative-text-color-overlay: black
narrative-background-color-sidebar: "#914e4e"
narrative-text-color-sidebar: white
section-background-color: "#213243"
---

Something something video

Now here's an example of a video filling to fit the space:

::::{.cr-section layout="overlay-left"}

You can tell all sorts of stories with videos.

Maybe you've gone on a journey.
[Credit: [Cristian-Manieri/Pixabay](https://pixabay.com/videos/drone-landscape-green-greenland-236893/)]{style="font-size: 65%;"} @cr-ship

:::{#cr-ship .scale-to-cover}
![](ship.mp4){alt="A ship sails in a polar region" loop="true"}
:::

You can trigger a video to play using `[@cr-ship]{play-video="true"}`. [@cr-ship]{play-video="true"}

You can also pause it using `[@cr-ship]{pause-video="true"}`. [@cr-ship]{pause-video="true"}

Apply the `.scale-to-cover` class to videos to ensure that they fill up the whole Closeread sticky column (even if they need to be cropped to do it).

This is great for photojournalistic use cases like interviews where not every part of the frame needs to be visible.

::::

\
\

The works for sidebar layouts too:

\
\

::::{.cr-section layout="sidebar-right"}

The video may even be there simply to establish a mood for your story.
[Credit: [Domka_1611/Pixabay](https://pixabay.com/videos/fire-evening-nature-forest-autumn-148594/)]{style="font-size: 65%;"} @cr-tea

![](tea.mp4){#cr-tea .scale-to-cover alt="One person pours tea into another person's tea cup in front of an open fire." autoplay="true" loop="true"}

:::{focus-on="cr-tea"}
You may want to add `autoplay="true"` and `loop="true"` to your videos.
\
\
In this case, we've shifted the video and put a crossfade in the middle of it for a seamless loop.
:::

::::

\
\

# Videos that progress on scroll

Let's use the popular `{rayshader}` package to generate a video like that. First, we'll build the scene up:

```{r}
#| label: build-scene
library(tibble)
library(dplyr)
library(stringr)
library(purrr)
library(rayshader)

mont_shadow <- ray_shade(montereybay, zscale = 50, lambert = FALSE)
mont_amb <- ambient_shade(montereybay, zscale = 50)
montereybay |>
sphere_shade(zscale = 10, texture = "imhof1") |>
add_shadow(mont_shadow, 0.5) |>
add_shadow(mont_amb, 0) ->
mont_scene
```

Now we'll render the movie as `rayshader.mp4`:

```{r}
#| label: render-images
# tibble(angle = seq(0.01, 1, by = 0.01) * 360 + 45) |>
# mutate(
# n = 1:n(),
# fname = paste0("image", str_pad(n, 3, pad = "0")),
# render = map2(angle, fname, function(angle, fname) {
# plot_3d(
# mont_scene, montereybay, zscale = 50, fov = 0, theta = angle, phi = 45,
# windowsize = c(1000, 800), zoom = 0.75, water = TRUE, waterdepth = 0,
# wateralpha = 0.5, watercolor = "lightblue", waterlinecolor = "white",
# waterlinealpha = 0.5)
# Sys.sleep(0.5)
# render_snapshot(fname)
# }))
plot_3d(
mont_scene, montereybay, zscale = 50, fov = 0, phi = 45,
windowsize = c(1000, 800), zoom = 0.75, water = TRUE, waterdepth = 0,
wateralpha = 0.5, watercolor = "lightblue", waterlinecolor = "white",
waterlinealpha = 0.5)

render_movie("rayshader.mp4")
```

Let's incorporate `rayshader.mp4` into a scrolly section:

::::{.cr-section}

Here's our rayshader video! [@cr-rayshader]{.scroll-video}

We can keep talking about it for a while... @cr-rayshader

... and a while longer! @cr-rayshader

:::{#cr-rayshader}
:::

::::


:::{.counter style="position: fixed; top: 10px; right: 10px; background-color: skyblue; border-radius: 5px; padding: 18px 18px 0 18px; line-height: .8em; z-index: 1000"}
```{ojs}
md`Active sticky: ${crActiveSticky}`
md`Active trigger: ${crTriggerIndex}`
md`Trigger progress: ${(crTriggerProgress * 100).toFixed(1)}%`
md`Scroll direction: ${crDirection}`
md`Progress Block progress: ${(crProgressBlock * 100).toFixed(1)}%`
```
:::
Binary file added docs/gallery/demos/videos/rayshader.mp4
Binary file not shown.
Binary file added docs/gallery/demos/videos/ship.mp4
Binary file not shown.
Binary file added docs/gallery/demos/videos/tea.mp4
Binary file not shown.
Loading