Add `AND webm_path IS NOT NULL` to the /api/freehunt pool so free-hunt
mode only selects ghosts with real media (and their derived webp/image
fallbacks), never bare procedural-wisp ghosts.
iOS won't animate a WebP inside a WebGL texture in any form. Render the
iOS/WebKit fallback as a real DOM <img> (animates natively) and project an
invisible 3D anchor's screen position onto it each frame, so it still tracks
distance/size and bobs. Flat 2D rather than in-scene, but animated + transparent.
iOS pauses animation on a detached <img>, so the CanvasTexture sampled a
frozen frame. Attach the img hidden off-screen (opacity 0.01) so WebKit
keeps advancing the WebP; remove it on clear.
Three.js doesn't advance an animated-image Texture on iOS, so the WebP
showed as a static frame. Draw the animating <img> into an offscreen
canvas each frame and sample it through a CanvasTexture, keeping the ghost
in 3D (bob/scale/lookAt) and animated, with transparency preserved.
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.
The WebM carries real alpha (alpha_mode=1) but rendered opaque because the
VideoTexture was forced to SRGBColorSpace and the material assumed
premultiplied alpha, crushing transparent regions to black. Removing the
colorspace override and setting premultipliedAlpha:false keys the black out.
The /api/admin/ghosts endpoint returns raw DB rows (webm_path,
webp_path, image_path as bare filenames), not the public API's
camelCase URL shape. buildGhost now reads those and prefixes /uploads/,
so WebM video renders in the preview instead of falling through to the
procedural wisp.
Camera-background single-ghost preview reusing the hunt's render path
(WebM VP9+alpha VideoTexture -> WebP/GIF -> procedural wisp). Login gate
matches admin; ghost list via JWT-protected /api/admin/ghosts. Dropdown
to pick a ghost, sliders for distance/size, camera toggle.
- openGhost() previews stored media, preferring WebM video over still image
- showPreview/hidePreview swap between <img> and <video> elements
- Live local preview on file pick (handles mp4/webm)
- Ghost-table row thumbnail renders <video> when only a WebM exists
- Widen file input accept to include .webm/.mp4
- Add a <video> preview element alongside the <img> preview
- Update label and help text to mention server-side MP4 conversion
- Allow .mp4/.webm in addition to image types; raise upload limit to 64MB
- MP4 uploads are luma-keyed to a VP9+alpha WebM plus an animated WebP
fallback via lib/ghost-media.js; the raw MP4 is discarded
- Pre-made .webm uploads are stored directly
- All prior media (image/webm/webp) is cleaned up on replace and on delete
- WebP doubles as the still thumbnail for converted ghosts
Shells out to system ffmpeg with the same luma-key pipeline as ghostify.sh.
Produces a VP9+alpha WebM (browser-decoded primary) and an animated WebP
fallback. No new npm dependency; fails gracefully if ffmpeg is absent.
- Detect VP9-alpha WebM support once at load; iOS Safari falls back to <img>
- addGhost prefers data.webm via THREE.VideoTexture (browser-decoded, no
per-frame needsUpdate pump) when supported
- On video error, gracefully swap the billboard to the GIF/image texture
- Pause + release ghost <video> elements on capture and on hunt teardown to
avoid leaking decoders