import fs from 'fs'; import path from 'path'; import puppeteer from 'puppeteer'; /** * Gera todos os ativos de marca a partir da geometria do logo: * favicons (transparentes), ícones de app (tile obsidiana), site.webmanifest * e a imagem de compartilhamento (Open Graph, 1200x630). * * Cores = tokens de app/globals.css. Uso: npm run favicons * (usa o Chrome do Puppeteer; defina CHROME_PATH para usar outro binário) */ const COR = { obsidiana: "#07090d", tealEscuro: "#2dd4bf", cobreEscuro: "#ffb39b", tealMedio: "#1a8f83", // legível em aba clara e escura cobreMedio: "#c8694e", texto: "#f4f6f8", suave: "#a6afbd", }; const marca = (teal, cobre) => ` `; function createIco(pngBuffers) { // pngBuffers: array of { width, height, buffer } const count = pngBuffers.length; const headerSize = 6; const dirEntrySize = 16; let offset = headerSize + dirEntrySize * count; const header = Buffer.alloc(headerSize); header.writeUInt16LE(0, 0); // reserved header.writeUInt16LE(1, 2); // ICO type header.writeUInt16LE(count, 4); // count const entries = []; for (const img of pngBuffers) { const entry = Buffer.alloc(dirEntrySize); entry.writeUInt8(img.width >= 256 ? 0 : img.width, 0); entry.writeUInt8(img.height >= 256 ? 0 : img.height, 1); entry.writeUInt8(0, 2); // colors entry.writeUInt8(0, 3); // reserved entry.writeUInt16LE(1, 4); // planes entry.writeUInt16LE(32, 6); // bpp entry.writeUInt32LE(img.buffer.length, 8); // size entry.writeUInt32LE(offset, 12); // offset entries.push(entry); offset += img.buffer.length; } return Buffer.concat([header, ...entries, ...pngBuffers.map(b => b.buffer)]); } const pagina = (corpo, css = "") => ` ${corpo}`; async function run() { const browser = await puppeteer.launch({ executablePath: process.env.CHROME_PATH || undefined, args: ['--no-sandbox', '--disable-setuid-sandbox'] }); const page = await browser.newPage(); // 1. Favicons: marca sozinha, fundo transparente. O logo ocupa x 2..55; centraliza. const favicon = pagina(`
${marca(COR.tealMedio, COR.cobreMedio).replace('`); await page.setContent(favicon); const rendered = {}; for (const size of [16, 32, 48]) { await page.setViewport({ width: size, height: size, deviceScaleFactor: 1 }); rendered[size] = await page.screenshot({ type: 'png', omitBackground: true, clip: { x: 0, y: 0, width: size, height: size } }); } // 2. Ícones de app: tile obsidiana, marca com margem de segurança (maskable). const tile = pagina(`
${marca(COR.tealEscuro, COR.cobreEscuro).replace('
`); await page.setContent(tile); for (const size of [180, 192, 512]) { await page.setViewport({ width: size, height: size, deviceScaleFactor: 1 }); rendered[size] = await page.screenshot({ type: 'png', clip: { x: 0, y: 0, width: size, height: size } }); } // 3. Open Graph / Twitter: 1200x630. const og = pagina(`
${marca(COR.tealEscuro, COR.cobreEscuro).replace('

Documentação do ESP32
em português

Especificações, pinagem, comparação e seleção das 12 séries do ESP32.

SériesSeletorComparadorDiagnósticoLivro
`, ` body{background:${COR.obsidiana};font-family:'Manrope',system-ui,sans-serif;color:${COR.texto};position:relative} body::before{content:"";position:absolute;inset:0;background:radial-gradient(52rem 30rem at 50% -14%,rgba(45,212,191,.16),transparent 70%),radial-gradient(40rem 26rem at 100% 30%,rgba(56,140,248,.09),transparent 70%)} main{position:relative;height:100%;padding:76px 88px 0} .topo{display:flex;align-items:center;gap:20px} .logo{width:68px;height:68px} .nome{font:600 44px 'Space Grotesk',sans-serif;letter-spacing:-0.045em} h1{margin-top:64px;font:600 86px/1.03 'Space Grotesk',sans-serif;letter-spacing:-0.04em} p{margin-top:30px;max-width:900px;font-size:30px;line-height:1.45;color:${COR.suave}} .chips{position:absolute;left:88px;bottom:62px;display:flex;gap:12px} .chips span{border:1.5px solid #2e3846;border-radius:999px;padding:9px 22px;font-size:22px;font-weight:500;color:${COR.texto}} .barra{position:absolute;left:0;right:0;bottom:0;height:10px;background:${COR.tealEscuro}}`); await page.setViewport({ width: 1200, height: 630, deviceScaleFactor: 1 }); await page.setContent(og, { waitUntil: 'networkidle0' }); await page.evaluate(() => document.fonts.ready); const ogPng = await page.screenshot({ type: 'png', clip: { x: 0, y: 0, width: 1200, height: 630 } }); await browser.close(); const icoBuffer = createIco([ { width: 16, height: 16, buffer: rendered[16] }, { width: 32, height: 32, buffer: rendered[32] }, { width: 48, height: 48, buffer: rendered[48] } ]); fs.writeFileSync('public/favicon.ico', icoBuffer); fs.writeFileSync('public/favicon-48x48.png', rendered[48]); fs.writeFileSync('public/favicon-32x32.png', rendered[32]); fs.writeFileSync('public/favicon-16x16.png', rendered[16]); fs.writeFileSync('public/apple-touch-icon.png', rendered[180]); fs.writeFileSync('public/icon-192.png', rendered[192]); fs.writeFileSync('public/icon-512.png', rendered[512]); fs.writeFileSync('app/opengraph-image.png', ogPng); console.log('Favicons, ícones de app e imagem Open Graph gerados.'); const manifest = { name: "ESPDocs", short_name: "ESPDocs", description: "Documentação do ecossistema ESP32 em português brasileiro", lang: "pt-BR", start_url: "/", display: "standalone", background_color: COR.obsidiana, theme_color: COR.obsidiana, icons: [ { src: "/favicon-48x48.png", sizes: "48x48", type: "image/png" }, { src: "/icon-192.png", sizes: "192x192", type: "image/png", purpose: "any maskable" }, { src: "/icon-512.png", sizes: "512x512", type: "image/png", purpose: "any maskable" } ] }; fs.writeFileSync('public/site.webmanifest', JSON.stringify(manifest, null, 2) + '\n'); console.log('site.webmanifest gerado.'); } run().catch(err => { console.error(err); process.exit(1); });