Rendering robuster + sichtbare Fehlermeldung + no-cache

- crossOrigin von Asset-Images entfernt (haeufige Ursache fuer "tainted canvas",
  wodurch der Szenen-Export fehlschlug -> kaputtes Bild)
- toBlob mit Fallback auf toDataURL
- Ladefehler werden jetzt sichtbar im UI angezeigt (statt nur Konsole)
- Frontend mit Cache-Control: no-cache -> kein veraltetes JS/CSS nach git pull

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Guido
2026-08-04 18:58:51 +02:00
parent b95464670a
commit adc0fae93f
4 changed files with 62 additions and 8 deletions

View File

@@ -269,6 +269,24 @@ html, body {
#next-scene:hover { background: var(--pink-soft); } #next-scene:hover { background: var(--pink-soft); }
@keyframes pop { from { transform: scale(0.85); opacity: 0; } to { transform: scale(1); opacity: 1; } } @keyframes pop { from { transform: scale(0.85); opacity: 0; } to { transform: scale(1); opacity: 1; } }
/* ---- Fehleranzeige ---- */
#error-box {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
max-width: 82%;
background: #2a1020;
color: #ffd0e2;
border: 1px solid var(--pink);
padding: 16px 20px;
border-radius: 12px;
font-size: 14px;
line-height: 1.4;
text-align: center;
z-index: 20;
}
.hidden { display: none !important; } .hidden { display: none !important; }
/* Mobile */ /* Mobile */

View File

@@ -31,12 +31,30 @@ function showToast(message) {
showToast._t = setTimeout(() => toast.classList.remove('show'), 900); showToast._t = setTimeout(() => toast.classList.remove('show'), 900);
} }
function showError(err) {
let box = document.getElementById('error-box');
if (!box) {
box = document.createElement('div');
box.id = 'error-box';
stage.appendChild(box);
}
const msg = err && err.message ? err.message : String(err);
box.textContent = 'Fehler beim Laden der Szene: ' + msg;
box.classList.remove('hidden');
}
function clearError() {
const box = document.getElementById('error-box');
if (box) box.classList.add('hidden');
}
async function loadScene() { async function loadScene() {
showLoading(true); showLoading(true);
clearError();
winOverlay.classList.add('hidden'); winOverlay.classList.add('hidden');
try { try {
const res = await fetch('/api/scene'); const res = await fetch('/api/scene');
if (!res.ok) throw new Error('Szene konnte nicht geladen werden.'); if (!res.ok) throw new Error('Server-Antwort ' + res.status);
current = await res.json(); current = await res.json();
await scene.render(current); await scene.render(current);
scene.resetView(); scene.resetView();
@@ -44,7 +62,7 @@ async function loadScene() {
document.getElementById('category-name').textContent = current.category || ''; document.getElementById('category-name').textContent = current.category || '';
} catch (err) { } catch (err) {
console.error(err); console.error(err);
showToast('Ups — Szene konnte nicht geladen werden.'); showError(err);
} finally { } finally {
showLoading(false); showLoading(false);
} }

View File

@@ -17,7 +17,8 @@ let emojiAssetsAvailable = null; // pro Session: liegt ein Emoji-Bildset vor?
function loadImage(url) { function loadImage(url) {
return new Promise((resolve) => { return new Promise((resolve) => {
const img = new Image(); const img = new Image();
img.crossOrigin = 'anonymous'; // Kein crossOrigin: Assets sind same-origin. crossOrigin kann je nach Browser/Proxy
// dazu fuehren, dass das Canvas als "tainted" gilt und der Export fehlschlaegt.
img.onload = () => resolve({ url, img }); img.onload = () => resolve({ url, img });
img.onerror = () => resolve({ url, img: null }); img.onerror = () => resolve({ url, img: null });
img.src = url; img.src = url;
@@ -155,9 +156,20 @@ export class Scene {
ctx.restore(); ctx.restore();
} }
return await new Promise((resolve) => { // Export als Blob-URL; mit Fallback auf dataURL, falls toBlob nicht greift.
canvas.toBlob((blob) => resolve(URL.createObjectURL(blob)), 'image/png'); try {
}); const blob = await new Promise((resolve, reject) => {
try {
canvas.toBlob((b) => (b ? resolve(b) : reject(new Error('toBlob lieferte null'))), 'image/png');
} catch (err) {
reject(err);
}
});
return URL.createObjectURL(blob);
} catch (err) {
console.warn('[scene] toBlob fehlgeschlagen, versuche toDataURL:', err);
return canvas.toDataURL('image/png'); // wirft nur, wenn Canvas wirklich "tainted" ist
}
} }
_drawItem(ctx, item) { _drawItem(ctx, item) {

View File

@@ -57,8 +57,14 @@ if (scenePool.length === 0) {
const app = express(); const app = express();
app.use(express.json()); app.use(express.json());
// Statisches Frontend + Assets // Statisches Frontend + Assets.
app.use(express.static(PUBLIC_DIR)); // no-cache fuers Frontend (HTML/JS/CSS): Browser/Proxy muss revalidieren -> nie altes
// Skript nach einem Update (haeufige Ursache fuer "geht gar nix" nach git pull).
app.use(
express.static(PUBLIC_DIR, {
setHeaders: (res) => res.setHeader('Cache-Control', 'no-cache'),
})
);
app.use('/scenes', express.static(SCENES_DIR)); app.use('/scenes', express.static(SCENES_DIR));
app.use('/library', express.static(LIBRARY_DIR)); app.use('/library', express.static(LIBRARY_DIR));
if (existsSync(PANZOOM_DIST)) { if (existsSync(PANZOOM_DIST)) {