Refrakt v1.0.0

Media Guide · Level 2

Media Guide

The full story on media. For most sites the short version is enough — a normal <img> per slide, around 1600 px wide — and that is covered in Start here, step 4 Level 1. The one-line recipes for video and YouTube slides are in Customizing § 9 Level 2.

Read on when you want the formats, the memory budget and the loading pipeline in detail.

Stuck on any of it? Email jawad.ahbab.turna@gmail.com — that reaches me, the developer who wrote the slider, not a ticket queue. Usually answered within a working day. What support covers.

Everything Refrakt draws — images, video frames, provider posters — becomes a WebGL texture sampled through the optics shader. This page covers how to prepare each media kind, the exact markup for each, and how the engine manages the GPU memory behind it all.

Image sizing

Slides are cover-fit: the texture is scaled to fill the container and cropped on the overflowing axis, exactly like CSS object-fit: cover. Author images accordingly:

  • 1600 px on the longest edge is the sweet spot for full-bleed heroes. It stays sharp at 2× device pixel ratio on typical laptop widths while keeping decode times and GPU memory low.
  • Textures are capped at 2048 px internally regardless of the source file or DPR — pixels beyond that are paid for in bytes and decode time but never displayed. Don’t ship 4000 px files.
  • Keep one aspect ratio per slider. Cover-fit hides mismatches by cropping, but consistent framing keeps captions and focal points predictable.
  • JPEG at quality 75–82 is ideal. The optics pass adds grain and light on top, which visually masks compression far better than a static viewer would.

The full-resolution file goes in data-rf-src; the inline src is the tiny placeholder (next two sections):

<figure class="rf-slide">
  <img src="img/peak-lqip.jpg" data-rf-src="img/peak.jpg" alt="North face at dawn">
  <figcaption class="rf-text"><h2>North Face</h2></figcaption>
</figure>

If an <img> has only an inline src and no data-rf-src, that file is treated as both placeholder and final image — fine for thumbnails-only sliders, but full-bleed heroes should always split the two.

Responsive sources — data-rf-srcset

To serve different resolutions per device, use data-rf-srcset with the same syntax as native srcsetwidth descriptors only (800w, 1600w…). Density descriptors (2x) are not supported and are ignored.

<img src="img/peak-lqip.jpg"
     data-rf-srcset="img/peak-800.jpg 800w,
                     img/peak-1600.jpg 1600w,
                     img/peak-2048.jpg 2048w"
     alt="North face at dawn">

Selection happens once, when the slide enters the preload window:

  1. Target width = container CSS width × min(devicePixelRatio, quality.maxDPR) (default DPR cap: 2).
  2. The smallest candidate ≥ target wins; if none is large enough, the largest available is used.
  3. If data-rf-srcset is absent, data-rf-src is used; if that is also absent, the inline src is final.

Container resizes after load do not re-fetch a larger candidate — selection is deliberately one-shot to avoid download storms during window drag-resizes.

The LQIP frost-up pattern

Loading is part of the aesthetic, not something to hide. The inline <img src> is the low-quality placeholder by contract, and it arrives on screen behind procedural frost:

  1. The inline LQIP uploads immediately and renders frosted — the frost surface and its blur are forced on while loading, whatever glass kind the slider is configured with. A frosted LQIP on screen counts as “ready”; the slider never blocks on full-resolution files.
  2. The full-resolution file decodes off the critical path and swaps in between frames — never mid-frame, so there is no visible pop.
  3. On swap, the frost veil melts away over 600 ms (cubic-bezier(0.33, 0, 0.2, 1)), revealing the sharp image — condensation clearing from glass. Under reduced motion this becomes a 200 ms linear fade.

Markup — LQIP inline, full res deferred:

<figure class="rf-slide">
  <!-- src: the LQIP (≤ 64 px long edge, same aspect) · data-rf-src: full res -->
  <img src="img/peak-lqip.jpg" data-rf-src="img/peak.jpg" alt="North face at dawn">
  <figcaption class="rf-text"><h2>North Face</h2></figcaption>
</figure>

LQIP guidance: 64 px or less on the long edge, same aspect ratio as the full file, quality ~50. At that size each placeholder is 1–2 KB and the initial paint is effectively free.

To disable the choreography set "media": { "frostUp": false } — the LQIP then renders as-is (blurred only by its own low resolution) and the full image swaps in hard.

Lazy loading is on by default: only the current slide ± media.preload neighbors (default 1) are fetched, and the window advances mid-drag so the incoming slide is already decoding before release. Set "media": { "lazy": false } to eager-load everything.

Self-hosted video

Self-hosted video renders inside the optics — frames are uploaded to a texture every frame and refract, disperse, and relight like any image. Requirements:

  • Format: MP4 with H.264 video. This is the one codec every supported browser decodes. WebM/VP9 works where supported, but always provide the H.264 MP4. HLS (.m3u8) plays only where natively supported (Safari/iOS); on other browsers the slide falls back to its poster image.
  • Muted autoplay. Playback always starts muted — muted is forced whenever autoplay is in effect, because unmuted autoplay is blocked by every modern browser. Refrakt never attempts it. If a browser still blocks the first play(), the poster frame stays up and playback retries on the next user gesture.
  • playsinline is set automatically (on by default in media.video) so iOS renders frames inline instead of hijacking into the native fullscreen player.
  • Always supply data-rf-poster — it is the LQIP-style first paint, the fallback if playback fails, and the still used when the video is evicted from the preload window.
  • Keep clips short and loopable (5–15 s), 1280–1920 px wide, and strip the audio track — it will never be heard in the slider and only costs bandwidth.

Markup — the video URL and poster live on the figure:

