iOS WebP: render as tracked DOM img overlay instead of WebGL texture
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.
This commit is contained in:
+43
-32
@@ -246,31 +246,25 @@
|
|||||||
const pr = vid.play(); if (pr && pr.catch) pr.catch(() => {});
|
const pr = vid.play(); if (pr && pr.catch) pr.catch(() => {});
|
||||||
group.userData.vidEl = vid;
|
group.userData.vidEl = vid;
|
||||||
} else if (image) {
|
} else if (image) {
|
||||||
// Animated WebP (the iOS/WebKit fallback). Three.js won't advance an
|
// Animated WebP (the iOS/WebKit fallback). Three.js can't advance an
|
||||||
// animated-image Texture on iOS, so instead we draw the <img> into an
|
// animated WebP in a WebGL texture on iOS, so we render it as a real DOM
|
||||||
// offscreen canvas every frame and use a CanvasTexture. The browser keeps
|
// <img> overlay (which animates natively) and project an invisible 3D
|
||||||
// animating the WebP internally; drawImage() samples the current frame, so
|
// anchor's screen position onto it each frame. The ghost still tracks
|
||||||
// the texture animates and transparency is preserved.
|
// distance/size and bobs, and transparency is native to the WebP.
|
||||||
const img = document.createElement('img');
|
const ovl = document.createElement('img');
|
||||||
img.crossOrigin = 'anonymous'; img.src = image;
|
ovl.crossOrigin = 'anonymous'; ovl.src = image;
|
||||||
// iOS pauses animation on a detached <img>, so attach it hidden off-screen
|
ovl.style.cssText = 'position:fixed;left:0;top:0;pointer-events:none;z-index:5;transform:translate(-50%,-50%);will-change:left,top,width;';
|
||||||
// to keep the WebP advancing. We sample its current frame into the canvas.
|
document.body.appendChild(ovl);
|
||||||
img.style.cssText = 'position:fixed;left:-99999px;top:0;width:64px;height:64px;pointer-events:none;opacity:0.01;';
|
// invisible anchor mesh so the existing bob/lookAt/position math applies
|
||||||
document.body.appendChild(img);
|
mesh = new THREE.Mesh(
|
||||||
const cnv = document.createElement('canvas');
|
new THREE.PlaneGeometry(1, 1),
|
||||||
cnv.width = 256; cnv.height = 256;
|
new THREE.MeshBasicMaterial({ visible: false })
|
||||||
const ctx = cnv.getContext('2d');
|
);
|
||||||
const tex = new THREE.CanvasTexture(cnv);
|
group.userData.overlayImg = ovl;
|
||||||
tex.minFilter = THREE.LinearFilter; tex.magFilter = THREE.LinearFilter; tex.generateMipmaps = false;
|
group.userData.overlayAspect = 1;
|
||||||
img.onload = () => {
|
ovl.onload = () => {
|
||||||
// size the canvas to the image's aspect once known
|
group.userData.overlayAspect = (ovl.naturalWidth || 1) / (ovl.naturalHeight || 1);
|
||||||
cnv.width = img.naturalWidth || 256;
|
|
||||||
cnv.height = img.naturalHeight || 256;
|
|
||||||
};
|
};
|
||||||
const mat = new THREE.MeshBasicMaterial({ map: tex, transparent: true, side: THREE.DoubleSide });
|
|
||||||
mesh = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), mat);
|
|
||||||
group.userData.canvasTex = tex; group.userData.canvasCtx = ctx; group.userData.canvasEl = cnv;
|
|
||||||
group.userData.animImg = img;
|
|
||||||
} else {
|
} else {
|
||||||
const geo = new THREE.SphereGeometry(0.4, 24, 24);
|
const geo = new THREE.SphereGeometry(0.4, 24, 24);
|
||||||
const mat = new THREE.MeshStandardMaterial({
|
const mat = new THREE.MeshStandardMaterial({
|
||||||
@@ -305,7 +299,7 @@
|
|||||||
function clearGhost() {
|
function clearGhost() {
|
||||||
if (!current) return;
|
if (!current) return;
|
||||||
if (current.userData.vidEl) { try { current.userData.vidEl.pause(); current.userData.vidEl.src = ''; } catch {} }
|
if (current.userData.vidEl) { try { current.userData.vidEl.pause(); current.userData.vidEl.src = ''; } catch {} }
|
||||||
if (current.userData.animImg) { try { current.userData.animImg.remove(); } catch {} }
|
if (current.userData.overlayImg) { try { current.userData.overlayImg.remove(); } catch {} }
|
||||||
scene.remove(current);
|
scene.remove(current);
|
||||||
current = null;
|
current = null;
|
||||||
}
|
}
|
||||||
@@ -324,6 +318,7 @@
|
|||||||
size = +e.target.value; $('#pv-size-val').textContent = size.toFixed(1); applyTransform();
|
size = +e.target.value; $('#pv-size-val').textContent = size.toFixed(1); applyTransform();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const _v = new THREE.Vector3();
|
||||||
function loop() {
|
function loop() {
|
||||||
raf = requestAnimationFrame(loop);
|
raf = requestAnimationFrame(loop);
|
||||||
const t = performance.now() / 1000;
|
const t = performance.now() / 1000;
|
||||||
@@ -332,14 +327,30 @@
|
|||||||
current.lookAt(camera.position);
|
current.lookAt(camera.position);
|
||||||
// Desktop WebM-onerror image fallback (rare): refresh its texture.
|
// Desktop WebM-onerror image fallback (rare): refresh its texture.
|
||||||
if (current.userData.gifTex && current.userData.gifImg?.complete) current.userData.gifTex.needsUpdate = true;
|
if (current.userData.gifTex && current.userData.gifImg?.complete) current.userData.gifTex.needsUpdate = true;
|
||||||
// Animated WebP path: redraw the current frame into the canvas texture
|
// iOS WebP overlay: project the invisible anchor to screen space and
|
||||||
// so the animation plays (works on iOS, where image textures don't tick).
|
// position/scale the DOM <img> so it tracks the 3D ghost (and animates).
|
||||||
const ud = current.userData;
|
const ud = current.userData;
|
||||||
if (ud.canvasTex && ud.animImg?.complete && ud.animImg.naturalWidth) {
|
if (ud.overlayImg) {
|
||||||
const c = ud.canvasEl;
|
_v.copy(current.position);
|
||||||
ud.canvasCtx.clearRect(0, 0, c.width, c.height);
|
_v.project(camera);
|
||||||
ud.canvasCtx.drawImage(ud.animImg, 0, 0, c.width, c.height);
|
const onScreen = _v.z < 1;
|
||||||
ud.canvasTex.needsUpdate = true;
|
if (onScreen) {
|
||||||
|
const sx = (_v.x * 0.5 + 0.5) * innerWidth;
|
||||||
|
const sy = (-_v.y * 0.5 + 0.5) * innerHeight;
|
||||||
|
// size in px: shrink with distance. fov-based scale at the anchor depth.
|
||||||
|
const depth = Math.abs(distance);
|
||||||
|
const worldH = size; // plane is 1 unit * size
|
||||||
|
const vFov = (camera.fov * Math.PI) / 180;
|
||||||
|
const viewH = 2 * Math.tan(vFov / 2) * depth;
|
||||||
|
const px = (worldH / viewH) * innerHeight;
|
||||||
|
ud.overlayImg.style.display = '';
|
||||||
|
ud.overlayImg.style.left = sx + 'px';
|
||||||
|
ud.overlayImg.style.top = sy + 'px';
|
||||||
|
ud.overlayImg.style.height = px + 'px';
|
||||||
|
ud.overlayImg.style.width = px * ud.overlayAspect + 'px';
|
||||||
|
} else {
|
||||||
|
ud.overlayImg.style.display = 'none';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
renderer.render(scene, camera);
|
renderer.render(scene, camera);
|
||||||
|
|||||||
Reference in New Issue
Block a user