Route iOS/WebKit to transparent WebP instead of opaque VP9 WebM

WebKit (all iOS browsers + desktop Safari) plays VP9 WebM but renders its
alpha as opaque black. SUPPORTS_WEBM_ALPHA now excludes iOS/Safari so those
devices fall through to the animated WebP fallback, which IS transparent.
Desktop Chromium/Gecko keep the WebM path.
This commit is contained in:
2026-06-19 22:38:53 +10:00
parent 6f8b67b583
commit 793a17dbdf
+14 -7
View File
@@ -95,9 +95,17 @@
const $ = (s) => document.querySelector(s); const $ = (s) => document.querySelector(s);
const TYPE_COLORS = { red: 0xff3b5c, yellow: 0xffc23b, blue: 0x3bb6ff }; const TYPE_COLORS = { red: 0xff3b5c, yellow: 0xffc23b, blue: 0x3bb6ff };
// Same VP9-alpha capability check as the hunt. // VP9-alpha WebM transparency works on desktop Chromium/Gecko but NOT on
// WebKit: all iOS browsers (Safari, Chrome, Firefox) and desktop Safari play
// the WebM but render its alpha as opaque black. Those devices must fall
// through to the animated WebP (which IS transparent), so exclude WebKit here.
const SUPPORTS_WEBM_ALPHA = (() => { const SUPPORTS_WEBM_ALPHA = (() => {
try { try {
const ua = navigator.userAgent;
const isIOS = /iPad|iPhone|iPod/.test(ua) ||
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1); // iPadOS 13+ poses as Mac
const isSafari = /AppleWebKit/.test(ua) && !/Chrome|Chromium|Edg|OPR/.test(ua);
if (isIOS || isSafari) return false;
const v = document.createElement('video'); const v = document.createElement('video');
return !!v.canPlayType && v.canPlayType('video/webm; codecs="vp9"') !== ''; return !!v.canPlayType && v.canPlayType('video/webm; codecs="vp9"') !== '';
} catch { return false; } } catch { return false; }
@@ -215,11 +223,10 @@
vid.muted = true; vid.loop = true; vid.playsInline = true; vid.autoplay = true; vid.preload = 'auto'; vid.muted = true; vid.loop = true; vid.playsInline = true; vid.autoplay = true; vid.preload = 'auto';
vid.src = webm; vid.src = webm;
const tex = new THREE.VideoTexture(vid); const tex = new THREE.VideoTexture(vid);
// VP9-alpha WebMs carry straight (non-premultiplied) alpha. Leave the // Desktop Chromium/Gecko only (WebKit excluded above). RGBAFormat keeps
// texture in the default (linear/no-color-conversion) space — forcing // the alpha channel on frame upload; no SRGB override (it can crush the
// SRGBColorSpace here makes three.js crush the alpha to black. The // alpha); premultipliedAlpha off so transparent regions stay see-through.
// material below blends on the video's own alpha with premultipliedAlpha tex.format = THREE.RGBAFormat;
// off so transparent regions stay see-through.
tex.minFilter = THREE.LinearFilter; tex.magFilter = THREE.LinearFilter; tex.generateMipmaps = false; tex.minFilter = THREE.LinearFilter; tex.magFilter = THREE.LinearFilter; tex.generateMipmaps = false;
const mat = new THREE.MeshBasicMaterial({ const mat = new THREE.MeshBasicMaterial({
map: tex, transparent: true, side: THREE.DoubleSide, map: tex, transparent: true, side: THREE.DoubleSide,
@@ -272,7 +279,7 @@
const hasWebm = !!(data.webm_path || data.webm); const hasWebm = !!(data.webm_path || data.webm);
const hasImg = !!(data.image_path || data.image || data.webp_path || data.webp); const hasImg = !!(data.image_path || data.image || data.webp_path || data.webp);
const kind = hasWebm const kind = hasWebm
? ' · WebM' + (SUPPORTS_WEBM_ALPHA ? '' : ' (no VP9-alpha fallback)') ? (SUPPORTS_WEBM_ALPHA ? ' · WebM' : ' · WebP (WebKit fallback)')
: hasImg ? ' · image' : ' · procedural wisp'; : hasImg ? ' · image' : ' · procedural wisp';
$('#pv-hint').textContent = `${data.name}${kind}`; $('#pv-hint').textContent = `${data.name}${kind}`;
} }