fix: carregador do mural à prova de loop no celular
build-atlas.js gera páginas leves de 3072 px para celular (--so-mobile regera só elas) e registra o campo mobile no atlas-map.json. wall.html usa essas páginas em toque ou GPU/aparelho fraco, detecta pela marca em sessionStorage que a tentativa anterior morreu e força o modo leve, e trata falha de download, de decodificação e perda de contexto WebGL com aviso e botão, sem recarregar sozinho. Detalhes e testes no achado 3.5.
This commit is contained in:
@@ -266,6 +266,56 @@ antes de tudo isso o mapa era 5,52 MB / 0,73 MB.
|
|||||||
debaixo do `#card-wrap` (20). Agora 25 (scrim 24), e no celular a ficha
|
debaixo do `#card-wrap` (20). Agora 25 (scrim 24), e no celular a ficha
|
||||||
estaciona acima do peek da sheet para não cobrir ‹ anterior / próxima ›.
|
estaciona acima do peek da sheet para não cobrir ‹ anterior / próxima ›.
|
||||||
|
|
||||||
|
### Achado 3.5 — o mural entrava em loop de carregamento no celular
|
||||||
|
|
||||||
|
Relato do usuário (14/09): no celular a tela inicial ficou em loop. Causa, lida
|
||||||
|
no carregador: o celular **decodificava cada página em 8192×8192** (~268 MB) e
|
||||||
|
só então reduzia para 4096 num canvas, retendo os canvases além das texturas —
|
||||||
|
pico de ~800 MB. O iOS não lança erro por falta de memória: mata a aba e a
|
||||||
|
**recarrega sozinho**, e a página recomeçava o mesmo carregamento. Não havia
|
||||||
|
tratamento de erro nenhum: fetch falho ou decodificação recusada deixavam a
|
||||||
|
barra parada para sempre.
|
||||||
|
|
||||||
|
Correção (commit `xyz` e `brasiliano` de 14/09):
|
||||||
|
- **Páginas leves** `atlas-m-0..3.webp` (3072², peça de 48 px), geradas pelo
|
||||||
|
`build-atlas.js` (`--so-mobile` regera só elas) e registradas em
|
||||||
|
`atlas-map.json` no campo `mobile`. Mesma posição de cada peça, então a conta de
|
||||||
|
uv não muda. 6,8 MB no total contra 42 MB; 36 MB decodificados por página.
|
||||||
|
- **Modo leve** em toque, `maxTextureSize` < 8192 ou `deviceMemory` ≤ 4, ou
|
||||||
|
forçado com `localStorage.mural_leve = '1'`.
|
||||||
|
- **Guarda contra loop**: marca `mural_carregando` em `sessionStorage` antes da
|
||||||
|
parte pesada, apagada só depois de dois quadros renderizados. Se a página abre
|
||||||
|
com a marca, a tentativa anterior morreu → vai direto para o modo leve e
|
||||||
|
mostra "modo leve — a tentativa anterior não terminou".
|
||||||
|
- **Nunca recarrega sozinho**: HTTP de erro, decodificação recusada e
|
||||||
|
`webglcontextlost` mostram aviso com botão "tentar de novo" (que recarrega uma
|
||||||
|
vez, já no modo leve). Progresso parado por 30 s mostra "o carregamento parou
|
||||||
|
de avançar" sem derrubar o que ainda pode estar vivo.
|
||||||
|
|
||||||
|
Verificado com Chrome real via `puppeteer-core` (perfil próprio, WebGL
|
||||||
|
swiftshader), emulando celular com toque — roteiros em
|
||||||
|
`scratchpad/teste-mural/testa.mjs` e `testa2.mjs`:
|
||||||
|
|
||||||
|
| cenário | resultado |
|
||||||
|
|---|---|
|
||||||
|
| A — celular carrega | só as 4 páginas leves, capa some em 8,3 s, marca apagada |
|
||||||
|
| C — marca de crash no desktop | aviso de modo leve, nenhuma página pesada, sem recarga |
|
||||||
|
| D — página falha no download | aviso com botão, **nenhuma recarga em 10 s** |
|
||||||
|
| E — botão tentar de novo | recarrega uma vez, termina no modo leve, sem 2ª recarga |
|
||||||
|
| F — perda de contexto WebGL | aviso com botão, sem recarga |
|
||||||
|
| G — download travado | aviso "parou de avançar" em 33 s, sem recarga |
|
||||||
|
|
||||||
|
O card no modo leve recorta a peça certa: a mesma peça tirada do atlas normal e
|
||||||
|
do leve difere 4/255 em média; a vizinha, 41,5/255.
|
||||||
|
|
||||||
|
**Limite do teste**: não há iPhone de verdade aqui — o limite de memória do iOS
|
||||||
|
não se emula no Chrome. O que está provado é que o celular não decodifica mais
|
||||||
|
8192² e que nenhum caminho de falha recarrega sozinho.
|
||||||
|
|
||||||
|
**Armadilha de teste**: `framenavigated` do Puppeteer conta a `about:blank`
|
||||||
|
inicial e mudanças de histórico do mural, então contar navegações dá falso
|
||||||
|
positivo de recarga. Usar uma marca em `window`, que só some numa recarga real.
|
||||||
|
|
||||||
## Miniaturas derivadas + reconciliação do ledger — 2026-09-12
|
## Miniaturas derivadas + reconciliação do ledger — 2026-09-12
|
||||||
|
|
||||||
Mural no ar com **13.059 peças** (antes 12.552): commits `bba32f6`,
|
Mural no ar com **13.059 peças** (antes 12.552): commits `bba32f6`,
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
+31
-1
@@ -19,6 +19,7 @@
|
|||||||
*
|
*
|
||||||
* Uso:
|
* Uso:
|
||||||
* node web/build-atlas.js [--n=99999] [--out=atlas] [--thumbs=DIR]
|
* node web/build-atlas.js [--n=99999] [--out=atlas] [--thumbs=DIR]
|
||||||
|
* node web/build-atlas.js --so-mobile (só as páginas leves, a partir do atlas pronto)
|
||||||
* node web/build-atlas.js [--n=99999] [--out=atlas] [--downloads=~/Downloads]
|
* node web/build-atlas.js [--n=99999] [--out=atlas] [--downloads=~/Downloads]
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -48,6 +49,12 @@ const ATLAS_SIZE = 8192; // dentro do limite de textura de qualquer GPU moderna
|
|||||||
const TILES_PER_ROW = ATLAS_SIZE / TILE_SIZE; // 64 -> capacidade 4096 peças por atlas
|
const TILES_PER_ROW = ATLAS_SIZE / TILE_SIZE; // 64 -> capacidade 4096 peças por atlas
|
||||||
const TILES_PER_ATLAS = TILES_PER_ROW * TILES_PER_ROW;
|
const TILES_PER_ATLAS = TILES_PER_ROW * TILES_PER_ROW;
|
||||||
const CATALOGO = path.resolve(__dirname, '..', 'pipeline', 'catalogo.jsonl');
|
const CATALOGO = path.resolve(__dirname, '..', 'pipeline', 'catalogo.jsonl');
|
||||||
|
// light pages for phones: the same layout scaled down, so every tile keeps its
|
||||||
|
// atlas/col/row and only the page size changes. 3072² stays under maxTextureSize
|
||||||
|
// everywhere and under the ~16.7 M-pixel image decode ceiling of iOS Safari — a
|
||||||
|
// phone never has to decode an 8192² page, which is what killed the tab.
|
||||||
|
const MOBILE_TILE = 48;
|
||||||
|
const MOBILE_SIZE = TILES_PER_ROW * MOBILE_TILE; // 3072
|
||||||
|
|
||||||
// ── acha os arquivos já baixados pela Trilha B em ~/Downloads ────────────────
|
// ── acha os arquivos já baixados pela Trilha B em ~/Downloads ────────────────
|
||||||
// nomes: BRAS__20.500.12156.1_<id>__THUMB__thumb_2.jpg (preferido, leve)
|
// nomes: BRAS__20.500.12156.1_<id>__THUMB__thumb_2.jpg (preferido, leve)
|
||||||
@@ -201,6 +208,27 @@ async function realTile(filePath) {
|
|||||||
.toBuffer();
|
.toBuffer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── light pages for phones, from the desktop pages already on disk ────────────
|
||||||
|
|
||||||
|
async function buildMobile(outDir) {
|
||||||
|
const mapPath = path.join(outDir, 'atlas-map.json');
|
||||||
|
const mapData = JSON.parse(fs.readFileSync(mapPath, 'utf8'));
|
||||||
|
const atlases = [];
|
||||||
|
for (let a = 0; a < mapData.atlases.length; a++) {
|
||||||
|
const name = `atlas-m-${a}.webp`;
|
||||||
|
await sharp(path.join(outDir, mapData.atlases[a]))
|
||||||
|
.resize(MOBILE_SIZE, MOBILE_SIZE, { kernel: 'lanczos3' })
|
||||||
|
.webp({ quality: 82, effort: 4 })
|
||||||
|
.toFile(path.join(outDir, name));
|
||||||
|
const mb = (fs.statSync(path.join(outDir, name)).size / 1024 / 1024).toFixed(2);
|
||||||
|
console.log(`Salvo ${name} (${mb} MB, ${MOBILE_SIZE}px)`);
|
||||||
|
atlases.push(name);
|
||||||
|
}
|
||||||
|
mapData.mobile = { size: MOBILE_SIZE, tile: MOBILE_TILE, atlases };
|
||||||
|
fs.writeFileSync(mapPath, JSON.stringify(mapData));
|
||||||
|
console.log(`atlas-map.json: variante leve registrada (${atlases.length} páginas de ${MOBILE_SIZE}px)`);
|
||||||
|
}
|
||||||
|
|
||||||
// ── main ────────────────────────────────────────────────────────────────────
|
// ── main ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
@@ -284,6 +312,8 @@ async function main() {
|
|||||||
const mapPath = path.join(OUT_DIR, 'atlas-map.json');
|
const mapPath = path.join(OUT_DIR, 'atlas-map.json');
|
||||||
fs.writeFileSync(mapPath, JSON.stringify(mapData));
|
fs.writeFileSync(mapPath, JSON.stringify(mapData));
|
||||||
console.log(`Salvo atlas-map.json (${(fs.statSync(mapPath).size / 1024).toFixed(0)} KB)`);
|
console.log(`Salvo atlas-map.json (${(fs.statSync(mapPath).size / 1024).toFixed(0)} KB)`);
|
||||||
|
await buildMobile(OUT_DIR);
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch(e => { console.error(e); process.exit(1); });
|
// --so-mobile: only regenerate the light pages from the desktop atlas on disk
|
||||||
|
(args['so-mobile'] ? buildMobile(OUT_DIR) : main()).catch(e => { console.error(e); process.exit(1); });
|
||||||
|
|||||||
+111
-5
@@ -94,6 +94,24 @@
|
|||||||
|
|
||||||
[hidden] { display: none !important; }
|
[hidden] { display: none !important; }
|
||||||
|
|
||||||
|
/* shown instead of a frozen bar: a load that dies or stalls says so, and the
|
||||||
|
retry is the reader's call — never an automatic reload, which on a phone
|
||||||
|
that just ran out of memory is exactly how the loop starts */
|
||||||
|
#falha {
|
||||||
|
position: fixed; inset: 0; z-index: 200; display: flex; flex-direction: column;
|
||||||
|
align-items: center; justify-content: center; gap: 14px; padding: 0 24px;
|
||||||
|
background: rgba(11,11,12,.94); color: #ddd; text-align: center;
|
||||||
|
font-family: system-ui, sans-serif; font-size: .9rem; line-height: 1.5;
|
||||||
|
}
|
||||||
|
#falha small { color: #888; font-size: .72rem; max-width: 34em; }
|
||||||
|
#falha button, #lento button {
|
||||||
|
font: inherit; font-size: .72rem; letter-spacing: .12em; text-transform: uppercase;
|
||||||
|
color: #0b0b0c; background: #7fa8c9; border: 0; border-radius: 3px; padding: 10px 16px;
|
||||||
|
}
|
||||||
|
#lento { margin-top: 18px; font-family: system-ui, sans-serif; font-size: .7rem; color: #999; }
|
||||||
|
#lento button { margin-top: 8px; font-size: .64rem; padding: 7px 12px; }
|
||||||
|
#aviso-leve { margin-top: 10px; font-family: system-ui, sans-serif; font-size: .62rem; color: #777; }
|
||||||
|
|
||||||
/* ── compact filter panel, top-left — dark chrome, so it reads as part of
|
/* ── compact filter panel, top-left — dark chrome, so it reads as part of
|
||||||
the wall's own darkness instead of a lit slab thrown on top of it.
|
the wall's own darkness instead of a lit slab thrown on top of it.
|
||||||
Every colour below comes from these tokens: retheme by editing them. ─── */
|
Every colour below comes from these tokens: retheme by editing them. ─── */
|
||||||
@@ -513,6 +531,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="enter" id="enter-label">carregando imagens… 0%</div>
|
<div class="enter" id="enter-label">carregando imagens… 0%</div>
|
||||||
<div id="progress-track"><div id="progress-bar"></div></div>
|
<div id="progress-track"><div id="progress-bar"></div></div>
|
||||||
|
<div id="aviso-leve" hidden>modo leve — a tentativa anterior não terminou de carregar</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -604,6 +623,44 @@ renderer.setPixelRatio(Math.min(window.devicePixelRatio, isTouch ? 1.75 : 2));
|
|||||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||||
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
||||||
|
|
||||||
|
// ─── light mode and the crash-loop guard ──────────────────────────────────
|
||||||
|
// On an iPhone, running out of memory throws nothing: the system kills the tab
|
||||||
|
// and reloads it by itself, the page starts the very same load, dies again — a
|
||||||
|
// loop. A mark in sessionStorage survives that automatic reload, so the next
|
||||||
|
// attempt knows the last one never finished and goes straight to the light
|
||||||
|
// atlas instead of repeating the crash.
|
||||||
|
const LOAD_MARK = 'mural_carregando';
|
||||||
|
const store = (kind, fn) => { try { return fn(window[kind]); } catch (e) { return null; } };
|
||||||
|
const previousLoadDied = !!store('sessionStorage', s => s.getItem(LOAD_MARK));
|
||||||
|
const lightMode = isTouch || previousLoadDied
|
||||||
|
|| store('localStorage', s => s.getItem('mural_leve')) === '1'
|
||||||
|
|| renderer.capabilities.maxTextureSize < 8192
|
||||||
|
|| (navigator.deviceMemory !== undefined && navigator.deviceMemory <= 4);
|
||||||
|
|
||||||
|
function showFailure(title, detail) {
|
||||||
|
if (document.getElementById('falha')) return;
|
||||||
|
const box = document.createElement('div');
|
||||||
|
box.id = 'falha';
|
||||||
|
const h = document.createElement('div'); h.textContent = title;
|
||||||
|
const small = document.createElement('small');
|
||||||
|
small.textContent = detail || 'Tentar de novo carrega a versão leve do mural, feita para celular.';
|
||||||
|
const btn = document.createElement('button'); btn.type = 'button'; btn.textContent = 'tentar de novo';
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
store('sessionStorage', s => s.setItem(LOAD_MARK, String(Date.now()))); // retry in light mode
|
||||||
|
location.reload();
|
||||||
|
});
|
||||||
|
box.append(h, small, btn);
|
||||||
|
document.body.appendChild(box);
|
||||||
|
}
|
||||||
|
|
||||||
|
// the GPU dropping the wall's textures is a memory signal too — say so, and let
|
||||||
|
// the reader decide; preventDefault keeps the browser from giving up silently
|
||||||
|
canvas.addEventListener('webglcontextlost', e => {
|
||||||
|
e.preventDefault();
|
||||||
|
store('sessionStorage', s => s.setItem(LOAD_MARK, String(Date.now())));
|
||||||
|
showFailure('O navegador liberou a memória do mural.');
|
||||||
|
}, false);
|
||||||
|
|
||||||
const scene = new THREE.Scene();
|
const scene = new THREE.Scene();
|
||||||
scene.background = new THREE.Color(0x0b0b0c);
|
scene.background = new THREE.Color(0x0b0b0c);
|
||||||
|
|
||||||
@@ -1563,6 +1620,7 @@ const asMB = bytes => `${(bytes / 1048576).toFixed(1)} MB`;
|
|||||||
// read a body in chunks so the wait is legible instead of a frozen line
|
// read a body in chunks so the wait is legible instead of a frozen line
|
||||||
async function fetchWithProgress(url, onProgress) {
|
async function fetchWithProgress(url, onProgress) {
|
||||||
const res = await fetch(url);
|
const res = await fetch(url);
|
||||||
|
if (!res.ok) throw new Error(`${url}: HTTP ${res.status}`);
|
||||||
const total = parseInt(res.headers.get('content-length') || '0', 10);
|
const total = parseInt(res.headers.get('content-length') || '0', 10);
|
||||||
const reader = res.body.getReader();
|
const reader = res.body.getReader();
|
||||||
const chunks = [];
|
const chunks = [];
|
||||||
@@ -1577,10 +1635,38 @@ async function fetchWithProgress(url, onProgress) {
|
|||||||
return { blob: new Blob(chunks), total };
|
return { blob: new Blob(chunks), total };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let lastActivity = performance.now();
|
||||||
|
let loadFinished = false;
|
||||||
|
const touchActivity = () => { lastActivity = performance.now(); };
|
||||||
|
|
||||||
|
// a bar that stops moving for half a minute is not "almost there": offer a way
|
||||||
|
// out without tearing down the load that may still be alive
|
||||||
|
const stallWatch = setInterval(() => {
|
||||||
|
if (loadFinished) { clearInterval(stallWatch); return; }
|
||||||
|
if (performance.now() - lastActivity < 30000 || document.getElementById('lento')) return;
|
||||||
|
const box = document.createElement('div');
|
||||||
|
box.id = 'lento';
|
||||||
|
box.textContent = 'o carregamento parou de avançar';
|
||||||
|
const btn = document.createElement('button'); btn.type = 'button'; btn.textContent = 'tentar de novo';
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
store('sessionStorage', s => s.setItem(LOAD_MARK, String(Date.now())));
|
||||||
|
location.reload();
|
||||||
|
});
|
||||||
|
box.append(document.createElement('br'), btn);
|
||||||
|
(document.getElementById('title-screen')?.classList.contains('show')
|
||||||
|
? document.getElementById('title-screen') : document.getElementById('loading')).appendChild(box);
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
|
// set before anything heavy: if this tab is killed mid-load, the automatic
|
||||||
|
// reload finds the mark and takes the light path
|
||||||
|
store('sessionStorage', s => s.setItem(LOAD_MARK, String(Date.now())));
|
||||||
|
if (previousLoadDied) document.getElementById('aviso-leve').hidden = false;
|
||||||
|
|
||||||
// the catalogue is megabytes of its own, and until now nothing said so
|
// the catalogue is megabytes of its own, and until now nothing said so
|
||||||
const loadingMsg = document.getElementById('loading-msg');
|
const loadingMsg = document.getElementById('loading-msg');
|
||||||
const catalogue = await fetchWithProgress('atlas/atlas-map.json', (loaded, total) => {
|
const catalogue = await fetchWithProgress('atlas/atlas-map.json', (loaded, total) => {
|
||||||
|
touchActivity();
|
||||||
// a gzipped response often carries no content-length: count bytes instead
|
// a gzipped response often carries no content-length: count bytes instead
|
||||||
loadingMsg.textContent = total
|
loadingMsg.textContent = total
|
||||||
? `carregando catálogo… ${pctOf(loaded, total)}%`
|
? `carregando catálogo… ${pctOf(loaded, total)}%`
|
||||||
@@ -1629,7 +1715,10 @@ async function main() {
|
|||||||
// and the plain <img> (for the card crop) from the same downloaded blob ──
|
// and the plain <img> (for the card crop) from the same downloaded blob ──
|
||||||
// Decoding four 8192² pages takes about as long as fetching them does, so the
|
// Decoding four 8192² pages takes about as long as fetching them does, so the
|
||||||
// bar covers both: parked at 100% for the whole decode, it read as stuck.
|
// bar covers both: parked at 100% for the whole decode, it read as stuck.
|
||||||
const nAtlases = mapData.atlases.length;
|
// phones get the light pages: same layout at 3072², so tiles keep their
|
||||||
|
// atlas/col/row and the uv math is unchanged
|
||||||
|
const pageSet = (lightMode && mapData.mobile) ? mapData.mobile : { size: mapData.size, atlases: mapData.atlases };
|
||||||
|
const nAtlases = pageSet.atlases.length;
|
||||||
const fileBytes = new Array(nAtlases).fill(0);
|
const fileBytes = new Array(nAtlases).fill(0);
|
||||||
const fileTotals = new Array(nAtlases).fill(0);
|
const fileTotals = new Array(nAtlases).fill(0);
|
||||||
let pagesReady = 0;
|
let pagesReady = 0;
|
||||||
@@ -1652,11 +1741,16 @@ async function main() {
|
|||||||
? `${down < 1 ? 'carregando' : 'preparando'} imagens… ${pct}%`
|
? `${down < 1 ? 'carregando' : 'preparando'} imagens… ${pct}%`
|
||||||
: `carregando imagens… ${asMB(loaded)}`;
|
: `carregando imagens… ${asMB(loaded)}`;
|
||||||
progressBar.style.width = `${pct}%`;
|
progressBar.style.width = `${pct}%`;
|
||||||
|
touchActivity();
|
||||||
}
|
}
|
||||||
|
|
||||||
// every response's headers first, bodies second: reading as they arrived meant
|
// every response's headers first, bodies second: reading as they arrived meant
|
||||||
// the last file's size landed mid-download and the percentage fell backwards
|
// the last file's size landed mid-download and the percentage fell backwards
|
||||||
const responses = await Promise.all(mapData.atlases.map(name => fetch(`atlas/${name}`)));
|
const responses = await Promise.all(pageSet.atlases.map(async name => {
|
||||||
|
const res = await fetch(`atlas/${name}`);
|
||||||
|
if (!res.ok) throw new Error(`atlas/${name}: HTTP ${res.status}`);
|
||||||
|
return res;
|
||||||
|
}));
|
||||||
responses.forEach((res, idx) => {
|
responses.forEach((res, idx) => {
|
||||||
fileTotals[idx] = parseInt(res.headers.get('content-length') || '0', 10);
|
fileTotals[idx] = parseInt(res.headers.get('content-length') || '0', 10);
|
||||||
});
|
});
|
||||||
@@ -1685,13 +1779,18 @@ async function main() {
|
|||||||
|
|
||||||
const atlasMats = [];
|
const atlasMats = [];
|
||||||
atlasSources = [];
|
atlasSources = [];
|
||||||
for (const blob of blobs) {
|
for (let b = 0; b < blobs.length; b++) {
|
||||||
|
const blob = blobs[b];
|
||||||
|
blobs[b] = null; // the compressed page is not needed once it is a bitmap
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
img.src = url;
|
img.src = url;
|
||||||
await img.decode().catch(() => {});
|
await img.decode().catch(() => {});
|
||||||
|
// a refused decode used to fall through as a blank page and a bar that
|
||||||
|
// never finished; say which page failed instead
|
||||||
|
if (!img.naturalWidth) throw new Error(`não foi possível decodificar ${pageSet.atlases[b]}`);
|
||||||
|
|
||||||
let source = img, size = img.naturalWidth || mapData.size;
|
let source = img, size = img.naturalWidth;
|
||||||
if (size > maxTex) {
|
if (size > maxTex) {
|
||||||
const c = document.createElement('canvas');
|
const c = document.createElement('canvas');
|
||||||
c.width = c.height = maxTex;
|
c.width = c.height = maxTex;
|
||||||
@@ -1733,11 +1832,17 @@ async function main() {
|
|||||||
// let the browser actually paint that frame before the freeze begins.
|
// let the browser actually paint that frame before the freeze begins.
|
||||||
enterLabel.textContent = 'montando o mural… 90%';
|
enterLabel.textContent = 'montando o mural… 90%';
|
||||||
progressBar.style.width = '90%';
|
progressBar.style.width = '90%';
|
||||||
|
touchActivity();
|
||||||
await new Promise(res => requestAnimationFrame(() => requestAnimationFrame(res)));
|
await new Promise(res => requestAnimationFrame(() => requestAnimationFrame(res)));
|
||||||
|
|
||||||
// ── ready — start rendering the wall right away, behind a translucent
|
// ── ready — start rendering the wall right away, behind a translucent
|
||||||
// title card, so the wall is already alive when the cover lifts ──
|
// title card, so the wall is already alive when the cover lifts ──
|
||||||
tick();
|
tick();
|
||||||
|
// two painted frames with every texture on the GPU: the heavy part is over,
|
||||||
|
// so the next visit — or a reload — takes the normal path again
|
||||||
|
await new Promise(res => requestAnimationFrame(() => requestAnimationFrame(res)));
|
||||||
|
loadFinished = true;
|
||||||
|
store('sessionStorage', s => s.removeItem(LOAD_MARK));
|
||||||
progressBar.style.width = '100%';
|
progressBar.style.width = '100%';
|
||||||
enterLabel.textContent = 'entrando…';
|
enterLabel.textContent = 'entrando…';
|
||||||
enterLabel.classList.add('ready');
|
enterLabel.classList.add('ready');
|
||||||
@@ -1774,8 +1879,9 @@ async function main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
main().catch(err => {
|
main().catch(err => {
|
||||||
document.getElementById('loading').innerHTML = `<p style="color:#e88">erro: ${err.message}</p>`;
|
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
loadFinished = true;
|
||||||
|
showFailure('O mural não terminou de carregar.', `${err.message} — tentar de novo carrega a versão leve.`);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user