<figure class="rf-slide"
        data-rf-video="video/reel-01.mp4"
        data-rf-poster="img/reel-01-poster.jpg">
  <img src="img/reel-01-lqip.jpg" alt="Reel 01 — title sequence">
  <figcaption class="rf-text"><h2>Title Sequence</h2></figcaption>
</figure>

Behavior to know about (all automatic):

  • A video plays only while its slide is the centered slide and the tab is visible; leaving center pauses (resume, not restart). Off-window videos cost zero per frame.
  • A shared pool of at most 4 <video> decoders serves all instances on the page (mobile Safari caps concurrent decoders). With more simultaneous video slides than that, the furthest-from-center slides show their poster stills.
  • Defaults live under media.video: { "muted": true, "autoplay": true, "loop": true, "playsinline": true }.
  • With lightbox.enabled, opening a video slide migrates the real <video> element into the lightbox with native controls — and since a user gesture opened it, sound is allowed there.

The YouTube / Vimeo poster pattern

Honest limitation first: a cross-origin iframe can never be a WebGL texture — no library can refract live YouTube or Vimeo playback, and Refrakt does not pretend to. Instead, provider slides use the poster pattern: a still poster image renders inside the optics (it refracts, tears, and frosts like any slide, and gets a play badge), and clicking it opens playback in a lightbox iframe. Provider slides force the lightbox on for themselves even when lightbox.enabled is false — playback has nowhere else to go.

<!-- YouTube — bare video ID -->
<figure class="rf-slide"
        data-rf-youtube="dQw4w9WgXcQ"
        data-rf-poster="img/talk-poster.jpg">
  <img src="img/talk-lqip.jpg" alt="Keynote — opening talk">
  <figcaption class="rf-text"><h2>Keynote</h2></figcaption>
</figure>

<!-- Vimeo — bare video ID; data-rf-poster is REQUIRED -->
<figure class="rf-slide"
        data-rf-vimeo="76979871"
        data-rf-poster="img/short-poster.jpg">
  <img src="img/short-lqip.jpg" alt="Short film still">
  <figcaption class="rf-text"><h2>Short Film</h2></figcaption>
</figure>

A full watch-page URL in data-rf-video also works — youtube.com/watch?v=…, youtu.be/…, youtube.com/embed|shorts/…, vimeo.com/… are all parsed — but the bare-ID attributes are unambiguous and preferred.

Where the poster image comes from

  1. data-rf-poster — author-supplied. Loaded through the normal image path (LQIP and frost-up included). Required for Vimeo, strongly recommended for YouTube: it is the only path with zero runtime network dependency on the provider, which also matters for GDPR-sensitive pages (no provider request until the visitor clicks play).
  2. YouTube derivation (no poster given): the engine fetches the video’s maxresdefault thumbnail, detecting and skipping YouTube’s gray 120×90 stub in favor of hqdefault.
  3. Vimeo derivation (no poster given): an oEmbed API call resolves the thumbnail. This fails offline, for private videos, and at Vimeo’s whim — which is exactly why data-rf-poster is documented as required for Vimeo. The failure warns: Vimeo oEmbed failed for <id>; supply data-rf-poster.

Derivation (items 2–3) is a convenience fallback, not the recommended path. Supply data-rf-poster and your page makes no provider requests at all until playback — every shipped demo does exactly this.

Lightbox playback uses privacy-enhanced endpoints: youtube-nocookie.com/embed for YouTube, player.vimeo.com for Vimeo, both with autoplay=1 — the iframe fades in only after the poster lands sharp. On strict-CSP sites see the CSP notes for the frame-src entries this needs.

CORS — images on a CDN

WebGL may only sample images the page is allowed to read. Refrakt requests every image and video with crossorigin="anonymous", so:

  • Same-origin files: nothing to do. Relative paths on your own host always work.
  • Cross-origin files (CDN, object storage) must send Access-Control-Allow-Origin — either * or your page’s origin. Without that header the browser blocks the texture upload and the slide takes the failure path (LQIP stays up, one console warning).

Enabling the header where you likely host media:

HostWhat to set
Amazon S3 / compatible Bucket CORS rule: AllowedOrigins: ["*"], AllowedMethods: ["GET"]
Cloudflare / most CDNs Response-header rule adding Access-Control-Allow-Origin: * for image/video paths
nginx add_header Access-Control-Allow-Origin *; on the media location
Apache Header set Access-Control-Allow-Origin "*" in the media directory

YouTube’s thumbnail host serves CORS-clean images already; that path needs no configuration. If your CDN cannot send the header, serve slider media from the page’s own origin — everything else about Refrakt works identically.

Texture memory — auto-managed

You do not manage GPU memory; the engine does, against a flat 96 MiB texture budget shared by every instance on the page:

  • Full-resolution textures are kept for the current slide ± media.preload neighbors; slides beyond that window become eviction candidates.
  • While the budget is exceeded, the least-recently-rendered candidate is evicted on the next idle frame. An evicted slide quietly reverts to its LQIP texture; if it re-enters the window, the full file re-fetches from HTTP cache and frosts back up. The current slide and its immediate neighbors are never evicted.
  • Slide textures are capped at 2048 px regardless of DPR, and each costs roughly width × height × 4 × 1.34 bytes (the 1.34 covers mipmaps) — a 1600×1000 image is ~8.6 MB of GPU memory.
  • On multi-instance pages the budget and the LRU list are global to the shared context, so ten product vitrines cannot stampede the GPU any harder than one hero.

Practical takeaway: follow the 1600 px guidance and roughly ten slides can be fully resident at once; beyond that the engine silently rotates textures and you will never notice on a working network.