Animate WebP fallback on iOS via per-frame CanvasTexture
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.
This commit is contained in:
+27
-3
@@ -246,13 +246,27 @@
|
|||||||
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-image Texture on iOS, so instead we draw the <img> into an
|
||||||
|
// offscreen canvas every frame and use a CanvasTexture. The browser keeps
|
||||||
|
// animating the WebP internally; drawImage() samples the current frame, so
|
||||||
|
// the texture animates and transparency is preserved.
|
||||||
const img = document.createElement('img');
|
const img = document.createElement('img');
|
||||||
img.crossOrigin = 'anonymous'; img.src = image;
|
img.crossOrigin = 'anonymous'; img.src = image;
|
||||||
const tex = new THREE.Texture(img);
|
const cnv = document.createElement('canvas');
|
||||||
img.onload = () => { tex.needsUpdate = true; };
|
cnv.width = 256; cnv.height = 256;
|
||||||
|
const ctx = cnv.getContext('2d');
|
||||||
|
const tex = new THREE.CanvasTexture(cnv);
|
||||||
|
tex.minFilter = THREE.LinearFilter; tex.magFilter = THREE.LinearFilter; tex.generateMipmaps = false;
|
||||||
|
img.onload = () => {
|
||||||
|
// size the canvas to the image's aspect once known
|
||||||
|
cnv.width = img.naturalWidth || 256;
|
||||||
|
cnv.height = img.naturalHeight || 256;
|
||||||
|
};
|
||||||
const mat = new THREE.MeshBasicMaterial({ map: tex, transparent: true, side: THREE.DoubleSide });
|
const mat = new THREE.MeshBasicMaterial({ map: tex, transparent: true, side: THREE.DoubleSide });
|
||||||
mesh = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), mat);
|
mesh = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), mat);
|
||||||
group.userData.gifTex = tex; group.userData.gifImg = img;
|
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({
|
||||||
@@ -311,7 +325,17 @@
|
|||||||
if (current) {
|
if (current) {
|
||||||
current.position.y = Math.sin(t * 1.5 + current.userData.bobPhase) * 0.06;
|
current.position.y = Math.sin(t * 1.5 + current.userData.bobPhase) * 0.06;
|
||||||
current.lookAt(camera.position);
|
current.lookAt(camera.position);
|
||||||
|
// 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
|
||||||
|
// so the animation plays (works on iOS, where image textures don't tick).
|
||||||
|
const ud = current.userData;
|
||||||
|
if (ud.canvasTex && ud.animImg?.complete && ud.animImg.naturalWidth) {
|
||||||
|
const c = ud.canvasEl;
|
||||||
|
ud.canvasCtx.clearRect(0, 0, c.width, c.height);
|
||||||
|
ud.canvasCtx.drawImage(ud.animImg, 0, 0, c.width, c.height);
|
||||||
|
ud.canvasTex.needsUpdate = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
renderer.render(scene, camera);
|
renderer.render(scene, camera);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user