Files
rodado 290c4cc21d feat: acervo Brasiliana Fotográfica coletado e mural 3D no ar
Captura integral do acervo da Brasiliana Fotográfica (Biblioteca Nacional)
para s3://brasiliana: 13.060 itens, 12.985 com imagem de preservação, 13.059
com miniatura derivada, 2.137 cópias legadas e 636 posts do blog. 41.444
objetos no ledger.

Os 75 itens sem preservação estão em errors.md: a página do item responde
200, mas a origem serve o bitstream com HTTP 500. Nenhum fica fora do mural,
porque todos têm thumbnail.

Não versionado, por viver no bucket ou ser reconstruível: catálogo,
posts .jsonl, atlas .webp, sqlite de estado e logs. Credenciais ficam de
fora pelo .gitignore.
2026-09-13 20:17:02 +02:00

334 lines
13 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Brasiliana — Linha do Tempo</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body { width: 100%; height: 100%; background: #000; color: #e8e8e0; font-family: system-ui, -apple-system, sans-serif; }
body { overflow: hidden; }
#title-bar {
position: fixed; top: 0; left: 0; right: 0; height: 40px; z-index: 30;
display: flex; align-items: center; justify-content: space-between;
padding: 0 16px; background: #000; border-bottom: 1px solid #222;
font-size: .82rem; letter-spacing: .04em;
}
#title-bar .side { color: #888; font-variant-numeric: tabular-nums; }
#title-bar .mid { font-weight: 600; letter-spacing: .12em; text-transform: uppercase; color: #ddd; }
#scroll {
position: fixed; top: 40px; left: 0; right: 0; bottom: 0;
overflow: auto;
}
#grid { position: relative; }
.row-label {
position: sticky; left: 0; z-index: 5;
background: #000;
display: flex; align-items: center;
padding: 0 10px;
font-size: .68rem; letter-spacing: .03em; color: #ddd;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
border-right: 1px solid #1c1c1c;
}
.row-label.dim { color: #777; font-style: italic; }
.row-chart { position: relative; border-bottom: 1px solid #141414; }
.row-chart:hover { background: rgba(255,255,255,.02); }
.lifeline { position: absolute; height: 1px; background: #555; top: 50%; }
.dot { position: absolute; width: 5px; height: 5px; border-radius: 50%; background: #888; top: 50%; transform: translate(-50%, -50%); }
.thumb {
position: absolute; top: 50%; transform: translateY(-50%);
cursor: pointer; border: 1px solid rgba(0,0,0,.6);
box-shadow: 0 0 0 1px rgba(255,255,255,.08);
transition: transform .12s ease, box-shadow .12s ease, z-index 0s;
}
.thumb:hover {
transform: translateY(-50%) scale(2.4);
z-index: 6;
box-shadow: 0 4px 18px rgba(0,0,0,.7), 0 0 0 1px rgba(255,255,255,.4);
}
/* axis header */
.axis-corner {
position: sticky; top: 0; left: 0; z-index: 12;
background: #000; border-right: 1px solid #1c1c1c; border-bottom: 1px solid #222;
display: flex; align-items: flex-end; padding: 0 10px 6px;
font-size: .62rem; letter-spacing: .08em; color: #666; text-transform: uppercase;
}
.axis-row {
position: sticky; top: 0; z-index: 8;
background: #000; border-bottom: 1px solid #222;
position: relative;
}
.tick { position: absolute; bottom: 0; width: 1px; background: #333; }
.tick.major { background: #555; }
.tick-label { position: absolute; bottom: 18px; transform: translateX(-50%); font-size: .62rem; color: #777; font-variant-numeric: tabular-nums; }
#loading {
position: fixed; inset: 0; background: #000; z-index: 100;
display: flex; align-items: center; justify-content: center;
font-size: .78rem; letter-spacing: .12em; text-transform: uppercase; color: #888;
transition: opacity .5s ease;
}
/* ── metadata card (same language as wall.html) ─────────────────────── */
#card-wrap {
position: fixed; inset: 0; z-index: 50;
display: flex; align-items: center; justify-content: center;
opacity: 0; pointer-events: none; transition: opacity .3s ease;
background: rgba(0,0,0,.55);
}
#card-wrap.open { opacity: 1; pointer-events: auto; }
#card {
position: relative; width: min(420px, 90vw); max-height: 86vh; overflow-y: auto;
background: rgba(10,10,10,.92); backdrop-filter: blur(6px); border-radius: 6px;
padding: 34px 30px 26px; text-align: center; box-shadow: 0 30px 80px rgba(0,0,0,.7);
font-family: Georgia, 'Times New Roman', serif;
transform: translateY(10px) scale(.97); transition: transform .3s ease;
}
#card-wrap.open #card { transform: translateY(0) scale(1); }
#card-back {
position: absolute; left: 14px; top: 14px; width: 30px; height: 30px; border-radius: 50%;
background: rgba(255,255,255,.08); border: 1px solid rgba(255,255,255,.18); color: #ddd;
font-size: 1rem; line-height: 28px; text-align: center; cursor: pointer; font-family: system-ui, sans-serif;
}
#card-back:hover { background: rgba(255,255,255,.18); }
#card-type { font-size: 1.5rem; font-weight: 400; color: #f2f2f2; margin-bottom: 6px; }
#card-seealso { font-size: .62rem; letter-spacing: .12em; text-transform: uppercase; color: #7fa8c9; margin-bottom: 18px; }
#card-img-wrap { margin: 0 auto 18px; max-width: 260px; }
#card-img { display: block; width: 100%; height: auto; border-radius: 2px; box-shadow: 0 12px 40px rgba(0,0,0,.55); }
#card-title { font-size: 1.02rem; color: #f5f5f0; }
#card-date { font-size: .85rem; color: #b9b9b0; margin-top: 2px; }
#card-contrib { font-size: .82rem; color: #cdd8e0; margin-top: 10px; }
#card-footer { margin-top: 16px; padding-top: 14px; border-top: 1px solid rgba(255,255,255,.12); font-family: system-ui, sans-serif; font-size: .68rem; letter-spacing: .04em; color: #888; }
#card-footer a { color: #d8b04a; text-decoration: none; }
#card-footer a:hover { text-decoration: underline; }
#card-rights { margin-top: 6px; font-size: .68rem; color: #777; }
</style>
</head>
<body>
<div id="loading">carregando linha do tempo…</div>
<div id="title-bar">
<span class="side" id="year-left">—</span>
<span class="mid">Autoria — Brasiliana Fotográfica</span>
<span class="side" id="year-right">—</span>
</div>
<div id="scroll"><div id="grid"></div></div>
<div id="card-wrap">
<div id="card">
<div id="card-back">←</div>
<div id="card-type">—</div>
<div id="card-seealso"><span id="card-community">—</span> · <span id="card-date-tag">—</span></div>
<div id="card-img-wrap"><img id="card-img" alt=""></div>
<div id="card-title">—</div>
<div id="card-date">—</div>
<div id="card-contrib"></div>
<div id="card-footer">
<div><a id="card-link" href="#" target="_blank" rel="noopener">ver item original ↗</a> · <span id="card-handle"></span></div>
<div id="card-rights"></div>
</div>
</div>
</div>
<script>
/*
* wall2.html — "Gestalter"-style timeline, uma linha por autor/fotógrafo.
* Baseado num diagrama clássico de história do design gráfico (Gestalter,
* 1925-2011): uma linha horizontal por pessoa cobrindo seu período de
* atividade conhecido, com miniaturas de cada obra plotadas no ano exato.
*
* Aqui a "pessoa" é o DC.contributor de cada item (o fotógrafo/autor, ou
* a instituição quando é essa a autoria registrada), e a "obra" é a foto.
* Sem data de nascimento/morte real: a linha vai do ano mais antigo ao
* mais recente entre as fotos DATADAS desse autor no catálogo — é um
* proxy honesto do período ativo, não uma biografia.
*
* Reaproveita o MESMO atlas de wall.html (atlas/atlas-map.json +
* atlas-0.webp) — nenhum build novo é necessário.
*/
const PX_PER_YEAR = 16;
const ROW_H = 32;
const THUMB = 20;
const LABEL_W = 190;
const AXIS_H = 34;
function yearOf(dateStr) {
if (!dateStr) return null;
const m = String(dateStr).match(/(1[5-9]\d{2}|20\d{2})/);
return m ? parseInt(m[1], 10) : null;
}
let atlasEntries = null, TPR = 1, atlasImg = null;
const bgTotal = {}; // computed per-thumb background-size string, cached
function thumbEl(entry, xPx) {
const el = document.createElement('div');
el.className = 'thumb';
el.style.left = `${xPx}px`;
el.style.width = `${THUMB}px`;
el.style.height = `${THUMB}px`;
el.style.backgroundImage = `url(atlas/atlas-0.webp)`;
el.style.backgroundSize = `${TPR * THUMB}px ${TPR * THUMB}px`;
el.style.backgroundPosition = `-${entry.col * THUMB}px -${entry.row * THUMB}px`;
el.addEventListener('click', () => showCard(entry));
return el;
}
function showCard(entry) {
const meta = entry.meta;
document.getElementById('card-type').textContent = meta.type || 'Fotografia';
document.getElementById('card-community').textContent = meta.community || '—';
document.getElementById('card-date-tag').textContent = meta.date || 's.d.';
document.getElementById('card-title').textContent = meta.title || '(sem título)';
document.getElementById('card-date').textContent = meta.date || '';
document.getElementById('card-contrib').textContent = meta.contributor || '';
document.getElementById('card-handle').textContent = meta.handle || '';
document.getElementById('card-link').href = meta.source_url || '#';
document.getElementById('card-rights').textContent = meta.rights ? `Direitos: ${meta.rights}` : '';
const img = document.getElementById('card-img');
if (atlasImg) {
const S = atlasImg.naturalWidth / TPR;
const c = document.createElement('canvas');
c.width = c.height = 512;
const ctx = c.getContext('2d');
ctx.drawImage(atlasImg, entry.col * S, entry.row * S, S, S, 0, 0, 512, 512);
img.src = c.toDataURL('image/jpeg', 0.92);
}
document.getElementById('card-wrap').classList.add('open');
}
function closeCard() { document.getElementById('card-wrap').classList.remove('open'); }
document.getElementById('card-back').addEventListener('click', closeCard);
document.getElementById('card-wrap').addEventListener('click', e => { if (e.target.id === 'card-wrap') closeCard(); });
window.addEventListener('keydown', e => { if (e.key === 'Escape') closeCard(); });
async function main() {
const mapData = await fetch('atlas/atlas-map.json').then(r => r.json());
TPR = mapData.size / mapData.tile;
atlasEntries = mapData.map;
// ── group items by author, keep only ones with a parseable year ──────────
const byAuthor = new Map();
let globalMin = Infinity, globalMax = -Infinity;
for (const [handle, entry] of Object.entries(mapData.map)) {
const meta = entry.meta;
const y = yearOf(meta.date);
const author = meta.contributor;
if (!author || y == null) continue;
if (!byAuthor.has(author)) byAuthor.set(author, []);
byAuthor.get(author).push({ ...entry, year: y });
if (y < globalMin) globalMin = y;
if (y > globalMax) globalMax = y;
}
const authors = [...byAuthor.keys()].sort((a, b) => a.localeCompare(b, 'pt-BR'));
authors.forEach(a => byAuthor.get(a).sort((x, y) => x.year - y.year));
const axisMin = Math.floor(globalMin / 5) * 5 - 5;
const axisMax = Math.ceil(globalMax / 5) * 5 + 5;
const totalWidth = (axisMax - axisMin) * PX_PER_YEAR;
const yearToX = y => (y - axisMin) * PX_PER_YEAR;
document.getElementById('year-left').textContent = globalMin;
document.getElementById('year-right').textContent = globalMax;
// ── build the grid: corner, axis row, then one label+chart row per author ──
const grid = document.getElementById('grid');
grid.style.display = 'grid';
grid.style.gridTemplateColumns = `${LABEL_W}px ${totalWidth}px`;
grid.style.gridAutoRows = 'auto';
const corner = document.createElement('div');
corner.className = 'axis-corner';
corner.style.height = `${AXIS_H}px`;
corner.textContent = `${authors.length} autores`;
grid.appendChild(corner);
const axisRow = document.createElement('div');
axisRow.className = 'axis-row';
axisRow.style.height = `${AXIS_H}px`;
axisRow.style.width = `${totalWidth}px`;
for (let y = axisMin; y <= axisMax; y++) {
const major = y % 5 === 0;
const tick = document.createElement('div');
tick.className = 'tick' + (major ? ' major' : '');
tick.style.left = `${yearToX(y)}px`;
tick.style.height = major ? '10px' : '5px';
axisRow.appendChild(tick);
if (major) {
const lbl = document.createElement('div');
lbl.className = 'tick-label';
lbl.style.left = `${yearToX(y)}px`;
lbl.textContent = y;
axisRow.appendChild(lbl);
}
}
grid.appendChild(axisRow);
for (const author of authors) {
const items = byAuthor.get(author);
const minY = items[0].year, maxY = items[items.length - 1].year;
const label = document.createElement('div');
label.className = 'row-label' + (/^(Anônimo|Autoria não identificada|s\/autor)$/i.test(author) ? ' dim' : '');
label.style.height = `${ROW_H}px`;
label.textContent = author;
label.title = `${author} · ${items.length} obra(s) · ${minY}–${maxY}`;
grid.appendChild(label);
const chart = document.createElement('div');
chart.className = 'row-chart';
chart.style.height = `${ROW_H}px`;
chart.style.width = `${totalWidth}px`;
const x0 = yearToX(minY), x1 = yearToX(maxY);
const line = document.createElement('div');
line.className = 'lifeline';
line.style.left = `${x0}px`;
line.style.width = `${Math.max(1, x1 - x0)}px`;
chart.appendChild(line);
[x0, x1].forEach(x => {
const d = document.createElement('div');
d.className = 'dot';
d.style.left = `${x}px`;
chart.appendChild(d);
});
// spread out thumbnails that would otherwise overlap at the same year
let lastX = -Infinity;
for (const it of items) {
let x = yearToX(it.year) - THUMB / 2;
if (x < lastX + THUMB * 0.9) x = lastX + THUMB * 0.9;
lastX = x;
chart.appendChild(thumbEl(it, x));
}
grid.appendChild(chart);
}
atlasImg = new Image();
atlasImg.src = 'atlas/atlas-0.webp';
await atlasImg.decode().catch(() => {});
document.getElementById('loading').style.opacity = '0';
setTimeout(() => document.getElementById('loading').remove(), 500);
}
main().catch(err => {
document.getElementById('loading').textContent = `erro: ${err.message}`;
console.error(err);
});
</script>
</body>
</html>