-
Notifications
You must be signed in to change notification settings - Fork 0
Media
Put all media in the media/ directory. Subdirectories are fine and recommended:
media/
├── chapter-01/
│ ├── photo1.jpg
│ └── demo.mp4
└── chapter-02/
└── screenshot.png
Reference files by relative path from the root: 'media/chapter-01/photo1.jpg'.
| Format | Notes |
|---|---|
.jpg, .jpeg
|
Recommended for photos |
.png |
Recommended for screenshots with transparency |
.webp |
Great compression, supported in all modern browsers |
.gif |
Animates in both direct <img> use and in the media cycler |
.mp4 |
Preferred video format; plays everywhere |
.webm |
Good alternative; smaller files |
.mov |
Works but larger; convert to mp4 before committing |
.svg |
Scalable, no quality loss at any size |
GitHub has a 100MB file limit. Keep images under 2560px wide:
# Resize a single file in place
sips --resampleWidth 2560 media/photo.jpg
# Resize all JPGs in a folder
for f in media/project/*.jpg; do sips --resampleWidth 2560 "$f"; done# Transcode large video (scale to 1280px wide, quality CRF 28)
ffmpeg -i input.mp4 -vf "scale=1280:-2" -crf 28 -preset fast output.mp4
# Quick check: will this fit GitHub's 100MB limit?
du -sh output.mp4For YouTube clips and trimming, use the fleet tool — see Import & Export Tools.
The simplest option. Specify a relative path and the case study renders a standard <img> tag:
{ title: 'My Project', img: 'media/project/hero.jpg', ... }- No reveal animation, no cycling — just the image.
- If you want the pixelated reveal effect on a single image, use
MEDIA_CYCLERwith a one-item array instead.
The media cycler is a canvas-based gallery with a signature pixelated de-pixelation reveal effect. Use it when a case study has multiple images or a mix of images and video.
- Set
img: 'MEDIA_CYCLER'on the case study in SECTIONS. - After the SECTIONS build loop (search for the end of the
SECTIONS.forEachblock), add an IIFE:
(function(){
const slide = allSlides.find(s =>
s.querySelector?.('.case-title')?.textContent.includes('My Project'));
if (!slide) return;
buildMediaCycler(slide, [
{type: 'image', src: 'media/project/photo1.jpg'},
{type: 'image', src: 'media/project/photo2.jpg', flipH: true},
{type: 'video', src: 'media/project/demo.mp4', loop: true},
], {imageDuration: 6000});
})();buildMediaCycler(slideElement, items, options)| Option | Default | Description |
|---|---|---|
imageDuration |
6000 |
Milliseconds to hold each image before advancing |
portrait |
false |
Use 9:16 canvas for vertical/portrait content |
enterDur |
700 |
Slide-in animation duration (ms) |
revealDur |
1600 |
Pixelated de-pixelation reveal duration (ms) |
exitDur |
500 |
Exit animation duration (ms) |
| Flag | Description |
|---|---|
flipH: true |
Mirror the image horizontally |
loop: true |
Loop a video continuously |
Use any unique text from the case study title:
// By case title
allSlides.find(s => s.querySelector?.('.case-title')?.textContent.includes('Your Title'))
// By slide index (0 = settings, 1 = cover, etc.)
allSlides[7]| Key | Action |
|---|---|
Shift + → |
Next item |
Shift + ← |
Previous item |
Shift + ↑ |
Resume auto-advance (if paused) |
Embed any web content directly in a case study slide:
{ title: 'Live Demo', img: 'IFRAME:https://www.youtube.com/embed/VIDEO_ID', ... }The IFRAME: prefix is stripped and the URL is placed in a responsive <iframe> with:
allow="autoplay; fullscreen; xr-spatial-tracking"| Content |
img value |
|---|---|
| YouTube | 'IFRAME:https://www.youtube.com/embed/VIDEO_ID' |
| Vimeo | 'IFRAME:https://player.vimeo.com/video/VIDEO_ID' |
| Sketchfab | 'IFRAME:https://sketchfab.com/models/MODEL_ID/embed' |
| Unreal Pixel Stream | 'IFRAME:https://your-server.com/stream' |
| Any URL | 'IFRAME:https://example.com/page' |
Note: YouTube autoplay won't work unless the user has interacted with the page first (browser policy). Use
?autoplay=1&mute=1in the YouTube embed URL to autoplay muted.
GIFs animate correctly in both direct and cycler contexts — buildMediaCycler auto-detects .gif items and uses a cross-fading <img> rendering path (not canvas) so the animation plays.
That said, convert large GIFs to MP4 before committing. GIFs compress poorly compared to H.264 — a 20MB GIF is often under 2MB as MP4, loads faster, and won't push you toward Git LFS. Rule of thumb: anything over ~5MB should be converted.
ffmpeg -i animation.gif -vf "scale=trunc(iw/2)*2:-2" -c:v libx264 -crf 22 \
-pix_fmt yuv420p -movflags +faststart -an animation.mp4Then use {type: 'video', src: 'media/animation.mp4', loop: true} in the cycler. For small GIFs (icons, short loops under 5MB) just use them as-is:
buildMediaCycler(slide, [
{type: 'image', src: 'media/animation.gif'},
], {imageDuration: 4000});For fine-grained positioning, use the placed layout instead of a media cycler. See Content Authoring § Layout Types.
If you need the presentation to run completely offline (USB stick, no Wi-Fi):
- Put all images and videos in
media/. - Use the 📦 Export for Offline button in the Settings slide — it patches the Three.js import map to point to a local
lib/file and downloads the bundle. - Or manually:
curl -o lib/three.module.min.js "https://cdn.jsdelivr.net/npm/three@0.164.1/build/three.module.min.js"then update the import map inindex.html.
Google Fonts are the other external dependency. If you need full offline font support, self-host the font file and replace the @import url(...) in the CSS.
Three.js is already loaded via the import map. The bonus slide includes a working 3D monocle example — a procedural gold torus with teal glass that auto-rotates and supports click-drag interaction.
To load a GLTF/GLB model:
const THREE = await import('three');
const { GLTFLoader } = await import('three/addons/loaders/GLTFLoader.js');
const loader = new GLTFLoader();
loader.load('media/model.glb', (gltf) => {
scene.add(gltf.scene);
// ... animate loop
});See the bonus slide IIFE in index.html for a full lifecycle-managed example (init on slide enter, dispose on leave).
Getting started
Presenting
Editing & design
Workflow
Reference