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.
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -0,0 +1,289 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
/*
|
||||
* build-atlas.js — mural 3D de miniaturas do acervo.
|
||||
*
|
||||
* Baseado em ../tocador/script/build-3d-atlas.js (mesma ideia: empacotar
|
||||
* miniaturas num atlas WebP único + um atlas-map.json com metadados por
|
||||
* peça, pra wall.html carregar 2 arquivos em vez de milhares).
|
||||
*
|
||||
* FONTE DAS IMAGENS: o site de origem (brasilianafotografica.bn.gov.br)
|
||||
* bloqueia requisições automatizadas (Cloudflare — ver README.md,
|
||||
* achados 3-5), então este script não busca nada pela rede. Em vez
|
||||
* disso ele lê o que a Trilha B (baixador-navegador.js) já trouxe pra
|
||||
* ~/Downloads — arquivos "BRAS__<handle>__<nome-original>.{jpg,JPG,png}",
|
||||
* usando o THUMB__thumb_2 quando existe (mais leve) e a imagem de
|
||||
* preservação/extra como alternativa. Itens sem nenhum arquivo baixado
|
||||
* ainda ficam de fora do mural — rode de novo depois de baixar mais.
|
||||
*
|
||||
* Uso:
|
||||
* node web/build-atlas.js [--n=99999] [--out=atlas] [--thumbs=DIR]
|
||||
* node web/build-atlas.js [--n=99999] [--out=atlas] [--downloads=~/Downloads]
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const sharp = require('sharp');
|
||||
const readline = require('readline');
|
||||
|
||||
const args = Object.fromEntries(
|
||||
process.argv.slice(2).map(a => {
|
||||
const m = a.match(/^--([\w-]+)=(.*)$/);
|
||||
return m ? [m[1], m[2]] : [a.replace(/^--/, ''), true];
|
||||
})
|
||||
);
|
||||
|
||||
const N_ITEMS = parseInt(args.n || '99999', 10); // effectively "all we have" unless capped
|
||||
const OUT_DIR = path.resolve(__dirname, args.out || 'atlas'); // web/atlas, sibling of this script
|
||||
const DOWNLOADS_DIR = path.resolve(args.downloads || path.join(os.homedir(), 'Downloads'));
|
||||
// --thumbs=DIR: pasta de miniaturas ja derivadas, uma por item, nomeadas
|
||||
// <handle-com-underscore>.webp (geradas por vips/magick a partir da imagem
|
||||
// de preservacao). E a fonte preferida: o subidor apaga de ~/Downloads assim
|
||||
// que sobe pro S3, entao Downloads nunca tem o acervo inteiro ao mesmo tempo.
|
||||
const THUMBS_DIR = args.thumbs ? path.resolve(args.thumbs) : null;
|
||||
const TILE_SIZE = 128;
|
||||
const ATLAS_SIZE = 8192; // dentro do limite de textura de qualquer GPU moderna (max comum 8192-16384)
|
||||
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 CATALOGO = path.resolve(__dirname, '..', 'pipeline', 'catalogo.jsonl');
|
||||
|
||||
// ── acha os arquivos já baixados pela Trilha B em ~/Downloads ────────────────
|
||||
// nomes: BRAS__20.500.12156.1_<id>__THUMB__thumb_2.jpg (preferido, leve)
|
||||
// ou BRAS__20.500.12156.1_<id>__<nome-original>.jpg (imagem cheia, alternativa)
|
||||
|
||||
function scanThumbs(dir) {
|
||||
const byHandle = new Map();
|
||||
let files;
|
||||
try {
|
||||
files = fs.readdirSync(dir);
|
||||
} catch {
|
||||
return byHandle;
|
||||
}
|
||||
for (const name of files) {
|
||||
const m = name.match(/^(20\.500\.12156\.1_\d+)\.(webp|jpg|png)$/i);
|
||||
if (!m) continue;
|
||||
const handle = m[1].replace('_', '/');
|
||||
byHandle.set(handle, { path: path.join(dir, name), isThumb: true });
|
||||
}
|
||||
return byHandle;
|
||||
}
|
||||
|
||||
function scanDownloads() {
|
||||
const re = /^BRAS__(20\.500\.12156\.1_\d+)__(.*)$/;
|
||||
const byHandle = new Map(); // handle (com "/") -> { path, isThumb }
|
||||
let files;
|
||||
try {
|
||||
files = fs.readdirSync(DOWNLOADS_DIR);
|
||||
} catch {
|
||||
return byHandle;
|
||||
}
|
||||
for (const name of files) {
|
||||
const m = name.match(re);
|
||||
if (!m) continue;
|
||||
const handle = m[1].replace('_', '/'); // "20.500.12156.1_10518" -> "20.500.12156.1/10518"
|
||||
const isThumb = /__THUMB__/.test(name);
|
||||
const existing = byHandle.get(handle);
|
||||
if (!existing || (isThumb && !existing.isThumb)) {
|
||||
byHandle.set(handle, { path: path.join(DOWNLOADS_DIR, name), isThumb });
|
||||
}
|
||||
}
|
||||
return byHandle;
|
||||
}
|
||||
|
||||
// ── util: cor média de uma faixa de pixels (mesma lógica do tocador) ──────────
|
||||
|
||||
function stripAvg(d, S, x0, y0, x1, y1) {
|
||||
let r = 0, g = 0, b = 0, n = 0;
|
||||
for (let y = y0; y <= y1; y++) {
|
||||
for (let x = x0; x <= x1; x++) {
|
||||
const i = (y * S + x) * 3;
|
||||
r += d[i]; g += d[i + 1]; b += d[i + 2]; n++;
|
||||
}
|
||||
}
|
||||
return (Math.round(r / n) << 16) | (Math.round(g / n) << 8) | Math.round(b / n);
|
||||
}
|
||||
|
||||
function computeEdgeColors(rgb, S) {
|
||||
const t = 2;
|
||||
const right = stripAvg(rgb, S, S - t, 0, S - 1, S - 1);
|
||||
const left = stripAvg(rgb, S, 0, 0, t - 1, S - 1);
|
||||
const top = stripAvg(rgb, S, 0, 0, S - 1, t - 1);
|
||||
const bottom = stripAvg(rgb, S, 0, S - t, S - 1, S - 1);
|
||||
const avg = stripAvg(rgb, S, 0, 0, S - 1, S - 1);
|
||||
const dk = h => ((((h >> 16) & 0xff) >> 1) << 16) | ((((h >> 8) & 0xff) >> 1) << 8) | ((h & 0xff) >> 1);
|
||||
return [right, left, top, bottom, dk(avg)];
|
||||
}
|
||||
|
||||
// ── metadados: pega campos úteis do dublin_core pro card ──────────────────────
|
||||
|
||||
function pickDC(dublinCore, name) {
|
||||
const hit = (dublinCore || []).find(e => e.name === name);
|
||||
return hit ? hit.value : null;
|
||||
}
|
||||
|
||||
// A record carries its subjects (and descriptions, and places) as *repeated*
|
||||
// dc.subject rows in full_table — six of them on average, 78.835 in all — and
|
||||
// pickDC would take only the first. The descriptive half of the catalogue lives
|
||||
// here: without it "indigenista" finds nothing even though five items are
|
||||
// classified "Política indigenista". Undefined keys vanish from the JSON, so an
|
||||
// item only pays for the fields it actually has.
|
||||
function pickAll(item, field, { noUrls = false } = {}) {
|
||||
const out = [];
|
||||
for (const row of item.full_table || []) {
|
||||
if (row.field !== field) continue;
|
||||
const v = String(row.value ?? '').trim();
|
||||
if (!v || out.includes(v)) continue;
|
||||
// dc.identifier and dc.relation are mostly bare URLs — a megabyte of text
|
||||
// nobody will ever search for. What is worth keeping is hiding underneath:
|
||||
// the archival codes (013RJ012036) and the collection names (Gilberto Ferrez).
|
||||
if (noUrls && /^https?:\/\//i.test(v)) continue;
|
||||
out.push(v);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
const some = (item, f, o) => { const v = pickAll(item, f, o); return v.length ? v : undefined; };
|
||||
const one = (item, f) => pickAll(item, f)[0] || undefined;
|
||||
|
||||
function summarize(item) {
|
||||
const dc = item.dublin_core || [];
|
||||
return {
|
||||
handle: item.handle,
|
||||
title: item.title,
|
||||
community: item.community,
|
||||
rights: item.rights,
|
||||
date: pickDC(dc, 'DC.date'),
|
||||
contributor: pickDC(dc, 'DC.contributor'),
|
||||
type: pickDC(dc, 'DC.type'),
|
||||
// sem source_url: é o mesmo endereço para todos os itens, com o handle no
|
||||
// meio — 1,2 MB de prefixo repetido 12.552 vezes que o mural remonta numa
|
||||
// linha. O handle acima é a chave do mapa e sai de meta lá embaixo.
|
||||
// what free-text search is actually for. dc.language ("por") and dc.format
|
||||
// ("JPG") are left out on purpose: technical, never searched, and "por"
|
||||
// alone would match a couple of thousand items by accident.
|
||||
subject: some(item, 'dc.subject'),
|
||||
description: some(item, 'dc.description'),
|
||||
coverage: some(item, 'dc.coverage'),
|
||||
source: one(item, 'dc.source'),
|
||||
publisher: one(item, 'dc.publisher'),
|
||||
relation: some(item, 'dc.relation', { noUrls: true }),
|
||||
identifier: some(item, 'dc.identifier', { noUrls: true }),
|
||||
};
|
||||
}
|
||||
|
||||
// ── carrega do catalogo.jsonl só os itens que têm arquivo baixado (streaming) ──
|
||||
|
||||
async function loadItems(n, byHandle) {
|
||||
const items = [];
|
||||
const rl = readline.createInterface({ input: fs.createReadStream(CATALOGO), crlfDelay: Infinity });
|
||||
for await (const line of rl) {
|
||||
if (!line.trim()) continue;
|
||||
const o = JSON.parse(line);
|
||||
// um item sem título ainda é uma peça: o card já cai pra "(sem título)"
|
||||
// sozinho, e descartá-lo aqui escondia do mural itens que têm imagem
|
||||
// (11749 do Museu Aeroespacial e 6241 do Instituto Moreira Salles)
|
||||
const file = byHandle.get(o.handle);
|
||||
if (!file) continue; // sem imagem baixada ainda — fora do mural por enquanto
|
||||
items.push({ ...summarize(o), _file: file.path });
|
||||
if (items.length >= n) { rl.close(); break; }
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
// ── lê o arquivo real e encaixa no tile ────────────────────────────────────────
|
||||
|
||||
async function realTile(filePath) {
|
||||
return sharp(filePath)
|
||||
.resize(TILE_SIZE, TILE_SIZE, { fit: 'cover' })
|
||||
.flatten({ background: '#000' })
|
||||
.raw()
|
||||
.toBuffer();
|
||||
}
|
||||
|
||||
// ── main ────────────────────────────────────────────────────────────────────
|
||||
|
||||
async function main() {
|
||||
fs.mkdirSync(OUT_DIR, { recursive: true });
|
||||
|
||||
let byHandle;
|
||||
if (THUMBS_DIR) {
|
||||
console.log(`Procurando miniaturas em ${THUMBS_DIR}...`);
|
||||
byHandle = scanThumbs(THUMBS_DIR);
|
||||
console.log(`${byHandle.size} peças com miniatura derivada.`);
|
||||
if (byHandle.size === 0) {
|
||||
console.error('Nenhum arquivo "<handle>.webp" encontrado em ' + THUMBS_DIR + '.');
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
console.log(`Procurando downloads em ${DOWNLOADS_DIR}...`);
|
||||
byHandle = scanDownloads();
|
||||
console.log(`${byHandle.size} peças com arquivo já baixado (Trilha B).`);
|
||||
if (byHandle.size === 0) {
|
||||
console.error('Nenhum arquivo "BRAS__<handle>__..." encontrado em ' + DOWNLOADS_DIR + '. Rode baixador-navegador.js primeiro.');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Lendo ${CATALOGO}...`);
|
||||
const items = await loadItems(N_ITEMS, byHandle);
|
||||
console.log(`${items.length} itens combinados (catálogo ∩ downloads).`);
|
||||
|
||||
const nAtlases = Math.max(1, Math.ceil(items.length / TILES_PER_ATLAS));
|
||||
console.log(`${nAtlases} atlas(es) de ${ATLAS_SIZE}px (até ${TILES_PER_ATLAS} peças cada, tile=${TILE_SIZE}px).`);
|
||||
|
||||
const map = {};
|
||||
const atlasFiles = [];
|
||||
let failed = 0;
|
||||
|
||||
for (let a = 0; a < nAtlases; a++) {
|
||||
const pageItems = items.slice(a * TILES_PER_ATLAS, (a + 1) * TILES_PER_ATLAS);
|
||||
const buf = Buffer.alloc(ATLAS_SIZE * ATLAS_SIZE * 3, 0);
|
||||
|
||||
for (let i = 0; i < pageItems.length; i++) {
|
||||
const col = i % TILES_PER_ROW;
|
||||
const row = Math.floor(i / TILES_PER_ROW);
|
||||
let rgb;
|
||||
try {
|
||||
rgb = await realTile(pageItems[i]._file);
|
||||
} catch (e) {
|
||||
failed++;
|
||||
continue;
|
||||
}
|
||||
for (let ty = 0; ty < TILE_SIZE; ty++) {
|
||||
const src = ty * TILE_SIZE * 3;
|
||||
const dst = ((row * TILE_SIZE + ty) * ATLAS_SIZE + col * TILE_SIZE) * 3;
|
||||
rgb.copy(buf, dst, src, src + TILE_SIZE * 3);
|
||||
}
|
||||
const [cr, cl, ct, cb, ck] = computeEdgeColors(rgb, TILE_SIZE);
|
||||
// handle vira a chave da entrada; repeti-lo dentro de meta custava 0,4 MB
|
||||
const { _file, handle, ...meta } = pageItems[i];
|
||||
map[handle] = { atlas: a, col, row, cr, cl, ct, cb, ck, meta };
|
||||
const n = a * TILES_PER_ATLAS + i + 1;
|
||||
if (n % 500 === 0) console.log(` ${n}/${items.length}`);
|
||||
}
|
||||
|
||||
const name = `atlas-${a}.webp`;
|
||||
const atlasPath = path.join(OUT_DIR, name);
|
||||
await sharp(buf, { raw: { width: ATLAS_SIZE, height: ATLAS_SIZE, channels: 3 } })
|
||||
.webp({ quality: 85, effort: 4 })
|
||||
.toFile(atlasPath);
|
||||
const mb = (fs.statSync(atlasPath).size / 1024 / 1024).toFixed(2);
|
||||
console.log(`Salvo ${name} (${mb} MB, ${pageItems.length} peças)`);
|
||||
atlasFiles.push(name);
|
||||
}
|
||||
if (failed) console.log(`${failed} arquivo(s) falharam ao decodificar e ficaram de fora.`);
|
||||
|
||||
const mapData = {
|
||||
tile: TILE_SIZE,
|
||||
size: ATLAS_SIZE,
|
||||
atlases: atlasFiles,
|
||||
placeholder: false, // fotos reais baixadas pela Trilha B, não mais o placeholder repetido
|
||||
map,
|
||||
};
|
||||
const mapPath = path.join(OUT_DIR, 'atlas-map.json');
|
||||
fs.writeFileSync(mapPath, JSON.stringify(mapData));
|
||||
console.log(`Salvo atlas-map.json (${(fs.statSync(mapPath).size / 1024).toFixed(0)} KB)`);
|
||||
}
|
||||
|
||||
main().catch(e => { console.error(e); process.exit(1); });
|
||||
Generated
+647
@@ -0,0 +1,647 @@
|
||||
{
|
||||
"name": "web",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"sharp": "^0.35.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.11.3",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz",
|
||||
"integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/colour": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
|
||||
"integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-darwin-arm64": {
|
||||
"version": "0.35.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.4.tgz",
|
||||
"integrity": "sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-darwin-arm64": "1.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-darwin-x64": {
|
||||
"version": "0.35.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.4.tgz",
|
||||
"integrity": "sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-darwin-x64": "1.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-freebsd-wasm32": {
|
||||
"version": "0.35.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.4.tgz",
|
||||
"integrity": "sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"dependencies": {
|
||||
"@img/sharp-wasm32": "0.35.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-darwin-arm64": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.3.tgz",
|
||||
"integrity": "sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-darwin-x64": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.3.tgz",
|
||||
"integrity": "sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-arm": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.3.tgz",
|
||||
"integrity": "sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-arm64": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.3.tgz",
|
||||
"integrity": "sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-ppc64": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.3.tgz",
|
||||
"integrity": "sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-riscv64": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.3.tgz",
|
||||
"integrity": "sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-s390x": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.3.tgz",
|
||||
"integrity": "sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-x64": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.3.tgz",
|
||||
"integrity": "sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.3.tgz",
|
||||
"integrity": "sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.3.tgz",
|
||||
"integrity": "sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-arm": {
|
||||
"version": "0.35.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.4.tgz",
|
||||
"integrity": "sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-arm": "1.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-arm64": {
|
||||
"version": "0.35.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.4.tgz",
|
||||
"integrity": "sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-arm64": "1.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-ppc64": {
|
||||
"version": "0.35.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.4.tgz",
|
||||
"integrity": "sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-ppc64": "1.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-riscv64": {
|
||||
"version": "0.35.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.4.tgz",
|
||||
"integrity": "sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-riscv64": "1.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-s390x": {
|
||||
"version": "0.35.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.4.tgz",
|
||||
"integrity": "sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-s390x": "1.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-x64": {
|
||||
"version": "0.35.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.4.tgz",
|
||||
"integrity": "sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-x64": "1.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linuxmusl-arm64": {
|
||||
"version": "0.35.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.4.tgz",
|
||||
"integrity": "sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linuxmusl-x64": {
|
||||
"version": "0.35.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.4.tgz",
|
||||
"integrity": "sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-wasm32": {
|
||||
"version": "0.35.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.4.tgz",
|
||||
"integrity": "sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==",
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/runtime": "^1.11.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-webcontainers-wasm32": {
|
||||
"version": "0.35.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.4.tgz",
|
||||
"integrity": "sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@img/sharp-wasm32": "0.35.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-arm64": {
|
||||
"version": "0.35.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.4.tgz",
|
||||
"integrity": "sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-ia32": {
|
||||
"version": "0.35.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.4.tgz",
|
||||
"integrity": "sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-x64": {
|
||||
"version": "0.35.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.4.tgz",
|
||||
"integrity": "sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.8.5",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
|
||||
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/sharp": {
|
||||
"version": "0.35.4",
|
||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.4.tgz",
|
||||
"integrity": "sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@img/colour": "^1.1.0",
|
||||
"detect-libc": "^2.1.2",
|
||||
"semver": "^7.8.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-darwin-arm64": "0.35.4",
|
||||
"@img/sharp-darwin-x64": "0.35.4",
|
||||
"@img/sharp-freebsd-wasm32": "0.35.4",
|
||||
"@img/sharp-libvips-darwin-arm64": "1.3.3",
|
||||
"@img/sharp-libvips-darwin-x64": "1.3.3",
|
||||
"@img/sharp-libvips-linux-arm": "1.3.3",
|
||||
"@img/sharp-libvips-linux-arm64": "1.3.3",
|
||||
"@img/sharp-libvips-linux-ppc64": "1.3.3",
|
||||
"@img/sharp-libvips-linux-riscv64": "1.3.3",
|
||||
"@img/sharp-libvips-linux-s390x": "1.3.3",
|
||||
"@img/sharp-libvips-linux-x64": "1.3.3",
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.3.3",
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.3.3",
|
||||
"@img/sharp-linux-arm": "0.35.4",
|
||||
"@img/sharp-linux-arm64": "0.35.4",
|
||||
"@img/sharp-linux-ppc64": "0.35.4",
|
||||
"@img/sharp-linux-riscv64": "0.35.4",
|
||||
"@img/sharp-linux-s390x": "0.35.4",
|
||||
"@img/sharp-linux-x64": "0.35.4",
|
||||
"@img/sharp-linuxmusl-arm64": "0.35.4",
|
||||
"@img/sharp-linuxmusl-x64": "0.35.4",
|
||||
"@img/sharp-webcontainers-wasm32": "0.35.4",
|
||||
"@img/sharp-win32-arm64": "0.35.4",
|
||||
"@img/sharp-win32-ia32": "0.35.4",
|
||||
"@img/sharp-win32-x64": "0.35.4"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD",
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"sharp": "^0.35.4"
|
||||
}
|
||||
}
|
||||
+1782
File diff suppressed because it is too large
Load Diff
+333
@@ -0,0 +1,333 @@
|
||||
<!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>
|
||||
Reference in New Issue
Block a user